Arduino EEPROMの使い方:read、write、update、put、getで読み書き
Arduino EEPROMを解説します。Unoの1024バイト、read、write、update、put、get、100,000回の書き込み寿命、カウンターやパスワードの保存、ESP32のPreferencesまで。
Arduino EEPROMは、ボードの電源が切れても内容を保持する小さな不揮発性メモリです。Arduino Uno、Nano、Pro MiniのATmega328Pには1024バイトのEEPROMがあり、Arduino IDEに標準で付属するEEPROMライブラリのEEPROM.read()、EEPROM.write()、EEPROM.update()、EEPROM.put()、EEPROM.get()でアクセスします。変数を保持するRAMは再起動のたびに消えますが、EEPROMに書き込んだ設定やカウンター、パスワードは、次に電源を入れたときもそのまま残っています。
このガイドでは、EEPROMのしくみ、ボードごとの容量、命令の違い、そして誰もが気になる「約100,000回の書き込み寿命」が実際に何を意味するのかを説明します。続いて、停電しても値を覚えているカウンター、パスワードとオプションを一度に保存する設定用の構造体、書き込み寿命を16倍にするリングバッファを、ステップごとに作ります。最後のセクションでは、本物のEEPROMを持たないESP32ではPreferencesライブラリで、Raspberry Pi PicoではMicroPythonのファイルシステムで、同じことをする方法を学びます。

