温湿度检测APP移植
239.5.3 温湿度检测APP移植
#include "canif.h" /* main.c:取得 CAN 过滤器初始化和底层收发接口的声明。 */
#include "can_handle.h" /* main.c:取得 Can_Handle 与升级状态查询函数的声明。 */
#include "inter_flashif.h" /* main.c:取得内部 Flash 地址及底层操作接口的声明。 */
#include "inter_flash_cfg.h" /* main.c:取得配置区初始化和升级标志写入接口的声明。 */
__set_PRIMASK(0); /* main.c:清除全局中断屏蔽;配套 Bootloader 跳转前曾把 PRIMASK 置 1。 */
MX_CAN_Init(); /* main.c:执行 CubeMX 生成的 CAN1 外设初始化,加载 500 kbit/s 位时序。 */
Can_Filter_Init(); /* main.c:配置接收过滤器并启动 CAN 外设,否则轮询接收不到报文。 */
printf(" DHT11 APP uart1\r\n"); /* main.c:经 USART1 输出当前 APP 名称。 */
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 中的配置魔数与校验和。 */
Can_Handle(); /* main.c 主循环:持续轮询 CAN FIFO,缺少此调用就永远不会识别升级控制帧。 */
if(get_ota_flag() == 1) /* main.c 主循环:CAN 解析层已收到合法的升级请求时进入分支。 */
{ /* 开始升级准备分支。 */
inter_flash_cfg_set_app_update_flag(1); /* 把持久化升级标志写为 1,供复位后的 Bootloader 读取。 */
printf("Restarting...\r\n"); /* 输出即将重启的提示。 */
HAL_Delay(1000); /* 等待 1 秒,让串口日志有机会发完;保留课程原值。 */
HAL_NVIC_SystemReset(); /* 触发系统复位,随后从 0x08000000 的 Bootloader 启动。 */
} /* 结束升级准备分支。 */
这是本课在温湿度工程中新增的主干代码。Can_Handle() 是与上一课最容易漏掉的区别:仪表盘工程本来就在主循环中调用它,原温湿度工程没有 CAN 业务,因此移植后必须显式加入。
BSP/Canif/canif.c:本课复制的 CAN 启动函数。
void Can_Filter_Init(void) /* 配置一个接收全部标准帧的过滤器,并启动 CAN1。 */
{ /* 开始过滤器初始化函数。 */
CAN_FilterTypeDef Can_FilterConfig; /* 在栈上创建 HAL 过滤器配置结构。 */
Can_FilterConfig.FilterBank = 0; /* STM32F103ZET6 的 CAN1 使用过滤器组 0。 */
Can_FilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; /* 采用 ID/掩码过滤模式。 */
Can_FilterConfig.FilterScale = CAN_FILTERSCALE_16BIT; /* 使用 16 位过滤器尺度。 */
Can_FilterConfig.FilterIdHigh = 0x0; /* 高半字 ID 设为 0。 */
Can_FilterConfig.FilterIdLow = 0x0; /* 低半字 ID 设为 0。 */
Can_FilterConfig.FilterMaskIdHigh = 0x0; /* 高半字掩码为 0,因此不屏蔽任何 ID 位。 */
Can_FilterConfig.FilterMaskIdLow = 0x0; /* 低半字掩码也为 0,结果是接收全部匹配类型的帧。 */
Can_FilterConfig.FilterFIFOAssignment = CAN_FILTER_FIFO0; /* 把通过过滤器的报文送入接收 FIFO0。 */
Can_FilterConfig.FilterActivation = CAN_FILTER_ENABLE; /* 启用该过滤器。 */
Can_FilterConfig.SlaveStartFilterBank = 14; /* 保留课程对双 CAN 过滤器分界的设置;F103ZE 实验使用 CAN1。 */
if (HAL_CAN_ConfigFilter(&hcan, &Can_FilterConfig) != HAL_OK) /* 把配置提交给 HAL,失败时进入统一错误处理。 */
{ /* 开始错误分支。 */
Error_Handler(); /* 配置失败后停入工程的错误处理循环。 */
} /* 结束错误分支。 */
HAL_CAN_Start(&hcan); /* 启动 CAN 外设;课程代码没有检查此函数返回值。 */
} /* 结束过滤器初始化函数。 */
该过滤器是“全接收”配置,业务层再由 Can_Handle() 按 0x0B4 和 0x0B3 分流。它方便实验,但总线负载大或节点不可信时,应设计更精确的硬件过滤规则。
BSP/Canif/can_handle.c/.h:从 32Car_APP 原样复用的升级控制部分。
#define CAN_ID_CALL_OTA 0x0B3 /* can_handle.h:升级控制帧的 11 位标准 ID。 */
uint8_t get_ota_flag(void); /* can_handle.h:向主循环声明升级状态查询接口。 */
static uint8_t tp_ota_flag = 0; /* can_handle.c:RAM 中的文件内升级请求标志,初值为 0。 */
uint8_t get_ota_flag(void) /* can_handle.c:读取升级请求标志。 */
{ /* 开始 getter。 */
return tp_ota_flag; /* 返回当前标志,不在 getter 中清除。 */
} /* 结束 getter。 */
else if (recv_id == CAN_ID_CALL_OTA){ /* CAN ID 为 0x0B3 时进入升级控制帧解析。 */
if ((rx_data[0] == 0xBE) && /* 校验四字节业务魔数的第 1 字节。 */
(rx_data[1] == 0xAD) && /* 校验四字节业务魔数的第 2 字节。 */
(rx_data[2] == 0xBE) && /* 校验四字节业务魔数的第 3 字节。 */
(rx_data[3] == 0xEF)) /* 校验四字节业务魔数的第 4 字节。 */
{ /* 魔数 BE AD BE EF 匹配后再判断命令。 */
if (rx_data[4] == 0x01) /* 命令字 01 表示请求升级。 */
{ /* 开始升级命令分支。 */
tp_ota_flag = 1; /* 通知主循环写 Flash 标志并复位。 */
printf("[tp server] Get ota flag\r\n"); /* 输出已收到升级请求的日志。 */
} /* 结束升级命令分支。 */
} /* 结束魔数校验分支。 */
} /* 结束升级控制帧分支。 */
复制过来的 can_handle.c 还保留仪表盘用的转速、水温、灯光、日期等 UI 缓存与解析代码;温湿度 APP 没有调用那些 getter,所以它们在本节没有实际显示作用。课程选择原样保留以降低移植风险,精校稿也不擅自删改配套源码。
Core/Src/usart.c:课程采用的 USART1 输出重定向。
#include <stdio.h> /* usart.c:声明 FILE 及标准输出接口,供 fputc 定义使用。 */
int fputc(int ch, FILE *f) /* MicroLIB 输出一个字符时调用本函数。 */
{ /* 开始 fputc。 */
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); /* 阻塞发送 ch 的最低字节到 USART1。 */
return ch; /* 返回已输出字符,符合 fputc 的基本接口约定。 */
} /* 结束 fputc。 */
Core/Src/system_stm32f1xx.c:把中断向量表移到 APP 起点。
#if defined(USER_VECT_TAB_ADDRESS) /* 只有 C/C++ Define 中存在该宏时才执行重定位。 */
//SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; /* 注释掉模板默认表达式;原 offset 仍为 0。 */
SCB->VTOR = 0x08008000; /* 让中断从 APP 自己位于 0x08008000 的向量表取入口地址。 */
#endif /* USER_VECT_TAB_ADDRESS */ /* 结束条件编译。 */
以上代码全部来自配套 33DHT11_APP 最终工程,仅补充逐行注释。Flash 配置模块与上一课完全相同,本篇解释调用关系,不重复抄写底层 HAL 擦写实现。
一、温湿度检测APP移植
1.1 找到之前的工程
以第 22 个工程 22STM32_Lvgl_DHT11 为温湿度业务基线,复制后命名为 33DHT11_APP。原工程已经具备 LCD、触摸、LVGL、DHT11、TIM1、TIM3 PWM、TIM6 节拍和 USART1,但没有 CAN 和 IAP 交接逻辑。
移植目标不是改变温湿度页面,而是在原业务外围补齐:CAN 外设、CAN 接收封装、Flash 配置模块、升级请求处理、APP 链接地址、向量表重定位、串口日志和 bin 生成命令。
复制工程后先确认 Keil 与 .ioc 都指向新目录。若在旧工程上直接修改,后续无法判断生成的 APP_DHT11.bin 属于哪个版本。
1.2 初始化代码
在 STM32CubeMX 中启用 CAN1,模式选择 Normal。配套 .ioc 和 can.c 的位时序为:
| 参数 | 值 |
|---|---|
| CAN 时钟 | APB1 时钟 36 MHz |
| Prescaler | 4 |
| SJW | 1 TQ |
| BS1 | 9 TQ |
| BS2 | 8 TQ |
| 总时间量子 | 1 + 9 + 8 = 18 TQ |
| 波特率 | 36 MHz ÷ (4 × 18) = 500 kbit/s |
| 采样点 | (1 + 9) ÷ 18 ≈ 55.6% |
原笔记中的“分频改为四分频、两个 team 改成 9 和 8”应准确理解为 Prescaler=4、Time Segment 1=9 TQ、Time Segment 2=8 TQ。CAN 两端必须使用相同的标称波特率和兼容采样点;只记“500K”而不核对时钟树与三个参数,换工程后可能算错。
生成代码后让 Keil Reload,并立即编译一次。这样可把 CubeMX 生成阶段的问题与随后手工移植的问题分开定位。
1.3 编译代码
本次要增加四个编译单元,而不是原笔记含糊说的“flash、if 和 handle 三个 C 文件”:
BSP/Canif/canif.c;BSP/Canif/can_handle.c;BSP/inter_flashif/inter_flashif.c;BSP/inter_flashif/inter_flash_cfg.c。
CAN 外设自己的 Core/Src/can.c 由 CubeMX 生成并已加入工程。上述四个文件则从上一课 32Car_APP 复制,分别负责 CAN 收发封装、业务帧解析、Flash 底层访问和配置区管理。
1.4 使用之前的工程
从最近验证过的 32Car_APP 复制 Canif 与 inter_flashif 两个文件夹,可以保持以下接口一致:升级控制 ID 0x0B3、控制魔数 BE AD BE EF、参数区 0x08006000、APP 区 0x08008000 和 ota_flag 语义。
复制模块的价值是复用已验证协议,不等于不用理解。至少要能说清 Can_Recv_Msg() 从 FIFO0 取帧、Can_Handle() 按 ID 解析、get_ota_flag() 把结果交给 main(),以及 inter_flash_cfg_set_app_update_flag(1) 为什么必须发生在复位前。
1.5 添加C文件
在 Keil 的 BSP 分组中添加上面四个 .c 文件。文件对话框必须进入 33DHT11_APP,不要引用 32Car_APP 的绝对路径。头文件不需要作为编译单元添加;它们由各 .c 文件通过预处理器包含。
最终项目文件显示四个源文件都位于新工程相对路径下,这才说明工程目录可以独立复制和构建。
1.6 添加头文件
在 Options for Target → C/C++ → Include Paths 中加入:
..\BSP\Canif;..\BSP\inter_flashif。
然后在 main.c 用户头文件区加入 canif.h、can_handle.h、inter_flashif.h、inter_flash_cfg.h。Include Path 只告诉编译器去哪里找文件,不会自动把 .c 文件编译进工程;添加 .c 与添加头文件搜索路径是两项独立操作。
1.7 编译主代码
温湿度工程包含大量 LVGL 生成代码和图片资源,全量编译比普通裸机工程慢是正常的。编译期间不要因为 IDE 暂时无输出就重复触发 Build。
编译结果需要区分 Error 与 Warning。视频口述“没有报错”只能说明没有阻止链接的错误;画面中的全量构建曾显示大量警告,配套目录保存的最后一次增量构建日志为 0 Error、7 Warning。警告数量会随是否 Clean/Rebuild 而变化,不能把 0 Error 当成 0 Warning。
1.8 继续看思维导图
移植顺序建议固定为:先启用 CAN 外设,再加入模块文件与 Include Path,然后改 main.c,最后统一改链接地址、VTOR、MicroLIB 和生成命令。每完成一组就编译,能迅速判断问题来自哪一步。
四个头文件分别承担不同层次:canif.h 是 CAN 收发封装,can_handle.h 是业务解析,inter_flashif.h 是 Flash 地址和底层接口,inter_flash_cfg.h 是升级配置结构与标志接口。
1.9 中断配置
在 SystemClock_Config() 后的 USER CODE BEGIN SysInit 中加入 __set_PRIMASK(0)。配套 Bootloader 在跳 APP 前使用 __set_PRIMASK(1) 关闭可屏蔽中断;若 APP 不清零,TIM6 的 LVGL tick 等中断不会响应。
这条指令只改变 Cortex-M3 的全局 PRIMASK。CAN 本课采用主循环轮询 FIFO0,仍需要 MX_CAN_Init()、过滤器配置和 HAL_CAN_Start();TIM、USART 等外设也仍要按各自工程初始化。
1.10 滤波器初始化及flash初始化
普通温湿度工程此前没有 CAN,所以除了 CubeMX 生成的 MX_CAN_Init(),还必须调用 Can_Filter_Init()。该函数配置 FIFO0 过滤器并启动 CAN;只生成 can.c 而不启动外设,Can_Handle() 仍收不到帧。
inter_flash_cfg_init() 应在 USART1 初始化之后调用,因为出错路径会使用 printf。它读取 0x08006000 的 12 字节配置:魔数或累加校验不正确时,函数会进入无限循环打印错误,后面的 LVGL 界面不会初始化。首次空片实验必须先正确烧入 info.bin。
1.11 添加can指令代码
本节没有重新手写升级解析,而是复制上一课已经加入升级分支的 can_handle.c/.h。核心仍是接收 CAN ID 0x0B3,校验 BE AD BE EF 01,再把 tp_ota_flag 置 1。
文件同时保留 CAN_ID_UI=0x0B4 的仪表盘数据逻辑。它在温湿度界面中没有消费者,属于可移除的冗余;课程为减少本次改动而保留。学习时应知道它为什么无效,不能误以为这些 UI getter 会更新温湿度控件。
1.12 添加检验升级代码
先在主循环执行 Can_Handle(),再调用 get_ota_flag()。老师特别说明,自己第一次测试时正是漏掉 Can_Handle(),导致无论怎样发送都不能升级:getter 只读变量,不会主动访问 CAN;只有 handler 调用 Can_Recv_Msg() 后,变量才可能变为 1。
代码位置并非真正“随便放”。它必须位于 while (1) 内,并且循环能够持续到达。当前课程顺序是:LVGL 处理、延时、读取 DHT11、更新界面、轮询 CAN、检查升级标志。DHT_Read() 是时序敏感且可能阻塞的操作,若后续要求更快响应升级命令,应评估采样周期与 CAN 轮询延迟,但本节保留原顺序。
1.13 更改Keil设置
与 32Car_APP 完全一致的关键设置是:
| 设置位置 | 值 |
|---|---|
| Target → IROM1 Start | 0x08008000 |
| Target → IROM1 Size | 0x00078000(480 KiB) |
| C/C++ → Define | 追加 USER_VECT_TAB_ADDRESS |
system_stm32f1xx.c | SCB->VTOR = 0x08008000; |
| User → After Build Run #2 | fromelf.exe --bin -o "APP_DHT11.bin" "#L" |
最终 Define 字符串为 USE_HAL_DRIVER,STM32F103xE,USER_VECT_TAB_ADDRESS,宏之间使用英文逗号。链接起点和 VTOR 必须一致;一个决定代码/向量表放置位置,另一个决定中断发生时 CPU 去哪里取向量。
生成命令前的复选框必须勾选,否则命令虽然留在文本框里也不会执行。.bin 不携带加载地址,后续手工烧录仍要输入 0x08008000。
1.14 串口重定向
正确步骤是勾选 Use MicroLIB,在 usart.c 包含 <stdio.h> 并实现 fputc()。原笔记写成“添加 stm32f10x_it.h”是转录错误,视频和最终源码实际添加的是标准头文件 stdio.h。
这里还有一个容易忽略的 C 语言规则:头文件只对当前 .c 翻译单元生效。usart.c 包含 <stdio.h> 能让它正确声明 FILE/fputc,却不能替 main.c 声明 printf/sprintf。最终 33DHT11_APP/main.c 没有包含 <stdio.h>,构建日志因此出现两条“function declared implicitly”警告;这也是“能生成固件”不代表代码已经整洁的实例。
最终代码还把 buff 定义为 uint8_t buff[4],再传给期望 const char * 的 lv_label_set_text(),日志出现两条指针类型警告。课程源码应原样学习和复现;自己维护工程时可在确认业务范围后,把文本缓冲区改成字符类型并在使用格式化函数的文件中正确包含 <stdio.h>。
1.15 编译并测试
配套最终构建数据为:
APP_DHT11.bin:310,132 B(302.86 KiB);- APP 分区:480 KiB,构建日志显示约使用 302.66 KiB,剩余约 181,592 B;
- RAM:约 61.32 KiB/64 KiB,剩余 2,744 B;
- 最后保存的增量日志:0 Error、7 Warning。
烧录前选择 33DHT11_APP/MDK-ARM/APP_DHT11.bin,检查生成时间,然后把 ST-LINK Utility 的 Start address 设为 0x08008000。若 Bootloader 和 info.bin 已由上一课保留,就只写 APP;空片则仍须先烧 Bootloader.bin@0x08000000 和 info.bin@0x08006000。
视频烧录前屏幕显示仪表盘,烧录完成后画面实际切换为温湿度界面。老师口述“现在就变成仪表盘了”是口误。因为没有连接 DHT11,温湿度值显示 0;画面可确认触摸滑块和板上 LED 有响应。这次用 ST-Link 直接烧 APP 只验证了 APP 的链接布局、Bootloader 跳转、向量表和业务运行,还没有重新验证 PC→C8T6→CAN→Bootloader 的整条远程升级链路。