第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 Uno 的 Flash、SRAM 與 EEPROM 分工。
- 理解 Stack、Heap、全域資料與區域變數的記憶體配置。
- 判斷 SRAM 不足、陣列越界、Buffer Overflow 與未初始化變數造成的異常。
- 降低 String、動態配置與記憶體碎片化風險。
- 使用 sizeof()、F()、PROGMEM 與固定字元緩衝區。
- 理解 Watchdog Timer、Reset 原因與 Brown-out。
- 使用示波器觀察 GPIO、PWM、類比訊號與電源瞬降。
- 使用邏輯分析儀解碼 UART、I²C 與 SPI。
- 處理 volatile、Atomic Access、Critical Section 與事件遺失。
- 建立長時間測試、事件記錄、Safe Mode 與 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 故障排查流程
- 查看編譯器顯示的 Dynamic Memory 使用量。
- 尋找大型全域陣列與大型區域陣列。
- 檢查 OLED、LCD 或其他顯示 Buffer。
- 檢查大量固定文字是否可改用 F()。
- 檢查 String 串接、malloc()、new() 與動態配置。
- 檢查 Buffer Overflow、陣列越界與未初始化變數。
- 量測 Free SRAM,觀察是否隨時間持續下降。
- 暫時移除部分函式庫或功能,比較故障是否消失。
- 執行數小時以上的 Soak Test。
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 的選擇
- Timeout 太短時,EEPROM 寫入、初始化、網路連線或正常耗時工作也可能誤觸 Reset。
- Timeout 太長時,真正卡住後的恢復時間會增加。
- 應根據最長正常工作時間、通訊 Timeout、初始化時間、主迴圈週期與安全需求設定。
- 只有在關鍵任務都成功完成後才重設 Watchdog,不應在 loop() 開頭無條件餵狗。
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 重啟
- 馬達應使用符合額定電壓與啟動電流需求的獨立電源。
- Arduino 與馬達驅動器必須共地。
- 使用適當馬達驅動器,不可由 GPIO 直接驅動馬達。
- 加入退耦電容與反電動勢保護。
- 避免馬達電流流經 Arduino 5V 腳位、麵包板細電源軌或過細導線。
為什麼萬用電表可能看不到瞬間掉電
萬用電表更新速度通常較慢。若電壓只下降數百微秒或數毫秒,畫面可能仍顯示接近 5V。這時應使用示波器、具 Min/Max 記錄的電表、電源監控 IC 或 Reset 原因記錄。
退耦電容與電源排查
0.1 µF 陶瓷電容通常應靠近 IC 的 VCC 與 GND 腳位,較大容量電容可協助處理較慢的負載變化。電容不是萬能解法;若電源額定電流不足、線路壓降過大或驅動方式錯誤,仍必須改善供電架構。
- 空載量測 5V 與 3.3V。
- 連接感測器後再次量測。
- 在馬達、Servo 或繼電器切換時觀察電壓。
- 使用示波器捕捉瞬間下降、紋波與尖峰。
- 檢查電源額定電流、穩壓器溫度與導線壓降。
- 將高電流負載改用獨立電源並確認共地。
- 加入適當退耦、保護與回復方案。
使用示波器觀察 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 解碼
- 設定 Baud Rate、Data Bits、Parity、Stop Bits、邏輯是否反相及對應 Channel。
- 常見設定 9600 8N1 代表 9600 Baud、8 Data Bits、No Parity、1 Stop Bit。
- 解碼錯誤時檢查 Baud Rate、取樣率、GND、TTL 與 RS232 是否混淆、電壓邏輯及訊號是否反相。
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 衝突
- 設定 MOSI、MISO、SCK、CS、CPOL、CPHA 與 Bit Order。
- 確認 CS 在 Transaction 開始前拉 LOW,資料完成後再拉 HIGH。
- 未使用裝置的 CS 應保持 HIGH,避免多個裝置同時被選中。
- 多個 SPI 裝置共享 MISO 時,只有被選中的裝置可驅動 MISO。
- 若未選中的裝置沒有進入 Tri-state,可能需要 Buffer、切換器或分離 Bus。
中斷、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 從數百微秒增加到數十毫秒甚至數秒。
可為每個任務記錄執行時間與最後成功時間,超過門檻時寫入事件記錄或設定錯誤旗標。
通訊恢復與降級模式
- 第一次失敗時進行有限次數重試。
- 連續失敗時重新初始化裝置。
- 仍失敗時重設通訊介面。
- 無法恢復時進入 Degraded Mode。
- 致命失敗且無法安全繼續時,才考慮 Watchdog Reset 或 Fatal Mode。
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。
完整診斷控制台的功能範圍
- 非阻塞接收固定長度命令 Buffer。
- 將命令轉大寫並解析參數。
- 顯示 Uptime、Loop Count、最大 Loop 時間與 Active Error Flags。
- 統計 ADC 最小值、最大值與平均值。
- 掃描 I²C 位址並記錄錯誤。
- 只允許讀取或寫入安全腳位。
- 執行 ADC、I²C 與按鍵自我測試。
- 提供 Debug Level、清除錯誤與狀態查詢。
- 對超長命令、未知命令與錯誤參數增加錯誤計數。
診斷架構限制
- GPIOREAD 可能改變指定腳位模式。
- I2CSCAN 可能干擾正在工作的 I²C 裝置。
- A0 讀值在 0 至 1023 範圍內,不代表感測器一定正常。
- 按鍵為 LOW 可能是使用者正在按住,不一定是故障。
- Self-test 只驗證被設計進去的條件與當下狀態。
- 正式專案還應加入 EEPROM、Watchdog、Reset 原因、Free SRAM 與權限限制。
故障注入與回歸測試
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。
- 可正常編譯與上傳。
- 開機自我測試通過。
- Serial 命令與參數驗證正常。
- 感測器讀值合理。
- GPIO 輸入輸出與 PWM 正常。
- UART、I²C、SPI 通訊正常。
- EEPROM 讀寫與版本驗證正常。
- Watchdog 與 Reset 原因記錄正常。
- 斷電重啟與 Safe Mode 正常。
- 長時間 Soak Test 通過。
Arduino 完整 Debug Checklist
A. 問題描述
- 預期結果是什麼?
- 實際結果是什麼?
- 問題何時開始?
- 是否每次都能重現?
- 最近修改了什麼?
- 是否只在特定狀態、負載或時間後發生?
B. 開發環境
- Arduino IDE 與 Board Core 版本是否記錄?
- 開發板、Processor 與 COM Port 是否正確?
- USB Driver 是否正常?
- 函式庫版本是否記錄?
- 是否存在多個同名函式庫?
C. 編譯與上傳
- 第一個編譯錯誤是什麼?
- 分號、括號、大小寫與宣告是否正確?
- .h 與 .cpp 宣告是否一致?
- 是否缺少或重複定義函式庫?
- USB 線是否支援資料?
- Serial Monitor 或其他程式是否占用 Port?
- D0、D1 是否接有外部設備?
- Board、Processor、Reset 與 Bootloader 是否正常?
D. 電源與接線
- 5V、3.3V 與外部模組電壓是否正確?
- 所有裝置是否共地?
- 馬達或 Servo 是否使用適當電源與驅動器?
- 負載啟動時是否掉電?
- 穩壓器是否過熱?
- 退耦電容與反電動勢保護是否正確?
- VCC、GND、TX、RX、SDA、SCL、MOSI、MISO、SCK、CS 是否接對?
- 麵包板電源軌與杜邦線是否導通?
E. GPIO、UART、I²C 與 SPI
- pinMode()、INPUT_PULLUP 與 Active High/Low 是否理解?
- GPIO 是否超過電流能力或被其他功能占用?
- UART Baud Rate、8N1、GND、TTL/RS232 與電壓邏輯是否正確?
- I²C 位址、上拉電阻、Bus Speed、線長與 Clock Stretching 是否正確?
- SPI CS、Mode、Clock、Bit Order、MISO 衝突與 Transaction 配對是否正確?
F. ADC、PWM、Timer 與 Interrupt
- ADC 輸入是否浮動?
- 參考電壓與換算公式是否正確?
- PWM 腳位、數值與 Timer 是否正確?
- ISR 是否太長或呼叫 Serial?
- 共享變數是否使用 volatile?
- 多 Byte 資料是否以 Critical Section 複製?
- bool Event Flag 是否會遺失重複事件?
- Timer 是否與 Servo、PWM 或 tone() 衝突?
G. 記憶體、Watchdog 與長時間穩定性
- Dynamic Memory 是否過高?
- 是否有大型全域或區域陣列?
- 是否大量串接 String 或使用 malloc()、new()?
- 是否存在 Buffer Overflow、陣列越界或未初始化變數?
- 固定文字是否使用 F()?
- OLED Buffer 是否占用過多 SRAM?
- Free SRAM 是否隨時間下降?
- Watchdog Timeout 是否合理且只在系統健康時重設?
- 是否記錄 Reset 原因與避免重啟循環?
- 是否進行 Soak Test、millis() 溢位測試與最大 Loop 時間監控?
初學者常見的進階除錯錯誤
本章實作練習
- 比較一般 Serial.println() 固定字串與 F() 版本的 Dynamic Memory。
- 分析 values[10] 搭配 index
- 使用 char message[32] 與 snprintf() 輸出 ADC=512 TIME=1000,不使用 String。
- 將 volatile bool sampleEvent 改為 pendingSamples 計數器,加入飽和與 Overflow Flag。
- 建立外部中斷 pulseCount,每秒以 Critical Section 安全複製後顯示。
- 設定約 2 秒 Watchdog,5 秒後故意卡住,確認 Reset 後移除故障程式。
- 使用低風險負載記錄空載、負載與切換瞬間的 5V,不可由 GPIO 直接驅動馬達。
- 量測 analogWrite(9, 64)、128、192 的頻率、HIGH 電壓與 Duty Cycle。
- 每秒輸出 HELLO,使用邏輯分析儀以正確與錯誤 Baud Rate 解碼 UART。
- 執行 I²C Scanner,觀察 Start、Address、ACK/NACK 與 Stop。
- 比較加入大量 Serial 輸出前後的最大 Loop 執行時間。
- 執行至少數小時 Soak Test,每分鐘記錄 Uptime、Loop Count、Max Loop Time 與 Error Count。
Challenge:Arduino 自我診斷與故障記錄系統
建立一個可長時間運作的 Arduino 診斷系統,除了顯示當前狀態,也記錄過去錯誤、偵測任務停滯,並在必要時進入 Safe Mode。
硬體可使用 Arduino Uno R3、A0 可變電阻或類比感測器、D2 按鍵、D8 LED、至少一個 I²C 裝置與適當限流電阻;示波器與邏輯分析儀為選配。
啟動與執行監控
- 讀取 Reset 原因並增加 Boot Count。
- 執行 GPIO、ADC、I²C、EEPROM 與按鍵自我測試。
- 根據結果進入 Normal Mode 或 Safe Mode。
- 記錄 Loop Count、最大 Loop 時間、ADC 最小值、最大值與平均值。
- 記錄 I²C 錯誤、命令錯誤、任務最後成功時間、Active Error Flags 與 Historical Error Count。
Watchdog 與 Safe Mode
- 只有關鍵任務都正常完成後才重設 Watchdog。
- Watchdog Reset 後記錄事件,避免開機初始化造成無限重啟。
- 連續三次 Watchdog Reset 後進入 Safe Mode。
- Safe Mode 關閉所有危險輸出,保留 Serial Diagnostic Console。
- Safe Mode 不啟動高負載功能,允許查看錯誤與執行 Self-test。
持久化資料與記憶體要求
- EEPROM 保存 Magic Number、設定版本、Boot Count、Watchdog Reset Count、上次致命錯誤、Debug Level 與 Safe Mode 鎖定狀態。
- 使用 EEPROM.get()、EEPROM.put()、EEPROM.update()、版本驗證、合理性驗證及低頻寫入策略。
- 不使用無限制 String,改用固定字元 Buffer。
- 所有陣列都做邊界檢查,固定文字使用 F()。
- 估算 Free SRAM,低於門檻時設定 Warning。
建議系統狀態與錯誤旗標
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
};
測試要求
- 正常開機。
- 拔除 I²C 裝置。
- 按住按鍵開機。
- 輸入超長命令與錯誤參數。
- 觸發 Watchdog。
- 斷電後重新開機。
- 執行至少數小時 Soak Test。
- 驗證 millis() 溢位安全寫法。
- 驗證 Safe Mode 中所有危險輸出都關閉。
本章總結
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 衝突與函式庫相容性。