EEPROMとは?そのしくみ
EEPROMはElectrically Erasable Programmable Read-Only Memory(電気的に消去・書き込みが可能な読み出し専用メモリ)の略です。各メモリセルは、絶縁されたゲートの中に閉じ込めた電荷として情報を保存し、この電荷は電源が切れても残ります。UnoのEEPROMは1024個の引き出しがある棚だと考えてください。各引き出しには0から1023までのアドレスがあり、ちょうど1バイト、つまり0〜255の数を1つ入れられます。一度も書き込んでいないセルは255(0xFF)と読めます。読み出しはほぼ一瞬で回数の制限もありませんが、書き込みは1バイトあたり約3.3 msかかり、セルを少しずつ消耗させます。ATmega328Pのデータシートでは、EEPROMの各セルについて少なくとも100,000回の書き込み・消去サイクルと、25 °Cで100年のデータ保持が規定されています。
| 特徴 | フラッシュ(プログラムメモリ) | SRAM | EEPROM |
|---|---|---|---|
| Unoでの容量 | 32 KB(うち0.5 KBはブートローダー) | 2 KB | 1 KB(1024バイト) |
| 何を保存する? | 書き込んだスケッチ | 変数、配列 | 設定、カウンター、ハイスコア |
| 電源が切れると | 保持される | 消える | 保持される |
| スケッチ実行中に書き込める? | 通常は不可(PROGMEMで読み出しのみ) | 可、無制限 | 可、EEPROMライブラリで |
| 書き込み寿命 | 約10,000回 | 無制限 | セルあたり約100,000回 |
| 書き込み速度 | スケッチの書き込み時のみ | 1クロックサイクル | 1バイトあたり約3.3 ms |
つまり、SRAMはホワイトボード、EEPROMはノートです。ホワイトボードには何度でも書いて消せますが、明かりが消えるとすべてなくなります。ノートに書いたことは残りますが、同じページを永遠に消して書き直すことはできません。そのためEEPROMは、キャリブレーション値、ユーザー設定、明るさや音量、ゲームのハイスコア、ドアの暗証番号、登録したRFIDカード、ランプの最後の状態など、小さくてめったに変わらない情報に向いています。1秒ごとに記録する温度ログのような、大きくて頻繁に書き込むデータにはSDカードモジュールのほうが適しています。RAMがすぐにいっぱいになる理由が気になる場合は、Arduinoのメモリ不足を読んでください。
ボードごとのEEPROMの容量
| ボード | プロセッサ | EEPROM | メモ |
|---|---|---|---|
| Arduino Uno R3, Nano, Pro Mini | ATmega328P | 1024バイト | 本物のEEPROM |
| Arduino Mega 2560 | ATmega2560 | 4096バイト | 本物のEEPROM |
| Arduino Leonardo, Micro | ATmega32U4 | 1024バイト | 本物のEEPROM |
| Arduino Nano Every | ATmega4809 | 256バイト | 本物のEEPROMだが小さい |
| Arduino Uno R4 Minima, WiFi | Renesas RA4M1 | 8 KB | データフラッシュ。EEPROMライブラリが同じ命令で動作 |
| ESP32 | ESP32 (Xtensa) | なし | フラッシュ内のNVSパーティション。Preferencesを推奨 |
| ESP8266 (NodeMCU, Wemos D1) | ESP8266 | なし | フラッシュでエミュレーション。EEPROM.begin()とcommit()が必要 |
| Raspberry Pi Pico | RP2040 | なし | MicroPythonではファイル、Arduinoコアではフラッシュでエミュレーションしたeeprom |
コードを別のボードに移す予定があるなら、EEPROMの容量を固定の数値で書かないでください。EEPROM.length()はボードの実際のEEPROM容量を返す(Unoでは1024、Megaでは4096)ので、同じ消去用やダンプ用のコードがどのボードでも動きます。Unoのほかのメモリやピンの詳細はArduino Unoのピン配置、ボード選びについてはどのArduinoを買えばいい?を参照してください。
EEPROMの命令:read、write、update、put、get
EEPROMライブラリはボードパッケージに含まれているので、インストールは不要です。スケッチの先頭に#include <EEPROM.h>の1行を加えるだけです。アドレスは0から始まり、Unoでは1023で終わります。次の表は、各命令の働きとセルを消耗させるかどうかをまとめたものです。
| 命令 | 働き | サイズ | 消耗 |
|---|---|---|---|
| EEPROM.read(アドレス) | 1バイトを読み出す(0〜255) | 1バイト | なし、読み出しは自由 |
| EEPROM.write(アドレス, 値) | 呼び出すたびにバイトを書き直す | 1バイト | 呼び出すたびに1サイクル消費 |
| EEPROM.update(アドレス, 値) | 先に読み、値が違うときだけ書き込む | 1バイト | 値が変わったときのみ |
| EEPROM.put(アドレス, データ) | int、float、配列、structを1バイトずつ書き込む。内部でupdateを使う | sizeof(データ) | 変わったバイトのみ |
| EEPROM.get(アドレス, 変数) | putで書いたものを同じ型で読み戻す | sizeof(変数) | なし |
| EEPROM[アドレス] | 配列のようにアクセス。読み出しは自由、=での代入はwriteと同じ動作 | 1バイト | 代入のたびに消費 |
| EEPROM.length() | EEPROMの全容量を返す | ― | なし |
最初のスケッチ:EEPROMへの書き込みと読み出し
1#include <EEPROM.h>
2
3void setup() {
4 Serial.begin(9600);
5 Serial.print(F("EEPROM boyutu / size: "));
6 Serial.println(EEPROM.length()); // Uno'da 1024 / 1024 on the Uno
7
8 byte first = EEPROM.read(0); // 0. adresi oku / read address 0
9 Serial.print(F("Adres 0 / address 0: "));
10 Serial.println(first); // ilk seferde 255 / 255 the first time
11
12 EEPROM.update(0, 42); // farklıysa yazar / writes only if different
13 EEPROM.write(1, 7); // her seferinde yazar / writes every time
14
15 int number = 300;
16 EEPROM.put(10, number); // int = 2 bayt: 10 ve 11 / int = 2 bytes: 10 and 11
17 float temperature = 23.75;
18 EEPROM.put(12, temperature); // float = 4 bayt: 12-15 / float = 4 bytes: 12-15
19
20 int readNumber;
21 float readTemperature;
22 EEPROM.get(10, readNumber); // put ile aynı tür / same type as put
23 EEPROM.get(12, readTemperature);
24 Serial.println(readNumber); // 300
25 Serial.println(readTemperature); // 23.75
26
27 EEPROM.update(20, 300); // HATA: yalnız alt bayt (44) kalır / BUG: only the low byte (44) is kept
28 Serial.println(EEPROM.read(20)); // 44
29}
30
31void loop() {}スケッチを書き込み、シリアルモニタを9600ボーで開きます。最初の実行では、アドレス0は255と読めます。そのセルにはまだ一度も書き込まれていないからです。次にボードのリセットボタンを押すか、USBケーブルを抜いて挿し直すと、今度は42と読めます。これがEEPROMがデータを保持している証拠です。一方、アドレス1に書き込むwrite()の行は、値がいつも7なのに起動のたびにセルを書き直します。update()なら最初の1回しか書き込みません。最後の行の値300は前述の注意にある落とし穴を示しており、画面には44が表示されます。「基本設定」の「コンパイラの警告」が「なし」以外になっていれば、コンパイラはこの行に対してlarge integer implicitly truncated to unsigned type(大きな整数が符号なし型に切り詰められた)という警告を出します。
put()とget()を使うときは、各データが何バイトを占めるかを知っておく必要があります。そうしないと2つの変数を重ねて書いてしまい、片方が壊れます。この例ではintがアドレス10と11を占めるので、floatはアドレス12から始めています。Unoでのデータ型のサイズは次のとおりです。
| データ型 | Unoでのサイズ | 範囲 | 使用例 |
|---|---|---|---|
| byte、bool、char | 1バイト | 0〜255、true/false、または1文字 | オン/オフの状態、モード選択 |
| int | 2バイト | −32,768〜32,767 | キャリブレーションのオフセット |
| unsigned int、uint16_t | 2バイト | 0〜65,535 | ゲームのハイスコア |
| long、unsigned long | 4バイト | 約±21億、または0〜42.9億 | 長期間動かすカウンター |
| float | 4バイト | 有効数字6〜7桁 | センサーのキャリブレーション係数 |
| char配列[10] | 10バイト | 9文字+終端文字 | パスワード、デバイス名 |
EEPROMの書き込み寿命:100,000回の制限とは?
100,000回というと多く聞こえますが、作りの悪いスケッチなら数分で使い切ってしまいます。寿命の計算式は簡単で、寿命 = 100,000 ÷ 同じセルへの書き込み頻度です。重要なのは、この制限がEEPROM全体ではなくセルごとに適用されることです。同じアドレスに書き続けると、そのセルだけが消耗します。読み出しは寿命にまったく影響しません。次の表は、同じアドレスにさまざまな頻度で書き込んだときに、セルがどれくらい持つかを示しています。
| 同じアドレスへの書き込み頻度 | おおよその寿命 |
|---|---|
| loop()内で待ち時間なしのwrite()(1秒に約300回) | 5〜6分 |
| 毎秒 | 約28時間 |
| 毎分 | 約69日 |
| 10分に1回 | 約1.9年 |
| 1時間に1回 | 約11年 |
| 1日に10回 | 約27年 |
100,000回はメーカーが保証する最小値で、多くのチップはそれよりずっと長く持ちますが、それを当てにしてプロジェクトを設計しないでください。消耗したセルは突然壊れるのではなく、書き込んだ値を保持できなくなり、一部のビットが勝手に反転し始めます。そうなったら、データを別のアドレスに移せば大丈夫です。ほかのセルは影響を受けないからです。寿命を延ばす方法は次のとおりです。
- update()とput()を使う:どちらも書き込む前にセルを読み、値が同じなら書き込みません。
- 変わったときだけ書く:loop()を1周するたびではなく、ユーザーが実際に設定を変えたときに保存します。
- 保存を遅らせる:値が短時間に続けて変わる場合は、最後の変更から数秒後に1回だけ書き込みます。millis()を使えば簡単です。
- イベントで書く:ハイスコアはゲームが終わったとき、メニューの設定はメニューを抜けるときに保存します。
- 書き込みを分散する:頻繁に変わるカウンターは、1つのアドレスではなく複数のアドレスに順番に書き込みます(後述のリングバッファの例)。
- 必要なら外部メモリを使う:非常に頻繁に保存する場合は、100万サイクルの外部EEPROM、FRAM、SDカードを選びます。
例1:停電しても忘れないカウンター
最初の例では、ボタンを押すたびに増え、ボードの電源を切って入れ直しても続きから数えるカウンターを作ります。ボタンの片方の足をD2、もう片方をGNDにつなぎます。内部プルアップ抵抗を使うので、抵抗を追加する必要はありません(詳細はArduinoに押しボタンをつなぐ方法、ピンの位置は「ピン配置」ツール)。ポイントは、押すたびに保存するのではなく、最後に押してから5秒後に保存することです。これなら誰かが20回続けて押しても、EEPROMへの書き込みは1回だけです。
1#include <EEPROM.h>
2
3const byte BUTTON_PIN = 2; // buton D2 ile GND arasında / button between D2 and GND
4const int COUNT_ADDR = 0; // sayaç 0-3. baytlarda / counter in bytes 0-3
5const unsigned long SAVE_DELAY = 5000; // son basıştan 5 sn sonra yaz / write 5 s after the last press
6
7unsigned long count = 0;
8bool dirty = false; // kaydedilmemiş değişiklik / unsaved change
9unsigned long lastPress = 0;
10unsigned long lastChange = 0;
11bool lastReading = HIGH;
12bool stableState = HIGH;
13
14void setup() {
15 Serial.begin(9600);
16 pinMode(BUTTON_PIN, INPUT_PULLUP);
17 EEPROM.get(COUNT_ADDR, count);
18 if (count == 0xFFFFFFFF) count = 0; // boş EEPROM / blank EEPROM
19 Serial.print(F("Kayitli sayac / stored count: "));
20 Serial.println(count);
21}
22
23void loop() {
24 bool reading = digitalRead(BUTTON_PIN);
25 if (reading != lastReading) { // pin değişti, sayacı başlat / pin changed, restart timer
26 lastChange = millis();
27 lastReading = reading;
28 }
29 if (millis() - lastChange > 30 && reading != stableState) { // 30 ms sabit kaldı / stable for 30 ms
30 stableState = reading;
31 if (stableState == LOW) { // yeni basış / new press
32 count++;
33 dirty = true;
34 lastPress = millis();
35 Serial.println(count);
36 }
37 }
38
39 if (dirty && millis() - lastPress > SAVE_DELAY) { // sessiz dönemden sonra yaz / write after a quiet period
40 EEPROM.put(COUNT_ADDR, count);
41 dirty = false;
42 Serial.println(F("EEPROM'a yazildi / saved to EEPROM"));
43 }
44}dirty変数は、RAM上にまだ保存していない変更があることを表します。カウンターはunsigned long型なので4バイト(アドレス0〜3)を使い、40億回以上の押下を数えられます。何も書き込まれていないEEPROMではこの4バイトが0xFFなので、最初の起動では4,294,967,295と読めます。コードはこの値を見つけると、カウンターを0から始めます。put()は変わったバイトだけを書き込むので、カウンターが増えるときに消耗するのはたいてい最下位のバイトだけです。保存前の5秒以内に電源が切れると最後の押下は失われますが、EEPROMを守るための小さな代償です。
例2:構造体(struct)で設定とパスワードを保存する
複数の設定を個別のアドレスに書くより、structにまとめて1回のput()で保存するほうが、整理しやすく安全です。この例では、LEDの明るさと4〜6文字のパスワードを保存します。構造体の最初の2つの項目は欠かせません。magic(マジックナンバー)は、このスケッチの設定が本当にEEPROMに書き込まれているかを判断するためのもので、versionは、あとで構造体に新しい項目を追加したときに、古くて互換性のないデータを読んでしまうのを防ぎます。どちらかが一致しなければ、スケッチはデフォルトの設定を書き込みます。
1#include <EEPROM.h>
2
3const uint16_t MAGIC = 0xB6A1; // "ayar yazıldı" işareti / "settings written" marker
4const byte VERSION = 1; // yapı değişince artır / bump when the struct changes
5const int SETTINGS_ADDR = 0;
6const byte LED_PIN = 9; // PWM pini / PWM pin
7
8struct Settings {
9 uint16_t magic;
10 byte version;
11 char password[7]; // en çok 6 karakter + '\0' / up to 6 chars + '\0'
12 byte brightness; // LED parlaklığı 0-255 / LED brightness 0-255
13};
14
15Settings cfg;
16
17void writeDefaults() {
18 cfg.magic = MAGIC;
19 cfg.version = VERSION;
20 strcpy(cfg.password, "1234");
21 cfg.brightness = 128;
22 EEPROM.put(SETTINGS_ADDR, cfg);
23}
24
25void setup() {
26 Serial.begin(9600);
27 pinMode(LED_PIN, OUTPUT);
28 EEPROM.get(SETTINGS_ADDR, cfg);
29 if (cfg.magic != MAGIC || cfg.version != VERSION) { // ilk açılış ya da eski yapı / first boot or old layout
30 writeDefaults();
31 Serial.println(F("Varsayilan ayarlar yazildi / defaults written"));
32 }
33 analogWrite(LED_PIN, cfg.brightness);
34 Serial.print(F("Ayar boyutu / settings size: "));
35 Serial.println(sizeof(Settings)); // Uno'da 11 bayt / 11 bytes on the Uno
36 Serial.println(F("Komutlar / commands: b0-255 p<eski>,<yeni> ?"));
37}
38
39void loop() {
40 if (!Serial.available()) return;
41 String line = Serial.readStringUntil('\n');
42 line.trim();
43
44 if (line.startsWith("b")) { // örnek / example: b200
45 cfg.brightness = constrain(line.substring(1).toInt(), 0, 255);
46 analogWrite(LED_PIN, cfg.brightness);
47 EEPROM.put(SETTINGS_ADDR, cfg); // yalnız değişen bayt yazılır / only the changed byte is written
48 Serial.println(F("Parlaklik kaydedildi / brightness saved"));
49 } else if (line.startsWith("p")) { // örnek / example: p1234,5678
50 int comma = line.indexOf(',');
51 if (comma < 0) {
52 Serial.println(F("Bicim / format: p1234,5678"));
53 return;
54 }
55 String oldPw = line.substring(1, comma);
56 String newPw = line.substring(comma + 1);
57 if (oldPw != cfg.password) {
58 Serial.println(F("Eski sifre yanlis / wrong old password"));
59 } else if (newPw.length() < 4 || newPw.length() > 6) {
60 Serial.println(F("Yeni sifre 4-6 karakter olmali / new password must be 4-6 chars"));
61 } else {
62 newPw.toCharArray(cfg.password, sizeof(cfg.password));
63 EEPROM.put(SETTINGS_ADDR, cfg);
64 Serial.println(F("Sifre degisti / password changed"));
65 }
66 } else if (line == "?") {
67 Serial.print(F("Parlaklik / brightness: "));
68 Serial.println(cfg.brightness);
69 }
70}LEDをLED用抵抗(220 Ωで十分)を通してD9につなぎます。D9はPWMピンです。シリアルモニタを9600ボーで開き、改行コードをLFのみ(New Line)にして、次の命令を試してください。b200は明るさを200に、p1234,5678はパスワードを1234から5678に変更し、?は現在の明るさを表示します。ボードの電源を切って入れ直しても、LEDは同じ明るさで点灯し、新しいパスワードもそのまま有効です。この構造体はUnoで11バイトを使います。put()は変わったバイトだけを書き込むので、明るさを変えても消耗するのは1つのセルだけです。同じ構造体をキーパッドの暗証番号ロックに応用したり、RFID RC522で許可したカードの4バイトのIDを配列として保存したりできます。シリアル命令が初めてなら、Arduinoのシリアルモニタを読んでください。
書き込み寿命を何倍にも:リングバッファ(ウェアレベリング)
頻繁に更新するカウンターを同じアドレスに書き続けると、その1つのセルがすぐに消耗します。リングバッファでは、値を異なるスロットに順番に書き込みます。1回目の保存はあるスロットへ、次の保存は次のスロットへ、最後のスロットの次は先頭に戻ります。各スロットは16回の保存で1回しか書き込まれないので、寿命は16倍、つまり約160万回の保存まで延びます。起動時にはすべてのスロットを読んで最新の値を探します。カウンターは増える一方なので、いちばん大きい値が最新です。
1#include <EEPROM.h>
2
3const int RING_START = 100; // halka 100. adresten başlar / ring starts at address 100
4const byte SLOTS = 16; // 16 yuva x 4 bayt = 64 bayt / 16 slots x 4 bytes = 64 bytes
5
6uint32_t counter = 0;
7byte slot = 0;
8
9void loadCounter() {
10 for (byte i = 0; i < SLOTS; i++) {
11 uint32_t v;
12 EEPROM.get(RING_START + i * 4, v);
13 if (v != 0xFFFFFFFF && v >= counter) { // en büyük değer en yenisidir / the largest value is the newest
14 counter = v;
15 slot = i;
16 }
17 }
18}
19
20void saveCounter() {
21 slot = (slot + 1) % SLOTS; // her kayıtta sıradaki yuva / next slot on every save
22 EEPROM.put(RING_START + slot * 4, counter);
23}
24
25void setup() {
26 Serial.begin(9600);
27 loadCounter();
28 counter++; // örnek: her açılışı say / example: count every boot
29 saveCounter();
30 Serial.print(F("Acilis sayisi / boot count: "));
31 Serial.println(counter);
32 Serial.print(F("Kullanilan yuva / slot used: "));
33 Serial.println(slot);
34}
35
36void loop() {}この例では起動するたびにカウンターを1増やしますが、ボタンやセンサーのイベント、稼働時間計などに置き換えられます。スロットを増やせば、寿命もその分だけ延びます。64スロットなら256バイトを使い、約640万回の保存に耐えます。ボードをほかのスケッチで使ったことがある場合、リング領域に古いデータが残っていることがあるので、初めて使う前に下のコードでEEPROMを消去してください。より本格的なプロジェクトでは、Arduino付属のeeprom_crcの例のようにCRCチェックサムを加えて、壊れたデータを検出することもできます。
EEPROMをリセットするには?書き込むと消える?
USBケーブルで普通に書き込む場合、書き込まれるのはプログラムメモリだけで、EEPROMのデータは保持されます。そのため新しいプロジェクトを書き込んでも前のプロジェクトの設定がEEPROMに残り、新しいコードが想定外の値を読むことがあります。UnoでEEPROMが消えるのは、ISP書込装置で「スケッチ」>「書込装置を使って書き込む」や「ツール」>「ブートローダを書き込む」を実行したときだけです。これらの操作はチップ全体を消去し、Unoのヒューズ設定ではEEPROMを保護するEESAVEビットがオフになっています。自分でEEPROMを消去するには、次のスケッチを1回書き込みます。
1#include <EEPROM.h>
2
3void setup() {
4 for (uint16_t i = 0; i < EEPROM.length(); i++) {
5 EEPROM.update(i, 255); // fabrika durumu 0xFF / factory state 0xFF
6 }
7 pinMode(LED_BUILTIN, OUTPUT);
8 digitalWrite(LED_BUILTIN, HIGH); // LED yandı: bitti / LED on: done
9}
10
11void loop() {}Arduino IDEの「ファイル」>「スケッチ例」>「EEPROM」>「eeprom_clear」はすべてのセルに0を書き込みますが、ここでは工場出荷時の状態である255に戻します。こうすると、マジックナンバーや0xFFをチェックするコードが、EEPROMを再び空として認識します。update()を使っているので、すでに255のセルには書き込みません。Unoの1024セルすべてを書き込んでも最大で約3.4秒です。内蔵LEDが点灯したら完了です。そのあと本来のスケッチを書き込んでください。
ESP32のEEPROM:Preferencesライブラリ
ESP32には本物のEEPROMがありません。不揮発データは、外付けフラッシュメモリのNVS(Non-Volatile Storage)パーティションに保存されます。デフォルトのパーティションテーブルでは、この領域は20 KBです。Espressifが推奨する方法は、ESP32のボードパッケージに付属するPreferencesライブラリです。Preferencesはアドレスではなく名前空間(namespace)とキー(key)の名前でデータを保存するので、カウンターがどのアドレスにあったかを覚えておく必要はありません。NVSは新しい値をページ内の次の空きエントリーに書き込み、古い値を消去済みとしてマークするため、ウェアレベリングが自動で行われます。名前空間とキーの名前は最大15文字で、大文字と小文字が区別されます。ESP32が初めてなら、まずESP32とは?から読んでください。
1#include <Preferences.h>
2
3Preferences prefs;
4
5void setup() {
6 Serial.begin(115200);
7 prefs.begin("bluegrays", false); // ad alanı; false = okuma + yazma / namespace; false = read + write
8
9 uint32_t boots = prefs.getUInt("boots", 0); // anahtar yoksa 0 / 0 if the key is missing
10 boots++;
11 prefs.putUInt("boots", boots);
12
13 if (!prefs.isKey("ssid")) { // ilk açılış / first boot
14 prefs.putString("ssid", "EvAgi"); // örnek değer / example value
15 }
16 String ssid = prefs.getString("ssid", ""); // kayıtlı Wi-Fi adı / stored Wi-Fi name
17 int brightness = prefs.getInt("bright", 128); // yoksa 128 / 128 if missing
18
19 Serial.print("Acilis / boot: ");
20 Serial.println(boots);
21 Serial.print("SSID: ");
22 Serial.println(ssid);
23 Serial.print("Parlaklik / brightness: ");
24 Serial.println(brightness);
25 Serial.print("Bos kayit yeri / free entries: ");
26 Serial.println(prefs.freeEntries());
27 prefs.end(); // ad alanını kapat / close the namespace
28}
29
30void loop() {}| 命令 | 働き |
|---|---|
| prefs.begin("名前", false) | 名前空間を開く。falseは読み書き、trueは読み出し専用 |
| putUInt、putInt、putFloat、putBool、putString、putBytes(保存用の関数) | 値をキーの名前で保存する |
| getUInt("キー", デフォルト値) | 値を読み出す。キーがなければデフォルト値を返す |
| isKey("キー") | キーが存在するかを確認する |
| remove("キー") | 1つのキーを削除する |
| clear() | 開いている名前空間のすべてのキーを削除する |
| freeEntries() | NVSに残っている空きエントリーの数を返す |
| end() | 名前空間を閉じる |
値は書き込んだときと同じ型で読み出す必要があります。putUIntで書いてgetIntで読むと、デフォルト値が返ってきます。getString()の前にisKey()を呼んでいるのにも理由があります。「ツール」メニューの「Core Debug Level」がError以上になっていると、存在しないテキストのキーを読んだときにシリアルモニタにnvs_get_str len fail: ssid NOT_FOUNDと表示されます。害はありませんが、まぎらわしいです。USBで再度書き込んでもNVSは消えませんが、Erase All Flash Before Sketch UploadがEnabledになっていると、書き込むたびにすべてのエントリーが消えます。Wi-Fiの情報をPreferencesで保存するのはよくある方法です。ネットワーク名とパスワードをスマホから入力したいならESP32 WiFiManagerも使えます。ESP32はWi-Fiの設定を同じNVS領域に保存しています。ディープスリープから復帰するときに残しておく必要がある短期間の値には、フラッシュをまったく消耗させないRTCメモリが向いています。詳しくはESP32のディープスリープを参照してください。
古いArduinoのコードをESP32に移植する場合、EEPROMライブラリも使えますが、違いが2つあります。使う前にEEPROM.begin(サイズ)で必要なバイト数を指定し、変更したあとはEEPROM.commit()を呼ぶ必要があります。put()やwrite()が変更するのはRAM上のコピーだけで、commit()を呼ばなければフラッシュには何も書き込まれません。ESP32ではこのライブラリはNVS内の1つのデータの塊(blob)として動作し、Espressifは非推奨(deprecated)としています。新しく作った領域はすべてのバイトが0xFFです。同じコードはESP8266でもコンパイルできます。ESP8266ではサイズは最大4096バイトで、commit()のたびに4 KBのフラッシュセクターを消去して書き直すので、commitはなるべく呼ばないようにしてください。
1#include <EEPROM.h>
2
3void setup() {
4 Serial.begin(115200);
5 EEPROM.begin(64); // kullanılacak bayt sayısı / number of bytes to use
6 uint32_t boots;
7 EEPROM.get(0, boots);
8 if (boots == 0xFFFFFFFF) boots = 0; // yeni alan 0xFF dolu / a fresh area is 0xFF
9 boots++;
10 EEPROM.put(0, boots); // yalnız RAM'deki kopyayı değiştirir / changes only the RAM copy
11 EEPROM.commit(); // flaşa şimdi yazılır / written to flash now
12 Serial.println(boots);
13 EEPROM.end();
14}
15
16void loop() {}Raspberry Pi Picoの不揮発データ:MicroPythonのファイルシステム
Raspberry Pi PicoのRP2040チップにもEEPROMはなく、ボード上には2 MBのフラッシュメモリがあります。MicroPythonはこのフラッシュのうち約1.4 MBをLittleFSのファイルシステムとして確保します(Pico WではWi-Fiのファームウェアがあるため少なくなります)。つまりPicoでEEPROMに相当するのは、設定を小さなファイルに書き込むことです。LittleFSは書き込みをフラッシュの異なるブロックに分散するので自分でウェアレベリングを行い、書き込み中に電源が切れてもファイルシステムが壊れません。次のコードは設定をJSON形式で保存し、起動回数を数え、GP15とGNDの間につないだボタンが押されるたびにカウンターを保存します。
1import json, os, time
2from machine import Pin
3
4FILE = "settings.json"
5DEFAULTS = {"boots": 0, "count": 0, "password": "1234"}
6
7def load():
8 try:
9 with open(FILE) as f:
10 data = json.load(f)
11 except (OSError, ValueError): # dosya yok ya da bozuk / file missing or corrupt
12 data = {}
13 for key, value in DEFAULTS.items(): # eksik anahtarları tamamla / fill in missing keys
14 data.setdefault(key, value)
15 return data
16
17def save(data):
18 with open(FILE + ".tmp", "w") as f: # önce geçici dosyaya yaz / write a temp file first
19 json.dump(data, f)
20 os.rename(FILE + ".tmp", FILE) # sonra eskisinin yerine koy / then replace the old one
21
22settings = load()
23settings["boots"] += 1
24save(settings)
25print("Acilis / boot:", settings["boots"], "Sayac / count:", settings["count"])
26
27button = Pin(15, Pin.IN, Pin.PULL_UP) # buton GP15 ile GND arasında / button between GP15 and GND
28
29while True:
30 if button.value() == 0: # basıldı / pressed
31 time.sleep_ms(30) # sıçramayı bekle / let the bounce settle
32 if button.value() == 0:
33 settings["count"] += 1
34 save(settings) # seyrek olay, her basışta yaz / rare event, save on each press
35 print("Sayac / count:", settings["count"])
36 while button.value() == 0: # bırakılmasını bekle / wait for release
37 time.sleep_ms(10)
38 time.sleep_ms(30) # bırakma sıçraması / release bounce
39 time.sleep_ms(10)ファイルを直接上書きするのではなく、まず一時ファイルの.tmpに書き込み、それをos.rename()で古いファイルと置き換えています。こうすれば、書き込みが途中で止まっても古い設定ファイルは無事です。ファイルがない、または中身が壊れている場合、load()はデフォルト値に戻ります。あとでプログラムに新しい設定を追加したときは、setdefault()が足りないキーを自動で補います。PicoをArduino IDEでプログラムする場合は、Earle Philhower氏のArduino-PicoコアにあるEEPROMライブラリも使えます。EEPROM.begin(256)で256〜4096バイトの領域を確保し、EEPROM.commit()で変更を保存します。このエミュレーションのEEPROMはフラッシュの最後の4 KBのセクターにあり、commit()のたびにそのセクターを消去して書き直します。フラッシュの消去寿命はセクターあたり約100,000回なので、commitはなるべく呼ばないようにしてください。Picoを始めたばかりなら、Raspberry Pi Picoとは?とMicroPythonの基本から読むのがおすすめです。
さらに大きな不揮発メモリ:外部EEPROM、FRAM、SDカード
1 KBでは足りない、またはとても頻繁に保存する必要があるなら、外部メモリを追加できます。AT24C256のようなI2C EEPROMチップは32 KBの容量と約100万回の書き込みサイクルがあり、SDAとSCLの2本の線だけでつなげます(I2Cとは?)。あまり知られていませんが、市販のDS3231のRTC時計モジュールの多くには、4 KBのAT24C32 EEPROMがすでに載っていて、たいていI2Cアドレス0x57でアクセスできます。1秒に何度も保存するような用途には約1兆(10^12)回の書き込みサイクルに耐えるFRAMモジュールが、大きなデータログにはデータをファイルとして保存するSDカードモジュールが向いています。
EEPROMを使ったプロジェクトのアイデア
- 最後の状態を覚えているランプ:電源が戻ったら、リレーにつないだランプを停電前の状態に戻します。
- 暗証番号を変更できる金庫:キーパッドで入力した新しい番号をEEPROMに書き込めば、スケッチを書き込み直さなくても番号を変えられます。
- カード式のドアロック:RFID RC522で読み取った許可カードのIDを保存し、マスターカードで新しいカードを追加します。
- 消えないハイスコア:サイモン、T-Rex、スネークで最高得点を保存します。サンプルコードはArduinoのゲームプロジェクトにあります。
- 動きを記録するロボットアーム:サーボの角度をポーズごとに記録して再生します。Arduinoのロボットアームのコードは、これをEEPROM.update()で行っています。
- キャリブレーションを覚えているはかり:HX711ロードセルで求めたキャリブレーション係数を、floatとしてput()で保存します。
- リセットカウンター:ボードが再起動した回数を数えて電源の問題を診断します。方法はArduinoがリセットを繰り返すときにあります。
よくある間違いと対処法
| 症状 | 原因 | 対処法 |
|---|---|---|
| 最初の起動で255、-1、65535、nanと読める | EEPROMに一度も書き込まれていない。空のセルは0xFF | マジックナンバーや0xFFのチェックでデフォルト値を書き込む |
| 300を書いたのに44と読める | write()とread()は1バイトしか扱えない | 複数バイトのデータにはput()とget()を使う |
| 2つの値が互いに壊し合う | アドレスが重なっている。intは2バイト、floatは4バイト | アドレスマップを作り、sizeofでアドレスを計算する |
| 再起動するとテキストが意味のない文字になる | Stringオブジェクトをput()で書いた | テキストはchar配列として保存する |
| 新しいコードが古いプロジェクトの設定を読む | 普通の書き込みではEEPROMは消えない | マジックナンバーとバージョン項目を使うか、EEPROMを消去する |
| 書き込んだら設定が消えた | Unoでは書込装置での書き込み、ESP32ではErase All Flashのオプション | USBで普通に書き込む。ESP32ではオプションをDisabledにする |
| ESP32で値が保存されない | EEPROM.begin()またはEEPROM.commit()がない | 両方を追加するか、Preferencesに切り替える |
| Preferencesがいつもデフォルト値を返す | 違う型で読んでいる、名前が15文字を超えている、または名前空間が違う | putとgetの型をそろえ、名前を短くする |
| 「nvs_open failed: NOT_FOUND」エラー | 一度も作成していない名前空間を読み出し専用(true)で開いた | まず名前空間をfalseで開いて値を書き込む |
| しばらくすると値が壊れる | loop()内で書き込み続けている、または書き込み中に電源電圧が下がった | update()と遅延保存を使い、リングバッファとCRCを加える |
EEPROMは、Arduinoのプロジェクトに記憶力を持たせるいちばん簡単な方法です。数行のコードで、デバイスの設定、カウンター、パスワードを停電から守れます。注意すべきなのは書き込み寿命だけです。update()とput()を使い、不要な書き込みをしなければ、UnoのEEPROMは何年も問題なく動きます。BlueGraysアプリには、4x4キーパッドのパスワードアプリ、スマート電子金庫、RFID認証、サイモンやT-Rexのゲームなど、EEPROMで発展させられるプロジェクトが、回路図とステップごとのコードつきでそろっています。
よくある質問
Arduino EEPROMには何回書き込めますか?
ATmega328Pのデータシートでは、EEPROMの各セルについて少なくとも100,000回の書き込み・消去サイクルが規定されています。制限はセルごとで、読み出しはまったく影響しません。同じアドレスに毎秒書き込むと約28時間でセルが消耗しますが、update()を使って値が変わったときだけ書き込めば、EEPROMは何年も持ちます。
新しいスケッチを書き込むとArduinoのEEPROMは消えますか?
いいえ。USBでの普通の書き込みはプログラムメモリだけに書き込み、EEPROMのデータは保持されます。UnoでEEPROMが消えるのは、ISP書込装置で書き込んだときやブートローダーを書き込んだときだけです。新しいプロジェクトが古い設定を読まないよう、マジックナンバーのチェックを使ってください。
EEPROM.writeとEEPROM.updateの違いは何ですか?
write()は呼び出すたびに値をセルに書き直し、そのたびに書き込みサイクルを1回消費します。update()は先にセルを読み、値が同じなら書き込みません。結果は同じですが、update()はEEPROMの寿命を守ります。put()も内部で同じ方法を使っています。
ArduinoのEEPROMを消去するには?
EEPROM.length()まで回るforループで、すべてのアドレスに255(または0)を書き込みます。Arduino IDEのeeprom_clearの例は、そのままで0を書き込みます。Unoで1024バイトすべてを消去するには約3.4秒かかります。終わったら消去用のスケッチをボードに残さず、本来のプログラムを書き込んでください。
ESP32にEEPROMはありますか?
ESP32には本物のEEPROMはなく、不揮発データはフラッシュメモリのNVSパーティションに保存されます。新しいプロジェクトにはPreferencesライブラリがおすすめです。古いコード用にEEPROMライブラリも使えますが、最初にEEPROM.begin(サイズ)を、変更のたびにEEPROM.commit()を呼ぶ必要があります。
int、float、テキストをEEPROMに保存するには?
int、long、float、構造体などの複数バイトのデータは、EEPROM.put(アドレス, 変数)で書き込み、EEPROM.get(アドレス, 変数)で読み出します。intは2バイト、floatは4バイトを使うので、次のデータのアドレスはそれに合わせて決めます。テキストはStringではなく、固定サイズのchar配列として保存してください。
この記事を読んだ後に作れるプロジェクト
全プロジェクトを見る
ボタンでLEDの点灯・消灯を切り替え
部屋の照明スイッチのように、ボタンを1回押すと点灯し、もう1回押すと消灯します。Arduinoはボタンが押されるたびにLEDの状態を反転させます。ボタン内部の小さな接点の振動で誤動作しないよう、短い待ち時間も設けます。1つのボタンでオン・オフを切り替える、シンプルな仕組みです。

