關閉

第20章 Arduino Debug(除錯)與系統化故障排除(下):記憶體、Watchdog、示波器與長時間測試

本章延續 Arduino Debug 系統化故障排除教學,深入說明 Arduino Uno 的 Flash、SRAM、EEPROM、Stack 與 Heap 記憶體配置,並分析陣列越界、Buffer Overflow、未初始化變數、String 碎片化與 SRAM 不足造成的隨機重啟問題。內容也涵蓋 Watchdog Timer、Brown-out、Reset 原因記錄、示波器與邏輯分析儀操作、UART、I²C、SPI 通訊分析、中斷競爭條件、Atomic Access、長時間 Soak Test、故障注入、回歸測試,以及 Diagnostic Console 診斷控制台的設計方法,協助建立穩定、安全且可維護的 Arduino 系統。

本章重點與適用情境

本章承接第 20 章上半部,進一步處理 Arduino 專案中最難追查的間歇性故障,包括記憶體耗盡、陣列越界、Watchdog Reset、Brown-out、通訊時序、中斷競爭與長時間累積錯誤。

內容以 Arduino Uno R3 與 ATmega328P 為主要範例,部分記憶體、Reset Flag、Watchdog 與 PROGMEM 程式具有 AVR 平台相依性,移植到 ESP32、SAMD 或其他架構前必須查閱對應平台文件。

完成本章後,讀者應能建立可重複使用的診斷流程,搭配 Serial、示波器、邏輯分析儀、事件記錄、Soak Test 與 Diagnostic Console 找出根本原因,而不是只靠反覆修改程式碰運氣。

學習目標

為什麼有些 Arduino 故障無法立即重現

無法編譯、無法上傳、I²C 找不到裝置或 LED 完全不亮,通常有明確現象;真正棘手的是執行數分鐘或數小時後才停止、偶爾重啟、顯示亂碼、增加一行 Serial.println() 後問題消失,或馬達啟動時 Arduino 才重新開機。

這類問題常同時涉及記憶體、時序、電源、中斷、通訊競爭、未初始化資料與長時間累積錯誤。除錯時必須記錄條件、建立最小可重現程式、一次只改一個因素,並使用量測工具收集證據。

Arduino Uno 的 Flash、SRAM 與 EEPROM

Arduino IDE 顯示的 Sketch uses ... bytes of program storage space 是 Flash 使用量;Global variables use ... bytes of dynamic memory 則主要反映全域與靜態資料預先占用的 SRAM。

編譯器顯示仍有數百 Bytes 可用,不代表執行時一定安全,因為剩餘 SRAM 還要供區域變數、函式呼叫、Stack、中斷、函式庫 Buffer 與動態配置使用。

SRAM 的配置概念

SRAM 低位址通常放置全域與靜態資料,Heap 由低位址向上成長,Stack 則由高位址向下成長。當 Heap 與 Stack 彼此接近甚至碰撞,系統可能覆寫變數、輸出亂碼、卡住、跳到錯誤分支或隨機 Reset。

Arduino Uno 只有約 2048 Bytes SRAM,因此顯示器畫面 Buffer、大型陣列、Serial Buffer、I²C Buffer 與動態 String 可能很快吃完可用空間。

Stack 與大型區域陣列

void processData()
{
  int samples[500];
}

在 AVR Arduino Uno 上,int 通常占 2 Bytes,因此 samples[500] 約占 1000 Bytes。若同時存在其他區域變數、巢狀函式呼叫或中斷,系統很容易發生 Stack Overflow。

無停止條件的遞迴會不斷建立新的函式呼叫並耗盡 Stack。嵌入式系統應避免無明確深度限制的遞迴。

Heap 與記憶體碎片化

malloc()、calloc()、realloc()、new 與部分 String 操作會使用 Heap。反覆配置、釋放不同大小的區塊,可能留下許多不連續空隙;即使總剩餘空間足夠,也可能找不到足夠大的連續區塊。

