Flash & Run 腳本解析(STM32 + WiFi HaLow)
本文逐步拆解 cross-compile flash 腳本範例,說明每一步的定義與分工,並解釋它為何**專門適配於「STM32 跑 WiFi stack」**的情況。
前置閱讀:從零開始在家開發 IoT: OpenOCD 與 GDB 協作指南(OpenOCD 與 GDB 的基本協作方式)。
1. 範例情境與說明
本文討論的情境是:STM32U5 主控,上面跑著 WiFi stack,並透過周邊介面(SPI / SDIO)連接 Morse Micro HaLow 晶片。 這個「MCU + 即時 WiFi stack + 外接無線晶片」的組合,在燒錄與除錯時會遇到一個特有的難題------核心很難被穩定地停住(halt)。後面第 3 節會解釋為什麼這個組合特別棘手,第 6、7 節會說明這支腳本如何針對性地解決它。
關於以下腳本: 下方僅為示意用的範例腳本,用來說明 flash & run 的流程與 OpenOCD / GDB 的分工。
2. 範例腳本
bash
#!/bin/bash
APP_NAME=$(basename $(find build -type f -name "*.elf"))
CFG=PATH/openocd.cfg
# Kill any lingering openocd instances to avoid ST-Link conflicts
pkill -f openocd 2>/dev/null
sleep 1
# Start openocd with init + reset halt to pin the core at Reset_Handler,
# then stay resident as a GDB server
openocd -f "$CFG" -c "init; reset halt" &
OPENOCD_PID=$!
trap "kill $OPENOCD_PID 2>/dev/null" EXIT # kill openocd when the script exits
sleep 2
# GDB actions: load → verify → reset and run, no reset halt involved
arm-none-eabi-gdb "build/$APP_NAME" \
-ex 'target extended-remote :3333' \
-ex 'load' \
-ex 'compare-sections' \
-ex 'monitor reset' \
-ex 'set confirm off' \
--batch
這是一個典型的 「燒錄 + 跑起來」(flash & run) 腳本,不是互動式單步除錯。整個設計都繞著「WiFi stack 跑起來就很難定住核心」這個痛點打造。
3. 為什麼 WiFi stack 讓核心「難定住」
當 STM32 上的 WiFi stack(很可能還壓著 FreeRTOS)正在跑時,從外部把 CPU 喊停(halt)會很不可靠:
- WiFi 對 HaLow chip 的 SPI / SDIO 通訊有很緊的時序,CPU 大量在中斷裡進出。
- stack 可能有 watchdog,一停它就觸發重置跟你對打。
- 省電模式會讓核心進 sleep,連除錯時脈都可能被 gate 掉,導致連線時斷時續。
也就是說:對著一個正在全速跑 WiFi 的活目標下 halt,是會打架的。
這支腳本的對策是完全不去停活的目標 ,改用 reset halt 把核心攔截在重置後的第一道指令(Reset_Handler)------WiFi stack、RTOS、watchdog、sleep 都還沒執行的那一瞬間。那是一個乾淨、確定的死寂狀態,在這個點上燒 Flash 最安全;燒完再放開,讓韌體從頭乾淨啟動。
4. 執行時序總覽
STM32 核心 GDB OpenOCD shell STM32 核心 GDB OpenOCD shell #mermaid-svg-7ctXKBlUAgJLRd6t{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7ctXKBlUAgJLRd6t .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7ctXKBlUAgJLRd6t .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7ctXKBlUAgJLRd6t .error-icon{fill:#552222;}#mermaid-svg-7ctXKBlUAgJLRd6t .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7ctXKBlUAgJLRd6t .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7ctXKBlUAgJLRd6t .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7ctXKBlUAgJLRd6t .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7ctXKBlUAgJLRd6t .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7ctXKBlUAgJLRd6t .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7ctXKBlUAgJLRd6t .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7ctXKBlUAgJLRd6t .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7ctXKBlUAgJLRd6t .marker.cross{stroke:#333333;}#mermaid-svg-7ctXKBlUAgJLRd6t svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7ctXKBlUAgJLRd6t p{margin:0;}#mermaid-svg-7ctXKBlUAgJLRd6t .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7ctXKBlUAgJLRd6t text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-7ctXKBlUAgJLRd6t .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7ctXKBlUAgJLRd6t .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-7ctXKBlUAgJLRd6t .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-7ctXKBlUAgJLRd6t .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-7ctXKBlUAgJLRd6t #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-7ctXKBlUAgJLRd6t .sequenceNumber{fill:white;}#mermaid-svg-7ctXKBlUAgJLRd6t #sequencenumber{fill:#333;}#mermaid-svg-7ctXKBlUAgJLRd6t #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-7ctXKBlUAgJLRd6t .messageText{fill:#333;stroke:none;}#mermaid-svg-7ctXKBlUAgJLRd6t .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7ctXKBlUAgJLRd6t .labelText,#mermaid-svg-7ctXKBlUAgJLRd6t .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-7ctXKBlUAgJLRd6t .loopText,#mermaid-svg-7ctXKBlUAgJLRd6t .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-7ctXKBlUAgJLRd6t .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7ctXKBlUAgJLRd6t .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-7ctXKBlUAgJLRd6t .noteText,#mermaid-svg-7ctXKBlUAgJLRd6t .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-7ctXKBlUAgJLRd6t .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7ctXKBlUAgJLRd6t .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7ctXKBlUAgJLRd6t .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7ctXKBlUAgJLRd6t .actorPopupMenu{position:absolute;}#mermaid-svg-7ctXKBlUAgJLRd6t .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-7ctXKBlUAgJLRd6t .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7ctXKBlUAgJLRd6t .actor-man circle,#mermaid-svg-7ctXKBlUAgJLRd6t line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-7ctXKBlUAgJLRd6t :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 停在 Reset_Handler WiFi stack 尚未啟動 韌體從頭執行 WiFi stack 正式上場 pkill openocd 釋放 ST-Link 啟動 openocd 背景常駐 init 後 reset halt target extended-remote 3333 load 燒錄 寫入 Flash 核心仍停住 compare-sections 驗證 monitor reset reset 不 halt batch 模式結束 trap 殺掉 openocd 釋放介面
5. 逐步拆解
5.1 準備:找出產物、清場
bash
APP_NAME=$(basename $(find build -type f -name "*.elf"))
CFG=PATH/openocd.cfg
pkill -f openocd 2>/dev/null
sleep 1
- 在
build/找出.elf檔並取其檔名。 - 指向特定平台的
openocd.cfg。 pkill -f openocd:ST-Link 是單一擁有者裝置,同一時間只能被一個 openocd 行程佔用。先殺掉殘留行程,否則新的連不上。
5.2 啟動 OpenOCD 並把核心釘在 Reset_Handler(OpenOCD 當家)
bash
openocd -f "$CFG" -c "init; reset halt" &
OPENOCD_PID=$!
trap "kill $OPENOCD_PID 2>/dev/null" EXIT
sleep 2
init:建立除錯連線(連上 target、初始化 Flash 驅動)。reset halt:硬體重置(透過 cfg 的srst_only即 NRST 腳)後立即停在第一道指令,把核心釘在Reset_Handler------這正是繞過 WiFi stack 的關鍵。&:讓 openocd 退到背景常駐,當 GDB server(3333)等著被連。OPENOCD_PID=$!+trap ... EXIT:記下行程 ID,確保腳本結束時一定殺掉 openocd、釋放 ST-Link。sleep 2:給 openocd 時間把 server 開好。
這一步也用到了 cfg 裡的 reset 事件鉤子(reset 過程降速 125 kHz、結束後拉回 4 MHz),讓重置在時脈未穩時更可靠。
5.3 GDB 接手:燒錄 → 驗證 → 放開執行
bash
arm-none-eabi-gdb "build/$APP_NAME" \
-ex 'target extended-remote :3333' \
-ex 'load' \
-ex 'compare-sections' \
-ex 'monitor reset' \
-ex 'set confirm off' \
--batch
| 指令 | 動作 | 由誰執行 |
|---|---|---|
target extended-remote :3333 |
GDB 連上 openocd 的 gdb server。用 extended(非 remote)讓連線在程式結束後仍保持、支援重啟 |
GDB ↔ OpenOCD |
load |
把 ELF 可載入區段(.text / .data)燒進 Flash。因核心被釘住,沒有 WiFi 中斷或 watchdog 搗亂 |
GDB 發請求,OpenOCD 實際燒錄 |
compare-sections |
把 Flash 內容讀回與 ELF 逐區段比對,確認燒錄正確(即 verify) | GDB ↔ OpenOCD |
monitor reset |
monitor = 轉給 OpenOCD。此處是 reset(不是 reset halt ),重置後放開核心全速跑,韌體從頭啟動,WiFi stack 上場 |
OpenOCD |
set confirm off |
關掉 GDB 的確認提示 | GDB |
--batch |
跑完所有 -ex 就自動退出、不進互動介面 |
GDB |
GDB 一退出,5.2 的 trap 觸發,殺掉 openocd、釋放 ST-Link,腳本收尾。
6. 設計重點:兩個 reset,目的相反
這支腳本用了兩次重置,方向完全相反,正是它適配 WiFi 情境的核心手法:
#mermaid-svg-mCX7av5FuTAwWuK8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mCX7av5FuTAwWuK8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mCX7av5FuTAwWuK8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mCX7av5FuTAwWuK8 .error-icon{fill:#552222;}#mermaid-svg-mCX7av5FuTAwWuK8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mCX7av5FuTAwWuK8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mCX7av5FuTAwWuK8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mCX7av5FuTAwWuK8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mCX7av5FuTAwWuK8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mCX7av5FuTAwWuK8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mCX7av5FuTAwWuK8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mCX7av5FuTAwWuK8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mCX7av5FuTAwWuK8 .marker.cross{stroke:#333333;}#mermaid-svg-mCX7av5FuTAwWuK8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mCX7av5FuTAwWuK8 p{margin:0;}#mermaid-svg-mCX7av5FuTAwWuK8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mCX7av5FuTAwWuK8 .cluster-label text{fill:#333;}#mermaid-svg-mCX7av5FuTAwWuK8 .cluster-label span{color:#333;}#mermaid-svg-mCX7av5FuTAwWuK8 .cluster-label span p{background-color:transparent;}#mermaid-svg-mCX7av5FuTAwWuK8 .label text,#mermaid-svg-mCX7av5FuTAwWuK8 span{fill:#333;color:#333;}#mermaid-svg-mCX7av5FuTAwWuK8 .node rect,#mermaid-svg-mCX7av5FuTAwWuK8 .node circle,#mermaid-svg-mCX7av5FuTAwWuK8 .node ellipse,#mermaid-svg-mCX7av5FuTAwWuK8 .node polygon,#mermaid-svg-mCX7av5FuTAwWuK8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mCX7av5FuTAwWuK8 .rough-node .label text,#mermaid-svg-mCX7av5FuTAwWuK8 .node .label text,#mermaid-svg-mCX7av5FuTAwWuK8 .image-shape .label,#mermaid-svg-mCX7av5FuTAwWuK8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-mCX7av5FuTAwWuK8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mCX7av5FuTAwWuK8 .rough-node .label,#mermaid-svg-mCX7av5FuTAwWuK8 .node .label,#mermaid-svg-mCX7av5FuTAwWuK8 .image-shape .label,#mermaid-svg-mCX7av5FuTAwWuK8 .icon-shape .label{text-align:center;}#mermaid-svg-mCX7av5FuTAwWuK8 .node.clickable{cursor:pointer;}#mermaid-svg-mCX7av5FuTAwWuK8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mCX7av5FuTAwWuK8 .arrowheadPath{fill:#333333;}#mermaid-svg-mCX7av5FuTAwWuK8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mCX7av5FuTAwWuK8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mCX7av5FuTAwWuK8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mCX7av5FuTAwWuK8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mCX7av5FuTAwWuK8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mCX7av5FuTAwWuK8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mCX7av5FuTAwWuK8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mCX7av5FuTAwWuK8 .cluster text{fill:#333;}#mermaid-svg-mCX7av5FuTAwWuK8 .cluster span{color:#333;}#mermaid-svg-mCX7av5FuTAwWuK8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mCX7av5FuTAwWuK8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mCX7av5FuTAwWuK8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-mCX7av5FuTAwWuK8 .icon-shape,#mermaid-svg-mCX7av5FuTAwWuK8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mCX7av5FuTAwWuK8 .icon-shape p,#mermaid-svg-mCX7av5FuTAwWuK8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mCX7av5FuTAwWuK8 .icon-shape .label rect,#mermaid-svg-mCX7av5FuTAwWuK8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mCX7av5FuTAwWuK8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mCX7av5FuTAwWuK8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mCX7av5FuTAwWuK8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 製造死寂狀態
放開核心
開頭
OpenOCD: reset halt
安全燒錄 + 驗證
結尾
GDB: monitor reset (run)
韌體從頭跑
WiFi stack 啟動
- 開頭的
reset halt:攔住核心,製造一個能安全燒錄的死寂狀態。 - 結尾的
monitor reset(不 halt):放開核心,讓新韌體乾淨啟動。
把「難定住的 WiFi 目標」這個問題,框在兩次重置之間處理掉。
7. 為何這是「STM32 + WiFi stack」的專屬寫法
| 設計選擇 | 一般 MCU | 為了 WiFi stack 而做 |
|---|---|---|
不停活目標,改用 reset halt |
直接連上 halt 即可 | 避免與 WiFi 中斷 / watchdog / sleep 打架 |
燒錄前釘在 Reset_Handler |
非必要 | 確保 WiFi stack 尚未啟動,Flash 寫入無干擾 |
結尾 monitor reset 而非 halt |
看需求 | 讓 WiFi stack 從乾淨狀態完整啟動 |
pkill openocd + trap kill |
好習慣 | WiFi 開發常反覆燒錄,嚴格管理 ST-Link 單一佔用 |
8. 延伸:若要在 WiFi 跑起來後仍能互動除錯
本腳本只負責「燒進去、放開跑」,沒有設中斷點或互動除錯。若要在 WiFi stack 啟動後仍能單步 / 下中斷點,挑戰才開始,常見對策:
- 開發版韌體中暫時關掉 watchdog。
- 停用進入 stop / sleep 的低功耗路徑,避免除錯時脈被 gate。
- 善用 cfg 的
CONNECT_ASSERT_SRST,在 reset 當下搶進去。 - 把腳本最後改成保留 GDB 互動 session(移除
--batch,加入monitor reset halt+ 中斷點設定),而非燒完就退出。