Release 2017.11.26

PROGMEMを使う

Arduinoでは、変数の数値はSRAMにキープされます。しかしながら、SRAMの容量は希少です。なので、一度に大量の数値を変数で扱おうとすると、SRAMを圧迫して動作が不安定になります。これは、ちょっとしたスケッチを書くのであれば、全然気にならないんですが、ビットマップデータや多量の文章なんかを扱おうとすると、結構切実な問題になってきます。

そこで、そういった大量の数値群は、比較的大きいスケッチ用記憶領域「フラッシュメモリ」へ避けておき、必要な時、必要な分だけ「SRAM」に読み込んで負担を軽くする、という機能があります。それがPROGMEMです。

自分の理解できている範囲ではありますが、PROGMEMの使い方について書いていきたいと思います。

ネット上で、「PROGMEMの使い方」を多く見つけることが出来ますが、どうやらIDEのバージョンアップで細かいところが変更されていて、古い情報通りにやると上手くいかない場合があります。そこら辺を考慮した内容にしているつもりですが、下記の説明も現行(2017.11.26 IDE ver 1.8.2)での話です。今後また、変更されていくかもしれないという事にご注意ください。

変数の振る舞い

まずは、比較できるよう、普通に変数を扱った場合の例を挙げます。

#define ARRY_SIZE 100

int val [ARRY_SIZE] = {
  10000, 10001, 10002, 10003, 10004, 10005, 10006, 10007, 10008, 10009,
  10010, 10011, 10012, 10013, 10014, 10015, 10016, 10017, 10018, 10019,
  10020, 10021, 10022, 10023, 10024, 10025, 10026, 10027, 10028, 10029,
  10030, 10031, 10032, 10033, 10034, 10035, 10036, 10037, 10038, 10039,
  10040, 10041, 10042, 10043, 10044, 10045, 10046, 10047, 10048, 10049,
  10050, 10051, 10052, 10053, 10054, 10055, 10056, 10057, 10058, 10059,
  10060, 10061, 10062, 10063, 10064, 10065, 10066, 10067, 10068, 10069,
  10070, 10071, 10072, 10073, 10074, 10075, 10076, 10077, 10078, 10079,
  10080, 10081, 10082, 10083, 10084, 10085, 10086, 10087, 10088, 10089,
  10090, 10091, 10092, 10093, 10094, 10095, 10096, 10097, 10098, 10099
};


void setup() {
  Serial.begin(9600);
  Serial.println("Values on SRAM");
}

void loop() {
  for (byte i = 0 ; i < ARRY_SIZE ; i++)
  {
    int temp = val[i];
    Serial.println(temp);
  }
  while (1);
}

100個の(適当な)数値を用意しint型の配列変数「val」へ仕込んでから、順当にシリアルモニタへ返していくだけの単純なスケッチです。

IDE上でコンパイルすると、下の情報にメモリの使用状況が見えます。「フラッシュメモリ」がスケッチ用記憶領域、「RAM」がSRAMです。

変数に代入される定数は、元々、スケッチ領域に書き込まれた情報がSRAMにコピーされて動作します。なので、定数が増えれば、スケッチ容量・SRAM共に増量することになります。

試しに、配列の中身を1つに減らしてみると、

#define ARRY_SIZE 1
int val [ARRY_SIZE] = {10000};

void setup() {
  Serial.begin(9600);
  Serial.println("Values on SRAM");
}

void loop() {
  for (byte i = 0 ; i < ARRY_SIZE ; i++)
  {
    int temp = val[i];
    Serial.println(temp);
  }
  while (1);
}

プログラム容量・SRAM共に連動している事がよく分かります。

PROGMEMを利用すると、これら定数の束はプログラム記憶領域にだけ保存され、必要な時だけ引き出すようになり、SRAMにゆとりが作れます。

PROGMEMの使い方

まず、多くの参考記事では最初にAVRライブラリをインクルードしていますが、

#include <avr/pgmspace.h>

これは、現行のIDE(version 1.8.2)だと、あってもなくても動作します。本来はAVR用の機能を直に扱えるようにするヘッダファイルだそうですが(Arduinoは元々、AVRマイコンの機能をカスタマイズしているようなもののようです)、今は勝手に組み込んでくれるみたいです。

なので、この記事のサンプルコードでは一切書きません。ただ、何か問題が起きた場合、ここら辺を探ってみると解決の糸口になるかもしれません。

変数の宣言

