APP代码流程

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

196.2.4 APP代码流程

/*
 * 本课源码位置:uint3code/09Ymodem_APP。
 * 整体目的:APP 正常运行业务的同时,通过 USART1 接收升级命令;收到命令后把升级请求写入
 *           内部 Flash 参数区,再触发系统复位,让 Bootloader 在下次启动时执行固件升级。
 * 标志分工:usart.c 中的 ota_flag 位于 RAM,只负责把中断接收结果通知给 APP 主循环;
 *           flash_cfg_param.ota_flag 最终写入内部 Flash,负责跨越系统复位通知 Bootloader。
 * 收录范围:下面只保留视频实际讲解的 APP 业务代码,不展开 STM32 HAL 库函数的实现。
 */

#define INTER_FLASH_PARAM_ADDR (0x8006000U) /* 定义参数区地址;数值等同于常见写法 0x08006000。 */

#pragma pack(1)                              /* 要求编译器按 1 字节边界排列下面的结构体成员。 */
typedef struct {                             /* 定义 APP 与 Bootloader 共同解释的 Flash 参数格式。 */
    uint8_t magic[4];                        /* 4 字节魔数,合法值依次为 AA、BB、CC、DD。 */
    uint32_t ota_bin_version;                /* 保存待升级固件的版本字段,本课没有修改它。 */
    uint8_t ota_flag;                        /* 持久化升级标志:0 表示不升级,1 表示请求升级。 */
    uint8_t checksum;                        /* 保存前 9 字节的 8 位累加和。 */
    uint8_t format[2];                       /* 保留 2 字节,使整个结构体长度为 12 字节。 */
}inter_flash_cfg_param_typeDef;              /* 给该参数结构体定义课程源码使用的类型名。 */
#pragma pack()                               /* 恢复编译器原来的默认对齐规则。 */

static inter_flash_cfg_param_typeDef flash_cfg_param; /* 保存从 Flash 读出的参数副本,或待写回的新参数。 */

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)); /* 从 0x08006000 读取 12 字节到 RAM 结构体。 */
    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");   /* 经 USART1 打印参数格式错误。 */
        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) /* 将计算值与 Flash 中保存的 checksum 比较。 */
    {                                       /* 两个校验和值不相等时进入错误分支。 */
        printf("flash checksum error\r\n"); /* 经 USART1 打印校验和错误。 */
        return 2;                            /* 返回 2,通知调用者参数内容校验失败。 */
    }                                       /* 校验和错误分支结束。 */

    return 0;                               /* 魔数和校验和都正确,返回成功。 */
}                                           /* 参数读取函数结束。 */

uint8_t inter_flash_cfg_init(void)          /* APP 启动时检查参数结构和 Flash 参数区。 */
{                                           /* Flash 参数初始化函数开始。 */
    if(sizeof(inter_flash_cfg_param_typeDef) % 4 != 0) /* 检查结构体长度能否被 4 整除。 */
    {                                       /* 长度不满足 32 位整字写入条件时进入错误循环。 */
        while(1)                            /* 参数布局错误时停止后续业务,不再继续启动 APP。 */
        {                                   /* 结构体长度错误循环开始。 */
            printf("flash cfg param error\r\n"); /* 每轮打印结构体布局错误。 */
            HAL_Delay(1000);                /* 等待 1 秒,避免错误日志连续刷屏。 */
        }                                   /* 结构体长度错误循环结束;正常情况下不会退出。 */
    }                                       /* 结构体长度检查结束。 */

    uint8_t ret = inter_flash_cfg_get_param_sector(); /* 读取并校验 0x08006000 中的参数。 */
    if(ret != 0)                            /* 非 0 表示魔数或校验和不正确。 */
    {                                       /* Flash 参数内容错误时进入错误循环。 */
        while(1)                            /* 阻止 APP 在参数不可信的情况下继续执行。 */
        {                                   /* 参数内容错误循环开始。 */
            printf("flash cfg param sector error\r\n"); /* 每轮打印参数区错误。 */
            HAL_Delay(1000);                /* 延时 1 秒后再次打印。 */
        }                                   /* 参数内容错误循环结束;正常情况下不会退出。 */
    }                                       /* Flash 参数内容检查结束。 */

    return 0;                               /* 参数结构和参数内容都合法,初始化完成。 */
}                                           /* Flash 参数初始化函数结束。 */

