目次 [ Contents ]
Arduinoでは、スイッチやロータリーエンコーダなんかを使って変数の値を変更しても、本体をリセットすれば初期値に戻ってしまいます。変数を扱っているSRAMは、通電していないと記憶を保持出来ないからです。…なんですが、そんな一時的記憶ではなく、電源を切っても記憶してくれる領域があります。
それが、EEPROMです。
EEPROMライブラリ
Arduinoのメモリにはプログラムなどを記憶する「フラッシュメモリ」があり、通電していなくても消えることはありません。ただし、基本的にIDEからスケッチを流し込む時しか書き込み出来ないし、電源を入れると、その都度「プログラムを読み出す」のみです。
対してEEPROMは、ユーザーが自由に読み書き出来ます。そして、電源を落としても記憶は保持されます。自作フォローフォーカスでは、サーボモータの位置や、ステータスの保存などにこの機能を使いました。
ただし、容量はかなり小さくて、ArduinoUnoで1KB(キロバイト)しかありません。(UNOのスケッチ領域は32KB)。それでも1024バイト分の情報は保存できるので、何かと使い道があるかと思います。
EEPROMライブラリの使い方は、扱う数値がバイト型(0~255)であれば、結構簡単で、ライブラリをincludeし、read / write関数を呼び出すだけです。1バイトに対して、1アドレスなので、場所を指定して「値」を出し入れする感じで使います。
関数
EEPROMライブラリで扱える関数はこちらで解説されていますが、実はスケッチ例を見ると、もう少しあります。更には「CRCチェック」なるスケッチ例も用意されていたり…(難しいので割愛します)、まあ、気になる方は探ってみてください。
EEPROM.read( int address )
EEPROMに保存されているバイトデータを取得します。アドレスを指定すると、その中の数値が返り値として戻ってきます。
int val = EEPROM.read(0);
EEPROM.write( int address, byte value )
EEPROMに指定のバイトデータを書き込みます。引数には、書き込みたいアドレスと数値を入れます。
EEPROM.write (0, digitalRead(10));
EEPROM.length()
搭載されているEEPROMの容量を返します。Unoの場合1024(バイト)。
int eep_size = EEPROM.length(); Serial.println(eep_size);
EEPROM.get( int address, T &data )
EEPROMの値を取得します。readと違うのは「構造体」等を含めた1バイト以上のやり取りが、まとめて出来るところです(ただし注意点アリ)。
EEPROM.put( int address, const T &data )
writeと同じくEEPROMに値を書き込みます。これもget同様、「構造体」等を含めた1バイト以上のやり取りが、まとめて出来ます(ただし注意点アリ)。
EEPROM.update( int address, byte value )
指定したアドレスの値を確認し、(現状の)valueと違っていれば書き換えます。readとwriteを使った「更新自動化」的な関数です。
EEPROM [ int address ]
EEPROMを変数配列と同じ感覚で扱えます。read・writeと結果的には一緒ですが、表記的にはこっちの方が簡潔で便利です。
int val = 50; EEPROM [ 0 ] = val; // address「0」の保存値は50に int num = EEPROM [ 0 ]; // 変数numの値は50に
使い方
上記の関数を使った具体的なスケッチ例を書いていきます。シリアルモニタを使っていきますが、ボーレート(bps)は9600に設定してください。
EEPROMの中身をリスト表示
.length()で容量を取得し、それに沿って、全アドレスの数値をシリアルモニタへ一覧表示します。
#include <EEPROM.h> void setup() { Serial.begin(9600); } void loop() { int eep_size = EEPROM.length(); Serial.print("EEPROM SIZE:"); Serial.println(eep_size); for (int i = 0 ; i < eep_size ; i++) { byte val = EEPROM[i]; Serial.print("#"); // fill by 0 for (byte zro = 1; zro < 4; zro++) if (i < pow(10, zro)) Serial.print("0"); Serial.print(i); // address number Serial.print(":"); // fill by 0 for (byte zro = 1; zro < 3; zro++) if (val < pow(10, zro)) Serial.print("0"); Serial.print(val); // value if (((i + 1) % 10) == 0) Serial.println(); else Serial.print(" "); } while (1); }
綺麗に表示されるよう、改行と、文字数が足りない数字には0を埋めるようにしました。
1バイト内のやりとり
物理的なパーツを使って、数値の書き込み・読み出しを行います。
回路図
用意するもの
- Arduino UNO
- タクトスイッチ
- 可変抵抗器
- LED&抵抗
スケッチ例 1 :boolean型の保存
タクトスイッチでLEDのON/OFFを切り替えます。そして、その状態をEEPROMに保存。起動時に読み出すようにします。
#include <EEPROM.h> #define SW 7 #define LED 11 #define EEP_SW_ADRS 0 boolean sw_status = EEPROM.read(EEP_SW_ADRS); #define PUSH_SHORT 300 void setup() { Serial.begin(9600); pinMode(SW, INPUT_PULLUP); pinMode(LED, OUTPUT); Serial.println("SAMPLE Sketch : saving LED status"); SERIAL_RET(); } void loop() { unsigned long gauge; while (!digitalRead(SW)) gauge++; if (gauge > PUSH_SHORT) { sw_status = !sw_status; EEPROM.write(EEP_SW_ADRS, sw_status); Serial.println("Saving LED status!"); SERIAL_RET(); } digitalWrite(LED, sw_status); } void SERIAL_RET() { for (int i = 0 ; i < 10 ; i++) { byte val = EEPROM.read(i); Serial.print("#"); for (byte zro = 1; zro < 3; zro++) if (i < pow(10, zro)) Serial.print("0"); Serial.print(i); // address number Serial.print(":"); for (byte zro = 1; zro < 3; zro++) if (val < pow(10, zro)) Serial.print("0"); Serial.print(val); // value Serial.print(" "); } Serial.println(); }
リセットを押しても、LEDは前と同じ状態から始まります。
スケッチ例 2 :byte型の保存
可変抵抗器でLEDの明るさを調節。スイッチを押すと、その値をEEPROMに記憶。リセット後も同じ明るさになるようにします。
#include <EEPROM.h> #define VR A0 #define SW 7 #define LED 11 #define EEP_LUMA_ADRS 1 byte led_luma = EEPROM.read(EEP_LUMA_ADRS); #define PUSH_SHORT 300 void setup() { Serial.begin(9600); pinMode(VR, INPUT); pinMode(SW, INPUT_PULLUP); pinMode(LED, OUTPUT); for (byte i = 0 ; i < 10 ; i++) VR_READ(); Serial.println("SAMPLE Sketch : saving LED luma"); SERIAL_RET(); } void loop() { led_luma = map(VR_READ(), 0, 1013, 0, 255); unsigned long gauge; while (!digitalRead(SW)) gauge++; if (gauge > PUSH_SHORT) { EEPROM.write(EEP_LUMA_ADRS, led_luma); Serial.print("Saving the value! LED:"); Serial.println(led_luma); SERIAL_RET(); } analogWrite(LED, led_luma); } int VR_READ() { static int vr_val; for (byte i = 0 ; i < 20 ; i++) vr_val = vr_val * 0.9 + analogRead(VR) * 0.1; return vr_val; } void SERIAL_RET() { for (int i = 0 ; i < 10 ; i++) { byte val = EEPROM.read(i); Serial.print("#"); for (byte zro = 1; zro < 3; zro++) if (i < pow(10, zro)) Serial.print("0"); Serial.print(i); // address number Serial.print(":"); for (byte zro = 1; zro < 3; zro++) if (val < pow(10, zro)) Serial.print("0"); Serial.print(val); // value Serial.print(" "); } Serial.println(); }
1バイト以上のデータやりとり
上述ではEEPROMの1アドレスに1バイトまでの情報(0〜255)を扱っていたので、何の問題ありませんでした。しかし、255以上の数値を保存・読み出したい場合、このままでは不可能です。
と言っておきながら、知ってしまえばそんなに難しい話ではありません。単純に桁を分けて複数のアドレスに分ければ、数値自体はいくらでも入ります。1526だったら、1つを「15」、1つを「26」と別々に保存すれば収まります。自作フォローフォーカスでもそういう感じで255以上の数値を記憶していました。
ただ、今回はもう少しスマートに、ビットシフトとビットマスクを使います。
Byteデータへ自力分割
変数は型によって扱える数値範囲が変ってきますが、これはバイト数を増やしているからです。つまり、下記の個数分アドレスを確保すれば、大きな値も保存できるようになります。
型 | バイト数 | 扱える数値(Arduino Uno) |
byte, char | 1 byte | 0 ~ 255, -128 ~ 127 |
int | 2 byte | -32768 ~ 32767 |
long | 4 byte | -2147483648 ~ 2147483647 |
そこで、ビット演算子を用い、複数のEEPROMアドレスへ分離し保存・読み出しをしてくれる関数を作ります。基点になるアドレス、値、そして型のバイト数を投げればいいだけにします。
void EEPROM_SAVER(int address, long val, byte val_type) { for (byte i = 0 ; i < val_type ; i++) { EEPROM[address + i] = (val >> (i * 8)) & B11111111; } }
「&」演算子は2つのビット値を比較し、共に1の場合のみ1、どちらかが0なら0に置き換わります。例えば、値Aに「187」、値Bに「94」があって「&」演算するとします。187は2進数でB10111011、94はB01011110なので、
数値A (187) | 1 | 0 | 1 | 1 | 1 | 0 | 1 | 1 |
数値B(94) | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 0 |
「&」の結果 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 0 |
これを利用して、上述のようにB1111111で括ってやると、結果的に下8桁のみを生かした状態、つまり下1バイト(8ビット)分だけが抜き出せるわけです。数値Aが2バイトだとしたら、
2バイトの数値A | B11101011 10010101 |
1バイトの数値B | B11111111 |
&の結果 | B |
「>>」演算子は、指定した分だけ2進数の桁をシフトします。つまり、上の”EEPROM_SAVER”のコードは(2進数を)8桁ずつ繰り下げて、下8桁のバイナリだけ抜き出す作業を繰り返しているだけです。
読み出しは、逆に繰り上げつつ足し算すれば、簡単に戻ります。
long EEPROM_LOADER(int address, byte val_type) { long ret_val = 0; for (byte i = 0 ; i < val_type ; i++) { ret_val += EEPROM[address + i] << (i * 8); } return ret_val; }
ご覧の通りfloatには対応してません。
これを使い、Sample3の可変抵抗器の値(0~1013)も保存・読み出し出来るようにしてみました。
#include <EEPROM.h> #define VR A0 #define SW 7 #define LED 11 #define EEP_LUMA_ADRS 1 #define EEP_VR_ADRS 2 byte led_luma = EEPROM.read(EEP_LUMA_ADRS); #define PUSH_SHORT 300 void EEPROM_SAVER(int address, long val, byte val_type) { for (byte i = 0 ; i < val_type ; i++) { EEPROM[address + i] = (val >> (i * 8)) & B11111111; } } long EEPROM_LOADER(int address, byte val_type) { long ret_val = 0; for (byte i = 0 ; i < val_type ; i++) { ret_val += EEPROM[address + i] << (i * 8); } return ret_val; } void setup() { Serial.begin(9600); pinMode(VR, INPUT); pinMode(SW, INPUT_PULLUP); pinMode(LED, OUTPUT); Serial.println("SAMPLE Sketch : saving LED luma & VR"); Serial.print("loaded Luma:"); Serial.print(EEPROM_LOADER(EEP_LUMA_ADRS, 1)); Serial.print(" loaded VR:"); Serial.println(EEPROM_LOADER(EEP_VR_ADRS, 2)); SERIAL_RET(); } void loop() { int temp_vr = VR_READ(); led_luma = map(temp_vr, 0, 1013, 0, 255); unsigned long gauge; while (!digitalRead(SW)) gauge++; if (gauge > PUSH_SHORT) { EEPROM_SAVER(EEP_LUMA_ADRS, led_luma, 1); EEPROM_SAVER(EEP_VR_ADRS, temp_vr, 2); Serial.print("Saving the values! LUMA:"); Serial.print(led_luma); Serial.print(" VR:"); Serial.println(temp_vr); SERIAL_RET(); } analogWrite(LED, led_luma); } int VR_READ() { static int vr_val; for (byte i = 0 ; i < 20 ; i++) vr_val = vr_val * 0.9 + analogRead(VR) * 0.1; return vr_val; } void SERIAL_RET() { for (int i = 0 ; i < 10 ; i++) { byte val = EEPROM.read(i); Serial.print("#"); for (byte zro = 1; zro < 3; zro++) if (i < pow(10, zro)) Serial.print("0"); Serial.print(i); // address number Serial.print(":"); for (byte zro = 1; zro < 3; zro++) if (val < pow(10, zro)) Serial.print("0"); Serial.print(val); // value Serial.print(" "); } Serial.println(); Serial.println(); }
put・getを使う
と、ここまで書いておいてなんですが、実はput・getを使うと、2バイト以上の数値も1発でやりとりできます。
こちらの関数は、投げた値のバイト数を自動で判別し、EEPROMのアドレスへ振り分けて保存・読み出ししてくれます。
EERPOM.put (int address, data);
EERPOM.get (int address, data);
putは「data」に入った型を判別し、それに即した分割でEEPROMに書き込みます。getは代入先となる「data」の型に合わせて、EEPROMの配列を復元し、そのまま代入してくれます。
以下のサンプルスケッチでは、前回の「シリアルモニタから数値送信」を利用してみます。シリアルモニタの入力フィールドから好きな数字を送信してください。その後、リセットしてもその数値を表示します。
#include <EEPROM.h> #define EEP_VAL_ADRS 5 long val; void setup() { Serial.begin(9600); EEPROM.get(EEP_VAL_ADRS, val); Serial.print("loaded value:"); Serial.println(val); } void loop() { if (KICK_SERIAL()) { val = SERIAL_VAL(); EEPROM.put(EEP_VAL_ADRS, val); Serial.print("saved value:"); Serial.println(val); } } bool KICK_SERIAL() { bool flag = false; if (Serial.available() > 0) { flag = true; delay(20); } return flag; } long SERIAL_VAL() { byte data_size = Serial.available(); byte buf[data_size], degree = 1; long recv_data = 0, dub = 1; bool minus = 0; for (byte i = 0 ; i < data_size ; i++) { buf[i] = Serial.read(); if (buf[i] >= '0' && buf[i] <= '9') buf[i] -= '0'; else { if (buf[0] == '-') minus = 1; else degree = 0; } } if (degree == 1) degree = data_size - minus; for (byte i = 0 ; i < degree ; i++) { recv_data += buf[(data_size - 1) - i] * dub; dub *= 10; } if (minus) recv_data *= -1; return recv_data; }
また、この関数は構造体になっている複数の変数もまとめて処理してくれます。
以下は再び、タクトスイッチと可変抵抗器を使った数値保存のサンプルです。構造体でEEPROMのくだりが大分スッキリしています。
#include <EEPROM.h> #define VR A0 #define SW 7 #define LED 11 #define EEP_LUMA_ADRS 0 #define PUSH_SHORT 300 struct Obj { byte luma; int vr; }; Obj status_val = {0, 0}; void setup() { Serial.begin(9600); pinMode(VR, INPUT); pinMode(SW, INPUT_PULLUP); pinMode(LED, OUTPUT); for (byte i = 0 ; i < 10 ; i++) VR_READ(); EEPROM.get(EEP_LUMA_ADRS, status_val); Serial.print("loaded luma:"); Serial.print(status_val.luma); Serial.print(" loaded VR:"); Serial.println(status_val.vr); } void loop() { status_val.vr = VR_READ(); status_val.luma = map(status_val.vr, 0, 1013, 0, 255); unsigned long gauge; while (!digitalRead(SW)) gauge++; if (gauge > PUSH_SHORT) { EEPROM.put(EEP_LUMA_ADRS, status_val); Serial.print("Saving the values! LUMA:"); Serial.print(status_val.luma); Serial.print(" VR:"); Serial.println(status_val.vr); } analogWrite(LED, status_val.luma); } int VR_READ() { static int vr_val; for (byte i = 0 ; i < 20 ; i++) vr_val = vr_val * 0.9 + analogRead(VR) * 0.1; return vr_val; }
アドレス指定の注意点
2バイト以上のデータをやり取りできる方法を説明してきましたが、複数のアドレスにまたがって保存するようなデータはアドレスに注意する必要があります。指定をちょっと間違えただけで読み書きが上手く機能しなくなります。
特にput・getは、型のサイズが分からなくても扱えてしまうので、うっかり他の領域に上書きしてしまうミスも起こりやすいです。
#include <EEPROM.h> #define EEP_ADRS1 0 long data_1; int data_2; byte data_3; void setup() { Serial.begin(9600); EEPROM.get(EEP_ADRS1, data_1); EEPROM.get(EEP_ADRS1 + 4, data_2); EEPROM.get(EEP_ADRS1 + 2, data_3); Serial.print("data_1:"); Serial.print(data_1); Serial.print(" data_2:"); Serial.print(data_2); Serial.print(" data_3:"); Serial.println(data_3); } void loop() { data_1 = 777777; data_2 = 5555; data_3 = 111; EEPROM.put(EEP_ADRS1, data_1); EEPROM.put(EEP_ADRS1 + 4, data_2); EEPROM.put(EEP_ADRS1 + 2, data_3); while (1); }
リセットしてもloop内で代入した通りの値は返ってきません。こういったミスは、構造体のように型違いの変数がまとめて扱われていると、更にこんがらがってきます。
自分はこういったアドレス指定の煩雑さには、define定義とsizeof()関数でアドレス指定を決めてしまうようにしています。上のスケッチを基にするなら、
#include <EEPROM.h> #define EEP_ADRS1 0 long data_1; #define EEP_ADRS2 EEP_ADRS1 + sizeof(data_1) int data_2; #define EEP_ADRS3 EEP_ADRS2 + sizeof(data_2) byte data_3; void setup() { Serial.begin(9600); EEPROM.get(EEP_ADRS1, data_1); EEPROM.get(EEP_ADRS2, data_2); EEPROM.get(EEP_ADRS3, data_3); Serial.print("data_1:"); Serial.print(data_1); Serial.print(" data_2:"); Serial.print(data_2); Serial.print(" data_3:"); Serial.println(data_3); } void loop() { data_1 = 777777; data_2 = 5555; data_3 = 111; EEPROM.put(EEP_ADRS1, data_1); EEPROM.put(EEP_ADRS2, data_2); EEPROM.put(EEP_ADRS3, data_3); while (1); }
こう書けば、途中の変数の型が変っても対応できますし、EEP_ADRS1を変更すれば、相対的に他のアドレスも変ってくれるので便利です。
最後に、EEPROMをシリアルモニタとのやりとりで整理整頓出来るユーティリティスケッチを描いてみたんですが、長くなるので別記事にしました。
シリアルモニタの入力フィールドからの送信で、リスト表示、指定データの書き換え、全データリセット、等が出来ます。お役立てください。
参考リンク