APP代码流程
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 起始地址 | 0x08008000 | Bootloader 写入和跳转的目标,也是 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 参数初始化与校验
参数结构体字段布局如下:
| 偏移 | 字段 | 长度 | 含义 |
|---|---|---|---|
0x00 | magic[4] | 4 B | 固定为 AA BB CC DD,用于识别参数格式 |
0x04 | ota_bin_version | 4 B | 固件版本字段 |
0x08 | ota_flag | 1 B | 持久化升级状态 |
0x09 | checksum | 1 B | 前 9 字节的低 8 位累加和 |
0x0A | format[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。
读取流程分为两层:
- 从
INTER_FLASH_PARAM_ADDR读取sizeof(flash_cfg_param),即 12 字节; - 检查前四字节是不是
AA BB CC DD; - 对结构体前
12-3=9字节求累加和; - 将结果与偏移
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 按以下顺序执行:
- 调用
inter_flash_cfg_set_app_update_flag(1); - 重新读取并校验原参数;
- 只修改结构体 RAM 副本中的
ota_flag; - 因为参与校验的数据变了,所以重新计算
checksum; - 把包含新标志和新校验和的 12 字节整体写回
0x08006000; - 打印
Restarting...并延时 1 秒; - 调用
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_flag | RAM | USART1 回调设置,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 课才会出现的工程修改。
后续移植时至少要同时保证:
- APP 的链接起始地址改为
0x08008000; - APP 中断向量表重定位到
0x08008000; - APP 与 Bootloader 使用完全相同的参数地址、结构体布局和校验算法;
- APP 能接收升级命令,并在复位前成功把
ota_flag=1写入 Flash; - 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并设计好并发访问规则。
顶部代码严格保留课程源码,只增加注释;这些问题应在真正产品化时修正,而不是在笔记中悄悄把课程源码改成另一份代码。
四、学习检查
完成本课后,应能回答:
- 为什么系统里需要一个 RAM
ota_flag和一个 Flashota_flag? - 为什么修改
ota_flag后必须重新计算checksum? sizeof(flash_cfg_param)-3为什么等于 9,它排除了哪些字段?0x8006000U与0x08006000U是不是两个不同地址?- 为什么必须先写 Flash,再调用
HAL_NVIC_SystemReset()? - APP 负责接收新固件,还是 Bootloader 负责接收新固件?
- 为什么 APP 的链接地址与
SCB->VTOR都要对应0x08008000? - USART1 回调末尾为什么必须再次调用
HAL_UART_Receive_IT()?