Bootloader代码流程

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

195.2.3 Bootloader代码流程

#define INTER_FLASH_PARAM_ADDR (0x8006000U) /* 参数区起始地址,等价于写成 0x08006000。 */
#define INTER_FLASH_APP_ADDR   (0x8008000U) /* APP 起始地址,等价于写成 0x08008000。 */

#pragma pack(1)                              /* 要求编译器按 1 字节边界布置下面的结构体成员。 */
typedef struct {                             /* 定义 Bootloader 与 APP 共同解释的 Flash 参数格式。 */
    uint8_t magic[4];                        /* 4 字节魔数,info.bin 中约定为 AA BB CC DD。 */
    uint32_t ota_bin_version;                /* 4 字节固件版本字段,Cortex-M 按小端序存放。 */
    uint8_t ota_flag;                        /* 1 字节升级标志:0 不升级,1 请求升级。 */
    uint8_t checksum;                        /* 1 字节累加和,用于校验前面的有效字段。 */
    uint8_t format[2];                       /* 2 字节占位,使结构体总长度保持为 12 字节。 */
}inter_flash_cfg_param_typeDef;              /* 为参数结构体定义课程源码使用的类型名。 */
#pragma pack()                               /* 恢复编译器默认的结构体对齐设置。 */

static inter_flash_cfg_param_typeDef flash_cfg_param; /* 在 RAM 中保存从参数页读取的 12 字节副本。 */

uint8_t inter_flash_cfg_get_param_sector()   /* 从 Flash 读取并校验参数结构。 */
{
    inter_flashif_read_page(INTER_FLASH_PARAM_ADDR, (uint8_t *)&flash_cfg_param, sizeof(flash_cfg_param)); /* 读取参数区的 12 字节。 */
    if((flash_cfg_param.magic[0] != 0xAA) || (flash_cfg_param.magic[1] != 0xBB) || /* 校验魔数前两个字节。 */
        (flash_cfg_param.magic[2] != 0xCC) || (flash_cfg_param.magic[3] != 0xDD))   /* 校验魔数后两个字节。 */
    {
        printf("flash magic error\r\n");      /* 魔数不符合约定时打印错误。 */
        return 1;                              /* 返回 1,表示参数格式错误。 */
    }

    uint8_t checksum = inter_flash_checksum((uint8_t *)&flash_cfg_param, sizeof(flash_cfg_param) - 3); /* 对前 9 字节求累加和。 */
    if(checksum != flash_cfg_param.checksum)  /* 比较计算值与结构体中的 checksum 字节。 */
    {
        printf("flash checksum error\r\n");   /* 累加和不一致时打印错误。 */
        return 2;                              /* 返回 2,表示参数校验错误。 */
    }

    return 0;                                 /* 魔数和累加和都正确。 */
}

int8_t inter_flash_cfg_get_app_update_flag() /* 读取参数并返回持久化升级标志。 */
{
    uint8_t ret = inter_flash_cfg_get_param_sector(); /* 先刷新 RAM 副本并检查参数合法性。 */
    if(ret != 0)                              /* 非 0 表示参数读取或校验失败。 */
    {
        return -1;                            /* 用 -1 向上层报告读取失败。 */
    }

    return flash_cfg_param.ota_flag;          /* 返回 Flash 中保存的 ota_flag。 */
}

uint8_t inter_flash_cfg_init(void)           /* Bootloader 启动时校验参数区。 */
{
    if(sizeof(inter_flash_cfg_param_typeDef) % 4 != 0) /* 确认结构长度可按 32 位字完整写入。 */
    {
        while(1)                              /* 长度不符合约定时停留在错误循环。 */
        {
            printf("flash cfg param error\r\n"); /* 周期性提示结构长度错误。 */
            HAL_Delay(1000);                  /* 每秒打印一次,避免占满串口。 */
        }
    }

    uint8_t ret = inter_flash_cfg_get_param_sector(); /* 从 0x08006000 读取并校验 info 参数。 */
    if(ret != 0)                              /* 参数不存在或校验不通过。 */
    {
        while(1)                              /* 课程源码在此永久等待人工修复。 */
        {
            printf("flash cfg param sector error\r\n"); /* 提示重新检查或烧写 info.bin。 */
            HAL_Delay(1000);                  /* 控制错误信息打印频率。 */
        }
    }

    return 0;                                 /* 参数结构和内容均合法。 */
}

