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.2. 硬體:透過 SWD 的 J-Link¶
將 Segger J-Link 連接到相機的 SWD 接腳(SWDIO、SWCLK、GND,以及作為參考的目標 VCC;相機一如往常透過 USB 供電)。J-Link EDU / Base / Pro 都可使用。除錯接腳露出的位置因相機而異——許多板子有專用的 JTAG/SWD 連接器,有些則在 I/O 排針或測試焊點上提供 SWD——因此請查閱 OpenMV 硬體文件中該板的接腳圖與電路圖,確認要接哪些接腳。請在實際插上探針的那台機器上,從 segger.com 安裝 J-Link Software and Documentation Pack。並保持其版本相當新——較舊的 J-Link 軟體不認得較新的裝置名稱(STM32N6、MIMXRT、Alif)。
每顆 MCU 都需要其確切的 J-Link 裝置名稱(device name),探針才能載入正確的快閃記憶體載入器與記憶體對映:
相機( |
MCU |
J-Link |
|---|---|---|
|
STM32F427 |
|
|
STM32F765 |
|
|
STM32H743 |
|
|
STM32N657 |
|
|
MIMXRT1062 |
|
|
Alif Ensemble (M55-HP) |
|
|
STM32H747 |
|
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"
}
]
}
請依你的板子變更 executable 與 device(參見上表)。按 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.5. Windows:WSL ↔ Windows 的 J-Link 橋接¶
WSL 2 無法直接看到 J-Link 的 USB 裝置,因此分工如下:Windows 提供探針(也就是探針實際插入的地方),而 VS Code + gdb 在 WSL 中執行,並透過 TCP 連上它。
在 Windows 上,安裝 Segger J-Link pack 並將 J-Link 插入 Windows 的 USB 連接埠。
在 Windows 上,啟動 J-Link Remote Server(它隨 J-Link pack 一起提供):在 J-Link 已連接的狀態下啟動它並按 OK。出現提示時允許它通過 Windows 防火牆。視窗會顯示它提供探針所用的 IP 位址——請記下來。
在 WSL 中,以
DEBUG=1建置,並確保arm-none-eabi-gdb可被存取(如上設定armToolchainPath)。在 WSL 的 VS Code 中,保留
servertype: "jlink"——GDB 伺服器在 WSL 中執行,並透過 Remote Server 連上探針——並加上serverpath+ipAddress:{ "name": "OpenMV J-Link (Windows host)", "type": "cortex-debug", "request": "launch", "cwd": "${workspaceFolder}", "executable": "${workspaceFolder}/build/OPENMV4/bin/firmware.elf", "servertype": "jlink", "serverpath": "/opt/SEGGER/JLink/JLinkGDBServer", "ipAddress": "192.168.x.x", "device": "STM32H743VI", "interface": "swd", "runToEntryPoint": "main", "armToolchainPath": "${env:HOME}/openmv-sdk-1.6.0/gcc/bin" }
將
ipAddress設為 Remote Server 視窗所顯示的位址。整個橋接就只有這樣。
小訣竅
GDB 伺服器橋接的替代方案:usbipd-win。 與其在 Windows 上執行伺服器,你也可以使用 usbipd-win 將 J-Link 的 USB 裝置直接掛接到 WSL 中。從管理員身分的 PowerShell:
winget install usbipd
usbipd list
usbipd bind --busid <busid>
usbipd attach --wsl --busid <busid>
(<busid> 是 usbipd list 中 J-Link 的匯流排 ID。)探針隨後便會出現在 WSL 內部,你便可使用來自 VS Code Cortex-Debug setup 的一般同機 servertype: "jlink" 設定,不需要 IP 位址,也不需要另外的 Windows 伺服器。GDB 伺服器橋接在偶爾使用時所需的設定較少;usbipd-win 則在日常開發中更為便利。
小訣竅
使用 "request": "attach" 可在 韌體已在執行中 的情況下對其除錯,而不重設或重新燒錄它——很適合用來捕捉現場的當機問題。使用 "request": "launch" 則會重設、燒錄 ELF 並從 runToEntryPoint 重新開始。
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
第一個位置引數選擇伺服器後端(jlink、stlink、qemu);其餘引數則轉送給該後端,並帶有適用於 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。變數 / 監看 / 呼叫堆疊 —— Variables 與 Call 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 的 RTT 或 SWO 可在目標執行時提供近乎零負擔的
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。