7セグメントディスプレイとボタンで作るカウンター
3つのボタンを使うシンプルなカウンターです。1つ目のボタンを押すと表示の数字が1増え、2つ目を押すと1減り、3つ目を押すと0に戻ります。1桁の7セグメントディスプレイで0から9まで数え、範囲の端に達すると隅の小さな点が0.5秒間点灯して知らせます。スコアボードや順番表示器などのプロジェクトの基礎になります。

4x4キーパッドとLCDで暗証番号ロック
正しい暗証番号を知らないと開けられない、小さな電子ドアロックです。ハッシュキーを押してキーパッドから暗証番号を入力します。正しければ画面に入室許可が表示され、緑のLEDが点灯し、サーボモーターがドアを5秒間開けてから再び閉めます。間違っていれば赤のLEDが点灯します。建物や金庫のロックの仕組みを自分で作ります。

スマート電子金庫
貴重品を保管できる、暗証番号で開く小さな金庫です。キーパッドで4桁の暗証番号を入力すると、正しければサーボモーターが解錠し、緑色のLEDが点灯してディスプレイに金庫の状態が表示されます。キーを押すたびにブザーが短く鳴ります。3回間違えると30秒間ロックアウトされます。暗証番号はいつでも変更でき、電源が切れても保持されます。