uint8_t inter_flash_cfg_set_app_update_flag(uint8_t flag) /* 修改持久化升级标志并写回 Flash。 */
{                                           /* 升级标志写入函数开始。 */
    uint8_t ret = inter_flash_cfg_get_param_sector(); /* 修改前先读取并校验现有的完整参数。 */
    if(ret != 0)                            /* 参数读取或校验失败时不能安全地继续修改。 */
    {                                       /* 读取失败分支开始。 */
        printf("flash get update flag error\r\n"); /* 打印升级标志读取失败信息。 */
        return -1;                          /* 保留课程源码写法;对 uint8_t 返回类型而言该值会变成 255。 */
    }                                       /* 读取失败分支结束。 */
    flash_cfg_param.ota_flag = flag;        /* 用实参更新 RAM 参数副本中的持久化升级标志。 */

    flash_cfg_param.checksum = inter_flash_checksum((uint8_t *)&flash_cfg_param, sizeof(flash_cfg_param) - 3); /* 标志改变后重新计算前 9 字节的累加和。 */

    inter_flashif_smart_write_page(INTER_FLASH_PARAM_ADDR, (uint32_t *)&flash_cfg_param, sizeof(flash_cfg_param)/sizeof(uint32_t)); /* 擦除参数页并把 12 字节作为 3 个 32 位字写回。 */

    return 0;                               /* 课程上层以 0 表示本次参数更新流程结束。 */
}                                           /* 升级标志写入函数结束。 */

static uint8_t ota_flag = 0;                /* USART1 模块中的 RAM 标志,上电初值为 0。 */
static uint8_t uart1_rec_byte = 0;          /* 暂存 USART1 本次中断收到的一个字节。 */
static UART_RX_typeDef uart1_rx_data;       /* 保存升级命令的接收缓冲区和当前字节计数。 */

void uart_receive_enable()                  /* 启动 USART1 的单字节中断接收。 */
{                                           /* 串口接收启动函数开始。 */
    HAL_UART_Receive_IT(&huart1, (uint8_t*)&uart1_rec_byte, 1); /* 请求收到 1 字节后进入 HAL 接收完成回调。 */
}                                           /* 串口接收启动函数结束。 */

