AVR Libc Home Page | ![]() |
AVR Libc Development Pages | |||
Main Page | User Manual | FAQ | Library Reference | Additional Documentation | Example Projects |
あなたは自分の持ち物を部屋の外に保管したいですか?
(※意訳 So you have some constant data and
you're running out of room to store it? )
多くのAVRデバイスは、データ保管用のRAM搭載量が少なく、制限されています。
しかし、RAMだけではなく、広いフラッシュメモリスペースがあります。
AVRはハーバードアーキテクチャプロセッサなので、フラッシュはプログラム用に、RAMはデータ用に使われます。
両者は別々のアドレス空間に置かれています。
ただし、定数データをプログラムスペースに置いたり、AVRアプリケーションがそれを引き出すのはちょっとやっかいです。
C言語はハーバードアーキテクチャプ向けに作られていないことが問題を悪化させます。
C言語は、コードとデータが同じアドレス空間に置かれるフォンノイマンアーキテクチャ向けに作られています。
このことは、AVRのようなハーバードアーキテクチャプロセッサ用のコンパイラは、何か別の手段で分離されたアドレス空間を操作しなければならないことになります。
ある種のコンパイラは非標準C言語キーワードを用いたり、標準文法を非標準の方法で拡張したりしています。
しかし、AVRツールセットは別のアプローチを取ります。
GCCは特別なキーワード __attribute__
を持っています。
これは関数宣言や変数や変数型などに異なる属性をつけることができます。キーワードにつづいて属性の明細が二重引用符でくくって置かれます。
※ __attribute__ "hogehoge"
の形式
AVR GCCでは、この場合 progmem という特別な属性を指定します。この属性はデータ宣言に用いられ、コンパイラにこのデータをプログラムメモリ(Flash)に置くよう指示します。
AVR-Libc は簡単なマクロ PROGMEM
を用意しています。
これはprogmem属性をつけるものです。
このマクロは、この後紹介するように、エンドユーザーにも簡単に扱えます。
PROGMEMマクロはシステムヘッダファイル <avr/pgmspace.h>
で定義しています。
GCCを改変してC言語に新たな拡張を加えるのは大変です。
そのかわりにavr-libcが、プログラムメモリからデータを引き出すマクロを提供しています。
これらのマクロはシステムヘッダファイル <avr/pgmspace.h>
で定義しています。
多くのユーザーがCのキーワード "const"
を使えば、プログラムスペースにデータを宣言できると考えるでしょう。
しかし、これはconstキーワードの意図に反しています。
const は、コンパイラに対し、このデータは読み出し専用であることを知らせるためのものでした。
これはコンパイラに確実な変換をさせたり、誤った変数の使い方をコンパイラがチェックするのに役立っています。
たとえば、const キーワードは一般的に多くの関数にパラメータタイプの修飾子として用いられています。
これはその関数がパラメータを読み出し専用として使い、パラメータを変更しないことをコンパイラに伝えます。
※ 文字列をアドレスで渡す場合などによく使われる。たとえば、int puts(const char *s); のように。(アドレス値sを変更されたら困る)
constはこのように使うことを想定されたもので、データをどこに置くべきかを指定するものではありません。
もしconstキーワードがデータ保存場所の決定に使われるのなら、関数パラメータなどでの本来の役割を失ってしまいます。
※ constは変更不可変数の意味で使われているのですから、データの置き場所指定は別の方法じゃないとだめです
グローバルデータを1つ上げて考えてみましょう
unsigned char mydata[11][10] = { {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09}, {0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13}, {0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0x1C,0x1D}, {0x1E,0x1F,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27}, {0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30,0x31}, {0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B}, {0x3C,0x3D,0x3E,0x3F,0x40,0x41,0x42,0x43,0x44,0x45}, {0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F}, {0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59}, {0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x61,0x62,0x63}, {0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D} };
後に、このデータは関数内などでアクセスし、その中の1要素を他の変数に保存することができます。
byte = mydata[i][j];
さて、このデータをプログラムメモリに置いてみましょう。
<avr/pgmspace.h>
で定義されたPROGMEMマクロを、変数宣言の後、初期値の前に書きます。
こんな感じです。↓
#include <avr/pgmspace.h> . . . unsigned char mydata[11][10] PROGMEM = { {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09}, {0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13}, {0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0x1C,0x1D}, {0x1E,0x1F,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27}, {0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30,0x31}, {0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B}, {0x3C,0x3D,0x3E,0x3F,0x40,0x41,0x42,0x43,0x44,0x45}, {0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F}, {0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59}, {0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x61,0x62,0x63}, {0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D} };
これです!これであなたのデータはプログラムスペースに置かれました。
これでコンパイル、リンクして、mapfileをチェックすれば定数mydataが正しいセクションに置かれていることが分かります。
データはプログラムスペースに置かれていますので、通常の読み込み手段ではデータは読み取れません。
普通に mydata[i][j]
をアクセスすると、mydata配列が置かれたアドレス+引数i,j
によって決まるオフセットで決まるアドレスを生成します。
しかしながら、この計算された最終的なアドレスは(SRAM上の)データスペースを指し示しているのです!
残念ながら実際にデータが置かれている(FLASH上の)プログラムメモリスペースではありません。
どうにかしなくてはなりません。問題は、AVR
GCCが、データがプログラムスペース内にあるということを知らない、と言うことにあります。
※机の本棚が狭いので、机の本棚の2段目に置く代わりに、書斎本棚の2段目に本を置いたのですが、
※本を探す人が書斎本棚の存在を知らず、机の本棚しか知らないなら、本が置かれていない机の本棚2段目3番目を探してしまいます。
解決法は簡単です。プログラムスペースからデータを引き出す"rule
of thumb"(経験則)は、データスペースのデータを引き出すのと同じいつものやり方から導き出されます。
ですThe solution is fairly simple. The "rule
of thumb" for accessing data stored
in the Program Space is to access the data
as you normally would (as if the variable
is stored in Data Space), like so:
byte = mydata[i][j];
データのアドレスを引き出してみましょう
byte = &(mydata[i][j]);
このアドレス&(mydata[i][j])
を pgm_read_*
マクロの引数にすればよいです。
byte = pgm_read_byte(&(mydata[i][j]));
pgm_read_*
マクロは、プログラムスペース内のアドレスを引数として、そのアドレスのデータをプログラムメモリから引き出します。
そのために一度アドレスの形式にしなければいけなかったわけです。アドレスはこのマクロの引数となり、マクロはプログラムメモリからデータを引き出す正しいコードを生成します。
引き出すデータのサイズに応じて、様々な種類のpgm_read_*
があります。
※pgm_read_byte、pgm_read_word、pgm_read_long
・・・・・
単独データをプログラムメモリへセットしてそれを引き出すことはできました。
今度は文字列の配列をプログラムスペースに保存読みだししたいでしょう。
※ ここで扱うのは、文字列(文字の配列)ではなく、文字列の配列です。
※ C言語では文字列はcharの配列と同じですから、単に以下のようにすればいいです。
※ char mystring[] PROGMEM = "String 1";
※ その文字列の利用法は後述するプログラムメモリ用文字列関数群を使うことになります
では、文字列配列にトライしましょう!こんな感じでしょうか?
char *string_table[] = { "String 1", "String 2", "String 3", "String 4", "String 5" };
これに例によって PROGMEM マクロを宣言の末尾につけます。
char *string_table[] PROGMEM = { "String 1", "String 2", "String 3", "String 4", "String 5" };
これでいいですか? いいえ、これ間違いなんです!
残念なことに、GCCの属性指定はその宣言名(変数名)にしか効きません。
そのためこの場合では、string_table
(文字列先頭アドレスを収容する配列)は確かにプログラムスペースに保存できます。
しかしstring_table
が指し示す先の文字列についてはプログラムスペースには置かれず、データスペースに置かれているのです。
これはあなたの望むところではないでしょう?
文字列自身をプログラムスペースに納めるには、明示的に各文字列について宣言し、プログラムスペースに納めてやらなければいけません。
char string_1[] PROGMEM = "String 1";
char string_2[] PROGMEM = "String 2";
char string_3[] PROGMEM = "String 3";
char string_4[] PROGMEM = "String 4";
char string_5[] PROGMEM = "String 5";
ここで新しいシンボルをあなたの文字列テーブルに使ってみましょう。こんな感じです。
PGM_P string_table[] PROGMEM = { string_1, string_2, string_3, string_4, string_5 };
PGM_P
には、この文字列テーブルをプログラムスペースに置き、この文字列テーブルはこれまたプログラムスペース内に置かれた文字列を指し示すポインタの配列であることを宣言する効果があります。
PGM_P
型はプログラムスペース内の文字変数に対するポインタを宣言するための型となります。
文字列を引き出すのはまた別の方法となります。pgm_read_byte()
マクロで1バイトずつ引っ張り出して使うなんてことはしたくないでしょう。
<avr/pgmspace.h> には、プログラムスペース内に保存された文字列をうまく使ってくれる関数群が納められています。
たとえば、プログラムスペース内文字列をSRAM内のバッファ(ローカルスコープ変数のように必要に応じてスタック上に確保される)にコピーしたい場合、こんな感じで行けます。
void foo(void) { char buffer[10]; for (unsigned char i = 0; i < 5; i++) { strcpy_P(buffer, (PGM_P)pgm_read_word(&(string_table[i]))); // Display buffer on LCD. } return; }
配列 string_table
はプログラムスペース内に保存されています。そのため、普通にアクセスするとデータスペースに探しに行ってしまいます。そこで、まずアドレスを&で得て、それをpgm_read_word
マクロに与えて、アドレス値を取り出します。ポインタは16bit値、つまりword
sizeなのでpgm_read_word
を使うわけです。pgm_read_word
は16bit unsigned整数を返します。そしてこれをプログラムスペース内メモリ用ポインタ型であるPGM_P
に型変換します。これによりこのポインタはコピーしたいと考えているプログラムスペース内のアドレスとなります。関数strcpy_P
は標準文字列関数 strcpy
と同じように働きます。ただ、プログラムスペース(2番目のパラメータ)から文字列をデータエリアのバッファ(1番目のパラメータ)へコピーすることが違います。
プログラムスペースに置かれた文字列に対して働くたくさんの関数があります。これら関数は全て関数名の末尾に
_P
がつけられており、<avr/pgmspace.h> ヘッダファイルで宣言されています。
このマクロや関数達はプログラムスペースからデータを引き出すもので、この操作のためにいくらか余分なコードを生成します。
これはコードサイズは実効速度に関していくらかのオーバーヘッドを生みます。通常、コードサイズと速度のオーバーヘッドは、プログラムスペースにデータを置くことによる節約効果に比べるとたいしたことはありません。しかし、プログラムスペースからデータを引き出すために1つの関数から繰り返し呼び出したりすることを極力控えるように注意してください。コンパイラの結果をディスアセンブルリストで見返しチェックすることは有益なことになります。
※基本的に、プログラムスペースに置く定数は、文字列や配列などSRAMに移すには大きすぎるもの、あちこちから繰り返し参照されるものに向いています。
※一カ所(同一関数内など)で何度も繰り返し参照される定数は、一度ローカル変数にコピーしてから使うのが賢明です。
※また、単発で使う単独の定数は、#defineで宣言して、必要時適切なローカル変数に即値代入した方がずっと簡潔です。
※SRAMメモリが余っていて、数バイト程度の定数なら、const宣言で確保しても構いません(FLASHメモリからSRAMにコピーしてから使用される)。その方がソースが簡潔です。