Bootloader复刻(B站超子物联网,使用F103瑞士军刀)
整体流程图
通过该工程你可以学习到什么?
把这个 Bootloader 工程 吃透,代码量不大,但知识点极其密集。学完就能直接写进简历。
启动流程与向量表重定位
- 知识点:上电顺序、MSP、Reset_Handler、VTOR
- 代码:LOAD_A() + SCB->VTOR = 0x08007000
- 迁移:可以根据需要“双程序”场景(OTA、双备份、A/B 分区)去改编该工程,适配不同的情景
链接脚本与分散加载
- 知识点:.bin 必须和 IROM1 起始地址一致,否则跳转瞬间 HardFault
- 代码:Keil 里 0x08007000 + --first=__Vectors
- 迁移:GD32、HK32、MM32 等同内核 MCU 通用
中断管理的“三把锁”
- 知识点:关总中断 __disable_irq()关外设中断源 USART1->CR1 &= ~RXNEIE清 NVIC 挂起 NVIC_DisableIRQ(USART1_IRQn)
- 代码:Bootloader_Clear()
- 迁移:USB、CAN、以太网 Bootloader 同样适用,少一步就跳飞
裸机状态机框架
- 知识点:不用 OS,也能把“接收-校验-写入”做成可阻塞可超时的状态机
- 代码:BootStaFlag 位图 + switch-case 多级嵌套(工程代码没实现,后面自己可以改一下哈哈)
- 迁移:Modbus、SLIP、自定义串口协议直接复用
串口流控与 XMODEM-CRC16 实战----CRC 还是比较常见的,虽然 AI 也可以写,还是了解一下这个原理吧
- 知识点:128 B 数据包、SOH/EOT、CRC16 查表法、ACK/NAK 重传
- 代码:Xmodem_CRC16() + BootLoader_Event()
- 迁移:产线批量烧录、无线升级、蓝牙 SPP 升级都能用
Flash 擦写对齐与寿命管理
- 知识点:STM32F1 页 1 KB、必须先擦后写、按字写入
- 代码:ST32F1_EraseFlash() + ST32F1_WriteFlash()
- 迁移:EEPROM 模拟、参数存储、日志环形缓冲同理
I²C 外挂 EEPROM 抽象层
- 知识点:AT24C02 页写延时、地址自增、CRC 校验元数据
- 代码:AT24C02_WriteOTAInfo() + 结构体封装
- 迁移:温湿度校准表、PID 参数、加密密钥存储通用
SPI 外挂 Flash 文件系统雏形
- 知识点:W25Q64 64 KB 块擦、256 B 页写、跨页连续写封装
- 代码:W25Q64_PageWrite() + 块号换算
- 迁移:LittleFS、FATFS、日志缓存、语音/图片资源缓存
看门狗与容错设计(没加,自己可以加一个看门狗,在下载程序的时候去进行一个喂狗的操作)
- 知识点:下载过程喂狗、异常不复位而是回退到菜单
- 代码:driver_timer.c 里喂狗 + 超时判断
- 迁移:蜂窝模组、NB-IoT、4G 升级容错必须加
串口命令行解析
- 知识点:单字节命令 + 超时重试 + 回显抑制
- 代码:BootLoader_Event() 里 switch(data[0])
- 迁移:产线测试指令、调试控制台、AT 指令集
跨平台构建思想
- 知识点:.c/.h 严格分层、HAL 层可替换、寄存器操作可插拔
- 代码:fmc.h 里纯函数声明,fmc.c 里才出现 FLASH->CR
- 迁移:移植到 GD32、CH32、NRF52、ESP32 时只换驱动层
清晰的接口设计
- 头文件定义:清晰的函数声明和类型定义
- 模块职责分离:BOOT、存储、通信等模块分离
- 错误处理机制:完善的错误检测和处理
这个 BOOT 程序是学习嵌入式 C 语言的优秀范例,它展示了:
- 结构体在复杂数据管理中的应用
- 函数指针在程序跳转中的关键作用
- 指针操作在底层编程中的重要性
- 状态机在协议实现中的应用
- 位运算在标志位管理中的使用
- 模块化设计思想在实际项目中的体现
前置知识学习
什么是 bootloader?实现什么功能?
Bootloader 是芯片上电后第一个运行的程序,它在你定义的 B 区(0x08000000)执行,主要任务是初始化硬件并决定启动哪个程序。就像电脑开机时先进入 BIOS,再加载 Windows。
为什么需要 Bootloader?
没有 Bootloader 的困境:
- 你的主程序在 A 区(0x08007000)这个可以自己修改哈
- 如果主程序有 bug 导致无法启动,无法通过 UART 更新程序(因为 UART 驱动在 A 区)
- 必须用 ST-Link 烧录器才能救砖,现场维护成本高
有 Bootloader 的优势:
- Bootloader 在 B 区,拥有最高启动优先级
- 即使 A 区程序崩溃,Bootloader 仍可运行→可通过 UART 接收新程序→写入 A 区→重启
- 实现"无烧录器"远程升级
- Code (26.6KB):你的代码指令,这是主要部分。对于 STM32F103C8T6(64KB Flash)来说,这个量级属于正常应用范围
- RO-data (660B):只读数据,如 const 修饰的常量、字符串字面量等
- RW-data (92B):已初始化变量的初始值,会存 Flash+RAM 两份
- ZI-data (8.2KB):未初始化变量,只占用 RAM,不占 Flash
- 如果想要编译出来的代码更小一些可以去提高一下编译的优化等级。
- 作用:不进行任何优化。
- 特点:生成的代码与源代码的结构非常接近,便于调试。编译速度最快。生成的代码通常较大且运行效率较低。
- 适用场景:主要用于调试阶段,因为生成的代码易于理解和跟踪。
- 作用:进行基本的优化,平衡编译时间和生成代码的性能。
- 特点:优化程度适中,会进行一些基本的优化,如常量传播、死代码删除等。编译速度较快。生成的代码性能和大小都有一定的提升,但不会像更高优化级别那样显著。
- 适用场景:适用于开发阶段,可以在调试和性能之间取得平衡。
- 作用:进行更深入的优化,生成更高效的代码。
- 特点:会进行更多的优化,如指令调度、循环展开、函数内联等。编译速度比-O1慢,但仍然相对较快。生成的代码性能和大小都有显著提升。
- 适用场景:适用于发布版本,可以在性能和编译时间之间取得较好的平衡。
- 作用:进行最深入的优化,生成尽可能高效的代码。
- 特点:会进行非常深入的优化,如更多的循环展开、函数内联、自动向量化等。编译速度最慢,因为编译器会花费更多时间来优化代码。生成的代码性能最高,但可能会导致生成的代码大小增加。
- 适用场景:适用于对性能要求极高的场景,如高性能计算、嵌入式系统等。
上电复位后去检查有没有 OTA 升级事件,如果有就更新程序然后跳转到 A 区,如果没有就直接跳转到 A 区。
这是最基础的,那么我们这部分可以自己再优化添加一些额外的功能:
1.比如通过串口更新应用程序下载到 A 区。
2.设置 OTA 的版本号。
3.利用外部 Flash 存放多个程序,随时更应应用程序到 A 区。
Flash 占用分析
在使用 keil 编译完之后会有这么一栏提示信息:
Program Size: Code=26580 RO-data=660 RW-data=92 ZI-data=8108
总 Flash 占用 = Code + RO-data + RW-data = 26580 + 660 + 92 = 27330 字节 ≈ 27.4KB
各部分含义:

