
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メモリ使用スケッチのサンプル
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
// // Flashメモリを使えるようにヘッダファイルをインクルードする。 // #include <avr/pgmspace.h> // // まずはフラッシュメモリにデータを配置する // const PROGMEM char char_flash[] = {'x', 'y', 'z', 'd', 'e'}; const PROGMEM int int_flash[] = {100, 200, 2450, 14555}; const PROGMEM long long_flash[] = {100000, 30000000, 400000000}; const PROGMEM float float_flash[] = {1.4142, 2.7182, 3.1415}; // 文字列をフラッシュメモリ上に配置する。 const char memory0[] PROGMEM = "arduino"; const char memory1[] PROGMEM = "memory01"; const char memory2[] PROGMEM = "memory02"; const char memory3[] PROGMEM = "memory03"; // 文字列の配列をフラッシュメモリ上に配置する。 // const は変数の直前に書く必要がある char * const memory_tb[] PROGMEM = {arduino1,arduino2,arduino3,arduino4,}; void setup() { char char_data ; int int_data ; long long_data; float float_data; static char str_buff[10]; Serial.begin(9600); // 1バイトのデータを読む。 for (int i = 0; i < sizeof(char_flash) / sizeof(*char_flash); i++) { char_data = pgm_read_byte(char_flash + i); Serial.println((char)char_data); } // 2バイトのデータを読む。 for (int i = 0; i < sizeof(int_flash) / sizeof(*int_flash); i++) { int_data = pgm_read_word(int_flash + i); Serial.println(int_data); } // 4バイトのデータを読む。 for (int i = 0; i < sizeof(long_flash) / sizeof(*long_flash); i++) { long_data = pgm_read_dword(long_flash + i); Serial.println(long_data); } // floatのデータを読む。 for (int i = 0; i < sizeof(float_flash) / sizeof(*float_flash); i++) { float_data = pgm_read_float(float_flash + i); Serial.println(float_data); } // 文字列のデータを読む。 for (int i = 0; i < sizeof(memory_tb) / sizeof(*memory_tb); i++) { strcpy_P(str_buff, (char *)pgm_read_byte(&(memory_tb[i])));//strcpy_Pはフラッシュメモリの文字をSRAMにコピーする Serial.println(str_buff); } } void loop() { } |
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)で用意されている文字関数
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 32 33 34 35 36 37 38 39 40 41 42 43 |
extern const void * memchr_P(const void *, int __val, size_t __len) __ATTR_CONST__; extern int memcmp_P(const void *, const void *, size_t) __ATTR_PURE__; extern void *memccpy_P(void *, const void *, int __val, size_t); extern void *memcpy_P(void *, const void *, size_t); extern void *memmem_P(const void *, size_t, const void *, size_t) __ATTR_PURE__; extern const void * memrchr_P(const void *, int __val, size_t __len) __ATTR_CONST__; extern char *strcat_P(char *, const char *); extern const char * strchr_P(const char *, int __val) __ATTR_CONST__; extern const char * strchrnul_P(const char *, int __val) __ATTR_CONST__; extern int strcmp_P(const char *, const char *) __ATTR_PURE__; extern char *strcpy_P(char *, const char *); extern int strcasecmp_P(const char *, const char *) __ATTR_PURE__; extern char *strcasestr_P(const char *, const char *) __ATTR_PURE__; extern size_t strcspn_P(const char *__s, const char * __reject) __ATTR_PURE__; extern size_t strlcat_P (char *, const char *, size_t ); extern size_t strlcpy_P (char *, const char *, size_t ); extern size_t strnlen_P(const char *, size_t) __ATTR_CONST__; /* program memory can't change */ extern int strncmp_P(const char *, const char *, size_t) __ATTR_PURE__; extern int strncasecmp_P(const char *, const char *, size_t) __ATTR_PURE__; extern char *strncat_P(char *, const char *, size_t); extern char *strncpy_P(char *, const char *, size_t); extern char *strpbrk_P(const char *__s, const char * __accept) __ATTR_PURE__; extern const char * strrchr_P(const char *, int __val) __ATTR_CONST__; extern char *strsep_P(char **__sp, const char * __delim); extern size_t strspn_P(const char *__s, const char * __accept) __ATTR_PURE__; extern char *strstr_P(const char *, const char *) __ATTR_PURE__; extern char *strtok_P(char *__s, const char * __delim); extern char *strtok_rP(char *__s, const char * __delim, char **__last); extern size_t strlen_PF(uint_farptr_t src) __ATTR_CONST__; /* program memory can't change */ extern size_t strnlen_PF(uint_farptr_t src, size_t len) __ATTR_CONST__; /* program memory can't change */ extern void *memcpy_PF(void *dest, uint_farptr_t src, size_t len); extern char *strcpy_PF(char *dest, uint_farptr_t src); extern char *strncpy_PF(char *dest, uint_farptr_t src, size_t len); extern char *strcat_PF(char *dest, uint_farptr_t src); extern size_t strlcat_PF(char *dst, uint_farptr_t src, size_t siz); extern char *strncat_PF(char *dest, uint_farptr_t src, size_t len); extern int strcmp_PF(const char *s1, uint_farptr_t s2) __ATTR_PURE__; extern int strncmp_PF(const char *s1, uint_farptr_t s2, size_t n) __ATTR_PURE__; extern int strcasecmp_PF(const char *s1, uint_farptr_t s2) __ATTR_PURE__; extern int strncasecmp_PF(const char *s1, uint_farptr_t s2, size_t n) __ATTR_PURE__; extern char *strstr_PF(const char *s1, uint_farptr_t s2); extern size_t strlcpy_PF(char *dst, uint_farptr_t src, size_t siz); extern int memcmp_PF(const void *, uint_farptr_t, size_t) __ATTR_PURE__; |
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 … 読み込むデータ(プリミティブ型もしくは作成した構造体) |
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 32 33 34 35 |
#include <EEPROM.h> struct MyObject{ float field1; byte field2; char name[10]; }; void setup(){ float f = 0.00f; //Variable to store data read from EEPROM. int eeAddress = 0; //EEPROM address to start reading from Serial.begin( 9600 ); while (!Serial) { ; // wait for serial port to connect. Needed for Leonardo only } Serial.print( "Read float from EEPROM: " ); //Get the float data from the EEPROM at position 'eeAddress' EEPROM.get( eeAddress, f ); Serial.println( f, 3 ); //This may print 'ovf, nan' if the data inside the EEPROM is not a valid float. // get() can be used with custom structures too. eeAddress = sizeof(float); //Move address to the next byte after float 'f'. MyObject customVar; //Variable to store custom object read from EEPROM. EEPROM.get( eeAddress, customVar ); Serial.println( "Read custom object from EEPROM: " ); Serial.println( customVar.field1 ); Serial.println( customVar.field2 ); Serial.println( customVar.name ); } void loop(){ /* Empty loop */ } |
EEPROM.put(int address, T &data) | |
EEPROMのaddressに任意の型のデータもしくはオブジェクトを書き込みます。 | |
引数 | int address … 読み込みアドレス T &data … 読み込むデータ(プリミティブ型もしくは作成した構造体) |
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 32 33 34 35 36 37 38 39 |
#include <EEPROM.h> struct MyObject { float field1; byte field2; char name[10]; }; void setup() { Serial.begin(9600); while (!Serial) { ; // wait for serial port to connect. Needed for native USB port only } float f = 123.456f; //Variable to store in EEPROM. int eeAddress = 0; //Location we want the data to be put. //One simple call, with the address first and the object second. EEPROM.put(eeAddress, f); Serial.println("Written float data type!"); /** Put is designed for use with custom structures also. **/ //Data to store. MyObject customVar = { 3.14f, 65, "Working!" }; eeAddress += sizeof(float); //Move address to the next byte after float 'f'. EEPROM.put(eeAddress, customVar); Serial.print("Written custom data type! \n\nView the example sketch eeprom_get to see how you can retrieve the values!"); } void loop() { /* Empty loop */ } |
EEPROM[address] | |
EEPROMという識別子を配列のように利用可能とするための演算子です。 これを用いることでEEPROMセルを直接読み書きできるます |
|
引数 | 読み書きするアドレス |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <EEPROM.h> void setup(){ unsigned char val; //Read first EEPROM cell. val = EEPROM[ 0 ]; //Write first EEPROM cell. EEPROM[ 0 ] = val; //Compare contents if( val == EEPROM[ 0 ] ){ //Do something... } } void loop(){ /* Empty loop */ } |
プログラムによるメモリ使用の確認方法
Arduinoでの開発中のプログラムが、どのようにメモリーを使用しているのかは、Arduinoに付属するコマンドラインのプログラム、avr-objdump.exeを利用することで確認することが可能です。
これはArduinoをインストールしたフォルダーに展開されている、コマンドラインで動作するプログラムです。
インストーラなどによってインストールした場合には、一般的には下記のフォルダーに展開されています。
C:Program Files (x86)Arduinohardwaretoolsavrbin
コンパイル時に.elfファイルが生成されメモリ使用状況が格納される
Arduinoプログラムのコンパイル時に .elfという拡張子が生成されます。
この .elfファイルをコマンドプログラム[avur-objdump]によって中身を表示することで確認することができます。
この.elfファイルは、バイナリーファイルであるため、テキストエディタなどによって中を見ることはできません。
まずは .elfファイルが生成される場所を確認しておくために
まずは.elfファイルが一時的にどこに作成されるのかを確認しなければなりません。
そのためには、ArduinoのIDEを起動し、[MENU]-[ファイル]-[環境設定]のダイアログにおいて、下記の部分「より詳細な情報を表示する:」にチェックを入れておきます。
こうすることで、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] という部分を確認することでメモリ使用を確認できます。
できるだけ#define等の定数を利用する
それでは、Arduinoプログラムにおいて定数と変数の使用によってどのくらいメモリ使用が変わるか簡単なプログラムを作り実験をして見ます。
#defineによる定数と、intによる変数でのコーディングにより、コンパイル時にコメントアウトを切り替えてその結果を見てみます。
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 32 33 34 35 36 37 38 39 40 41 |
#define p2 13 #define p3 13 #define p4 13 #define p5 13 #define p6 13 #define p7 13 #define p8 13 #define p9 13 /* int p1= 0; int p2= 1; int p3= 2; int p4= 3; int p5= 4; int p6= 5; int p7= 6; int p8= 7; int p9= 8; */ void setup() { pinMode(p1,OUTPUT); pinMode(p2,OUTPUT); pinMode(p3,OUTPUT); pinMode(p4,OUTPUT); pinMode(p5,OUTPUT); pinMode(p6,OUTPUT); pinMode(p7,OUTPUT); pinMode(p8,OUTPUT); pinMode(p9,OUTPUT); } void loop() { digitalWrite(p1,OUTPUT); digitalWrite(p2,OUTPUT); digitalWrite(p3,OUTPUT); digitalWrite(p4,OUTPUT); digitalWrite(p5,OUTPUT); digitalWrite(p6,OUTPUT); digitalWrite(p7,OUTPUT); digitalWrite(p8,OUTPUT); digitalWrite(p9,OUTPUT); } |
#defineによる結果
0.data 1.textのサイズがメモリに関する結果です。
ここでは dataは24 textは000011a0という値になっています。
intによる結果
ここでは dataは34 testは000011c4です。
結果
#defineで宣言した場合より明らかにおおきなメモリを使用していますます。
最後に….
これだけでもメモリ使用はこれだけ変化します。
そのほか、メモリ節約プログラミングにはいろいろなコマンドがありますが、技術の深い面になってきますので、ここでは割愛しておきます。
とにかく、組込みはメモリーとの戦いとなります。
この先ご紹介するIO制御の関数の使い方でも、メモリーや処理速度に影響しますので、製品化を考えた場合、このあたりをきっちり押さえておく必要があります。
コメント