ArduinoにおいてFlashに格納したオブジェクトを読み込むクラス
こんにちは、スタッフの高木です。連休も今日で最後なので、もう一記事だけ技術情報を書くことにします。
今回はArduinoに関する話題です。正確にはArduinoではなくAVRに関する話題ですね。Arduino DueなんかのAVR以外のマイコンを搭載したボードには通用しませんから。
本題に入る前に、Arduinoについて簡単に紹介することにします。Arduinoは、主にAVRを用いたマイコンボードと、C++ベースのArduino言語およびそのための統合開発環境から構成されています。組込み開発の初心者でも(多くの場合、プログラミングや電子工作の初心者でも)比較的簡単にマイコンプログラミングや電子工作を行えるようになっています。
Arduinoに関連する回路図やソースコードはすべてオープンになっていますので、誰でも自由に利用することができます。また、入出力ポートのピン配置や機械的特性(寸法など)も概ね規格化されていますので、Arduinoに対応するシールド(拡張ボード)がいろいろ販売されています。
そんなArduinoで主に使われているマイコンがAtmel AVRです。AVRは8ビットのRISCベースのマイコンで、PICと同様、ハーバード・アーキテクチャを採用しているのが特徴です。ハーバード・アーキテクチャというのは、最近のプロセッサの主流であるフォンノイマン・アーキテクチャとは異なり、プログラムとデータが別々のアドレス空間を持っています。フォンノイマン・アーキテクチャのプロセッサでも、キャッシュは命令キャッシュとデータキャッシュに分かれていたりするので、その場合はフォンノイマンとハーバードの折衷型だと考えることもできるでしょう。
さて、ここからが本題です。AVRはハーバード・アーキテクチャですので、プログラムとデータのアドレス空間が分離されています。プログラムはFlashメモリに、データはRAMに配置されるわけですが、Flashメモリは数十kバイト以上あるのに対して、RAMは2kバイト程度しかありません。
固定データは、テーブルなどの形で、できればFlashメモリに配置したいのですが、Flashメモリは原則としてプログラムを配置する領域ですので、そうはいかないのです。フォンノイマン・アーキテクチャのマイコンであれば、const修飾子を付けて定義した静的オブジェクトはROMやFlashメモリに配置されるのが普通ですから、これはかなり大きな制約になります。
何とかしてFlashメモリに固定データを配置したいと考えるのはみんな同じで、PROGMEMというマクロを使えばデータをFlashメモリに配置することができます。
1 | PROGMEM int a = 123; |
これでFlashメモリに配置することはできます。しかし、オブジェクトの型はRAMに配置した場合と同じですので、そのままオブジェクトにアクセスしようとしても、あさっての方向のメモリを読み込もうとしてしまいます。これを回避するには、pgm_read_byteなどの専用のマクロを使う必要があります。上記の場合はint型ですので、
1 | int t = pgm_read_word(&a); |
のようにしなければなりません。これは非常に面倒ですし、オブジェクトの型をハードコーディングしないといけなくなるのも問題です。第一、静的な型チェックがまったく行われないので非常に間違いやすいのです。この問題を多少なりとも軽減するために、int型ではなくprog_int16_t型という独自の型を使うことを推奨しているようですが焼け石に水でしょう。これなら、オブジェクト名にシステムハンガリアンを用いた方がまだましかもしれません。
Cの場合はせいぜい型名やオブジェクト名を工夫する程度の対策しかできませんが、Arduino言語は実質的にはC++ですので、もう少し工夫の余地があります。つまり、Flashメモリに配置したオブジェクトにアクセスするためのクラスを定義して、そのクラス型のオブジェクトを介してメモリアクセスするようにすれば、うっかりミスはほとんど排除することができるでしょう。
Flashメモリに配置したオブジェクトへのアクセスを行うクラスはpgm_ptrと名付けることにしましょう。このpgm_ptrクラスを実装する前に、下請け用の関数を定義しておくことにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | template <size_t N> inline void* pgm_read(void* ram, uintptr_t pgm) { return memcpy_P(ram, reinterpret_cast<PGM_VOID_P>(pgm), N); } template <> inline void* pgm_read<1>(void* ram, uintptr_t pgm) { *static_cast<uint8_t*>(ram) = pgm_read_byte(pgm); return ram; } template <> inline void* pgm_read<2>(void* ram, uintptr_t pgm) { *static_cast<uint16_t*>(ram) = pgm_read_word(pgm); return ram; } template <> inline void* pgm_read<4>(void* ram, uintptr_t pgm) { *static_cast<uint32_t*>(ram) = pgm_read_dword(pgm); return ram; } |
標準で用意されているマクロは、オブジェクトの型によって名前が変わるため、シンタックスに依存するC++のテンプレートでは非常に使いにくいものでした。上記のようにしておけば、オブジェクトのサイズさえテンプレート引数で渡せば、どんな型でも扱うことができます。ただし、POD型(C互換型)に限りますが…。
下請け関数を用意したので、実際のpgm_ptrクラスの実装に取りかかりましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | template <class T> class pgm_ptr { public: explicit pgm_ptr(const T* pgm_addr) : address_(reinterpret_cast<uintptr_t>(pgm_addr)) { } const T operator*() const { T temp; pgm_read<sizeof(T)>(&temp, this->address_); return temp; } uintptr_t get() const { return this->address_; } private: uintptr_t address_; }; |
上記のpgm_ptrクラスは、ごく基本的な機能しか備えていません。実用的なクラスにするには、Safe-boolイディオムを使ってアドレスがNULLかどうかを判定させたり、増分・減分演算子や等価・関係演算子などを追加する必要があるでしょう。
配列を扱うことを考えると、添え字演算子も定義した方がよいでしょうね。構造体を扱うことを考えると矢印演算子も定義したいところですが、矢印演算子を実現するにはpgm_ptr内部に構造体の内容をキャッシュしておかざるを得ず、小さなRAMしか持たないAVRないしはArduinoの事情を考えるとあまり適切ではないかもしれません。
なお、今回実装したpgm_ptrクラスのようにテンプレートを用いた関数やクラスの定義は、Arduino言語で直接記述することができません。Arduino IDEではC++のソースファイル(ヘッダファイルを含む)を記述することができますので、今回の場合はC++のヘッダファイルでpgm_ptrの定義を行い、そのヘッダファイルをArduino言語のソースコードからインクルードするのがよいでしょう。
Arduino自体は、デザイナーなどのノンプログラマーでも比較的簡単にプログラミングができるようになっています。しかし、ちょっと凝ったことをやるには、あるいは効率よくプログラミングできる仕掛けを作るには、やはりマイコンやC++に関して十分な知識を持ったプログラマーでなければ難しいのではないかと思います。