Arduinoプログラムメモリ消費量について

Arduinoはとても便利で簡単に組み込みプログラムを作れるのですが、どうも少し本格的なプログラムを作ろうとするとメモリ(MEMORY)の壁で悩むシーンが出てきます。

コンパイルをして、ArduinoIDEで見る限りメモリの残りサイズがまだ数%あるからと安心していると、急に変な動きが起こったりします。
まったく何もソースコードを変えていないのにさっきまで動いていたところが急に動かなくなったりします。
こんなときはだいたいメモリ(MEMORY)を圧迫している原因が多いです。
Arduinoでのコンパイル後の残りメモリ(MEMORY)サイズは数%では結構ギリギリのようです。

 

 

Arduino でメモリの種類と働き

代表的なArduinoのボードUNOでは、メモリのサイズが下記のように表記されています。
今回は、このメモリが一体Arduinoのコンパイルの中でどのように使われているのかを調査してみます。

Arduino Unoのメモリ構成と働き

Arduino Unoでは下の表のように3種類のメモリがボード上に搭載されています。
そしてmArduino Unoが採用しているCPU(ATmega328P)では、ハーバードアーキテクチャの採用のもと、プログラムとデータが物理的に分離された領域に配置されて動作しています。

メモリ種別 用途 サイズ 揮発性
フラッシュメモリ
FlashMemory
プログラム、ブートローダ、読み取り専用ユーザデータ 32KByte(内Bootloader 5KByte) 不揮発
SRAM ユーザーデータ 2KByte 揮発
EEPROM ユーザーデータ 1KByte 不揮発

Arduinoにおいて、プログラム内で利用する変数はSRAM上に配置されます。

SRAMの消費を減らすためには、フラッシュメモリのおける読み取り専用のデータ(変数宣言時に初期化できる変数)をうまく使うことで、SRAMを節約することができます。

EEPROMにも読み書き可能なデータを格納することもできますので、SRAMの圧迫を避けプログラム領域を増やすこともできます。

しかし、フラッシュメモリとEEPROMに配置したデータは、スケッチの中で変数として直接アクセスができないため、別途関数を利用して一旦SRAM上の変数に値をコピーするなどして使用するひと手間が増えます。

注意する点は、フラッシュメモリのデータはスケッチ内で初期化はできますが、スケッチの実行中の書き換えはできません。また、EEPROMに初期値を書き込むには、avr-gccの機能を利用する必要があります。

フラッシュメモリ [FlashMemory]

電源を落としても記憶されている不揮発性メモリで、Arduinoでは開発したプログラムとArduinoのブートローダが配置されます。
また、プログラム内で利用する変数も格納することもがきます。

フラッシュメモリにデータを格納には、単純に変数宣言をするだけではなく、フラッシュメモリに格納する旨を宣言する必要あります。
そして、フラッシュメモリのデータは、Arduinoに用意されている特別な関数を使いいったんSRAM上の変数に読み込んでから使用しなくてはなりません。

つまり、例えばあらかじめ用意する大量のデータ(配列など)などをフラッシュメモリに配置しておいて、必要な時に必要なに部分をSRAMに読み込んで使うことでArduinoの少ないメモリ資源を有効活用できます。

フラッシュメモリの使用方法

フラッシュメモリを使う場合には、Arduinoで用意されているPROGMEM修飾子を変数宣言時に使用します。
この機能を使うためには、Arduinoスケッチ上で、avr/pgmspace.hをインクルードする必要があります。

Flashメモリ使用スケッチのサンプル

Arduinoで宣言できるデータ型
prog_char char (1 byte) -127~128
prog_uchar unsigned char (1 byte) 0~255
prog_int16_t int (2 bytes) -32,767~32,768
prog_uint16_t unsigned int (2 bytes) 0~65,535
prog_int32_t long (4 bytes) -2,147,483,648~2,147,483,647.
prog_uint32_t unsigned long (4 bytes) 0~4,294,967,295

Arduino(pgmspace.h)で用意されている文字関数

SRAM

電源を落としたと同時に消滅するデータを一時展開するためのメモリーです。
プログラム内で使用する、int やchar等で宣言された一時的な変数が展開されます。
よって、変数を使いすぎるとこの領域がどんどん減ってしまい、動かないプログラムが自然にできあがってしまう可能性があります。
コンパイル結果として、残メモリー容量などが目安として表示され、問題ないように見えても、動作中に一時的に多くのメモリーを使い、メモリー破壊によりあるときから急に挙動不審に陥ることがあります。
変数や特に文字列の使用には気をつけなくてはいけません。

領域名 機能・働き プログラムでの使用
data 初期化済みデータ 初期化したグローバル変数やstatic変数で、静的記憶域期間を持つ変数で初期値を与えたもの。
bss 非初期化データ 初期化していないグローバル変数やstatic変数で、静的記憶域期間を持つ変数で初期値を与えていないもの。
ヒープ プログラム実行時に動的に確保するメモリ領域 malloc()などで確保する領域
スタック 実行中の関数に関するデータを格納する領域 関数内で定義したauto変数、関数の引数、関数終了後に戻るアドレスなど動記憶域期間を持つ変数

※プログラムが不定になる要素
ヒープ領域はアドレスの大きい方向へ伸び、スタックはアドレスの小さい方向へ伸びます。
ここがぶつかるとプログラムは不定な動きとなります。

EEPROM

