Arduino IDE 2的偵錯(debug)功能教學:以Arduino UNO R4 WiFi開發板為例

本文將大致說明Arduino IDE 2.x版提供的程式偵錯功能,並使用Arduino UNO R4 WiFi開發板(原廠或相容板)示範。先說一下,”debug”指的是找出並解決軟硬體的問題,中文翻譯成「除錯」、「偵錯」或「調試」,本文採用跟Arduino IDE和VS Code編輯器一致的「偵錯」翻譯。

檢查程式是否錯誤,例如,某個條件式是否被執行,某個變數值是否跟預期一樣,可以在程式中加入print()函式,或者透過ESP32晶片的Core Debug(核心偵錯)功能(請參閱《超圖解ESP32應用實作》第2章)在「序列監控窗」輸出一段訊息或變數值。

使用print()偵錯,簡單有效但主要問題是,開發者只能被動地接收微控器在預定情況下發出的訊息,無法動態地調整執行中的程式的參數,也無法改變執行流程或暫停程式碼。用看病來比喻,使用print()輸出訊息,相當於醫生問診,讓病患描述身體的狀況。

為了方便開發人員找出軟硬體的問題,微控器廠商各自推出不同的偵錯方案,例如,一種簡稱ICE(In-Circuit Emulator,線上模擬器)以及JTAG和SWD(Serial Wire Debug,序列線偵錯)等介面裝置,它們宛如醫療診斷儀器,能夠即時查看微控器的運作狀態,而且不限於觀察普通變數,也能深入到處理器內部的暫存器,還有呼叫函式時用到的堆疊(stack)記憶體區域。

此外,這些偵錯工具(硬體設備)也允許開發人員暫停,甚至一步一步執行並觀察程式碼。但是,不同微控器廠商的專屬偵錯技術和軟硬體工具,彼此可能不相容,需要額外購買。目前的主流偵錯介面和工具是JTAG和SWD,關於JTAG在ESP32晶片的應用說明,請參閱《超圖解ESP32應用實作》第14章。

Arduino UNO R4 Minima開發板預留一個連接外部偵錯工具用的SWD介面,日後再介紹。

Arduino UNO R4 Minima開發板

Arduino UNO R4 WiFi開發板的結構不同於Minima,多了一個ESP32-S3晶片,使得UNO R4 WiFi板具備「僅需USB接線,即可進行偵錯」的功能,無需添購其他偵錯設備。

使用USB接線進行Arduino UNO R4 WiFi開發板偵錯

Arduino IDE上方的工具列,有個「開始偵錯」鈕,左側工具列可開、關「偵錯」面板。

Arduino IDE上方的工具列

先把Arduino UNO R4 WiFi開發板接上電腦。

在Arduino IDE貼入底下的範例程式。這個程式將從UNO R4微控器的DAC(數位類比轉換器)輸出「超級瑪利兄弟主題曲」的一段旋律,程式的說明請參閱《超圖解Arduino互動設計入門》第11章:

// 超級瑪利兄弟主題曲
#include <analogWave.h>
analogWave wave(DAC);

int tempo = 200;  // 節拍
int beatNote = 2;
int wholenote = (60000 * beatNote) / tempo;  // 全音符

int melody[][2] = {  // 旋律
  {76, 4}, {76, 4}, {0, 4}, {76, 4},
  {0, 4}, {72, 4}, {76, 4}, {0, 4},
  {79, 4}, {0, 4}, {0, 2}
};

int noteTotal = sizeof(melody) / sizeof(melody[0]);
int noteCounter = 0;

void setup() {
  wave.sine(10);  // 輸出正弦波
  wave.stop();
}

void loop() {
  int currentNote = melody[noteCounter][0];
  int noteDuration = wholenote / melody[noteCounter][1];
  float noteFreq =  440 * pow(2, ((currentNote - 69) / 12.0));

  if (currentNote != 0) {
    wave.freq(noteFreq);      // 調整輸出頻率
    delay(noteDuration * 0.9);
    wave.stop();              // 停止輸出
    delay(noteDuration * 0.1);
  } else {  // 休止符
    wave.stop();
    delay(noteDuration);
  }

  noteCounter++;
  if (noteCounter > noteTotal) {
    noteCounter = 0;
    wave.stop();
    delay(1000);
  }
}

勾選主功能表的「Sketch / 除錯最佳化」。

除錯最佳化

工具 / 燒錄器」選項,請選擇“ARM CMSIS-DAP compatible”(CMSIS-DAP相容燒錄器):

ARM CMSIS-DAP compatible

設定中斷點(breakpoint)

中斷點是在程式碼中的特定點故意停止或暫停的地方,例如,假設我們想讓程式執行到第28行的位置時暫停。按一下原始碼行號左側,將出現一個紅點標記,代表該行已設定了一個中斷點;若按一下紅點,將能取消中斷點。

中斷點(breakpoint)

偵錯工具所能追蹤的中斷點數量是有限的,例如:8個。Arduino官方的UNO R4 WiFi的偵錯教學文件指出,隨便你設定多少中斷點都行,額…這個說法有點隨便,不過我們通常只關注一個程式的幾個地方,不會去設置一大堆中斷點。

