google-site-verification: google3bd66dd162ef54c7.html
FC2ブログ

ArduinoのRAMの状態(ヒープとスタックのアドレス)を調べるプログラム

まえがき
Arduino で複雑なプログラムを書いていると、メモリー不足で動かなくなることがあります。ヒープとスッタックの領域が衝突していることが原因になっていることが多いのですが、こういう問題があるコードでも、コンパイラからエラーや警告が出ない場合があるのでちょっと困ります。

もう少し具体的に示すと
これはあるプログラムを書き込んだ時の Arduino IDE の表示です。
書き込み完了表示
二行目の最後に「ローカル変数で1279バイト使うことが出来ます」と表示されています。つまり、メモリーは半分以上残っているので、まだまだ余裕がありそうに見えます。でも実際には、これはかなり危ない状態でした。

ここで表示されている、ローカル変数と言うのが曲者です。自分が書いた関数の中で使っている変数はもちろんここに入りますが、それ以外にライブラリで使われている変数も入ります。ライブラリで使われているローカル変数のサイズなんて、普通は知らないですから困っちゃいます。これ以外に、後で説明しますが割込みなどで使われているスタックもここに含まれるので、話はもっと複雑になります。

しつこいですが、もう一度書きます。このメッセージは、ローカル変数で1279バイト使えます、と書かれています。これは RAM のサイズの2048バイトから、単にグローバル変数のサイズの769バイトを引いた残りの値を表示しているだけです。C言語ではこの残りの範囲でいろいろと重要なことをやっているのですが、そういうのをひとくくりにして、ローカル変数と称しているようです。

難しいことを書いてもユーザーが混乱するだけなので、こういう表現になっているのだと思いますが、注意が必要だと思います。

RAMの中身のおさらい
ヒープとスタック
この図の出展が見つからなかったのでURLを示せないのですが、これは arduino の RAM の割り当てを示しています。なお、書くのが後になってしまいましたが、この記事では CPU に ATmega328 を使っているという前提で話を進めます。

この図の左の水色 (.data) と緑色 (.bss) はグローバル変数用の領域で、両者のサイズの合計が、arduino IDE では、「グローバル変数が769バイト(37%)を使っていて...」 と表示されています。

RAM の残りはヒープ (heap) とスタック (stack) に使われています。ヒープとスタックがぶつからないようにすることが重要ですが、このあたりの位置関係がどうなっているかを知るには、実際に動いているシステムの状態で調べるしか無いようです。

メモリーの使用状態を調べるプログラム
メモリーの使われ方を確認するためにはプログラムを作るしかありません。調べてみると、freeMemory() というスタックとヒープの間隔を調べる関数が公開されていました。これならターゲットシステムへのコンタミは最小限で済むのですが、判るのはその間隔の値だけです。

どうせなら、各部のアドレスまで含んだメモリーマップの全体像を知りたいので、そういうことが判るプログラムを作ってみました。ちょっと長めですが、使用事例の形で公開します。
// RAMの使用状況チェックプログラム
// グローバル変数、ヒープ、空き、スタック領域のアドレスとサイズをシリアルに出力
// 2019/5/13 ラジオペンチ
// (コンパイル結果:スケッチが4266バイトを使っています。グローバル変数が222バイト、)

// メモリチェック用変数(ここから)
int aRamStart = 0x0100; // RAM先頭アドレス(固定値)
int aGvalEnd; // グローバル変数領域末尾アドレス
int aHeapEnd; // ヒープ領域末尾アドレス(次のヒープ用アドレス)
int aSp; // スタックポインタアドレス(次のスタック用アドレス)
char aBuff[6]; // 表示フォーマット操作バッファ

void setup() {
Serial.begin(115200);
checkMem(); // メモリーの使用状況をチェック
printMem(); // チェック結果をシリアルに表示
}

void loop() {
}