ユーザープログラムによって制御できる、電源が落ちても消滅して欲しくないデータを記憶させておくためのメモリーです。(Arduino Unoでは ATmega328=1024バイト使用可)

使えるメモリサイズはCPUによって異なるため、事前に確認をして使います。

EEPROMライブラリの簡単なコマンドで読み書きが可能ですので、PCのHDDと同じ感覚で使いがちですが、EEPROMの読み書の限界回数(10万回程度)があるので使用には気をつけなくてはいけません。

また、読み書きには1バイトあたり3.3msec程度かかります。

考えられる対策としては、プログラム動作中に書き換えられるデータはSRAMに記憶し、電源OFF前に一気にEEPROMに格納するという処理まどで工夫をします。

EEPROMを操作する関数

EEPROM.read(int address)
EEPROMのaddressから1バイト読み込みます。一度も書き込まれたことのないメモリ値は255を返します。
引数 int address … 読み込みアドレス
戻り値 int  val … 読み込んだデータ(0~255)
EEPROM.write(int address,uint8_t value)
EEPROMのaddressに1バイトを書き込みます
引数 int address … 書き込みアドレス
uint8_t value … 書き込むデータ
EEPROM.update(int address,uint8_t value)
EEPROMのaddressに1バイトを書き込みます。
すでに書き込まれている値と、書き込む値が異なる場合にだけ値が書き込まれます。

引数 int address … 書き込みアドレス
uint8_t value … 書き込むデータ
EEPROM.get(int address, T & data)
EEPROMのaddressから任意の型のデータもしくはオブジェクトを読み込みます
引数 int address … 読み込みアドレス
T &data … 読み込むデータ(プリミティブ型もしくは作成した構造体)

EEPROM.put(int address, T &data)
EEPROMのaddressに任意の型のデータもしくはオブジェクトを書き込みます。
引数 int address … 読み込みアドレス
T &data … 読み込むデータ(プリミティブ型もしくは作成した構造体)

EEPROM[address]
EEPROMという識別子を配列のように利用可能とするための演算子です。
これを用いることでEEPROMセルを直接読み書きできるます

引数 読み書きするアドレス

プログラムによるメモリ使用の確認方法

Arduinoでの開発中のプログラムが、どのようにメモリーを使用しているのかは、Arduinoに付属するコマンドラインのプログラム、avr-objdump.exeを利用することで確認することが可能です。

これはArduinoをインストールしたフォルダーに展開されている、コマンドラインで動作するプログラムです。

インストーラなどによってインストールした場合には、一般的には下記のフォルダーに展開されています。

C:Program Files (x86)Arduinohardwaretoolsavrbin

コンパイル時に.elfファイルが生成されメモリ使用状況が格納される

Arduinoプログラムのコンパイル時に .elfという拡張子が生成されます。
この .elfファイルをコマンドプログラム[avur-objdump]によって中身を表示することで確認することができます。
この.elfファイルは、バイナリーファイルであるため、テキストエディタなどによって中を見ることはできません。

まずは .elfファイルが生成される場所を確認しておくために

まずは.elfファイルが一時的にどこに作成されるのかを確認しなければなりません。
そのためには、ArduinoのIDEを起動し、[MENU]-[ファイル]-[環境設定]のダイアログにおいて、下記の部分「より詳細な情報を表示する:」にチェックを入れておきます。

WS000004-300x257
Arduino環境設定ダイアログ

こうすることで、Arduino の下段のコンパイル情報部分に詳しく表示されます。
コンパイル後、この情報の中から.elfファイルの存在位置を確認し以下の方法でダンプをします。
例)C:Usersユーザー名AppDataLocalTempbuild21c5289bb87da5bd7776d02aa59a41ae.tmp

avr-objdumpを使いメモリ使用状況を確認する方法

avr-objdumpはコマンドプロンプトで操作するプログラムです。
そのため、このプログラムをいつでも使えるようにまずはPathを通しておく必要があります。
例)C:>PATH C:Program Files (x86)Arduinohardwaretoolsavrbin

その後、elfファイルをavr-objdupによってリードします。
c:>avr-objdump -h memorytest.ino.elf

赤文字部分はご自分で作成されたプログラム名称 +.elfとなります。
※今回は、memorytestというスケッチをデスクトップに作成して実験です。
下記が表示されたイメージです。
このなかで、 [0.data] [1.text] [2.bass] という部分を確認することでメモリ使用を確認できます。

intarduino-memory

できるだけ#define等の定数を利用する

それでは、Arduinoプログラムにおいて定数と変数の使用によってどのくらいメモリ使用が変わるか簡単なプログラムを作り実験をして見ます。
#defineによる定数と、intによる変数でのコーディングにより、コンパイル時にコメントアウトを切り替えてその結果を見てみます。

#defineによる結果

define-300x281

0.data 1.textのサイズがメモリに関する結果です。
ここでは dataは24 textは000011a0という値になっています。

intによる結果

int-300x275

ここでは dataは34 testは000011c4です。

結果

#defineで宣言した場合より明らかにおおきなメモリを使用していますます。

最後に….

これだけでもメモリ使用はこれだけ変化します。
そのほか、メモリ節約プログラミングにはいろいろなコマンドがありますが、技術の深い面になってきますので、ここでは割愛しておきます。
とにかく、組込みはメモリーとの戦いとなります。
この先ご紹介するIO制御の関数の使い方でも、メモリーや処理速度に影響しますので、製品化を考えた場合、このあたりをきっちり押さえておく必要があります。

コメント

タイトルとURLをコピーしました