在只有 2 KB SRAM 的 Arduino Uno 上,這種碎片化特別容易造成長時間後才出現的故障。

String 的風險與 reserve()

String message;

void setup()
{
  message.reserve(64);
}

void loop()
{
  message = "ADC=" + String(analogRead(A0))
          + ",TIME=" + String(millis());
  Serial.println(message);
}

String 並非完全不能使用。它適合快速原型與短時間實驗,但頻繁串接、長度反覆變化及建立多個暫時物件,都可能增加重新配置與碎片化風險。

reserve(64) 可預先保留容量,降低一定長度範圍內反覆配置的機會,但不保證永遠安全;字串超過保留容量時仍可能重新配置。

固定 char Buffer 與 snprintf()

char message[64];

int adcValue = analogRead(A0);

snprintf(
  message,
  sizeof(message),
  "ADC=%d TIME=%lu",
  adcValue,
  millis()
);

Serial.println(message);

固定字元 Buffer 的容量可預測,不需要反覆動態配置,也便於做長度檢查。snprintf() 的第二個參數應使用 sizeof(message),避免寫入超過緩衝區。

處理不確定長度資料時,不建議使用沒有長度限制的 sprintf() 或 strcpy()。

Buffer Overflow 與陣列越界

char text[5];
strcpy(text, "Arduino");

text 只有 5 Bytes,但 Arduino 需要 7 個字元加上結尾的 \0,共至少 8 Bytes。超出範圍的資料會覆寫其他記憶體,可能造成變數改變、程式跳轉錯誤、卡住或隨機 Reset。

int values[5];

for (int index = 0; index <= 5; index++)
{
  values[index] = 0;
}

values[5] 已超出合法索引 0 至 4。迴圈條件應改為 index < 5,或由陣列本身計算元素數量。

int values[5];

const size_t VALUE_COUNT =
  sizeof(values) / sizeof(values[0]);

for (size_t index = 0; index < VALUE_COUNT; index++)
{
  values[index] = 0;
}

未初始化變數與指標錯誤

void loop()
{
  static unsigned long count = 0;

  count++;
  Serial.println(count);
}

一般區域變數若未明確初始化,初始內容不可預測。一般只在單次函式呼叫中使用的變數應明確初始化;若 count 需要跨越多次 loop() 保留累積值,應使用 static 區域變數或全域變數。全域與靜態儲存期變數通常會初始化為 0,但仍建議明確指定初始值,以提高可讀性與維護性。

空指標、野指標或回傳區域變數位址,都可能讓程式寫入無效記憶體。Arduino Uno 通常不會顯示 Segmentation Fault,而是直接卡住、重啟或執行錯亂。

void createMessage(char *buffer, size_t bufferSize)
{
  snprintf(buffer, bufferSize, "Hello");
}

char message[20];
createMessage(message, sizeof(message));

sizeof()、F() 與 PROGMEM

sizeof() 可取得資料型別、變數、陣列或物件的大小。函式參數 char *buffer 已退化為指標,因此 sizeof(buffer) 得到的是指標大小,不是原始陣列容量;容量應由呼叫端另外傳入。

大量固定除錯文字可使用 F() 保留在 Flash,減少 SRAM 占用。大型固定查詢表則可考慮 PROGMEM,但 AVR 與其他 Arduino 架構的讀取方式不同。

Serial.println(
  F("Sensor initialization failed")
);
const uint16_t lookupTable[] PROGMEM =
{
  100,
  200,
  300,
  400
};

uint16_t value =
  pgm_read_word(&lookupTable[index]);

顯示器 Buffer 的 SRAM 風險

128 × 64 單色 OLED 的完整畫面 Buffer 需要 128 × 64 ÷ 8,也就是 1024 Bytes,約占 Arduino Uno 一半 SRAM。若再加入 Serial、I²C、字串、感測器陣列與區域變數,系統很容易接近極限。

加入 OLED 後程式重啟,不一定是顯示器故障。應比較加入前後的 Dynamic Memory,減少大型陣列與字串、使用 F()、停用無關函式庫,並評估是否需要較大 SRAM 的開發板。

