搞了两个月 STM32F407 的串口 Bootloader,总算把链路里所有坑都踩平了。这篇文章就是我的完整实操备忘,把硬件接线、FLASH 分区、跳转细节、自定义串口协议、上位机发送、还有我实际遇到的几个疑难故障全部记录下来。如果你正准备给 F407 或者说整个 F4 系列加一个串口升级能力,这份记录应该能帮你少走不少弯路。
先说明一下适用范围。F407 的 FLASH 是 1MB,主频 168MHz,片上 SRAM 有 128KB 加 4KB 备份 RAM。这套方案不依赖外部 Flash,也不用 RTOS,Bootloader 就是一个裸机工程,配一个串口接收就够。简单说,我把“Bootloader 占用 64KB + App 从 0x08010000 开始 + 串口 Ymodem 协议 + 上电超时等待升级指令”这套组合跑通了。实际操作部分我会把每一步都写出来,包括 Keil 工程里 IROM 地址怎么改、bin 文件怎么自动生成、上位机怎么发送、以及跳转 App 时最容易翻车的几个点。
1. 为什么要做串口 Bootloader:应用场景与方案选型
1.1 现实痛点:没有 Bootloader 的升级有多痛苦
我之前做过一个放在机柜里的采集设备,用的就是 F407。外壳封装好之后,每次想改代码都得把设备拆开,用 J-Link 或者 ST-Link 接到 SWD 口重新烧。问题在于这类设备经常是装在高处或者密封环境,拆一次外壳要动螺丝、拔线、重新打密封胶,来回折腾不仅耽误时间,还有可能把板子上的连接器弄松。
真正让我下决心做 Bootloader 的是产品迭代比较密集的那两三个月。功能要加、Bug 要改,现场设备又不止一台,靠人工拆壳烧录根本不现实。串口 Bootloader 解决的就是这个核心问题:通过一个预留的调试串口,让不拆机、不接仿真器的情况下直接更新固件,也就是 OTA 的本地串口版。
这个需求在工业设备、仪器仪表、消费电子开发调试阶段都特别常见。哪怕你不做量产,光是自己调试阶段,能用串口直接刷 App,也能省下大量插拔仿真器的时间。
1.2 方案横向对比:几类常见升级方式
做串口升级之前,我把常见的实现方案都过了一遍,各有适用场景。
| 方案 | 实现难度 | 是否拆机 | 速度 | 适用场景 |
|---|---|---|---|---|
| SWD 仿真器烧录 | 低 | 是 | 快 | 开发调试、产线出厂 |
| 芯片出厂系统 Bootloader(串口) | 低 | 否 | 中等 | 早期烧录,不能定制协议 |
| 自制 App Bootloader(串口/Ymodem) | 中 | 否 | 中等 | 现场升级、量产维护、二次引导 |
| 以太网 / CAN / USB 升级 | 高 | 否 | 快 | 产品本身带通信接口时 |
芯片出厂自带的系统 Bootloader 虽然也能用串口烧程序,但它有几个限制:第一,需要拉 BOOT0 到高电平再复位,对现场人员不太友好;第二,它的通信协议是 ST 自定义的,工具链固定;第三,它只负责把数据写进 Flash,做不了版本校验、A/B 备份、断电续传这些更灵活的事情。自己做 Bootloader,说白了就是把这些控制权全部拿回来。
1.3 为什么最终选择“串口 + 自制协议”
我的场景对升级工具的要求很简单:一个串口调试助手或者一段 Python 脚本就够,不依赖任何第三方烧录软件。所以我选择了“自制 Bootloader + Ymodem 协议”这套组合。Ymodem 的优势是包格式成熟、自带 CRC 校验、支持文件名传输和批量发送,网上资料也很多。我不想自己造一套完整协议轮子,基于 Ymodem 做裁剪是最平衡的选择。
如果你后面想做得更简单,也可以直接改成“自定义帧头 + 长度 + 数据 + 累加和”的裸协议。我最初就是这么做的,但后来发现一旦要传大文件、做断点续传,还是 Ymodem 这类现成协议更省事。选型的核心思路是:优先复用成熟协议,再针对自己的硬件做裁剪。
2. Bootloader 核心原理:FLASH 分区、跳转与串口协议
2.1 FLASH 分区与地址规划
F407 的 FLASH 从 0x08000000 开始,大小 1MB,分成 8 个扇区。前 4 个扇区每个 16KB,第 5 个扇区 64KB,后面 3 个扇区每个 128KB。这个扇区分布对 App 起始地址的选择有直接影响,因为擦除是按扇区来的,App 首地址最好落在扇区边界上。
我用的分区方案:
| 地址范围 | 大小 | 用途 |
|---|---|---|
| 0x08000000 - 0x0800FFFF | 64KB | Bootloader(实际只用了不到 20KB) |
| 0x08010000 - 0x0807FFFF | 448KB | App 主程序区 |
| 0x08080000 - 0x0809FFFF | 128KB | App 备份区 / 参数存储区(可选) |
| 0x080A0000 - 0x080FFFFF | 384KB | 预留 |
选 0x08010000 作为 App 起始地址,一方面是因为它正好落在扇区 4 的起始位置,擦除时不用把 Bootloader 所在的前 4 个扇区牵扯进来;另一方面,给 Bootloader 留 64KB 绝对够用,哪怕以后要加驱动、加通信协议也不会紧张。实际编译下来,我的 Bootloader 加 Ymodem 实现、串口驱动、Flash 擦写逻辑,总共不到 20KB。
App 侧要注意,编译器里链接地址必须改成 0x08010000,中断向量表偏移也要同步设置。这块我在第 3 节实操部分会详细讲,这里先记住一个原则:Bootloader 和 App 必须对“Flash 怎么分”达成一致,各用各的区间,谁也别跨界。
2.2 从 Bootloader 跳转到 App 的关键动作
跳转是整个 Bootloader 里最核心、最容易出错的一步。很多人写的跳转代码“能跑”,但实际上隐藏着各种隐患。标准的跳转流程包含三个关键动作:
第一,校验 App 起始位置的合法性。App 的起始地址处存放的应该是栈指针值,紧接着的 4 字节是复位处理函数的地址。F407 的 SRAM 起始地址是 0x20000000,Flash 起始地址是 0x08000000,所以可以靠这两个值做一次快速判断,防止跳到空白 Flash。
第二,清理现场。跳转前要把全局中断关掉,把用到的串口外设反初始化,把 SysTick 关掉,把 PendSV、SVC 这些系统异常挂起标志清掉。否则跳过去之后,App 在初始化中断时可能撞上旧的异常状态,直接进 HardFault。
第三,重设栈指针并跳转。先读 App 起始地址处的 4 字节作为新的 MSP,然后调用 App 的复位处理函数。这个过程可以用函数指针实现。
我实际的跳转代码长这样:
#define APP_ADDR 0x08010000U #define APP_MSP (*(volatile uint32_t *)APP_ADDR) #define APP_RESET_HANDLER (*(volatile uint32_t *)(APP_ADDR + 4)) typedef void (*pFunction)(void); static void do_jump_to_app(void) { uint32_t app_msp = APP_MSP; uint32_t app_entry = APP_RESET_HANDLER; // 校验 MSP 落在片内 RAM 范围,入口地址落在 Flash 范围 if ((app_msp & 0x2FFE0000) != 0x20000000) { // 参数异常,禁止跳转 return; } if ((app_entry & 0x2FFE0000) != 0x08000000) { return; } __disable_irq(); HAL_UART_DeInit(&huart1); // 反初始化串口,避免跳转后串口中断残留 HAL_Delay(10); __set_MSP(app_msp); // 先设置栈指针 pFunction jump = (pFunction)app_entry; jump(); // 再跳转到 App 复位向量 }这段代码里最关键的就是跳转前“关中断 + 反初始化外设”。我最早写 Bootloader 时省了这一步,结果跳转后 App 里创建的工程任务一堆 HardFault,排查了很久才发现是串口中断残留导致的。
2.3 串口协议与握手流程设计
我之前提到采用 Ymodem,但实际落地时做了一点调整。Ymodem 的完整实现里面有 128 字节包、1024 字节包、EOT、ACK/NAK 等状态,代码量比较可观。这里给出两个方向:
如果你要的是“能跑、好调试、协议透明”,建议直接先实现一个简化版的自定义协议。每个数据包的格式定义为:
包结构:帧头(0x5A 0xA5) + 类型(1字节) + 长度(2字节) + 数据 + CRC16(2字节)类型分为握手、开始传输、数据帧、结束帧、应答、错误请求。上位机按固定节奏发,Bootloader 收到一帧就回一个 ACK,收到错误帧回 NAK。这套协议虽然简单,但足够覆盖“写入 Flash + 校验 + 跳转”的全部流程。它的最大好处是任何串口调试助手都能手工发包,对排查问题非常有帮助。
如果你的目标是传输大文件、要求可靠和效率,直接上 Ymodem 更合适。Ymodem 有现成的开源实现,移植到 F407 上主要工作是拆包、组包、CRC16 校验和 Flash 写操作。我用的是 Ymodem 的 1024 字节变体,传输 100KB 级别的 App 镜像,115200 波特率下大概 10 秒左右,完全可接受。
握手流程我采用的是“超时等待型”。Bootloader 启动后先初始化串口,然后等待 2 秒。如果上位机在这 2 秒内发送同步字节 0xAA 0x55,就进入升级模式;否则直接跳到 App。这样每次复位后,正常运行时不会卡在 Bootloader 里,需要升级时也不需要额外按键。
个人经验,这个 2 秒等待时间比较重要。设 2 秒,设备上电到进入 App 大约 2.1 秒,大多数现场用户能接受。如果你希望上电速度更快,可以把等待缩短到 500ms,但上位机必须在复位瞬间立刻发同步字节,否则容易错过窗口。
3. 实操全过程:从接线到烧录再到升级验证
3.1 硬件准备与接线要点
做这套实验前,你需要准备的东西:一块 STM32F407 开发板(某公司标准核心板就行,不一定非要原厂)、一个 USB 转 TTL 模块、几根杜邦线。如果你板子上有板载调试器并且引出了调试串口,那会更方便。
接线非常简单,用 USART1:
| 开发板引脚 | 连接 USB 转 TTL 模块 |
|---|---|
| PA9(USART1_TX) | RX |
| PA10(USART1_RX) | TX |
| GND | GND |
三个必须注意的地方:
第一,TX 和 RX 必须交叉连接。开发板的 TX 要接模块的 RX,开发板的 RX 接模块的 TX。接反的典型表现是:串口调试助手能打开端口,但完全收不到开发板发送的数据。
第二,必须共地。USB 转 TTL 模块的 GND 要和开发板的 GND 接到一起,否则电平参考点不一致,会出现偶发乱码和通信失败。
第三,电平必须匹配。F407 的串口 IO 是 3.3V 电平,USB 转 TTL 模块选 3.3V 的,不要用 5V 输出的型号。5V 电平直接灌到 PA9/PA10 上,短时间可能没事,但长期使用有烧引脚的风险。
开发板如果引出的是 TTL 串口,直接这样接就行。如果只有一个 USB 口并且没有 TTL 引出,通常需要看原理图找到 UART1 对应的排针。不同开发板引脚位置不同,画了表格也未必准,所以这一点建议自己对照板子原理图确认。
3.2 Keil 工程配置:App 起始地址和 bin 文件生成
这一步非常关键,配置错一个地方,跳转就失败。工程我是在标准库 + HAL 库两种方式下都验证过,配置逻辑完全一样。
先看 Bootloader 工程。Bootloader 工程本身不需要特殊设置,链接地址保持默认(从 0x08000000 开始)就行。唯一要注意的是,Bootloader 和 App 在同一个工程里无法共存,所以建议建两个独立工程。
再看 App 工程。进入 Keil 的 “Options for Target” → “Target” 选项卡,修改 IROM1:
IROM1: Start 0x08010000 Size 0x70000这里的 Size 是按 448KB 算的。如果你完全从 0x08000000 写到 0x080FFFFF 是 1MB,但 App 区起点偏移了 64KB,所以可用空间是 0x70000。如果你的分区方案和我不一样,这里的 Size 也要跟着改。
还要在“C/C++”选项卡里给预处理宏加上 VECT_TAB_OFFSET。以标准固件库为例,在 stm32f4xx.h 里配置中断向量表偏移:
#define VECT_TAB_OFFSET 0x10000U如果是 HAL 库,通常在 system_stm32f4xx.c 里直接改这个宏。它的作用是把 NVIC 的中断向量表从默认的 0x08000000 移到 0x08010000。这一条绝对不能省。否则 App 一旦有任何中断发生,CPU 会去 0x08000000(也就是 Bootloader 区域)取中断向量,轻则行为错乱,重则 HardFault。
生成 bin 文件,我在 Keil 里配置了 User 选项卡的 After Build 命令:
fromelf.exe --bin -o "./Build/app.bin" "./Build/app.axf"这样每次编译完自动输出 app.bin,供串口发送使用。
3.3 Bootloader 烧录与首次启动验证
Bootloader 工程编译通过后,先用仿真器把它烧到 0x08000000。这一步是一次性的,之后所有 App 更新都可以走串口。烧录方式无所谓,J-Link、ST-Link、OpenOCD 都可以,只要地址对得上。
烧完 Bootloader 之后,我把 App 也先通过仿真器烧到 0x08010000。这么做是为了先验证“Bootloader 上电超时后能正确跳转”,把串口升级这个变量留到最后再引入。
这时候拨动或者短接复位脚,Bootloader 启动后如果串口接好了,你会在调试助手里看到一段提示文字,比如:
Bootloader V1.0 Waiting for update command...等 2 秒没有同步指令,开发板就会执行跳转,此时串口应该会再打印 App 启动信息。如果你能看到这行打印,说明整套方案的“链接地址、向量表偏移、跳转函数”这三项已经全部通过验证,接下来就可以开始真正用串口升级了。
3.4 串口升级完整操作流程
以 Ymodem 方式为例,我用的是 Tera Term 的 Ymodem 发送功能。你也可以用 SecureCRT 或者自写 Python 脚本。操作流程是这样的:
Bootloader 和 App 都烧好之后,在 Tera Term 中打开串口,设置波特率 115200,数据位 8,停止位 1,无校验。然后按下开发板的复位键,当调试助手界面出现“Waiting for update command...”时,立即从菜单选择“File” → “Transfer” → “YMODEM” → “Send”,选中 app.bin 文件。
此时 Bootloader 会收到同步握手信息并进入升级模式。在 115200 波特率下传输一个 100KB 左右的 bin 文件,大约需要 10 到 15 秒。传输过程中 Bootloader 会边收边擦写 Flash,收完后自动进行 CRC 校验,校验通过后自动跳转到新固件。
如果你用的是 Python 自写脚本,核心逻辑只需要用 pyserial 发送同步字节 0xAA 0x55,然后等待 ACK,再按包发送数据。下面是一个极简的发送脚本示例:
import serial import time ser = serial.Serial('COM3', 115200, timeout=1) ser.write(b'\xAA\x55') # 发送同步字节 with open('app.bin', 'rb') as f: data = f.read() seq = 0 for i in range(0, len(data), 256): chunk = data[i:i+256] frame = b'\x5A\xA5' + bytes([0x01]) + len(chunk).to_bytes(2, 'little') + chunk # 简化起见,降 CRC 暂不加入,实际工程一定要加 ser.write(frame) ack = ser.read(1) if ack != b'\x06': # ACK print('packet failed at', i) break seq += 1这段脚本只展示了发送框架,实际产品级别必须补上 CRC 校验和重传机制。
如果你不想写任何上位机,又觉得 Tera Term 不够自动化,另一个选择是直接使用 STM32CubeProgrammer 的 UART 模式。不过注意,CubeProgrammer 连的是芯片出厂自带的 System Bootloader,前提是 BOOT0 拉高。这跟自己写的串口 Bootloader 是两个方向的手段,看你自己习惯用哪个。
3.5 自动生成版本信息和升级校验
工程体验中很容易忽略的一点是:bootloader 在升级前应该显示当前 App 的版本号。我在 App 的固定地址处放了一个版本结构体,Bootloader 跳转前会读出来打印:
typedef struct { uint32_t magic; uint8_t major; uint8_t minor; uint8_t patch; uint32_t build_time; } app_version_t;地址选在 App 向量表之后的一段固定偏移位置,比如 0x08010080。这样 Bootloader 在跳转之前就能知道当前版本,上位机升级前也能核对版本号,避免发错文件覆盖出新版本。这个做法看着不起眼,但在量产维护和多人协作时非常有用,因为现场如果有人把旧版固件覆盖到新设备上,版本信息会立刻暴露问题。
4. 踩坑实录:常见问题与排查技巧
这部分是我认为整篇里“真金白银”的内容。Bootloader 网上资料一大把,但真正跑起来全在细节里翻车。
4.1 跳转后进 HardFault 或者干脆没反应
这是所有做过 Bootloader 的人一定会遇到的现象。常见原因有三个:
一是 App 的向量表偏移没改。解决办法是确认 VECT_TAB_OFFSET 等于 0x10000,且宏在实际编译中生效。
二是跳转前没有关闭中断。我之前遇到的现象是:Bootloader 正常打印“Jump to App”,然后设备死掉。排查时发现是串口收到了最后一个字节,触发了串口中断,而这个中断响应函数是在 Bootloader 里注册的。跳转到 App 后,App 的中断向量表还没完全初始化,这时一个残留的串口中断请求进来,CPU 找不到对应处理函数,直接 HardFault。解决方法是跳转前调用 HAL_UART_DeInit,再补一条 __disable_irq 保险。
三是栈指针或者入口地址校验太宽松。我见过有人直接读 APP 地址的前 4 字节,不管内容是什么就设置 MSP,结果 Flash 完全空白时程序也跑去执行,必然死机。加了校验之后,空白区会被提前拦截。
4.2 串口能收到数据但全部是乱码
我调试过程中曾花了一个下午,把波特率换来换去,最后发现根本不是波特率问题,而是 RX/TX 接反了。USB 转 TTL 模块如果支持交叉线开关,拨错一个开关也会乱码。建议排查顺序是这样的:先测模块驱动是否正常、再用回环方式测串口是否通、然后检查接线顺序、最后核对波特率是否和代码里的配置一致。
115200 下如果代码配置的是 8N1,而上位机选成 7E1,也会出现一种特有的“能识别几个字符但整体乱”的现象。遇到这种情况我一般是直接数上位机和代码里的数据位、校验位、停止位,逐项对齐。
4.3 串口收包不完整或者校验失败
这个坑在 Ymodem 传输时特别容易发生。现象是升级一半弹出校验错误,或者上位机报超时重传。
从 Bootloader 侧看,接收数据用中断逐字节收时,如果处理中断的程序比较重,比如每收一个字节就去读一次 Flash 做校验,就会丢失下一个字节。正确做法是用 DMA 接收 + 空闲中断,或者至少用 FIFO 缓冲区做着包解析。以 115200 波特率计算,每个字节大约 87 微秒,中断服务函数耗时必须严格控制在这个范围内。
从协议侧看,很多自定义协议只做累加和,不做 CRC。累加和在数据流出错时经常算不出来,尤其是有符号数参与的时候,很容易出现“校验本身没问题但数据是错的”。尽量用 CRC16,成本低可靠性高。
4.4 擦写 Flash 时死机或者数据错位
F407 的 Flash 编程有几个硬性要求,违反就直接死:
第一,擦除和编程操作必须解锁 FLASH,操作完后上锁。
第二,编程单位是双字,也就是 8 字节。数据缓冲区地址也必须是 8 字节对齐的,否则 HAL_FLASH_Program 会报错。很多人的数据缓冲区是在结构体里定义的,编译器默认只按 4 字节对齐,这时候需要用attribute((aligned(8))) 强制对齐。
第三,擦除扇区时不能擦到当前正在执行的代码区。Bootloader 在 0x08000000 扇区,App 在 0x08010000 扇区,Bootloader 运行中擦除 0x08010000 及后面的扇区没问题。但如果你把擦除范围算错,把 Bootloader 自己所在的扇区也擦掉了,程序会当场跑飞。
这里贴一个我在工程里验证过的擦写函数:
static void flash_erase_app(uint32_t addr, uint32_t size) { FLASH_EraseInitTypeDef erase_cfg; uint32_t sect = get_flash_sector(addr); uint32_t sect_end = get_flash_sector(addr + size - 1); uint32_t num = (sect_end - sect) + 1; uint32_t error = 0; HAL_FLASH_Unlock(); erase_cfg.TypeErase = FLASH_TYPEERASE_SECTORS; erase_cfg.Sector = sect; erase_cfg.NbSectors = num; erase_cfg.VoltageRange = FLASH_VOLTAGE_RANGE_3; HAL_FLASHEx_Erase(&erase_cfg, &error); HAL_FLASH_Lock(); }地址换算扇区时,F407 的扇区大小不规律,我建议写一个专用的查表函数,不要做连续的除法计算。比如:
static uint32_t get_flash_sector(uint32_t addr) { if (addr < 0x08004000) return FLASH_SECTOR_0; if (addr < 0x08008000) return FLASH_SECTOR_1; if (addr < 0x0800C000) return FLASH_SECTOR_2; if (addr < 0x08010000) return FLASH_SECTOR_3; if (addr < 0x08020000) return FLASH_SECTOR_4; if (addr < 0x08040000) return FLASH_SECTOR_5; if (addr < 0x08060000) return FLASH_SECTOR_6; return FLASH_SECTOR_7; }这个函数在讲解时有不错的教学价值,也能避免很多人以为 F407 扇区是均分 128KB。
4.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 跳转后 HardFault | 向量表偏移未设置 | 检查 VECT_TAB_OFFSET |
| 跳转后死机 | 串口中断残留 | 跳转前 DeInit 外设并关中断 |
| 上电后无任何输出 | TX/RX 接反 | 交换线序,测回环 |
| 串口乱码 | 波特率、校验位、停止位不匹配 | 逐项比对上位机与代码配置 |
| 升级过程中校验失败 | 未做 CRC 或数据缓冲未对齐 | 增加 CRC16,强制 8 字节对齐 |
| 擦除时程序跑飞 | 擦除范围跨到 Bootloader 区 | 检查扇区查表函数 |
| 发送后 APP 没更新 | bin 文件配置错误,仍是从 0 开始 | 确认 Keil IROM 和 fromelf 输出 |
5. 进阶扩展思路:从“能用”到“好用”
5.1 升级标志位与失败回滚
如果你只是自己调试用,上面这套加上“上电 2 秒等待”就足够了。但如果给量产设备用,我建议再加一个升级标志位:App 收到升级指令后,先在 Flash 参数区写入“待升级”标志,然后软复位,Bootloader 检测到这个标志后直接进入升级模式,不需要用户掐时间发同步字节。
升级完成并且校验通过后,把标志清掉。如果升级失败,Bootloader 在超时后可以先检查 App 区域是否有效,如果发现 App 校验失败且备份分区存在可用固件,就自动从备份分区恢复。这一步把“变砖”的概率降到很低,是产品级 Bootloader 和玩具级 Bootloader 的分水岭。
5.2 升级过程中的断电保护
串口升级最怕的是升级一半断电。Flash 写了一半,App 区域就处于一种“既不是旧程序也不是新程序”的状态。我实测下来的经验是:写入 Flash 前先擦掉整个 App 区,然后逐帧写入,写完立刻校验整个区域,全部通过后,Bootloader 才允许跳转。如果中途断电重启,Bootloader 先校验 App 区,发现无效则停留在等待升级状态,不会跳到残缺的 App。
这个流程看起来简单,但实际项目里能救回好几台设备。
5.3 上位机工具的选择
我自己的实际经验是,调试阶段用 Tera Term 就够了,因为它的 Ymodem 发送稳定可靠,界面直观。但到了要给同事或者产线用的阶段,Tera Term 这种通用工具不太合适,因为发送哪个文件完全靠人工选择,容易选错。
我后来用 Python 写了一个小的发送脚本,配合 pyinstaller 打包成 exe。界面上只有四个东西:串口号下拉框、bin 文件选择框、升级按钮、日志输出区。同事使用起来零学习成本。脚本内部做的事就是把 0xAA 0x55 同步字节发出去,然后用 Ymodem 发送指定文件,并在日志里显示校验结果。
这个脚本的逻辑核心并不复杂,难的是处理好“发送超时重传”和“检测 Bootloader 回应”。如果你不太想折腾,也可以考虑用一些现成的开源串口工具,开启 Ymodem 后手动发送。
5.4 通讯方式扩展
串口 Bootloader 拿到手以后,你会发现它的核心思想其实是“从某个通道接收数据,写入 Flash,然后跳转”。如果把串口换掉,这套逻辑完全可以复用。我在这套方案跑通之后,又花了一天时间,把同样的逻辑移植到了 CAN 通道上,上位机从一个 CAN 盒发送 bin 文件,设备通过 CAN Bootloader 升级,效果同样可行。
以太网、USB 也是同理,和 Bootloader 的核心逻辑解耦之后,剩下要做的事情就是改一下传输层的驱动函数。这也是为什么我在设计协议时比较强调“把传输层和 Flash 操作层分开”,因为这样可以极大减少后续扩展成本。
6. 一点个人经验
整套串口 Bootloader 做完之后,我最大的感受是:难点从来不在“怎么把数据写进 Flash”,而在于把这些临界点想清楚。比如升级断电怎么办、Flash 写一半失败怎么办、跳转时中断怎么处理、上位机和 Bootloader 的时间窗口怎么对齐。这些细节任何一个没处理到位,现场都会以一种极其难看的方式暴露出来。
如果你第一次做,我的建议是不要上来就搞 Ymodem + A/B 分区 + 加密校验,先做一个最简版本:上电等 2 秒,收到同步字节就接收数据,直接写 Flash,写完跳转。这个过程跑通之后,再逐步加入标志位、CRC 校验、备份回滚。
还有一个小技巧分享给你:开发阶段在 Bootloader 里多写一个“跳转前打印 App 首地址的 SP 和 PC 值”的调试函数。就是跳转前把要设置的 MSP 和函数指针打出来对比一下。很多时候问题一眼就能看出来:如果打印出来 SP 是 0xFFFFFFFF,说明 Flash 里压根没写东西,根本不用去查跳转函数。后来我在正式版本里把这个打印去掉了,但调试阶段它帮我省了至少两次“对着屏幕发呆二十分钟”的时间。