uint8_t uart_get_ota_flag()                 /* 为主循环提供 RAM 升级标志的读取接口。 */
{                                           /* RAM 标志读取函数开始。 */
    return ota_flag;                        /* 返回 USART1 回调设置的 ota_flag。 */
}                                           /* RAM 标志读取函数结束。 */

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) /* HAL 在一个字节接收完成后调用此函数。 */
{                                           /* 串口接收完成回调开始。 */
    if(huart ->Instance == USART1)           /* 只处理由 USART1 触发的接收完成事件。 */
    {                                       /* USART1 处理分支开始。 */
        uart1_rx_data.rx_buf[uart1_rx_data.rx_cnt] = uart1_rec_byte; /* 把刚收到的字节追加到命令缓冲区。 */
        uart1_rx_data.rx_cnt ++;             /* 接收计数加 1,使其指向下一个待写位置。 */

        if(uart1_rx_data.rx_buf[0] == 0xAF) /* 只有第一个字节为帧头 0xAF 才继续解析。 */
        {                                   /* 帧头正确分支开始。 */
            if(uart1_rx_data.rx_cnt >= 48)  /* 接收计数达到课程设定的上限时判定数据过长。 */
            {                               /* 数据过长分支开始。 */
                uart1_rx_data.rx_cnt = 0;   /* 丢弃当前帧并从缓冲区起点重新接收。 */
                printf("ota data len error\r\n"); /* 打印命令长度错误。 */
            }                               /* 数据过长分支结束。 */
            else                            /* 当前接收长度还没有超过课程设定的上限。 */
            {                               /* 正常长度分支开始。 */
                if(uart1_rx_data.rx_cnt > 2) /* 至少收到帧头、长度和一个后续字节才解析长度。 */
                {                           /* 已具备基础帧字段时继续检查。 */
                    uint8_t data_len = uart1_rx_data.rx_buf[1]; /* 第 2 字节表示 DATA 区长度。 */

                    if((data_len > 0) && (data_len < 50)) /* 课程协议要求 DATA 长度位于 1~49。 */
                    {                       /* 长度字段表面合法时继续等待完整帧。 */
                        if(uart1_rx_data.rx_cnt >= (data_len + 3)) /* 完整帧总长为帧头 1 B、长度 1 B、DATA 和帧尾 1 B。 */
                        {                   /* 已收到长度字段声明的整帧。 */
                            if(uart1_rx_data.rx_buf[data_len + 2] == 0xFA) /* 检查 DATA 后面的字节是否为帧尾 0xFA。 */
                            {               /* 帧头、长度和帧尾均通过检查。 */
                                ota_flag = 1; /* 设置 RAM 标志,通知 APP 主循环发起升级切换。 */
                                uart1_rx_data.rx_cnt = 0; /* 当前帧处理结束,清零接收计数。 */
                                printf("OTA FLAG SET, VER:%d.%d.%d\r\n",uart1_rx_data.rx_buf[2],uart1_rx_data.rx_buf[3],uart1_rx_data.rx_buf[4]); /* 把 DATA 的前三字节按版本号打印。 */
                            }               /* 正确帧尾分支结束。 */
                            else            /* 指定位置不是帧尾 0xFA。 */
                            {               /* 帧尾错误分支开始。 */
                                uart1_rx_data.rx_cnt = 0; /* 丢弃错误帧并准备重新接收。 */
                                printf("ota data tail error\r\n"); /* 打印帧尾错误。 */
                            }               /* 帧尾错误分支结束。 */
                        }                   /* 完整帧处理结束。 */
                    }                       /* 合法长度字段处理结束。 */
                    else                    /* DATA 长度字段为 0 或不小于 50。 */
                    {                       /* 长度字段错误分支开始。 */
                        uart1_rx_data.rx_cnt = 0; /* 丢弃当前错误帧。 */
                        printf("ota data len error\r\n"); /* 打印命令长度错误。 */
                    }                       /* 长度字段错误分支结束。 */
                }                           /* 基础帧字段检查结束。 */
            }                               /* 正常长度分支结束。 */
        }                                   /* 帧头正确分支结束。 */
        else                                /* 第一个接收字节不是帧头 0xAF。 */
        {                                   /* 错误帧头分支开始。 */
            uart1_rx_data.rx_cnt = 0;       /* 丢弃该字节,下一字节仍从帧头位置开始判断。 */
        }                                   /* 错误帧头分支结束。 */

        HAL_UART_Receive_IT(&huart1, &uart1_rec_byte, 1); /* 重新挂起 1 字节中断接收,否则只能收到第一个字节。 */
    }                                       /* USART1 处理分支结束。 */
}                                           /* 串口接收完成回调结束。 */

__set_PRIMASK(0);                           /* 清除全局中断屏蔽,恢复 Bootloader 跳转前关闭的可屏蔽中断。 */
uart_receive_enable();                      /* 开始监听 USART1 发来的升级命令。 */
printf("                    APP  can Start\r\n"); /* 打印课程源码中的 APP 启动提示。 */
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 参数内容。 */

while (1)                                  /* APP 进入不会主动退出的前台主循环。 */
{                                           /* APP 主循环开始。 */
    if(uart_get_ota_flag() == 1)            /* 轮询 USART1 回调是否已经收到有效升级命令。 */
    {                                       /* 收到升级命令后开始切换到 Bootloader。 */
        inter_flash_cfg_set_app_update_flag(1); /* 把 Flash 中可跨复位保存的升级标志置 1。 */
        printf("Restarting...\r\n");       /* 通知调试串口系统即将复位。 */
        HAL_Delay(1000);                    /* 留出 1 秒,使提示信息有时间发送完成。 */

        HAL_NVIC_SystemReset();             /* 请求 MCU 系统复位,复位后先重新执行 Bootloader。 */
    }                                       /* 升级切换分支结束。 */

    HAL_Delay(1000);                        /* 当前示例每秒查询一次 RAM 升级标志。 */
}                                           /* 主循环结束位置;while(1) 会再次回到循环顶部。 */

本课打开的是 uint3code/09Ymodem_APP。学习重点不是在 APP 中接收并写入整个固件,而是弄清 APP 怎样接收一条“请求升级”的控制命令,并把执行权可靠地交回 Bootloader。