估算 Free SRAM

extern unsigned int __heap_start;
extern void *__brkval;

int freeMemory()
{
  int localVariable;
  int heapTop;

  if (__brkval == nullptr)
  {
    heapTop = reinterpret_cast<int>(&__heap_start);
  }
  else
  {
    heapTop = reinterpret_cast<int>(__brkval);
  }

  return reinterpret_cast<int>(&localVariable) - heapTop;
}

這類函式高度依賴 AVR 記憶體模型,只能估算當下 Stack 與 Heap 之間的距離,無法完整判斷碎片化,也不能保證未來函式呼叫不會耗盡 Stack。移植到 ESP32、SAMD 或其他架構時不可直接照用。

SRAM 故障排查流程

Watchdog Timer 與 Reset 原因

Watchdog Timer 是獨立計時機制。程式必須定期重設 Watchdog;若因無限迴圈、死鎖、通訊卡住或邏輯錯誤而未在期限內重設,Watchdog 可讓微控制器重新啟動。

Watchdog 是故障恢復工具,不是除錯替代品。若設備每分鐘都被 Watchdog 重啟,表面上仍在運作,根本問題仍然存在。

AVR Watchdog 基本範例

#include <avr/wdt.h>

void setup()
{
  Serial.begin(9600);
  wdt_enable(WDTO_2S);
}

void loop()
{
  wdt_reset();
  // 正常工作
}

模擬 Watchdog Reset

#include <avr/wdt.h>

void setup()
{
  Serial.begin(9600);
  Serial.println(F("System started"));
  wdt_enable(WDTO_2S);
}

void loop()
{
  wdt_reset();

  if (millis() > 5000)
  {
    while (true)
    {
      // 模擬程式卡住
    }
  }
}

約 5 秒後程式進入無限迴圈,接著在約 2 秒 Timeout 後 Reset。測試完成後應移除故障注入程式,避免裝置持續重啟。

Watchdog Timeout 的選擇

bool sensorOk = updateSensor();
bool communicationOk = updateCommunication();
bool controlOk = updateControl();

if (sensorOk && communicationOk && controlOk)
{
  wdt_reset();
}

避免 Watchdog 重啟循環

若開機初始化時間超過 Watchdog Timeout,系統可能在初始化尚未完成前 Reset,然後再次進入相同流程。開機初期應先正確處理既有 Watchdog 狀態,確保初始化時間符合 Timeout,並保存 Reset 原因。

Reset 原因擷取與啟動區段程式屬於 AVR 平台相依技術,必須在目標板上仔細測試。

#include <avr/io.h>
#include <avr/wdt.h>

uint8_t resetFlags
  __attribute__((section(".noinit")));

void captureResetFlags()
  __attribute__((naked))
  __attribute__((section(".init3")));

void captureResetFlags()
{
  resetFlags = MCUSR;
  MCUSR = 0;
  wdt_disable();
}

解析 Reset Flag

void printResetReason()
{
  Serial.print(F("RESET_FLAGS=0x"));
  Serial.println(resetFlags, HEX);

  if (resetFlags & _BV(WDRF))
  {
    Serial.println(F("RESET=WATCHDOG"));
  }

  if (resetFlags & _BV(BORF))
  {
    Serial.println(F("RESET=BROWN_OUT"));
  }

  if (resetFlags & _BV(PORF))
  {
    Serial.println(F("RESET=POWER_ON"));
  }

  if (resetFlags & _BV(EXTRF))
  {
    Serial.println(F("RESET=EXTERNAL"));
  }
}

需要跨重啟保留資訊時,可將 Reset 原因、錯誤代碼、最後任務、啟動次數或 Watchdog 次數保存到 EEPROM。應使用 EEPROM.update()、狀態改變時才寫入、延遲寫入或 Wear Leveling,避免高頻率重複寫入。

Brown-out 與電源瞬降

