論よりコード
- F()マクロやPSTR()マクロで指定した文字列分が、SRAMからFlashメモリに移動します。
- Flashメモリからの読み出しに対応した
**_P
関数が利用できます。
F()マクロ
//前:
Serial.print("0123456789abcdef");
//後
Serial.print(F("0123456789abcdef"));
PSTR()マクロ (**_P 関数と共に使用)
//前:
sprintf(buf, "Uptime: %lu sec", millis() / 1000L);
//後:
sprintf_P(buf, PSTR("Uptime: %lu sec"), millis() / 1000L);
Arduino UNO のメモリ構造とSRAMの消費
Arduino UNO は32KBのFlashメモリと2KBのSRAM(Static Random Access Memory)を持っています。
Flashメモリはプログラム領域とも呼ばれ、プログラムの保存先です。実行時には読み取りだけが可能です。SRAMは実行時におけるメモリー領域です。変数はここに保存され、実行時に読み書きできます。
SRAMはFlashメモリに比べて小さく、また、実行時においてSRAMの領域が不足するとプログラムが実行できなくなったり、不定な挙動を起こすことから、できる限り空けておきたい領域です。
SRAMを消費する要因の1つに文字列があります。数値型であれば1~4バイトもあれば表現できますが、文字は1文字1バイトと、表現内容に比べて必要とする領域が大きくなります。例えば
Serial.println("Hello World");
これだけで少なくとも Hello world\0
の12バイト分をSRAMで消費することになります。よって、文字列の扱いを工夫することが、SRAMの節約につながります。
※ この記事で使用したバージョンは Arduino IDE 1.8.13 / Arduino AVR Boards 1.8.3 です。
F()マクロの効果と問題点
SRAMを節約する方法として、書き換えが発生しない文字列はSRAMではなくFlashメモリに配置する手法があります。これを簡単に実現するのが F()マクロ(定義: WString.h)です。コード内の "Text"
を F("Text")
とするだけで、この文字列をSRAMからFlashに移動しつつ、Flashから読み取ることができます。
void setup() {
Serial.begin(115200);
Serial.print("0123456789abcdef");
}
void loop() {
}
void setup() {
Serial.begin(115200);
Serial.print(F("0123456789abcdef"));
}
void loop() {
}
コンパイル後に表示されるstatsを比較すると、SRAMの領域を空けることに成功しています。
Case | Flash usage (bytes) | SRAM usage (bytes) |
---|---|---|
raw_print | 1472 | 202 |
using_f_macro | 1484 | 184 |
※raw_printからの増減 | +12 | -18 |
F()マクロも万能ではありません。たいていの関数はSRAM内の値を操作の対象としており、Flashに配置されてしまうと読み出しができません。例えば文字列をフォーマッティングできる sprintf()
の第二引数には、Fマクロを使うことができません。
sprintf(buf, F("Uptime: %lu sec"), millis() / 1000L);
// => cannot convert 'const __FlashStringHelper*' to 'const char*' for argument '2' to 'int sprintf(char*, const char*, ...)'
**_P
関数とPSTRマクロ
avr-libcには **_P
と付いた関数がいくつか存在しています。これは操作の対象をSRAMではなくFlashにしている関数であること以外は、元の名前の関数と同じ挙動をするものです。
関数の一覧は AVR libc の stdio.h: Standard IO facilities や avr/pgmspace.h: Program Space Utilities で確認できます。
sprintf_P()
や strlen_P()
といった、文字列操作でよく使う関数もあります。
**_P
関数に引き渡す文字列はPSTR()マクロを通します。使い方はF()マクロと同じです。
ちなみにF()マクロも内部的にはPSTR()マクロを使っています
これらを組み合わせると sprintf()
は以下のように書き換えることが可能です。
//前:
sprintf(buf, "Uptime: %lu sec", millis() / 1000L);
//後:
sprintf_P(buf, PSTR("Uptime: %lu sec"), millis() / 1000L);
実際の効果を確認してみます。
void setup() {
Serial.begin(115200);
}
void loop() {
char buf[64];
sprintf(buf, "Uptime: %lu sec", millis() / 1000L);
Serial.println(buf);
delay(1000);
}
void setup() {
Serial.begin(115200);
}
void loop() {
char buf[64];
sprintf_P(buf, PSTR("Uptime: %lu sec"), millis() / 1000L);
Serial.println(buf);
delay(1000);
}
Case | Flash usage (bytes) | SRAM usage (bytes) |
---|---|---|
sprintf | 3286 | 204 |
sprintf_P && PSTR | 3286 | 188 |
※sprintfからの増減 | +-0 | -16 |
sprintfの第二引数分がSRAM上で節約できていることは確認できました。
Flashの消費が全く同じなのが解せないのですが。 → これは注意点で解決しました。
PSTRマクロは自作関数内でも利用可能に
PSTR()マクロは自作関数にも引き渡し可能です。読み出しには pgm_read_byte()
を利用します。
例えば長い文字列をOLEDモニターの表示可能な文字数に合わせて改行表示する自作関数では、以下のようにしています。
※ U8X8は U8g2 というモノクロディスプレイ向けライブラリ のテキスト表示専用ライブラリです。
# define OLED_MAX_CHAR_LENGTH 16
void drawText_P(U8X8 *u8x8, const char *pgm_s, int width = OLED_MAX_CHAR_LENGTH) {
size_t len = strlen_P(pgm_s);
for (int i = 0 ; i < len ; i++) {
if (u8x8->tx > width - 1) {
u8x8->tx = 0; // CR
u8x8->ty++; // LF
}
char c = pgm_read_byte(pgm_s++);
u8x8->print(c);
}
}
呼び出し側はPSTR()マクロを使って呼び出すことができます。
drawText_P(&u8x8, PSTR("Welcome to Arduino world!!"));
注意点
F()マクロやPSTR()マクロは、変数の保管先をSRAMからFlashメモリにするだけであり、総量を削減するものではありません。たとえば、Flashメモリがひっ迫しているため、あえてSRAM側に置くといった考え方も必要となります。ここはコンパイル時に表示される値を見ながら配置先の調整を行っていく事になるでしょう。
コメントいただきまして https://qiita.com/ma2shita/items/89ed5b74698d4922efdd#comment-834ae14d3cb3efc0cb4d より引用です。
SRAM に配置される文字列は Arduino のプログラム起動時に Flash に記録されたものが SRAM にコピーされる仕組みであり、この記事にある SRAM に配置せずに Flash の配置された文字列をプログラムが読む構成であれば SRAM が使用されない分メモリの総量は削除されます。
とのこと。
それで sprintf から sprintf_P への書き換えの際、Flashのメモリサイズのみ変わったんですね、納得。
ありがとうございました!
ESP32系の場合
IoTでよく使われるマイコンと言えばEPS32も挙げられます。Arduino core for esp32 は、互換性がかなり高いようで、今回紹介したF()マクロやPSTR()マクロ両方とも利用できました。
読み出し速度は未検証
Flash、SRAMそれぞれに配置されたメモリからの読み出し速度については未検証です。検証において速度面の優劣は感じませんでしたが、長期もしくは大量の読み出しでは差が出る可能性もあるでしょう。そういった運用が想定されている場合は、あらかじめ検証をおすすめします。
あとがき
なんでこんなブログを書いているかというと、先日発売を開始した LTE-M Shield for Arduino を用いたサンプルコードを書いていたら「メモリが足りません」という表示が!
手抜きをするためにライブラリを取り込みまくってたらこの有様だったわけです。泣く泣く ArduinoJson や ArduinoHttpClient を削除して sprintf() を使って手書きのJSONエンコードやHTTPシーケンスを実装したのですが、それでも足りない!ということで、SRAM領域を空ける手段についてまとめたという経緯です。
こういったチャレンジは楽しいですが、ビジネス的には微妙ですね。
今や、時間と人間は貴重なリソースで、相対的にハードは安価になっています。メモリがあれば1時間で終わる実装が、その10倍くらいかかってしまったわけですが、できるものは同じです。しかも、省メモリ化すごい!よりも「時間かかったね」という評価になるんじゃないかな。
大切な事なので2回言いますが、時間と人間は貴重なリソースです。これからもハードウェアのお力を借りて「この実装が数分で!すごい!」を目指していきたいと思います!
参考資料
- Arduino 日本語リファレンス | PROGMEMとFマクロ / http://www.musashinodenpa.com/arduino/ref/index.php?f=0&pos=1830
- AVR Libc | avr/pgmspace.h: Program Space Utilities / http://www.nongnu.org/avr-libc/user-manual/group__avr__pgmspace.html
- AVRWiki - FLASH上定数 / http://avrwiki.osdn.jp/cgi-bin/wiki.cgi?page=FLASH%BE%E5%C4%EA%BF%F4
EoT