简介:一套面向STM32嵌入式开发者的IAP Bootloader实现资料,完整演示基于YModem协议通过串口在线升级APP的流程。资源内含bootloader与APP两个独立工程,覆盖启动初始化、串口接收、CRC校验、Flash写入和跳转执行等关键环节,适合正在做远程升级或量产固件更新方案的中高级开发者参考。压缩包共475个文件,以C源码(61个.c、60个.h)、编译中间文件(45个.o、45个.d)及Keil工程配置(uvprojx/uvoptx)为主,另有hex/bin烧录文件和说明文档,整体约9.71MB,目录结构清晰便于定位代码。目前已有2377人学习下载。无论直接复用YModem解析逻辑,还是对照理解STM32 IAP的分区与跳转设计,这套资料都能提供完整样例和可编译工程,是串口升级方案落地的高价值参考。
1. 把升级固件这事做稳:STM32 IAP与YMODEM的搭配逻辑
做过量产设备的人都知道,产品出了厂,软件不可能永远不变。Bug要修、功能要加、参数要调,如果每次都得拆机接下载器,售后成本会直接拖垮项目。STM32 IAP(In-Application Programming)解决的就是这个问题:用一段常驻的bootloader程序,在设备启动时接管串口或网络,接收上位机发来的固件文件,写入Flash后跳转到新程序运行。而文件传输这一环,老牌协议YMODEM至今仍是嵌入式IAP里最可靠的选项之一——它支持断点续传的衍生版本、带CRC16校验、块大小可切换,在115200波特率下传输几百KB的固件,速度和稳定性都够用。
这套方案适合谁?正在做量产固件升级功能的嵌入式开发者,尤其是用过串口烧录但还没上IAP、或者用过XMODEM但被单文件限制和弱校验折腾过的人。接下来我会从协议细节、STM32端的分层实现、App跳转、掉电保护到现场踩坑,完整走一遍这个方向的落地路径。
2. YMODEM协议拆解:把文件拆包、应答、校验一次看明白
2.1 YMODEM与XMODEM的差异:多文件、长度校验是核心
做IAP选型时,很多人第一反应是用XMODEM,因为它最简单:固定128字节一块,CRC校验,几十行代码就能跑通。但XMODEM有两个硬伤:一是每次只能传一个文件,bootloader和App如果分开烧写就要传两次;二是它不携带文件长度信息,接收端根本不知道数据何时真正结束,只能靠文件尾部的填充字节判断,万一填充字节恰好是控制字符,整个包时序就乱了。
YMODEM在XMODEM基础上做了三处关键改进。第一,支持多文件传输,每个文件前都有一个包含文件名和长度信息的0号块;第二,数据块长度可以从128字节切换到1024字节,同样数据量下传输次数少了8倍,115200波特率下速度差异非常明显;第三,0号块里明确携带文件字节数,接收端可以精确控制写入Flash的数据量,不用再把尾部填充字节写进应用区。
选择YMODEM而不是ZMODEM的原因也很实际:ZMODEM虽然支持断点续传和更高效的流控,但协议状态机复杂度翻倍,MCU端RAM占用和代码量都更大,而且很多串口终端工具对ZMODEM的支持反而不如YMODEM全。YMODEM在功能和复杂度之间取了一个平衡点,这也是它在嵌入式领域几十年没被淘汰的原因。
2.2 传输帧结构:从SOH/STX到CRC16的逐字节解读
YMODEM的帧结构基于XMODEM扩展而来,但理解它不需要死记硬背,抓住几个关键控制字符就够了。
控制字符定义如下:
SOH(0x01):后跟128字节数据块的帧头STX(0x02):后跟1024字节数据块的帧头EOT(0x04):文件数据传输完毕ACK(0x06):正确接收应答NAK(0x15):错误接收应答,请求重发CAN(0x18):取消传输C(0x43):请求发送,表示接收端就绪
数据帧格式是:帧头 + 块序号(1字节,从1递增) + 块序号反码 + 数据块 + CRC16校验(2字节)。0号块特殊一些,它的数据部分是文件名 + 空格 + 文件大小字符串,不足部分用0x00填充,最后以CRC16结尾。
CRC16的算法是CCITT标准,多项式0x1021,初始值0x0000。这个细节值得多说一句:CRC算错是整个传输过程里最隐蔽的问题之一,因为YMODEM的CRC校验字节是低字节在前、高字节在后,很多人按高字节在前实现,结果发现每次都在同一个块报错。具体的CRC计算代码我会在下一章给出,这里先把帧结构交代清楚。
2.3 状态机视角:发送端和接收端怎么握手、续传、结束
把YMODEM传输过程看成状态机,比逐行读代码直观得多。接收端(STM32 bootloader)是主控方,它主动发出请求字符,发送端(PC工具)根据请求回应数据。
整体流程分三个阶段:
第一阶段是会话启动。接收端发送字符C,等待发送端回应0号块。发送端发来0号块后,接收端解析文件名和长度,如果长度合法就回ACK,然后额外发一个C表示“文件内容可以开始了”。如果0号块校验失败,接收端回NAK请求重发,连续收到多次错误后发CAN取消。
第二阶段是文件数据传输。发送端从块序号1开始,按128或1024字节一帧地发送数据。接收端每收到一帧,先验帧头、序号、序号反码,再算CRC16,全部通过就写入Flash并回ACK,否则回NAK。这里有一个重要的超时逻辑:接收端不能无限等数据帧,一般设1秒超时,超时后重发NAK,连续超时5次以上就要放弃本次会话。
第三阶段是文件结束。发送端发EOT,接收端回NAK表示“确认结束请求”,发送端再发一次EOT,接收端回ACK。随后进入下一个文件的0号块协商。如果发送端没有后续文件,会直接发一个只有文件名为空、长度为0的0号块,接收端识别后回ACK并以CAN结束整个会话。
理解这三个阶段后,移植协议的核心就变成了一个有限状态机的实现,而不是一行行抄代码。下一章直接落代码。
3. 移植YMODEM到STM32:分层设计与核心代码
3.1 串口抽象层:把字节收发从协议里剥出去
我在实际项目中习惯把YMODEM的协议解析和硬件收发彻底分开。原因很简单:bootloader可能跑在USART1上,也可能跑在UART4上,甚至可能换成CAN或USB虚拟串口。协议层只调用两个函数:接收一个字节(带超时)和发送一个字节,具体实现由硬件抽象层提供。
先看串口初始化,这里以USART1为例,配置为115200、8N1:
void uart1_init(uint32_t baudrate) { GPIO_InitTypeDef gpio_init = {0}; USART_InitTypeDef usart_init = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_USART1_CLK_ENABLE(); gpio_init.Pin = GPIO_PIN_9 | GPIO_PIN_10; gpio_init.Mode = GPIO_MODE_AF_PP; gpio_init.Pull = GPIO_PULLUP; gpio_init.Speed = GPIO_SPEED_FREQ_HIGH; gpio_init.Alternate = GPIO_AF7_USART1; HAL_GPIO_Init(GPIOA, &gpio_init); usart_init.BaudRate = baudrate; usart_init.WordLength = USART_WORDLENGTH_8B; usart_init.StopBits = USART_STOPBITS_1; usart_init.Parity = USART_PARITY_NONE; usart_init.Mode = USART_MODE_TX_RX; usart_init.HwFlowCtl = USART_HWFLOWCONTROL_NONE; HAL_UART_Init(&huart1); }逻辑说明:GPIO配置为复用推挽,Alternate映射到USART1;串口参数里关掉硬件流控,YMODEM这种半双工轮询协议不用RTS/CTS,省两个引脚。
接收和发送字节的封装如下:
uint8_t ymodem_uart_get_byte(uint8_t *byte, uint32_t timeout_ms) { uint32_t start_tick = HAL_GetTick(); while (HAL_GetTick() - start_tick < timeout_ms) { if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET) { *byte = (uint8_t)(huart1.Instance->DR & 0xFF); return 1; } } return 0; // 超时未收到字节 } void ymodem_uart_put_byte(uint8_t byte) { while (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE) == RESET); huart1.Instance->DR = byte & 0xFF; }参数说明:get_byte用查询方式读数据寄存器而非中断,因为协议引擎本身是阻塞式状态机,中断反而要处理临界区问题;超时时间由调用方传入,协议层会根据不同状态传不同值——正常等待数据帧时给1000ms,等待握手响应时给3000ms。
这套封装的收益在换平台时体现得最明显。我遇到过一个项目从STM32F103换到STM32F407,bootloader里所有YMODEM逻辑一行没改,只替换了这两个函数的内部实现。
3.2 协议引擎:CRC校验、帧解析与应答状态机实现
协议引擎是bootloader里最核心的部分。我把状态机定义成枚举,每个状态对应一个处理函数,主循环里根据当前状态调用对应函数并迁移状态。这种做法比大段的switch-case清晰得多,也方便后续加入超时重试。
先看CRC16的实现,这是YMODEM里最容易写错的部分:
uint16_t crc16_ccitt(uint8_t *data, uint32_t len) { uint16_t crc = 0x0000; while (len--) { crc ^= (uint16_t)(*data++) << 8; for (int i = 0; i < 8; i++) { if (crc & 0x8000) { crc = (crc << 1) ^ 0x1021; } else { crc <<= 1; } } } return crc; }逻辑说明:这是按位计算的CCITT CRC16,多项式0x1021,初始值0x0000。注意每次异或的是数据字节左移8位后的值,因为CCITT的MSB-first处理方式就是这样。实际工程中可以用查表法替代按位计算,速度提升明显,4KB的查找表在Flash里占用也很小。
然后是协议引擎的核心,接收端状态机的关键部分:
typedef enum { YMODEM_STATE_WAIT_0_BLOCK, // 等待0号块(会话开始) YMODEM_STATE_RECV_DATA, // 接收文件数据块 YMODEM_STATE_WAIT_EOT, // 等待EOT结束 YMODEM_STATE_FINISHED, // 会话结束 YMODEM_STATE_ERROR // 错误状态 } ymodem_state_t; int ymodem_receive(uint8_t *buf, uint32_t buf_size, uint32_t *file_len, uint8_t *file_name) { ymodem_state_t state = YMODEM_STATE_WAIT_0_BLOCK; uint32_t received_len = 0; uint8_t block_num = 1; uint8_t byte; while (state != YMODEM_STATE_FINISHED && state != YMODEM_STATE_ERROR) { switch (state) { case YMODEM_STATE_WAIT_0_BLOCK: ymodem_uart_put_byte(0x43); // 发送'C'请求0号块 if (ymodem_recv_block(buf, &block_num, &byte, 3000)) { if (block_num == 0 && byte == 0) { // 空文件名,表示整个会话结束 ymodem_uart_put_byte(0x06); // ACK ymodem_uart_put_byte(0x43); state = YMODEM_STATE_WAIT_EOT; } else { // 解析文件名和长度 parse_0_block(buf, file_name, file_len); ymodem_uart_put_byte(0x06); // ACK ymodem_uart_put_byte(0x43); // 'C'表示开始传数据 state = YMODEM_STATE_RECV_DATA; } } else { state = YMODEM_STATE_ERROR; } break; case YMODEM_STATE_RECV_DATA: if (ymodem_recv_block(buf, &block_num, &byte, 1000)) { if (byte == 0x04) { // EOT ymodem_uart_put_byte(0x15); // NAK确认结束 state = YMODEM_STATE_WAIT_EOT; } else { // 写入Flash... // 注意:如果block_num小于预期,可能是重传 ymodem_uart_put_byte(0x06); // ACK } } else { ymodem_uart_put_byte(0x15); // NAK请求重发 } break; case YMODEM_STATE_WAIT_EOT: if (ymodem_uart_get_byte(&byte, 3000)) { if (byte == 0x04) { ymodem_uart_put_byte(0x06); // ACK state = YMODEM_STATE_FINISHED; } else if (byte == 0x06) { // 发送端确认结束,会话完成 state = YMODEM_STATE_FINISHED; } else { state = YMODEM_STATE_ERROR; } } else { state = YMODEM_STATE_ERROR; } break; default: break; } } return (state == YMODEM_STATE_FINISHED) ? 0 : -1; }这里有一段代码被封装成了ymodem_recv_block,它负责一帧的完整接收:读帧头、读序号、读反码、读数据区、读CRC,然后校验。帧接收的完整处理逻辑如下:
uint8_t ymodem_recv_block(uint8_t *buf, uint8_t *block_num, uint8_t *type, uint32_t timeout_ms) { uint8_t hdr, seq, seq_comp, crc_h, crc_l; uint16_t crc_calc, crc_recv; uint32_t data_len; if (!ymodem_uart_get_byte(&hdr, timeout_ms)) return 0; if (hdr == 0x01) { // SOH, 128字节块 data_len = 128; } else if (hdr == 0x02) { // STX, 1024字节块 data_len = 1024; } else if (hdr == 0x04) { // EOT *type = 0x04; return 1; } else { return 0; } if (!ymodem_uart_get_byte(&seq, 1000)) return 0; if (!ymodem_uart_get_byte(&seq_comp, 1000)) return 0; if ((seq ^ seq_comp) != 0xFF) return 0; // 序号反码不正确 *block_num = seq; *type = hdr; if (!ymodem_uart_get_bytes(buf, data_len, 1000)) return 0; if (!ymodem_uart_get_byte(&crc_h, 1000)) return 0; if (!ymodem_uart_get_byte(&crc_l, 1000)) return 0; crc_calc = crc16_ccitt(buf, data_len); crc_recv = ((uint16_t)crc_h << 8) | crc_l; if (crc_calc != crc_recv) return 0; return 1; }参数说明:timeout_ms是等待帧头的超时时间,用于区分正常数据帧和EOT——EOT是单独一个字节,不会有帧头超时问题;seq ^ seq_comp的校验是YMODEM协议固有的防错机制,序号0到255循环,发送端按块递增。CRC先收高位再收低位,对应发送端的存储顺序。
3.3 Flash写入层:页擦除、双Bank与写入时序
数据块接收成功后,下一步就是写Flash。写入层的设计直接影响IAP的可靠性,我不建议把擦写逻辑直接揉进协议引擎里。更常见的做法是单独维护一个iap_flash.c,对外提供三个接口:扇区擦除、数据写入、固件校验。
以STM32F103为例,Flash操作的关键点是必须在擦除和写入前解锁,完成后重新锁定。代码实现:
void flash_unlock(void) { while (FLASH->CR & FLASH_CR_BSY); if (FLASH->CR & FLASH_CR_LOCK) { FLASH->KEYR = 0x45670123; FLASH->KEYR = 0xCDEF89AB; } } void flash_lock(void) { FLASH->CR |= FLASH_CR_LOCK; } int flash_erase_sector(uint32_t sector_addr) { flash_unlock(); while (FLASH->CR & FLASH_CR_BSY); FLASH->CR &= ~(FLASH_CR_SECTORS_MASK | FLASH_CR_OP_MASK); FLASH->CR |= FLASH_CR_PER; // 页擦除模式 FLASH->AR = sector_addr; // 指定页地址 FLASH->CR |= FLASH_CR_STRT; // 启动擦除 while (FLASH->CR & FLASH_CR_BSY); flash_lock(); return 0; } int flash_write_word(uint32_t addr, uint32_t data) { flash_unlock(); while (FLASH->CR & FLASH_CR_BSY); FLASH->CR &= ~(FLASH_CR_SECTORS_MASK | FLASH_CR_OP_MASK); FLASH->CR |= FLASH_CR_PG; // 编程模式 *(volatile uint32_t *)addr = data; while (!(FLASH->SR & FLASH_SR_EOP)); FLASH->SR |= FLASH_SR_EOP; flash_lock(); return 0; }逻辑说明:STM32 Flash编程是半字或全字操作,不能像SRAM那样按字节写。YMODEM收到的数据是字节流,写入前要拼装成32位字。接收缓冲区用uint8_t数组,写入时用指针强转或者memcpy到对齐地址,但必须确保目标地址是4字节对齐的。
擦除策略上,我一般不在收到第一块数据时就把整个App区全擦掉,而是按页懒擦除:每收到一个新页的首块数据,先把这一页擦掉再写。原因很直接——如果传输中途用户拔线,至少之前写进去的页还在,配合后面讲的掉落保护策略,设备还能回滚到上一个正常版本。
4. bootloader与App的协同:跳转、中断向量与固件签名
4.1 中断向量重映射:让App跑起来的第一步
YMODEM把固件完整写入Flash后,bootloader还不能直接把PC指针指过去就完事。STM32上电后默认从0x08000000取栈顶地址和复位向量,如果App被烧写到0x08010000,中断向量表还在0x08000000,App里任何一个中断触发,CPU都会跳到bootloader的中断处理函数,结果就是死循环或者HardFault。
解决这个问题需要两步。第一步,App工程在编译时把向量表偏移改到实际地址:
// App工程的system_stm32f1xx.c或启动文件里 #define VECT_TAB_OFFSET 0x10000 // 64KB偏移,对应0x08010000第二步,bootloader在跳转前设置SCB->VTOR寄存器。Cortex-M3/M4内核支持向量表重定位,这一行代码是IAP跳转的核心:
typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t stack_top = *(volatile uint32_t *)app_addr; pFunction app_entry = (pFunction)(*(volatile uint32_t *)(app_addr + 4)); if ((stack_top & 0x2FFE0000) != 0x20000000) { return; // 栈顶地址非法,拒绝跳转 } SCB->VTOR = app_addr; __set_MSP(stack_top); app_entry(); }参数说明:跳到App前检查栈顶地址是否落在SRAM范围内,是防止跳转到垃圾数据的常见手段。app_addr通常定义为#define APP_ADDR 0x08010000,和App工程里的VECT_TAB_OFFSET严格对应,这里改错一个字,后面全是玄学问题。
4.2 多版本固件管理:用头部元数据做校验和版本比对
实际产品里,bootloader接收固件后不能直接就跳转。如果固件在传输中损坏,或者文件根本不是给这个硬件型号编译的,跳转过去就是砖机。更稳妥的做法是给固件文件定义一个头部结构,YMODEM传完数据后,bootloader先解析头部再决定是否更新。
我常用的固件头部结构定义如下:
typedef struct { uint32_t magic; // 魔数,例如0xA5A5A5A5,用于识别合法固件 uint32_t version; // 固件版本号,递增比较 uint32_t length; // 固件实际代码长度(不含头部) uint32_t checksum; // 对整个固件代码区的简单累加和或CRC32 uint32_t hw_type; // 硬件型号标识,防止固件混用 uint32_t reserved[3]; // 保留,用于后续扩展 } app_header_t;这个头部可以放在App程序的开头,也可以由PC上位机在发送前自动拼接。bootloader接收完文件后,按如下逻辑校验:
int verify_app_header(app_header_t *hdr, uint32_t app_addr) { if (hdr->magic != 0xA5A5A5A5) return -1; if (hdr->hw_type != THIS_HW_TYPE) return -2; uint32_t calc = 0; uint32_t *p = (uint32_t *)(app_addr + sizeof(app_header_t)); uint32_t len = hdr->length; while (len >= 4) { calc += *p++; len -= 4; } if (calc != hdr->checksum) return -3; return 0; }这段校验逻辑在量产设备上很有价值。版本回退策略也依赖这个头部:如果新固件启动后运行不稳定,可以在App里写一个标志位,bootloader下次启动时检测到这个标志就自动加载上一版本固件,相当于给了自己一个后悔药。
4.3 掉电保护:双备份区和标志位设计的取舍
掉电是IAP最大的敌人。传输到一半拔电、写入Flash的瞬间抖动、校验完成后复位前断电,任何一种情况都可能让设备变砖。轻量方案是加一个升级标志位,bootloader启动时先检查这个标志:
// 定义在Flash的最后一个扇区,长度一个word以上 volatile uint32_t *upgrade_flag = (uint32_t *)(0x0800FC00); #define FLAG_UPGRADE_PENDING 0x55AA55AA #define FLAG_UPGRADE_DONE 0x5A5AA55A // bootloader启动时检查 if (*upgrade_flag == FLAG_UPGRADE_PENDING) { // 进入YMODEM接收流程 ymodem_receive(...); // 接收完成后,校验固件,跳转前清除标志 *upgrade_flag = FLAG_UPGRADE_DONE; }这个方案能防住“意外进入升级状态”的问题,但防不住升级过程中掉电——App区可能已经写了一半。如果产品允许,更推荐双Bank方案:当前运行版本放在A区,新固件先写入B区,全部校验通过后再把引导标志切到B区。代价是Flash占用翻倍,不是所有MCU都承受得起。
折中方案是A/B分区只保留一个备份:bootloader区固定不动,App区(BANK0)放着当前运行版本,固件缓存区(BANK1)接收新版本。新版本校验通过后,bootloader把BANK1整体复制到BANK0,复制过程中Flasherase失败就停留在BANK1继续跑。这个方案在资源允许时我一般优先用。
5. IAP常见避坑:地址算错、CRC不一致、跑飞等5个现场
5.1 现象:跳转App后死在HardFault
原因:SCB->VTOR设置正确,但跳转前没有关闭全局中断。bootloader的串口中断、定时器中断在跳转瞬间仍然开启,而App的向量表已经切换,中断触发时CPU跳到App的中断入口,但中断服务函数可能还没开始执行,环境完全不一致。
解决:跳转前执行__disable_irq(),进入App后由App自己在main入口重新初始化中断并调用__enable_irq()。如果App里有FreeRTOS之类的系统,还需要额外处理PendSV和SysTick的优先级,否则系统一跑就崩溃。
5.2 现象:YMODEM传输到99%卡死
原因:这不是协议问题,而是文件长度计算不一致。PC端发送工具计算的文件长度包含了头部和填充字节,但bootloader按0号块解析出的长度写入Flash,两者间有偏差,导致最后一个块的数据还没收完就提前认为结束。
解决:固定文件生成流程,在PC端用脚本把固件头部和应用代码打包成单一文件,确保0号块里的长度信息就是这个文件的实际字节数。接收端解析长度后,用这个值决定写多少个数据块,而不是收到EOT才算结束。
5.3 现象:CRC校验总是不对,且总是同一个位置
原因:CRC16的字节序反了。YMODEM协议里CRC高字节先传、低字节后传,接收端按这个顺序拼接,但计算时把输入数据的字节序也做了反转,导致高位的校验永远差一个字节。这个问题最大的迷惑性在于它能在整个文件的绝大部分数据块上通过,只在某些特殊字节组合时出错。
解决:写一个对照测试用例,构造全0和全1的数据块,用已知的YMODEM工具抓取标准结果比对。或者更简单——用PC工具传一个固定文件,bootloader把收到的CRC值和计算值分别打印到串口对比,一眼就能看出字节序问题。
5.4 现象:App能跑但所有中断都不响应
原因:App工程里没设置VECT_TAB_OFFSET,或者设置了但编译器没有把向量表放到正确位置。STM32标准库和HAL库都提供了VECT_TAB_OFFSET宏定义,但有时工程从别的平台移植过来,启动文件里的向量表声明还是0地址。
解决:在App工程的system_xxx.c文件里确认VECT_TAB_OFFSET的值,同时查启动文件里的__initial_sp和Reset_Handler地址是否在0x08010000之后。还有一种土办法:App工程编译完后查看map文件,确认g_pfnVectors的地址落在App的起始地址,一秒钟就能确认问题。
5.5 现象:写Flash后校验读回一致,重启却丢了
原因:写入地址越界到了Flash的保护扇区,写入操作被硬件丢弃(写操作本身没有报错,只是数据没写进去);或者擦除操作没有按扇区边界对齐,导致部分数据被后续擦除误删。
解决:严格检查App地址和Flash扇区边界。STM32F103的Flash扇区是1KB、2KB、4KB、8KB不等,写之前把目标地址打印出来,和参考手册上的扇区表比对一下。我遇到过最隐蔽的一次,是把App写到了0x0800F000,而这个地址恰好是选项字节区,直接导致MCU上电后进入Bootloader模式,物理上无法执行App代码。
6. 让bootloader更抗造:三组进阶配置与验证手段
YMODEM协议本身足够稳定,但一个量产级bootloader的可靠性,更多取决于你在协议之外补了多少层保护。我习惯在交付前做三件事,每一件都真实地拦下过问题。
第一,给接收缓冲区做两层防护。YMODEM最大块长1024字节,接收缓冲区不能只开这么大。我会额外维护一个覆盖缓冲区,防止帧头错位后把数据当控制字符解析,同时设置最大重传次数——超过5次连续NAK就主动跳出状态机,而不是无限循环。这个限制在串口被干扰时保护了MCU端资源。
第二,传输完成后加一个自检阶段。YMODEM的EOT只能表示数据收完了,不能表示数据写对了。我会在收到EOT后,从Flash里重新读回全部固件数据,再算一遍CRC32与头部里的校验值比对。这个自检会让跳转前多花几百毫秒,但换来的确定性很值。
第三,用回环测试验证串口管线。把bootloader的接收函数输入输出短接,从PC端发送一个已知文件,bootloader收到后原样回传,PC端比对两个文件是否完全一致。这个测试能同时暴露波特率偏差、奇偶校验错误、DMA边界问题。没有跑通过回环测试之前,不要直接上真机升级,否则你分不清是协议问题还是串口问题。
我自己的习惯是,每改动一次Flash写入层或串口配置,就跑一遍回环测试加一次断电中断测试——分别在25%、50%、75%、99%进度时随机断电,然后重新上电确认bootloader能稳定回到接收状态。这套流程跑过三次没有翻车,我才敢把固件发给产线。希望这套从协议到落地的路径能帮到你,少走几趟我之前掉进去的坑。
本文还有配套的精品资源,点击获取