
前幾天又遇到一次典型的STM32WB5x BLE OTA卡死問題手機端顯示固件包傳輸到一半進度條不再前進設備側此時本來應該在收包、寫Flash但就是沒有任何回包。等了幾分鐘手機端BLE連接超時斷開之后不管怎么掃描都找不到設備廣播。重新上電也一樣——好像固件升級把整臺設備送進了某個“黑洞”狀態。這個問題的標題很直接STM32WB5x BLE OTA gets stuck mid-update with no way to reconnect — is a firmware-side timeout/reset the right approach? 我先說結論不能用一套簡單的 timeout reset 邏輯去掃尾但也不能完全不處理。關鍵是要找到卡在哪個階段、誰在卡、以及復位后從哪里啟動。這篇文章就圍繞這個思路把 STM32WB5x 上 BLE OTA 卡死的原因、排查手段、分層超時設計和可落地的復位策略完整拆開聊。1. 卡死現場從“傳輸中”到“徹底失聯”的三種表現1.1 下載階段卡住數據傳著傳著就沒有ACK了第一種卡死最容易被誤判為“藍牙斷連”。現象是手機APP通過 BLE 自定義服務向設備持續發送固件分片前面幾十個包都正常設備也回了 Write Response 或 Notify ACK在某個分片之后設備突然不回任何 Confirm 了。手機端要么等本次 GATT 寫操作超時要么等到 BLE 連接事件丟失最終報錯斷開。這種卡在下載階段的問題根源幾乎都不在協議棧而在 M4 應用核。STM32WB5x 的雙核架構里M0 核負責 BLE 協議棧M4 核負責用戶的業務代碼。GATT 寫請求來了M0 會把數據通過 IPCC 放進共享內存然后通知 M4 去取。M4 收到數據后要做的是把分片寫入目標 Flash。如果這段 Flash 寫入邏輯寫得很“霸道”——比如在擦寫整個扇區時長時間關中斷或者HAL_FLASH_Program一次寫入后再做 CRC 校驗那么 M4 處理完這個分片的時間可能超過 BLE 的連接間隔M0 側的 ACK 隊列就會越積越多。積壓到一定程度M0 的接收緩沖區滿協議棧開始丟棄新的 GATT 寫請求。手機端發出去的包沒人應答連接事件也會因為沒有及時交換空包而觸發鏈路監督超時。此時設備并沒有死只是 M4 忙到忘記喂 BLE 協議棧看起來就像被卡死了。1.2 安裝階段卡住FUS 一張接管設備直接消失第二種卡死比下載階段更迷惑人。所有固件分片都傳完了校驗也通過了然后 APP 端發送“開始安裝”的命令設備收到命令后可能回了一幀 ACK然后連接斷開。此后無論怎么掃描設備再也不會廣播。重新上電也一樣設備仿佛人間蒸發。如果你用 ST-Link 接上芯片很可能會發現 M4 還在跑但 M0 停在某個 FUS 服務里——這時設備并不是“壞了”而是進入了無線棧固件升級Wireless Stack Update的安裝流程。STM32WB5x 的 FUSFirmware Upgrade Service運行在 M0 核上負責安全安裝無線棧固件、管理密鑰等。當 FUS 開始安裝或者擦寫無線棧區域時BLE 協議棧必然停止運行所有無線活動都會終止。這個階段本來就不應該期望還能搜索到設備。問題在于FUS 安裝完成后可能需要幾秒到幾十秒一旦安裝失敗或 FUS 卡在某個內部狀態設備就永遠停留在“沒有無線棧可跑”的局面自然也不會有廣播。1.3 復位后卡住看起來是醒了其實在裸奔第三種情況出現在“timeout/reset 已經寫進固件”的設備上。工程師發現 OTA 卡住后在 M4 側加了一個看門狗或者收到“開始安裝”后倒計時 10 秒強制NVIC_SystemReset()。結果是設備確實能重啟但重啟后既不廣播、也不進入正常應用調試器連上去發現程序跑飛或死在 HardFault。這通常是復位后的啟動入口出了問題。OTA 過程如果已經把新的應用固件寫入了當前運行分區但復位后 bootloader 沒有做“鏡像有效性檢查”直接跳轉到一個寫了一半或者 CRC 校驗失敗的鏡像CPU 就會取指失敗卡在啟動階段。還有更隱蔽的一種OTA 期間使用獨立看門狗 IWDG但喂狗邏輯放在主循環里。當 M4 阻塞等待 FUS 回復時主循環停擺IWDG 超時復位。運氣好時復位后舊應用還在但升級標志被寫壞運氣不好正趕上 Flash 擦寫中途復位導致 Flash 內容不完整設備便再也沒能起來。2. 雙核架構下的OTA到底是誰在“卡”誰2.1 先理清 M4、M0 和 FUS 的職責邊界要處理卡死問題首先得把 STM32WB5x 里的三角關系搞清楚。M4 核跑應用邏輯也就是你寫的產品代碼M0 核跑 BLE 協議棧對外呈現為“無線核”。兩個核通過 IPCC 硬件單元和共享內存通信。M4 不需要知道 BLE 連接的底層細節它只負責從共享內存里取數據和發送命令。FUS 是 M0 上的一套系統服務類似一個運行在無線核上的 mini bootloader。M4 可以通過 IPCC 向 FUS 發送命令比如查詢版本、安裝無線棧、刪除無線棧、管理安全密鑰等。FUS 有自己的安全狀態和錯誤碼。當執行覆蓋無線棧區域的寫操作時FUS 會暫停當前無線棧運行因此 BLE 連接一定會斷。很多第一次做 STM32WB OTA 的人會以為“BLE OTA”就是把數據寫到 Flash 然后跳轉但實際上要區分升級對象升級用戶應用固件數據寫入應用分區通常與 FUS 無關。升級無線棧固件必須通過 FUS 或類似機制不僅要寫 M0 的代碼區域還要更新藍牙棧版本和配置。同時升級兩者先升級用戶應用再升級無線棧或反過來中間需要明確的狀態管理。如果沒有做這個區分就會在應用 OTA 過程中試圖調用 FUS或者在無線棧 OTA 過程中關閉了 FUS導致整個狀態機錯亂。2.2 應用固件OTA和無線棧OTA是兩條完全不同的路應用固件 OTA 通常走雙 Bank 方案當前從 Bank1 啟動新固件寫入 Bank2寫完后置位切換標志復位后 bootloader 檢查標志并嘗試從 Bank2 啟動如果 Bank2 無效則自動回退 Bank1。這種方式的好處是即使新固件寫壞了Bootloader 還能救回來前提是 Bank1 的舊固件仍然可用。無線棧 OTA 就復雜一點。無線棧代碼和 FUS 安全性綁定不能由 M4 直接寫入只能讓 FUS 去處理。M4 要做的是把無線棧固件包傳給 FUS再由 FUS 完成校驗、擦寫、安裝。這個過程中 M4 與 FUS 通過 IPCC 異步交互不能一邊等 FUS 一邊又把 BLE 協議棧殺掉。從調試角度看應用 OTA 卡住大多能在 M4 用戶代碼里找到原因無線棧 OTA 卡住則要優先查 FUS 返回的錯誤碼和 M0 狀態。兩者排查手段完全不同所以“一堆 timeout 邏輯走天下”在雙核 OTA 里根本行不通。2.3 “無法重連”的深層原因往往不在BLE超參數而在于狀態機遇到設備不能重新連接時很多人的第一反應是調 BLE 廣播參數縮短廣播間隔、打開可發現模式、延長連接超時。這些參數調整可能有效但治標不治本。真正的深層原因往往是沒有把 OTA 狀態持久化。比如OTA 下載了一半設備復位舊應用還在但 SFlash 里某個“升級進行中”的標志位被誤寫成了“升級完成”。新固件寫入 Bank2 后由于復位發生在標志位寫入之后、CRC 校驗完成之前Bootloader 檢查標志時認為可以切換結果跳到損壞的 Bank2。FUS 安裝無線棧時M4 因為超時執行了復位復位后 FUS 尚未完成安裝M0 沒有任何可運行的無線棧設備自然無法廣播。這些問題不是靠“多等幾秒”或“多復位幾次”能解決的。你得有一個“升級狀態機 啟動驗證 回退路徑”讓設備無論什么時候掉電都能從持久化狀態里判斷該繼續升級、該回退舊版本、還是進入恢復模式。3. 排查套路從復位原因到FUS狀態都翻一遍3.1 上ST-Link先看芯片還能不能連上調試口遇到 OTA 卡死別急著改代碼先把芯片接到 ST-Link打開 STM32CubeProgrammer。如果連接成功你能讀出器件 ID、Flash 大小、當前 FUS 版本和無線棧版本。這一步能立刻確認設備是否“活”著以及是哪個核在跑。如果連接失敗優先檢查目標板電源以及在 CubeProgrammer 連接設置里選擇正確的接口模式和復位模式。有時設備運行在不正常狀態需要把 BOOT0 拉高進入系統 Bootloader 再連接。不過 STM32WB5x 的內部 Bootloader 走的是 USART/USB不是 SWD所以如果 SWD 連不上通常說明芯片內部時鐘或電源出了大問題或者調試引腳被 M4 代碼復用并配置成了模擬輸入。3.2 第二眼看RCC_CSR這臺設備是“怎么死的”如果 SWD 能連上下一步我會讀取復位原因寄存器RCC_CSR。這個寄存器能告訴你最后一次復位是由誰觸發的上電復位 / 欠壓復位外部復位引腳獨立看門狗 IWDG 復位窗口看門狗 WWDG 復位軟件復位 NVIC_SystemReset其他原因在項目里用一個調試命令把復位原因打印出來會非常有價值。比如你發現設備每次 OTA 卡死后的復位原因是 IWDG那說明問題出在“沒有及時喂狗”如果復位原因是軟件復位說明你的超時邏輯真的執行了NVIC_SystemReset()但顯然它沒有解決問題。讀取代碼如下uint32_t csr RCC-CSR; if (csr RCC_CSR_RMVF_Msk) { RCC-CSR | RCC_CSR_RMVF_Msk; // 清除復位標志 } // 判斷 if (csr RCC_CSR_WDGRSTF_Msk) { // IWDG復位 } else if (csr RCC_CSR_SFTRSTF_Msk) { // 軟件復位 }我在實際項目中遇到過很隱蔽的情況設備上電后RCC_CSR里的 IWDG 復位標志一直存在而代碼里沒有在啟動早期清除它導致 Bootloader 以為上一次 OTA 異常退出不斷觸發回滾。這種“歷史復位原因”沒有清理干凈也會造成奇怪的啟動行為。3.3 第三步翻FUS狀態和無線棧版本STM32CubeProgrammer 有一個專門的 FUS 頁面打開后能直接看到 FUS 版本、無線棧版本、當前運行的安全狀態以及上一次 FUS 命令的執行狀態。如果上位機顯示無線棧區域為空或者 FUS 狀態不是 Running那基本可以判斷設備是卡在無線棧升級的安裝階段。此時你要么重新通過腳本刷入一套完整的無線棧固件要么用 Flash 全擦除后重建。注意STM32WB5x 的無線棧區域有安全校驗單純在 MDK 里隨便寫一個地址是沒用的必須走 FUS 或官方工具鏈。在自定義 OTA 流程中我建議把 FUS 命令的返回碼納入日志系統。比如FUS_ERROR_IMG_NOT_AUTHENTICATED固件包簽名校驗失敗。FUS_ERROR_OP_ABORTED操作被中斷。FUS_ERROR_NO_DEVICE_ACCESSM4 沒有對應權限。這些錯誤碼能幫你判斷問題出在 OTA 包的合法性還是出在 FUS 命令交互流程。3.4 如果連SWD都連不上優先懷疑Flash被寫花假設設備徹底沒反應SWD 也連不上這時不要上來就懷疑芯片壞了。先強制進入系統 Bootloader把調試口重新救出來。如果 STM32CubeProgrammer 能連接系統 Bootloader再對 Flash 做整片擦除擦除后一般都能通過 SWD 重新連接。這種情況多發生在 FUS 安裝過程中被 M4 側硬復位打斷。雖然 STM32WB5x 在 Flash 擦寫上做了一些保護但復位發生在閃存編程時序中間仍可能導致無線棧區域處于不穩定狀態。而一旦無線棧區域內容損壞M0 無法啟動協議棧整顆芯片就表現為“BLE 完全消失”但 M4 可能還在空轉。所以從設計層面講任何可能中斷 Flash 編程的操作都必須格外小心。尤其是無線棧升級期間不是萬不得已不要用 M4 側看門狗去打斷 FUS 流程。4. 固件側timeout/reset能解決什么不能解決什么4.1 超時機制的正確分層傳輸層、安裝層、啟動層回到標題里的問題firmware-side timeout/reset 到底是不是正確方案我的答案是timeout 是必須的但 reset 要分級、分層不能一把梭。可以把整個 OTA 流程分成三個層次層次典型超時場景正確處理方式是否建議直接復位傳輸層長時間沒有收到下一個固件分片中止本次下載保持廣播等待重連通常不需要復位安裝層FUS 安裝無線棧超時查詢 FUS 狀態記錄錯誤再決定復位謹慎需先保護標志啟動層Bootloader 檢測到鏡像無效回退到上一分區或進入恢復模式通過跳轉邏輯完成不屬于普通復位4.2 傳輸層超時該中止但不該急著復位傳輸層的超時最簡單。M4 收到固件分片后維護一個last_rx_tick。如果超過某個閾值比如 5 秒沒有收到新的分片就認為本次傳輸已經失敗。此時正確的做法是把 OTA 狀態機切到OTA_STATE_FAILED。記錄失敗的偏移量和錯誤碼。釋放本次 OTA 占用的緩存。判斷當前正在運行的應用是否有效如果有效繼續跑舊應用同時繼續保持 BLE 可連接。如果舊應用已經被部分覆蓋比如采用了單 Bank 原地升級則應讓設備停留在“可發現且可重新下載”的狀態等待手機端重連后重新把完整固件包傳一遍。為什么不要立即復位因為傳輸層超時往往只是手機空中的射頻干擾、BLE 連接參數不匹配、或手機端 APP 異常此時設備本身沒有掛。你復位自己反而會讓手機端徹底丟掉連接不復位至少手機端還能通過當前連接繼續補發幾包。我在實測中發現BLE OTA 傳文件時如果手機屏幕熄滅或系統進入省電模式連接會進入 dormant 狀態幾秒鐘沒有數據是很正常的。把傳輸超時設成 2 秒會誤傷設成 10 秒又會讓卡死恢復太慢。一般建議根據實際數據包間隔設置比如你的 APP 每 20ms 發一包可以設 2 秒如果每 100ms 發一包可以設 4~5 秒。4.3 安裝層超時需要“先確認狀態再決定是否復位”安裝層指的就是“固件傳輸完成開始擦寫/切換”這個階段。如果是應用固件雙 Bank 切換整個過程很短M4 直接操作 Flash超時主要關注擦寫時間是否異常。如果是 FUS 安裝無線棧過程可能跨越數秒到幾十秒此時 M4 與 FUS 基于 IPCC 異步交互。這個階段最忌諱的就是“M4 等不到回復后直接調用NVIC_SystemReset()”。為什么因為 FUS 可能在擦寫無線棧 Flash此時硬復位可能會打斷 FUS 的擦寫流程。STM32WB5x 的 FUS 內部有一定恢復機制但你在錯誤的時間點打斷它可能讓無線棧區域處于“存在但校驗失敗”的狀態。與其這樣不如在進入安裝前就設計好先通過 FUS 查詢當前狀態如果 FUS 還活著讓它繼續跑或者主動發送一個放棄命令。如果查詢 FUS 也得不到響應才考慮復位并且復位后要檢查無線棧版本和 FUS 錯誤碼決定是否進入“恢復模式”。4.4 啟動層回退把最后一道防線放在bootloaderOTA 中最重要的一道防線不是“超時復位”而是“啟動時驗證”。無論你采用雙 Bank 還是單 BankBootloader 都要在跳轉到應用之前驗證目標鏡像的有效性。STM32WB5x 的雙 Bank 結構非常適合做 A/B 升級。你可以在 Bootloader 里做以下檢查讀取用戶分區頭部的鏡像 CRC 或簽名。如果 CRC 有效正常跳轉。如果 CRC 無效檢查另一個 Bank 的 CRC。如果另一個 Bank 有效切換啟動。如果兩個 Bank 都無效進入串口或 BLE 恢復模式。我在項目里把這條規則固化為“啟動鏈”Bootloader - App A / App B。OTA 寫入只寫非運行 Bank寫完置位 “pending” 標志復位后 Bootloader 根據標志和校驗結果決定正式切換還是回滾。這樣一來即使安裝階段復位時機不對只要舊 Bank 還完整設備至少能回到舊版本而不是“徹底失聯”。回退機制雖然不能替代超時復位但它能在超時復位做錯時兜底。很多“OTA 卡死無法重連”的最終原因其實是 Bootloader 沒有做鏡像有效性檢查啟動鏈斷了。5. 一套可以落地的OTA超時復位參考設計5.1 狀態標志和數據布局先行在設計 OTA 邏輯之前先確定狀態標志放在哪。ST 官方的雙 Bank 方案一般會使用幾個 Flash 字或 Option Byte 作為升級標志。由于 Flash 寫入要擦除不建議頻繁修改同一個字。更穩妥的做法是用一個獨立的配置扇區專門保存 OTA 狀態。一個簡單的狀態結構體大概長這樣typedef struct { uint32_t magic; uint32_t ota_state; // 0: idle, 1: transferring, 2: pending_switch, 3: failure uint32_t transfer_offset; uint32_t target_bank; // 目標啟動區域 uint32_t last_error_code; uint32_t crc32_of_struct; } OtaPersistentStatus;每次更新狀態后先計算整個結構體的 CRC32再寫入保存區。Bootloader 啟動時先檢查 magic 和 CRC32如果校驗失敗就認為狀態不可信強制走安全啟動路徑。別小看這一步很多 OTA 卡死就是狀態標志半寫半不寫造成的。5.2 傳輸超時的計算和喂狗節奏傳輸超時不能拍腦袋定。先量一下你的 BLE 連接實際帶寬手機端每隔多少毫秒收到一個 Write Response如果平均間隔是 30ms那 3 秒內沒有任何包就說明鏈路已經斷了或者對端已經放棄。設計喂狗時也要注意喂狗不能放在while(1)主循環里因為 OTA 接收數據時主循環可能長時間阻塞。更好的做法是“事件驅動 后臺任務輪詢”void OTA_BleOnWrite(uint8_t *pData, uint16_t len) { // 接收更多數據 OTA_Context.rx_data_happened true; OTA_Context.last_rx_tick HAL_GetTick(); // 寫Flash或者緩存 } void App_MainLoop(void) { uint32_t now HAL_GetTick(); if (OTA_Context.state OTA_STATE_TRANSFERRING) { if (now - OTA_Context.last_rx_tick OTA_TRANSFER_TIMEOUT_MS) { OTA_AbortTransfer(); } } // 其他任務 HAL_IWDG_Refresh(hiwdg); // 不要在Flash擦寫期間長時間不進這里 }如果在接收回調里直接寫 Flash而且 Flash 擦寫時間比較長要注意在擦寫前開一個足夠寬容的看門狗窗口。或者干脆把接收緩沖做深一點數據先放進 RAM后臺慢慢寫 Flash。這樣 BLE 協議棧能更及時地回 ACK連接不容易斷。我在實測中比較推薦“RAM 緩沖 后臺 Flash 寫入”的模式。每次 BLE 收滿一個扇區大小的數據就把數據優先緩存到內部 SRAM 或外部 PSRAM然后在主循環的批量寫入階段統一寫 Flash。缺點是占用 RAM但能極大降低傳輸層卡死的概率。5.3 安裝階段復位策略的偽代碼思路當所有固件包接收完成進入安裝階段時我會把流程拆成三步void OTA_StartInstall(void) { // 1. 持久化狀態進入 install 階段 OTA_SaveStatus(OTA_STATE_INSTALLING, target_bank); // 2. 等待FUS或Flash切換完成 uint32_t timeout HAL_GetTick() OTA_INSTALL_TIMEOUT_MS; while (HAL_GetTick() timeout) { HAL_IWDG_Refresh(hiwdg); // 對FUS安裝流程輪詢FUS查詢命令 FUS_Status_t st FUS_QueryStatus(); if (st FUS_STATUS_READY) { OTA_SaveStatus(OTA_STATE_COMPLETED, target_bank); return; } if (st FUS_STATUS_ERROR) { OTA_HandleFusError(st); return; } } // 3. 超時處理先記錄錯誤再考慮復位 OTA_SaveStatus(OTA_STATE_FAILURE, OTA_GetCurrentBank()); NVIC_SystemReset(); }注意第 3 步的復位不是首選項。如果 FUS 查詢命令能響應我寧可用 FUS 命令去中止或確認而不是直接系統復位。只有 FUS 已經無響應、且確認繼續等下去沒有任何進展時才執行復位。復位后用 Bootloader 的鏡像校驗來兜底。5.4 實測驗證哪些方案能“救回來”哪些會“越救越死”我實際測試過三種策略結果差別很大策略 A傳輸層超時后立即NVIC_SystemReset()。結果設備重啟后能起來但如果 OTA 有大量緩存沒有落盤導致寫了一半Bootloader 沒有回退邏輯設備卡死。越救越死。策略 B傳輸層超時后中止升級但保持廣告等待重連。結果設備能重新被發現手機端重新連接后可以重新傳輸雖然浪費時間但可靠。策略 CFUS 安裝階段從HAL_GetTick()到 30 秒超時后硬復位。結果有幾次設備能恢復正常有幾次無線棧區域被寫壞需要用 ST-Link 全擦后重刷。風險很高。綜合來看最可靠的設計是傳輸層盡量不重啟安裝層盡量不硬復位啟動層盡量依賴 Bootloader 自動回退。6. 幾個關鍵決策和給后來者的建議回到最開始的問題firmware-side timeout/reset 是不是正確做法我的看法是timeout 必須放在 OTA 的每一層但 reset 是最后手段不能放在傳輸層“每等不到回包就復位”。正確姿勢是把 OTA 設計成狀態機每個狀態都有明確的超時行為、持久化標志和恢復路徑。復位只是所有恢復路徑都不生效時的最終兜底。我在實際項目里踩過坑尤其建議注意這幾點第一不要讓 M4 在等待 FUS 回復時完全阻塞。哪怕是一個 5ms 的HAL_Delay也要保證主循環仍然能喂狗、能處理其他后臺任務。第二復位之前一定要保存錯誤信息。你可以把last_error_code和ota_state寫到備份寄存器或者 Flash 的專用區域這樣復位后可以通過日志直接看到“上次是為什么復位”。沒有這一步你只能靠猜。第三升級無線棧之前先確認當前 FUS 版本和無線棧版本確保無線棧固件包與 FUS 兼容。版本不匹配導致 FUS 安裝失敗的情況比硬件問題常見得多。使用官方 OTA 例程時也要看清示例中給的無線棧版本不要隨手拿別的版本刷進去。最后再分享一個排查技巧OTA 卡死時用 STM32CubeMonitor 或一個簡單的串口日志把 M4 側收到的 BLE 數據字節數和 FUS 返回的每一條命令狀態都記錄下來。我靠這招定位過很多“看起來像玄學”的升級失敗——其實只是某個包在傳輸層丟了但上層一直沒等回來。把日志打印到 Flash 尾巴上掉電也不丟。這樣即使設備徹底失聯接上 ST-Link 還能從 Flash 里把最后的現場數據讀出來比盲猜高效得多。