uint8_t ymodem_run_flag = 0;                 /* RAM 运行标志:1 表示主循环应执行 YMODEM 接收。 */

printf("                    Bootloader\t\r\n"); /* 通过 USART1 打印 Bootloader 启动标题。 */
printf("----------------------------------------------------------------\r\n"); /* 打印分隔线。 */
printf("flash cfg size:%d\r\n", sizeof(inter_flash_cfg_param_typeDef)); /* 课程实验中应打印 12。 */
printf("----------------------------------------------------------------\r\n"); /* 打印分隔线。 */

inter_flash_cfg_init();                      /* 读取并校验 Flash 参数区;失败时不会继续。 */

int8_t ret = inter_flash_cfg_get_app_update_flag(); /* 获取持久化 ota_flag。 */
if (ret == 1)                                /* ota_flag=1,表示 APP 已请求升级。 */
{
    ymodem_run_flag = 1;                     /* 让下面的主循环进入 YMODEM 接收分支。 */
}
else if (ret == 0)                           /* ota_flag=0,表示当前无需升级。 */
{
    printf("[BOOT]--->jump app\r\n\r\n");    /* 记录即将跳转到现有 APP。 */
    boot_check_stack2jump_app(INTER_FLASH_APP_ADDR); /* 检查 APP 初始栈并跳到 0x08008000。 */
    ymodem_run_flag = 0;                     /* 若跳转函数意外返回,则保持非升级状态。 */
}

while (1)                                    /* Bootloader 主循环。 */
{
    if (ymodem_run_flag == 1)                /* 只有升级标志有效时才接收新固件。 */
    {
        printf("Ymodem is run,waiting for upgrade...\r\n"); /* 提示 USART2 已等待文件。 */

        extern uint8_t aFileName[FILE_NAME_LENGTH]; /* 引用 ymodem.c 保存的文件名缓冲区。 */

        uint8_t number[11] = {0};            /* 保存文件大小转换后的十进制字符串。 */
        uint32_t size = 0;                    /* 接收 YMODEM 返回的固件字节数。 */
        COM_StatusTypeDef result;             /* 保存 YMODEM 会话结果。 */
        result = Ymodem_Receive(&size);       /* 经 USART2 接收固件并在函数内部写入 APP 区。 */
        if (result == COM_OK)                 /* 只有协议层返回成功才执行收尾。 */
        {
            printf((uint8_t *)"\n\n\r Programming Completed Successfully!\n\r--------------------------------\r\n Name: %s", aFileName); /* 保留课程源码的完成日志。 */

            Int2Str(number, size);            /* 把二进制整数文件长度转换成字符串。 */
            printf("Size: %dBytes", number); /* 保留课程源码;此处格式符存在类型问题,正文会说明。 */

            inter_flash_cfg_set_app_update_flag(0); /* 升级成功后把 Flash 中的 ota_flag 清零。 */
            printf("--->jump app\r\n");       /* 提示即将启动新 APP。 */
            boot_check_stack2jump_app(INTER_FLASH_APP_ADDR); /* 跳到刚写入的 APP。 */
        }
        else                                  /* YMODEM 超时、取消、越界或数据错误。 */
        {
            printf("=---<ret:%d\r\n", result); /* 打印协议函数返回的错误码。 */
        }
    }
    else                                      /* 未进入升级且前面的 APP 跳转又意外返回。 */
    {
        HAL_Delay(1000);                      /* 延时 1 秒。 */
        printf("system error\r\n");          /* 提示系统处于异常路径。 */
        printf("reboot...\r\n");             /* 提示即将复位。 */
        HAL_Delay(2000);                      /* 给日志留出发送时间。 */
        NVIC_SystemReset();                   /* 触发 Cortex-M3 系统复位。 */
    }
}

