14.1.1.4. 為韌體除錯

硬體上除錯指的是停住處理器、在 C 原始碼中設定中斷點、單步執行,以及檢視變數、記憶體、register(暫存器)與 peripheral(周邊裝置)——而且這一切都在 VS Code 內進行。這需要三樣東西:一個 除錯版本(debug build)、一個 SWD 除錯探針(Segger J-Link),以及驅動 arm-none-eabi-gdb 連上 J-Link GDB 伺服器的 Cortex-Debug 擴充套件。

14.1.1.4.1. 建置除錯版本

務必使用 DEBUG=1 重新建置目標:

make -j$(nproc) TARGET=<TARGET> DEBUG=1

釋出版本(DEBUG=0)映像以 -O2 編譯;在除錯器中你會看到許多變數顯示為 <optimized out>,內聯函式會併入其呼叫者,單步執行也會無法預測地跳來跳去。DEBUG=1 則以 -Og -ggdb3 建置,既可除錯又仍能在相機上開機。你要讓除錯器指向的 ELF 是:

build/<TARGET>/bin/firmware.elf

(對於 Alif AE3,請除錯 build/OPENMV_AE3/bin/firmware_M55_HP.elf——也就是高效能核心。)

14.1.1.4.3. VS Code Cortex-Debug 設定

在儲存庫中建立 .vscode/launch.json。最簡單的情況——VS Code、J-Link 與建置全都在 同一台 Linux / macOS 機器上——使用 servertype: "jlink",讓 Cortex-Debug 自行啟動一個 J-Link GDB 伺服器:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "OpenMV J-Link",
      "type": "cortex-debug",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "executable": "${workspaceFolder}/build/OPENMV4/bin/firmware.elf",
      "servertype": "jlink",
      "device": "STM32H743VI",
      "interface": "swd",
      "runToEntryPoint": "main",
      "armToolchainPath": "${env:HOME}/openmv-sdk-1.6.0/gcc/bin",
      "gdbPath": "${env:HOME}/openmv-sdk-1.6.0/gcc/bin/arm-none-eabi-gdb"
    }
  ]
}

請依你的板子變更 executabledevice(參見上表)。按 F5 即可建置、燒錄並執行到 main 並停在該處。

小訣竅

若要每次開始除錯時自動重新建置,請在 .vscode/tasks.json 中新增一個建置任務,並在啟動設定中以 "preLaunchTask" 參照它。例如一個執行 make -j$(nproc) TARGET=OPENMV4 DEBUG=1 的任務,命名為 "build-firmware",再於上述設定中加上 "preLaunchTask": "build-firmware",如此按下 F5 便能一步完成重新建置、燒錄並啟動除錯器。

警告

Cortex-Debug 需要 arm-none-eabi-gdb。它隨 SDK 一起提供於 ~/openmv-sdk-<version>/gcc/bin,但預設 不在 PATH 上,因此除錯會以 "GDB executable 'arm-none-eabi-gdb' was not found" 失敗。修正方法是依上述設定 armToolchainPath / gdbPath,或將 ~/openmv-sdk-<version>/gcc/bin 加入你的 PATH(之後 printenv PATH 應會列出它)。

14.1.1.4.4. Peripheral register 檢視(SVD)

將 Cortex-Debug 指向一個 CMSIS SVD 檔案,即可依名稱與位元欄位取得解碼後的 peripheral register(周邊暫存器)檢視(計時器、DMA、相機介面等):

"svdFile": "/path/to/STM32H743.svd"

對於 STM32 與 MIMXRT,請從 ST / NXP CMSIS pack 或 Cortex-Debug SVD 登錄處取得 SVD。Alif 的 SVD 則隨附於韌體儲存庫的 lib/micropython/lib/alif_ensemble-cmsis-dfp/Debug/SVD/(AE3 HP 核心請使用 ..._CM55_HP_View.svd)。

14.1.1.4.6. 使用 gdbrunner 進行命令列除錯

手動為嵌入式目標設定 GDB 工作階段是一段五步的舞步:在一個視窗中以正確的裝置、連接埠與介面旗標啟動 J-Link / ST-Link GDB 伺服器;等它印出 Waiting for GDB connection;在第二個視窗中執行 arm-none-eabi-gdb;輸入 target remote localhost:<port>;讓 gdb 指向 ELF。當 gdb 工作階段結束時,還得記得把伺服器視窗關掉。gdbrunner 是一個小型 CLI,將以上全部濃縮成一條前景指令。它隨附於 OpenMV SDK 的 Python 環境,因此無需安裝任何東西;常用的進入點是韌體儲存庫的 make debug 目標:

make -j$(nproc) TARGET=<TARGET> DEBUG=1 debug

這會以該板設定中的除錯器引數執行 gdbrunner——也就是 J-Link 裝置名稱,以及在需要時的 ST-Link 外部快閃記憶體載入器——並讓 SDK 的 arm-none-eabi-gdb 已在 PATH 上。預設後端是 J-Link;改用 make DEBUGGER=STLINK debug 則改以 ST-Link 探針運作。

gdbrunner 也可以直接呼叫(在 SDK 之外,pip install gdbrunner):

gdbrunner jlink --device STM32H743VI build/OPENMV4/bin/firmware.elf
gdbrunner stlink build/OPENMV4/bin/firmware.elf
gdbrunner qemu --machine mps2-an500 build/MPS2_AN500/bin/firmware.elf

第一個位置引數選擇伺服器後端(jlinkstlinkqemu);其餘引數則轉送給該後端,並帶有適用於 OpenMV 相機的預設值。gdbrunner --help 會列出每個後端的完整旗標清單;每個後端的引數表都由 JSON 驅動(src/gdbrunner/backends.json),因此新增一個伺服器只需編輯設定而非寫程式。

