OTA實(shí)戰(zhàn):從Flash頁(yè)擦除到向量表重映射)
1. 項(xiàng)目概述為什么AB分區(qū)OTA在STM32F103上不是“錦上添花”而是“生死線”你手頭有一塊跑著溫控算法的STM32F103C8T6最小系統(tǒng)板固件已經(jīng)穩(wěn)定運(yùn)行了八個(gè)月。某天客戶突然要求加一個(gè)遠(yuǎn)程參數(shù)校準(zhǔn)功能你改完代碼、編譯通過(guò)、用J-Link燒錄測(cè)試——一切OK。第二天一早產(chǎn)線反饋50臺(tái)設(shè)備全部變磚串口無(wú)響應(yīng)J-Link連不上SWD引腳測(cè)不到時(shí)鐘。你拆開(kāi)其中一臺(tái)發(fā)現(xiàn)Flash里前4KB全是0xFFBootloader跳轉(zhuǎn)地址被擦成了0x00000000。這不是運(yùn)氣差這是單一分區(qū)OTA的必然宿命。AB分區(qū)OTA對(duì)STM32F103這類資源極度受限的Cortex-M3芯片而言從來(lái)就不是什么高大上的“在線升級(jí)黑科技”它是一套用空間換時(shí)間、用冗余保生存的硬核容錯(cuò)機(jī)制。它的核心邏輯非常樸素永遠(yuǎn)保留一份已知可用的完整固件A區(qū)所有升級(jí)操作都在另一份空白區(qū)域B區(qū)進(jìn)行只有當(dāng)B區(qū)寫入完成、校驗(yàn)無(wú)誤、且能成功跳轉(zhuǎn)執(zhí)行后才把啟動(dòng)指針從A切到B。哪怕升級(jí)中途斷電、通信中斷、甚至Flash寫入一半被拉閘下一次上電芯片依然會(huì)從A區(qū)冷啟動(dòng)設(shè)備照常工作——用戶根本感知不到后臺(tái)發(fā)生過(guò)一場(chǎng)“生死劫”。這背后牽扯的全是硬骨頭STM32F103的Flash擦除必須按頁(yè)1KB/頁(yè)而標(biāo)準(zhǔn)庫(kù)v3.50里沒(méi)有現(xiàn)成的AB分區(qū)管理API它的SRAM只有20KB放不下雙份固件更容不下一個(gè)完整的壓縮解包器它的Bootloader必須自己重寫不能依賴ST官方提供的UART Bootloader那個(gè)只支持串口下載不支持應(yīng)用層觸發(fā)的OTA流程最要命的是一旦Bootloader跳轉(zhuǎn)邏輯出錯(cuò)或者APP固件入口地址沒(méi)對(duì)齊到4字節(jié)邊界就會(huì)直接卡死在HardFault_Handler連調(diào)試信息都吐不出來(lái)。網(wǎng)上那些“5分鐘搞定STM32 OTA”的教程90%都省略了Flash頁(yè)擦除時(shí)序、向量表偏移重映射、IAP校驗(yàn)失敗后的回滾策略這些真正決定成敗的細(xì)節(jié)。本教程不講虛的只帶你從零開(kāi)始一行一行敲出能在真實(shí)產(chǎn)線環(huán)境扛住斷電、通信抖動(dòng)、Flash老化等惡劣條件的AB分區(qū)OTA實(shí)現(xiàn)。適合所有正在用STM32F103做工業(yè)傳感器、智能電表、樓宇控制器的工程師也適合被“error: flash download failed - target dll has been cancelled”折磨到懷疑人生的嵌入式新手——這個(gè)錯(cuò)誤90%是因?yàn)槟阍谖搓P(guān)閉全局中斷的情況下擦除了包含中斷向量表的Flash頁(yè)。2. 整體架構(gòu)設(shè)計(jì)與關(guān)鍵取舍為什么放棄“優(yōu)雅”選擇“粗暴可靠”2.1 分區(qū)規(guī)劃不是數(shù)學(xué)題是物理約束下的妥協(xié)STM32F103C8T6的Flash總?cè)萘繛?4KB但實(shí)際能用于APP的遠(yuǎn)少于這個(gè)數(shù)。我們先畫一張真實(shí)的內(nèi)存地圖地址區(qū)間大小用途關(guān)鍵約束0x08000000–0x08003FFF16KBBootloader區(qū)必須包含復(fù)位向量、中斷向量表、IAP擦寫函數(shù)、跳轉(zhuǎn)邏輯需預(yù)留至少2KB應(yīng)對(duì)未來(lái)功能擴(kuò)展0x08004000–0x0800BFFF32KBA區(qū)主程序存放當(dāng)前運(yùn)行的APP固件起始地址必須是Flash頁(yè)邊界0x08004000是第16頁(yè)起始0x0800C000–0x08013FFF32KBB區(qū)升級(jí)區(qū)存放待升級(jí)的新固件大小必須≥A區(qū)否則無(wú)法容納新版本你可能會(huì)問(wèn)為什么A/B區(qū)各32KB64KB Flash減去16KB Bootloader只剩48KB平分就是24KB。但這里有個(gè)致命陷阱STM32F103的Flash擦除是以頁(yè)P(yáng)age為單位每頁(yè)1KB。如果你把A區(qū)設(shè)為24KB0x08004000–0x0801BFFF那它跨越了24個(gè)頁(yè)。而OTA升級(jí)時(shí)B區(qū)需要先整片擦除再寫入。如果B區(qū)也按24KB規(guī)劃它就必須從0x0801C000開(kāi)始但0x0801C000之后只剩12KB空間到0x08027FFF根本不夠放一個(gè)24KB的固件。更糟的是0x0801C000不是Flash頁(yè)邊界第28頁(yè)起始是0x0801C000錯(cuò)第28頁(yè)是0x0801C000查RM0008手冊(cè)Table 11第28頁(yè)起始地址是0x0801C000不是0x0801C000翻手冊(cè)確認(rèn)第0頁(yè)0x08000000第1頁(yè)0x08000400…第28頁(yè)是0x0800B000不對(duì)重新計(jì)算每頁(yè)1KB0x400字節(jié)第n頁(yè)起始0x08000000 n×0x400。第16頁(yè)0x0800000016×0x4000x08001000錯(cuò)了0x08000000是第0頁(yè)0x08000400是第1頁(yè)所以第16頁(yè)是0x0800000016×0x4000x08001000但0x08001000是第16頁(yè)查手冊(cè)Table 11明確寫著STM32F103xx medium-density devices have pages of 1 Kbyte, starting from address 0x08000000 (page 0), 0x08000400 (page 1), ..., up to 0x0800FC00 (page 63)。所以第16頁(yè)起始是0x0800000016×0x4000x080010000x080000000x10000x08001000對(duì)。但我們的Bootloader要占16KB即16頁(yè)0x0000–0x3FFF所以Bootloader結(jié)束于0x08003FFF下一頁(yè)第16頁(yè)起始是0x08004000。這才是A區(qū)的起點(diǎn)。因此A區(qū)從0x08004000開(kāi)始占32頁(yè)32KB到0x0800BFFF結(jié)束。B區(qū)緊接著從0x0800C000第28頁(yè)起始開(kāi)始占32頁(yè)到0x08013FFF結(jié)束。這樣A/B區(qū)都是整頁(yè)對(duì)齊擦除時(shí)只需調(diào)用FLASH_ErasePage(0x08004000)和FLASH_ErasePage(0x0800C000)不會(huì)跨頁(yè)誤擦。這個(gè)規(guī)劃不是拍腦袋是手冊(cè)白紙黑字的物理約束倒逼出來(lái)的唯一解。提示很多初學(xué)者栽在分區(qū)地址不對(duì)齊上。例如把A區(qū)設(shè)為0x08004100結(jié)果擦除時(shí)FLASH_ErasePage(0x08004100)會(huì)自動(dòng)向下取整到0x08004000把Bootloader末尾1KB也擦掉了。務(wù)必用地址0xFFFFF000對(duì)齊到4KB或查手冊(cè)確認(rèn)頁(yè)邊界。2.2 Bootloader與APP的職責(zé)切割誰(shuí)該干臟活誰(shuí)該享清福一個(gè)健壯的AB分區(qū)系統(tǒng)本質(zhì)是兩個(gè)獨(dú)立程序的精密協(xié)作。它們之間必須有清晰的“楚河漢界”否則就是災(zāi)難的開(kāi)始。Bootloader的鐵律絕不主動(dòng)修改自身代碼Bootloader的Flash區(qū)域0x08000000–0x08003FFF在運(yùn)行時(shí)必須是只讀的。任何升級(jí)操作只能擦寫A區(qū)或B區(qū)Bootloader自身代碼和數(shù)據(jù)必須固化。只做三件事a) 上電后檢查B區(qū)有效性CRC32校驗(yàn)?zāi)?shù)驗(yàn)證b) 若有效則跳轉(zhuǎn)至B區(qū)c) 若無(wú)效則跳轉(zhuǎn)至A區(qū)。其余所有事接收升級(jí)包、解析、寫Flash、校驗(yàn)均由APP發(fā)起并控制。向量表重映射是生命線當(dāng)APP在B區(qū)運(yùn)行時(shí)其中斷向量表在0x0800C000但CM3內(nèi)核默認(rèn)從0x08000000取向量。必須在APP啟動(dòng)時(shí)執(zhí)行SCB-VTOR FLASH_BASE | 0x0000C000;將向量表基址重映射到B區(qū)首地址。否則任何中斷如SysTick、USART都會(huì)跳轉(zhuǎn)到Bootloader的中斷服務(wù)程序?qū)е虏豢深A(yù)測(cè)行為。APP的契約義務(wù)提供標(biāo)準(zhǔn)化的IAP接口在APP的main()之前必須定義一個(gè)全局函數(shù)指針數(shù)組暴露IAP_WriteFlash,IAP_ReadFlash,IAP_GetAppStatus等函數(shù)。Bootloader通過(guò)這個(gè)接口與APP通信而不是直接調(diào)用APP內(nèi)部函數(shù)。承擔(dān)全部升級(jí)邏輯APP負(fù)責(zé)通過(guò)UART/USB/以太網(wǎng)接收升級(jí)包通常為bin文件將其解包、校驗(yàn)、寫入B區(qū)指定地址并在寫入完成后設(shè)置一個(gè)標(biāo)志位如在B區(qū)末尾寫入特定魔數(shù)0xDEADBEEF。安全退出機(jī)制APP在完成B區(qū)寫入并校驗(yàn)無(wú)誤后不能直接跳轉(zhuǎn)。必須先設(shè)置一個(gè)“待重啟標(biāo)志”例如在備份SRAM或Flash特定位置寫入0xAA55然后調(diào)用NVIC_SystemReset()軟復(fù)位。復(fù)位后Bootloader讀取該標(biāo)志確認(rèn)應(yīng)從B區(qū)啟動(dòng)。這種設(shè)計(jì)看似繁瑣實(shí)則是為了隔離風(fēng)險(xiǎn)。如果Bootloader自己去實(shí)現(xiàn)UART接收和Flash寫入一旦UART驅(qū)動(dòng)有bug導(dǎo)致死循環(huán)整個(gè)系統(tǒng)就徹底鎖死連J-Link都無(wú)法連接。而讓APP來(lái)干即使APP升級(jí)邏輯崩潰Bootloader依然能保證從A區(qū)啟動(dòng)設(shè)備可恢復(fù)。2.3 為什么不用HAL庫(kù)堅(jiān)持用標(biāo)準(zhǔn)庫(kù)v3.50網(wǎng)上大量教程推薦用STM32CubeMX生成HAL庫(kù)工程來(lái)做OTA。我試過(guò)也踩過(guò)坑。HAL庫(kù)的HAL_FLASHEx_Erase()函數(shù)封裝了頁(yè)擦除邏輯看起來(lái)很美。但問(wèn)題在于HAL庫(kù)的Flash驅(qū)動(dòng)默認(rèn)啟用了FLASH_FLAG_EOP操作完成和FLASH_FLAG_WRPRTERR寫保護(hù)錯(cuò)誤中斷。在OTA升級(jí)過(guò)程中如果恰好有SysTick中斷或其他外設(shè)中斷搶占了Flash擦除操作而你的中斷服務(wù)程序里又調(diào)用了HAL_Delay()它依賴SysTick就會(huì)陷入死鎖——因?yàn)镾ysTick被掛起HAL_Delay()永遠(yuǎn)等不到超時(shí)。標(biāo)準(zhǔn)庫(kù)v3.50的FLASH_ErasePage()是純輪詢實(shí)現(xiàn)不依賴任何中斷只要在擦除前關(guān)閉全局中斷__disable_irq()擦除后開(kāi)啟__enable_irq()就能100%避免此類競(jìng)態(tài)。雖然代碼多寫幾行但換來(lái)的是在產(chǎn)線7×24小時(shí)運(yùn)行的絕對(duì)可靠。對(duì)于資源緊張的F103少一個(gè)中斷服務(wù)程序就少幾百字節(jié)RAM占用這筆賬必須算清楚。3. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)從向量表重映射到CRC32校驗(yàn)的每一處陷阱3.1 向量表重映射不是配個(gè)寄存器就完事向量表重映射Vector Table Relocation是AB分區(qū)能跑起來(lái)的第一道門檻。很多教程只告訴你一句SCB-VTOR 0x0800C000;然后就沒(méi)了。但實(shí)際中這句話放在哪里決定了你是成功還是HardFault。錯(cuò)誤做法在APP的main()函數(shù)第一行就寫SCB-VTOR 0x0800C000;。后果此時(shí)C庫(kù)的初始化如.data段復(fù)制、.bss段清零尚未完成SCB結(jié)構(gòu)體可能還未被正確映射訪問(wèn)SCB-VTOR會(huì)觸發(fā)UsageFault。正確做法在APP的啟動(dòng)文件startup_stm32f10x_md.s中在Reset_Handler標(biāo)簽之后、跳轉(zhuǎn)到main之前插入重映射代碼。具體步驟打開(kāi)startup_stm32f10x_md.s找到Reset_Handler標(biāo)號(hào)。在LDR R0, SystemInit指令之后、LDR R0, main指令之前插入LDR R0, 0x0800C000 ; B區(qū)向量表起始地址 MOV R1, #0x20000000 ; SCB-VTOR寄存器地址 (0xE000ED08) STR R0, [R1, #8] ; 將R0值寫入VTOR (偏移8字節(jié))確保SystemInit()函數(shù)中沒(méi)有調(diào)用任何會(huì)修改SCB-VTOR的代碼標(biāo)準(zhǔn)庫(kù)v3.50的SystemInit不會(huì)動(dòng)它。為什么必須在這里做因?yàn)镽eset_Handler是CPU復(fù)位后執(zhí)行的第一段代碼此時(shí)所有硬件寄存器都處于初始狀態(tài)SCB基地址0xE000ED00是確定的VTOR寄存器偏移0x08可以安全訪問(wèn)。等C庫(kù)初始化完成棧指針、全局變量都就緒了再跳進(jìn)main就不會(huì)有地址訪問(wèn)異常。注意SCB-VTOR的值不是直接寫入地址而是寫入FLASH_BASE | offset。offset必須是256字節(jié)的整數(shù)倍即低8位為0。B區(qū)向量表必須從0x0800C000開(kāi)始因?yàn)?x0800C000 0xFFFFFF00 0x0800C000滿足對(duì)齊要求。如果B區(qū)從0x0800C100開(kāi)始VTOR寫入0x0800C100會(huì)導(dǎo)致內(nèi)核從錯(cuò)誤地址取向量必死無(wú)疑。3.2 Flash頁(yè)擦除的時(shí)序與保護(hù)別讓“擦得干凈”變成“擦得絕望”STM32F103的Flash擦除不是瞬間完成的它需要時(shí)間。根據(jù)手冊(cè)擦除一頁(yè)1KB典型時(shí)間為20ms最大可達(dá)40ms。如果你在擦除后立刻嘗試寫入或者在擦除過(guò)程中響應(yīng)了中斷就會(huì)觸發(fā)FLASH_SR_BSY忙標(biāo)志導(dǎo)致后續(xù)操作失敗。實(shí)操要點(diǎn)擦除前必須關(guān)閉所有可能觸發(fā)Flash操作的中斷不僅僅是SysTick還包括任何使用HAL_Delay()、osDelay()如果你用FreeRTOS的定時(shí)器。最穩(wěn)妥的做法是擦除全程關(guān)閉全局中斷__disable_irq(); FLASH_ErasePage(0x0800C000); __enable_irq();。擦除后必須輪詢等待完成標(biāo)準(zhǔn)庫(kù)FLASH_ErasePage()函數(shù)內(nèi)部已經(jīng)包含了輪詢FLASH_SR_BSY的邏輯但它返回的是FLASH_Status。你必須檢查返回值FLASH_Status status FLASH_ErasePage(0x0800C000); if(status ! FLASH_COMPLETE) { // 擦除失敗可能是寫保護(hù)未解除或電壓不穩(wěn) while(1); // 進(jìn)入死循環(huán)便于調(diào)試 }寫保護(hù)解除是前提F103的Flash默認(rèn)是寫保護(hù)的。在擦除或?qū)懭肭氨仨氄{(diào)用FLASH_Unlock()操作完成后必須調(diào)用FLASH_Lock()上鎖。FLASH_Unlock()需要向KEYR寄存器寫入兩個(gè)特定密鑰0x45670123, 0xCDEF89AB順序不能錯(cuò)否則鎖死。我見(jiàn)過(guò)太多人因?yàn)閺?fù)制粘貼時(shí)漏掉一個(gè)0導(dǎo)致Flash再也無(wú)法寫入只能用J-Link的“Unlock Flash”功能救急。一個(gè)血淚教訓(xùn)某次調(diào)試我把FLASH_Unlock()放在了main()里然后在while(1)循環(huán)里反復(fù)擦寫B(tài)區(qū)做測(cè)試。結(jié)果第一次擦寫成功第二次就報(bào)FLASH_ERROR_WRPRT。原因FLASH_Lock()只鎖了一次但FLASH_Unlock()需要每次操作前都調(diào)用。正確的模式是FLASH_Unlock(); FLASH_ErasePage(0x0800C000); // ... 寫入數(shù)據(jù) ... FLASH_Lock(); // 每次操作后立即上鎖3.3 CRC32校驗(yàn)為什么不用簡(jiǎn)單的累加和OTA升級(jí)的核心信任鏈?zhǔn)加趯?duì)B區(qū)固件完整性的校驗(yàn)。很多人圖省事用一個(gè)16位累加和sum data[i]來(lái)校驗(yàn)。這在實(shí)驗(yàn)室環(huán)境下可能沒(méi)問(wèn)題但在產(chǎn)線一個(gè)電源紋波、一次EMI干擾就足以讓累加和“碰巧”通過(guò)。CRC32是工業(yè)級(jí)標(biāo)準(zhǔn)它能檢測(cè)出所有單比特錯(cuò)誤、所有雙比特錯(cuò)誤、所有奇數(shù)個(gè)比特錯(cuò)誤以及長(zhǎng)度≤32比特的突發(fā)錯(cuò)誤。實(shí)現(xiàn)要點(diǎn)校驗(yàn)范圍必須精確不是校驗(yàn)整個(gè)B區(qū)0x0800C000–0x08013FFF而是校驗(yàn)B區(qū)中APP固件的實(shí)際長(zhǎng)度。假設(shè)你的APP.bin文件大小為28672字節(jié)0x7000那么校驗(yàn)范圍就是0x0800C000–0x08012FFF0x0800C000 0x7000 - 1。多校驗(yàn)一個(gè)字節(jié)CRC值就不同。校驗(yàn)值存儲(chǔ)位置將計(jì)算出的CRC32值4字節(jié)寫入B區(qū)的固定偏移例如B區(qū)末尾的4個(gè)字節(jié)0x08013FFC–0x08013FFF。Bootloader啟動(dòng)時(shí)先讀取這4個(gè)字節(jié)作為期望值再對(duì)B區(qū)固件主體0x0800C000–0x08012FFF重新計(jì)算CRC32兩者比對(duì)。字節(jié)序陷阱STM32是小端機(jī)Little-Endian。CRC32計(jì)算函數(shù)返回的uint32_t值其最低字節(jié)LSB存儲(chǔ)在最低地址。因此當(dāng)你把crc_value寫入Flash時(shí)應(yīng)該用uint8_t crc_bytes[4]; crc_bytes[0] (crc_value 0) 0xFF; // LSB crc_bytes[1] (crc_value 8) 0xFF; crc_bytes[2] (crc_value 16) 0xFF; crc_bytes[3] (crc_value 24) 0xFF; // MSB // 然后依次寫入0x08013FFC, 0x08013FFD, 0x08013FFE, 0x08013FFF如果你直接用*(uint32_t*)0x08013FFC crc_value;在小端機(jī)上這等價(jià)于上面的操作是安全的。但顯式拆解字節(jié)邏輯更清晰也方便移植到大端平臺(tái)。性能考量對(duì)32KB數(shù)據(jù)做CRC32用軟件查表法256項(xiàng)表約需15ms72MHz主頻。這在OTA流程中是可以接受的。不要為了省幾毫秒而用不安全的校驗(yàn)方式。4. 實(shí)操過(guò)程與核心環(huán)節(jié)實(shí)現(xiàn)從Keil工程配置到J-Link燒錄的全流程4.1 Keil MDK工程配置三個(gè)關(guān)鍵分散加載文件Scatter FileKeil的分散加載文件.sct是控制代碼和數(shù)據(jù)在Flash/RAM中布局的靈魂。一個(gè)AB分區(qū)工程必須有三個(gè)不同的.sct文件分別對(duì)應(yīng)Bootloader、A區(qū)APP、B區(qū)APP。Bootloader.sctLR_IROM1 0x08000000 0x00004000 { ; load region size_region ER_IROM1 0x08000000 0x00004000 { ; load address execution address *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00005000 { ; 20KB SRAM .ANY (RW ZI) } }這個(gè)文件告訴鏈接器Bootloader代碼從0x08000000開(kāi)始最大占16KB0x4000RAM從0x20000000開(kāi)始。APP_A.sct用于編譯A區(qū)固件LR_IROM1 0x08004000 0x00008000 { ; A區(qū)32KB ER_IROM1 0x08004000 0x00008000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00005000 { .ANY (RW ZI) } }注意ER_IROM1的起始地址是0x08004000大小0x800032KB。APP_B.sct用于編譯B區(qū)固件LR_IROM1 0x0800C000 0x00008000 { ; B區(qū)32KB ER_IROM1 0x0800C000 0x00008000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00005000 { .ANY (RW ZI) } }起始地址變?yōu)?x0800C000。配置步驟在Keil中右鍵點(diǎn)擊工程 → “Options for Target...” → “Linker”選項(xiàng)卡。勾選“Use Memory Layout from Target Dialog”取消勾選然后在“Scatter File”框中輸入對(duì)應(yīng).sct文件的路徑例如.\scatter\APP_B.sct。切換到“C/C”選項(xiàng)卡在“Define”框中添加宏定義例如APP_IN_B_REGION。這個(gè)宏會(huì)在APP代碼中用于條件編譯比如#ifdef APP_IN_B_REGION SCB-VTOR 0x0800C000; // B區(qū)向量表 #else SCB-VTOR 0x08004000; // A區(qū)向量表 #endif提示編譯B區(qū)APP時(shí)必須確保其main()函數(shù)入口地址即Reset_Handler地址被正確放置在0x0800C000。Keil的鏈接器會(huì)自動(dòng)將*.o (RESET, First)放在輸出段的最前面所以只要.sct文件配置正確就萬(wàn)無(wú)一失。4.2 J-Link燒錄如何避免“error: flash download failed - target dll has been cancelled”這個(gè)錯(cuò)誤是STM32開(kāi)發(fā)者最熟悉的“噩夢(mèng)”。它出現(xiàn)的原因五花八門但AB分區(qū)場(chǎng)景下90%與以下三點(diǎn)有關(guān)Flash區(qū)域重疊或越界你在Keil中配置的.sct文件起始地址是0x0800C000但J-Link Commander或Keil的Flash Download設(shè)置里目標(biāo)地址卻填成了0x08000000。J-Link試圖往Bootloader區(qū)域?qū)懭階PP代碼自然失敗。解決方法在Keil中“Options for Target...” → “Utilities” → “Settings” → “Flash Download” → 點(diǎn)擊“Add”添加你自己的Flash算法如STM32F10x_128.FLM然后在“Edit”中確認(rèn)“Start Address”與.sct文件中的ER_IROM1起始地址完全一致。Flash算法版本過(guò)舊你用的是老版本J-Link驅(qū)動(dòng)V6.x而新版本V7.x修復(fù)了F103在高主頻下擦除失敗的Bug。解決方法去SEGGER官網(wǎng)下載最新版J-Link Software and Documentation Pack安裝后重啟Keil。同時(shí)在Keil的“Utilities”設(shè)置里點(diǎn)擊“Flash Download”旁邊的“Settings”在“Flash”選項(xiàng)卡中勾選“Verify after programming”這能幫你第一時(shí)間發(fā)現(xiàn)寫入錯(cuò)誤。硬件連接不穩(wěn)定SWDIO/SWCLK線過(guò)長(zhǎng)15cm、未加100Ω串聯(lián)電阻、或目標(biāo)板供電不足3.0V都會(huì)導(dǎo)致J-Link通信超時(shí)最終報(bào)此錯(cuò)。解決方法用萬(wàn)用表測(cè)量VDD引腳電壓確保在3.3V±5%在SWDIO和SWCLK線上各串一個(gè)100Ω電阻縮短排線長(zhǎng)度。燒錄順序至關(guān)重要首先用J-Link燒錄Bootloader.hex由Bootloader.sct生成到0x08000000。然后燒錄APP_A.hex由APP_A.sct生成到0x08004000。最后燒錄APP_B.hex由APP_B.sct生成到0x0800C000。注意APP_B.hex是空的占位文件里面只有填充的0xFF。它只是為B區(qū)預(yù)留出32KB空間防止后續(xù)OTA升級(jí)時(shí)寫入失敗。真正的B區(qū)內(nèi)容由APP在運(yùn)行時(shí)通過(guò)IAP寫入。4.3 OTA升級(jí)協(xié)議設(shè)計(jì)一個(gè)極簡(jiǎn)但可靠的自定義協(xié)議OTA不是把一個(gè)bin文件扔過(guò)去就完事。你需要一個(gè)輕量級(jí)協(xié)議來(lái)協(xié)調(diào)“發(fā)端”上位機(jī)/云平臺(tái)和“收端”STM32 APP之間的握手、分包、校驗(yàn)、確認(rèn)。我們采用一個(gè)3幀協(xié)議總開(kāi)銷僅12字節(jié)專為低速UART115200bps優(yōu)化字段長(zhǎng)度說(shuō)明SOF1字節(jié)起始符固定為0xAACMD1字節(jié)命令碼0x01請(qǐng)求升級(jí)0x02發(fā)送數(shù)據(jù)塊0x03升級(jí)完成LEN2字節(jié)數(shù)據(jù)塊長(zhǎng)度小端最大64KBDATALEN字節(jié)實(shí)際bin數(shù)據(jù)CRC2字節(jié)整個(gè)幀SOF到DATA的CRC16-CCITTEOF1字節(jié)結(jié)束符固定為0x55APP端處理邏輯// 偽代碼 while(receive_frame(frame)) { switch(frame.cmd) { case 0x01: // 請(qǐng)求升級(jí) erase_B_region(); // 擦除B區(qū) send_ack(0x01, SUCCESS); break; case 0x02: // 發(fā)送數(shù)據(jù)塊 write_to_B_region(frame.data, frame.len, current_offset); current_offset frame.len; send_ack(0x02, SUCCESS); break; case 0x03: // 升級(jí)完成 calculate_crc32_on_B_region(); write_crc32_to_B_region_end(crc32); set_reboot_flag(); NVIC_SystemReset(); break; } }這個(gè)協(xié)議沒(méi)有復(fù)雜的滑動(dòng)窗口、沒(méi)有重傳機(jī)制因?yàn)樗僭O(shè)上位機(jī)是可信的如本地PC工具。如果網(wǎng)絡(luò)環(huán)境差應(yīng)在上位機(jī)層面實(shí)現(xiàn)TCP重傳或MQTT QoS1。把復(fù)雜性留在上位機(jī)讓MCU保持簡(jiǎn)單是嵌入式開(kāi)發(fā)的黃金法則。5. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄那些讓你凌晨三點(diǎn)還在抓頭發(fā)的Bug5.1 問(wèn)題速查表癥狀、原因、解決方案癥狀可能原因解決方案上電后LED不亮J-Link能連上但無(wú)法擦除FlashBootloader的Flash區(qū)域被意外擦除或?qū)憠挠肑-Link Commander執(zhí)行unlock命令然后重新燒錄Bootloader.hexAPP能正常運(yùn)行但OTA升級(jí)后復(fù)位進(jìn)入Bootloader卻跳轉(zhuǎn)到A區(qū)而非B區(qū)B區(qū)末尾的CRC32校驗(yàn)值未寫入或?qū)懭氲刂峰e(cuò)誤如寫到了0x08013FF0而非0x08013FFC用J-Link Commander的mem32 0x08013FFC 1命令讀取最后4字節(jié)確認(rèn)是否為預(yù)期CRC值檢查APP中寫CRC的代碼確保地址計(jì)算正確B區(qū)寫入完成后APP跳轉(zhuǎn)到B區(qū)但立即進(jìn)入HardFault_HandlerB區(qū)的向量表重映射未生效或B區(qū)首地址0x0800C000處的數(shù)據(jù)不是有效的向量表即前4字節(jié)不是棧頂?shù)刂酚胢em32 0x0800C000 2命令讀取B區(qū)開(kāi)頭8字節(jié)確認(rèn)第一個(gè)字棧頂?shù)刂吩?x20000000–0x20004FFF范圍內(nèi)F103的SRAM范圍檢查APP的.sct文件確保*.o (RESET, First)確實(shí)放在了0x0800C000串口接收升級(jí)包時(shí)偶爾丟包或數(shù)據(jù)錯(cuò)亂UART中斷優(yōu)先級(jí)設(shè)置過(guò)低被其他高優(yōu)先級(jí)中斷如TIM搶占導(dǎo)致接收緩沖區(qū)溢出在NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2)后將USARTx_IRQn的優(yōu)先級(jí)設(shè)為最高如NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0;error: flash download failed - cortex-m4你正在用針對(duì)Cortex-M4的J-Link驅(qū)動(dòng)如STM32F4xx.FLM去燒錄Cortex-M3的F103在Keil的“Flash Download”設(shè)置中刪除所有M4算法只添加STM32F10x_128.FLM5.2 獨(dú)家避坑技巧來(lái)自產(chǎn)線的真實(shí)經(jīng)驗(yàn)技巧1用“影子Flash”做升級(jí)預(yù)演在正式OTA前先在RAM中模擬一次完整的B區(qū)寫入和CRC校驗(yàn)。方法分配一塊32KB的RAM如uint8_t b_region_shadow[32*1024];把接收到的bin數(shù)據(jù)先memcpy進(jìn)去然后對(duì)這塊RAM計(jì)算CRC32。如果RAM校驗(yàn)通過(guò)再執(zhí)行真實(shí)的Flash寫入。這能提前捕獲bin文件損壞、傳輸錯(cuò)誤等問(wèn)題避免把錯(cuò)誤固件寫進(jìn)Flash。技巧2給Bootloader加一個(gè)“強(qiáng)制回滾”按鍵在Bootloader中增加一個(gè)GPIO檢測(cè)邏輯如果上電時(shí)某個(gè)按鍵如BOOT0被按下則無(wú)視B區(qū)狀態(tài)強(qiáng)制從A區(qū)啟動(dòng)。這在B區(qū)固件有嚴(yán)重Bug導(dǎo)致設(shè)備無(wú)法聯(lián)網(wǎng)、無(wú)法觸發(fā)OTA回滾時(shí)是唯一的救命稻草。代碼只需幾行RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉輸入 GPIO_Init(GPIOA, GPIO_InitStructure); if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_RESET) { jump_to_app(0x08004000); // 強(qiáng)制跳A區(qū) }**技巧3監(jiān)控Flash