關閉

第16章 Arduino EEPROM 教學:非揮發性記憶體、資料讀寫與設定保存

完整介紹 Arduino EEPROM 非揮發性記憶體的用途與操作方式,說明 Flash、SRAM 與 EEPROM 的差異,並實作 EEPROM.read()、write()、update()、get() 與 put()。內容涵蓋 int、float、Struct 儲存、位址配置、Magic Number、版本驗證、恢復預設值及減少 EEPROM 寫入次數的方法,適合 Arduino 初學者建立可在斷電後保留設定的控制系統。

Arduino 執行期間常用 SRAM 變數保存亮度、門檻、模式或校正值,但只要斷電、按下 Reset 或重新啟動,這些內容就會消失。本章將介紹如何使用 EEPROM,讓裝置重新開機後仍能恢復重要設定。

內容涵蓋 EEPROM.read()、EEPROM.write()、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。

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,斷電後仍能保留資料。

EEPROM 適合與不適合儲存的資料

適合儲存

不適合儲存

大量或高頻資料記錄通常應改用 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 寫入指定地址。

#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.update(0, 150);

第二個實驗:儲存 LED 亮度

實驗目標

接線

程式

#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);
  }
}

程式運作流程

此範例使用 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() 的實際結果為準。

手動拆分與組合 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() 計算下一個起始地址,可降低型別大小改變造成的維護問題。

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,範圍驗證才更有作用。

恢復出廠設定

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);
  }
}

使用明確的儲存按鍵

另一種安全設計是調整設定時只修改 SRAM,只有使用者按下 SAVE 按鍵時才呼叫 EEPROM.put()。

此方式適合設備選單、LCD 設定頁面、工業控制器、校正程序與多項參數設定。

Wear Leveling 磨耗平均

若某個數值必須較頻繁保存,可考慮 Wear Leveling。核心概念是不要永遠寫入同一位址,而是在多個位址間輪流保存,將磨耗分散到不同儲存單元。

實作 Wear Leveling 還需要處理序號、有效資料判斷、最新資料位置、循環使用與資料損壞恢復。初學階段先理解概念即可。

EEPROM 與 FRAM 的差異

若專案需要高頻計數、頻繁狀態記錄或電力中斷前保存狀態,FRAM 通常比 EEPROM 更合適。

綜合實驗:可保存設定的 LED 控制器

專案功能

接線

完整程式

#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 除錯技巧

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 實務使用原則

本章實作練習

練習一:儲存單一 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 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。

本章重點整理

下一章預告

第 17 章將介紹 Arduino Timer 與精準時間控制,包含 Timer0、Timer1、Timer2、Prescaler、Overflow、Compare Match、Timer 中斷,以及 millis()、micros()、PWM、Servo 與 Timer 之間的關係。