本课从 08Ymodem_Bootloader 的 main.c 入手,学习目标不是默写整个 YMODEM 状态机,而是能沿着“参数校验 → 判断升级 → 接收固件 → 清标志 → 跳 APP”这条主线读懂代码,并知道每个地址为什么这样设置。

一、Bootloader 代码分析

1.1 Bootloader 主函数分析

1.1.1 复位后的执行顺序

开发板复位后,Cortex-M3 从 0x08000000 处的向量表取得初始 MSP 和复位入口。因为 Bootloader 工程链接在 Flash 起始地址,所以执行顺序是:

Bootloader Reset_Handler
  → HAL_Init / SystemClock_Config
  → GPIO、USART1、USART2 初始化
  → 参数区校验
  → 读取 ota_flag
  → 选择跳 APP 或接收升级

USART1 和 USART2 的职责不同:

  • printf() 最终经 USART1 输出日志;
  • ymodem_porting.c 中的 Serial_PutByte()、Serial_Recv_data() 使用 USART2 传输 YMODEM 数据。
1.1.2 地址必须能算出来

源码中的三个地址是:

地址含义
0x08000000Bootloader 起始地址,也是主 Flash 起始地址
0x08006000参数区起始地址
0x08008000APP 起始地址

正确换算:

[ 0x6000=6\times4096=24576\ B=24\ KiB ]

[ 0x8000-0x6000=0x2000=8192\ B=8\ KiB ]

课程字幕把 0x6000 识别成 0x630、把 0x2000 识别成 0x230,原笔记因此出现错误。验证地址时应直接看 inter_flashif.h 和两个工程的 scatter 文件,不要依赖口述转写。

1.1.3 参数结构体为什么是 12 字节

字段按顺序占用:

偏移字段长度作用
0x00magic[4]4 B判断参数区是否符合约定格式
0x04ota_bin_version4 B保存版本信息
0x08ota_flag1 B0 不升级,1 进入升级
0x09checksum1 B校验偏移 0x00~0x08 的 9 字节
0x0Aformat[2]2 B占位,使总长度为 12 B

总长度为 4+4+1+1+2=12 字节。#pragma pack(1)使跨工程的字段偏移不依赖编译器插入的内部填充,#pragma pack()则恢复编译器默认设置,并不是硬编码“恢复为 4 字节对齐”。

本结构本身即使按常见 4 字节规则也恰好可能仍为 12 字节,但把 Flash 中的裸字节直接解释成结构体是一种持久化 ABI 约定。Bootloader 与 APP 必须保持字段顺序、类型宽度、字节序和对齐方式一致。

1.1.4 info.bin 如何通过校验

配套 info.bin 的 12 字节是:

AA BB CC DD 02 08 04 01 00 1D 00 00

程序先验证前四字节是否为 AA BB CC DD,再调用:

inter_flash_checksum((uint8_t *)&flash_cfg_param, sizeof(flash_cfg_param) - 3); /* 12-3=9,只累计前 9 字节。 */

前 9 字节包含 magic、version 和 ota_flag,不包含 checksum 自身及末尾两个占位字节。其低 8 位累加和为 0x1D,与偏移 9 的字节一致。

magic 只能说明“这里看起来像课程定义的配置”,1 字节累加和也只能检测部分偶然错误;二者都不是安全认证。

1.1.5 inter_flash_cfg_init() 的真实作用

初始化函数做两层检查:

  1. sizeof(struct) % 4 == 0:保证后面按 32 位字写回时没有不足一个字的尾部;它并不等价于“结构体必须是 12 字节”。
  2. 读取参数页并检查 magic、checksum:失败后在 while(1) 中每秒打印错误。