关于编译器优化
1. -O0(无优化)
2. -O1(轻度优化)
3. -O2(中度优化)
4. -O3(高度优化)
bootloader 和 APP 怎么区分?

仔细想一下为啥我们要把 BOOT 区放在前面?这个地方需要掌握 MCU 的启动流程,我就不赘述了,大家记得学习一下!
区域 | 扇区编号 | 起始地址 | 大小 | 用途推测 |
B 区(BOOT 区) | 0 ~ 27 |
| 28KB | Bootloader(启动程序) |
A 区 | 28 ~ 63 |
| 36KB | 主应用程序 |
这个地方 0x08000000+28*1024 正好是 0x08007000,因为刚开始我的 boot 编译出来的代码有点大,所以我给 boot 分的空间是 28K,至于大家自己的 boot 可以视情况而定。如果你的 boot 代码编译出来是 20k 完全可以把 A 区增大一点
当然了,你的 A 区也可以放在 30-63,这些都无所谓,只要你能保证你的程序编译出来的 bin 文件大小能符合就可以。
以本工程为例子:
Keil 怎么编译 bin 文件?

fromelf --bin -o "$L@L.bin" "#L"
自己的 APP 编译出 bin 文件之前一定要去修改自己的偏移!!!


OTA-Flag 是干嘛的?存在哪里?
OTA-Flag 是一个非易失性标志位,存储在 Flash 中(你要是存储在 RAM 里面,掉电就会丢失了呀)本文使用 EEPROM(AT24C02--淘宝买一个模块 2.7 元一个包邮,IIC 读写)模拟,用于在 Bootloader 和主程序(App)之间传递升级状态。它的核心作用是:告诉 Bootloader 下次启动时该做什么。
一定是外部 Flash 下载完成之后再由❌变✅!
然后 reset 运行 boot 去检查 OTA-flag。
如果检测到升级,那么 bootloader 的代码会将 A 区的程序擦除,然后将需要升级的代码下载到 A 区,然后再去置位 OTA-Flag,然后重启,重启之后就会检查 OTA-Flag,然后跳转。
这个跳转的话需要掌握一下函数指针的概念!
什么是空闲中断?
空闲中断 = DMA 持续收数据 + 硬件自动检测帧结束 + 中断通知 CPU 处理。
空闲(IDLE)的本质:从最后一个停止位开始计时,如果在一个字节的传输时间内(注意:不是"一个字符")没有检测到下一个起始位,则触发中断。
单独使用空闲中断是无意义的,因为它只告诉你"帧结束了",但不告诉你"收到了什么"。所以必须配合:
- DMA 接收:DMA 在后台默默搬运数据到内存,不占用 CPU
- 空闲中断:帧结束时,告诉 CPU"DMA 可以停了,数据处理吧"
具体的学习顺序
第一步:使用串口实现 DMA+空闲中断,这个地方可以去好好学习一下,串口最高效的实现了
因为我用的 HAL 库,所以直接在 HAL 库的回调函数实现的,每触发一次空闲中断后会进入这个函数,然后在函数里面更新数据块。
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
/* 过滤非USART1的接收事件,确保只处理USART1的数据 */
if (huart->Instance != USART1) return;
/* 当接收到有效数据时进行处理(Size > 0表示有数据到达) */
if (Size > 0)
{
/* 更新总接收字节计数器,记录累计接收的数据量 */
U0CB.URxCounter += Size;
/* 记录当前数据块的结束位置指针 * end指针指向接收到的最后一个字节位置 */
U0CB.URxDataIN->end = &U0_RxBuff[U0CB.URxCounter-1];
/* 移动输入指针到下一个数据块位置* 准备为下一次接收分配新的数据块 */
U0CB.URxDataIN++;
/* 处理环形缓冲区回卷:当输入指针到达缓冲区末尾时回到起始位置 */
if (U0CB.URxDataIN == U0CB.URxDataEND){
U0CB.URxDataIN = &U0CB.URxDataPtr[0];
}
/* 为下一个数据块预分配起始指针位置
* 根据剩余缓冲区空间决定分配策略 */
if(U0_RX_SIZE - U0CB.URxCounter >= U0_RX_MAX){
/* 缓冲区剩余空间充足,继续在当前位置分配 */
U0CB.URxDataIN->start = &U0_RxBuff[U0CB.URxCounter];
}else{
/* 缓冲区空间不足,回卷到起始位置重新开始 */
U0CB.URxDataIN->start = U0_RxBuff;
U0CB.URxCounter = 0;
}
/* 处理缓冲区溢出保护:当输入指针追上输出指针时
* 强制移动输出指针,丢弃最旧的数据块 */
if (U0CB.URxDataIN == U0CB.URxDataOUT)
{
U0CB.URxDataOUT++;
if (U0CB.URxDataOUT > U0CB.URxDataEND)
U0CB.URxDataOUT = &U0CB.URxDataPtr[0];
}
}
/* 重新启动DMA接收,准备接收下一批数据
* 使用当前输入指针的起始位置作为DMA目标地址
* 接收长度设置为U0_RX_MAX+1,确保能触发空闲中断 */
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, U0CB.URxDataIN->start, U0_RX_MAX);
}
第二步:实现软件 IIC 驱动 AT24C02,这个应该是和 M24C02 平替的,2K 的容量用来存放我们的标志位和版本号还有其他的东西(当然了,如果你本身 flash 就比较大,你完全可以不用外加的模块)
我是用的这个模块去替代的
然后呢,在这个工程里用的是:引脚为 PB6(SCL)和 PB7(SDA)
我建议先去实现 IIC 的起始、停止、读取、发送、应答的函数,然后再根据 AT24C02 的数据手册去实现对应的读取和写入的函数。