RFID認証
マンションのカード式入館システムと同じ仕組みです。カードをRFIDリーダーに近づけると、Arduinoがカードの番号を登録済みの番号と比較します。登録済みのカードなら緑色、未登録のカードなら赤色のLEDが3秒間点灯し、結果はコンピューターの画面にも表示されます。カード式のドアや錠のプロジェクトの基礎をここで学べます。

Arduino記憶ゲーム(LED4個+ボタン4個+ブザー)
記憶力に自信はありますか?4色のLEDが順番に点灯し、それぞれの音を鳴らします。同じ順番でボタンを押して再現してください。正解するたびに新しい色が1つ追加され、短い勝利のメロディーが流れて、LCDディスプレイにスコアが表示されます。1回でも押し間違えると、悲しい音とともにゲームが終了し、最初からやり直しになります。

OLED T-Rexラン:Arduino恐竜ゲーム
インターネットが切れると現れる恐竜ゲームを、机の上で楽しめます。小さなOLEDディスプレイを恐竜が走り、迫るサボテンを1つのボタンでジャンプして避けます。横の4桁ディスプレイには得点がリアルタイムで表示され、100点ごとにブザーがお祝いの音を鳴らします。サボテンにぶつかるとゲーム終了です。ボタンを押すと、もう一度始められます。

ESP32のディープスリープで節電(OLED + RTC RAMカウンター)
ESP32のディープスリープモードは、CPUとほとんどの周辺機器を停止し、消費電流を約10 µAまで抑えます。電池で動くIoT機器に欠かせない機能です。このプロジェクトでは、タイマー(15秒)、外部ピン(EXT0、ボタンはGPIO 33)、タッチ(T0/GPIO 4)の3種類の復帰方法を使います。RTC_DATA_ATTRを付けた変数はディープスリープ中も保持され、起動回数、タイマーによる復帰回数、ボタンによる復帰回数を継続して記録できます。復帰するたびにSSD1306 OLEDディスプレイに復帰理由と回数を表示します。
ほかの記事
BlueGraysアプリ
この回路をスマホでステップごとに作る
回路図、コード、部品リストを1つの画面で確認できます。Arduino、ESP32、Picoのプロジェクトを200件以上用意しています。

