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

發布日期: |更新日期:
文章主圖:第16章 Arduino EEPROM 教學:非揮發性記憶體、資料讀寫與設定保存

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。

知識卡:EEPROM
項目 說明
全名 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 比較
項目 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 寫入指定地址。

EEPROM.write() 參數
參數 說明
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.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);
  }
}

程式運作流程

  1. Arduino 開機後從 EEPROM 讀取亮度。
  2. LED 恢復上次亮度。
  3. 轉動可變電阻調整亮度。
  4. 按下儲存按鍵。
  5. 將目前亮度寫入 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() 的實際結果為準。

Arduino Uno 常見資料型別大小
資料型別 大小
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() 計算下一個起始地址,可降低型別大小改變造成的維護問題。

EEPROM 位址配置範例
位址範圍 資料
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,範圍驗證才更有作用。

恢復出廠設定

  1. 偵測特定按鍵或選單指令。
  2. 載入預設設定。
  3. 將預設設定寫回 EEPROM。
  4. 重新啟動或繼續執行。
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);
  }
}
  1. 使用者調整數值。
  2. 程式記錄最後變更時間。
  3. 數值持續變動時重新計時。
  4. 停止變動 2 秒後寫入 EEPROM 一次。

使用明確的儲存按鍵

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

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

Wear Leveling 磨耗平均

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

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

EEPROM 與 FRAM 的差異

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

  1. 確認程式頂端已加入 #include <EEPROM.h>。
  2. 確認位址未超出 EEPROM.length()。
  3. 使用 sizeof() 檢查資料是否互相覆蓋。
  4. 寫入與讀取時使用相同資料型別。
  5. 確認 Magic Number、Struct 與起始地址一致。
  6. 將實際資料輸出到 Serial Monitor 檢查。
  7. 確認 EEPROM.write()、EEPROM.update() 與 EEPROM.put() 沒有在每次 loop() 都執行。
  8. 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 實務使用原則

  1. 只保存真正需要跨重新啟動保留的資料。
  2. 不要在每次 loop() 中寫入。
  3. 優先使用 EEPROM.update() 或 EEPROM.put()。
  4. 使用 EEPROM.length() 檢查容量。
  5. 使用 sizeof() 安排地址。
  6. 多個設定集中在 Struct。
  7. 加入 Magic Number 驗證資料。
  8. 設定格式改變時使用版本管理。
  9. 載入後檢查資料範圍。
  10. 提供恢復預設設定機制。
  11. 使用延遲保存或明確的儲存按鍵。
  12. 高頻率寫入需求改用 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 之間的關係。

相關資源與文章匯出
下載 Arduino 入門基礎教材 下載 DOCX PDF/列印

← 回文章列表