仪表盘APP移植
238.5.2 仪表盘APP移植
#include "inter_flashif.h" /* main.c:取得内部 Flash 参数区和 APP 区地址、读写接口的声明。 */
#include "inter_flash_cfg.h" /* main.c:取得升级配置结构、初始化函数和升级标志写入函数的声明。 */
__set_PRIMASK(0); /* main.c:把 PRIMASK 清零,重新允许可屏蔽中断;Bootloader 跳转前曾把它置 1。 */
printf(" Car APP uart1\r\n"); /* main.c:经 USART1 打印当前运行的是仪表盘 APP,便于区分 Bootloader 日志。 */
printf("----------------------------------------------------------------\r\n"); /* main.c:打印分隔线,让启动日志更容易阅读。 */
printf("flash cfg size:%d\r\n", sizeof(inter_flash_cfg_param_typeDef)); /* main.c:打印配置结构体大小;按 1 字节对齐后应为 12 字节。 */
printf("----------------------------------------------------------------\r\n"); /* main.c:再次打印分隔线。 */
inter_flash_cfg_init(); /* main.c:读取并验证 0x08006000 参数区的魔数和累加校验和。 */
if (get_ota_flag() == 1) /* main.c 主循环:CAN 处理层确认收到合法升级请求后,返回 1。 */
{ /* 进入升级准备分支。 */
inter_flash_cfg_set_app_update_flag(1); /* 把参数区中的 ota_flag 写为 1,让下次启动的 Bootloader 留在升级模式。 */
printf("Restarting...\r\n"); /* 在复位前输出提示;printf 已重定向到 USART1。 */
HAL_Delay(1000); /* 延时 1 秒,让日志有机会发送完毕;本课保留原延时。 */
HAL_NVIC_SystemReset(); /* 请求系统复位;复位后从 0x08000000 的 Bootloader 重新开始。 */
} /* 结束升级准备分支。 */
上面是本课加入 Core/Src/main.c 的 APP 交接代码。它不是把程序“直接跳回 Bootloader”,而是先把升级标志持久化到 Flash,再执行系统复位;复位向量仍位于 0x08000000,因此首先运行 Bootloader。
BSP/Canif/can_handle.c:接收升级控制帧并封装状态查询。
#include "usart.h" /* 取得 USART1 句柄及相关声明,供本文件打印调试日志。 */
#include <stdio.h> /* 声明 printf,避免隐式声明。 */
static uint8_t tp_ota_flag = 0; /* 文件内升级请求标志,static 限制其只能被 can_handle.c 直接访问。 */
uint8_t get_ota_flag(void) /* 提供给 main.c 的只读查询接口;void 表示函数不接收参数。 */
{ /* 开始 getter 函数。 */
return tp_ota_flag; /* 返回 CAN 解析层保存的升级请求状态。 */
} /* 结束 getter 函数。 */
else if (recv_id == CAN_ID_CALL_OTA) /* 与 CAN_ID_UI 同级:标准 ID 为 0x0B3 时按升级控制帧解析。 */
{ /* 开始升级控制帧分支。 */
if ((rx_data[0] == 0xBE) && /* 第 0 字节必须为升级控制头的第一个字节 BE。 */
(rx_data[1] == 0xAD) && /* 第 1 字节必须为 AD。 */
(rx_data[2] == 0xBE) && /* 第 2 字节必须为 BE。 */
(rx_data[3] == 0xEF)) /* 第 3 字节必须为 EF,四字节合起来是 BE AD BE EF。 */
{ /* 魔数匹配后再判断命令值。 */
if (rx_data[4] == 0x01) /* 第 4 字节为 01 表示请求进入升级模式。 */
{ /* 开始升级命令分支。 */
tp_ota_flag = 1; /* 只设置 RAM 标志;真正写 Flash 和复位由 main.c 完成。 */
printf("[tp server] Get ota flag\r\n"); /* 经 USART1 记录已收到升级请求。 */
} /* 结束升级命令分支。 */
} /* 结束魔数校验分支。 */
} /* 结束 CAN ID 0x0B3 分支。 */
这里的 else if 必须接在 if (recv_id == CAN_ID_UI) { ... } 的结束花括号之后,仍位于 if (recv_len > 0) 内。课程画面中的流程图曾把第 5 字节画成 00,老师在视频中也指出应改为 01;最终工程和上一课的 C8T6 发送代码都使用 BE AD BE EF 01 AA AA AA。
BSP/Canif/can_handle.h:新增 CAN ID 和函数声明。
#define CAN_ID_CALL_OTA 0x0B3 /* 把升级请求标准 ID 定义为 0x0B3,供接收分支统一引用。 */
uint8_t get_ota_flag(void); /* 声明升级状态查询函数;末尾分号不可省略。 */
Core/Src/usart.c:把 printf 重定向到 USART1。
#include <stdio.h> /* 声明 FILE 和标准输出相关接口,配合 MicroLIB 使用。 */
int fputc(int ch, FILE *f) /* MicroLIB 调用该函数输出一个字符;参数 f 在本实现中未使用。 */
{ /* 开始字符输出函数。 */
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); /* 阻塞发送 ch 的最低 1 字节,超时参数为 0xFFFF ms。 */
return ch; /* 按 fputc 约定返回已输出字符;失败处理在本课代码中未实现。 */
} /* 结束字符输出函数。 */
Core/Src/system_stm32f1xx.c:重定位 APP 的中断向量表。
#if defined(USER_VECT_TAB_ADDRESS) /* 只有 Keil 的 C/C++ Define 中加入该宏,下面的重定位代码才参与编译。 */
//SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; /* 课程注释掉模板默认表达式,因为默认 offset 仍是 0。 */
SCB->VTOR = 0x08008000; /* 把 Cortex-M3 的向量表基址改为 APP 的链接起点。 */
#endif /* USER_VECT_TAB_ADDRESS */ /* 结束条件编译范围。 */
BSP/inter_flashif:本课复制进工程、并由上述代码实际使用的关键接口与参数结构。
#pragma pack(1) /* 按 1 字节对齐,保证结构体布局与 12 字节 info.bin 一致。 */
typedef struct { /* 定义保存在内部 Flash 参数区的数据格式。 */
uint8_t magic[4]; /* 四字节魔数,期望依次为 AA BB CC DD。 */
uint32_t ota_bin_version; /* 保存待升级固件版本;STM32F1 按小端序解释这四字节。 */
uint8_t ota_flag; /* 升级标志:APP 写 1 请求升级,Bootloader 完成后清 0。 */
uint8_t checksum; /* 保存配置内容的低 8 位累加校验和。 */
uint8_t format[2]; /* 两字节占位,使整个结构体为 12 字节且可按 32 位字写回。 */
}inter_flash_cfg_param_typeDef; /* 结束配置结构体并定义类型名。 */
#pragma pack() /* 恢复编译器默认对齐,避免影响后续结构体。 */
#define INTER_FLASH_PARAM_ADDR (0x8006000U) /* 参数区起点,规范写法等价于 0x08006000。 */
#define INTER_FLASH_APP_ADDR (0x8008000U) /* APP 区起点,规范写法等价于 0x08008000。 */
uint8_t inter_flash_cfg_set_app_update_flag(uint8_t flag) /* 修改配置区里的升级标志。 */
{ /* 开始升级标志写入函数。 */
uint8_t ret = inter_flash_cfg_get_param_sector(); /* 先从 Flash 读出原配置并校验,避免覆盖其他字段。 */
if(ret != 0) /* 非 0 表示魔数或校验失败。 */
{ /* 开始错误分支。 */
printf("flash get update flag error\r\n"); /* 输出参数区读取失败提示。 */
return -1; /* 原工程保留该写法;uint8_t 返回类型会把 -1 转换为 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); /* 重算 magic、version 和 flag 共 9 字节的累加校验。 */
inter_flashif_smart_write_page(INTER_FLASH_PARAM_ADDR, (uint32_t *)&flash_cfg_param, sizeof(flash_cfg_param)/sizeof(uint32_t)); /* 擦除参数页并按 3 个 32 位字写回 12 字节配置。 */
return 0; /* 返回 0 表示本函数已执行写回流程。 */
} /* 结束升级标志写入函数。 */
这些代码均按配套 32Car_APP 工程保留,只补充逐行解释;没有把后续课程代码提前加入。Flash 底层的 HAL 擦写实现未整段抄录,因为本课只复制现成模块,实际新增业务是“校验控制帧→置 RAM 标志→更新参数区→复位”。
一、硬件连接及实验演示
本节把第 28 个最终仪表盘工程 28Can_Recv_Ok 复制为 32Car_APP,使它能与放在 Flash 起始位置的 Bootloader 配套运行。所谓“改成 APP”,包含四类改动:
- APP 能识别 C8T6 发来的升级请求,并在参数区写入升级标志。
- APP 的代码和向量表从
0x08008000开始,给 Bootloader 与参数区留出空间。 - Bootloader 跳转前关闭了中断,APP 启动后重新允许中断。
- 编译结束后自动生成可由升级链路传输的
APP_Car.bin。
仪表盘业务、LVGL 界面、RTC 和原有 CAN UI 数据解析保持不变。Bootloader 负责启动判定、固件接收与跳转;APP 负责正常业务,并在收到升级命令时提出下一次启动进入升级模式的请求。
二、仪表盘代码改成APP
2.1 任务信息资源导图
课程先打开较早的 10Can_DHT11_OLED_IAP 工程作为 IAP 移植参考,再复制已完成的 28Can_Recv_Ok 仪表盘工程。这样做的目的是把“已经验证过的升级框架”移到“已经验证过的业务工程”中,减少同时调试多个未知问题。
参考工程只能提供思路,不能机械复制整个工程。移植时应逐项确认目标 MCU、Flash 容量、APP 起点、参数区地址、CAN ID、串口实例和链接器设置均与当前工程相符。本课两边都是 STM32F103ZET6 布局,APP 起点统一为 0x08008000。
2.2 实验工程分析
运行时状态转换如下:
仪表盘 APP 正常运行 → 收到 CAN 0x0B3 控制帧 → 校验 BE AD BE EF 01 → tp_ota_flag=1 → 主循环把 Flash 中 ota_flag 写为 1 → 延时 → 系统复位 → Bootloader 检测到标志并等待升级。
其中有两个不同的标志:
tp_ota_flag位于 RAM,只用于把 CAN 解析结果传给主循环,掉电或复位后会消失;- 配置结构中的
ota_flag位于0x08006000的 Flash,复位后仍存在,Bootloader 正是读取它来决定是否升级。
因此只设置 tp_ota_flag 而不写 Flash,复位后 Bootloader 不会知道 APP 曾收到升级请求;只写 Flash 而不复位,当前仪表盘 APP 仍会继续运行,也不会自动进入 Bootloader。
2.3 工作流程图
2.3.1 添加H头文件
从参考工程复制整个 inter_flashif 文件夹到 32Car_APP/BSP。配套最终工程包含四个文件:
inter_flashif.c/.h:内部 Flash 的读取、擦页、写入和累加校验接口;inter_flash_cfg.c/.h:12 字节配置结构、参数校验以及升级标志读写。
在 Keil 的 BSP 分组中只需加入两个 .c 文件;.h 文件通过 #include 使用,不作为编译单元单独添加。随后在 Options for Target → C/C++ → Include Paths 中加入 ..\BSP\inter_flashif。课程操作时文件选择窗口一度默认停在旧工程路径,必须确认实际添加的是第 32 个工程里的文件,否则会出现“本机能编译、复制工程后丢文件”或改错工程的问题。
2.3.2 更改main.c初始化
在 main.c 中完成三处改动:
- 在用户头文件区包含
inter_flashif.h和inter_flash_cfg.h。 - 在
SystemClock_Config()之后的USER CODE BEGIN SysInit中执行__set_PRIMASK(0)。 - 所有 CubeMX 外设初始化完成、尤其是
MX_USART1_UART_Init()完成后,再打印 APP 日志并调用inter_flash_cfg_init()。
第三点的顺序很重要:这些 printf 最终调用 USART1,若串口尚未初始化,日志不能可靠输出。inter_flash_cfg_init() 会读取 0x08006000,检查魔数 AA BB CC DD 及校验和;若 info.bin 没有正确烧入,它会反复打印错误并停在循环中,仪表盘界面不会继续初始化。
__set_PRIMASK(0) 的背景也要说准确:配套 Bootloader 的跳转函数在调用 APP 复位入口前执行 __set_PRIMASK(1),因此 APP 需要清除 PRIMASK,才能让 TIM6、CAN 等可屏蔽中断重新响应。它恢复的是 CPU 全局中断屏蔽状态,不能替代各外设和 NVIC 的初始化。
2.3.3 添加can指令代码
在 can_handle.c 中加入 usart.h、stdio.h、文件内静态标志和 getter,然后把升级分支接到 UI 报文分支之后。判断次序是:先确认确实收到了 CAN 帧,再按 ID 分流,最后检查升级魔数和命令字。
CAN ID 0x0B3 只表明这是升级控制通道,数据中的 BE AD BE EF 再提供一次业务层识别,rx_data[4]==0x01 才表示开始升级。当前代码没有校验 recv_len 是否至少为 5,也没有检查最后三个 0xAA;实验发送固定 8 字节帧,因此能正常运行,但移植到不可信网络时应补齐长度和完整性检查。
2.3.4 更改Keil设置
把前面代码加入主循环后,get_ota_flag() 为 1 时依次执行:写 Flash 标志、打印 Restarting...、延时 1 秒、系统复位。代码应放在 while (1) 内,并在日常 LVGL/CAN 业务之前检查,这样收到请求后不会继续长时间执行业务逻辑。
课程口述中的“在任意位置粘贴”容易误解。C 代码必须处在正确的函数和作用域中:升级判断属于 main() 的 while (1);getter 的定义属于 can_handle.c 文件作用域;getter 的声明属于 can_handle.h 的头文件保护范围。
2.3.5 更改main.c
本小节实际仍是在补充主循环的升级判断。原笔记中的“添加 lca 符”“print 符”是转录错误,分别应理解为给 if 补齐正确的花括号、处理 printf 标红。
如果 printf 在编辑器中标红,应先检查是否包含 <stdio.h>、是否声明重定向函数以及工程是否启用匹配的 C 库;不能靠随意加入函数声明消除错误。完成后还要确认链接结果里确实使用了自己的 fputc。
2.3.6 串口重定向
课程采用 Arm Compiler 5 + MicroLIB 的方式:
- 在 Options for Target → Target 勾选 Use MicroLIB;最终工程文件中的
useUlib为 1。 - 在
usart.c的用户头文件区包含<stdio.h>。 - 在文件末尾的
USER CODE BEGIN 1区域实现fputc(),把单字符发送到huart1。
当前 fputc() 使用阻塞式 HAL_UART_Transmit,每个字符最多等待 0xFFFF ms,适合少量调试日志;高频日志会拖慢主循环。参数 FILE *f 没有使用是这个简单重定向写法的正常现象。
2.4 更改Keil设置
2.4.1 更改Keil设置与ROM运行地址和大小
在 Options for Target → Target 中把 IROM1 改为:
| 字段 | 数值 | 含义 |
|---|---|---|
| Start | 0x08008000 | APP 的链接与执行起点 |
| Size | 0x00078000 | 从 0x08008000 到 512 KiB Flash 末端的可用空间,即 480 KiB |
计算过程是 0x08080000 - 0x08008000 = 0x78000。区间末端是开区间 0x08080000,因此不会越过 ZET6 的 512 KiB 内部 Flash。
这项设置决定链接器把向量表和代码放在哪里。配套 .sct 文件也显示加载区与执行区均从 0x08008000 开始,最大长度为 0x78000。只在下载工具里填写地址,不能修正一个仍链接到 0x08000000 的 APP。
2.4.2 CAN与IAP固件升级、LVGL专题介绍
LVGL 使固件较大,但分区原则没有变化。最终构建日志显示 APP 使用的 Flash 约 312.33 KiB,占 480 KiB APP 分区约 65.07%,仍有约 167.7 KiB 余量;RAM 为 61.13 KiB/64 KiB,余量仅 2,944 B。RAM 已很紧张,后续添加大缓冲区时要检查 map 文件,而不能只看 Flash 还剩多少。
课程画面在修改设置前曾显示 512 KiB Flash 的占用比例;改成 APP 分区后,分母应是 480 KiB。两组百分比来自不同链接布局,不能直接比较成“代码突然变大”。
2.4.3 工程文件与代码修改细节
原笔记中的“0s八六个零”“八两个零”“七八零零”都是地址转写错误。应直接核对完整十六进制数:
- 原普通工程:IROM1 Start=
0x08000000,Size=0x00080000; - APP 工程:IROM1 Start=
0x08008000,Size=0x00078000。
少写一个 0、把 0x08008000 看成 0x08080000,都会把程序放到完全不同的位置。输入后重新打开 Target 页确认一次,比凭“倒数第几个零”记忆可靠。
2.4.4 代码修改与Keil设置更改的详细步骤
在 Options for Target → C/C++ → Define 的已有宏之后加入 USER_VECT_TAB_ADDRESS。最终字符串为:
USE_HAL_DRIVER,STM32F103xE,USER_VECT_TAB_ADDRESS
宏之间使用英文逗号。该宏本身不提供地址,只让 system_stm32f1xx.c 中 #if defined(USER_VECT_TAB_ADDRESS) 包围的向量表重定位语句参与编译;真正地址由本课写入的 SCB->VTOR = 0x08008000 给出。
2.4.5 查找与修改特定代码段的方法
可以从启动文件 startup_stm32f103xe.s 中的 SystemInit 调用跳转到 system_stm32f1xx.c,也可以直接搜索 SCB->VTOR。最终工程注释掉模板语句:
SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET;
随后直接写 SCB->VTOR = 0x08008000;。链接地址和 VTOR 必须一致:前者决定代码及向量数据放在哪里,后者决定中断发生时 CPU 到哪里取处理函数地址。只改其一时,普通顺序代码可能暂时运行,但 TIM6、CAN、USART 等中断会跳错位置。
这处修改位于 CubeMX 提供的系统文件,重新生成工程时可能被覆盖;重新生成后应再次核对,或在自己的工程规范中采用可保留的等效配置。
2.4.6 添加编译后指令与生成bin文件
在 Options for Target → User → After Build/Rebuild 的 Run #2 中填入并勾选:
fromelf.exe --bin -o "APP_Car.bin" "#L"
#L 由 Keil 展开为当前链接输出的 .axf 路径,fromelf 再生成无地址信息的原始二进制文件。最终构建日志显示实际执行为:
fromelf.exe --bin -o "APP_Car.bin" "12LCD_Display\12LCD_Display.axf"
.bin 不携带目标地址,所以它用于 ST-LINK Utility 时仍必须手工填 0x08008000,用于 Bootloader 升级时则由 Bootloader 固定写到 APP 分区。
2.4.7 修改system_stm32f1xx.c文件与Keil编译设置
完成 ROM 起点、ROM 大小、预处理宏、VTOR、MicroLIB、Include Path 和 After Build 指令后重新 Build。老师口述“没有报错”指 0 Error;配套最终构建日志实际上是 0 Error、4 Warning,其中包括文件末尾缺少换行,以及 inter_flash_cfg_get_app_update_flag() 使用旧式空参数列表。警告不妨碍本次演示生成固件,但不能把“能生成 bin”理解为“工程完全无警告”。
最终的 APP_Car.bin 位于 32Car_APP/MDK-ARM,本地文件精确大小为 320,012 B(312.51 KiB)。Windows 资源管理器按 KiB 向上显示时可看到约 313 KB,与视频一致。应同时核对时间戳,防止误选旧构建产物。
2.5 应用案例
2.5.1 例题:验证bin文件
如果 ZET6 已经正确烧有 Bootloader.bin 和 info.bin,验证本节 APP 时只需把新生成的 APP_Car.bin 写到 0x08008000。若芯片内容未知,则按完整布局重新烧录:
| 文件 | 地址 |
|---|---|
Bootloader.bin | 0x08000000 |
info.bin | 0x08006000 |
APP_Car.bin | 0x08008000 |
烧录前把 ST-Link 从 C8T6 转接到 ZET6,并确认工具识别到目标芯片。不要把“文件夹里有 bin”当成全部验证;还应查看 Build 日志中的 0 Error、文件大小、生成时间和 map 文件中的 __Vectors = 0x08008000。
三、移植仪表盘工程
视频第一次选择 APP_Car.bin 后,没有把 ST-LINK Utility 的 Start address 从默认 0x08000000 改成 0x08008000,因此新 APP 覆盖了 Flash 起始处的 Bootloader,并因整片/扇区擦除影响了原布局。复位后没有正常界面,老师随即发现地址错误。
正确操作是:
- 选择
32Car_APP/MDK-ARM/APP_Car.bin,核对时间戳和大小。 - 把 Start address 明确设为
0x08008000。 - 开始下载并启用写后校验。
- 复位 ZET6。Bootloader 读取配置后应跳转到仪表盘 APP,LCD 从先前温湿度界面变回仪表盘。
这里有两个独立地址:Keil 的 IROM1/VTOR 决定固件内部如何链接,ST-LINK Utility 的 Start address 决定 .bin 字节实际写到哪里。两边都必须是 0x08008000。
四、重新烧固件
地址错误已经覆盖启动区域后,仅把 APP 再烧到正确位置还不够,因为复位首先读取 0x08000000。视频采用的恢复顺序虽然先补写 APP、再补 Bootloader 和参数区也能恢复,但更容易核对的完整顺序是:
Bootloader.bin→0x08000000;info.bin→0x08006000;APP_Car.bin→0x08008000。
每次重新选择 .bin 后都检查 Start address,尤其不能把上一个文件的地址沿用到下一个文件。最终复位后仪表盘正常显示,说明本课至少验证了 Bootloader 启动、配置区校验、APP 栈顶检查、跳转到 0x08008000、中断恢复与 LVGL 业务初始化这一整条链路。真正的远程升级还需要在下一步通过 C8T6 和 CAN TP 再测试一次。