//-------メモリ使用状況表示-------------------------------------------------
// ここから↓
void printMem() { // RAM使用状況を表示
Serial.println();
Serial.println(F("RAM allocation table"));
Serial.println(F("usage start end size"));

Serial.print(F("groval: ")); // 固定アドレス
printHexDecimal(aRamStart); // 開始
Serial.print(F(" - "));
printHexDecimal(aGvalEnd); // 終了
Serial.print(" ");
printHexDecimal(aGvalEnd - aRamStart + 1); // サイズ
Serial.println();

Serial.print(F("heap : ")); // ヒープ
printHexDecimal(aGvalEnd + 1); // 開始
Serial.print(F(" - "));
printHexDecimal(aHeapEnd - 1); // 終了(aHeapEndは次のヒープ用のアドレスなので-1)
Serial.print(F(" "));
printHexDecimal(aHeapEnd - 1 - (aGvalEnd + 1) + 1); // サイズ
Serial.println();

Serial.print(F("free : ")); // Free領域
printHexDecimal(aHeapEnd); // 開始
Serial.print(F(" - "));
printHexDecimal(aSp); // 終了
Serial.print(F(" "));
printHexDecimal(aSp - aHeapEnd + 1); // サイズ
Serial.println();

Serial.print(F("stack : ")); // スタック領域
printHexDecimal(aSp + 1); // 開始(aSpは次のスタック用アドレスなので+1)
Serial.print(F(" - "));
printHexDecimal(RAMEND); // 終了(RAMENDは0x8fffでシステム側で定義)
Serial.print(F(" "));
printHexDecimal(RAMEND - (aSp + 1) + 1); // サイズ
Serial.println();
Serial.println();
}

void checkMem() { // RAM使用状況を記録
// 意味不明なところもあるが、そのまま使用
uint8_t *heapptr, *stackptr;
stackptr = (uint8_t *)malloc(4); // とりあえず4バイト確保
heapptr = stackptr; // save value of heap pointer
free(stackptr); // 確保したメモリを返却
stackptr = (uint8_t *)(SP); // SPの値を保存(SPには次のスタック用の値が入っている)
aSp = (int)stackptr; // スタックポインタの値を記録
aHeapEnd = (int)heapptr; // ヒープポインタの値を記録
aGvalEnd = (int)__malloc_heap_start - 1; // グローバル変数領域の末尾アドレスを記録
}

void printHexDecimal(int x) { // 引数を16進と10進で表示 0xHHHH(dddd)
sprintf(aBuff, "%04X", x); // 16進4桁に変換
Serial.print(F("0x")); Serial.print(aBuff);
Serial.print(F("("));
sprintf(aBuff, "%4d", x); // 10進4桁に変換
Serial.print(aBuff); Serial.print(F(")"));
}
// ここまで↑ コピペ

22行目以降が必要な関数で、これをターゲットとなるプログラムにペーストしておきます。また、7-11行が関数で使っている変数なので、これをプログラムの最初の部分(グローバル変数になる場所ならどこでも可)にペーストしておきます。また、シリアル通信を使うので、setup()の中に14行目のようにSerial.begin を書いておきます。

プログラムとしてはsetup(); と loop(); だけで中身は何もないものになっています。ここに、15,16行目のようにcheckMem(); printMem(); 関数を呼ぶようにしておけば、メモリーの状態がシリアルに出力されます。

checkMem(); printMem(); の二つに分かれているので、checkMem をスピードが問題になる部分(例えば割込み処理ルーチン内)に置いて情報だけ記録しておき、後で printMem でゆっくり出力するなんて使い方も出来るはずです。おっと、そういう場合は7-11行目に volatile 属性を付けないとまずいです。

実行結果
実行結果(何も無い状態)
プログラムを実行すると、このように各領域の開始アドレス、終了アドレスとそのサイズを表示します。値は16進と()内に10進の値を表示しています。

最初の行(gloval)の右端の値は(222)、IDE がコンパイル後の表示するグローバル変数のサイズと一致しているはずです。次行以降にヒープ (heap)、空き (free)、スタック (stack) のサイズが表示されていますが、このプログラムはメモリーの表示以外何もやっていないので、heap と stack は最小限で、ほとんど全部が free になっていることが判ります。

別の事例
プログラムが長いのでポイントとなる部分だけ示します。
なお、全体はこちら。20190513HeapStackCheckDemo.txt。ちなみに元となったプログラムは、この記事のものです。
// void setup() {
pinMode(13, OUTPUT);
Serial.begin(115200);
checkMem(); // メモリーの使用状況をチェック
printMem(); // チェック結果をシリアルに表示

if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x64
Serial.println(F("SSD1306 allocation failed"));
for (;;); // Don't proceed, loop forever
}
checkMem(); // メモリーの使用状況をチェック
printMem(); // チェック結果をシリアルに表示
これはSSD1306インターフェイスのOLEDを使ったプログラムです。setup()の中でそのインターフェイスの初期化をやっているのでその前後でメモリーの状態がどう変化するか調べてみました。