開始偵錯

依序點擊工具列的「驗證」、「上傳」鈕,然後點擊「開始偵錯」。

開始偵錯

過一會兒,IDE底部的狀態列將變為橙色,並顯示gdb-server和「編輯主控台」視窗。

偵錯模式

如果出現如下的OpenOCD錯誤訊息,代表「燒錄工具」沒有選用“ARM CMSIS-DAP compatible”。

OpenOCD錯誤訊息

在Arduino IDE底層,實際負責偵錯的程式是附屬在GCC(Arduino採用的C/C++語言編譯器)的“GDB”(GNU Debugger,開源的偵錯器)。

偵錯器的架構

OpenOCD(Open On-Chip Debugger,直譯為「開源晶片上的偵錯器」)則是另一個開源的偵錯工具,負責連接到JTAG或SWD等偵錯介面。GDB Server(GDB伺服器)則是OpenOCD的一個元件,扮演「中間人」的角色,將GDB指令轉譯為微控制器可以理解的動作。

控制偵錯器的執行流程

開始偵錯時,「偵錯」面板上方的工具列將呈現「可操作」狀態。

偵錯器工具列

它們的作用:

  • 繼續 / 暫停:繼續執行程式,直到碰到下一個斷點時暫停;如果沒有設定斷點,則持續執行。
  • 不進入函式(Step Over):執行一步敘述然後暫停,如果是呼叫函式的敘述,則會執行完該函式但不會進入函式內部。
  • 逐步執行(Step Into):單步執行敘述,如果是呼叫函式的敘述,則進入函式內部。
  • 跳離函式(Step Out):一口氣執行完函式的剩餘程式碼,然後跳回到呼叫該函式的那一行敘述。
  • 重新啟動:停止目前的偵錯任務並重新啟動偵錯器。
  • 停止:停止並退出偵錯。

程式目前暫停在行28,若持續按一下「逐步執行」,執行到行29的wave.freq()敘述時,偵錯器將進入analogWave.cpp裡的analogWave::freq()方法:

逐步執行

你可以持續按一下「逐步執行」,或點擊「跳離函式」,回到主程式檔:

跳離函式

若不想進入行30的delay()內部,可點擊「不進入函式」:

不進入函式

查看並修改變數值

點擊「偵錯」面板的「變數」欄開啟它,可看到裡面的Local(區域)Global(全域)Static(靜態)Register(暫存器)等分類。底下畫面顯示目前所在函式(loop)的所有區域變數名稱及其值。

區域變數

你可以雙按其中的任意變數值,例如,暫存音頻的noteFreq變數:

修改變數值

即可重新設定變數值:

設定變數值

改成740的結果:

設定好變數值

偵錯器的一些小bug

本文原本打算用底下這個,把簡單問題稍微複雜化的閃爍LED程式碼,示範Arduino IDE 2.x版內建的「偵錯」功能,其中的blink()函式有3個區域變數,透過在偵錯過程調整這些參數,可觀察並改變程式的行為。

const byte LED_PIN = LED_BUILTIN;  // 定義LED接腳的常數
bool ledState = false;             // 定義LED的初始狀態

// 閃爍LED的函式
void blink() {
  static int counter = 0;  // 計數器變數
  int times = 3;           // 次數變數
  int max_counter = 50;    // 計數器上限

  if (counter % times == 0) {  // 若計數了times次…
    ledState = !ledState;      // 反轉LED的狀態
  } 

  if (counter < max_counter) {
    counter ++;
  } else {
    counter = 0;
  }

  digitalWrite(LED_PIN, ledState);  // 設定LED腳的輸出
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, ledState);
}

void loop() {
  blink();

  delay(500);
}

然而,Arduino IDE的「偵錯」功能本身也有些小bug…使用帶有bug的工具來檢查程式的bug,這感覺有點微妙,一言難盡…不過也許你看到這篇文章的時候,Arduino IDE本身的bug已經解決了。

這是在macOS的Arduino IDE 2.3.4版偵錯上述程式碼的畫面,這裡出現一個bug:區域變數只列出1個:

Mac版的Arduino IDE

另外,我不確定這是不是bug:上面的blink()函式定義1個靜態(static)變數,但它並沒有被列在「變數」欄的Static分類。我試過定義一個全域型的靜態變數,也沒有顯示在Static分類。

這是在Windows的Arduino IDE 2.3.4版偵錯相同程式的畫面,區域變數同樣只顯示1個,而且目前所在的行號前面並沒有呈現三角形指示符號:

Windows版的Arduino IDE

編輯有設定「中斷點」的那一行程式時,例如,刪除下圖所示的第3行,內文會突然冒出一個紅點。

莫名的紅點

最後,在偵錯上一節的超級瑪利兄弟旋律程式時,它有時會出現錯誤的變數值,解決辦法是先停止偵錯器、重新上傳程式,再重新開始偵錯。

重新上傳程式

Posts created 497

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top