PROGMEMは変数の中へ具体的な数値を仕込みます。なので、基本的には通常の変数宣言と同じ手順です。

ただ、以前のやり方では、特定の「型の書き方」をしないといけませんでした。

符号付き文字型 prog_char -128 ~ 127
符号なし文字型 prog_uchar 0 ~ 255
符号付きint型 prog_int16_t -32768 ~ 32767
符号なしint型 prog_uint16_t 0 ~ 65535
符号付きlong型 prog_int32_t 2147483648 ~ 2147483647
符号なしlong型 prog_uint32_t 0 ~ 4294967295
const prog_int16_t p_val PROGMEM = 100;

これらの「型」は現状では使えず、逆にエラーが出ます。代わりに普段の変数宣言に付け足すことでPROGMEM用となります。

const 変数名 PROGMEM = ;

const int p_val PROGMEM = 100;

読み出し

PROGMEMとして記憶された数値を呼び出すには、下記のAVR用関数を使って、やりくりする必要が出てきます。

1バイト) pgm_read_byte (フラッシュメモリ上アドレス)
2バイト) pgm_read_word (フラッシュメモリ上アドレス)

引数の「フラッシュメモリ上アドレス」は、単純に変数名を入れます。ここら辺がちょっとクセがあって自分もしっかり把握できていませんが、この中に書かれると、「変数の数値」ではなく、ポインタとして参照するようです。

これら関数は指定したアドレスの中身が返り値となって戻ってくるので、後は通常のやり方と一緒です。

PROGMEMの使用例

PROGMEMの使用方法はこちらで網羅されていて、下記の使用例もこれを元にしています。

数値を扱う

上述のSample 1をPROGMEM仕様にします。

#define ARRY_SIZE 100
const int val [ARRY_SIZE] PROGMEM = {
  10000, 10001, 10002, 10003, 10004, 10005, 10006, 10007, 10008, 10009,
  10010, 10011, 10012, 10013, 10014, 10015, 10016, 10017, 10018, 10019,
  10020, 10021, 10022, 10023, 10024, 10025, 10026, 10027, 10028, 10029,
  10030, 10031, 10032, 10033, 10034, 10035, 10036, 10037, 10038, 10039,
  10040, 10041, 10042, 10043, 10044, 10045, 10046, 10047, 10048, 10049,
  10050, 10051, 10052, 10053, 10054, 10055, 10056, 10057, 10058, 10059,
  10060, 10061, 10062, 10063, 10064, 10065, 10066, 10067, 10068, 10069,
  10070, 10071, 10072, 10073, 10074, 10075, 10076, 10077, 10078, 10079,
  10080, 10081, 10082, 10083, 10084, 10085, 10086, 10087, 10088, 10089,
  10090, 10091, 10092, 10093, 10094, 10095, 10096, 10097, 10098, 10099
};


void setup() {
  Serial.begin(9600);
  Serial.println("Values from PROGMEM");
}

void loop() {
  for (byte i = 0 ; i < ARRY_SIZE ; i++)
  {
    int temp = pgm_read_word(val + i);
    Serial.println(temp);
  }
  while (1);
}

pgm_read_wordの引数がval+iというのは「変数valの(頭の)ポインタアドレスに、i分足したアドレス」という解釈になります。そのアドレス内の値を変数「temp」に落とし、シリアルモニタに出すという流れです。

Sample1と比べると、SRAMの使用量が減っているのが分かります。

上:Sample 1、下:Sample 2

文字列を扱う

文字列の扱い方は数値よりも面倒です。ただ、EEPROMを使う目的は、こっちの方が多くなるのでは、と思います。

まずは、文字列を普通に扱った場合のスケッチ例です。複数の単語を2次元の配列変数に収め、順次シリアルモニタへ吐き出します。

#define STR_SIZE 14
#define CHR_SIZE 23
char str[STR_SIZE][CHR_SIZE] = {
  "ARDUINO UNO", "ARDUINO 101", "ARDUINO PRO", "ARDUINO MICRO",
  "ARDUINO PRO MINI", "ARDUINO MEGA 2560", "ARDUINO ZERO",
  "LILYPAD ARDUINO USB", "LILYPAD ARDUINO SIMPLE", "ARDUINO YUN",
  "ARDUINO DUE", "ARDUINO LEONARDO", "ARDUINO MINI", "ARDUINO NANO"
};


void setup() {
  Serial.begin(9600);
  Serial.println("Charactors on SRAM");
}

