仪表盘Can数据接收(2)
229.4.3 仪表盘Can数据接收(2)
/* 本节目的:把每项仪表的数据与刷新标志放在同一个结构体内,实现收到新报文后才更新对应对象。 */
/* 以下为 can_handle.h 的本节末态:先定义公共类型,再声明五个全局对象和处理函数。 */
/* 日期、时间指令本节明确暂不处理,所以不提前收录后续 ui_date、ui_time 及其分支。 */
#ifndef __CAN_HANDLE_H__ /* 第一次包含时允许展开,防止同一翻译单元重复处理这个头文件。 */
#define __CAN_HANDLE_H__ /* 标记本头文件已展开,宏名沿用课堂源码。 */
#include "main.h" /* 提供 uint32_t、uint8_t 等工程基础类型。 */
typedef struct /* 声明一个结构体类型,并用 typedef 为它建立简短类型名。 */
{ /* 结构体成员定义开始;这里只描述布局,还没有创建具体对象。 */
uint32_t ui_value; /* 保存该项目最新收到的数值,如指针百分比或报警状态。 */
uint8_t ui_freshflag; /* 保存是否有待更新数据的标志:0 无更新请求,1 有更新请求。 */
} ui_Typedef; /* 结束结构体类型定义,以后用 ui_Typedef 声明具体对象。 */
extern ui_Typedef ui_rpm; /* 声明指针对象的定义在 can_handle.c 中,使 main.c 使用同一份对象。 */
extern ui_Typedef ui_water_temp; /* 声明水温图标对象,包含状态值和刷新标志。 */
extern ui_Typedef ui_safty_belt; /* 声明安全带对象,保留课程 safty 的拼写,不擅自改接口名称。 */
extern ui_Typedef ui_light; /* 声明灯光对象,接收白、黄、红三种状态。 */
extern ui_Typedef ui_turn_light; /* 声明转向灯对象,控制白色与红色状态。 */
void Can_Handle(void); /* 对外声明接收解析函数,主循环通过它更新上述对象。 */
#endif /* 结束头文件重复包含保护。 */
can_handle.c——本节完成的五类数据接收与置位:
#include "main.h" /* 引入公共类型和工程声明。 */
#include "canif.h" /* 引入上一节复用的 Can_Recv_Msg() 接收接口。 */
#include "can.h" /* 引入 CAN 外设声明,与课堂文件保持一致。 */
#include "can_handle.h" /* 取得 ui_Typedef 类型和本模块公开接口。 */
ui_Typedef ui_rpm; /* 定义指针数据对象;文件作用域对象启动时成员置零。 */
ui_Typedef ui_water_temp; /* 定义水温对象,ui_value 在这里表示报警状态而非温度数值。 */
ui_Typedef ui_safty_belt; /* 定义安全带对象,字段命名沿用课程源码。 */
ui_Typedef ui_light; /* 定义灯光对象,独立保存它的数据与待更新状态。 */
ui_Typedef ui_turn_light; /* 定义转向灯对象,不与灯光对象共用刷新标志。 */
void Can_Handle(void) /* 每轮主循环调用,尝试从 CAN FIFO0 取出一帧。 */
{ /* CAN 应用处理函数体开始。 */
uint32_t recv_len; /* 保存接收接口返回的数据长度。 */
uint32_t recv_id; /* 保存标准 CAN 标识符,通过接口指针输出参数获得。 */
uint8_t rx_data[8] = {0}; /* 分配并清零最多 8 字节的数据接收数组。 */
recv_len = Can_Recv_Msg(&recv_id, rx_data); /* 取一帧,填入 ID 和字节数据,并返回其 DLC。 */
if(recv_len > 0) /* 课程先检查长度非零;严格的 8 字节长度校验仍未加入。 */
{ /* 有非零长度数据,继续识别应用消息。 */
if(recv_id == 0x0B4) /* 筛选发送端与接收端共同约定的标准 CAN ID。 */
{ /* CAN ID 满足仪表业务约定。 */
if(rx_data[0] == 0xAF) /* 检查 Byte0 是否为课程自定义数据头。 */
{ /* 数据头正确,接着按 Byte1 选择唯一功能分支。 */
if(rx_data[1] == 0x1) /* 功能码 1 表示指针位置数据。 */
{ /* 指针分支开始。 */
ui_rpm.ui_value = rx_data[2]; /* 保存 Byte2 的 0~100 指针位置百分比。 */
ui_rpm.ui_freshflag = 1; /* 通知 main.c:指针收到了一次新的更新请求。 */
} /* 指针分支结束。 */
else if(rx_data[1] == 0x2) /* 功能码 2 表示水温报警状态,与上一个分支互斥。 */
{ /* 水温分支开始。 */
ui_water_temp.ui_value = rx_data[2]; /* 保存水温报警状态,协议约定 0 白色、1 红色。 */
ui_water_temp.ui_freshflag = 1; /* 仅请求刷新水温图标,不改变其他项目标志。 */
} /* 水温分支结束。 */
else if(rx_data[1] == 0x3) /* 功能码 3 表示安全带状态,原笔记的 13 是识别错误。 */
{ /* 安全带分支开始。 */
ui_safty_belt.ui_value = rx_data[2]; /* 保存该图标的状态值。 */
ui_safty_belt.ui_freshflag = 1; /* 请求 main.c 更新安全带图标。 */
} /* 安全带分支结束。 */
else if(rx_data[1] == 0x4) /* 功能码 4 表示灯光状态,原笔记的 14 是识别错误。 */
{ /* 灯光分支开始。 */
ui_light.ui_value = rx_data[2]; /* 保存协议约定的 0 白、1 黄、2 红状态值。 */
ui_light.ui_freshflag = 1; /* 置位灯光对象的刷新标志。 */
} /* 灯光分支结束。 */
else if(rx_data[1] == 0x5) /* 功能码 5 表示转向灯状态,原笔记的 15 是识别错误。 */
{ /* 转向灯分支开始。 */
ui_turn_light.ui_value = rx_data[2]; /* 保存转向灯当前状态。 */
ui_turn_light.ui_freshflag = 1; /* 请求 main.c 更新转向灯图标。 */
} /* 本节最后一个功能分支结束。 */
} /* 应用数据头检查结束。 */
} /* CAN ID 检查结束。 */
} /* 长度非零检查结束。 */
} /* 处理完一帧后返回,后续界面操作由 main.c 执行。 */
main.c——沿用的颜色宏与颜色变量:
#define COLOR_RED 0xff,0x0,0x0 /* 宏展开为三个实参 R=255、G=0、B=0,表示红色。 */
#define COLOR_WHITE 0xff,0xff,0xff /* 宏展开为 R=255、G=255、B=255,表示白色。 */
#define COLOR_YELLOW 0xff,0xff,0x0 /* 宏展开为 R=255、G=255、B=0,表示黄色。 */
lv_color_t color_value; /* 沿用文件作用域的 LVGL 颜色变量,供各图标分支依次赋值使用。 */
main.c——本节在 while(1) 内写到的接收与五项显示处理:
Can_Handle(); /* 每轮先解析一帧 CAN 数据,有有效业务数据时相应对象标志被置 1。 */
if(ui_rpm.ui_freshflag == 1) /* 仅当指针收到新报文时调用它的角度设置接口。 */
{ /* 指针更新分支开始。 */
ui_rpm.ui_freshflag = 0; /* 消费本次更新请求,下一轮没有新数据就不会再次进入。 */
lv_img_set_angle(ui_ImageNeedle, ui_rpm.ui_value*2500/100); /* 将 0~100 百分比映射到 0~250.0°,乘法先于整数除法执行。 */
} /* 指针更新分支结束。 */
if(ui_water_temp.ui_freshflag == 1) /* 判断水温对象是否有新的报警状态要显示。 */
{ /* 水温图标更新分支开始。 */
ui_water_temp.ui_freshflag = 0; /* 清除本次水温更新标志,避免下一轮重复设置图标。 */
if(ui_water_temp.ui_value == 0) /* 状态为零时,按课堂约定显示白色。 */
{ /* 水温白色分支开始。 */
color_value = lv_color_make(COLOR_WHITE); /* 将宏展开的三个 RGB 分量转换为 LVGL 使用的颜色值。 */
lv_obj_set_style_img_recolor(ui_ImageWatertemp, color_value, 0); /* 设置水温图片主部分默认状态的重着色颜色。 */
lv_obj_set_style_img_recolor_opa(ui_ImageWatertemp, 255, 0); /* 设置完全重着色;255 是最大混合强度而非报警值。 */
} /* 水温白色分支结束。 */
else /* 课程将所有非零值都当作红色,并未严格限定为 1。 */
{ /* 水温红色分支开始。 */
color_value = lv_color_make(COLOR_RED); /* 生成红色颜色值。 */
lv_obj_set_style_img_recolor(ui_ImageWatertemp, color_value, 0); /* 对水温对象应用红色重着色。 */
lv_obj_set_style_img_recolor_opa(ui_ImageWatertemp, 255, 0); /* 使重着色完整生效,原图片透明区域仍由其 alpha 控制。 */
} /* 水温红色分支结束。 */
} /* 水温图标本次更新结束。 */
if(ui_safty_belt.ui_freshflag == 1) /* 判断安全带对象是否收到更新请求。 */
{ /* 安全带图标更新分支开始。 */
ui_safty_belt.ui_freshflag = 0; /* 消费本次安全带状态更新。 */
if(ui_safty_belt.ui_value == 0) /* 状态为零时显示白色。 */
{ /* 安全带白色分支开始。 */
color_value = lv_color_make(COLOR_WHITE); /* 生成白色颜色值。 */
lv_obj_set_style_img_recolor(ui_ImageSfatyBelt, color_value, 0); /* 设置安全带图片颜色;保留课程对象名 Sfaty 的拼写。 */
lv_obj_set_style_img_recolor_opa(ui_ImageSfatyBelt, 255, 0); /* 启用完全重着色,使白色状态可见。 */
} /* 安全带白色分支结束。 */
else /* 课程把非零安全带状态显示为红色。 */
{ /* 安全带红色分支开始。 */
color_value = lv_color_make(COLOR_RED); /* 生成红色颜色值。 */
lv_obj_set_style_img_recolor(ui_ImageSfatyBelt, color_value, 0); /* 红色作用于安全带对象,避免误用复制来源的水温对象。 */
lv_obj_set_style_img_recolor_opa(ui_ImageSfatyBelt, 255, 0); /* 使安全带红色重着色完全生效。 */
} /* 安全带红色分支结束。 */
} /* 安全带本次更新结束。 */
if(ui_light.ui_freshflag == 1) /* 判断灯光对象是否收到新的颜色状态。 */
{ /* 灯光更新分支开始。 */
ui_light.ui_freshflag = 0; /* 清除灯光本次更新请求。 */
if(ui_light.ui_value == 0) /* 值为 0 时显示白色。 */
{ /* 灯光白色分支开始。 */
color_value = lv_color_make(COLOR_WHITE); /* 生成白色颜色值。 */
lv_obj_set_style_img_recolor(ui_ImageLight, color_value, 0); /* 设置灯光对象的白色重着色。 */
lv_obj_set_style_img_recolor_opa(ui_ImageLight, 255, 0); /* 完全应用重着色。 */
} /* 灯光白色分支结束。 */
else if(ui_light.ui_value == 2) /* 值为 2 时显示红色,区别于两状态图标。 */
{ /* 灯光红色分支开始。 */
color_value = lv_color_make(COLOR_RED); /* 生成红色颜色值。 */
lv_obj_set_style_img_recolor(ui_ImageLight, color_value, 0); /* 将灯光图标设为红色。 */
lv_obj_set_style_img_recolor_opa(ui_ImageLight, 255, 0); /* 完全应用红色重着色。 */
} /* 灯光红色分支结束。 */
else /* 其余值都进入此分支;协议合法值中此时仅剩 1,但代码未排除非法值。 */
{ /* 灯光黄色分支开始。 */
color_value = lv_color_make(COLOR_YELLOW); /* 生成黄色颜色值。 */
lv_obj_set_style_img_recolor(ui_ImageLight, color_value, 0); /* 设置灯光图标的黄色重着色。 */
lv_obj_set_style_img_recolor_opa(ui_ImageLight, 255, 0); /* 完全应用黄色重着色。 */
} /* 灯光黄色分支结束。 */
} /* 灯光本次更新结束。 */
if(ui_turn_light.ui_freshflag == 1) /* 判断转向灯对象是否有待显示状态。 */
{ /* 转向灯更新分支开始。 */
ui_turn_light.ui_freshflag = 0; /* 清除转向灯本次更新标志。 */
if(ui_turn_light.ui_value == 0) /* 零值按课堂显示为白色,未调用隐藏或删除对象接口。 */
{ /* 转向灯白色分支开始。 */
color_value = lv_color_make(COLOR_WHITE); /* 生成白色颜色值。 */
lv_obj_set_style_img_recolor(ui_ImageTurnlight, color_value, 0); /* 重着色对象为转向灯图片。 */
lv_obj_set_style_img_recolor_opa(ui_ImageTurnlight, 255, 0); /* 使白色重着色生效。 */
} /* 转向灯白色分支结束。 */
else /* 所有非零值按课堂显示为红色。 */
{ /* 转向灯红色分支开始。 */
color_value = lv_color_make(COLOR_RED); /* 生成红色颜色值。 */
lv_obj_set_style_img_recolor(ui_ImageTurnlight, color_value, 0); /* 设置转向灯图片红色重着色。 */
lv_obj_set_style_img_recolor_opa(ui_ImageTurnlight, 255, 0); /* 完全应用红色重着色。 */
} /* 转向灯红色分支结束。 */
} /* 转向灯本次更新结束,主循环继续执行。 */
源码按本节视频与配套工程核对,保留五项业务代码和现有拼写。各块属于指定文件与局部位置,不是完整独立程序。原有 CAN 初始化、滤波器、LVGL 初始化、RTC 显示和节拍沿用前课;本节未改写的库实现不在这里重复抄录。
一、数据接收基本功能实现
上一节每轮 while(1) 都调用 lv_img_set_angle(),即使没有新报文也重新传入旧值。本节增加一个软件刷新标志,让“收到数据”和“更新对象”通过状态衔接。
| 时刻 | ui_value | ui_freshflag | 发生的动作 |
|---|---|---|---|
| 启动后 | 0 | 0 | 全局对象由启动初始化置零,没有 CAN 更新请求 |
| 收到有效指针报文 | Byte2 的值 | 1 | 接收解析保存数据并提出更新请求 |
| 主循环检查到 1 | 保留该值 | 清为 0 | 调用角度设置函数一次 |
| 下一轮无新报文 | 仍保留该值 | 0 | 跳过指针设置,界面保持原有状态 |
| 收到下一帧 | 新的 Byte2 | 1 | 再触发一次对应更新 |
视频先用独立变量表示指针数据和标志,随后发现每项数据都需要一对变量,于是改成结构体。最终采用 ui_rpm.ui_freshflag,原笔记的 RPM_flash_flag 并非本节最终名称;这里的 fresh/refresh 表示刷新,与 Flash 存储器无关。
必须清零标志,否则它一直为 1,每轮仍会重复调用。课堂源码是进入更新分支后先清零,再调用 LVGL 接口;语义上是在消费一次请求,并没有等待 LCD 硬件刷屏完成后才清零。
同一数值重复收到两次也会两次置位,因为当前代码判断的是“有新报文”,不是“数值改变”。若接收频率本身很高,更新请求也会相应增加。本节的优化是消除无报文时的重复业务调用,没有设置固定帧率上限。
lv_img_set_angle() 本身会对相同角度作提前返回等处理,所以不应把每次函数调用都等同于一次完整屏幕重绘。刷新标志仍可减少主循环无意义的接口调用,并把接收数据与界面更新职责连接得更清楚。
1.1 定义结构体
ui_Typedef 把两个相关状态放在一起:
ui_value:最新收到的数据值;ui_freshflag:这个数据是否还需要提交给界面。
typedef struct { ... } ui_Typedef; 定义的是类型,相当于规定“每个对象都有这两个成员”;ui_Typedef ui_rpm; 才创建指针对象。水温、安全带、灯光、转向灯分别创建各自对象,成员名相同但内存和状态独立。
类型定义放在 can_handle.h 中,其他文件包含该头文件后才能知道对象有哪些成员。若类型只写在 can_handle.c 内,main.c 和头文件中的 extern ui_Typedef ... 就无法识别这个类型。
使用对象时写英文点运算符,例如 ui_water_temp.ui_value。这里 ui_water_temp 是一个具体结构体对象,不是结构体指针,所以使用 .;本节不需要 ->。
结构体让数据组织清楚,但不会自动消除每种功能的接收分支和显示分支,也不保证节省 RAM。C 允许结构体为对齐插入填充,不能把 uint32_t 的 4 字节与 uint8_t 的 1 字节简单相加,断言 sizeof(ui_Typedef) 必定为 5。若关心空间应按当前编译器实际 sizeof 核对,而本节并未测量或打包结构体。
跨文件定义与声明:
| 文件位置 | 写法/职责 |
|---|---|
can_handle.h | 定义公共类型,写 extern ui_Typedef ui_rpm; 等声明 |
can_handle.c | 写 ui_Typedef ui_rpm; 等定义,建立实际对象 |
main.c | 包含 can_handle.h 后读写同一对象的成员 |
视频强调“extern 声明不要赋值”,在本工程头文件里是正确的操作规则。准确原理是:文件作用域的 extern ... = ...; 带初始化时会成为定义,多个 .c 包含该头文件便可能形成多个同名定义,导致链接错误;并非 C 语言一律禁止 extern 与初始化一起出现。
更换普通变量为结构体后,必须删掉旧的同名定义/声明,并同步修改所有使用点。头文件保护只防止一个翻译单元内重复包含,不会防止多个 .c 分别定义同一个外部对象。
接收功能码采用 if/else if 链,因为一帧 Byte1 只表示一种功能。一旦前面分支成立,后面分支跳过。相比彼此独立的 if,它也更清楚地表达了“每帧只选择一项”的协议关系。
1.2 结构体的使用
本节完成前五个功能码,日期和时间设置明确暂不处理:
| Byte1 | 保存到的对象 | Byte2 协议意义 | 对应 LVGL 对象 |
|---|---|---|---|
0x01 | ui_rpm | 0~100 指针位置百分比 | ui_ImageNeedle |
0x02 | ui_water_temp | 0 白色,1 红色 | ui_ImageWatertemp |
0x03 | ui_safty_belt | 0 白色,1 红色 | ui_ImageSfatyBelt |
0x04 | ui_light | 0 白色,1 黄色,2 红色 | ui_ImageLight |
0x05 | ui_turn_light | 0 白色,1 红色 | ui_ImageTurnlight |
原笔记中的安全带=13、灯=14、转向灯=15 是识别错误,应分别为 0x03、0x04、0x05。源码中的 safty、Sfaty 是课程已有拼写,阅读时理解为 safety,抄录时保留,以免与生成的 UI 对象名称不一致。
ui_water_temp.ui_value 存的只是报警状态,不是摄氏度;ui_rpm.ui_value 存的只是百分比,不是已经标定好的 r/min。每个成员的业务单位由协议决定,不由成员类型或名称自动决定。
RGB 宏为什么看似一个参数,实际是三个:
COLOR_RED 宏替换文本为 0xff,0x0,0x0。预处理之后,lv_color_make(COLOR_RED) 相当于 lv_color_make(0xff,0x0,0x0),函数实际收到红、绿、蓝三个分量。两个逗号属于宏展开文本,不能把宏误认为一个打包整数或一个颜色对象。
| 宏 | R | G | B | 结果 |
|---|---|---|---|---|
COLOR_WHITE | 255 | 255 | 255 | 白色 |
COLOR_RED | 255 | 0 | 0 | 红色 |
COLOR_YELLOW | 255 | 255 | 0 | 黄色 |
lv_color_make() 返回符合当前 LVGL 色深配置的 lv_color_t,赋给 color_value 后用于图像重着色。不要为这个宏自行补一层括号:把三个逗号分隔的实参整体包在一个括号中,会改变 C 表达式含义,与课堂写法不再相同。
每个图标分支调用两个接口:img_recolor 指定要混入的颜色;img_recolor_opa=255 指定完全混入该颜色。若混合强度是 0,就不会出现设置的重着色效果。最后的选择器 0 对应主部分、默认状态,不是关闭接口。
重着色不等同于改变整张图片透明度,更不等同于隐藏对象;原图透明区域仍由图像透明信息决定。尤其转向灯零值在当前源码中是白色,非零为红色,并没有实现自动闪烁、隐藏或左右方向选择。
灯光是三状态,源码先判断 0 白色,再判断 2 红色,最后 else 黄色。合法协议输入下,最后分支对应 1;但非法的 3、255 等也会显示黄色。水温、安全带、转向灯则把任何非零值当作红色。学习时要把协议约定和代码实际接受范围区分开,当前课程仍缺少参数范围校验。
复制相似显示代码后,至少核对四处:刷新标志对象、清零对象、值判断对象、两个 LVGL 调用的目标对象。只修改其中一个名称,会出现“收到安全带数据却改变水温图标”等串项目错误。视频也现场纠正过复制对象错误,因此这一点直接关系实验调试。
1.3 数据接收功能优化
当前 Can_Handle() 依次判断非零长度、CAN ID、应用数据头和功能码,符合条件时保存值并置位。接收端仍复用第 228 篇的轮询 FIFO 封装,每次调用最多取一帧,没有改成 CAN 中断接收。
主循环中的五个刷新判断使用独立的 if,与解析函数的 else if 用途不同:解析时一帧只属于一种指令;显示时检查每个对象是否有待更新请求,独立判断能逐个消费各对象的标志。
接收处理和显示处理都在同一主循环中顺序执行,当前没有两个任务同时修改这些成员的问题。若后续改用 ISR、RTOS 任务或 DMA 回调并发更新,就需要重新设计数据与标志的交接,不能只因为变量加了 volatile 就认为已经解决竞争。本节无需凭空加入这类并发改造。
本节还删除了前面自动自增指针、周期自动变色等演示代码,让显示由 CAN 数据控制。RTC 时间/日期读取显示仍保留,CAN 设置时间/日期尚未完成。
主循环保留的工作:
- 周期调用
lv_timer_handler(),处理 LVGL 定时器、输入、动画及刷新等相关任务;它不是只负责时间标签。 - 周期读取 RTC 并更新日期、时间标签。
- 调用
Can_Handle()取帧并解析。 - 根据各对象刷新标志,设置指针角度或图标颜色。
工程以 HAL_GetTick()-lvtick>5 控制 LVGL 调用。HAL tick 为 1 ms 时,严格大于 5 意味着差值至少 6 ms,再叠加主循环其他工作所用时间,所以不能写成“精确每 5 ms 调用”。RTC 显示的 >500 同样是至少 501 个 tick 后满足条件。本节沿用这些语句,没有改变比较运算符。
软件标志清零只表示“请求已经交给界面接口”,实际 LCD 刷新由 LVGL 调度和显示驱动完成。没有新 CAN 报文时,图标仍保留最后状态;本节也没有超时恢复、断线提示或强制恢复默认状态。
课程末尾编译检查未出现错误,但这只是课堂演示结果,不代表已在硬件上验证 CAN 接收。下一篇开始写 C8T6 发送端,再联合测试五项指令。原接收代码仍保留长度检查、帧类型检查、HAL 返回状态和参数校验不足,见第 228 篇;增加结构体和标志不会自动修复这些问题。
二、学习检查与资料依据
完成本节后,应能解释:为什么一项数据需要值和标志;为什么 typedef 放在头文件里;为何头文件只用 extern 声明对象;接收端的 else if 与显示端独立 if 分别解决什么问题;颜色宏如何变成三个函数实参;为什么 255 的重着色强度不能理解为温度或报警码。
- 课程视频与转录:约 00:00~04:38 的刷新标志、04:41~11:28 的结构体和跨文件声明、11:30~18:12 的五项显示、18:16~20:02 的主循环整理。
- 配套
26Can_Recv/BSP/Canif/can_handle.c/.h与Core/Src/main.c:核对本节前五个分支,未抄录后续日期和时间设置。 - WG14 C11 草案 N1570:6.7.2.1 核对结构体布局/填充,6.7.8 核对 typedef,6.9.2 核对带初始化的 extern 定义。
- LVGL 8.3 Image 文档:核对图片重着色颜色、混合强度与角度单位。
- LVGL 8.3 Timer Handler 文档:核对
lv_timer_handler()的调度用途。 - LVGL 8.3.11 官方 lv_img.c:核对角度设置对相同值的处理;笔记仅引用行为,没有抄录库函数实现。