基于STM32F407的双分区Bootloader系统,包含Bootloader和配套应用程序,支持通过USB CDC接口进行安全可靠的固件升级。
本项目是一个完整的STM32F407固件升级解决方案,包含:
- Bootloader: 引导加载程序,负责固件升级和程序跳转
- Application: 主应用程序,与Bootloader配合实现远程升级(固件中自带了一个PB12的方波产生任务,只是用来调试,正式使用时可以关闭)
- ESP32-S3 OTA Host:ESP32-S3(Seeed XIAO ESP32S3)作为 WiFi 上位机(网页上传 bin),并作为 USB Host 通过 CDC-ACM 将固件下发给 STM32 Bootloader
这一套 OTA 的核心是:浏览器把 bin 上传到 ESP32-S3,ESP32-S3 把文件缓存到 SPIFFS,然后用 USB Host CDC-ACM 连接 STM32(CDC Device),按现有 Bootloader 协议(0xA5 0x5A 帧头 + Cmd_Updata_A + 512 分包 + ACK)完成升级。
- 浏览器/PC:访问 ESP32 网页并上传
.bin - WiFi 路由器/局域网:浏览器与 ESP32 的数据通道
- ESP32-S3(Seeed XIAO ESP32S3):
- WebServer:接收网页上传(HTTP)
- SPIFFS:缓存固件文件
- USB Host(CDC-ACM Host):连接 STM32 的 USB CDC 设备口
- STM32F407:运行 Bootloader/APP,通过 USB CDC 作为 Device 与 ESP32 通信
- (可选)USB-TTL:用于查看 ESP32 UART0 日志(避免依赖 USB 串口)
- 浏览器通过 HTTP 把
.bin上传到 ESP32(Web 表单上传)。 - ESP32 将固件写入 SPIFFS(作为临时缓存),并计算固件
size与CRC32。 - ESP32 切到 USB Host,枚举并打开 STM32 的 USB CDC(按 VID/PID 匹配)。
- ESP32 发送“启动升级”命令给 STM32 Bootloader:
[0xA5][0x5A][0x07][0x00] + size(LE32) + crc32(LE32)
- STM32 擦除 A 区 Flash,完成后回
ACK。 - ESP32 开始发送“裸 bin 数据流”(推荐 512 字节分包);STM32 每写完一包回
ACK。 - 传输结束后 STM32 进行 CRC 校验;通过后跳转到 A 区 APP。
- ESP32 通过
/status(JSON)向网页提供:USB 连接状态、升级进度、错误信息。
- 浏览器 ⇄ ESP32:HTTP 上传(multipart/form-data) +
/status轮询 - ESP32 ⇄ STM32:USB CDC(CDC-ACM)
- 控制帧:4/12 字节的
BootloaderPacket(0xA5 0x5A+ cmd/state + 可选 payload) - 数据帧:启动命令后直接发送“裸 bin 数据”,下位机用 ACK/NAK(端点 NAK)实现流控
- 控制帧:4/12 字节的
- ESP32-S3 的原生 USB OTG Host/Device 互斥:固件运行后启用 USB Host 时,PC 侧的 USB CDC 串口可能会消失(属于预期)。
- 如果需要在 Windows 下稳定烧录 ESP32:请让板子进入 ROM 下载模式 再执行
pio run ... -t upload(此时应用不运行,也就不会切 Host)。
My_Bootloader/:STM32 Bootloader 工程(USB CDC Device + 协议实现)My_APP/:STM32 应用工程ESP32_S3_OTA_Host/:ESP32-S3 OTA Host(网页上传 + USB CDC Host 下发)
相关文档入口:
- ESP32-S3 使用说明:ESP32_S3_OTA_Host/Use.md
- Bootloader 协议细节:My_Bootloader/通信协议.md
- 上位机使用说明(PC Python/ESP32 两套):My_Bootloader/USAGE.md
为避免量产/现场刷屏,本工程默认尽量“静默”。需要调试时再打开对应宏。
-
串口调试总开关:
STM32_SERIAL_DEBUG_ENABLE- 位置:My_Bootloader/Core/Inc/serial_debug.h
- 默认:
0(关闭) - 打开方式:
- 直接改文件里的宏为
1,或 - 在 Keil 工程里添加编译宏:
STM32_SERIAL_DEBUG_ENABLE=1
- 直接改文件里的宏为
-
USB Device 库的
USBD_UsrLog/ErrLog/DbgLog:当前也被STM32_SERIAL_DEBUG_ENABLE额外门控- 位置:My_Bootloader/USB_DEVICE/Target/usbd_conf.h
- 说明:即使将
USBD_DEBUG_LEVEL调高,如果STM32_SERIAL_DEBUG_ENABLE=0仍然不会输出。
-
ESP32 工程日志总开关:
ESP_SERIAL_DEBUG_ENABLE- 位置:ESP32_S3_OTA_Host/src/config.h
- 作用:控制工程代码里
ESP_LOGx包装日志是否输出。
-
升级阶段 USB 原始字节流打印:
USB_RAW_RX_LOG_DURING_UPGRADE- 位置:ESP32_S3_OTA_Host/src/config.h
- 默认建议关闭:打开后如果下位机持续输出文本,日志会非常密集,可能拖慢任务甚至触发复位。
目标:仅更换 STM32 型号/Flash 布局,ESP32 侧的协议、命令码、状态机、分包(512)、ACK 逻辑全部保持不变。
这意味着 STM32 侧必须继续满足:
- USB CDC 枚举正常(CDC Device)
0xA5 0x5A协议帧不变,命令码不变(例如Cmd_Updata_A=0x07)- 升级流程不变:先发启动命令(size+crc)→ 再发“裸 bin 数据流” → 每包写完回
ACK
- Flash 分区定义(A/B/Bootloader 地址与大小)
- Bootloader 分区宏入口: My_Bootloader/Core/Inc/Bootloader.h
- APP 分区宏入口: My_APP/Core/Inc/APP_Base.h
- 需要按目标芯片 Flash 容量与擦除粒度重新规划并修改:
BOOTLOADER_START_ADDR / BOOTLOADER_END_ADDR / BOOTLOADER_SIZE
APP_A_START_ADDR / APP_A_END_ADDR / APP_A_SIZE
APP_B_START_ADDR / APP_B_END_ADDR / APP_B_SIZE
FLASH_TOTAL_SIZE重要约束:三段区域不能重叠;起止地址需要对齐到“最小擦除单位”(页/扇区)。
- Bootloader 自身链接区域(防止代码落到分区外)
- Keil scatter: My_Bootloader/MDK-ARM/My_Bootloader/My_Bootloader.sct
- 如果调整了 Bootloader 区大小,必须同步修改
LR_IROM1的长度,确保与BOOTLOADER_SIZE一致。
- APP 工程的链接地址(必须与 A 区一致)
- ESP32 下发的固件是“写入 A 区并从 A 区启动”的
.bin。 - 因此 APP 工程必须链接到新的
APP_A_START_ADDR(向量表起始地址一致),否则写入成功也无法正常跳转运行。
- APP(A 区)备份到 B 区逻辑适配(必须同步移植)
- 本项目的“备份到 B 区”逻辑在 A 区 APP 内实现,而不是在 Bootloader 内实现。
- 若仅修改 Bootloader 的分区/擦写逻辑,而未同步修改 APP 的备份实现,则可能出现:备份擦写越界、误擦 Bootloader、备份失败导致无法回滚等问题。
- 相关实现位置:
- 分区宏与声明: My_APP/Core/Inc/APP_Base.h(例如
APP_A_START_ADDR/APP_B_START_ADDR/CopyAtoB()) - 备份实现: My_APP/Core/Src/APP_Base.c(
CopyAtoB()及擦除/写入/CRC 相关代码)
- 分区宏与声明: My_APP/Core/Inc/APP_Base.h(例如
- Flash 擦除/写入实现适配目标芯片的 Flash 几何
- 当前工程在 My_Bootloader/Core/Inc/Bootloader.h 中写死了 F407 的
SECTOR_SIZE_0..7。 - 换到其它系列时(F1/G0/L4/H7 等),擦除粒度可能变成“页擦除”或“不同扇区布局”,需要把“擦除 A 区”的实现改成适配目标芯片:
- 能正确覆盖
APP_A_START_ADDR..APP_A_END_ADDR - 绝不能擦到 Bootloader 区或越界
- 写入对齐/最小写入单位满足目标 Flash 要求
- 能正确覆盖
- 跳转到 APP 的地址与向量表处理
- 跳转时必须基于新的
APP_A_START_ADDR读取向量表(MSP/Reset_Handler)。 - 如工程使用
SCB->VTOR,也必须指向新的 A 区起点。
ESP32 Host 侧是按 VID/PID 匹配设备的。如果不改 ESP32:
- STM32 侧请保持与当前一致的 VID/PID: My_Bootloader/USB_DEVICE/App/usbd_desc.c
如果愿意同步改 ESP32(不改协议也可以,只改枚举匹配),则 ESP32 侧在:
- ESP32_S3_OTA_Host/src/config.h(
STM32_USB_VID/STM32_USB_PID)
- 选定目标 STM32(Flash 总容量、擦除粒度),规划 Bootloader/A/B 三段地址。
- 修改分区宏: My_Bootloader/Core/Inc/Bootloader.h 与 My_APP/Core/Inc/APP_Base.h。
- 修改 My_Bootloader/MDK-ARM/My_Bootloader/My_Bootloader.sct 的 Bootloader 链接区域大小。
- 适配 Bootloader 的 Flash 擦除/写入实现,确保只作用于新的 A 区范围。
- 修改 APP 工程链接地址到新的
APP_A_START_ADDR,并适配 APP 的 A→B 备份实现(CopyAtoB()),重新生成.bin。 - 实机验证:升级一次(ESP32/PC 上位机均可)、CRC 通过、可跳转运行、断电重启仍正常。
- 双分区设计:A区为运行/更新区,B区为备份区
- USB CDC全双工通信:支持实时双向通信
- 智能Flash管理:仅擦除非FF扇区,提高效率
- CRC32校验:确保固件完整性
- 状态机控制:清晰的状态转换逻辑
- 看门狗集成:防止程序卡死
- 超时处理机制:接收bin文件时3秒超时处理,未接命令时15秒自动跳转
- 错误处理机制:完善的错误反馈
- 自动备份:启动时自动备份到B区(必须)
- 远程升级响应:可响应升级请求跳转到Bootloader(必须)
- 看门狗管理:持续喂狗保证系统稳定(必须)
- 状态监控:监控系统运行状态
- STM32F407VET6
- 512KB Flash
- 192KB SRAM
- USB接口
| 分区 | 起始地址 | 结束地址 | 大小 | 用途 |
|---|---|---|---|---|
| Bootloader | 0x08000000 | 0x08007FFF | 32KB | Bootloader程序 |
| B区(备份) | 0x08008000 | 0x0803FFFF | 224KB | B备份区域 |
| A区(运行+更新) | 0x08040000 | 0x0807FFFF | 256KB | A运行+更新区域 |
[魔数头(1字节)][魔数尾(1字节)][命令(1字节)][状态(1字节)][数据(...字节)]
- 魔数头:
0xA5 - 魔数尾:
0x5A - 命令: 指令码
- 状态: 当前运行状态
- 数据: 可选数据字段
| 命令码 | 十六进制 | 描述 |
|---|---|---|
| Cmd_Invalid | 0x01 | 无效命令/错误响应 |
| Cmd_ACK | 0x02 | 确认响应 |
| Cmd_NACK | 0x03 | 否认响应 |
| Cmd_Update_State | 0x04 | 更新状态 |
| Cmd_Jump_To_A | 0x05 | 跳转到A区 |
| Cmd_CopyB_To_A_Jump_To_A | 0x06 | 用B区覆盖A区并跳转 |
| Cmd_Updata_A | 0x07 | 升级A区固件 |
| 错误码 | 十六进制 | 描述 |
|---|---|---|
| ERROR_Flash_Erasure | 0x01 | Flash擦除错误 |
| ERROR_Flash_Download | 0x02 | Flash下载错误 |
| ERROR_Copy_B_To_A | 0x03 | B区复制到A区错误 |
| ERROR_Copy_A_To_B | 0x04 | A区复制到B区错误 |
| ERROR_Jump | 0x05 | 跳转错误 |
| ERROR_CRC | 0x06 | CRC校验错误 |
当发生错误时,Bootloader发送错误响应包:
- Flash下载错误:
[0xA5][0x5A][0x01][0x00][NULL](Cmd=ERROR_Flash_Download, State=Run_Invalid) - Flash擦除错误:
[0xA5][0x5A][0x01][0x00][NULL](Cmd=ERROR_Flash_Erasure, State=Run_Invalid) - 跳转错误:
[0xA5][0x5A][0x01][0x00][NULL](Cmd=ERROR_Jump, State=Run_Invalid) - CRC校验错误:
[0xA5][0x5A][0x01][0x00][CRC值(4字节LE)](Cmd=ERROR_CRC, State=Run_Invalid, Data=CRC值)
-
启动命令:
- 上位机发送:
[0xA5][0x5A][0x07][0x00][固件大小(4字节LE)][CRC32值(4字节LE)] - 其中
[0x07]表示Cmd_Updata_A命令 - 固件大小为32位小端序整数
- CRC32值为固件数据的CRC32校验值(32位小端序整数)
- 上位机发送:
-
Flash擦除:
- Bootloader接收到启动命令后,开始擦除A区Flash
- 擦除完成后发送ACK:
[0xA5][0x5A][0x02][0x00][NULL] - 状态从
Run_Flash_Erasure转换到Run_Receiving_Bin
-
数据传输:
- 上位机连续发送固件数据块
- 每次数据块大小为512字节(建议大小,如果需要修改,请调整USB接收部分)
- 每次接收到数据时,Bootloader会重置超时计数器(
TimerCounter_ms=0) - 数据被缓存到
data_buffer中,当累积达到512字节时触发Flash写入
-
数据写入:
- 当缓冲区满512字节时,状态从
Run_Receiving_Bin转换到Run_Flash_Write - Bootloader将数据写入Flash
- 写入成功后发送ACK:
[0xA5][0x5A][0x02][0x00][NULL],状态回到Run_Receiving_Bin - 写入失败发送错误响应:
[0xA5][0x5A][0x01][0x00][NULL](ERROR_Flash_Download),进入BootloaderErrorInterFunc()错误函数
- 当缓冲区满512字节时,状态从
-
传输完成与CRC校验:
- 3秒无数据传输后,进入超时处理:
- 如有剩余数据(
data_buffer_offset != 0),则写入Flash - 写入完成后,计算A区固件的CRC32值
- 将计算得到的CRC32值与启动命令中接收到的预期CRC32值进行比较
- 如果CRC32值匹配,发送状态更新:
[0xA5][0x5A][0x04][0x05][NULL],状态转换到Run_Prepare_To_Jump_To_A,跳转到A区应用程序 - 如果CRC32值不匹配,发送错误响应:
[0xA5][0x5A][0x01][0x00][预期CRC32值(4字节LE)](ERROR_CRC),进入错误处理函数
- 如有剩余数据(
- 3秒无数据传输后,进入超时处理:
- 启动时:应用程序自动将自身备份到B区
- 持续运行:应用程序正常执行业务逻辑
- 升级响应:接收到升级指令时跳转到Bootloader
| 状态 | 值 | 描述 |
|---|---|---|
| Start_Invalid | 0x00 | 无效状态,等待命令 |
| Start_Dirrectly_Jump_To_A | 0x01 | 直接跳转到A区 |
| Start_CopyB_To_A_Jump_To_A | 0x02 | 用B区覆盖A区并跳转 |
| Start_Updata_A | 0x03 | 升级A区固件 |
| 状态 | 值 | 描述 |
|---|---|---|
| Run_Invalid | 0x00 | 无效状态 |
| Run_Waiting_Cmd | 0x01 | 等待命令 |
| Run_Receiving_Bin | 0x02 | 接收bin文件 |
| Run_Flash_Erasure | 0x03 | Flash擦除 |
| Run_Flash_Write | 0x04 | Flash写入 |
| Run_Prepare_To_Jump_To_A | 0x05 | 准备跳转到A区 |
| Run_Prepare_To_Jump_To_B | 0x06 | 准备跳转到B区 |
[系统启动] -> [Start_Invalid, Run_Waiting_Cmd]
↓
[接收Cmd_Updata_A] -> [Start_Updata_A, Run_Flash_Erasure]
↓
[Flash擦除完成] -> [Start_Updata_A, Run_Receiving_Bin]
↓
[接收数据] -> [Start_Updata_A, Run_Receiving_Bin]
↓
[缓冲区满512字节] -> [Start_Updata_A, Run_Flash_Write]
↓
[Flash写入完成] -> [Start_Updata_A, Run_Receiving_Bin]
↓
[3秒超时且有剩余数据] -> [写入剩余数据] -> [Start_Updata_A, Run_Prepare_To_Jump_To_A]
↓
[3秒超时无剩余数据] -> [Start_Updata_A, Run_Prepare_To_Jump_To_A]
↓
[跳转到A区应用程序]
在固件升级过程中,Bootloader使用3秒超时机制来处理数据传输结束:
- 数据接收阶段:每当接收到数据时,
TimerCounter_ms被重置为0 - 超时检测:当
TimerCounter_ms > 3000时触发超时处理 - 剩余数据处理:
- 检查
data_buffer_offset是否非零 - 如果有剩余数据(
data_buffer_offset != 0),则执行最后一次Flash写入 - 写入成功后发送ACK,失败则发送错误响应
- 检查
- CRC校验:计算A区固件的CRC32值并与预期值比较
- 如果CRC校验成功,状态设置为
Run_Prepare_To_Jump_To_A,跳转到A区应用程序 - 如果CRC校验失败,发送错误响应并进入错误处理函数
- 如果CRC校验成功,状态设置为
- 跳转执行:发送状态更新包后跳转到A区应用程序
另外,如果Bootloader启动后15秒内未接收到上位机命令,会自动跳转到A区应用程序。
- 数据接收超时时间为3秒
- 每次接收到数据时重置超时计数器
- 超时后检查是否存在未写入的剩余数据
- 如有剩余数据则将其写入Flash,然后进行CRC校验
- CRC校验通过后跳转到应用程序
- 未接收到上位机命令时,15秒后自动跳转到A区
- 支持奇数长度数据的正确写入Flash
- 自动处理字节对齐问题
- Flash擦除错误检测
- Flash写入错误检测
- 跳转地址验证
- BootloaderErrorInterFunc()错误处理函数
- 写入A区前,检查A区每个扇区(扇区6和7)的前256字节,非全FF则擦除该扇区
- 写入B区前,检查B区每个扇区(扇区2, 3, 4, 5)的前256字节,非全FF则擦除该扇区
- 确保写入前目标区域都是空的(全FF状态)
- 避免重复擦除,提高效率
- 其他区域按需检查并擦除对应扇区
- 固件升级时,上位机计算固件的CRC32值并随启动命令发送
- 下位机接收完所有数据后计算A区的CRC32值并与预期值比较
- B区覆盖A区时,复制完成后计算A区和B区的CRC32值进行比较
- CRC校验失败时发送错误响应并进入错误处理函数
- 连接USB线到目标板
- 发送启动命令
[0xA5][0x5A][0x07][0x00][固件大小(4字节LE)][CRC32值(4字节LE)] - 等待ACK响应
- 连续发送固件数据
- 等待自动跳转或手动跳转
- 系统将在传输完成后进行CRC校验,校验通过后自动跳转到应用程序
- 发送跳转命令
[0xA5][0x5A][0x04][0x00][NULL] - 等待跳转到A区应用程序
- 发送命令
[0xA5][0x5A][0x06][0x00][NULL] - Bootloader将B区内容复制到A区
- 复制完成后进行CRC校验
- 校验通过后跳转到A区应用程序
应用程序启动时会自动将自身备份到B区,确保有可用的备份固件。
- UART1和PB12用于调试,在实际工程中可以注释掉
- 任务状态判断交由上位机处理
- USB CDC通信支持全双工操作
- 确保固件大小不超过A区容量(256KB)
- 固件必须是有效的二进制格式(bin)
- 传输过程中不要断开USB连接
- 升级失败时可使用B区恢复功能
- 保持足够的供电以避免升级过程中的意外断电
- CRC校验确保数据完整性,校验失败时系统将进入错误处理状态
- 启动命令中包含固件大小和CRC32值,确保计算准确
- Bootloader启动后15秒内未接收到命令会自动跳转到A区
- 接收bin文件时3秒无数据传输会触发超时处理
- 检查硬件连接
- 确认Bootloader是否正确烧录
- 检查固件大小是否超出限制
- 验证固件格式是否正确
- 确认USB连接稳定
- 检查CRC32值计算是否正确
- 确保传输过程中数据未损坏
- 检查应用程序向量表是否正确
- 验证Flash写入是否完整
该项目遵循MIT许可证。