gdbrunner 在命令列工作中所做的事:

  • 單一行程,乾淨的生命週期。 伺服器啟動,連接埠開放時 gdb 連上,gdb 退出時伺服器被乾淨地終止。不會有 JLinkGDBServer 孤兒行程在工作階段後存活,也不需要管理兩個終端機。

  • STM32CubeProgrammer 自動探索。 stlink 後端會搜尋常見的安裝位置(~/STM32CubeProgrammer//opt/st/、STM32CubeIDE 外掛樹)來尋找 STM32CubeProgrammer 工具,因此那條長長的 --cube-prog 路徑不必每次都輸入。SDK 自帶一份副本於 ~/openmv-sdk-<version>/stcubeprog/bin——若系統上沒有安裝,請將 --cube-prog 指向該處。

  • 尊重每個專案的 gdbinit。 目前目錄中的 .gdbinit 會以 -ix 載入——覆寫使用者層級的 ~/.gdbinit——因此每個專案各自的 gdb 指令稿(pretty-printer、特定板子的巨集、中斷點集合)只要放在工作目錄中即可生效。make debug 從儲存庫根目錄執行,因此放在那裡的 .gdbinit 便會套用。

  • 乾跑(dry run)。 --dryrun 會印出伺服器指令而不實際執行,適合用來把該呼叫改寫到包裝指令稿、複製到 IDE 啟動設定中,或單純檢查 gdbrunner 正在組出哪些引數。

  • 可見的伺服器輸出。 --show-output 會讓伺服器的 stdout / stderr 保持可見。預設會抑制它(讓 gdb 的 UI 保持乾淨);當出問題的正是伺服器本身時,再翻開這個旗標。

  • QEMU 後端。 qemu-system-arm 可在沒有任何板子插上的情況下對韌體建置除錯。MPS2_AN500 目標會在其板子設定中選擇此後端,因此 make TARGET=MPS2_AN500 DEBUG=1 debug 會為 QEMU 的 mps2-an500 機器建置,並在飛行途中單步執行與平台無關的程式碼——也就是所有不觸及相機專屬周邊裝置的部分。(qemu-system-arm 是主機端的安裝,並非 SDK 的一部分。)

若要進行帶有中斷點欄與 peripheral register 檢視的原始碼層級單步執行,上述的 VS Code Cortex-Debug 設定是較好的工具;而 gdbrunner 則是處理所有命令列上事務的合適選擇。

14.1.1.4.7. 使用除錯器

一旦工作階段執行中(處理器停在 main):

  • 中斷點 ——點擊某行 C 程式碼旁的欄區,或在 Debug Console 中輸入 break <file>:<line> / break <function>。Cortex-M 核心只有少量的 硬體 中斷點比較器(M7 / H7 上通常為 6--8 個,M55 上為 8 個)。對位於快閃記憶體中的程式碼超出該數量會無聲失敗——請讓使用中的中斷點數量保持精簡。

  • 單步執行 —— F10 跳過(next)、F11 進入(step)、Shift+F11 跳出(finish)、F5 繼續。指令層級的單步執行在 Debug Console 中是 stepi / nexti

  • 變數 / 監看 / 呼叫堆疊 —— VariablesCall Stack 窗格會顯示區域變數與回溯(backtrace);可在 Watch 中加入運算式。把游標停在原始碼中的某個變數上即可看到其值。任何顯示 <optimized out> 的情況都代表你不是在 DEBUG=1 版本上。

  • 監看點(資料中斷點) —— watch <expr> 會在變數被寫入時停住,rwatch 在讀取時停住,awatch 則在兩者皆停。Cortex-M 的 DWT 單元支援約 4 個硬體監看點——對於捕捉 是誰 弄壞了某個變數極為寶貴。

  • register 與 peripheral —— Cortex Registers 檢視會顯示核心 register;在設定 svdFile 後,Peripherals 檢視會解碼每個 peripheral register 與位元欄位(DMA、計時器、相機 / CSI 介面、XSPI 等)——這是看出某個驅動程式為何出狀況的最快方法。

  • 記憶體 ——使用 Cortex-Debug 的記憶體檢視器或 gdb 的 x/ 直接檢視影格緩衝區、DMA 緩衝區與結構。

  • 不停住的 printf(SWO/RTT) ——對於對時序敏感的問題,Segger 的 RTTSWO 可在目標執行時提供近乎零負擔的 printf。請以 DEBUG_PRINTF=1 建置,並加上 Cortex-Debug 的 rttConfig(RTT)或 swoConfig(SWO,需要核心時脈)。當中斷點會改變你正試圖觀察的時序時,這便是合適的工具。

  • 中斷連線 ——對 launch 工作階段執行 Stop 會停住目標;對 attach 工作階段執行 Disconnect 則會讓相機繼續執行。之後請對相機重新上電,使其回到正常運作。

14.1.1.4.8. 除錯時的陷阱

  • 被最佳化掉的變數。 所有東西都顯示 <optimized out>——你建置的是 DEBUG=0。請以 DEBUG=1 重新建置。

  • "GDB executable not found" —— SDK 的 gcc/bin 不在 PATH 上;請設定 armToolchainPath / gdbPath

  • "Cannot connect" / 記憶體對映錯誤 ——錯誤或缺少的 device 名稱;請使用表中的確切字串。

  • 中斷點無聲地未命中 ——對位於快閃記憶體中的程式碼設定了過多硬體中斷點;請減少數量。

  • 原始碼路徑不符(Docker 建置的 ELF) ——請以 Docker 的 build-firmware-dev 目標建置(容器內外的絕對路徑相同),或設定 gdb 的 set substitute-path