Arduino のヒープとスタックを視覚化しよう(もう String は怖くない!?)

(更新

Visualize Heap and Stack usage of Arduino (Uno).

先日こちらでも触れさせて頂きましたが、電子技術者、組込技術者にとって Arduino は試作設計のための重要なツールとして定着しています。最近は Arduino と言っても、8ビット AVR マイコン ATmega328 だけでなく、32ビットマイコンである SAM D21、ESP32 などが簡単に利用できるようになってきましたが、シールドと呼ばれる拡張基板の豊富な、オリジナルの Arduino フォームファクタを使用した Arduino Uno や Mega も、まだまだ広く利用されています。

ここで、特に Arduino Uno を利用する上での問題の一つは、搭載している AVR マイコン ATmega328 の RAM が 2キロバイトと小さく、ヒープを利用する Arduino ライブラリ(文字列を扱う String クラスなど)を多用すると、メモリが不足しがち、あるいはメモリ使用量を心配しながらソフト設計をしなくてはならないことです。実際、ネット検索エンジンで「arduino heap string issue」などと検索すると、以下のように多くの記事が見つかります。

もちろん、String クラスは注意して扱うべきですが、Arduino におけるヒープやスタックの利用状況を確認しながら作業することができるなら、もっとずっと安心して、プログラムを書けるとは思いませんか?

ヒープとスタックの視覚化ツールを書いてみた

そんな訳で、Arduino のヒープとスタックの使用状況を視覚化するツールを書いてみました。基本的には、Arduino Uno 専用とお考えください。(ATmega 328 を搭載した Arduino であれば、同様に利用可能と思います。)

またまだ改良の余地はあるかと思いますが、まずは成果を GitHub に上げさせて頂きました。

詳しくは上記サイトの README.md を御参考頂くとして、簡単に使い方を説明させて頂きます。

本ツールは、2つの部分から構成さています。

  • arduino_heap: Arduino 上で動作するプログラム(C++ にて記述)
  • visualizer_python: PC 上で動作する視覚化プログラム(Python3 にて記述)

arduino_heap のほうは、サンプルプログラム arduino_heap.inoと、ヘルパーライブラリ heapmon.cpp から構成されています。このヘルパーライブラリは、ATmega328 のメモリ構成や、Arduino IDE で利用している avr-libc の設計に非常に依存した設計となっていますので、他のアーキテクチャのマイコン(SAM D21 や ESP32 など)では動きませんし、avr-libc のバージョンが異なる場合には、動作しない可能性があります。今回は、以下の環境で動作確認しました。

  • Arduino IDE 1.8.19

Arduino 側でプログラムをビルドしたら、フラッシュメモリにアップロードしてください。なお、arduino_heap.ino の中に test1()test2() という関数を呼んでいる場所があります。適宜変更して実験頂ければと思います。なお、もっと良いテスト関数を書かれた方がありましたら、是非 pull request を頂ければ幸いです。

次に PC 側で動かす visualizer_python です。実行環境として、Python 3 が必要です。また、pip で pyserial と termcolor をインストールしておいてください。Arduino ボードが接続されたポートを確認して、例えば次のように実行します。

bash$ python visualize.py -b 115200 /dev/ttyACM0

ヘルプを見る場合には、python visualize.py -h などとしてください。このプログラムを実行して、(必要に応じて)Arduino 側をリセットすると、次のようなグラフがいくつも表示されることでしょう。

ヒープやスタックに関する記事は、ネット上に多くあると思いますので御参考ください。ここでは、このツールで表示される情報について説明します。

Arduino Uno (ATmega328)の RAM は、0x0100〜0x08ff の 2Kバイトです。ツールで表示されるグラフ(?)の左端には、メモリアドレスが 16進数で表示されています。その右に、メモリの利用状況が表示されます。ダンプ部分の 1文字は、1バイトに相当します。

  • data セクションは、Arduino スケッチや C 言語、C++ で定義された定数が配置されるエリアです。
  • BSS セクションは、グローバル変数(グローバルスタティック変数を含む)が配置されるエリアです。
  • heap セクションは、malloc()  や realloc() 関数を呼び出すことで動的に配置されるヒープが配置されるエリアです。小さな(下位の)アドレス側から使用され、上位アドレス側に伸びていきます。
    • String クラスの変数を生成すると、内部的に realloc() 関数が実行され、ヒープが消費されます。
    • heap セクションのうち、ドット(...)が記された部分は、現在実際にヒープが配置されているエリアです。
    • ドットが無い部分は「フリーリスト」と呼ばれ、現在は何も配置されていないエリアです。断片化(fragmentation)と呼ばれることもあります。断片化が進むと、実際にはメモリがあるのに、ヒープを利用できなくなることがあります。
  • stack(スタック)セクションは、動的にローカル変数が配置されるエリアです。動的というのは、必要に応じてメモリの使用量が変化する、という意味です。大きな(上位の)アドレス側から使用され、下位アドレス側に伸びていきます。
    スタックは、関数のネスト呼び出しや、割込サービスルーチン(ISR)でも利用されます。スタックセクションのうち、現在利用されているもっとも小さな(下位の)アドレスのことをスタックポインタ(SP)と呼びます。(厳密に言うと、AVR マイコンアーキテクチャや、多くの他のアーキテクチャでは、利用されているエリアよりも 1バイト小さなアドレスが SP に格納されます。)

    • スタックセクションのうち、ドット(...)が記された部分は、現在実際にローカル変数等で利用されているエリアです。
    • ドットが無い部分は、現在は利用されていないものの、かつて関数呼び出しのネスト等により利用されたことがある(ゴミデータが残っている)エリアです。これが問題となるのは、ヒープ割当の繰り返しと、関数呼び出しのネストにより、もしかすると、現在までにスタックエリアとヒープエリアとの間に衝突があったかも知れないからです。
      言い換えると、現在のスタックポインタ(SP)だけでなく、現在までに SP が取った最小の値が重要だということになります。そして、スタックとヒープが衝突すると、メモリの内容が破壊され、多くの場合、プログラムの動作が不正となります。(プログラムが暴走することもあります。)

もっと説明したいことはたくさんありますが、参考となるサイトはたくさんありますし、私の説明よりも詳細なものが多いと思いますので、ぜひ、そのような情報も御参考ください。もちろん、御不明な点がありましたら、上記 GitHub にて issue として報告頂いたり、以下のフォームからお問い合わせ頂ければ回答させて頂きます。

今日はここまで。

お問い合わせはお気軽に!

お問い合わせを頂いた後、継続して営業活動をしたり、ニュースレター等をお送りしたりすることはございません。
御返答は 24時間以内(営業時間中)とさせて頂いております。必ず返信致しますが、時々アドレス誤りと思われる返信エラーがございます。返答が届かない場合、大変お手数ではございますが別のメールアドレスで督促頂けますと幸いです。