從零開始在家開發 IoT: Flash & Run 腳本解析(STM32 + WiFi HaLow)

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 啟動後仍能單步 / 下中斷點,挑戰才開始,常見對策:

  1. 開發版韌體中暫時關掉 watchdog
  2. 停用進入 stop / sleep 的低功耗路徑,避免除錯時脈被 gate。
  3. 善用 cfg 的 CONNECT_ASSERT_SRST,在 reset 當下搶進去。
  4. 把腳本最後改成保留 GDB 互動 session(移除 --batch,加入 monitor reset halt + 中斷點設定),而非燒完就退出。
相关推荐
agathakuan1 小时前
從零開始在家開發 IoT: OpenOCD 與 GDB 協作指南
stm32·gnu·rtc
czy878747511 小时前
vscode编译make命令要修改stm32cubemx生成的STM32F103XX_FLASH.ld文件
ide·vscode·stm32
优信电子13 小时前
STM32/C51驱动 DHTC11 温湿度传感器
stm32·单片机·嵌入式硬件·c51·温湿度传感器·dhtc11·环境测量
2zcode15 小时前
基于STM32的多功能万年历电子闹钟设计与实现
stm32·单片机·嵌入式硬件
0南城逆流015 小时前
【STM32】RTT-Studio中HAL库开发教程十四:MSMART串口组件
stm32·单片机·嵌入式硬件
小慧102416 小时前
STM 32 TIM定时器(2)
stm32·单片机
无痕幽雨16 小时前
STM32实现MQTT及JSON包思路二
stm32·单片机·嵌入式硬件
風清掦16 小时前
【STM32学习笔记-14】WDG看门狗 - 14.2 WWDG窗口看门狗
笔记·stm32·单片机·嵌入式硬件·学习·fpga开发
物联通信量讯说18 小时前
物联网卡、流量卡、SIM 卡到底有什么区别?
物联网·iot·物联网卡