実行結果例(OLED付きの状態)
上が SSD1306 のインターフェイス初期化前、下が初期化後の値です。

空きメモリ (free) のサイズが大きく減り、その分が heap に移っています。この OLED は128x64 画素あり、そのサイズに相当する量のバッファを RAM に確保しています。128x64=8192 画素なので、1024 バイトの RAM が必要になりますが、これで(この時点の) heap のサイズのほとんどを使っていることになります。

残りメモリーが217バイトしか無いので、あまり余裕が無いことが判ります。こんな状態になっていても arduino IDE は、「ローカル変数で1259バイト使うことが出来ます。」なんて脳天気なことを言ってくるので、油断が出来ません。

まとめ
Arduino のRAMの使用状態を表示するプログラムを作ってみました。これを使うとメモリーの状態が良く判り、特に空き容量を確認するのに便利だと思います。

このプログラムで表示するメモリーのアドレスは chekMem関数 を実行した瞬間に取得していますが、以下のような点に注意が必要です。
・試しにメモリーを確保 (malloc) して、そのアドレスをヒープのアドレスとみなしています。もしメモリーが虫食い状態になっていたら、手前のアドレスを返してくる可能性があります。(そんなに賢くないかも?)
・2バイトのアドレスを取得中にその値が変化している可能性があります。(たぶんアトミックアクセスになっていない)
・ソースでは関数を呼び出しするように書いておいても、コンパイラの最適化で関数呼び出し無しのコードに修正されることがあるようです (その場合スタックポインタの値はそのまま)。また意味の無いソースの記述は無視されます。ということで、C言語からヒープやスタックの量を予想するのは結構難しいです。

メモリーの使用状態を知る方法として、.elf ファイルを avr-objdump や avr-nm で調べる方法が良く知られています。でもこのやり方では動的に変化するヒープやスタックのアドレスまで知ることは出来ないはずです。

このプログラムを動かすためにはかなりの量のメモリを消費します(フラッシュ、SRAM 共)。空きメモリが枯渇しそうになると Serial.print すら動かなくなるので、利用には注意が必要です。新しいライブラリなどを試す時に、事前にそのメモリーの使用量を確認する、などの使い方が良いと思います。

そんなことでかなりクセの強いプログラムです。実用的というより、C言語の勉強に面白い素材ではないかと思います。

せっかく面白いものが出来たので、悪乗りしてコアダンプというか、RAMのダンププログラムも作ってみたいと思います。実は以前に似たものを作ったので、ちょっと修正するだけで動くはずです。スタックやヒープの位置が判った上でダンプリストを眺めるのは面白いと思います。
関連記事

コメントの投稿

管理者にだけ表示を許可する

No title

Panda43です
私も以前、同じようなことが原因で大変悩んだ記憶があります
そのため、今ではavr-objdump と avr-nmを使って調べています
使い方はここを参考にしました

http://d.hatena.ne.jp/clayfish/20090301/1235839554

Panda43さん、おはようございます

同じことで悩んだ方がいて安心しました。

色々調べるなかで、貼っていただいたサイトの情報はとてっも参考になりました。
というか、この記事中のRAMのアロケーションの図は、そのサイトから持ってきました。そのサイトの記事で、元情報へのリンクが切れていたので出展元のURLが貼れなかったんですよね。

似たような図

Arduino未経験者なので見当違いかもしれませんが、似たような図を探してみました。
下記リンクの一番下の図です。

Arduino Unoのメモリについての説明
https://garretlab.web.fc2.com/arduino/introduction/memory/index.html

re:似たような図

mytoshi さん、今晩は。

はい、galletlab さんの資料のその図で、実際のアドレスがどうなっているかを調べるのがこの記事のプログラムの目的です。
カレンダー
07 | 2019/08 | 09
- - - - 1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
プロフィール

ラジオペンチ

Author:ラジオペンチ
電子工作を中心としたブログです。たまに近所(東京都稲城市)の話題など。60過ぎて視力や器用さの衰えを感じつつ日々挑戦!
コメントを入れる時にメールアドレスの記入は不要です。なお、非公開コメントは受け付けていません。

記事が気に入ったらクリックを!
最新記事
カテゴリ
最新コメント
リンク
FC2カウンター
検索フォーム
月別アーカイブ
RSSリンクの表示
QRコード
QRコード