Brown-out 是供電電壓下降到不足以可靠運作、但不一定完全斷電的狀態。馬達啟動、Servo 突然動作、繼電器吸合、USB 電源不足、電池下降、穩壓器過熱、導線壓降與負載過大都可能造成 Brown-out。

結果可能包括 Arduino Reset、感測器失聯、通訊錯誤、顯示器閃爍、EEPROM 寫入中斷或程式隨機異常。

馬達啟動造成 Arduino 重啟

為什麼萬用電表可能看不到瞬間掉電

萬用電表更新速度通常較慢。若電壓只下降數百微秒或數毫秒,畫面可能仍顯示接近 5V。這時應使用示波器、具 Min/Max 記錄的電表、電源監控 IC 或 Reset 原因記錄。

退耦電容與電源排查

0.1 µF 陶瓷電容通常應靠近 IC 的 VCC 與 GND 腳位,較大容量電容可協助處理較慢的負載變化。電容不是萬能解法;若電源額定電流不足、線路壓降過大或驅動方式錯誤,仍必須改善供電架構。

使用示波器觀察 Arduino 訊號

示波器顯示電壓隨時間變化,適合觀察 GPIO HIGH/LOW、PWM、UART、I²C Clock、SPI Clock、類比輸出、電源紋波、瞬間掉電、雜訊與脈衝寬度。

垂直軸代表電壓,水平軸代表時間。常見設定包括 Volts per Division、Time per Division 與 Trigger。

探棒接地安全

低電壓 Arduino 實驗中,探棒尖端接被測訊號,地夾接 Arduino GND。桌上型示波器地夾通常與保護接地相連,若誤接到非地端高電位節點,可能造成短路、設備損壞或人身風險。

不可把一般探棒地夾當成任意兩點的差動量測工具。需要量測兩個非地端節點時,應使用差動探棒、隔離量測設備或經確認安全的雙通道數學方法。本章只討論低電壓 Arduino 電路。

觀察 GPIO 方波

const int TEST_PIN = 8;

void setup()
{
  pinMode(TEST_PIN, OUTPUT);
}

void loop()
{
  digitalWrite(TEST_PIN, HIGH);
  delay(1);
  digitalWrite(TEST_PIN, LOW);
  delay(1);
}

預期波形約為 1 ms HIGH、1 ms LOW,完整週期約 2 ms,頻率約 500 Hz。若沒有波形,檢查程式是否執行、腳位是否正確及探棒是否共地。

觀察 PWM 與 Duty Cycle

analogWrite(9, 128);

示波器應看到約 0V 的 LOW、約 5V 的 HIGH,以及接近 50% 的 Duty Cycle。analogWrite() 輸出的是 PWM,不是穩定類比電壓。

Duty Cycle = HIGH 時間 ÷ 完整週期 × 100%。若 HIGH 為 1 ms、週期為 2 ms,Duty Cycle 為 50%。

Trigger 與電源波形

Trigger 可讓重複波形穩定顯示,例如選擇 Rising Edge、觸發來源 CH1、觸發電壓約 2.5V。Trigger 設定錯誤時,波形可能在畫面上漂移。

量測 5V 電源時,可觀察平均電壓、紋波、負載切換雜訊與 Reset 前後的瞬間下降。觀察小幅紋波時可能需要 AC Coupling、適當 Offset、較小垂直刻度與短探棒地線。

常見示波器判讀

使用邏輯分析儀分析 UART、I²C 與 SPI

邏輯分析儀主要判斷數位 HIGH/LOW,可同時擷取多個 Channel,適合通訊解碼、多訊號時序比較與長時間捕捉偶發事件。它無法取代示波器對實際電壓、振鈴、過衝與電源雜訊的觀察。

接線時必須將 Logic Analyzer GND 接 Arduino GND,並確認輸入電壓範圍。只支援 3.3V 的輸入不可直接接 5V 訊號,除非設備明確標示可容忍。

Sampling Rate