因此首次实验必须提前把 info.bin 写到 0x08006000。若参数页是擦除态 0xFF,Bootloader 不会自动构造默认配置,而会永久停在错误循环。

1.1.6 读取升级标志并分支

inter_flash_cfg_get_app_update_flag() 每次都会重新读取并校验参数,而不是只返回旧的 RAM 值:

  • 返回 1:设置 ymodem_run_flag=1,主循环进入固件接收;
  • 返回 0:调用 boot_check_stack2jump_app(0x08008000);
  • 返回 -1:主函数的这两个分支都不匹配。不过正常情况下,前面的 inter_flash_cfg_init() 已经在参数错误时阻塞,不会走到这里。

ymodem_run_flag 只是本次运行中的 RAM 标志,真正跨复位保存状态的是 Flash 中的 ota_flag。

1.1.7 YMODEM 接收分支

进入 while(1) 后,Ymodem_Receive(&size)承担了复杂工作:

  1. USART2 周期性发送 C 请求 CRC-16 模式;
  2. 解析 0 号包中的文件名和大小;
  3. 检查包号、包号反码和 CRC;
  4. 把数据积累到 2 KiB RAM 缓冲区;
  5. 擦除 0x08008000 + 页偏移 的 Flash 页;
  6. 按 32 位字写入并回读比较;
  7. 用 ACK/NAK 驱动发送端继续或重传;
  8. 结束后向主函数返回 COM_OK 或错误码。

主函数只在 COM_OK 时清除 Flash 升级标志并跳 APP。失败分支不清标志,因此复位后仍可再次进入升级模式,这是单槽系统保留恢复机会的关键。

1.1.8 Bootloader 如何跳到 APP

boot_check_stack2jump_app() 的核心逻辑是:

  1. 读取 APP 地址处第 1 个 32 位字,它应是 APP 的初始主栈指针,并落在 SRAM 范围;
  2. 读取 APP_ADDR+4 处第 2 个 32 位字,它是 APP 的 Reset_Handler 地址;
  3. 把 MSP 改成 APP 的初始栈值;
  4. 关闭中断后,通过函数指针跳到 APP 复位入口。

跳转能否长期正常运行还依赖 APP 工程:

  • scatter 文件必须把 APP 链接到 0x08008000;
  • APP 的向量表必须重定位到 0x08008000;
  • Bootloader 关闭了全局中断,配套 APP 在早期执行 __set_PRIMASK(0) 重新开放中断;
  • APP 应重新初始化自己使用的时钟和外设,不能依赖 Bootloader 留下的外设状态。

课程源码检查了初始 MSP 是否像 SRAM 地址,但没有进一步验证 Reset_Handler 是否落在合法 APP Flash 范围,也没有验证整个镜像的 CRC/签名。

1.1.9 升级完成日志中的格式问题

课程源码先用 Int2Str(number, size)把长度转为字符串,随后却执行:

printf("Size: %dBytes", number); /* number 是 uint8_t 数组退化出的指针,%d 期望 int,类型不匹配。 */

这行可能打印一个指针数值或产生未定义行为,逻辑上应使用字符串格式符。但按照笔记规则,顶部代码保留课程源码原样,只在这里指出问题,不擅自改写。

1.2 源码审查:实验能运行不代表实现已健壮

下面是配套源码中值得学习者特别注意的风险。这些结论来自当前归档工程,而不是对课程流程的否定。

1.2.1 固件长度检查顺序有误

Ymodem_Receive()解析到文件大小 filesize 后,源码先检查 *p_size,之后才执行 *p_size = filesize。调用者传入的 size 初值为 0,因此这次检查实际上无法阻止超大文件。可靠实现必须用刚解析出的 filesize 与 APP 分区剩余容量比较,并在任何擦除发生前拒绝越界镜像。

