PROGMEM の解説と実装リファレンス

記事内には広告が含まれています。

PROGMEM の解説と実装リファレンス

【概要】

PROGMEM は、Arduino(特にAVRマイコンアーキテクチャ)において、変数や配列をSRAM(静的メモリ)ではなく、Flashメモリ(プログラムメモリ)に配置するためのマクロです。

通常、グローバル変数や静的な配列はコンパイル時に初期化され、実行時にはSRAMに展開されます。しかし、AVRマイコンのSRAMは非常に限られているため、大きなルックアップテーブル、フォントデータ、あるいは巨大な定数配列をSRAMに配置するとメモリ不足を引き起こします。PROGMEM を使用することで、これらのデータをFlashメモリ上に保持したまま参照することが可能になります。

【仕様】

  • 型: マクロ(__attribute__((PROGMEM)) をラップしたもの)。
  • 対象: 変数、配列、構造体など。
  • 動作原理: リンカに対して、指定されたシンボルをデータセクションではなくプログラムメモリセクションへ配置するよう指示します。
  • アクセス方法: PROGMEM で定義されたデータは、通常のポインタ参照やインデックスアクセス(例:array[i])では取得できません。アクセスには pgm_read_...() 系関数を使用する必要があります。

【技術的特徴】

  • Harvard Architecture: AVRマイコンはハーバードアーキテクチャを採用しており、命令メモリとデータメモリが分離されています。PROGMEM はこのアーキテクチャを最大限に活用する仕組みです。
  • メモリ節約: SRAMの消費を抑え、より複雑なアルゴリズムやリッチなUI(文字表示など)を実装可能にします。
  • アーキテクチャ依存: PROGMEM は主にAVR(Uno, Nano, Mega等)向けの実装です。ARM Cortex-M(Arduino Due, Zero等)やESP32などの一部のプラットフォームでは、メモリマップが統合されているため、PROGMEM を指定しても挙動が変わらない、あるいは無視される場合があります。

【注意点】

  1. 直接参照の禁止:
    PROGMEM を付与した配列に対して value = my_array[i]; と記述すると、コンパイラはSRAMからの読み出しを想定するため、誤った値を読み取るか、実行時エラーを引き起こします。必ず pgm_read_byte()、pgm_read_word()、pgm_read_dword() を使用してください。
  2. データサイズ:
    読み出すデータの型(byte, word, dword)に合わせて適切な関数を選択する必要があります。選択を誤ると、ビットのずれや不正確な値の取得が発生します。
  3. リソース制約:
    Flashメモリ自体にも容量制限があるため、非常に巨大なデータは外部ストレージ(SDカード等)への移行を検討する必要があります。
  4. ポインタの扱い:
    PROGMEM 内にある構造体へのポインタを扱う場合、ポインタ自体を PROGMEM にするか、ポインタが指す先をどう扱うかについて慎重な設計が必要です。

【サンプルコード】

以下は、SRAMを消費せずにサイン波のルックアップテーブルをFlashメモリに保持する実装例です。

#include <avr/pgmspace.h>

// Flashメモリに配置されるルックアップテーブル
const uint16_t sine_table[] PROGMEM = {
  0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 
  4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 6656, 6912, 7168, 7424, 7680, 7936, 8192 
};

void setup() {
  Serial.begin(9600);

  // 間違ったアクセス例(SRAMを想定しているため正しく動作しない)
  // uint16_t val = sine_table[10]; 

  // 正しいアクセス例(pgm_read_wordを使用)
  for (int i = 0; i < 32; i++) {
    uint16_t val = pgm_read_word(&(sine_table[i]));
    Serial.print("Index ");
    Serial.print(i);
    Serial.print(" : ");
    Serial.println(val);
  }
}

void loop() {
  // 何もしない
}

【活用例】

  • フォント描画: LCDやOLEDディスプレイに表示するための数千ピクセルのフォントデータをFlashに格納する。
  • 信号処理: 複雑な波形やフィルタ係数のルックアップテーブルをメモリ消費なしで保持する。
  • ステートマシン: 膨大な数の遷移先やパラメータを持つ状態遷移テーブルの管理。
  • 通信プロトコル: 固定のバイナリメッセージテンプレートや、通信用コマンドのヘッダー情報の保持。

コメント

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