實(shí)戰(zhàn):Ymode協(xié)議從零搭建Bootloader)
簡(jiǎn)介本資源面向STM32嵌入式開(kāi)發(fā)者與需要實(shí)現(xiàn)固件遠(yuǎn)程升級(jí)的工程師提供一套基于Ymode協(xié)議的IAP在線升級(jí)Bootloader完整工程可直接用于STM32G431平臺(tái)的產(chǎn)品固件迭代與維護(hù)場(chǎng)景。壓縮包共200個(gè)文件約10.62MB以C源碼與頭文件為核心包含26個(gè).c與69個(gè).h文件覆蓋HAL庫(kù)外設(shè)驅(qū)動(dòng)、Flash讀寫、串口通信等模塊同時(shí)附帶Keil工程文件、編譯中間產(chǎn)物、hex燒錄文件及map映射表便于直接編譯驗(yàn)證與二次開(kāi)發(fā)。目前已有205人學(xué)習(xí)下載。讀者可從中獲取Ymode協(xié)議解析、IAP跳轉(zhuǎn)邏輯、Flash分區(qū)規(guī)劃與串口收發(fā)等關(guān)鍵實(shí)現(xiàn)思路并借助完整工程結(jié)構(gòu)快速搭建自己的Bootloader框架減少?gòu)牧阏{(diào)試的時(shí)間成本適合具備一定STM32基礎(chǔ)、希望深入理解固件升級(jí)機(jī)制的開(kāi)發(fā)者參考。1. STM32G431 上用 Ymode 協(xié)議做 IAP為什么值得從零搭一遍手里有一塊 STM32G431 的開(kāi)發(fā)板想讓它能通過(guò)串口自己給自己升級(jí)固件不用每次都把調(diào)試器插上去燒錄——這個(gè)需求在量產(chǎn)設(shè)備、現(xiàn)場(chǎng)部署、遠(yuǎn)程維護(hù)場(chǎng)景里非常常見(jiàn)。IAPIn-Application Programming的核心思路是芯片里同時(shí)存在兩段程序一段是 bootloader負(fù)責(zé)接收新固件并寫入 Flash另一段是用戶應(yīng)用App負(fù)責(zé)實(shí)際業(yè)務(wù)邏輯。bootloader 永遠(yuǎn)不參與升級(jí)App 可以被反復(fù)替換。那 Ymode 協(xié)議在這里扮演什么角色簡(jiǎn)單說(shuō)它是一種輕量級(jí)的串口傳輸協(xié)議用來(lái)在 PC 和單片機(jī)之間可靠地傳輸固件數(shù)據(jù)。相比自己裸寫一個(gè)「發(fā)一包、回一個(gè) ACK」的私有協(xié)議Ymode 提供了更完整的幀結(jié)構(gòu)、校驗(yàn)機(jī)制和握手流程能顯著降低傳輸過(guò)程中丟包、錯(cuò)包導(dǎo)致的升級(jí)失敗概率。STM32G431 自帶 128KB Flash、32KB RAMCortex-M4 內(nèi)核跑 170MHz資源做 IAP 綽綽有余關(guān)鍵是把分區(qū)規(guī)劃、跳轉(zhuǎn)邏輯和協(xié)議解析寫對(duì)。這篇文章面向的是有基本 STM32 開(kāi)發(fā)經(jīng)驗(yàn)、會(huì)用 Keil 或 CubeIDE、能看懂 HAL 庫(kù)代碼的嵌入式工程師。如果你之前沒(méi)做過(guò) IAP跟著走能跑通如果你做過(guò)但用的是自定義協(xié)議這里能看到 Ymode 的幀設(shè)計(jì)和分區(qū)方案有什么不同。下面從 Flash 分區(qū)開(kāi)始一步步把 bootloader 搭起來(lái)。2. Flash 分區(qū)規(guī)劃與 Ymode 幀結(jié)構(gòu)動(dòng)手前必須定下來(lái)的事2.1 STM32G431 的 Flash 頁(yè)分布與分區(qū)方案STM32G431 的 Flash 按頁(yè)擦除每頁(yè) 2KB總共 64 頁(yè)128KB。做 IAP 的第一步不是寫代碼而是畫一張分區(qū)表。分錯(cuò)了后面全要返工這是血淚經(jīng)驗(yàn)。我一般會(huì)這樣分區(qū)域起始地址大小用途Bootloader0x0800000020KB10頁(yè)存放 bootloader 代碼參數(shù)區(qū)0x080050002KB1頁(yè)存放升級(jí)標(biāo)志、固件長(zhǎng)度、CRCApp 區(qū)0x08005800100KB50頁(yè)存放用戶應(yīng)用固件預(yù)留區(qū)0x0801E8006KB3頁(yè)后續(xù)擴(kuò)展或備份Bootloader 放最前面是因?yàn)?Cortex-M4 復(fù)位后從 0x08000000 取向量表這是硬件決定的。參數(shù)區(qū)單獨(dú)占一頁(yè)用來(lái)存「是否需要升級(jí)」「新固件多大」「CRC 校驗(yàn)值」這幾個(gè)關(guān)鍵信息掉電不丟。App 區(qū)從 0x08005800 開(kāi)始注意這個(gè)地址必須是 2KB 對(duì)齊的否則擦除時(shí)會(huì)誤傷前一頁(yè)。注意G431 的 Flash 擦除粒度是頁(yè)寫入粒度是雙字64位。寫之前必須整頁(yè)擦除不能像 EEPROM 那樣按字節(jié)改。2.2 Ymode 協(xié)議幀格式與校驗(yàn)邏輯Ymode 的幀結(jié)構(gòu)不復(fù)雜但每個(gè)字段都有存在的理由。典型幀格式如下| 幀頭(1B) | 命令(1B) | 長(zhǎng)度(2B) | 數(shù)據(jù)(NB) | CRC16(2B) |幀頭固定為 0x59Ymode 的標(biāo)識(shí)命令字段區(qū)分握手、數(shù)據(jù)、結(jié)束等操作長(zhǎng)度字段告訴接收方后面跟多少字節(jié)數(shù)據(jù)CRC16 覆蓋命令長(zhǎng)度數(shù)據(jù)部分。接收方收到幀后先驗(yàn)幀頭再驗(yàn) CRC兩步都過(guò)了才處理。為什么用 CRC16 而不是簡(jiǎn)單累加和因?yàn)楣碳鬏敂?shù)據(jù)量大累加和漏檢率太高CRC16 能檢出絕大多數(shù)隨機(jī)錯(cuò)誤和突發(fā)錯(cuò)誤。Ymode 的 CRC16 一般用 CCITT 多項(xiàng)式0x1021初始值 0xFFFF這個(gè)組合在嵌入式里很常見(jiàn)代碼也好找。命令字段我一般定義這幾個(gè)0x01握手請(qǐng)求PC 問(wèn)單片機(jī)「你準(zhǔn)備好了嗎」0x02握手應(yīng)答單片機(jī)回「我準(zhǔn)備好了」0x03數(shù)據(jù)幀攜帶固件內(nèi)容0x04結(jié)束幀告訴單片機(jī)「發(fā)完了」0x05應(yīng)答 ACK接收方確認(rèn)上一幀正確0x06錯(cuò)誤 NAK要求重發(fā)握手階段很重要不能省。它讓雙方確認(rèn)波特率、數(shù)據(jù)格式一致避免 PC 已經(jīng)開(kāi)始發(fā)了、單片機(jī)還在初始化的情況。2.3 用代碼定義幀結(jié)構(gòu)和 CRC 計(jì)算先看幀結(jié)構(gòu)體的定義和 CRC16 的實(shí)現(xiàn)#include stdint.h #include string.h #define YMODE_HEADER 0x59 #define YMODE_CMD_HANDSHAKE 0x01 #define YMODE_CMD_HANDSHAKE_ACK 0x02 #define YMODE_CMD_DATA 0x03 #define YMODE_CMD_END 0x04 #define YMODE_CMD_ACK 0x05 #define YMODE_CMD_NAK 0x06 #define YMODE_MAX_DATA_LEN 256 typedef struct { uint8_t header; // 幀頭固定 0x59 uint8_t cmd; // 命令字 uint16_t data_len; // 數(shù)據(jù)長(zhǎng)度小端序 uint8_t data[YMODE_MAX_DATA_LEN]; // 載荷 uint16_t crc; // CRC16 校驗(yàn)值 } ymodem_frame_t; // CRC16-CCITT 查表法實(shí)現(xiàn)速度快適合 170MHz 的 G431 static const uint16_t crc16_table[256] { 0x0000, 0x1021, 0x2042, 0x3063, /* ... 完整表省略實(shí)際使用需補(bǔ)全 ... */ }; uint16_t ymodem_crc16(const uint8_t *buf, uint32_t len) { uint16_t crc 0xFFFF; // 初始值 for (uint32_t i 0; i len; i) { crc (crc 8) ^ crc16_table[((crc 8) ^ buf[i]) 0xFF]; } return crc; }這段代碼里ymodem_frame_t結(jié)構(gòu)體定義了幀的內(nèi)存布局注意data_len和crc都是小端序因?yàn)?STM32 是小端芯片PC 端打包時(shí)也要按小端來(lái)。CRC 計(jì)算覆蓋范圍是cmd到data末尾不包括幀頭本身——幀頭只用來(lái)做同步不參與校驗(yàn)。參數(shù)方面YMODE_MAX_DATA_LEN設(shè) 256 是權(quán)衡結(jié)果太小則幀數(shù)多、握手開(kāi)銷大太大則接收緩沖區(qū)占 RAM 多而且串口中斷處理時(shí)間變長(zhǎng)。256 字節(jié)在 115200 波特率下大約 22ms 傳完中斷不會(huì)阻塞太久。2.4 串口接收狀態(tài)機(jī)別在中斷里做重活串口接收不能收到一個(gè)字節(jié)就處理一個(gè)字節(jié)那樣容易丟數(shù)據(jù)。常見(jiàn)做法是中斷里只往環(huán)形緩沖區(qū)塞主循環(huán)里再解析幀。下面是一個(gè)簡(jiǎn)化的狀態(tài)機(jī)typedef enum { STATE_IDLE, // 等待幀頭 STATE_CMD, // 收到幀頭等命令字 STATE_LEN, // 等長(zhǎng)度字段2字節(jié) STATE_DATA, // 收數(shù)據(jù) STATE_CRC, // 收 CRC2字節(jié) } rx_state_t; static rx_state_t rx_state STATE_IDLE; static ymodem_frame_t rx_frame; static uint16_t rx_data_idx 0; static uint8_t rx_len_bytes[2]; static uint8_t rx_len_idx 0; void ymodem_rx_byte(uint8_t byte) { switch (rx_state) { case STATE_IDLE: if (byte YMODE_HEADER) { rx_frame.header byte; rx_state STATE_CMD; } break; case STATE_CMD: rx_frame.cmd byte; rx_state STATE_LEN; rx_len_idx 0; break; case STATE_LEN: rx_len_bytes[rx_len_idx] byte; if (rx_len_idx 2) { rx_frame.data_len rx_len_bytes[0] | (rx_len_bytes[1] 8); if (rx_frame.data_len YMODE_MAX_DATA_LEN) { rx_state STATE_IDLE; // 長(zhǎng)度非法丟棄 } else { rx_data_idx 0; rx_state (rx_frame.data_len 0) ? STATE_DATA : STATE_CRC; } } break; case STATE_DATA: rx_frame.data[rx_data_idx] byte; if (rx_data_idx rx_frame.data_len) { rx_state STATE_CRC; rx_len_idx 0; } break; case STATE_CRC: rx_len_bytes[rx_len_idx] byte; if (rx_len_idx 2) { rx_frame.crc rx_len_bytes[0] | (rx_len_bytes[1] 8); // 校驗(yàn)對(duì) cmd len data 算 CRC uint8_t tmp[YMODE_MAX_DATA_LEN 3]; tmp[0] rx_frame.cmd; tmp[1] rx_frame.data_len 0xFF; tmp[2] (rx_frame.data_len 8) 0xFF; memcpy(tmp[3], rx_frame.data, rx_frame.data_len); uint16_t calc ymodem_crc16(tmp, rx_frame.data_len 3); if (calc rx_frame.crc) { ymodem_frame_ready(rx_frame); // 交給主循環(huán)處理 } rx_state STATE_IDLE; } break; } }狀態(tài)機(jī)的邏輯是逐字節(jié)推進(jìn)只有完整收到一幀且 CRC 通過(guò)才置標(biāo)志位。ymodem_frame_ready只是把幀拷到另一個(gè)緩沖區(qū)并置一個(gè) volatile 標(biāo)志主循環(huán)看到標(biāo)志再處理這樣中斷里不會(huì)做 Flash 擦寫這種耗時(shí)操作。注意rx_frame和rx_state如果主循環(huán)也要讀需要加 volatile 或關(guān)中斷保護(hù)否則編譯器優(yōu)化可能出玄學(xué)問(wèn)題。3. Bootloader 跳轉(zhuǎn)與 App 接收從握手到寫入 Flash 的完整鏈路3.1 跳轉(zhuǎn)前的準(zhǔn)備工作關(guān)中斷、設(shè)棧頂、改向量表Bootloader 收到完整固件并寫入 App 區(qū)之后下一步是跳過(guò)去執(zhí)行。跳轉(zhuǎn)不是一句goto就完事要做三件事第一關(guān)閉所有中斷和外設(shè)。SysTick、USART、DMA 都要 DeInit不然跳過(guò)去之后中斷向量還指向 bootloader 的處理函數(shù)一觸發(fā)就 HardFault。第二設(shè)置主棧指針MSP。App 的向量表第一個(gè)字就是棧頂?shù)刂诽D(zhuǎn)前要把它讀出來(lái)賦給 MSP。第三設(shè)置向量表偏移。App 的向量表在 0x08005800而 SCB-VTOR 默認(rèn)指向 0x08000000必須改過(guò)來(lái)。typedef void (*app_entry_t)(void); void jump_to_app(uint32_t app_addr) { uint32_t stack_top *(volatile uint32_t *)app_addr; uint32_t reset_handler *(volatile uint32_t *)(app_addr 4); // 檢查棧頂是否合法在 SRAM 范圍內(nèi) if (stack_top 0x20000000 || stack_top 0x20008000) { return; // 非法棧頂不跳 } // 關(guān)中斷 __disable_irq(); // 關(guān) SysTick SysTick-CTRL 0; SysTick-LOAD 0; SysTick-VAL 0; // 反初始化 HAL 和串口 HAL_DeInit(); // 設(shè)置向量表偏移 SCB-VTOR app_addr; // 設(shè)置 MSP __set_MSP(stack_top); // 跳轉(zhuǎn) app_entry_t app (app_entry_t)reset_handler; __enable_irq(); app(); }app_addr就是 0x08005800。棧頂檢查那一步別省如果 App 區(qū)是空的全 0xFF棧頂讀出來(lái)就是 0xFFFFFFFF直接設(shè) MSP 會(huì)立刻 HardFault連調(diào)試信息都看不到。加上范圍檢查至少能安全返回。3.2 接收固件并寫入 Flash按頁(yè)擦、按雙字寫Flash 寫入流程是收到數(shù)據(jù)幀 → 攢夠一頁(yè) → 擦除該頁(yè) → 寫入 → 回 ACK。G431 的 HAL 庫(kù)提供了HAL_FLASH_Unlock、HAL_FLASHEx_Erase、HAL_FLASH_Program三個(gè)函數(shù)用起來(lái)不復(fù)雜但有幾個(gè)參數(shù)必須設(shè)對(duì)。#define APP_START_ADDR 0x08005800 #define FLASH_PAGE_SIZE 2048 static uint32_t write_addr APP_START_ADDR; static uint8_t page_buf[FLASH_PAGE_SIZE]; static uint16_t page_offset 0; int flash_write_page(uint32_t addr, uint8_t *data, uint32_t len) { HAL_FLASH_Unlock(); // 擦除目標(biāo)頁(yè) FLASH_EraseInitTypeDef erase {0}; erase.TypeErase FLASH_TYPEERASE_PAGES; erase.Page (addr - 0x08000000) / FLASH_PAGE_SIZE; erase.NbPages 1; uint32_t page_error 0; if (HAL_FLASHEx_Erase(erase, page_error) ! HAL_OK) { HAL_FLASH_Lock(); return -1; } // 按雙字64位寫入 for (uint32_t i 0; i len; i 8) { uint64_t dword 0; uint32_t remain len - i; uint32_t n (remain 8) ? 8 : remain; memcpy(dword, data[i], n); if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr i, dword) ! HAL_OK) { HAL_FLASH_Lock(); return -2; } } HAL_FLASH_Lock(); return 0; }erase.Page的計(jì)算方式是(addr - 0x08000000) / 2048因?yàn)轫?yè)號(hào)是從 Flash 基地址開(kāi)始算的。HAL_FLASH_Program的第二個(gè)參數(shù)是目標(biāo)地址第三個(gè)參數(shù)是 64 位數(shù)據(jù)注意最后一個(gè)雙字如果不滿 8 字節(jié)要補(bǔ) 0xFF 而不是補(bǔ) 0因?yàn)?Flash 擦除后的狀態(tài)就是 0xFF。數(shù)據(jù)攢頁(yè)的邏輯每收到一幀數(shù)據(jù)就往page_buf里拷page_offset累加到 2048 就調(diào)flash_write_page寫一頁(yè)然后page_offset歸零。最后一頁(yè)不滿也要寫不足部分補(bǔ) 0xFF。3.3 升級(jí)標(biāo)志與 CRC 校驗(yàn)讓 bootloader 知道該不該跳B(niǎo)ootloader 上電后第一件事是讀參數(shù)區(qū)看有沒(méi)有升級(jí)標(biāo)志。如果有就留在 bootloader 等 PC 發(fā)固件如果沒(méi)有直接跳 App。參數(shù)區(qū)的數(shù)據(jù)結(jié)構(gòu)我一般這樣定義#define PARAM_ADDR 0x08005000 typedef struct { uint32_t magic; // 魔數(shù)0x5A5A5A5A 表示參數(shù)有效 uint32_t upgrade_flag; // 1 表示需要升級(jí) uint32_t fw_size; // 固件字節(jié)數(shù) uint32_t fw_crc; // 固件 CRC32 } param_t;上電流程void bootloader_main(void) { param_t *param (param_t *)PARAM_ADDR; if (param-magic ! 0x5A5A5A5A || param-upgrade_flag ! 1) { // 無(wú)升級(jí)請(qǐng)求直接跳 App jump_to_app(APP_START_ADDR); } // 有升級(jí)請(qǐng)求初始化串口等 PC 發(fā)固件 uart_init(115200); ymodem_receive_firmware(); // 接收完成后校驗(yàn) CRC uint32_t crc crc32_calc((uint8_t *)APP_START_ADDR, param-fw_size); if (crc param-fw_crc) { param-upgrade_flag 0; flash_write_param(param); // 清除升級(jí)標(biāo)志 jump_to_app(APP_START_ADDR); } else { // 校驗(yàn)失敗留在 bootloader 等重發(fā) uart_send_nak(); } }CRC32 用標(biāo)準(zhǔn)多項(xiàng)式 0x04C11DB7PC 端算好之后通過(guò)結(jié)束幀發(fā)過(guò)來(lái)bootloader 收到后對(duì) App 區(qū)數(shù)據(jù)再算一遍兩邊一致才跳轉(zhuǎn)。這一步是最后一道防線能攔住傳輸過(guò)程中被干擾但 CRC16 沒(méi)檢出的錯(cuò)誤。3.4 PC 端發(fā)送工具的關(guān)鍵參數(shù)PC 端用 Python 寫一個(gè)發(fā)送腳本核心是pyserial庫(kù)。關(guān)鍵參數(shù)import serial import struct import time ser serial.Serial( portCOM3, baudrate115200, bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeout2 ) def send_frame(cmd, datab): length len(data) payload struct.pack(BH, cmd, length) data crc crc16_ccitt(payload) frame b\x59 payload struct.pack(H, crc) ser.write(frame) # 等 ACK resp ser.read(1) return resp b\x06 # 假設(shè) ACK 用 0x06 表示波特率 115200 是保守選擇G431 跑 170MHz 可以上到 921600 甚至更高但前提是串口時(shí)鐘配置正確、線材質(zhì)量過(guò)關(guān)。第一次調(diào)通建議先用 115200穩(wěn)定后再提速。timeout2是等 ACK 的超時(shí)太短容易誤判重發(fā)太長(zhǎng)則升級(jí)慢。4. 避坑與排查IAP 升級(jí)中最容易翻車的五個(gè)地方4.1 跳轉(zhuǎn)后 App 跑不起來(lái)卡在 HardFault現(xiàn)象bootloader 跳轉(zhuǎn)后App 沒(méi)有任何輸出調(diào)試器顯示停在 HardFault_Handler。原因最常見(jiàn)的是向量表沒(méi)設(shè)對(duì)。App 編譯時(shí)鏈接地址還是 0x08000000但實(shí)際燒在 0x08005800中斷向量表里的地址全是錯(cuò)的。另一個(gè)可能是跳轉(zhuǎn)前沒(méi)關(guān) SysTickSysTick 中斷觸發(fā)后跳到 bootloader 的舊向量。解決App 工程的鏈接腳本.ld 或 Keil 的 IROM 設(shè)置必須改成起始 0x08005800同時(shí)在 App 的main函數(shù)開(kāi)頭加SCB-VTOR 0x08005800;。跳轉(zhuǎn)前確認(rèn)SysTick-CTRL 0已執(zhí)行。4.2 Flash 寫入返回 HAL_ERROR現(xiàn)象HAL_FLASH_Program返回 HAL_ERROR數(shù)據(jù)寫不進(jìn)去。原因三種可能——目標(biāo)頁(yè)沒(méi)擦除、寫入地址不是 8 字節(jié)對(duì)齊、Flash 沒(méi)解鎖。G431 的 Flash 寫入要求地址 8 字節(jié)對(duì)齊如果write_addr是奇數(shù)或者不是 8 的倍數(shù)直接報(bào)錯(cuò)。解決檢查write_addr是否從 0x08005800 開(kāi)始且每次加 8確認(rèn)HAL_FLASH_Unlock()在擦除和寫入之前都調(diào)了擦除后讀一下目標(biāo)地址應(yīng)該是 0xFF。4.3 串口丟數(shù)據(jù)升級(jí)到一半卡死現(xiàn)象升級(jí)過(guò)程中進(jìn)度條走到某個(gè)位置就不動(dòng)了PC 端等 ACK 超時(shí)。原因接收中斷優(yōu)先級(jí)太低被其他中斷打斷導(dǎo)致串口溢出或者環(huán)形緩沖區(qū)太小主循環(huán)處理不及時(shí)。解決把串口中斷優(yōu)先級(jí)設(shè)到最高搶占優(yōu)先級(jí) 0環(huán)形緩沖區(qū)至少 512 字節(jié)主循環(huán)里處理幀的速度要快Flash 擦寫操作放在幀處理之后不要阻塞接收。4.4 升級(jí)完成后 App 的全局變量初值不對(duì)現(xiàn)象App 跑起來(lái)后一些初始化為非零的全局變量值是 0。原因啟動(dòng)文件里的.data段拷貝邏輯依賴鏈接腳本如果鏈接腳本沒(méi)改.data段的加載地址和運(yùn)行地址對(duì)不上拷貝出來(lái)就是錯(cuò)的。解決App 工程的鏈接腳本要同時(shí)改 Flash 起始地址和.data段的 LMA加載地址。用 CubeIDE 的話在STM32G431XX_FLASH.ld里把FLASH的ORIGIN改成0x08005800.data段會(huì)自動(dòng)跟著調(diào)整。4.5 參數(shù)區(qū)數(shù)據(jù)掉電丟失現(xiàn)象升級(jí)標(biāo)志寫進(jìn)去了斷電再上電讀出來(lái)是 0xFFFFFFFF。原因參數(shù)區(qū)所在的頁(yè)沒(méi)有正確擦除就寫入或者寫入后沒(méi)有等 Flash 操作完成就斷電。解決寫參數(shù)前先擦除整頁(yè)寫入后調(diào)HAL_FLASH_Lock()并加一個(gè)短延時(shí)比如 10ms再斷電參數(shù)結(jié)構(gòu)體加魔數(shù)校驗(yàn)?zāi)?shù)不對(duì)就認(rèn)為參數(shù)無(wú)效走默認(rèn)跳 App 流程。5. 進(jìn)階技巧雙區(qū)備份與差分升級(jí)的取舍把基礎(chǔ) IAP 跑通之后下一步自然會(huì)想如果升級(jí)過(guò)程中斷電了怎么辦App 區(qū)被擦了一半跳也跳不過(guò)去設(shè)備變磚。這就是雙區(qū)備份要解決的問(wèn)題。雙區(qū)方案是在 Flash 里劃兩個(gè) App 區(qū)比如 App-A 從 0x08005800 開(kāi)始App-B 從 0x0800C800 開(kāi)始各占 50KB。bootloader 里維護(hù)一個(gè)「當(dāng)前活動(dòng)區(qū)」標(biāo)志升級(jí)時(shí)往非活動(dòng)區(qū)寫寫完后校驗(yàn) CRC通過(guò)則切換活動(dòng)區(qū)標(biāo)志并跳轉(zhuǎn)不通過(guò)則繼續(xù)用舊區(qū)。這樣即使升級(jí)中斷舊區(qū)仍然完整可用。代價(jià)是 Flash 利用率減半。G431 只有 128KB雙區(qū)之后每個(gè) App 最多 50KB 左右稍微大一點(diǎn)的工程就放不下。所以雙區(qū)適合對(duì)可靠性要求極高、固件體積可控的場(chǎng)景。另一個(gè)方向是差分升級(jí)只傳新舊固件之間的差異部分減少傳輸時(shí)間。但差分算法比如 bsdiff需要額外的 RAM 和 Flash 來(lái)解壓和合并G431 的 32KB RAM 跑起來(lái)比較吃力而且 bootloader 里要集成解壓庫(kù)復(fù)雜度上升不少。我的習(xí)慣是如果串口速率能到 921600全量升級(jí) 100KB 固件也就十幾秒差分帶來(lái)的收益不值得那個(gè)復(fù)雜度。最后說(shuō)一個(gè)驗(yàn)證技巧每次升級(jí)完成后不要只看「跳轉(zhuǎn)成功」就完事。在 App 里加一段代碼上電后往參數(shù)區(qū)寫一個(gè)「啟動(dòng)計(jì)數(shù)」如果連續(xù)啟動(dòng)失敗三次自動(dòng)回退到 bootloader 等待重新升級(jí)。這個(gè)機(jī)制在實(shí)驗(yàn)室里可能永遠(yuǎn)用不上但在現(xiàn)場(chǎng)部署時(shí)能救命。我自己就遇到過(guò)一批設(shè)備因?yàn)殡娫醇y波導(dǎo)致 App 啟動(dòng)偶發(fā)失敗靠這個(gè)回退機(jī)制遠(yuǎn)程恢復(fù)了大部分剩下的才需要現(xiàn)場(chǎng)處理。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取