仪表盘Can接收代码改进(1)
233.4.7 仪表盘Can接收代码改进(1)
/* 整体目的:保持仪表 CAN 协议数值与业务不变,为 ID、应用头及七个功能码提供清楚的名字。 */
/* 本块放入 can_handle.h;下方枚举值与视频 06:32 的最终定义及 28Can_Recv_Ok 配套文件一致。 */
/* 课程标识符 SAFTY、TURN_DATE、TURN_TIME 按原样保留,没有为统一英文拼写擅自改名。 */
#define CAN_ID_UI 0x0B4 /* 预处理阶段用 0x0B4 替换此宏名,表示本项目仪表帧的标准 CAN ID。 */
#define CAN_MSG_HEAD 0xAF /* 为数据区 Byte0 的 AF 应用头命名,不是 CAN 控制器的物理帧头。 */
typedef enum /* 创建枚举类型并用 typedef 在结束处取类型别名。 */
{ /* 枚举常量列表开始,每个项目用逗号分隔。 */
E_UI_TYPE_RPM = 0x1, /* 显式从数值 1 开始,表示仪表转速百分比/指针位置功能。 */
E_UI_TYPE_WATER_TEMP, /* 未显式赋值,采用前一项加一,因此等于 2,表示水温状态图标。 */
E_UI_TYPE_SAFTY_BELT, /* 自动取 3,表示安全带图标;SAFTY 是课程保留的拼写。 */
E_UI_TYPE_LIGHT, /* 自动取 4,表示灯光图标,不是图标状态值或颜色值。 */
E_UI_TYPE_TURN_LIGHT, /* 自动取 5,表示转向灯图标。 */
E_UI_TYPE_TURN_DATE, /* 自动取 6,实际表示日期;名字中的 TURN 不影响功能码含义。 */
E_UI_TYPE_TURN_TIME, /* 自动取 7,实际表示时间;末项保留课程的逗号。 */
} E_Ui_Type; /* E_Ui_Type 是上述枚举类型的别名,末尾分号结束这条类型声明。 */
can_handle.c——本节以命名常量替换后,仍然轮询接收的业务函数:
void Can_Handle(void) /* 沿用原解析函数,无参数、无返回值,仍由 main.c 主循环调用。 */
{ /* 函数体开始,本节没有改为 CAN 中断回调。 */
uint32_t recv_len; /* 保存课程接收接口返回的数据长度。 */
uint32_t recv_id; /* 保存接收接口通过指针写回的帧 ID。 */
uint8_t rx_data[8] = {0}; /* 建立当前轮询的八字节缓冲,未写入的元素先为零。 */
recv_len = Can_Recv_Msg(&recv_id, rx_data); /* 沿用原接收封装,读取一帧并取得 ID 与数据。 */
if(recv_len > 0) /* 原课程仍只判断长度大于零,没有因使用宏/枚举新增八字节校验。 */
{ /* 有数据时继续进行应用筛选。 */
if(recv_id == CAN_ID_UI) /* 宏展开后仍与 0x0B4 比较,筛选条件没有变化。 */
{ /* 属于仪表 ID 的帧分支开始。 */
if(rx_data[0] == CAN_MSG_HEAD) /* 宏展开后仍检查 Byte0 是否为 AF。 */
{ /* 应用头正确后,根据 Byte1 区分功能。 */
if(rx_data[1] == E_UI_TYPE_RPM) /* 与枚举常量 1 比较,表示指针百分比命令。 */
{ /* 指针数据分支开始。 */
ui_rpm.ui_value = rx_data[2]; /* 将 Byte2 的百分比保存到原共享对象。 */
ui_rpm.ui_freshflag = 1; /* 标记有指针数据待主循环应用。 */
} /* 指针分支结束。 */
else if(rx_data[1] == E_UI_TYPE_WATER_TEMP) /* 与数值 2 比较,选择水温图标。 */
{ /* 水温分支开始。 */
ui_water_temp.ui_value = rx_data[2]; /* 保存水温图标状态,不是测量温度值。 */
ui_water_temp.ui_freshflag = 1; /* 主循环将消费此刷新标志。 */
} /* 水温分支结束。 */
else if(rx_data[1] == E_UI_TYPE_SAFTY_BELT) /* 与数值 3 比较,选择安全带图标。 */
{ /* 安全带分支开始。 */
ui_safty_belt.ui_value = rx_data[2]; /* 保存安全带图标状态,保持原对象拼写。 */
ui_safty_belt.ui_freshflag = 1; /* 标记安全带图标需要刷新。 */
} /* 安全带分支结束。 */
else if(rx_data[1] == E_UI_TYPE_LIGHT) /* 与数值 4 比较,选择灯光图标。 */
{ /* 灯光分支开始。 */
ui_light.ui_value = rx_data[2]; /* 保存状态,0/1/2 在主循环决定白/黄/红颜色。 */
ui_light.ui_freshflag = 1; /* 标记收到一条灯光指令。 */
} /* 灯光分支结束。 */
else if(rx_data[1] == E_UI_TYPE_TURN_LIGHT) /* 与数值 5 比较,选择转向灯图标。 */
{ /* 转向灯分支开始。 */
ui_turn_light.ui_value = rx_data[2]; /* 保存转向灯状态。 */
ui_turn_light.ui_freshflag = 1; /* 标记转向灯待刷新。 */
} /* 转向灯分支结束。 */
else if(rx_data[1] == E_UI_TYPE_TURN_DATE) /* 与数值 6 比较,选择上一课新增的日期业务。 */
{ /* 日期分支开始。 */
ui_date.ui_value = ((uint32_t)rx_data[2] << 24) | ((uint32_t)rx_data[3] << 16) | ((uint32_t)rx_data[4] << 8) | ((uint32_t)rx_data[5] << 0); /* 保留上一课世纪、年内数、月、日的四字节打包。 */
ui_date.ui_freshflag = 1; /* 标记日期待拆分并设置 RTC。 */
} /* 日期分支结束。 */
else if(rx_data[1] == E_UI_TYPE_TURN_TIME) /* 与数值 7 比较,选择时间业务。 */
{ /* 时间分支开始。 */
ui_time.ui_value = ((uint32_t)rx_data[2] << 24) | ((uint32_t)rx_data[3] << 16) | ((uint32_t)rx_data[4] << 8); /* 保留时、分、秒放高三个字节,最低字节为零的布局。 */
ui_time.ui_freshflag = 1; /* 标记时间待处理,仍由原 main.c 业务消费。 */
} /* 时间分支结束;未知功能码没有新增业务。 */
} /* 应用头筛选结束。 */
} /* CAN ID 筛选结束。 */
} /* 有接收数据的分支结束。 */
} /* 一次轮询处理结束,返回原主循环。 */
本节新建
28Can_Recv_Ok,但下载给 ZET6 的协议值仍是0x0B4、AF、01~07。上方只收录本节命名改动与实际展示的解析业务;配套工程最终文件里的ui_get_*_value()属于下一课,不提前加入。视频本节的头文件仍保留七个extern ui_Typedef声明,不能把配套最终头文件已换成 getter 的状态误当成本课结果。
一、规范之前的工程
讲师本课目标是整理已经实现的接收工程,提高读代码时的辨识度。先复制第 26 个 26Can_Recv 到同级目录,重命名为 28Can_Recv_Ok,保留上一阶段工程以便对照。本节没有新加仪表功能,也没有改变 CAN 位速率、接线或报文布局。
“第 26 到第 28”是课程的工程编号;第 27 是 C8T6 发送工程。复制接收工程生成第 28,不是把两个板合并成一份固件,也不是自动升级芯片型号。
复制后检查打开的是新目录内的 .uvprojx,确认工程树中的源文件路径指向副本。复制整个工程目录比只改工程显示名更关键:如果仍打开旧目录的工程,就会把后续改动写回第 26 个版本。原资料称“版本管理”在本课只是保留目录副本,并没有演示 Git 提交或分支。
二、打开第28个工程
视频打开 28Can_Recv_Ok/MDK-ARM/12LCD_Display.uvprojx,目标名仍为 12LCD_Display,这是接收工程沿用之前 LCD 模板留下的名称。确认副本路径后,先修改 can_handle.h、can_handle.c 中的小处,本课不改主循环的数据消费方式。
头文件已包含类型、对象声明和函数声明,适合集中放协议常量;解析文件已包含 can_handle.h,因此能使用新增宏和枚举名字。修改 .h 后保存,再替换 .c 中的比较项,最后重新编译验证两边拼写一致。
三、修改代码提高可读性
3.1 写宏代替数据
原 if(recv_id == 0x0B4) 能运行,但阅读者需要另查协议才知道数字用途。命名后成为 if(recv_id == CAN_ID_UI),名字直接说明这是仪表 UI 的 ID。CAN_MSG_HEAD 则说明 AF 是数据区应用头,两个名字使不同层次的筛选更清楚。
语法是 #define 名字 替换内容,本课定义末尾没有分号。如果误写 #define CAN_ID_UI 0x0B4;,分号会被带进 if 的条件表达式而产生语法问题。宏不是运行时变量,也不是给 Flash 中另建一个可以赋值修改的对象;预处理器在编译前按标记替换使用位置。
本节宏名精确为大写 CAN_ID_UI、CAN_MSG_HEAD,不是转录里的 can_ID 或 can_message_head。C 大小写敏感,一个字符不同就是另一个标识符。源码行既保存原数值,也保持原比较逻辑,只替换名字。
集中定义便于检查多个使用点的一致性,但修改 ID 数值时也需要让发送端匹配,不会因为两个工程用了相似宏名就自动同步。本课 C8T6 仍发送 0x0B4,接收端宏也仍展开成同一个值,所以链路保持可用。
宏也可替换字符串或其他记号,本课只用简单常量。不要由此推广成所有表达式都可以随意写复杂宏;本课学习目标是看出常量的业务意义,尚未涉及函数式宏的括号和重复求值问题。
3.2 写枚举
3.2.1 找到main.c
讲师从 main.c 的 MX_TIM1_Init() 沿 Go To 进入定时器初始化,再查看 HAL 返回类型 HAL_StatusTypeDef,借此找已有 typedef enum 示例。这个过程是在查语言写法,不是因为新增枚举需要修改定时器或 RTC。
HAL 状态常量依次为 HAL_OK、HAL_ERROR、HAL_BUSY、HAL_TIMEOUT,课程版本值为 0、1、2、3。它们是命名整数常量,不是“四个函数”或“一个结构体”。HAL_StatusTypeDef 是枚举类型别名,可以作函数返回类型。
本课仿照语法创建自己的协议功能码枚举。没有显式赋值的第一项默认是 0,但课程明确给第一项 0x1,因此当前表从 1 开始。不能把一般默认规则照抄成“这份协议枚举从0开始”。
3.2.2 定义第一个变量
原标题“变量”实际是第一个枚举常量 E_UI_TYPE_RPM。它声明在 can_handle.h 的枚举表中,赋值 0x1,然后用于 can_handle.c 里 rx_data[1] 的功能选择比较。
名字开头的 E 是讲师采用的命名习惯,C 不要求所有枚举名字以 E 开头。RPM 通常指 revolutions per minute(每分钟转数);在本仪表协议里,该功能传的实际是指针百分比。它不是 C 指针类型,也不表示这个常量保存了地址。真正的 C 指针语法与 *、地址有关,不能因为仪表上有“指针”就混为一谈。
成员间用逗号,整段类型声明末尾用分号。E_UI_TYPE_RPM 不是可在运行时重新赋值的变量,收到的字节也不会因为用了它比较就自动改变自身类型。
3.2.3 定义第二个变量
第二项 E_UI_TYPE_WATER_TEMP 未写 =...,自动等于前一项加一,即 2。解析条件把原 0x2 换成该常量;Byte2 的图标状态读取仍然不变。
讲师接着添加第三项 E_UI_TYPE_SAFTY_BELT,自动为 3,并替换安全带分支的 0x3。源码里的 SAFTY 拼写并不是正确英语 safety,但它是现有代码标识符,忠实抄录时保留;解释文字使用“安全带”,不能偷偷改源码名字导致引用不一致。
原笔记“word temp”“赋值到0.42”“模特/model”“开门破”等没有对应本课协议或代码含义,是识别噪声,予以纠正/删除。实际步骤是复制枚举项目、改名字、保留逗号、替换匹配的比较常量,不由“对吧”这类口头语证明代码正确。
3.2.4 定义第四个变量
第四项 E_UI_TYPE_LIGHT 自动为 4,替换灯光功能选择里的 0x4。这是功能码,而 Byte2 的 0/1/2 是状态码。把功能码命名不等于同时把颜色状态改了编码。
本节采用大写枚举常量名,原笔记“命名规范使用小写”与画面相反。语言本身也没有要求它必须大写;关键是有意义、项目一致、定义与引用完全匹配。
3.2.5 定义第五个变量
第五项 E_UI_TYPE_TURN_LIGHT 自动为 5,对应转向灯。它替换 can_handle.c 的功能选择值,仍保存 Byte2 并置 ui_turn_light.ui_freshflag,主循环的图标消费逻辑没有改。
转向灯是界面图标,不是本课新增接线的车辆实体灯。宏与枚举的改动发生在应用解析层,CAN 收发器、CANH/CANL 和终端电阻仍沿用前课。
3.2.6 定义第六个变量
第六项为 E_UI_TYPE_TURN_DATE,自动为 6;第七项为 E_UI_TYPE_TURN_TIME,自动为 7。它们分别替换日期、时间分支的数字,上一课的四字节/三字节移位表达式保持原样。
两个名字保留了复制转向灯项留下的 TURN 前缀,实际含义由对应分支确定为日期和时间。DATE 不是 DATA:date 是日期,data 是泛指数据;转录“delta”也不是本课新类型。不要根据 TURN 前缀把它们解释为两个额外转向灯功能。
3.2.7 定义类型别名
结尾 } E_Ui_Type; 和开头 typedef enum 配合,为枚举类型提供别名。标准 C 不是原笔记所写的 type e = UI 语法。别名 E_Ui_Type 也不是新建了一个同名变量。
本节实际只把枚举常量用在比较表达式,没有声明一个 E_Ui_Type 变量保存串口/总线接收值。这里不需要强制把 Byte1 转成枚举才可比较,也不能说编译器已替代码自动拦截所有未知功能码;未知值仍能通过总线到达,只是原 if/else 链没有匹配业务。
| 枚举常量 | 最终值 | 对应协议功能 |
|---|---|---|
| E_UI_TYPE_RPM | 1 / 01 | 指针百分比 |
| E_UI_TYPE_WATER_TEMP | 2 / 02 | 水温图标 |
| E_UI_TYPE_SAFTY_BELT | 3 / 03 | 安全带图标 |
| E_UI_TYPE_LIGHT | 4 / 04 | 灯光图标 |
| E_UI_TYPE_TURN_LIGHT | 5 / 05 | 转向灯图标 |
| E_UI_TYPE_TURN_DATE | 6 / 06 | 日期 |
| E_UI_TYPE_TURN_TIME | 7 / 07 | 时间 |
自动递增适合当前连续值,但如果以后在中间插入一个未赋值的新项目,后面的值会随之改变,可能破坏既有协议。因此维护时要对照这张协议值表,不能把“只赋第一项”当作任何通信协议都安全的普遍规则。本节保留课程写法,不替讲师给每项补显式赋值。
四、打开串口助手发送数据
保存并重新编译第 28 个工程,下载到 ZET6,等待烧录和校验结束。视频观察 ST-Link 指示灯只是辅助迹象,最终仍应看下载输出及运行现象,不因灯闪就提前发指令。C8T6 沿用第 27 个转发工程。
本次视频的窗口位置与上课不同:左侧看到 C8T6 running 的是 USART1 日志口,右侧是向 USART2 发 HEX 的窗口。原笔记“wrong影提示错误窗口”应纠正为启动日志 running;它说明程序到了启动打印位置,没有表示端口错误。
两个窗口为 115200、8N1、无流控,发送用 HEX,完整八字节,不自动加回车换行。清空日志便于对照,再复位 C8T6 确认日志口。测试日期和水温以验证命名替换没有改变协议:
- 标准日期指令
AF 06 14 19 08 19 AF AF对应 2025-8-25。所谓“发一个6”是发 Byte1 为06的一整帧,不是只送单字节 06。 - 水温红色用
AF 02 01 AF AF AF AF AF。原笔记“把02改成01触发报警”容易误导:Byte1=02 选择水温,Byte2=01 才选择红色状态;不能把功能码改成01后还称水温命令。
视频约 07:29 的发送记录实际出现 AF 06 29 19 08 19 AF AF,Byte2 的世纪数为 0x29=41,屏幕却显示 2025-8-25。这与上一课已核对的限制一致:当前 RTC 设置忽略最高世纪字节,界面固定以 20 开头。本节画面不能作为“世纪字段正确生效”的证据;按协议测试应使用 14(数值20)。课程后段复位后日期异常,再发日期恢复,只说明收到命令后能重新设置日期,不能证明复位已恢复完整日历状态。
验证的重点是行为等价:相同功能码仍命中相同分支,相同载荷仍产生相同仪表结果。宏名/枚举名只在源码里出现,串口发送框继续填写十六进制字节,不能发送文本 E_UI_TYPE_TURN_DATE 让 MCU 自动识别。下节课再整理主函数里直接读取共享对象的逻辑。
五、核对依据
- 本节完整视频/转录:00:00~02:02 核对工程副本和宏定义;02:03~06:38 核对 HAL 枚举示例、自定义枚举及顺序;06:39~08:15 核对下载、窗口分工和实际测试帧。
- 配套
源码/3.1 CAN、LVGL、IAP固件升级专题/uint3code/28Can_Recv_Ok/BSP/Canif/can_handle.h/.c:核对宏、枚举名字和替换后的解析函数;视频中未出现的 getter 函数留到下一课。 - C11 委员会草案 N1570,6.7.2.2、6.7.8、6.10.3:核对枚举递增、typedef 和宏替换语义,课程使用的编译器及具体选项另以配套工程为准。