一、APP代码流程分析

1.1 打开工程

APP 与 Bootloader 是两个独立工程,但必须遵守同一套约定:

共同约定本课程的值作用
Flash 参数地址0x08006000让 APP 写入、Bootloader 读取同一个升级状态
APP 起始地址0x08008000Bootloader 写入和跳转的目标,也是 APP 的链接地址
参数结构12 字节 inter_flash_cfg_param_typeDef保证两边以相同字段偏移解释 Flash 原始字节
ota_flag 语义0 不升级,1 请求升级在复位前后传递升级请求

APP 工程的 Keil scatter 文件把代码链接到 0x08008000,system_stm32f1xx.c 又把 SCB->VTOR 设置为 0x08008000。前者决定程序被放到哪里,后者决定中断发生时去哪里查找中断向量;两者必须一致。

1.1.1 main函数

APP 进入 main() 后与本课相关的顺序是:

恢复全局中断
  → 初始化 GPIO、USART1
  → 启动 USART1 单字节中断接收
  → 打印 APP 启动信息和参数结构体大小
  → 校验内部 Flash 参数
  → 进入主循环,等待 RAM 升级标志变为 1

__set_PRIMASK(0)容易被初学者忽略。上一课的 Bootloader 在跳 APP 前关闭了全局中断,因此 APP 要清除 PRIMASK,USART1 接收中断才能真正响应。Arm CMSIS 对 PRIMASK 的定义是:值为 1 时屏蔽所有可配置优先级的异常,值为 0 时不进行这种屏蔽。参见 Arm CMSIS Core Register Access。

uart_receive_enable()调用一次 HAL_UART_Receive_IT(..., 1),含义是“用中断方式接收一个字节”。每接收完一个字节,HAL 调用 HAL_UART_RxCpltCallback();回调末尾必须再次调用该函数,才能继续接收下一个字节。

课程源码实际打印的是 APP can Start,不是原笔记中的 APP 1 start。

1.1.1.1 Flash 参数初始化与校验

参数结构体字段布局如下:

偏移字段长度含义
0x00magic[4]4 B固定为 AA BB CC DD,用于识别参数格式
0x04ota_bin_version4 B固件版本字段
0x08ota_flag1 B持久化升级状态
0x09checksum1 B前 9 字节的低 8 位累加和
0x0Aformat[2]2 B占位字段

总长度为:

[ 4+4+1+1+2=12\ \text{字节} ]

#pragma pack(1)规定成员之间不插入额外填充,使 APP 与 Bootloader 对字段偏移的理解保持一致。inter_flash_cfg_init()检查的条件实际上是 sizeof(struct) % 4 == 0,也就是总长度能否按 32 位字完整写回;它并没有直接写死“长度必须等于 12”。在当前字段定义下,串口打印结果才是 12。

读取流程分为两层:

  1. 从 INTER_FLASH_PARAM_ADDR 读取 sizeof(flash_cfg_param),即 12 字节;
  2. 检查前四字节是不是 AA BB CC DD;
  3. 对结构体前 12-3=9 字节求累加和;
  4. 将结果与偏移 0x09 处的 checksum 比较。

减去的 3 字节是 checksum 本身和末尾两个 format 占位字节。校验范围包含 ota_flag,所以每次改变升级标志后都必须同步重算校验和,否则 Bootloader 下一次读取时会认为参数已损坏。

源码和 scatter 文件共同确认参数地址为 0x08006000。原转录中的“630 地址”“8006320”,以及原笔记中的 0x80063000 都是识别错误。

1.1.2 while循环

主循环每秒调用一次 uart_get_ota_flag()。当 USART1 回调把 RAM 中的标志置为 1 后,APP 按以下顺序执行:

  1. 调用 inter_flash_cfg_set_app_update_flag(1);
  2. 重新读取并校验原参数;
  3. 只修改结构体 RAM 副本中的 ota_flag;
  4. 因为参与校验的数据变了,所以重新计算 checksum;
  5. 把包含新标志和新校验和的 12 字节整体写回 0x08006000;
  6. 打印 Restarting... 并延时 1 秒;
  7. 调用 HAL_NVIC_SystemReset()发起系统复位。

HAL_NVIC_SystemReset()最终调用 CMSIS 的 NVIC_SystemReset(),通过 AIRCR 寄存器的 SYSRESETREQ 请求系统复位。它不是普通的 C 函数返回,也不是直接跳转到 Bootloader 地址。复位后,处理器重新走复位启动流程,位于 0x08000000 的 Bootloader 再次取得控制权。参见 Arm CMSIS NVIC 文档。

1.1.2.1 uart_get_ota_flag函数

这里有两个名字相近但生命周期完全不同的标志,必须分清:

标志保存位置谁设置/读取复位后是否保留
usart.c 的 static uint8_t ota_flagRAMUSART1 回调设置,APP 主循环读取否
flash_cfg_param.ota_flag内部 Flash 参数区APP 写入,Bootloader 读取并清零是

uart_get_ota_flag()只是返回第一个 RAM 标志,并不会读取 Flash。原笔记把“返回 ota_flag”和“从 Flash 读取升级标志”混在了一起。

USART1 接收协议在源码中写成:

┌──────────┬──────────┬──────────────────┬──────────┐
│ HEAD     │ LEN      │ DATA             │ TAIL     │
│ 0xAF     │ 数据长度 │ LEN 个数据字节   │ 0xFA     │
└──────────┴──────────┴──────────────────┴──────────┘
  1 字节      1 字节       LEN 字节          1 字节

回调确认帧头、长度和帧尾后执行 ota_flag = 1,主循环随后才能观察到升级请求。源码还把 DATA 区前三字节作为版本号打印,但当前函数没有把这个版本号写入 ota_bin_version。

原转录在约 03:46 处把“给这个 OTA 置 1”识别成“OTA -1”,又把 main.c 识别成“MIDI-C”。源码第 183 行和主循环条件都明确使用数值 1,因此正确流程不是判断 -1。

1.2 功能总结

整个升级切换过程可以连成一条完整链路:

上位机经 USART1 发送升级命令
          ↓
APP 串口中断逐字节组帧,检查 0xAF / LEN / 0xFA
          ↓
RAM 中的 ota_flag = 1
          ↓
APP 主循环发现标志,向 0x08006000 写入 Flash ota_flag = 1
          ↓
重算 checksum,写回整个 12 字节参数,然后请求系统复位
          ↓
Bootloader 启动并读取 Flash ota_flag
          ↓
ota_flag = 1:经 USART2/YMODEM 接收固件并写入 0x08008000
          ↓
升级成功后,Bootloader 把 Flash ota_flag 清为 0
          ↓
Bootloader 跳转到 0x08008000,运行新的 APP

APP 和 Bootloader 的职责边界是:

  • APP 继续承担正常业务,并监听“需要升级”的控制命令;
  • APP 不接收、擦写整个新固件,只记录请求并复位;
  • Bootloader 决定是否升级,负责 YMODEM 接收、APP 区擦写、成功后清标志和跳转;
  • APP 写 1 是提出请求,Bootloader 写 0 是确认升级已经成功收尾。

转录中的“打开 Modem 接收”应理解为启动 YMODEM 固件接收流程,而不是打开一个叫 Modem 的外部硬件。ST 官方的 USART IAP 方案同样把 Loader 与用户 APP 分开放置,并包含 Flash 擦除、Flash 编程和 UART/YMODEM 通信,可参阅 ST X-CUBE-IAP-USART 和 AN4657。

1.3 任务分析

视频最后只布置了下一节任务:选择前面某个现有 APP 工程(课程举例为第 4 个工程),把本课分析的升级触发能力移植进去,使它能与 Bootloader 配合完成远程升级。本节没有实施移植,因此这里不提前补写第 197、198 课才会出现的工程修改。

后续移植时至少要同时保证:

  1. APP 的链接起始地址改为 0x08008000;
  2. APP 中断向量表重定位到 0x08008000;
  3. APP 与 Bootloader 使用完全相同的参数地址、结构体布局和校验算法;
  4. APP 能接收升级命令,并在复位前成功把 ota_flag=1 写入 Flash;
  5. Bootloader 升级成功后把标志清零,否则下次复位还会再次进入升级。

二、初学者必须理解的 Flash 写入细节

2.1 写 12 字节为什么会擦除一整页

inter_flash_cfg_set_app_update_flag()调用的 inter_flashif_smart_write_page()不是在原字节上直接把 0 改成 1。STM32F1 内部 Flash 的常规更新过程是“解锁 → 按页擦除 → 编程 → 加锁”。当前 MCU 配置是 STM32F103ZE,属于高密度器件;其主 Flash 页大小为 2 KiB,具体组织方式可查 ST RM0008。

课程封装会擦除从 0x08006000 开始的一整页,然后只写回结构体的 3 个 32 位字:

0x08006000 ── 第 1 个 32 位字:magic[0..3]
0x08006004 ── 第 2 个 32 位字:ota_bin_version
0x08006008 ── 第 3 个 32 位字:ota_flag、checksum、format[0..1]

因此这一页应专门用于该参数,不能在同一页其他位置悄悄存放仍需保留的数据;擦页会把它们一起清掉。写入长度实参 sizeof(struct)/sizeof(uint32_t)等于 12/4=3,单位是“32 位字”,不是字节。

2.2 为什么先写 Flash 再复位

RAM 会在系统复位后重新初始化。如果 APP 只把 usart.c 中的 RAM 标志设为 1 就复位,Bootloader 看不到这个状态。只有先把 1 写进非易失的内部 Flash,升级请求才能跨过复位。

顺序也不能颠倒:如果先执行系统复位,后面的 Flash 写入语句根本没有机会运行。课程中 1 秒延时主要给串口提示留下发送时间;真正决定 Bootloader 能否看到请求的是 Flash 写入已经在复位前完成。

三、源码中应当警惕的问题

以下问题不改变本课要学习的主流程,但说明“实验能够演示”与“可直接用于产品”并不是一回事。

3.1 Flash 写入结果没有被可靠处理

  • inter_flash_cfg_set_app_update_flag()的返回值在 main() 中没有检查,写入失败后仍会继续复位;
  • 底层 inter_flashif_smart_write_page()没有检查 HAL_FLASHEx_Erase()和 HAL_FLASH_Program()的返回状态;
  • 该底层函数声明返回 uint8_t,但正常路径末尾没有 return,这在 C 语言中属于未定义行为;
  • 参数页更新存在“已经擦除、尚未完整写回”的掉电窗口。一旦此时断电,下一次启动可能因 magic 或 checksum 错误而停在死循环。

更稳健的工程应检查每一步返回值,写后回读验证,并考虑双备份参数页、序号或有效标记,避免一次掉电让唯一配置失效。

3.2 串口帧检查不足

当前回调只验证帧头、长度范围和帧尾,没有 CRC、消息认证或升级授权。任何能访问 USART1 的设备只要构造相同外形的帧,就可能触发升级复位。

此外,源码还有几个边界问题:

  • 声明允许 data_len < 50,但接收计数达到 48 就提前丢帧,两个限制并不一致;
  • 打印版本号会读取 DATA[0]~DATA[2],却只要求 data_len > 0,没有明确要求至少 3 字节;
  • printf()在串口接收中断回调里使用阻塞发送,日志较长或串口异常时会延长中断占用时间;
  • ota_flag由中断回调修改、由主循环读取,却没有声明为 volatile。在嵌入式 C 中,这类被异步执行上下文修改的共享变量通常应使用 volatile并设计好并发访问规则。

顶部代码严格保留课程源码,只增加注释;这些问题应在真正产品化时修正,而不是在笔记中悄悄把课程源码改成另一份代码。

四、学习检查

完成本课后,应能回答:

  1. 为什么系统里需要一个 RAM ota_flag 和一个 Flash ota_flag?
  2. 为什么修改 ota_flag 后必须重新计算 checksum?
  3. sizeof(flash_cfg_param)-3为什么等于 9,它排除了哪些字段?
  4. 0x8006000U与 0x08006000U是不是两个不同地址?
  5. 为什么必须先写 Flash,再调用 HAL_NVIC_SystemReset()?
  6. APP 负责接收新固件,还是 Bootloader 负责接收新固件?
  7. 为什么 APP 的链接地址与 SCB->VTOR都要对应 0x08008000?
  8. USART1 回调末尾为什么必须再次调用 HAL_UART_Receive_IT()?