取樣率必須高於被測訊號頻率,實務上通常需要數倍以上。若 SPI Clock 為 1 MHz,取樣率只有 1 MHz 通常不足,可考慮 10 MHz、20 MHz 或更高,並保留足夠餘裕。

UART 解碼

I²C 解碼

I²C 解碼器可顯示 Start、Address、Read/Write、ACK/NACK、Data 與 Stop。若 Address 後立即 NACK,常見原因包括位址錯誤、裝置未供電、尚未準備、接線問題、模組損壞或電壓不相容。

若 Address ACK 但資料階段 NACK,可能是暫存器地址錯誤、裝置狀態不接受命令或通訊時序問題。某些裝置會使用 Clock Stretching,將 SCL 暫時拉低要求主機等待。

START
0x3C WRITE ACK
0x00 ACK
0xAE ACK
STOP

SPI 解碼與 MISO 衝突

中斷、volatile 與競爭條件

Race Condition 發生在 loop()、Timer ISR、External Interrupt ISR、Serial ISR 或函式庫 ISR,以不可預測順序存取共享資料時。

volatile 告訴編譯器變數可能在目前流程之外被修改,避免只使用暫存器中的舊值;但 volatile 不保證多 Byte 讀寫具 Atomic 性。

volatile 不等於 Atomic

volatile unsigned long pulseCount = 0;
unsigned long pulseCopy;

noInterrupts();
pulseCopy = pulseCount;
interrupts();

Serial.println(pulseCopy);

Arduino Uno 是 8 位元 MCU,讀取 32 位元 unsigned long 需要多次操作。若 ISR 在讀取中途修改資料,主程式可能取得混合的新舊值。應在極短的 Critical Section 中複製共享資料,再於中斷恢復後處理。

若需要保留呼叫前的中斷狀態,AVR 進階程式可保存 SREG 後使用 cli(),完成後再還原 SREG;這是平台相依作法。

uint8_t oldSreg = SREG;
cli();
pulseCopy = pulseCount;
SREG = oldSreg;

單一 bool Flag 可能遺失事件

若 ISR 連續發生三次,而 loop() 尚未處理,eventFlag 仍只會是 true,三次事件會被合併成一次。每次事件都重要時,應改用計數器並處理飽和。

volatile byte pendingEvents = 0;
volatile bool eventOverflow = false;

void onEvent()
{
  if (pendingEvents < 255)
  {
    pendingEvents++;
  }
  else
  {
    eventOverflow = true;
  }
}

安全取出事件數量

byte takePendingEvents()
{
  noInterrupts();

  byte count = pendingEvents;
  pendingEvents = 0;

  interrupts();
  return count;
}

void loop()
{
  byte eventCount = takePendingEvents();

  while (eventCount > 0)
  {
    processEvent();
    eventCount--;
  }
}

臨界區必須保持極短

不要在 noInterrupts() 與 interrupts() 之間執行 Serial.println()、EEPROM 寫入、顯示更新或其他耗時工作。只複製必要資料,立即恢復中斷。

millis() 與 micros() 依賴 Timer 中斷。長時間關閉中斷會使 millis() 暫停、micros() 精度下降、delay() 異常及系統時間偏差。ISR 內也不應依賴 millis() 長時間計時。

間歇性故障記錄與 Ring Buffer

間歇性問題需要長時間收集資料。建議記錄啟動次數、最長運作時間、最後成功任務、Reset 原因、通訊錯誤、感測器 Timeout、Watchdog 次數、Free SRAM、最小供電電壓與最後錯誤代碼。

enum EventCode
{
  EVENT_BOOT,
  EVENT_SENSOR_ERROR,
  EVENT_I2C_TIMEOUT,
  EVENT_LOW_VOLTAGE,
  EVENT_WATCHDOG_RESET
};

struct EventRecord
{
  uint32_t timestamp;
  uint16_t code;
  int16_t value;
};

RAM 環形事件記錄

const byte EVENT_CAPACITY = 16;

EventRecord eventLog[EVENT_CAPACITY];
byte eventWriteIndex = 0;

