與跳轉避坑指南)
簡介面向STM32F103C8T6開發(fā)者的串口IAP升級固件源碼包完整解決單片機通過串口實現在線固件升級與跳轉執(zhí)行的需求適合需要掌握Bootloader原理或正在調試在線升級方案的嵌入式工程師也適合為儀器儀表、物聯網節(jié)點等設備增加遠程維護能力的項目參考。資源共430個文件、2.23MB以86個C源文件與86個頭文件構成主要邏輯另有匯編啟動文件、鏈接腳本以及uvproj、ewp等工程配置覆蓋從源碼修改、編譯鏈接到生成hex/bin固件的完整開發(fā)鏈條匯編入口與鏈接腳本便于分析啟動過程和內存布局說明文檔則可幫助快速理解代碼結構。包內基礎工程直接搭建了IAP升級環(huán)境按典型流程展示Flash分區(qū)規(guī)劃、串口通信協(xié)議與固件跳轉處理且保留較多可配置參數方便根據實際硬件或協(xié)議需求進行裁剪移植省去從零構建引導程序的繁瑣環(huán)節(jié)。該資源已有5148人學習下載對初學STM32在線升級或需要快速復現Bootloader方案的中高級開發(fā)者是一份實用價值較高的參考源碼包。1. 串口IAP升級STM32F103C8T6最反直覺的一次開發(fā)在 STM32F103C8T6 的項目里做串口 IAP 固件升級最反直覺的一點是決定升級過程能不能可靠完成的不是那幾行“接收串口數據、寫 Flash”的代碼而是 Bootloader 里兩個看起來不起眼的瞬間——上電后等多久再跳 App以及“升級請求”標志被放在哪個地址。這兩處沒想清楚設備在產線上燒好第一版時一切正常發(fā)到現場要通過串口升級時十次里有三次變磚。這個源碼包做的就是把 C8T6 的 64KB Flash 拆成 16KB Bootloader 和 48KB App用串口協(xié)議把新固件分包寫入 App 區(qū)校驗通過后跳轉執(zhí)行。適合剛接觸 IAP 的人照著完整流程手搓一遍也適合正在做量產設備、被現場升級穩(wěn)定性反復折磨的開發(fā)者對照排查。下面按我從拿到源碼到跑通、再到改出自己一版的順序來講。2. 整體設計與 Flash 分區(qū)Bootloader 和 App 怎么在 64KB Flash 里共存拿到 C8T6 的第一件事是把“Flash 有 64KB”這個數字忘掉。實際應用可用的只有 48KB因為前面 16KB 要劃給 Bootloader。很多人的 App 加了協(xié)議棧、日志和狀態(tài)機之后塞不進 48KB十有八九是在選型時沒做這個心理預期。IAP 不是把兩個程序隨便丟進 Flash而是先畫好地盤再寫代碼。2.1 雙區(qū)架構與內存規(guī)劃16KB Bootloader 48KB App源碼包里的分區(qū)方案很直接一張表就能說清區(qū)域起始地址大小存放內容Bootloader 區(qū)0x0800000016KBIAP 主程序、串口驅動、Flash 擦寫驅動App 區(qū)0x0800400048KB業(yè)務固件、App 中斷向量表參數頁0x08003C001KB升級請求標志、版本號記錄為什么要留 16KB 給 Bootloader而不是省到 8KB我用標準外設庫編譯過這個工程串口加 Flash 驅動加上協(xié)議解析編譯出來大約 6~9KB留 16KB 是給自己留后路。后面要加 CRC 校驗之外的加密、簽名代碼量會明顯膨脹如果把 Bootloader 壓縮到 4KB純寄存器操作確實能塞下但可讀性和可維護性都會變差。這里有個繞不開的硬件事實Cortex-M3 上電后從 Flash 起始地址取兩個字——第一個字是初始主堆棧指針 MSP第二個字是復位向量。Bootloader 的向量表必須在 0x08000000因為這是硬件固定的。而 App 的向量表本來也應在 0x08000000可那個位置被 Bootloader 占著App 只能把向量表挪到 0x08004000。麻煩在于 CPU 并不知道這個偏移必須通過 SCB-VTOR 告訴它。STM32F103 的 VTOR 在 0xE000ED08F1 全系列都有這個寄存器這點比某些 Cortex-M0 單片機要省事。2.2 啟動流程與跳轉條件升級標志位、串口窗口與跳轉判定整個啟動流程如果用文字描述是這樣的鏈路上電復位 → Bootloader 初始化時鐘和串口 → 讀取參數頁的升級請求標志 → 若標志有效清標志并直接進入升級等待模式 → 若標志無效等待約 300ms 的串口命令窗口 → 窗口內收到握手幀進入升級模式 → 窗口結束仍未收到命令跳轉到 App。為什么是“等約 300ms”而不是無限等現場升級要求上電盲操作設備一上電 Bootloader 先開一個短暫的升級窗口上位機只要能在這個窗口里發(fā)出握手幀就能搶占。窗口太短現場人員來不及窗口太長設備每次開機都要慢幾秒。300ms 是我在速度和成功率之間試出來的經驗值源碼包里也留了宏定義可以自己調。升級請求標志放在參數頁而不是放在 App 區(qū)尾部原因很實際App 運行時不能擦寫自己正在執(zhí)行的 Flash 頁。假如把標志寫在 App 區(qū)同一頁程序一邊從 Flash 取指令一邊擦寫那一頁輕則寫失敗重則 HardFault。所以 App 請求升級時是把標志寫給 Bootloader 區(qū)里獨立的一頁。核心代碼是這樣的#define PARA_FLAG_ADDR 0x08003C00 /* Bootloader區(qū)最后一頁1KB */ #define FLAG_MAGIC 0x5A5AA5A5 void app_request_upgrade(void) { uint32_t flags FLAG_MAGIC; FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); FLASH_ErasePage(PARA_FLAG_ADDR); /* 先整頁擦除 */ FLASH_ProgramWord(PARA_FLAG_ADDR, flags); /* 寫入魔數 */ FLASH_Lock(); NVIC_SystemReset(); /* 軟復位進Bootloader */ }這段代碼關鍵在兩點擦除后寫入是標準流程不能省掉 ClearFlag寫完魔數后立刻軟復位讓 Bootloader 來決定接下來是升級還是正常跳轉App 自己不做升級流程的分發(fā)?,F場人員只要觸發(fā) App 里的這個函數設備重啟后就會自動停在升級等待狀態(tài)。Bootloader 啟動時的判斷同樣簡單if (*(volatile uint32_t *)PARA_FLAG_ADDR FLAG_MAGIC) { FLASH_ErasePage(PARA_FLAG_ADDR); /* 清掉請求標志防止下次重復誤入升級 */ iap_state IAP_STATE_UPGRADE; /* 進入升級等待模式 */ }清標志必須在跳轉升級邏輯之前完成否則升級過程中途斷電再上電會又一次進入升級模式反而把正常 App 擋住。這個順序我一開始寫反過結果現場出現“設備永遠停在升級等待App 起不來”的翻車原因就是標志沒被清掉。3. 串口升級協(xié)議與 Bootloader 核心代碼拆解一幀一幀把固件搬進 FlashBootloader 的本質是一個串口透傳寫入器上位機發(fā)一幀Bootloader 解析一幀、校驗一幀、寫一次 Flash。協(xié)議設計得不好升級過程就會變成一場對時鐘和運氣的賭博。這章我把協(xié)議幀、狀態(tài)機、Flash 操作和跳轉邏輯逐一拆開講。3.1 串口協(xié)議幀設計幀頭、命令字、長度與 CRC16 校驗源碼包里的協(xié)議幀格式很簡單但足夠防錯字節(jié)序字段長度說明0幀頭2 字節(jié)固定 0xAA 0x552命令字1 字節(jié)0x01 升級開始0x02 數據塊0x03 完成校驗0x04 跳轉 App3數據長度2 字節(jié)小端序payload 字節(jié)數不含幀頭和 CRC5Payload變長數據塊命令里是 offset(4 字節(jié)) 固件數據(最多 256 字節(jié))末尾CRC162 字節(jié)CRC16-MODBUS覆蓋數據長度和 Payload命令字是升級流程的骨架。0x01 升級開始命令里上位機告訴 Bootloader 固件總長度Bootloader 收到后擦除整個 App 區(qū)并回復 ACK0x02 數據塊命令承擔真正的搬磚工作每幀最多帶 256 字節(jié)數據0x03 完成校驗命令用于回讀 Flash 算 CRC0x04 才是真正觸發(fā)跳轉的那一下。串口接收我建議用狀態(tài)機而不是直接等一個“完整幀中斷”因為 STM32F103 的串口一次中斷只停在一個字節(jié)上數據是碎片化到達的。狀態(tài)機在串口中斷里逐字節(jié)喂最穩(wěn)妥typedef enum { ST_WAIT_HEAD1 0, ST_WAIT_HEAD2, ST_WAIT_CMD, ST_WAIT_LEN, ST_WAIT_PAYLOAD, ST_WAIT_CRC } RX_STATE; static RX_STATE rx_state ST_WAIT_HEAD1; static uint16_t payload_len 0; static uint16_t payload_idx 0; static uint16_t crc_idx 0; void IAP_RxHandler(uint8_t byte) { switch (rx_state) { case ST_WAIT_HEAD1: if (byte 0xAA) rx_state ST_WAIT_HEAD2; break; case ST_WAIT_HEAD2: rx_state (byte 0x55) ? ST_WAIT_CMD : ST_WAIT_HEAD1; break; case ST_WAIT_CMD: rx_frame.cmd byte; payload_len 0; payload_idx 0; crc_idx 0; rx_state ST_WAIT_LEN; break; case ST_WAIT_LEN: if (payload_idx 0) { payload_len byte; /* 長度低字節(jié)先到 */ payload_idx 1; } else { payload_len | (uint16_t)byte 8; /* 高字節(jié)拼出完整長度 */ payload_idx 0; rx_state ST_WAIT_PAYLOAD; } break; case ST_WAIT_PAYLOAD: rx_frame.payload[payload_idx] byte; if (payload_idx payload_len) { crc_idx 0; rx_state ST_WAIT_CRC; } break; case ST_WAIT_CRC: if (crc_idx 0) { rx_frame.crc byte; /* CRC 低字節(jié) */ crc_idx 1; } else { rx_frame.crc | (uint16_t)byte 8; IAP_OnFrameComplete(rx_frame); /* 一幀收齊 */ rx_state ST_WAIT_HEAD1; /* 回到幀頭狀態(tài) */ } break; default: rx_state ST_WAIT_HEAD1; break; } }狀態(tài)機的幾個參數值得說明payload_len 是兩字節(jié)小端所以要先收低字節(jié)再收高字節(jié)payload_idx 在這里實際是長度收齊后的數據計數實現時也可以單獨用一個計數變量邏輯更清楚任何一幀中間字節(jié)出錯狀態(tài)機都會回到 ST_WAIT_HEAD1下一幀能自動恢復不用重啟設備。幀收齊后進入分發(fā)性處理先把 CRC 校驗做掉再按命令字分發(fā)void IAP_OnFrameComplete(IAP_Frame *frame) { uint16_t calc_crc CRC16_Modbus((uint8_t *)frame-cmd, 3 frame-len); if (calc_crc ! frame-crc) { IAP_SendAck(AK_CRC_ERR); return; } switch (frame-cmd) { case CMD_UPGRADE_START: IAP_OnStart(frame); break; case CMD_UPGRADE_DATA: IAP_OnData(frame); break; case CMD_UPGRADE_FINISH: IAP_OnFinish(frame); break; case CMD_UPGRADE_JUMP: IAP_JumpToApp(); break; default: IAP_SendAck(AK_CMD_ERR); break; } }CRC 的覆蓋范圍是命令字 長度 Payload幀頭不參與計算。這是有意為之因為幀頭本身有 AA 55 做同步而 CRC 要保護的是真正影響數據的字段。源碼里用的是 CRC16-MODBUS初值 0xFFFF 多項式 0xA001實現只有幾行uint16_t CRC16_Modbus(uint8_t *buf, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ buf[i]; for (uint8_t j 0; j 8; j) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }上位機計算 CRC 時必須用同樣的多項式否則聯調時會出現“每一幀都回 CRC_ERR”的現象。這種問題排查起來特別像玄學其實是兩端 CRC 不一致。3.2 Flash 擦寫與存儲邏輯先整片擦除、再半字寫入、最后回讀校驗STM32F103 的 Flash 有一個特性寫入以半字16 位為單位擦除以頁為單位每頁 1KB。寫入前目標頁必須處于已擦除狀態(tài)否則寫入結果不確定。這直接決定了升級策略收到第一幀命令時不能邊收邊寫而是先把整個 App 區(qū)擦干凈。#define APP_BASE_ADDR 0x08004000 #define FLASH_PAGE_SIZE 1024 #define APP_TOTAL_SIZE 0xC000 /* 48KB */ static uint32_t write_offset 0; static uint8_t flash_started 0; void IAP_OnStart(IAP_Frame *frame) { uint32_t total_len *(uint32_t *)frame-payload[0]; if (total_len APP_TOTAL_SIZE) { IAP_SendAck(AK_LEN_ERR); return; } FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); /* 整片擦除App區(qū)防止舊數據和殘留頁干擾 */ for (uint32_t addr APP_BASE_ADDR; addr APP_BASE_ADDR APP_TOTAL_SIZE; addr FLASH_PAGE_SIZE) { FLASH_ErasePage(addr); } write_offset 0; flash_started 1; IAP_SendAck(AK_OK); }整片擦除 48KB 共 48 頁按 HSI 8MHz 或 PLL 倍頻后的 CPU 時鐘算大約要幾百毫秒。所以上位機發(fā)完升級開始命令后不能立刻發(fā)數據必須等 Bootloader 回 ACK。這個等待關系在協(xié)議設計里要寫清楚否則上位機發(fā)得快數據幀會因為 Flash 還沒擦完而被忽略。數據塊寫入是每幀動作核心是把 payload 里的偏移量和數據取出來逐個半字寫入void IAP_OnData(IAP_Frame *frame) { uint32_t offset; uint8_t *data; uint16_t data_len; uint32_t target_addr; if (!flash_started) { IAP_SendAck(AK_NOT_STARTED); /* 沒收到升級開始命令直接拒絕 */ return; } offset *(uint32_t *)frame-payload[0]; /* 前4字節(jié)是目標偏移 */ data frame-payload[4]; data_len frame-len - 4; target_addr APP_BASE_ADDR offset; /* 越界檢查防住上位機發(fā)瘋 */ if ((target_addr data_len) (APP_BASE_ADDR APP_TOTAL_SIZE)) { IAP_SendAck(AK_ADDR_ERR); return; } /* 半字寫入data_len 確保是偶數 */ for (uint16_t i 0; i data_len; i 2) { uint16_t half_word data[i] | (data[i 1] 8); FLASH_ProgramHalfWord(target_addr i, half_word); } write_offset offset data_len; IAP_SendAck(AK_OK); }這里有兩個必須守住的約定offset 必須按 2 字節(jié)對齊data_len 必須是偶數。上位機在發(fā)送數據幀之前要把 bin 文件按 2 字節(jié)補齊最后不足 2 字節(jié)補 0xFF。如果上位機發(fā)來奇數長度的數據Bootloader 應該直接回錯誤而不是硬寫否則最后一次寫入會踩到未知地址。寫完后回 ACK 表示這一幀落盤上位機再發(fā)下一幀。升級完成的校驗比“寫入成功”更可靠的是“整片回讀再算 CRC”。FLASH_ProgramHalfWord 返回狀態(tài)位在某些操作異常時不代表數據真寫對了回讀是最后防線void IAP_OnFinish(IAP_Frame *frame) { uint32_t expect_crc *(uint32_t *)frame-payload[0]; uint32_t real_crc CRC32_Flash(APP_BASE_ADDR, app_total_len); if (real_crc expect_crc) { IAP_SendAck(AK_CRC_OK); } else { IAP_SendAck(AK_CRC_ERR); /* 不跳轉保留現場等上位機重傳 */ } }CRC32_Flash 是源碼包里提供的函數遍歷 App 區(qū)已寫入的字節(jié)計算 CRC32。比較失敗時不擦除、不跳轉等上位機重新發(fā)起升級。這比直接跳進一個損壞的 App 要安全得多。3.3 跳轉到 App 的代碼實現棧頂檢查、VTOR 切換與中斷屏蔽最后一步跳轉是 IAP 里最容易寫翻車的地方。跳轉的本質不是“跳”而是“把 CPU 的執(zhí)行環(huán)境完整換掉”換中斷向量表、換棧指針、換 PC 指針。typedef void (*IAP_JumpFunc)(void); void IAP_JumpToApp(void) { uint32_t app_sp; uint32_t app_pc; IAP_JumpFunc jump_func; app_sp *(volatile uint32_t *)APP_BASE_ADDR; app_pc *(volatile uint32_t *)(APP_BASE_ADDR 4); /* 棧頂值必須在RAM地址范圍內否則App區(qū)是空白或損壞 */ if ((app_sp 0x2FFE0000) ! 0x20000000) { return; } if ((app_pc 0xFFFFF000) 0) { return; } jump_func (IAP_JumpFunc)app_pc; /* 跳轉前徹底關中斷防止VTOR切換瞬間中斷亂入 */ __set_PRIMASK(1); SCB-VTOR APP_BASE_ADDR; /* 讓App找到自己的向量表 */ __set_MSP(app_sp); /* 換成App的初始棧指針 */ jump_func(); /* 跳到App復位向量 */ /* 如果走到這里說明App沒起來 */ __set_PRIMASK(0); }這個函數里有幾處細節(jié)決定生死。app_sp 是 App 向量表第一個字也就是 App 初始主堆棧指針跳轉前不換棧指針Bootloader 的棧調用鏈會繼續(xù)壓棧App 的 main 函數跑起來后??臻g被污染。__set_PRIMASK(1)在切換 VTOR 前關閉全部可屏蔽中斷避免串口中斷在向量表切換的瞬間來一下CPU 拿著新向量表去找舊中斷入口直接進 HardFault。跳轉前的兩次檢查能攔掉“App 區(qū)是空白 Flash”的情況空白 Flash 讀出來是 0xFFFFFFFF通不過 RAM 地址范圍判斷。4. 編譯配置與燒錄避坑讓 Bootloader 和 App 各就各位代碼寫完只是第一步真正讓這個工程跑起來的是兩個 Keil 工程的配置。這章講清楚怎么建兩個工程、怎么生成 bin、怎么用串口助手手搓一次升級最后給出我踩過的三個坑。4.1 兩套 Keil 工程的 IROM 配置與 bin 文件生成源碼包分了兩個 Keil 工程一個 Bootloader一個 App。各自的關鍵配置用一張表說清配置項Bootloader 工程App 工程IROM1 起始地址0x080000000x08004000IROM1 大小0x40000xC000是否設置 VTOR不需要設置偏移 0x4000燒錄方式ST-Link 寫入通過 Bootloader 串口升級App 工程里設置向量表偏移有兩種方式一種是在 main 函數里手動執(zhí)行SCB-VTOR 0x08004000;另一種是在 system_stm32f10x.c 里宏定義VECT_TAB_OFFSET 0x4000。我推薦用后一種因為 SystemInit 在 startup 文件里比 main 更早執(zhí)行向量表在 C 環(huán)境初始化之前就切好了更穩(wěn)。如果是手動方式注意別放在 main 的后面否則啟動早期來的中斷照樣會跑飛。生成 bin 文件是升級的前提Keil 的 After Build 命令行里加一句fromelf --bin -o $LL.bin #L$LL會自動展開成不帶擴展名的工程輸出路徑#L是鏈接后的映像文件。生成后把這顆 bin 交給上位機程序就能用于串口升級。注意工程路徑不要帶空格否則 fromelf 會把路徑拆成多個參數這是我實際遇到過的低級問題。4.2 用 Python 腳本模擬上位機手搓一遍完整升級流程在沒有現成上位機程序時用 Python 串口腳本就能聯調起來。核心是構造協(xié)議幀import serial import struct def crc16_modbus(data: bytes) - int: crc 0xFFFF for b in data: crc ^ b for _ in range(8): if crc 0x0001: crc (crc 1) ^ 0xA001 else: crc 1 return crc def make_frame(cmd: int, payload: bytes) - bytes: body bytes([cmd]) struct.pack(H, len(payload)) payload crc crc16_modbus(body) return b\xaa\x55 body struct.pack(H, crc)聯調時 BOOT0 保持低電平先用 ST-Link 燒 Bootloader然后只接 USB 轉串口 TTL打開串口后按這個順序跑發(fā)送升級開始幀payload 里是固件總長度等待 Bootloader 回 ACK分段讀取 bin 文件每段 256 字節(jié)前三字節(jié)是因為對齊要處理發(fā)數據幀并逐幀確認 ACK發(fā)完成校驗幀收到 CRC_OK 后發(fā)跳轉幀。第一次聯調建議用 9600 波特率雖然慢但容錯高。波特率越高現場線材和干擾對幀錯誤率的影響越大。源碼包里默認配置是 115200如果發(fā)現 CRC 錯誤率高先把波特率降下來再排查。4.3 避坑記錄變磚、Flash 寫入 HardFault、App 跑飛的三類排查經驗這一節(jié)記錄的是我實際踩過的三個坑每條都按現象、原因、解決展開。第一個坑是“升級中途斷電再上電設備變磚”。現象是升級到一半現場工人誤拔電源再上電后設備沒有任何反應App 起不來Bootloader 也沒響應。原因是寫 Flash 只擦除了一部分頁App 區(qū)里既有新數據又有殘留的舊數據程序還嘗試直接跳轉。解決方法是跳轉前做嚴格的棧頂檢查和 App 有效魔數檢查魔數在 App 工程編譯時固定在向量表后第 4 個字節(jié)位置Bootloader 跳轉前讀這個位置不是約定的值就絕不跳轉并且等待上位機重新升級。第二個坑是“FLASH_ProgramHalfWord 寫入時進 HardFault”。現象是寫入執(zhí)行到一半卡死調試器定位到 HardFault_Handler。原因有兩個目標頁沒擦除干凈或者寫地址不是半字對齊。我第一次寫 IAP 時邊收邊寫收到哪頁寫哪頁沒先整片擦除結果在舊數據頁上寫入直接觸發(fā)錯誤。解決方法是升級開始命令里整片擦除并在 IAP_OnData 里校驗 offset 是否為 2 的倍數、data_len 是否為偶數兩端約定數據補齊策略。第三個坑是“跳轉后 App 跑飛”?,F象是升級流程全部返回成功設備串口就是不工作調試器顯示停在了 HardFault_Handler。原因是 App 向量表偏移沒設。跳轉成功后App 的 main 還沒跑起來一個 SysTick 或串口中斷到達CPU 從 0x08000000 讀中斷向量讀到的是 Bootloader 的中斷處理函數地址入口對不上直接 HardFault。解決方式就是在 App 工程里設置 VECT_TAB_OFFSET 為 0x4000并在跳轉函數里先__set_PRIMASK(1)再切 VTOR。這類問題表現很像玄學但根因基本都是向量表偏移。5. 二次開發(fā)思路給串口 IAP 加上固件加密、版本回滾和生產驗證跑通基礎升級之后要考慮的是怎么讓這套東西更接近產品級。Flash 只有 64KB很多雕花功能做不了但有幾件務實的事值得做。5.1 固件加密與簽名從裸數據幀到加密幀裸 bin 文件被上位機直接抓走就能逆向出固件所以量產產品通常會做一層加密。我在二次開發(fā)時采用的思路是上位機把 bin 用 AES-128-CBC 加密密鑰硬編碼在 Bootloader 里升級時先傳加密數據Bootloader 收到一幀后先解密、再寫入 Flash。解密用軟件實現STM32F103 的主頻跑 AES 會慢一點但好在數據量不大48KB 的固件多花幾百毫秒可以接受。要注意的是密鑰放 Flash 里只能防外行不能防專業(yè)抄板。真要防篡改得上簽名校驗上位機對固件做 SHA256 摘要再用私鑰簽個名Bootloader 里放公鑰驗簽。驗簽通過才擦寫。這個項目里我把簽名設計成可選的Bootloader 編譯宏打開才啟用節(jié)省了不必要的時間開銷。5.2 版本回滾、斷點續(xù)傳與 BKP 記錄Flash 小的務實方案C8T6 的 64KB 放不下 A/B 雙系統(tǒng)的大回滾方案但可以實現小成本回滾升級前把當前運行中的 App 關鍵版本號記錄在備份寄存器里。STM32F103 的 BKP 寄存器在復位后不會丟失App 啟動時讀 BKP 里的版本號和當前固件版本比對如果發(fā)現版本回退或者寫失敗就主動請求重寫。斷點續(xù)傳比回滾更實用。因為串口升級最怕斷斷了要從頭傳。Bootloader 里記錄已經寫入的偏移量到 BKP再次上電時上位機可以直接從斷點續(xù)傳省掉整包重傳的時間。這個功能實現不復雜但要改協(xié)議在升級開始命令里帶上“上次寫入偏移”的查詢幀。5.3 出廠驗證的習慣比功能本身更值錢做完這些擴展有個動作是每次都必須做的把整個驗證腳本固化成出廠測試用例。上電不跳 App 的空白 Flash 測試、升級到一半斷電再上電的斷電測試、下載完成后斷電重啟的驗收測試這三條我每做一個 IAP 工程都會強制走一遍。源碼包能幫你跑通流程但這三次測試能幫你確定它在現場不會翻車。從那以后我每次做升級功能都堅持把這三步寫進測試清單一次都沒漏過希望幫到你。本文還有配套的精品資源點擊獲取