Arduino 執行期間常用 SRAM 變數保存亮度、門檻、模式或校正值,但只要斷電、按下 Reset 或重新啟動,這些內容就會消失。本章將介紹如何使用 EEPROM,讓裝置重新開機後仍能恢復重要設定。
內容涵蓋 EEPROM.read()、EEPROM.write()、EEPROM.update()、EEPROM.get() 與 EEPROM.put(),並進一步使用 Struct、Magic Number、版本欄位與範圍驗證,建立安全且可維護的設定儲存機制。
EEPROM 寫入次數有限,不適合當成高頻資料記錄器。實作時應只在資料真正改變且需要永久保存時才寫入。
本章學習目標
- 了解 EEPROM 的用途與工作原理。
- 分辨 Flash、SRAM 與 EEPROM 的差異。
- 理解一般變數在重新啟動後消失的原因。
- 使用 EEPROM.read() 與 EEPROM.write() 存取單一 Byte。
- 使用 EEPROM.update() 減少不必要的寫入。
- 使用 EEPROM.get() 與 EEPROM.put() 儲存完整資料型別。
- 使用 Struct 集中管理設備設定。
- 使用 Magic Number、版本欄位與範圍檢查驗證資料。
- 避免頻繁寫入造成 EEPROM 壽命縮短。
- 建立恢復預設值與安全保存機制。
為什麼 Arduino 需要 EEPROM
程式執行時,一般變數通常存放在 SRAM。例如 int brightness = 128; 可以代表 LED 亮度、馬達速度、警報門檻、音量、感測器校正值或使用者模式。
當 Arduino 斷電、重設、重新上傳程式或重新啟動時,SRAM 中的內容會消失。需要跨重新啟動保留的設定,應儲存在 EEPROM 這類非揮發性記憶體。
什麼是 EEPROM
EEPROM 是 Electrically Erasable Programmable Read-Only Memory 的縮寫,中文常稱為電子抹除式可程式唯讀記憶體。
雖然名稱中包含唯讀記憶體,但 EEPROM 可以由程式讀取、寫入與修改。它最大的特色是裝置斷電後,資料仍能保留,因此屬於 Non-volatile Memory。
| 項目 | 說明 |
|---|---|
| 全名 | Electrically Erasable Programmable Read-Only Memory |
| 中文 | 電子抹除式可程式唯讀記憶體 |
| 特性 | 斷電後資料仍保留 |
| Arduino 函式庫 | EEPROM.h |
| Arduino Uno EEPROM 容量 | 1024 Bytes |
| 位址範圍 | 0~1023 |
| 單一位址大小 | 1 Byte |
| 常見用途 | 設定、校正值、計數器、模式記錄 |
| 注意事項 | 寫入次數有限 |
Arduino Uno 的三種主要記憶體
Flash
Flash 用來儲存 Arduino 程式、編譯後的機器碼與固定資料。程式上傳時主要就是寫入 Flash。
Arduino Uno 的 Flash 容量約為 32 KB,其中一部分由 Bootloader 使用。Flash 斷電後不會消失。
SRAM
SRAM 用來儲存一般變數、陣列、函式區域變數、堆疊、動態記憶體與執行中的字串。
Arduino Uno 的 SRAM 容量約為 2 KB。SRAM 速度快,但斷電後內容會消失。
EEPROM
EEPROM 適合保存需要長期保留的小量資料,例如使用者設定、感測器校正值、啟動模式、裝置編號與累積運轉次數。
Arduino Uno 的 EEPROM 容量為 1024 Bytes,斷電後仍能保留資料。
| 項目 | Flash | SRAM | EEPROM |
|---|---|---|---|
| 主要用途 | 儲存程式 | 執行中的變數 | 長期設定資料 |
| 斷電後保留 | 是 | 否 | 是 |
| Arduino Uno 容量 | 約 32 KB | 約 2 KB | 1024 Bytes |
| 讀取速度 | 快 | 很快 | 較慢 |
| 寫入速度 | 較慢 | 很快 | 較慢 |
| 寫入次數限制 | 有 | 無此類限制 | 有 |
| 程式可直接修改 | 不方便 | 可以 | 可以 |
EEPROM 適合與不適合儲存的資料
適合儲存
- LED 亮度設定。
- 溫度或警報門檻。
- 馬達速度設定。
- 選單中的使用者選項。
- 感測器校正偏移量。
- 裝置位址與開機模式。
- 最後一次運作狀態。
- 小型累積計數器。
- 音量、語言與顯示設定。
不適合儲存
- 大量感測器歷史資料。
- 每秒更新的即時數據。
- 圖片、音訊或大型字串。
- 長時間連續記錄。
- 高頻率變動的變數。
大量或高頻資料記錄通常應改用 SD Card、外接 Flash、FRAM、電腦資料庫或雲端伺服器。
為什麼 EEPROM 不能頻繁寫入
EEPROM 每個儲存單元的寫入壽命有限。一般常見規格約為單一位址 100,000 次寫入,實際壽命仍會受到晶片規格、溫度與使用方式影響。
若在 loop() 中持續寫入,程式每秒可能執行數千次,很快就會消耗同一位址的寫入壽命。正確原則是只在資料真正改變,而且確實需要保存時才寫入。
#include <EEPROM.h>
// 錯誤示範:不要在每次 loop() 都寫入 EEPROM。
void loop()
{
byte sensorValue = map(analogRead(A0), 0, 1023, 0, 255);
EEPROM.write(0, sensorValue);
}
載入 EEPROM 函式庫與確認容量
Arduino IDE 已內建 EEPROM 函式庫,通常不需要額外安裝。程式頂端先加入下列標頭檔。
#include <EEPROM.h>
Arduino Uno 的 EEPROM 容量為 1024 Bytes,位址範圍是 0~1023。每個位址可儲存 1 Byte,也就是 0~255。
使用 EEPROM.length() 可以取得目前開發板的 EEPROM 容量,讓程式更容易移植到其他 Arduino 開發板。
#include <EEPROM.h>
void setup()
{
Serial.begin(9600);
Serial.print("EEPROM size: ");
Serial.print(EEPROM.length());
Serial.println(" bytes");
}
void loop()
{
}
Arduino Uno 的預期輸出為 EEPROM size: 1024 bytes。
使用 EEPROM.write() 寫入單一 Byte
EEPROM.write(address, value); 會將一個 0~255 的 Byte 寫入指定地址。
| 參數 | 說明 |
|---|---|
| address | EEPROM 位址 |
| value | 要寫入的 Byte,範圍 0~255 |
#include <EEPROM.h>
void setup()
{
EEPROM.write(0, 123);
}
void loop()
{
}
這段程式會將 123 寫入 EEPROM 位址 0。Arduino 重新開機後,資料仍會存在。
使用 EEPROM.read() 讀取單一 Byte
EEPROM.read(address); 會讀取指定 EEPROM 位址中的一個 Byte。
#include <EEPROM.h>
void setup()
{
Serial.begin(9600);
byte value = EEPROM.read(0);
Serial.print("EEPROM value: ");
Serial.println(value);
}
void loop()
{
}
若位址 0 先前寫入 123,預期輸出為 EEPROM value: 123。
第一個完整實驗:寫入並讀取資料
#include <EEPROM.h>
const int EEPROM_ADDRESS = 0;
void setup()
{
Serial.begin(9600);
EEPROM.write(EEPROM_ADDRESS, 150);
byte savedValue = EEPROM.read(EEPROM_ADDRESS);
Serial.print("Saved value: ");
Serial.println(savedValue);
}
void loop()
{
}
預期 Serial Monitor 顯示 Saved value: 150。
使用 EEPROM.update() 減少不必要寫入
若每次開機都執行 EEPROM.write(0, 150);,即使原本資料已經是 150,仍會消耗一次寫入壽命。
EEPROM.update(address, value); 會先比較目前值與新值,只有兩者不同時才真正寫入。保存設定時通常應優先使用 EEPROM.update()。
| 函式 | 行為 |
|---|---|
| EEPROM.write() | 每次都執行寫入 |
| EEPROM.update() | 數值不同時才寫入 |
EEPROM.update(0, 150);
第二個實驗:儲存 LED 亮度
實驗目標
- 使用可變電阻設定 LED 亮度。
- 按下按鍵後將亮度存入 EEPROM。
- Arduino 重新啟動時自動恢復上次亮度。
| 元件 | 數量 |
|---|---|
| Arduino Uno R3 | 1 |
| LED | 1 |
| 220Ω 電阻 | 1 |
| 10KΩ 可變電阻 | 1 |
| 按鍵 | 1 |
| Breadboard | 1 |
| 杜邦線 | 若干 |
接線
- 可變電阻一側腳接 5V,中間腳接 A0,另一側腳接 GND。
- D9 經 220Ω 電阻連接 LED,再接 GND。
- D2 經按鍵接 GND,程式使用 INPUT_PULLUP。
程式
#include <EEPROM.h>
const int POT_PIN = A0;
const int LED_PIN = 9;
const int SAVE_BUTTON_PIN = 2;
const int BRIGHTNESS_ADDRESS = 0;
int currentBrightness = 0;
void setup()
{
pinMode(LED_PIN, OUTPUT);
pinMode(SAVE_BUTTON_PIN, INPUT_PULLUP);
Serial.begin(9600);
currentBrightness = EEPROM.read(BRIGHTNESS_ADDRESS);
analogWrite(LED_PIN, currentBrightness);
Serial.print("Loaded brightness: ");
Serial.println(currentBrightness);
}
void loop()
{
int adcValue = analogRead(POT_PIN);
currentBrightness = map(adcValue, 0, 1023, 0, 255);
analogWrite(LED_PIN, currentBrightness);
if (digitalRead(SAVE_BUTTON_PIN) == LOW)
{
EEPROM.update(BRIGHTNESS_ADDRESS, currentBrightness);
Serial.print("Brightness saved: ");
Serial.println(currentBrightness);
delay(300);
}
}
程式運作流程
- Arduino 開機後從 EEPROM 讀取亮度。
- LED 恢復上次亮度。
- 轉動可變電阻調整亮度。
- 按下儲存按鍵。
- 將目前亮度寫入 EEPROM。
此範例使用 delay(300) 做簡單按鍵去彈跳。較完整的專案應使用 millis()、狀態變化偵測與按下瞬間觸發,避免按住按鍵時重複執行。
使用按鍵邊緣偵測保存資料
#include <EEPROM.h>
const int POT_PIN = A0;
const int LED_PIN = 9;
const int SAVE_BUTTON_PIN = 2;
const int BRIGHTNESS_ADDRESS = 0;
int currentBrightness = 0;
int lastRawButtonState = HIGH;
int stableButtonState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long DEBOUNCE_DELAY = 50;
void setup()
{
pinMode(LED_PIN, OUTPUT);
pinMode(SAVE_BUTTON_PIN, INPUT_PULLUP);
Serial.begin(9600);
currentBrightness = EEPROM.read(BRIGHTNESS_ADDRESS);
analogWrite(LED_PIN, currentBrightness);
}
void loop()
{
updateBrightness();
checkSaveButton();
}
void updateBrightness()
{
int adcValue = analogRead(POT_PIN);
currentBrightness = map(adcValue, 0, 1023, 0, 255);
currentBrightness = constrain(currentBrightness, 0, 255);
analogWrite(LED_PIN, currentBrightness);
}
void checkSaveButton()
{
int rawButtonState = digitalRead(SAVE_BUTTON_PIN);
if (rawButtonState != lastRawButtonState)
{
lastDebounceTime = millis();
}
if (millis() - lastDebounceTime >= DEBOUNCE_DELAY)
{
if (rawButtonState != stableButtonState)
{
stableButtonState = rawButtonState;
if (stableButtonState == LOW)
{
saveBrightness();
}
}
}
lastRawButtonState = rawButtonState;
}
void saveBrightness()
{
EEPROM.update(BRIGHTNESS_ADDRESS, currentBrightness);
Serial.print("Brightness saved: ");
Serial.println(currentBrightness);
}
此程式使用原始讀值與穩定狀態分離的完整去彈跳邏輯,只有按鍵穩定按下時才保存一次,避免彈跳或長按造成重複寫入。
單一 EEPROM 位址只能儲存 0~255
EEPROM.write()、EEPROM.read() 與 EEPROM.update() 都以一個 Byte 為單位,因此可以直接保存 byte,但不能完整保存超過 1 Byte 的 int、long、float 或 struct。
可使用 sizeof() 確認資料型別所占空間。不同微控制器架構的資料型別大小可能不同,例如部分 32 位元開發板的 int 為 4 Bytes,應以 sizeof() 的實際結果為準。
| 資料型別 | 大小 |
|---|---|
| bool | 1 Byte |
| byte | 1 Byte |
| char | 1 Byte |
| int | 2 Bytes |
| unsigned int | 2 Bytes |
| long | 4 Bytes |
| unsigned long | 4 Bytes |
| float | 4 Bytes |
手動拆分與組合 int 資料
例如 int threshold = 700; 大於 255,無法放進單一 Byte。可以使用 highByte() 與 lowByte() 拆成兩個 Byte,再用 word() 組合回原值。
#include <EEPROM.h>
const int HIGH_BYTE_ADDRESS = 0;
const int LOW_BYTE_ADDRESS = 1;
void setup()
{
Serial.begin(9600);
int originalValue = 700;
EEPROM.update(HIGH_BYTE_ADDRESS, highByte(originalValue));
EEPROM.update(LOW_BYTE_ADDRESS, lowByte(originalValue));
int loadedValue = word(
EEPROM.read(HIGH_BYTE_ADDRESS),
EEPROM.read(LOW_BYTE_ADDRESS)
);
Serial.print("Loaded value: ");
Serial.println(loadedValue);
}
void loop()
{
}
預期輸出為 Loaded value: 700。若要儲存不同資料型別,EEPROM.get() 與 EEPROM.put() 會更方便。
使用 EEPROM.get() 與 EEPROM.put() 儲存完整資料型別
EEPROM.get()
EEPROM.get(address, variable); 會依照 variable 的資料型別,自動讀取正確的 Byte 數量。
int threshold;
EEPROM.get(0, threshold);
EEPROM.put()
EEPROM.put(address, variable); 可以寫入完整資料型別。其內部通常只更新有變動的 Byte,以減少不必要的寫入。
int threshold = 700;
EEPROM.put(0, threshold);
第三個實驗:儲存 int 設定值
#include <EEPROM.h>
const int THRESHOLD_ADDRESS = 0;
void setup()
{
Serial.begin(9600);
int originalThreshold = 700;
EEPROM.put(THRESHOLD_ADDRESS, originalThreshold);
int loadedThreshold = 0;
EEPROM.get(THRESHOLD_ADDRESS, loadedThreshold);
Serial.print("Loaded threshold: ");
Serial.println(loadedThreshold);
}
void loop()
{
}
預期輸出為 Loaded threshold: 700。
儲存 float 校正值
#include <EEPROM.h>
const int OFFSET_ADDRESS = 0;
void setup()
{
Serial.begin(9600);
float offset = -1.25;
EEPROM.put(OFFSET_ADDRESS, offset);
float loadedOffset = 0.0;
EEPROM.get(OFFSET_ADDRESS, loadedOffset);
Serial.print("Calibration offset: ");
Serial.println(loadedOffset, 2);
}
void loop()
{
}
正確規劃 EEPROM 位址
不同資料若從相同地址開始寫入,後寫入的內容會覆蓋前面的資料。位址配置必須依照每個資料型別的大小安排。
使用 sizeof() 計算下一個起始地址,可降低型別大小改變造成的維護問題。
| 位址範圍 | 資料 |
|---|---|
| 0~1 | int threshold |
| 2~5 | float calibration |
| 6 | byte mode |
const int THRESHOLD_ADDRESS = 0;
const int CALIBRATION_ADDRESS =
THRESHOLD_ADDRESS + sizeof(int);
const int MODE_ADDRESS =
CALIBRATION_ADDRESS + sizeof(float);
使用 Struct 集中管理多個設定
當專案有多個設定值時,可使用 struct 將資料集中管理,再用 EEPROM.put() 與 EEPROM.get() 一次保存或載入整組設定。
struct DeviceSettings
{
int temperatureThreshold;
byte brightness;
bool alarmEnabled;
float calibrationOffset;
};
第四個實驗:儲存完整設備設定
#include <EEPROM.h>
struct DeviceSettings
{
int temperatureThreshold;
byte brightness;
bool alarmEnabled;
float calibrationOffset;
};
DeviceSettings settings;
void setup()
{
Serial.begin(9600);
settings.temperatureThreshold = 35;
settings.brightness = 160;
settings.alarmEnabled = true;
settings.calibrationOffset = -0.50;
EEPROM.put(0, settings);
DeviceSettings loadedSettings;
EEPROM.get(0, loadedSettings);
Serial.print("Temperature threshold: ");
Serial.println(loadedSettings.temperatureThreshold);
Serial.print("Brightness: ");
Serial.println(loadedSettings.brightness);
Serial.print("Alarm enabled: ");
Serial.println(loadedSettings.alarmEnabled ? "YES" : "NO");
Serial.print("Calibration offset: ");
Serial.println(loadedSettings.calibrationOffset, 2);
}
void loop()
{
}
EEPROM 初始內容不一定是 0
全新或曾經使用過的 EEPROM 可能包含 255 或其他舊資料,不能只用 EEPROM.read(0) == 0 判斷是否初始化。
若直接將未知內容當成有效設定,可能導致亮度異常、門檻錯誤、模式超出範圍、浮點數無效或裝置啟動行為異常。
使用 Magic Number 驗證設定資料
可在設定結構中加入固定識別碼,例如 0x41524331。載入後若識別碼不符,就代表 EEPROM 尚未初始化、資料格式不同、資料可能損壞或程式版本已改變。
Magic Number 沒有固定標準,只要在同一專案中保持唯一且一致即可。識別碼只是有效性判斷的一部分,仍應搭配版本欄位與資料範圍驗證;更高可靠度的專案可再加入 Checksum 或 CRC。
const unsigned long SETTINGS_MAGIC = 0x41524331;
struct DeviceSettings
{
unsigned long magic;
int temperatureThreshold;
byte brightness;
bool alarmEnabled;
float calibrationOffset;
};
Magic Number 基礎安全設定範例
#include <EEPROM.h>
const unsigned long SETTINGS_MAGIC = 0x41524331;
struct DeviceSettings
{
unsigned long magic;
int temperatureThreshold;
byte brightness;
bool alarmEnabled;
float calibrationOffset;
};
DeviceSettings settings;
void setup()
{
Serial.begin(9600);
loadSettings();
printSettings();
}
void loop()
{
}
void loadSettings()
{
EEPROM.get(0, settings);
if (settings.magic != SETTINGS_MAGIC)
{
Serial.println("Invalid settings. Loading defaults.");
loadDefaultSettings();
saveSettings();
}
else
{
Serial.println("Settings loaded from EEPROM.");
}
}
void loadDefaultSettings()
{
settings.magic = SETTINGS_MAGIC;
settings.temperatureThreshold = 35;
settings.brightness = 128;
settings.alarmEnabled = true;
settings.calibrationOffset = 0.0;
}
void saveSettings()
{
EEPROM.put(0, settings);
Serial.println("Settings saved.");
}
void printSettings()
{
Serial.print("Threshold: ");
Serial.println(settings.temperatureThreshold);
Serial.print("Brightness: ");
Serial.println(settings.brightness);
Serial.print("Alarm: ");
Serial.println(settings.alarmEnabled ? "ON" : "OFF");
Serial.print("Offset: ");
Serial.println(settings.calibrationOffset, 2);
}
加入設定版本欄位
專案更新後,Struct 可能新增或刪除欄位,舊資料的位置就可能與新程式不一致。可在設定結構中加入 version 欄位。
每次設定格式改變時更新 SETTINGS_VERSION。若 Magic Number 或版本不符,就載入預設值並重新保存。
const byte SETTINGS_VERSION = 1;
struct DeviceSettings
{
unsigned long magic;
byte version;
int threshold;
byte brightness;
bool alarmEnabled;
};
if (
settings.magic != SETTINGS_MAGIC
|| settings.version != SETTINGS_VERSION
)
{
loadDefaultSettings();
saveSettings();
}
載入後驗證資料範圍
即使 Magic Number 與版本正確,其他欄位仍可能因資料損壞而不合理。載入後應逐項驗證允許範圍。
bool settingsAreValid()
{
if (settings.magic != SETTINGS_MAGIC)
{
return false;
}
if (
settings.temperatureThreshold < -20
|| settings.temperatureThreshold > 100
)
{
return false;
}
if (
settings.calibrationOffset < -20.0
|| settings.calibrationOffset > 20.0
)
{
return false;
}
return true;
}
byte 本身最大值就是 255,因此 brightness 若宣告為 byte,檢查是否大於 255 沒有實際意義。若改用 int,範圍驗證才更有作用。
恢復出廠設定
- 偵測特定按鍵或選單指令。
- 載入預設設定。
- 將預設設定寫回 EEPROM。
- 重新啟動或繼續執行。
void factoryReset()
{
loadDefaultSettings();
saveSettings();
Serial.println("Factory settings restored.");
}
清除 EEPROM 的正確方式
可以逐一將所有位址寫入 0,但清除動作本身也是寫入操作,不應頻繁執行。
多數情況只需要清除 Magic Number、有效旗標或特定設定區段,使下一次啟動時驗證失敗並載入預設值。
#include <EEPROM.h>
void setup()
{
for (int address = 0; address < EEPROM.length(); address++)
{
EEPROM.update(address, 0);
}
}
void loop()
{
}
const unsigned long INVALID_MAGIC = 0;
EEPROM.put(0, INVALID_MAGIC);
重新上傳程式後 EEPROM 仍可能保留舊資料
重新上傳 Arduino 程式時,Flash 中的程式會更新,但 EEPROM 通常不會自動清除,因此舊設定可能繼續存在。
新程式應使用 Magic Number、版本編號與範圍驗證,避免讀取舊版 Struct 留下的不相容資料。
延遲儲存策略
若使用旋鈕調整亮度,每次數值變化都立即寫入 EEPROM,仍可能產生大量寫入。
較好的方式是記錄最後變更時間,等使用者停止調整一段時間後再保存一次。
#include <EEPROM.h>
const int POT_PIN = A0;
const int BRIGHTNESS_ADDRESS = 0;
int currentBrightness = 0;
int lastSavedBrightness = -1;
unsigned long lastChangeTime = 0;
const unsigned long SAVE_DELAY = 2000;
void setup()
{
Serial.begin(9600);
lastSavedBrightness = EEPROM.read(BRIGHTNESS_ADDRESS);
}
void loop()
{
int adcValue = analogRead(POT_PIN);
int newBrightness = map(adcValue, 0, 1023, 0, 255);
if (newBrightness != currentBrightness)
{
currentBrightness = newBrightness;
lastChangeTime = millis();
}
if (
currentBrightness != lastSavedBrightness
&& millis() - lastChangeTime >= SAVE_DELAY
)
{
EEPROM.update(BRIGHTNESS_ADDRESS, currentBrightness);
lastSavedBrightness = currentBrightness;
Serial.print("Auto saved: ");
Serial.println(currentBrightness);
}
}
- 使用者調整數值。
- 程式記錄最後變更時間。
- 數值持續變動時重新計時。
- 停止變動 2 秒後寫入 EEPROM 一次。
使用明確的儲存按鍵
另一種安全設計是調整設定時只修改 SRAM,只有使用者按下 SAVE 按鍵時才呼叫 EEPROM.put()。
此方式適合設備選單、LCD 設定頁面、工業控制器、校正程序與多項參數設定。
Wear Leveling 磨耗平均
若某個數值必須較頻繁保存,可考慮 Wear Leveling。核心概念是不要永遠寫入同一位址,而是在多個位址間輪流保存,將磨耗分散到不同儲存單元。
實作 Wear Leveling 還需要處理序號、有效資料判斷、最新資料位置、循環使用與資料損壞恢復。初學階段先理解概念即可。
EEPROM 與 FRAM 的差異
| 項目 | EEPROM | FRAM |
|---|---|---|
| 斷電後保留資料 | 是 | 是 |
| 寫入速度 | 較慢 | 較快 |
| 寫入壽命 | 有限 | 通常遠高於 EEPROM |
| 常見連接方式 | 微控制器內建或外接 | 常透過 I²C 或 SPI |
| 適合用途 | 低頻率設定保存 | 高頻計數器、頻繁狀態記錄、工業設備資料 |
若專案需要高頻計數、頻繁狀態記錄或電力中斷前保存狀態,FRAM 通常比 EEPROM 更合適。
綜合實驗:可保存設定的 LED 控制器
專案功能
- 使用可變電阻調整 LED 亮度。
- 使用按鍵切換 LED 啟用狀態。
- 使用另一個按鍵保存設定。
- 使用 EEPROM Struct 保存亮度與啟用狀態。
- Arduino 開機後恢復上次設定。
- 使用 Magic Number 驗證資料。
- 不在 loop() 中持續寫入 EEPROM。
接線
- 可變電阻一側腳接 5V,中間腳接 A0,另一側腳接 GND。
- D9 經 220Ω 電阻連接 LED,再接 GND。
- 啟用按鍵由 D2 接至 GND。
- 儲存按鍵由 D3 接至 GND。
- 兩個按鍵都使用 INPUT_PULLUP。
完整程式
#include <EEPROM.h>
const int POT_PIN = A0;
const int LED_PIN = 9;
const int TOGGLE_BUTTON_PIN = 2;
const int SAVE_BUTTON_PIN = 3;
const unsigned long SETTINGS_MAGIC = 0x4C454431;
struct DeviceSettings
{
unsigned long magic;
byte brightness;
bool ledEnabled;
};
DeviceSettings settings;
int lastRawToggleState = HIGH;
int stableToggleState = HIGH;
int lastRawSaveState = HIGH;
int stableSaveState = HIGH;
unsigned long lastToggleDebounceTime = 0;
unsigned long lastSaveDebounceTime = 0;
const unsigned long DEBOUNCE_DELAY = 50;
void setup()
{
pinMode(LED_PIN, OUTPUT);
pinMode(TOGGLE_BUTTON_PIN, INPUT_PULLUP);
pinMode(SAVE_BUTTON_PIN, INPUT_PULLUP);
Serial.begin(9600);
loadSettings();
applyLedOutput();
printSettings();
}
void loop()
{
updateBrightness();
checkToggleButton();
checkSaveButton();
applyLedOutput();
}
void loadSettings()
{
EEPROM.get(0, settings);
if (settings.magic != SETTINGS_MAGIC)
{
loadDefaultSettings();
saveSettings();
Serial.println("Default settings created.");
}
else
{
Serial.println("Settings loaded.");
}
}
void loadDefaultSettings()
{
settings.magic = SETTINGS_MAGIC;
settings.brightness = 128;
settings.ledEnabled = true;
}
void saveSettings()
{
EEPROM.put(0, settings);
Serial.println("Settings saved to EEPROM.");
}
void updateBrightness()
{
int adcValue = analogRead(POT_PIN);
int mappedBrightness = map(adcValue, 0, 1023, 0, 255);
settings.brightness = constrain(mappedBrightness, 0, 255);
}
void checkToggleButton()
{
int rawButtonState = digitalRead(TOGGLE_BUTTON_PIN);
if (rawButtonState != lastRawToggleState)
{
lastToggleDebounceTime = millis();
}
if (millis() - lastToggleDebounceTime >= DEBOUNCE_DELAY)
{
if (rawButtonState != stableToggleState)
{
stableToggleState = rawButtonState;
if (stableToggleState == LOW)
{
settings.ledEnabled = !settings.ledEnabled;
Serial.print("LED enabled: ");
Serial.println(settings.ledEnabled ? "YES" : "NO");
}
}
}
lastRawToggleState = rawButtonState;
}
void checkSaveButton()
{
int rawButtonState = digitalRead(SAVE_BUTTON_PIN);
if (rawButtonState != lastRawSaveState)
{
lastSaveDebounceTime = millis();
}
if (millis() - lastSaveDebounceTime >= DEBOUNCE_DELAY)
{
if (rawButtonState != stableSaveState)
{
stableSaveState = rawButtonState;
if (stableSaveState == LOW)
{
saveSettings();
printSettings();
}
}
}
lastRawSaveState = rawButtonState;
}
void applyLedOutput()
{
analogWrite(LED_PIN, settings.ledEnabled ? settings.brightness : 0);
}
void printSettings()
{
Serial.print("Brightness: ");
Serial.println(settings.brightness);
Serial.print("LED enabled: ");
Serial.println(settings.ledEnabled ? "YES" : "NO");
}
旋鈕位置與保存亮度的衝突
程式載入 EEPROM 後,loop() 很快就會以可變電阻目前位置重新計算亮度,因此保存值可能只短暫生效。這不是 EEPROM 失效,而是 EEPROM 恢復值與絕對位置旋鈕同時控制同一參數。
可改用按鍵增加或減少亮度、加入旋鈕接管機制,或只保存模式、啟用狀態與校正值。
if (abs(newBrightness - lastPotBrightness) > 5)
{
potHasTakenControl = true;
}
EEPROM 除錯技巧
- 確認程式頂端已加入 #include <EEPROM.h>。
- 確認位址未超出 EEPROM.length()。
- 使用 sizeof() 檢查資料是否互相覆蓋。
- 寫入與讀取時使用相同資料型別。
- 確認 Magic Number、Struct 與起始地址一致。
- 將實際資料輸出到 Serial Monitor 檢查。
- 確認 EEPROM.write()、EEPROM.update() 與 EEPROM.put() 沒有在每次 loop() 都執行。
- Struct 格式改變後更新版本編號或使舊 Magic Number 失效。
Serial.println(settings.magic, HEX);
Serial.println(settings.brightness);
Serial.println(settings.alarmEnabled);
初學者常見錯誤
在 loop() 中持續寫入
每次迴圈都呼叫 EEPROM.write() 會快速消耗同一位址的寫入壽命。
用 EEPROM.write() 儲存大於 255 的值
EEPROM.write(0, 700); 只能保存最低 1 Byte,無法完整保留 700。應改用 EEPROM.put()。
位址重疊
若 int 從位址 0 開始並占用 0~1,下一個 float 不可從位址 1 開始,否則會覆蓋部分 int。
假設空白 EEPROM 一定是 0
EEPROM 可能包含 255、舊資料、測試資料或不完整資料,應使用 Magic Number、版本與範圍驗證。
每次開機都重新寫入預設值
預設值應只在資料無效時寫入,而不是每次 setup() 都直接 EEPROM.put()。
修改 Struct 後未更新版本
舊資料格式與新程式不一致,可能造成欄位錯位與異常值。
將 EEPROM 當成資料記錄器
每秒寫入一次,一天就會寫入 86,400 次,很快接近單一位址的典型寫入壽命。大量記錄應改用 SD Card、FRAM 或其他儲存方式。
儲存未初始化的 Struct
寫入 Struct 前應先為每個欄位設定明確值,避免把不確定內容保存到 EEPROM。
EEPROM 實務使用原則
- 只保存真正需要跨重新啟動保留的資料。
- 不要在每次 loop() 中寫入。
- 優先使用 EEPROM.update() 或 EEPROM.put()。
- 使用 EEPROM.length() 檢查容量。
- 使用 sizeof() 安排地址。
- 多個設定集中在 Struct。
- 加入 Magic Number 驗證資料。
- 設定格式改變時使用版本管理。
- 載入後檢查資料範圍。
- 提供恢復預設設定機制。
- 使用延遲保存或明確的儲存按鍵。
- 高頻率寫入需求改用 FRAM 或其他儲存設備。
本章實作練習
練習一:儲存單一 Byte
將 LED 亮度 180 寫入 EEPROM 位址 0,重新啟動後讀取並在 Serial Monitor 顯示 Brightness: 180。
練習二:使用 EEPROM.update()
建立一個按鍵,每次按下時才保存 LED ON 或 OFF 狀態,重新啟動後恢復上次狀態。
練習三:儲存 int 門檻值
使用 EEPROM.put() 保存溫度門檻 35,再使用 EEPROM.get() 讀取。
練習四:儲存 float 校正值
保存 -1.25 作為溫度感測器校正偏移量,並顯示 Calibration: -1.25。
練習五:設定結構
建立包含 threshold、brightness 與 alarmEnabled 的 DeviceSettings,使用 EEPROM.put() 與 EEPROM.get() 保存完整結構。
練習六:Magic Number
為設定結構加入 Magic Number。EEPROM 資料無效時載入預設值、存回 EEPROM,並顯示 Default settings loaded。
Challenge:可保存設定的環境警報器
- 使用可變電阻設定警報門檻。
- 使用按鍵切換警報啟用或停用。
- 使用另一個按鍵儲存設定。
- 使用 Struct 保存 Magic Number、版本、警報門檻、警報啟用狀態與感測器校正值。
- Arduino 開機後載入設定。
- 資料無效時載入預設值。
- 只有按下儲存按鍵時才寫入 EEPROM。
- 不可在 loop() 中持續寫入。
- 使用自訂函式整理程式。
struct AlarmSettings
{
unsigned long magic;
byte version;
int threshold;
bool alarmEnabled;
float calibrationOffset;
};
loadSettings();
saveSettings();
loadDefaultSettings();
validateSettings();
readControls();
updateAlarm();
printSettings();
factoryReset();
參考輸出可包含 Settings loaded、Threshold: 35、Alarm: ON 與 Calibration: -0.50。
儲存時顯示 Settings saved;資料無效時顯示 Invalid EEPROM data 與 Default settings restored。
本章重點整理
- EEPROM 是斷電後仍可保留資料的非揮發性記憶體。
- Arduino Uno 的 EEPROM 容量為 1024 Bytes。
- EEPROM.read() 與 EEPROM.write() 每次處理一個 Byte。
- EEPROM.update() 只在資料改變時寫入。
- EEPROM.get() 與 EEPROM.put() 可以處理完整資料型別。
- sizeof() 可協助安排 EEPROM 地址。
- Struct 適合集中管理多個設定。
- EEPROM 初始資料不一定是 0。
- Magic Number、版本與範圍驗證可提高設定可靠性。
- EEPROM 寫入次數有限,不應在 loop() 中持續寫入。
- 按鍵儲存或延遲儲存可減少寫入次數。
- 大量或高頻資料記錄應使用 SD Card、FRAM 或其他儲存裝置。
下一章預告
第 17 章將介紹 Arduino Timer 與精準時間控制,包含 Timer0、Timer1、Timer2、Prescaler、Overflow、Compare Match、Timer 中斷,以及 millis()、micros()、PWM、Servo 與 Timer 之間的關係。