void logEvent(uint16_t code, int16_t value)
{
  eventLog[eventWriteIndex] =
  {
    millis(),
    code,
    value
  };

  eventWriteIndex++;

  if (eventWriteIndex >= EVENT_CAPACITY)
  {
    eventWriteIndex = 0;
  }
}

Ring Buffer 使用固定記憶體,新事件覆蓋最舊事件,不會隨時間無限成長,適合長時間運作。缺點是 SRAM 容量有限、重啟後資料消失,並需處理正確的讀取順序。

EEPROM 適合保存致命錯誤、Reset 前最後狀態、啟動次數與低頻故障統計;不適合保存每次 loop、每毫秒取樣或每次 Serial 傳輸。

Soak Test 與長時間穩定性

Soak Test 是讓設備持續運行數小時、數天或數週,觀察記憶體是否逐漸下降、錯誤是否累積、裝置是否重啟、溫度是否升高、計時是否漂移、Buffer 是否塞滿與感測器是否失聯。

millis() 溢位安全寫法

if (currentMillis - previousMillis >= interval)
{
  previousMillis = currentMillis;
  // 執行週期工作
}

millis() 在 32 位元無號整數上約 49.7 天溢位。使用無號時間差 currentMillis - previousMillis,可在合理間隔範圍內正確處理溢位。不要使用 currentMillis >= previousMillis + interval。

測量最大 Loop 執行時間

unsigned long maximumLoopTime = 0;

void loop()
{
  unsigned long loopStartMicros = micros();

  updateSensor();
  updateControl();
  updateCommunication();

  unsigned long loopTime =
    micros() - loopStartMicros;

  if (loopTime > maximumLoopTime)
  {
    maximumLoopTime = loopTime;
  }
}

平均 Loop 很快,不代表沒有偶發阻塞。SD Card 寫入、I²C Timeout、Buffer Flush、EEPROM 寫入或大量 Serial 輸出,可能讓某一次 Loop 從數百微秒增加到數十毫秒甚至數秒。

可為每個任務記錄執行時間與最後成功時間,超過門檻時寫入事件記錄或設定錯誤旗標。

通訊恢復與降級模式

const byte MAX_RETRIES = 3;
bool sensorReady = false;

for (byte attempt = 0; attempt < MAX_RETRIES; attempt++)
{
  if (sensor.begin())
  {
    sensorReady = true;
    break;
  }

  delay(100);
}

if (!sensorReady)
{
  enterDegradedMode();
}

不要使用 while (!sensor.begin()) 無限制重試,否則裝置不存在時 Arduino 會永遠卡在 setup()。Degraded Mode 可在部分功能失效時保留通訊、錯誤警示與其他安全功能。

Fail-safe State 是故障時的安全輸出,例如馬達停止、加熱器關閉、雷射關閉、繼電器解除、PWM 設為 0。應先進入安全狀態,再記錄與嘗試恢復。

建立 Arduino Diagnostic Console

Diagnostic Console 是透過 Serial 提供的診斷命令介面,可查看系統狀態、錯誤計數、Reset 原因、記憶體、硬體自我測試、GPIO、I²C Scanner、Debug Level 與重新初始化結果。

診斷命令必須限制可操作腳位與危險功能。D0、D1 可能供 Serial 使用,Timer、SPI、馬達、繼電器與高風險負載也不可被任意控制。

GPIO 安全允許清單

bool isDiagnosticOutputPin(int pin)
{
  return pin == 7
      || pin == 8
      || pin == LED_BUILTIN;
}

系統診斷資料結構

struct SystemDiagnostics
{
  unsigned long bootCount;
  unsigned long loopCount;
  unsigned int sensorErrors;
  unsigned int communicationErrors;
  unsigned int watchdogResets;
  unsigned long maximumLoopMicros;
  int minimumFreeMemory;
  bool sensorReady;
  bool systemReady;
};

Active Error 與 Historical Error