1.2.2 存在遗留地址宏

ymodem.c仍定义 APPLICATION_ADDRESS = 0x08004000,但实际擦写使用 INTER_FLASH_APP_ADDR = 0x08008000。前者主要遗留自 ST 示例,当前接收路径的真正目标由后者决定。多个“APP 地址”宏并存容易在后续修改时造成分区不一致,应在维护时统一来源。

1.2.3 包数和页尾处理需要复核

源码用 filesize / PACKET_SIZE 得到包数后,又用“商 % PACKET_SIZE”判断是否加 1;数学上正确的余数判断应针对原始 filesize。同时,ReceivePacket()支持 128 字节 SOH 包和 1024 字节 STX 包,但接收分支固定只 memcpy 128 字节并按 128 字节累计页缓冲。若发送端改用 1 KiB 数据包,其余 896 字节不会按当前逻辑写入。课程的约 10 KiB 固件完成了演示,说明当时工具与当前实现形成了可用组合,但更换终端软件、包长或固件长度时,必须重新测试协议互操作及最后一个不足整页的缓冲区是否被完整写入。

1.2.4 回读比较长度不一致

源码从 Flash 回读 2040 字节,却用 memcmp(..., 2048)比较 2048 字节。最后 8 字节不是本次回读结果,可能造成误判或掩盖错误。并且比较结果只打印,没有转化成 COM_DATA 阻止“成功”路径。

1.2.5 Flash HAL 返回值未检查

课程的擦除和编程封装没有检查 HAL_FLASHEx_Erase()、HAL_FLASH_Program() 的返回值,部分声明为 uint8_t 的函数也没有实际 return。在 C 中从非 void 函数末尾返回属于未定义行为。量产代码必须逐页检查擦除、写入和回读结果,并在失败时保持升级标志、停止跳转。

1.2.6 单槽覆盖的掉电风险

当前方案直接擦除旧 APP。如果中途掉电:

  • Bootloader 和参数页未被覆盖时,仍可能在下次复位进入升级模式进行恢复;
  • 但旧 APP 已不完整,无法回退;
  • 如果参数页恰在更新时损坏,inter_flash_cfg_init()又会永久阻塞。

更稳健的系统会使用 A/B 双 APP 槽、外部 Flash 暂存,或至少为参数保存冗余副本和状态机,只有整包验证成功后才原子地切换活动镜像。

1.3 建议的调试观察顺序

学习本工程时,不要只看最终是否出现 APP2 Start,应分层观察:

  1. USART1 是否打印 Bootloader 标题和 flash cfg size:12;
  2. 参数 magic/checksum 是否通过;
  3. ota_flag 是 0 还是 1;
  4. 标志为 0 时 APP 初始 MSP、复位入口和跳转是否正确;
  5. 标志为 1 时 USART2 是否持续发送 C;
  6. YMODEM 是否得到文件名和正确大小;
  7. 每个 2 KiB 页的擦除、写入、回读地址是否递增;
  8. 只有所有验证通过后才清标志并跳转;
  9. APP 启动后中断是否恢复、VTOR 是否为 0x08008000。

官方 IAP 参考流程可查阅 ST AN4657。其中同样强调:跳转时设置 APP 主栈,APP 启动后使用自己的中断向量表,下载前检查可用 Flash 空间,并在写入后验证数据。

二、学习检查

完成本课后,应能回答:

  1. 为什么参数结构计算校验时使用 sizeof-3?
  2. ymodem_run_flag 与 Flash 中的 ota_flag 有什么区别?
  3. 为什么 ota_flag 必须在固件确认成功后再清零?
  4. APP 地址的前两个 32 位字分别是什么?
  5. 只修改 Keil 的 APP 起始地址、不修改 VTOR 会出现什么问题?
  6. 当前源码有哪些地方可能让超长、非整页或传输损坏的固件被误判为成功?