简介:面向嵌入式开发者与单片机学习者,这份资料针对GD32E230平台设计了一套完整的单片机自动升级方案。GD32E230基于Arm Cortex-M23内核,常用于IoT设备、工业控制与消费电子,其固件更新通常需要Bootloader、通信协议和Flash分区等环节协同配合。压缩包共283个文件,以C/H源码、IAR与Keil工程文件(ewp/ewd/uvprojx)、编译输出(o/out/bin)、链接映射(map)及调试脚本为主,整体大小10.8MB,同时包含了gd32E230.bin与boot.bin两类固件,便于核对引导程序和应用代码的烧录关系。已有2645人学习下载。资料不仅演示了从Bootloader启动到应用跳转的完整流程,还覆盖了UART等通信接口下的固件传输、校验机制、错误恢复以及闪存分区管理等关键技术点,并附有工程配置与调试脚本,可直接用于代码研读和二次开发,对正在规划IAP升级或想深入了解GD32E230启动过程的开发者有较强的参考价值。
1. 单片机自动升级程序:基于GD32E230平台,把产线升级从拆机烧录变成串口一键刷写
之前在某公司做一批基于GD32E230的设备,生产到一半发现固件要改,几十台机器已经装壳封好,只能一台台拆开、插烧录器、重新烧,折腾了两天。后来我把这套单片机自动升级程序整理了出来:一个放在片内的Bootloader,配合串口和上位机,把新固件分包下发到Flash,校验通过后自动跳转运行,再也不碰烧录器。项目基于GD32E230这颗Arm Cortex-M23内核、主频72MHz的国产MCU,适合正在做GD32产品、需要在产线批量刷固件、或者想给现有单片机增加串口和OTA升级能力的开发者。整篇文章不讲虚的,直接拆解这套IAP(在应用编程)方案的存储规划、核心代码、跑通步骤和二次改造。
2. IAP整体架构:一个升级请求从进入到跳转的完整链路
2.1 Flash空间如何划分:Bootloader、APP与升级标志位
做IAP第一步是重新规划Flash,不是把固件直接烧在0x08000000就完事。GD32E230的Flash起始地址是0x08000000,我手里这颗型号是64KB Flash、8KB SRAM的配置,整个升级方案都是围绕这个容量设计的。存储布局按三个区域划分:Bootloader区、APP区、标志参数区。
Bootloader区放在0x08000000到0x08003FFF,大小16KB。这个区域存放复位向量、中断向量表和一整套升级逻辑代码,上电后CPU永远先从这里开始执行。Bootloader区的空间要留够,因为后面要放YModem协议解析、Flash驱动、串口驱动,代码量一般会到8KB到12KB,留16KB余量比较稳。
APP区从0x08004000开始,大小48KB,最大支持48KB的应用程序。为什么从0x08004000开始而不是紧挨着Bootloader?因为GD32的Flash页擦除有页对齐要求,把分区边界放在页边界上,擦除和写入时就不会跨区操作。
标志参数区放在0x08003C00附近,跟Bootloader共用最后一块Flash页。这个区域存两个关键信息:一个升级请求魔数,比如0xA5A5,Bootloader每次启动都读它来判断是进升级模式还是直接跳APP;另一个是升级完成标记,防止意外断电后固件写了一半但标志位已经被置位。实际代码里我会把魔数设计成写入时带反码,读出来校验魔数和反码同时匹配才算有效。
这个分区方案对上位机而言是透明的。上位机只需要知道APP的起始地址和最大长度,发送YModem固件包时按顺序下发二进制文件即可,目标地址由Bootloader内部维护。这样换不同型号的GD32E230时,只要改三个宏定义,上位机和协议都不用动。
2.2 升级状态机:Bootloader靠什么样的流程保证固件安全
这套IAP的整个升级控制逻辑是一个标准状态机,包含空闲、同步、接收、校验、跳转五个状态。状态机的好处是每一包数据进来都知道当前该干什么,协议解析不会乱。
我习惯用枚举定义状态,配合一个switch分发。空闲状态是上电默认状态,此时Bootloader检查升级标志:如果标志有效,进入同步状态,向上位机发送字符C,表示准备好接收YModem数据;如果标志无效,直接检查APP区首地址的栈顶指针合法性,合法就跳转。
同步状态下,Bootloader等待上位机的握手包。上位机发来第一帧数据,包含文件名、文件大小等信息,解析出真实固件字节数后,进入接收状态。接收状态处理数据帧,每收到一帧,先校验序号、序号反码和CRC16,校验通过就擦写Flash,然后回复ACK;校验失败回复NAK,要求上位机重发当前帧。
全部帧收完后,上位机发EOT结束帧,Bootloader进入校验状态,把Flash里读出来的数据做一次sum校验,跟上位机在最后一帧携带的校验值对比。通过后置升级完成标志,执行软复位,重新走一遍Bootloader引导流程,这次检测到升级完成标志,去校验APP区有效性,再跳转到新固件。
这个流程看起来简单,但每一步都有防守。序号反码校验挡住错帧和乱序;CRC16挡住数据污染;最后的sum校验挡住Flash写入失败但报告成功的情况。三道防线卡下来,量产环境里传输错误率能压得很低。
2.3 上电引导时的三选一逻辑
Bootloader启动逻辑其实是三选一:进升级模式、跳转APP、或者什么都不做。什么都不做的情况是升级标志和APP都无效,这时候说明Flash是空的,或者上次升级失败把APP区弄坏了。我一般的兜底处理是LED快闪,同时不停向上位机发字符C,等工程师用上位机重新推送固件。
判断APP区是否有效,最朴素的做法是看首地址存的值。Cortex-M23内核的程序起始处必须是栈顶地址,而且栈顶地址必须落在SRAM范围0x20000000到0x20002000内。第二个字是复位向量,也就是APP的入口函数地址,必须落在APP区范围内。这两个条件同时满足,才认为APP区可以跳转。这份检查代码是Bootloader安全性的地基,后面所有升级操作前都要先过这一关。
3. 核心代码拆解:串口收包、Flash擦写与跳转实现
3.1 串口DMA加空闲中断:接收大包不丢数据的缓冲设计
升级固件时每一包YModem数据最大是1024字节,如果串口用传统的逐字节中断接收,频繁进中断会占用大量CPU时间,擦Flash的时候还会丢数据。我在这套代码里用DMA加串口空闲中断的方式接收,DMA负责把串口数据搬到内存缓冲区,CPU只在收到一帧完整数据后处理一次。
DMA配置代码如下,基于GD32的库函数,缓冲大小设成2048字节,能装下一帧YModem数据加上头尾校验,还有余量:
#define RX_BUF_SIZE 2048 uint8_t rx_buff[RX_BUF_SIZE]; volatile uint16_t rx_len = 0; volatile uint8_t rx_frame_done = 0; void uart_dma_init(void) { dma_config(DMA0, DMA_CH3, (uint32_t)&USART1_DATA, (uint32_t)rx_buff, RX_BUF_SIZE); dma_transfer_config(DMA0, DMA_CH3, DMA_PERIPH_TO_MEMORY, DMA_MEMORY_INCREASE_ENABLE, DMA_PERIPH_8BIT, DMA_MEMORY_8BIT, DMA_PDATA_ALIGN_BYTE, DMA_MDATA_ALIGN_BYTE); dma_circulation_enable(DMA0, DMA_CH3); dma_channel_enable(DMA0, DMA_CH3); USART_INT_ENABLE(USART1, USART_INT_IDLE); NVIC_EnableIRQ(USART1_IRQn); }这段代码的核心思路是让DMA工作在循环模式,数据不断往缓冲区里填。串口空闲中断在每一帧数据接收完毕后触发,此时在中断里读取DMA当前计数器的值,用缓冲区总长度减去剩余计数,就是这一帧收到多少字节。
需要注意DMA循环模式的一个坑:如果这一帧数据跨过了缓冲区末尾,数据会被分成两段存在缓冲区的尾部和头部。处理方式是在解析函数里先判断rx_len + current_offset是否超过缓冲区大小,超过则做内存拼接,或者直接做两次拷贝。我在实际工程里选择简单粗暴的办法:把缓冲区设成比最大帧多一倍,YModem最大1024字节加头部5字节,2048字节基本不会跨边界,但代码里仍然保留跨边界处理逻辑,防止上位机抽风多发了几字节。
3.2 YModem协议解析:从握手包到数据帧的逐字段校验
YModem协议本身不复杂,一帧数据由帧头、帧序号、帧序号反码、数据段、CRC校验五部分组成。帧头0x01表示128字节帧,0x02表示1024字节帧。传输结束时上位机发0x04,即EOT。
我实现的解析函数把一帧数据拆开,逐一验证各字段,返回不同的错误码,方便排查上位机问题:
typedef enum { FRAME_SOH = 0x01, /* 128字节帧 */ FRAME_STX = 0x02, /* 1024字节帧 */ FRAME_EOT = 0x04, /* 传输结束 */ FRAME_ACK = 0x06, /* 正确应答 */ FRAME_NAK = 0x15, /* 错误应答 */ FRAME_C = 0x43 /* 请求发送 */ } ymodem_frame_t; int ymodem_parse_packet(uint8_t *buf, uint16_t len, uint16_t *block_no) { if (len < 3) { return -1; /* 数据长度不足以包含帧头 */ } if (buf[0] != FRAME_SOH && buf[0] != FRAME_STX) { return -2; /* 未知帧头 */ } uint16_t payload_len = (buf[0] == FRAME_SOH) ? 128 : 1024; if (len != payload_len + 5) { return -3; /* 帧长度不匹配 */ } *block_no = (buf[1] << 8) | buf[2]; if ((buf[1] ^ buf[2]) != 0xFF) { return -4; /* 帧序号反码校验失败 */ } uint16_t crc = crc16_ccitt(buf + 3, payload_len); uint16_t pack_crc = (buf[payload_len + 3] << 8) | buf[payload_len + 4]; if (crc != pack_crc) { return -5; /* CRC校验失败 */ } return 0; }这个函数每次收到一帧完整数据后调用。返回值0表示帧有效,直接交给Flash写入逻辑;负值表示不同位置的错误,调试时通过串口打印错误码,能快速定位是上位机协议栈问题还是传输链路问题。帧序号block_no要重点说明:YModem的序号不是从0开始,握手包的序号固定为0,第一个数据帧序号为1,而且序号超过255后会回绕。我在Bootloader里不做序号连续性检查,只检查反码,因为CRC已经能挡住数据损坏,序号连续性依赖上位机的协议栈保证。
握手包解析要单独处理。握手包同样是SOH开头带128字节数据,但数据内容是文件名字符串加文件大小字符串。解析时先判断block_no是否为0,再在数据段里找00分隔符,把文件名和大小提取出来。文件大小字符串要转成整数,限制最大不能超过APP区的48KB,超了直接回NAK拒绝升级。
3.3 Flash擦除与写入:对齐、解锁和擦写时序
GD32E230的Flash操作有三条铁律:写之前必须先擦除,擦除按页进行,编程按32位字写入。违反任何一条,轻则写入失败,重则触发总线错误直接进HardFault。
Flash擦除代码我封装成独立函数,升级开始时一次性擦掉整个APP区,避免边写边擦把上一帧数据弄丢:
#define APP_BASE_ADDR 0x08004000u #define APP_MAX_SIZE 0x0000C000u #define FMC_PAGE_SIZE 0x00001000u void flash_erase_app_area(void) { fmc_unlock(); fmc_flag_clear(FMC_FLAG_BANK0_END); fmc_flag_clear(FMC_FLAG_BANK0_WPERR); uint32_t addr = APP_BASE_ADDR; while (addr < APP_BASE_ADDR + APP_MAX_SIZE) { fmc_page_erase(addr); addr += FMC_PAGE_SIZE; } fmc_lock(); }fmc_unlock()是GD32 Flash控制器的解锁操作,往密钥寄存器写特定序列,解锁后才能执行擦写命令。每次擦写完成后要清一次标志位,否则Flash操作完成标志一直挂着,下次擦写命令会误判状态。擦除是整页操作,页大小取决于具体型号,我按0x1000也就是4KB配置,如果你的那颗GD32E230页大小不一样,改这个宏就行。
写入侧要注意一次写32位,也就是4字节。YModem数据帧是字节流,长度不一定能被4整除,所以最后一帧不足4字节的部分要凑齐。看一下写入代码:
int flash_write_app(uint32_t offset, uint8_t *data, uint32_t len) { if (offset + len > APP_MAX_SIZE) { return -1; } fmc_unlock(); uint32_t addr = APP_BASE_ADDR + offset; uint32_t i = 0; for (i = 0; i + 4 <= len; i += 4) { uint32_t word = (uint32_t)data[i] | ((uint32_t)data[i + 1] << 8) | ((uint32_t)data[i + 2] << 16) | ((uint32_t)data[i + 3] << 24); fmc_word_program(addr + i, word); } if (i < len) { uint32_t last = 0; for (uint32_t j = 0; j < 4; j++) { if (i + j < len) { last |= ((uint32_t)data[i + j] << (j * 8)); } else { last |= 0xFFu << (j * 8); /* 补0xFF */ } } fmc_word_program(addr + i, last); } fmc_lock(); return 0; }offset参数是相对APP起始地址的偏移,调用方在接收YModem数据帧时用当前帧序号 * 帧长计算偏移,每一帧写入前都要确认偏移加长度不越过APP区边界。补0xFF的做法来自Flash的物理特性:擦除后的Flash位是1,写入操作只能把1变成0,所以补齐的字节填0xFF不会改变实际数据的有效性,后续旧数据残留在补齐位上的0也不会被误当成有效数据。
3.4 跳转到APP:栈顶地址校验和中断向量表重映射
跳转是整个Bootloader的临门一脚。很多人在这里翻车,跳过去直接HardFault,原因就两个:栈指针不对,或者中断向量表没重映射。我这套代码里简化了跳转方式,用软复位引导,让APP自己从头跑启动文件:
void boot_check_and_run(void) { if (upgrade_flag == MAGIC_DONE) { upgrade_flag = 0x0000; fmc_word_program(FLAG_ADDR, 0x0000); if (app_image_valid(APP_BASE_ADDR)) { NVIC_SystemReset(); } } }软复位的好处是干净利落,所有外设寄存器恢复默认状态,时钟配置也回到默认值,APP从启动文件开始执行,跟独立烧录表现完全一致。但前提是Bootloader里要提前把升级标志清掉,否则复位回来又进入升级模式,陷入死循环。
app_image_valid检查的就是前面讲的栈顶地址和复位向量地址是否合法:
uint8_t app_image_valid(uint32_t addr) { uint32_t sp = *(volatile uint32_t *)addr; uint32_t pc = *(volatile uint32_t *)(addr + 4); if ((sp & 0xFFF00000) != 0x20000000) { return 0; } if (pc < addr || pc >= addr + APP_MAX_SIZE) { return 0; } return 1; }除了软复位,还有一种更直接的跳转方式,从APP起始地址取出栈顶指针,调__set_MSP(app_sp)切换主栈指针,再通过函数指针跳转到复位向量。不过这种方式要求Bootloader在跳转前把中断关了、把用过的串口外设DeInit掉,还要处理SysTick定时器残留的计数状态。我吃过一次亏:跳过去之后串口打印乱码,查了半天发现是Bootloader里设置的波特率寄存器和APP里设置的值不一样,寄存器残留没清干净。从那以后我统一用软复位跳转,省心。
4. 工程落地:从Keil双工程到完成一次真实升级
4.1 双工程的组织方式与编译参数
拿到这套源码,第一件事不是看代码,是建两个Keil工程:一个编译Bootloader,一个编译APP,两者绝对不能混在同一个工程里。
Bootloader工程的Flash配置:IROM1起始地址0x08000000,大小0x4000即16KB;IRAM起始地址0x20000000,大小0x2000即8KB。编译产物用于烧录器直接写入芯片。APP工程的Flash配置:IROM1起始地址0x08004000,大小0xC000即48KB;IRAM起始地址0x20000000,大小0x2000。编译产物是待升级的目标固件。
APP工程里最关键的一处配置:中断向量表偏移。GD32E230的启动文件system_gd32e230.c里有一个宏VECT_TAB_OFFSET,默认是0。APP要在Flash偏移0x4000处运行,这个宏必须改成0x4000。原理是Cortex-M23内核的中断向量表基地址由VTOR寄存器控制,启动代码会在main函数之前把VECT_TAB_OFFSET配置到VTOR,中断才能正确跳转到APP的中断服务函数。改完这个宏,编译出的hex文件里所有中断向量都带上了0x4000偏移,烧进APP区才能用。
我的习惯是在APP的main函数最开头再强制写一次VTOR,双保险:
SCB->VTOR = APP_BASE_ADDR & 0x1FFFFF80u;这样就算启动代码里的宏漏配了,程序跑起来第一步也能把向量表重映射过来。
4.2 上位机选型与YModem参数匹配
Bootloader准备好之后,上位机可以选现成的串口工具,也可以写个小脚本。成熟方案是SecureCRT的自带YModem发送功能,但我建议你在量产工具里用Python脚本自己实现,原因后面避坑章节会讲。
我写的Python发送脚本核心逻辑很薄,用pyserial打开串口,先发字符C等待握手,然后按YModem协议分包发送固件文件:
import serial ser = serial.Serial("COM10", 115200, timeout=1) def send_ymodem(filename): with open(filename, "rb") as f: data = f.read() ser.write(b"C") resp = ser.read(1) if resp != b"\x06": # ACK print("handshake failed") return block = 1 for i in range(0, len(data), 1024): chunk = data[i:i+1024] frame = b"\x02" + bytes([block & 0xFF, (block & 0xFF) ^ 0xFF]) frame += chunk + b"\x00" * (1024 - len(chunk)) frame += crc16_ccitt(chunk) ser.write(frame) if ser.read(1) != b"\x06": print("block %d failed" % block) return block += 1 ser.write(b"\x04") # EOT if ser.read(1) != b"\x06": print("eot failed") send_ymodem("app.bin") ser.close()脚本里的握手时序要和Bootloader严格对应。Bootloader在接收状态发字符C,上位机收到后回复ACK,然后才开始发数据帧。如果你的Bootloader改成了发NAK触发握手,这里也要对应改,否则卡在第一步。
4.3 一次完整升级的操作步骤
拿到工程后我建议按下面的顺序走一遍,每一步都确认现象,不要跳步。
第一步,烧录Bootloader。用烧录器把Bootloader的hex文件烧进GD32E230,烧录配置里确认起始地址是0x08000000。烧完后不要急着连串口,用调试器读一下0x08004000的Flash内容,正常情况下全是0xFF。
第二步,编译APP工程,确认map文件里Image$$RW_IRAM1$$Base和Image$$ER_IROM1$$Base的地址。IROM基地址必须是0x08004000,不是的话回去检查4.1的配置。同时确认编译出来的bin文件大小不超过48KB。
第三步,连接串口。按Bootloader里的串口号和波特率把设备接电脑,打开Python脚本或者SecureCRT,先手动发一个字符C,观察串口是否有ACK回应。这一步确认物理链路通。
第四步,发送固件。走完整个YModem流程后,设备会自动复位进入APP。此时观察设备运行现象,如果LED正常闪烁或者功能正常,升级完成。
第五步,断电验证。重新上电,确认Bootloader能自动跳转到APP,不需要再发一次升级命令。这一步很关键,能暴露升级标志没清干净、APP区校验不过这类问题。
5. 避坑记录:IAP升级中那些一踩一个准的问题
5.1 跳转后立即HardFault
现象是升级流程全部走完,设备也复位了,但程序一跑就进HardFault,调试器挂上去看到PC指针停在0x00000000附近,或者停在未定义的中断向量地址。
原因十有八九是APP的中断向量表没重映射。APP的代码编译地址是0x08004000,但VTOR寄存器还指在0x08000000,中断一来CPU去Bootloader的向量表里找中断函数,找到的函数地址和APP期待的不对应,直接炸。
解决方法是检查两处:一是system_gd32e230.c里的VECT_TAB_OFFSET宏是否改成0x4000,二是在APP的main函数第一行写上SCB->VTOR = 0x08004000 & 0x1FFFFF80u;。我用这个方法解决了这个频发问题。
5.2 Flash写入后读回来全是0xFF
现象是升级过程没有报错,每帧都回ACK,但升级完跳转失败,调试器读APP区Flash全是0xFF,固件压根没写进去。
原因通常是写入前漏了擦除。Flash只能把1写成0,不能把0写成1,不擦除直接写等于大部分位写不进去,写入函数可能根本没报错。另一个常见原因是擦除后没有清FMC标志位,导致后续编程操作被错误的状态位拦截。
解决方法是升级流程开始时先对整个APP区做一次完整擦除,每页擦除后清一次END和WPERR标志位。写入函数里不要依赖返回值判断是否成功,写完后再读一遍Flash逐字节比对,不一致就回NAK重发当前帧。
5.3 升级到一半看门狗复位
现象是升级大固件时,擦写Flash耗时太长,看门狗在擦写过程中超时复位,升级被迫中断,APP区被写了一半。
原因很直接:擦写Flash期间CPU被Flash控制器占用,看门狗喂狗代码根本执行不到。我最初喂狗放在每帧收完之后的处理函数里,但一帧1024字节的固件在4KB页擦除时,Flash擦除耗时接近毫秒级,如果看门狗超时周期设得太短就会触发复位。
解决方式有两种。第一种是升级期间关闭看门狗,升级完成跳转APP后由APP重新初始化看门狗,适合不要求强制看门狗的产品。第二种是我最终采用的方案:把看门狗超时时间设成2秒以上,喂狗放在每个数据帧的CRC校验通过之后,确保任何时候喂狗周期都比擦写耗时长。同时升级开始前先擦完整个APP区,后续只写不擦,把单帧处理时间进一步压短。
5.4 DMA循环接收导致第二帧数据错位
现象是升级小固件时一切正常,升级超过2048字节的大固件时,第二帧数据丢了一半,解析返回错误码-3。
原因是DMA缓冲区只有2048字节,工作在循环模式。第一帧1024字节写完后停在缓冲区前半段,第二帧数据到达时DMA继续往后写,如果两帧间隔时间太长,串口空闲中断把第一帧处理完时DMA已经写入了第二帧的部分数据,缓冲区里出现了两帧数据的拼接残影。
解决方法是串口空闲中断触发后,先把DMA通道失能,再从缓冲区读取长度和内容,解析完重新配置DMA并启用。这样保证每一帧处理期间不会有新数据混入缓冲区。另外帧头同步也不能省,解析时先找SOH或者STX字符,找不到就丢弃缓冲区开头若干字节再尝试,提升抗干扰能力。
5.5 SecureCRT发送YModem总是没反应
现象是网上教程用SecureCRT传YModem很顺,我这里发出去Bootloader没有任何回应,甚至直接收到一串乱码。
原因是SecureCRT的终端模式默认会做本地回显和换行转换,文件传输时会把\r和\n改写,YModem的二进制帧被污染了。另一个原因是在发送前勾选了“发送前擦除Flash”之类的选项,直接发指令给Bootloader,但我们的Bootloader只认YModem协议,这两个功能对不上。
解决方法是SecureCRT里把终端协议改成“无”,关闭本地回显和换行转换,文件传输协议选“YModem”,确认对话框里取消所有附加选项。如果还不行,就别跟SecureCRT较劲了,用前面的Python脚本最省事,你能完全控制每一帧数据内容。
6. 二次开发思路:从串口IAP扩展到带校验的双备份升级
6.1 传输层之上加一层CRC32校验
YModem本身的CRC16只能检错不能纠错,对消费级产品够用,但如果升级的是医疗或工业设备,我建议外挂一层CRC32。具体做法是把固件文件末尾追加4字节CRC32值,上位机发送完YModem数据后再发一帧特殊格式的CRC信息帧,Bootloader写完固件后对整个APP区做CRC32计算,比对通过才置升级完成标志。这样把YModem的逐帧校验提升到整包校验,能挡住“每帧都对但帧顺序被打乱”的极端情况。实现CRC32时注意用查表法,Bootloader里RAM和Flash都够用,一张256项的查表放Flash就行。
6.2 双Bank分区与回滚机制
量产设备最怕升级失败变砖,尤其是现场断电不可控的情况。把APP区一分二是我现在做产品时的标配:原40KB APP区拆成两个16KB Bank,留下16KB作为固件暂存区。升级逻辑改成先写备份Bank,全部校验通过后原子更新启动标志,下次开机引导到新Bank。如果新固件启动后看门狗没有被及时喂狗,Bootloader在下次复位后发现运行标志不对,自动回退到旧Bank。
双Bank的回滚判定代码不复杂,核心是维护一个运行计数:
#define BANK_A 0x08004000u #define BANK_B 0x08008000u uint8_t app_valid(uint32_t addr) { uint32_t sp = *(volatile uint32_t *)addr; uint32_t pc = *(volatile uint32_t *)(addr + 4); return (sp & 0xFFF00000) == 0x20000000 && pc >= addr && pc < addr + 0x4000; } uint32_t select_boot_bank(void) { if (run_failed_flag && app_valid(BANK_A)) { return BANK_A; } if (upgrade_flag == MAGIC_NEW_BANK && app_valid(BANK_B)) { return BANK_B; } return app_valid(BANK_A) ? BANK_A : BANK_B; }run_failed_flag由APP在启动阶段写入标志区,如果新固件连续启动三次都没能正常运行,Bootloader就强制切回旧Bank。注意Bank大小要跟Flash页大小对齐,我这颗4KB一页,16KB正好4页,擦写和地址计算会很干净。
这套方案的代价是APP可用空间砍半,如果固件体积大,考虑用压缩率高的差分升级,比如对比新旧固件只传差异块。不过那种方案复杂度暴涨,对现场传输链路的要求高,大多数场景双Bank足够。
6.3 把传输通道从串口抽象出来
如果后续要支持CAN或者蓝牙升级,不要在各处直接调串口函数。我在这个工程里把发送和接收抽象成两个弱函数:transport_send(uint8_t*, uint16_t)和transport_recv(uint8_t*, uint16_t),YModem解析层只调这两个接口。串口实现、CAN实现各自维护自己的缓冲区和中断逻辑,切换传输方式时上层协议代码一行不用改。蓝牙升级还能顺带把固件下到手机App上,现场维护体验提升一大截。
做完这套改造之后,我每次拿到新项目第一件事就是先想清楚升级失败的最坏后果:是拆壳重新烧,还是能远程一把救回来。这个单片机自动升级程序是从拆壳烧录的坑里爬出来的产物,从那以后我每次做Bootloader都强制走一遍空片启动、正常升级、升级中断、断电重启四条验证路径,确认每个环节都有兜底才敢交付。希望帮到你。
本文还有配套的精品资源,点击获取