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 语言的优秀范例,它展示了:

  1. 结构体在复杂数据管理中的应用
  2. 函数指针在程序跳转中的关键作用
  3. 指针操作在底层编程中的重要性
  4. 状态机在协议实现中的应用
  5. 位运算在标志位管理中的使用
  6. 模块化设计思想在实际项目中的体现

前置知识学习

什么是 bootloader?实现什么功能?

Bootloader 是芯片上电后第一个运行的程序,它在你定义的 B 区(0x08000000)执行,主要任务是初始化硬件并决定启动哪个程序。就像电脑开机时先进入 BIOS,再加载 Windows。

为什么需要 Bootloader?

没有 Bootloader 的困境:

  • 你的主程序在 A 区(0x08007000)这个可以自己修改哈
  • 如果主程序有 bug 导致无法启动,无法通过 UART 更新程序(因为 UART 驱动在 A 区)
  • 必须用 ST-Link 烧录器才能救砖,现场维护成本高

有 Bootloader 的优势:

  • Bootloader 在 B 区,拥有最高启动优先级
  • 即使 A 区程序崩溃,Bootloader 仍可运行→可通过 UART 接收新程序→写入 A 区→重启
  • 实现"无烧录器"远程升级
  • 上电复位后去检查有没有 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

    各部分含义:

  • Code (26.6KB):你的代码指令,这是主要部分。对于 STM32F103C8T6(64KB Flash)来说,这个量级属于正常应用范围
  • RO-data (660B):只读数据,如 const 修饰的常量、字符串字面量等
  • RW-data (92B):已初始化变量的初始值,会存 Flash+RAM 两份
  • ZI-data (8.2KB):未初始化变量,只占用 RAM,不占 Flash
  • 如果想要编译出来的代码更小一些可以去提高一下编译的优化等级。
  • 关于编译器优化

    1. -O0(无优化)

    • 作用:不进行任何优化。
    • 特点:生成的代码与源代码的结构非常接近,便于调试。编译速度最快。生成的代码通常较大且运行效率较低。
    • 适用场景:主要用于调试阶段,因为生成的代码易于理解和跟踪。

    2. -O1(轻度优化)

    • 作用:进行基本的优化,平衡编译时间和生成代码的性能。
    • 特点:优化程度适中,会进行一些基本的优化,如常量传播、死代码删除等。编译速度较快。生成的代码性能和大小都有一定的提升,但不会像更高优化级别那样显著。
    • 适用场景:适用于开发阶段,可以在调试和性能之间取得平衡。

    3. -O2(中度优化)

    • 作用:进行更深入的优化,生成更高效的代码。
    • 特点:会进行更多的优化,如指令调度、循环展开、函数内联等。编译速度比-O1慢,但仍然相对较快。生成的代码性能和大小都有显著提升。
    • 适用场景:适用于发布版本,可以在性能和编译时间之间取得较好的平衡。

    4. -O3(高度优化)

  • 作用:进行最深入的优化,生成尽可能高效的代码。
  • 特点:会进行非常深入的优化,如更多的循环展开、函数内联、自动向量化等。编译速度最慢,因为编译器会花费更多时间来优化代码。生成的代码性能最高,但可能会导致生成的代码大小增加。
  • 适用场景:适用于对性能要求极高的场景,如高性能计算、嵌入式系统等。

bootloader 和 APP 怎么区分?

仔细想一下为啥我们要把 BOOT 区放在前面?这个地方需要掌握 MCU 的启动流程,我就不赘述了,大家记得学习一下!

区域

扇区编号

起始地址

大小

用途推测

B 区(BOOT 区)

0 ~ 27

0x08000000

28KB

Bootloader(启动程序)

A 区

28 ~ 63

0x08007000

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 中断数据

    ❌ 无缓冲区,假设单帧数据必须一次性完整到达

    处理粒度

    字节级状态机,可处理各种边界情况

    包级判断,强依赖len==133len==1

    容错能力

    滑动窗口丢弃非法字节,自动同步

    ❌ 无同步机制,一旦失步(如丢字节)直接卡死

    循环处理

    while循环持续解析,一次 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.cBootLoader_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 字节为单位触发中断,原因如下:

    1. USB 分包传输:USB 传输通常以 64 字节为一包。电脑发送 133 字节时,可能会拆分成 64 + 64 + 5 发送。
    2. 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--;
                    }
                }
            }
        }
        // ... [其他情况] ...
    }
    

    为什么这样修改能解决问题?

    1. 拼接分片:当 main.c 收到 128 字节时,XModem_RxCnt 变为 128。while 循环检查 XModem_RxCnt >= 133 不满足,函数退出,但数据保留在 XModem_RxBuf 中。
    2. 完成组包:当 main.c 紧接着收到 5 字节时,代码将其追加到缓冲区,XModem_RxCnt 变为 133。
    3. 触发处理:此时 while 循环检查 XModem_RxCnt >= 133 满足条件,开始 CRC 校验和 Flash 写入逻辑,问题迎刃而解。
    4. 抗干扰:memmove 移位操作和 XModem_RxBuf[0] 检查允许代码在发生数据错位时自动重新寻找包头,增强了鲁棒性。

    请将上述代码替换到您的 boot.c 中,即可解决升级失败的问题。

#发面经攒人品##投递实习岗位前的准备##牛客在线求职答疑中心##开工第一帖#
泻湖花园嵌入式Offer指南 文章被收录于专栏

从入门到上岸,一站式搞定求职! 本硕纯机械,无竞赛无论文,后转行嵌入式软件开发(因为课题组师哥转嵌入式拿到30Woffer之后狠狠心动),秋招最终收获35W+offer可以为27届或者28届的的UU们提供参考,可以关注一下!!!

全部评论
可以作为零基础入门嵌入式软件的一个小demo嗷
点赞 回复 分享
发布于 昨天 22:36 山东

相关推荐

最喜欢秋天的火龙果很...:第一份工作一定要往大的去,工资低点没事。后面换工作会更好找,即使你去小公司,你也不可能不会换工作的。所以找大的去
点赞 评论 收藏
分享
评论
3
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务