第三步:使用硬件 SPI,先实现 SPI 去读写一个字节的函数,然后进阶去 或者读写多个字节,然后去读写 W25Q64
PA5 ------> SPI1_SCK
PA6 ------> SPI1_MISO
PA7 ------> SPI1_MOSI
PBP-------->CS
#define CS_ENABLE HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET)
#define CS_DISABLE HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET)
这个的话就是用的个 W25Q64 模块,当然了,你使用别的外部 Flash 模块是一样的,只不过你一定要去理解他的物理结构!!!
因为我们的 STM32F1 的 Flash 是 64k 的,所以我们可以一次写入 1K,正好 W25Q64 是有 64KB,每次可以编程 256 字节,然后四个 256 不就是 1K 啦,所以在程序里如果是正好 1k 的,我们就用 for 循环 4 次 256 对应 1K。

然后具体的读写指令就去看手册就可以了。
第四步:实现内部 Flash 的擦除和写入
这个地方当时遇到的坑是这个 PER 位需要手动去清除,后来发现另外一个函数可以自动清除。
关于 Flash 还是很有必要去了解一下的。有的地方用 Flash 去模拟 EEP,不过由于咱们的 Flash 比较小,所以就只设置一个 APP 区域(就是前面说的 A 区),如果你的 Flash 足够大,完全可以去实现三个分区,boot 区,APP1 区,APP2 区,然后把备用的区域放在 APP2 或者 APP1,然后收到指令或者发生需要升级的事件之后去搬运然后升级,这个放在后面去实现哈!
void ST32F1_EraseFlash(uint16_t start, uint16_t num) {
uint16_t i;
HAL_FLASH_Unlock(); /* 解锁Flash,允许进行擦除操作 */
__disable_irq();
for(i = 0; i < num; i++) {
/* 计算当前扇区的物理地址并执行擦除 */
FLASH_PageErase((0x08000000 + start * 1024) + (1024 * i));
while(__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY)) {}; /* 等待擦除操作完成 */
FLASH->CR &= ~FLASH_CR_PER; /* 清除页擦除位PER,为下一次擦除做准备 */
}
__enable_irq();
HAL_FLASH_Lock(); /* 锁定Flash,保护Flash不被意外修改 */
}
//可以自动擦除
void ST32F1_EraseFlash(uint16_t start, uint16_t num)
{
FLASH_EraseInitTypeDef EraseInitStruct;
uint32_t PageError = 0;
// 以下是稳定代码,无需修改
HAL_FLASH_Unlock();
EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
EraseInitStruct.PageAddress = 0x08000000 + start * FLASH_PAGE_SIZE;
EraseInitStruct.NbPages = num;
__disable_irq();
if(HAL_FLASHEx_Erase(&EraseInitStruct, &PageError) != HAL_OK) {
// 错误处理(留给你实现)
printf("error\r\n");
}
__enable_irq();
HAL_FLASH_Lock();
}
//第二种实现
void ST32F1_EraseFlash(uint16_t start, uint16_t num) {
uint16_t i;
HAL_FLASH_Unlock(); /* 解锁Flash,允许进行擦除操作 */
__disable_irq();
for(i = 0; i < num; i++) {
/* 计算当前扇区的物理地址并执行擦除 */
FLASH_PageErase((0x08000000 + start * 1024) + (1024 * i));
while(__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY)) {}; /* 等待擦除操作完成 */
FLASH->CR &= ~FLASH_CR_PER; /* 清除页擦除位PER,为下一次擦除做准备 */
}
__enable_irq();
HAL_FLASH_Lock(); /* 锁定Flash,保护Flash不被意外修改 */
}
//可以自动擦除
void ST32F1_EraseFlash(uint16_t start, uint16_t num)
{
FLASH_EraseInitTypeDef EraseInitStruct;
uint32_t PageError = 0;
// 以下是稳定代码,无需修改
HAL_FLASH_Unlock();
EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
EraseInitStruct.PageAddress = 0x08000000 + start * FLASH_PAGE_SIZE;
EraseInitStruct.NbPages = num;
__disable_irq();
if(HAL_FLASHEx_Erase(&EraseInitStruct, &PageError) != HAL_OK) {
// 错误处理(留给你实现)
printf("error\r\n");
}
__enable_irq();
HAL_FLASH_Lock();
}
第五步:实现基础的 BOOT(检测标志位然后判断是要更新还是跳转)
我们可以实现一个最简单的 BOOT,就是在 main 去手动改写一个标志位
这个结构体是我们用来保存 OTA 的一些信息的,这个 OTA_flag 就是用来判断是更新还是跳转的。因为是保存在 AT24C02 里面,所以,在这之前要熟悉 AT24C02 的读写!可以去看看这两个函数:void AT24C02_WriteOTAInfo(void)和 void AT24C02_ReadOTAInfo(void)。
typedef struct{
uint32_t OTA_flag; // OTA升级标志位,用于控制升级流程
uint32_t Firelen[11]; // 预留OTA文件大小数组,Firelen[0]存储实际文件长度
uint8_t OTA_ver[32]; // OTA版本信息字符串
}OTA_InfoCB; // OTA信息控制块类型定义
void BootLoader_Brance(void)
{
/*
* BootLoader_Enter(20):等待用户输入,参数20表示等待2秒(20×100ms)
* 如果用户在2秒内输入小写字母'w',函数返回1(进入Bootloader命令行)
* 否则返回0(继续执行后续逻辑)
*/
if(BootLoader_Enter(20) == 0){
/* 检查OTA标志是否被设置 */
if(OTA_Info.OTA_flag == OTA_SET_FLAG){
/* OTA标志已设置,准备进行OTA更新 */
printf("OTA更新\r\n");
}
else{
/* OTA标志未设置,跳转到应用程序A区 */
printf("OTA跳转\r\n");
//LOAD_A(ST32F1_A_START_ADDR); /* 跳转到应用程序起始地址 */
}
}
/*
* 以下代码在两种情况下执行:
* 1. 用户在2秒内输入了'w'
* 2. 应用程序跳转失败
*/
printf("进入Bootloader命令行\r\n");
BootLoader_Info(); /* 显示Bootloader命令菜单 */
}
第六步:实现了最简单的跳转就可以再去增加新的功能了,这时候就可以添加命令行了
串口解析数据然后判断指令,本工程是 6 个指令,先实现最简单的两个,一个是擦除 A 区,一个是重启。
这两个就不赘述了,擦除的话就是第四步的 Flash 的内容,至于重启,每个 ARM 的内核 M3 呀,M4 呀都有自己的复位函数, NVIC_SystemReset(); 这个是 M3 的。
然后再去增加新的命令,比如 2 命令,通过 Xmodem 去下载程序然后更新到 A 区,那么在这之前你是不是应该先去擦除 A 区?所以把指令 1 的代码复制过来即可,至于 Xmodem 的协议大家可以去看一下超子的视频解析
BootStaFlag
这个标志位是用来判断是否下载用的,重点就去看 Xmodem 下载的这个地方,这个也是一个坑,这个问题我觉得大家完全可以准备一下,当做面试的时候:
面试官:你在做这个 OTA 升级的时候有没有遇到什么问题?
你:通过 Xmodem 去下载数据包的时候遇到了沾包的问题……
然后是怎么个事呢?
else if(BootStaFlag & IAP_XMODEMD_FLAG){
/* 处理Xmodem数据包(数据标志为0x01) */
if((len == 133)&&(data[0] == 0x01)){
BootStaFlag &=~ IAP_XMODEMC_FLAG; // 清除Xmodem请求标志
UpdataA.XmodemCRC = Xmodem_CRC16(&data[3], 128); // 计算接收到的数据的CRC校验值
/* 检查CRC校验是否正确 */
if(UpdataA.XmodemCRC == data[131] * 256 + data[132]){
UpdataA.XmodemNum++; // 数据包计数器加1
/* 将接收到的数据复制到临时缓冲区 */
memcpy(&UpdataA.UpDatabuff[((UpdataA.XmodemNum - 1) % (ST32F1_PAGE_SIZE / 128)) * 128], &data[3], 128);
/* 当临时缓冲区满一页时,写入Flash */
if((UpdataA.XmodemNum % (ST32F1_PAGE_SIZE / 128)) == 0){
/* 判断是写入外部Flash还是内部Flash */
if(BootStaFlag & CMD5_XMODEM_FLAG){
/* 写入外部Flash(W25Q64) */
for(i = 0; i < 4; i++){
W25Q64_PageWrite(&UpdataA.UpDatabuff[i * 256], (UpdataA.XmodemNum / 8 - 1) * 4 + i + UpdataA.W25Q64_BlockNum * 64 * 1024 / 256);
}
}else{
/* 写入内部Flash A区 */
ST32F1_WriteFlash(ST32F1_A_START_ADDR + ((UpdataA.XmodemNum / (ST32F1_PAGE_SIZE / 128)) - 1) * ST32F1_PAGE_SIZE,
(uint32_t *)UpdataA.UpDatabuff, ST32F1_PAGE_SIZE);
}
}
printf("\x06"); // 发送ACK确认
}else printf("\x15"); // 发送NAK,请求重传
}
/* 处理Xmodem结束包(结束标志为0x04) */
if((len == 1)&&(data[0] == 0x04)){
printf("\x06"); // 发送ACK确认
/* 如果临时缓冲区有剩余数据,写入Flash */
if((UpdataA.XmodemNum % (ST32F1_PAGE_SIZE / 128)) != 0){
if(BootStaFlag & CMD5_XMODEM_FLAG){
/* 写入外部Flash */
for(i = 0; i < 4; i++){
W25Q64_PageWrite(&UpdataA.UpDatabuff[i * 256], (UpdataA.XmodemNum / 8) * 4 + i + UpdataA.W25Q64_BlockNum * 64 * 1024 / 256);
}
}else{
/* 写入内部Flash */
ST32F1_WriteFlash(ST32F1_A_START_ADDR + ((UpdataA.XmodemNum / (ST32F1_PAGE_SIZE / 128))) * ST32F1_PAGE_SIZE,
(uint32_t *)UpdataA.UpDatabuff, (UpdataA.XmodemNum % (ST32F1_PAGE_SIZE / 128)) * 128);
}
}
BootStaFlag &= ~IAP_XMODEMD_FLAG; // 清除Xmodem传输标志
/* 如果是写入外部Flash,更新OTA信息 */
if(BootStaFlag & CMD5_XMODEM_FLAG){
BootStaFlag &= ~CMD5_XMODEM_FLAG; // 清除外部Flash传输标志
OTA_Info.Firelen[UpdataA.W25Q64_BlockNum] = UpdataA.XmodemNum * 128; // 更新程序长度信息
AT24C02_WriteOTAInfo(); // 保存OTA信息到AT24C02
mdelay(100); // 短暂延迟
BootLoader_Info(); // 重新显示命令菜单
}
else{
mdelay(100); // 短暂延迟
NVIC_SystemReset(); // 下载完成,重启系统
}
}
}
/* 情况2:正在进行Xmodem数据传输 */
else if(BootStaFlag & IAP_XMODEMD_FLAG)
{
/*--------------------- 1. 接收缓存保护 ---------------------*/
/* 将串口新数据拼接到静态缓冲区,防止溢出 */
if(XModem_RxCnt + len <= sizeof(XModem_RxBuf))
{
memcpy(&XModem_RxBuf[XModem_RxCnt], data, len);
XModem_RxCnt += len;
}
else
{
XModem_RxCnt = 0; /* 溢出后丢弃已收数据 */
printf("Buffer Overflow\r\n");
}
/*--------------------- 2. 数据包解析循环 ---------------------*/
/* 只要缓冲区里还有数据就继续处理,直到拆完所有完整包 */
while(1)
{
if(XModem_RxCnt == 0) break; /* 缓冲区空,退出循环 */
/*========== 2.1 普通数据包(ID=0x01,133字节)==========*/
if(XModem_RxBuf[0] == 0x01)
{
/* 不足一个完整包则等待后续数据 */
if(XModem_RxCnt < 133) break;
/* 清除“等待首包”标志,正式进入流控状态 */
BootStaFlag &= ~IAP_XMODEMC_FLAG;
/* 计算128字节有效负载的CRC16 */
UpdataA.XmodemCRC = Xmodem_CRC16(&XModem_RxBuf[3], 128);
uint16_t packet_crc = (uint16_t)XModem_RxBuf[131] * 256 + XModem_RxBuf[132];
/* CRC 正确 → 存数据 + 回 ACK(0x06) */
if(UpdataA.XmodemCRC == packet_crc)
{
UpdataA.XmodemNum++; /* 全局包序号递增 */
/* 将128字节负载拷贝到临时页缓冲区 */
memcpy(&UpdataA.UpDatabuff[((UpdataA.XmodemNum - 1) % (ST32F1_PAGE_SIZE / 128)) * 128],
&XModem_RxBuf[3], 128);
/* 若页缓冲区满1KB,则执行一次Flash写(或W25Q64写) */
if((UpdataA.XmodemNum % (ST32F1_PAGE_SIZE / 128)) == 0)
{
if(BootStaFlag & CMD5_XMODEM_FLAG) /* 下载到外部Flash */
{
for(i = 0; i < 4; i++)
{
W25Q64_PageWrite(&UpdataA.UpDatabuff[i * 256],
(UpdataA.XmodemNum / 8 - 1) * 4 + i +
UpdataA.W25Q64_BlockNum * 64 * 1024 / 256);
}
}
else /* 下载到内部Flash A区 */
{
ST32F1_WriteFlash(ST32F1_A_START_ADDR +
((UpdataA.XmodemNum / (ST32F1_PAGE_SIZE / 128)) - 1) * ST32F1_PAGE_SIZE,
(uint32_t *)UpdataA.UpDatabuff, ST32F1_PAGE_SIZE);
}
}
printf("\x06"); /* 回 ACK */
}
else
{
printf("\x15"); /* CRC 错误 → 回 NAK */
}
/* 从缓冲区移除已处理包(133字节) */
if(XModem_RxCnt > 133)
{
memmove(XModem_RxBuf, &XModem_RxBuf[133], XModem_RxCnt - 133);
}
XModem_RxCnt -= 133;
continue; /* 继续检查是否还有完整包 */
}
/*========== 2.2 结束包(ID=0x04,EOT)==========*/
else if(XModem_RxBuf[0] == 0x04)
{
printf("\x06"); /* 回 ACK */
/* 若最后一页不足1KB,把剩余数据补写 */
if((UpdataA.XmodemNum % (ST32F1_PAGE_SIZE / 128)) != 0)
{
if(BootStaFlag & CMD5_XMODEM_FLAG) /* 外部Flash */
{
for(i = 0; i < 4; i++)
{
W25Q64_PageWrite(&UpdataA.UpDatabuff[i * 256],
(UpdataA.XmodemNum / 8) * 4 + i +
UpdataA.W25Q64_BlockNum * 64 * 1024 / 256);
}
}
else /* 内部Flash A区 */
{
ST32F1_WriteFlash(ST32F1_A_START_ADDR +
(UpdataA.XmodemNum / (ST32F1_PAGE_SIZE / 128)) * ST32F1_PAGE_SIZE,
(uint32_t *)UpdataA.UpDatabuff,
(UpdataA.XmodemNum % (ST32F1_PAGE_SIZE / 128)) * 128);
}
}
/* 退出XMODEM接收状态 */
BootStaFlag &= ~IAP_XMODEMD_FLAG;
/*========== 2.2.1 下载到外部Flash(W25Q64)==========*/
if(BootStaFlag & CMD5_XMODEM_FLAG)
{
BootStaFlag &= ~CMD5_XMODEM_FLAG;
OTA_Info.Firelen[UpdataA.W25Q64_BlockNum] = UpdataA.XmodemNum * 128;
AT24C02_WriteOTAInfo();
printf("写入外部Flash——W25Q64成功");
mdelay(100);
BootLoader_Info(); /* 回到命令菜单 */
}
/*========== 2.2.2 下载到内部Flash A区 ==========*/
else
{
printf("DownLoad Successfully!!!");
mdelay(100);
NVIC_SystemReset(); /* 软件复位,稍后跳转到A区APP */
}
XModem_RxCnt = 0;
return; /* 处理完毕,直接返回 */
}
/*========== 2.3 非法字节,滑动窗口 ==========*/
else
{
/* 丢弃首字节,继续检查 */
if(XModem_RxCnt > 0)
{
memmove(XModem_RxBuf, &XModem_RxBuf[1], XModem_RxCnt - 1);
XModem_RxCnt--;
}
}
}
}
我最开始是跟着视频来的:
因为咱们前面说了串口开启了 DMA 空闲中断,若 133 字节数据分多次到达,会直接丢弃前段数据!!!导致接收到的数据包根本就不完整,程序里面只去判断了是不是 133 个字节,所以导致我们最后写入的不对。
// 第一版:缓冲区保护 + 累积
if(XModem_RxCnt + len <= sizeof(XModem_RxBuf)) {
memcpy(&XModem_RxBuf[XModem_RxCnt], data, len);
XModem_RxCnt += len; // 可累积
}
// 第二版:无缓冲,直接覆盖
// 若数据分两次到达(如133字节分包),第二次直接覆盖第一次
还有就是为了提高传输速度,我们就加大了波特率,在波特率高、串口驱动缓冲小时,极易因分包导致传输失败。
// 第一版:先检查长度,不足则等待
if(XModem_RxBuf[0] == 0x01) {
if(XModem_RxCnt < 133) break; // 等待后续数据
}
// 第二版:强依赖len==133
if((len == 133) && (data[0] == 0x01)) { ... }
// 若len != 133(如UART分包或丢字节),直接跳过
第二版在电磁干扰环境下,一旦接收到非法字节,协议永久卡死。可以好好学习一下滑动窗口的思想,包括你后面去刷力扣,这个思想也是非常重要的!
// 第一版:滑动窗口丢弃首字节,自动恢复同步
else {
if(XModem_RxCnt > 0) {
memmove(XModem_RxBuf, &XModem_RxBuf[1], XModem_RxCnt - 1);
XModem_RxCnt--;
}
}
// 第二版:无同步机制
// 若因干扰收到0xFF等非法字节,后续所有包都无法匹配
特性 | 第一版(基于缓冲区) | 第二版(直接处理) |
缓冲策略 | ✅ 有环形缓冲区保护,可累积多次 UART 中断数据 | ❌ 无缓冲区,假设单帧数据必须一次性完整到达 |
处理粒度 | ✅ 字节级状态机,可处理各种边界情况 | ❌ 包级判断,强依赖 |
容错能力 | ✅ 滑动窗口丢弃非法字节,自动同步 | ❌ 无同步机制,一旦失步(如丢字节)直接卡死 |
循环处理 | ✅ | ❌ 单分支处理,一次只处理一包 |
重点去了解一下 Xmodem 传输数据的这个过程就 OK 了
工程:
通过网盘分享的文件:OTA_STM32F103.zip
链接: https://pan.baidu.com/s/1oGeZGDPmazy_gpJf0PfqVA?pwd=8888 提取码: 8888
附录:
当时一直没法成功下载之后找了一下 AI:
通过深度分析您提供的代码和描述的问题现象,可以确定问题的根本原因在于 XMODEM 协议处理逻辑中的数据包长度检查过于严格,无法适应串口DMA 接收时的“分包”或“粘包”现象。
以下是详细的问题分析和解决方案:
问题根源深度剖析
现象回顾
- 指令 2 发送后:进入 IAP_XMODEMD_FLAG 状态,开始打印 'C' 请求发送。
- 上传 bin 文件时:串口打印 接收128字节、接收5字节、接收256字节 等。
- 结果:一直打印 'C',没有进行烧写和复位。
逻辑漏洞分析
您的 boot.c 中 BootLoader_Event 函数有如下代码:
/* 情况2:正在进行Xmodem数据传输 */
else if(BootStaFlag & IAP_XMODEMD_FLAG){
/* 处理Xmodem数据包(数据标志为0x01) */
if(len == 133 && data[0] == 0x01){ // <--- 致命问题在这里
// ... 处理逻辑 ...
}
}
问题核心: 代码强制要求 单次串口接收中断(DMA 事件) 接收到的数据长度 len 必须 严格等于 133 字节(XMODEM 标准包长:1 头 +1 包号 +1 反码 +128 数据 +2CRC)。
然而,在实际的嵌入式系统中,特别是波特率较高(921600)且使用 USB 转串口 时,数据往往不会正好以 133 字节为单位触发中断,原因如下:
- USB 分包传输:USB 传输通常以 64 字节为一包。电脑发送 133 字节时,可能会拆分成 64 + 64 + 5 发送。
- DMA 中断机制:STM32 的串口 IDLE(空闲)中断会在检测到总线空闲时触发。
- 现象解释:您看到的 接收128字节 和 接收5字节 正好加起来是 133 字节。这说明 133 字节的包被拆成了两部分到达 BootLoader_Event。
- 第一次进入函数:len = 128。判断 if(128 == 133) -> 不满足,数据被丢弃。
- 第二次进入函数:len = 5。判断 if(5 == 133) -> 不满足,数据被丢弃。
因为所有数据都被丢弃了,单片机没有收到有效包,也没有发送 ACK,所以上位机会认为发送失败或一直在重试,而单片机认为一直没收到包,所以主循环继续打印 'C' 请求开始。
解决方案
要解决这个问题,必须在 boot.c 中引入一个 接收缓冲区(Cache),将零散接收到的数据先存起来,凑够 133 字节后再进行处理。
请按以下步骤修改 boot.c 文件:
第一步:添加静态缓冲区变量
在 boot.c 的头部或 BootLoader_Event 函数外部添加:
/* XMODEM 接收缓存,用于处理 DMA 分包/粘包问题 */
static uint8_t XModem_RxBuf[1024]; // 缓冲区稍微大一点,防止溢出
static uint16_t XModem_RxCnt = 0; // 当前缓冲区内的数据长度
第二步:重写 BootLoader_Event 的 XMODEM 处理部分
修改 BootLoader_Event 函数中 else if(BootStaFlag & IAP_XMODEMD_FLAG) 分支的代码。
修改后的完整代码逻辑如下:
void BootLoader_Event(uint8_t *data, uint16_t len)
{
int temp, i;
// ... [保留原有的 情况1:BootStaFlag为0 的处理代码] ...
if(BootStaFlag == 0){
// ... 原有代码保持不变 ...
}
/* 情况2:正在进行Xmodem数据传输 */
else if(BootStaFlag & IAP_XMODEMD_FLAG){
/* 1. 将新接收到的数据追加到静态缓冲区中 */
if(XModem_RxCnt + len <= sizeof(XModem_RxBuf)) {
memcpy(&XModem_RxBuf[XModem_RxCnt], data, len);
XModem_RxCnt += len;
} else {
// 缓冲区溢出保护,简单清空重新接收(视情况可优化)
XModem_RxCnt = 0;
printf("Buffer Overflow\r\n");
}
/* 2. 循环检查缓冲区中是否包含完整的数据包 /
while(1) {
/ 如果缓冲区为空,直接退出 */
if(XModem_RxCnt == 0) break;
/*检测包头:XMODEM数据包 (SOH=0x01) /
if(XModem_RxBuf[0] == 0x01) {
/ 检查数据量是否足够一个完整包 (133字节) */
if(XModem_RxCnt >= 133) {
BootStaFlag &= ~IAP_XMODEMC_FLAG; // 清除 'C' 发送标志
// 进行CRC校验(注意:这里用 buffer 中的数据)
UpdataA.XmodemCRC = Xmodem_CRC16(&XModem_RxBuf[3], 128);
uint16_t packet_crc = (uint16_t)XModem_RxBuf[131] * 256 + XModem_RxBuf[132];
if(UpdataA.XmodemCRC == packet_crc){
UpdataA.XmodemNum++;
// 提取有效数据到 UpDatabuff
memcpy(&UpdataA.UpDatabuff[((UpdataA.XmodemNum - 1) % (ST32F1_PAGE_SIZE / 128)) * 128], &XModem_RxBuf[3], 128);
// 满一页写入Flash (保持原有逻辑不变)
if((UpdataA.XmodemNum % (ST32F1_PAGE_SIZE / 128)) == 0){
if(BootStaFlag & CMD5_XMODEM_FLAG){
for(i = 0; i < 4; i++){
W25Q64_PageWrite(&UpdataA.UpDatabuff[i * 256], (UpdataA.XmodemNum / 8 - 1) * 4 + i + UpdataA.W25Q64_BlockNum * 64 * 1024 / 256);
}
}else{
ST32F1_WriteFlash(ST32F1_A_START_ADDR + ((UpdataA.XmodemNum / (ST32F1_PAGE_SIZE / 128)) - 1) * ST32F1_PAGE_SIZE,
(uint32_t *)UpdataA.UpDatabuff, ST32F1_PAGE_SIZE);
}
}
printf("\x06"); // 发送 ACK
} else {
printf("\x15"); // CRC错误,发送 NAK
}
/* 核心步骤:从缓冲区移除已处理的133字节,将剩余数据前移 */
if(XModem_RxCnt > 133) {
memmove(XModem_RxBuf, &XModem_RxBuf[133], XModem_RxCnt - 133);
}
XModem_RxCnt -= 133;
// 继续循环处理可能存在的下一个包(粘包情况)
continue;
} else {
// 数据不够133字节,等待下一次接收
break;
}
}
/* 检测包头:结束包 (EOT=0x04) */
else if(XModem_RxBuf[0] == 0x04) {
printf("\x06"); // 发送 ACK
// 处理剩余未满一页的数据 (保持原有逻辑)
if((UpdataA.XmodemNum % (ST32F1_PAGE_SIZE / 128)) != 0){
if(BootStaFlag & CMD5_XMODEM_FLAG){
for(i = 0; i < 4; i++){
W25Q64_PageWrite(&UpdataA.UpDatabuff[i * 256], (UpdataA.XmodemNum / 8) * 4 + i + UpdataA.W25Q64_BlockNum * 64 * 1024 / 256);
}
}else{
ST32F1_WriteFlash(ST32F1_A_START_ADDR + ((UpdataA.XmodemNum / (ST32F1_PAGE_SIZE / 128))) * ST32F1_PAGE_SIZE,
(uint32_t *)UpdataA.UpDatabuff, (UpdataA.XmodemNum % (ST32F1_PAGE_SIZE / 128)) * 128);
}
}
// 结束传输后续操作 (保持原有逻辑)
BootStaFlag &= ~IAP_XMODEMD_FLAG;
if(BootStaFlag & CMD5_XMODEM_FLAG){
BootStaFlag &= ~CMD5_XMODEM_FLAG;
OTA_Info.Firelen[UpdataA.W25Q64_BlockNum] = UpdataA.XmodemNum * 128;
AT24C02_WriteOTAInfo();
mdelay(100);
BootLoader_Info();
} else {
mdelay(100);
NVIC_SystemReset();
}
XModem_RxCnt = 0; // 清空缓存
return;
}
/* 异常数据处理:既不是SOH也不是EOT */
else {
// 如果缓冲区的第一个字节不是有效头,可能是上一次接收的残余垃圾或同步丢失
// 丢弃第1个字节,尝试在下一位寻找包头(滑动窗口机制)
if(XModem_RxCnt > 0) {
memmove(XModem_RxBuf, &XModem_RxBuf[1], XModem_RxCnt - 1);
XModem_RxCnt--;
}
}
}
}
// ... [其他情况] ...
}
为什么这样修改能解决问题?
- 拼接分片:当 main.c 收到 128 字节时,XModem_RxCnt 变为 128。while 循环检查 XModem_RxCnt >= 133 不满足,函数退出,但数据保留在 XModem_RxBuf 中。
- 完成组包:当 main.c 紧接着收到 5 字节时,代码将其追加到缓冲区,XModem_RxCnt 变为 133。
- 触发处理:此时 while 循环检查 XModem_RxCnt >= 133 满足条件,开始 CRC 校验和 Flash 写入逻辑,问题迎刃而解。
- 抗干扰:memmove 移位操作和 XModem_RxBuf[0] 检查允许代码在发生数据错位时自动重新寻找包头,增强了鲁棒性。
请将上述代码替换到您的 boot.c 中,即可解决升级失败的问题。
从入门到上岸,一站式搞定求职! 本硕纯机械,无竞赛无论文,后转行嵌入式软件开发(因为课题组师哥转嵌入式拿到30Woffer之后狠狠心动),秋招最终收获35W+offer可以为27届或者28届的的UU们提供参考,可以关注一下!!!


查看20道真题和解析