建議分開保存目前仍存在的 Active Error,以及曾經發生過的 Historical Error Count。I²C 暫時失敗後恢復時,Active I²C Error 可清除,但歷史錯誤次數應保留。

錯誤嚴重程度可分為 Info、Warning、Error 與 Fatal。一次感測器重試可能只是 Warning;持續失聯可能是 Error;安全輸入失效或危險輸出無法關閉則可能是 Fatal。

完整診斷控制台的功能範圍

診斷架構限制

故障注入與回歸測試

Fault Injection 是刻意製造可控錯誤,確認系統能正確偵測、記錄、進入安全狀態並恢復。可拔除 I²C 裝置、改錯位址、模擬函式失敗、通訊 Timeout、計數器飽和、低記憶體或 Watchdog Timeout。

不得為了測試而短路 5V 與 GND、將高電壓接入 GPIO、讓馬達撞擊、讓加熱器無限制加熱或超出元件電流規格。應優先使用軟體模擬與低風險斷線方式。

回歸測試

修正 Timer、I²C、PWM 或中斷問題後,必須確認既有功能沒有被破壞。修正 Timer 後應重新測試 millis()、Servo、PWM、tone()、Serial 與 LED 週期;修正 I²C 後應重測顯示器、其他感測器、位址衝突與 Bus Speed。

Arduino 完整 Debug Checklist

A. 問題描述

B. 開發環境

C. 編譯與上傳

D. 電源與接線

E. GPIO、UART、I²C 與 SPI

F. ADC、PWM、Timer 與 Interrupt

G. 記憶體、Watchdog 與長時間穩定性

初學者常見的進階除錯錯誤

本章實作練習

Challenge:Arduino 自我診斷與故障記錄系統

建立一個可長時間運作的 Arduino 診斷系統,除了顯示當前狀態,也記錄過去錯誤、偵測任務停滯,並在必要時進入 Safe Mode。

硬體可使用 Arduino Uno R3、A0 可變電阻或類比感測器、D2 按鍵、D8 LED、至少一個 I²C 裝置與適當限流電阻;示波器與邏輯分析儀為選配。

啟動與執行監控

Watchdog 與 Safe Mode

持久化資料與記憶體要求

建議系統狀態與錯誤旗標

enum SystemMode
{
  MODE_BOOT,
  MODE_SELF_TEST,
  MODE_NORMAL,
  MODE_SAFE,
  MODE_FATAL
};

enum ErrorFlag
{
  ERROR_NONE       = 0,
  ERROR_ADC        = 1 << 0,
  ERROR_I2C        = 1 << 1,
  ERROR_EEPROM     = 1 << 2,
  ERROR_BUTTON     = 1 << 3,
  ERROR_MEMORY     = 1 << 4,
  ERROR_WATCHDOG   = 1 << 5,
  ERROR_TASK_STALL = 1 << 6
};

測試要求

本章總結

Arduino Uno 的 SRAM 只有約 2 KB,Stack、Heap、Buffer、String 與大型陣列都可能造成難以重現的錯誤。固定 char Buffer、snprintf()、F()、邊界檢查、短小 Critical Section 與長時間監控,是提升穩定性的核心方法。

Watchdog 可以恢復卡住的系統,但必須記錄 Reset 原因並修正根本問題。Brown-out、馬達啟動電流與導線壓降常被誤認為程式錯誤,應使用示波器觀察真實電壓。

邏輯分析儀適合 UART、I²C、SPI 與多通道時序;示波器則負責電壓品質、PWM、雜訊與瞬間掉電。兩者互補,不能互相取代。

真正有效的 Debug 是清楚描述現象、分類問題、建立最小可重現程式、收集證據、一次只修改一個因素、驗證根本原因,最後執行回歸與 Soak Test。

下一章預告

第 21 章將整理 Arduino 常見錯誤、設計陷阱與不良程式習慣,包括 LED 限流、按鍵浮動、delay() 阻塞、analogWrite()、TTL UART 與 RS232、馬達驅動、整數除法、陣列索引、Timer 衝突與函式庫相容性。