void loop() {
  for (byte i = 0 ; i < STR_SIZE ; i++)
  {
    Serial.println(str[i]);
  }
  while (1);
}

これをPROGMEM仕様にするためには、いくつか手順を加えないといけません。以下は参考サイトに習って書き直したスケッチです。

#define STR_SIZE 14
#define CHR_SIZE 23
const char str[STR_SIZE][CHR_SIZE] PROGMEM = {
  "ARDUINO UNO", "ARDUINO 101", "ARDUINO PRO", "ARDUINO MICRO",
  "ARDUINO PRO MINI", "ARDUINO MEGA 2560", "ARDUINO ZERO",
  "LILYPAD ARDUINO USB", "LILYPAD ARDUINO SIMPLE", "ARDUINO YUN",
  "ARDUINO DUE", "ARDUINO LEONARDO", "ARDUINO MINI", "ARDUINO NANO"
};

const char* const string_table[] PROGMEM = {
  str[0], str[1], str[2], str[3], str[4], str[5], str[6], str[7],
  str[8], str[9], str[10], str[11], str[12], str[13]
};


void setup() {
  Serial.begin(9600);
  Serial.println("Charactors from PROGMEM");
}

void loop() {
  for (byte i = 0 ; i < STR_SIZE ; i++)
  {
    char buf[30];
    strcpy_P(buf, pgm_read_word(&(string_table[i])));
    Serial.println(buf);
  }
  while (1);
}   

まず、一番核となる部分は、strcpy_P()というAVRの関数を使っている事です。

strcpy_P(結果を代入する変数 , フラッシュメモリ上アドレス)

これはプログラム用フラッシュメモリ上の文字列を、SRAM上文字列変数へコピーするためのものです。取得した文字列が配列変数「buf」に代入されます(ちなみにbufの30個は、拾う文字数を越えない程度の「適当」な数字です)。

そして、もうひとつ。2次元的な配列を扱う場合、配列用テーブルを用意する必要があるようです。

const char* const string_table[] PROGMEM = {
  str[0], str[1], str[2], str[3], str[4], str[5], str[6], str[7],
  str[8], str[9], str[10], str[11], str[12], str[13]
};

そして、そのテーブルを基に、ポインタの参照をするようにしているみたいです。

strcpy_P(buf, pgm_read_word(&(string_table[i])));

なんか回りくどい気もするんですが、strcpy_Pを使う場合、こう書かないと上手くいきませんでした。ここら辺はAVRでの書き方と仕組みも関わり、よく理解できていないので、このまま覚えるしかありません。どうしても気になる方はこちらで勉強するといいか、と。

ここまで参考サイトの内容と変わらないですが、今度は自分なりの工夫を盛り込んだサンプルコードを書いてみました。

先の数値を扱う方法を流用して、もう少しスッキリさせています。

#define STR_SIZE 14
#define CHR_SIZE 23
const char str[STR_SIZE][CHR_SIZE] PROGMEM = {
  "ARDUINO UNO", "ARDUINO 101", "ARDUINO PRO", "ARDUINO MICRO",
  "ARDUINO PRO MINI", "ARDUINO MEGA 2560", "ARDUINO ZERO",
  "LILYPAD ARDUINO USB", "LILYPAD ARDUINO SIMPLE", "ARDUINO YUN",
  "ARDUINO DUE", "ARDUINO LEONARDO", "ARDUINO MINI", "ARDUINO NANO"
};


void setup() {
  Serial.begin(9600);
  Serial.println("Charactors from PROGMEM");
}

void loop() {
  for (byte i = 0 ; i < STR_SIZE ; i++)
  {
    char buf[30];
    for (byte ii = 0 ; ii < CHR_SIZE ; ii++)
    {
      buf[ii] = pgm_read_byte(str[i] + ii);
    }
    Serial.println(buf);
  }
  while (1);
}

今までやってきた「数字を文字列として収納していく反復作業」と同じで分かりやすいし、テーブル配列を用意する必要もありません。ただし、大事なのは、配列のサイズ(STR_SIZE/CHR_SIZE)をしっかり設定しているところです。ここをハッキリしておかないと成立しません。

 

Arduinoのプログラム用フラッシュメモリ自体がさほどないので、このPROGMEMを必要とする状況もそんなに無いかもしれません。でも、冒頭でも書いたとおり、大量のデータをさばく時は非常に有益なので、こういう方法もあると知っておくと何かの折に役立つこともあるかと思います。

参考リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください