仪表盘Can接收代码改进(1)

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

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_RPM1 / 01指针百分比
E_UI_TYPE_WATER_TEMP2 / 02水温图标
E_UI_TYPE_SAFTY_BELT3 / 03安全带图标
E_UI_TYPE_LIGHT4 / 04灯光图标
E_UI_TYPE_TURN_LIGHT5 / 05转向灯图标
E_UI_TYPE_TURN_DATE6 / 06日期
E_UI_TYPE_TURN_TIME7 / 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 和宏替换语义,课程使用的编译器及具体选项另以配套工程为准。