仪表盘Can数据接收(3)
232.4.6 仪表盘Can数据接收(3)
/* 整体目的:ZET6 接收功能码 06/07,把日期/时间字节打包保存,再在主循环拆开并设置 RTC。 */
/* 以下是 can_handle.c 本节新增部分;两条全局定义放在文件顶部,else if 接在已有功能码 05 分支之后。 */
/* 这里只收录课堂实际新增的局部代码,不凭空补出完整 Can_Handle 或下一课中断处理实现。 */
ui_Typedef ui_date; /* 定义日期数据及刷新标志,文件范围对象上电时零初始化。 */
ui_Typedef ui_time; /* 定义时间数据及刷新标志;其类型已在 can_handle.h 定义。 */
else if(rx_data[1] == 0x6) /* 接在原 01~05 分支后,识别日期功能码,不能独立写在文件顶层。 */
{ /* 日期分支开始;外层已经检查收到数据、ID 和 AF 应用头。 */
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] == 0x7) /* 同一个分支链识别时间功能码。 */
{ /* 时间分支开始。 */
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; /* 标记有新时间,主循环随后负责取出字段并设置 RTC。 */
} /* 时间分支结束,后面保留原函数的外层闭合括号。 */
can_handle.h——课堂查看的已有类型,以及本节追加的外部声明:
typedef struct /* 定义可重复使用的“数值 + 刷新标志”结构体类型。 */
{ /* 结构体成员表开始。 */
uint32_t ui_value; /* 32 位无符号值,既能存一个状态,也能容纳四个打包字节。 */
uint8_t ui_freshflag; /* 本课程用 1 表示待处理,主循环处理时清为 0。 */
} ui_Typedef; /* 类型别名;此行并未创建 ui_date 或 ui_time 的存储。 */
extern ui_Typedef ui_date; /* 声明实际对象在 can_handle.c,供 main.c 引用,不能在头文件再定义一份。 */
extern ui_Typedef ui_time; /* 对时间对象作同样的跨文件声明。 */
main.c——接在原图标处理之后的两段主循环业务:
if(ui_date.ui_freshflag == 1) /* 检测日期解析分支是否留下新数据。 */
{ /* 新日期处理开始。 */
ui_date.ui_freshflag = 0; /* 先消费本次标志,避免没有新指令时每轮重复设置日期。 */
RTC_DateTypeDef struct_rtc_set_date; /* 建立本次日期设置参数;原课没有加整体零初始化。 */
struct_rtc_set_date.Year = ((ui_date.ui_value >> 16) & 0xff); /* 提取打包值的第二个字节,2025 的这部分为 25。 */
struct_rtc_set_date.Month = ((ui_date.ui_value >> 8) & 0xff); /* 提取原 Byte4,即月份。 */
struct_rtc_set_date.Date = ((ui_date.ui_value >> 0) & 0xff); /* 提取原 Byte5,即月内日期;右移零位保持数值。 */
if (HAL_RTC_SetDate(&hrtc, &struct_rtc_set_date, RTC_FORMAT_BIN) != HAL_OK) /* 将普通二进制数值交给本工程 F1 HAL 的日期接口。 */
{ /* HAL 设置失败时执行本分支。 */
Error_Handler(); /* 进入课程错误处理;原实现关中断后死循环,不会继续正常刷新界面。 */
} /* 日期设置错误分支结束。 */
} /* 这一批日期处理结束。 */
if(ui_time.ui_freshflag == 1) /* 独立检测新时间,同一轮可先处理日期再处理时间。 */
{ /* 新时间处理开始。 */
ui_time.ui_freshflag = 0; /* 清除待处理标志,设置操作只针对新收到的一批。 */
RTC_TimeTypeDef struct_rtc_set_time; /* 建立时分秒设置参数,区别于 RTC_DateTypeDef。 */
struct_rtc_set_time.Hours = ((ui_time.ui_value >> 24) & 0xff); /* 时间的小时在最高字节,故右移 24 位。 */
struct_rtc_set_time.Minutes = ((ui_time.ui_value >> 16) & 0xff); /* 分钟在第二个字节,故右移 16 位。 */
struct_rtc_set_time.Seconds = ((ui_time.ui_value >> 8) & 0xff); /* 秒在第三个字节,故右移 8 位,不是零位。 */
if (HAL_RTC_SetTime(&hrtc, &struct_rtc_set_time, RTC_FORMAT_BIN) != HAL_OK) /* 使用二进制格式的小时、分钟、秒设置当前 RTC 计数。 */
{ /* 设置失败时进入错误分支。 */
Error_Handler(); /* 按课程写法停止正常业务,没有自行添加重试或错误回执。 */
} /* 时间设置错误分支结束。 */
} /* 时间业务结束,继续已有主循环。 */
源码取自第 26 个工程
26Can_Recv,与本节视频最终结果核对一致,只有排版和逐行注释发生变化。第一块特意保留else if,因为它确实是原函数的分支片段;不能将三块连续粘成一个独立程序。原工程main.c已包含rtc.h、can_handle.h,调用前也已有MX_RTC_Init()、CAN 初始化及轮询Can_Handle()。本节不新增 C8T6 转发代码,也不提前收录下一课的 CAN 中断版本。
一、Can数据接收
1.1 改CAN数据接收代码
本课修改的是 ZET6 仪表接收端,不是 C8T6 的串口接收。第 231 篇已经能把电脑送来的 AF 开头八字节从串口转发到 CAN,但 ZET6 原来只处理功能码 01~05,所以缺的是日期 06 和时间 07 的应用处理。
打开课程 uint3code/26Can_Recv/MDK-ARM 下的工程。它由之前 LCD 工程复制演进而来,视频中的工程文件/目标名称仍显示 12LCD_Display,不能只因名字不是“26”就认定开错;应核对当前工程所在目录、芯片和 BSP/Canif/can_handle.c 内容。
本节分两层修改:can_handle.c 增加 ui_date、ui_time 和两个协议分支,负责把收到的数据保存并置标志;main.c 读取标志,拆开数值并调用 RTC 设置。原笔记的“针对 ZET6 串口接收补充”应改为 CAN 接收后协议解析与 RTC 设置。电脑串口接在 C8T6 一侧,ZET6 收到的是 CAN 帧。
原 Can_Handle() 先调用 Can_Recv_Msg(),然后依次判断收到数据、ID=0x0B4、Byte0=AF,再按 Byte1 选择功能。两个新 else if 接在 05 后面。说“第二个字节获取数据”容易混淆:第二个字节 Byte1 是功能码,业务载荷从第三个字节 Byte2 开始。
1.2 日期和时间接收问题
旧的五个功能只从 rx_data[2] 取一个状态值,直接把这行复制给日期/时间会漏数据。日期需要四字节载荷,时间需要三字节载荷,但每个 CAN 数据区仍为八字节:
| 下标 | 日期帧 06 | 时间帧 07 |
|---|---|---|
Byte0 / [0] | AF 应用头 | AF 应用头 |
Byte1 / [1] | 06 功能码 | 07 功能码 |
Byte2 / [2] | 世纪数,例如 20(HEX 14) | 小时,例如 16(HEX 10) |
Byte3 / [3] | 世纪内年份,例如 25(HEX 19) | 分钟,例如 8(HEX 08) |
Byte4 / [4] | 月,例如 7(HEX 07) | 秒,例如 33(HEX 21) |
Byte5 / [5] | 月内日期,例如 20(HEX 14) | 填充 AF |
| Byte6~Byte7 | 填充 AF | 填充 AF |
“年的高位、低位”是课程给两个十进制组成部分起的名字:2025 写成数值 20 和数值 25,即 14 19。它不是把整数 2025(HEX 07E9)按二进制高低字节拆为 07 E9,也不是 BCD 的 20 25。概念上的完整年份为 20×100+25=2025。
本课把四个载荷字节先保存完整,但设置 RTC 时只使用世纪内年份 25。最高字节的世纪数虽然被接收并打包,并没有参与 RTC 的世纪判断,显示字符串也固定前缀 20。因此不能说当前代码支持任意世纪,或只要把最高字节改成 21 就能显示 2125。
1.3 定义结构体
ui_Typedef 是上一课已有类型:ui_value 用 uint32_t,ui_freshflag 用 uint8_t。本节不需要把 ui_value 从八位再改成三十二位;讲师查看已有定义,是为了说明之前预留的宽度现在可以装下多个字段。
八位无符号数赋给三十二位无符号成员不会丢失原值,所以前五项照旧可用。日期四字节则分配到三十二位的四个八位区域。成员宽度不等于整个结构体占用空间:在课程常见 Arm 编译布局下可能因对齐而有填充,不能看到 4+1 就笼统认定 sizeof(ui_Typedef) 一定是五字节;具体以工具链结果为准。
ui_date、ui_time 是两个对象,ui_Typedef 是它们共同的类型,不能把“新增两个结构体变量”写成“新增两种结构体类型”。extern 放进 can_handle.h 后,main.c 才能使用同一份对象。刷新标志把“已接收”与“已应用”区分开:解析成功置 1,主循环处理前清 0,没有新指令时不反复改 RTC。
1.4 日期和时间处理
1.4.1 日期数据的处理与移位操作
把 32 位值想象成四个八位格子,从高到低分别为 [31:24] [23:16] [15:8] [7:0]。日期打包布局为:
接收数组: [2] [3] [4] [5]
业务字段: 世纪 年内数 月 日
左移位数: 24 16 8 0
ui_value: [31:24] [23:16] [15:8] [7:0]
以 AF 06 14 19 07 14 AF AF 为例:
| 运算 | 结果(HEX) |
|---|---|
0x14 << 24,前提已转成 uint32_t | 14000000 |
0x19 << 16 | 00190000 |
0x07 << 8 | 00000700 |
0x14 << 0 | 00000014 |
| 四项按位或 | 14190714 |
| 是按位或,不是逻辑或 ||:把各项的 1 位合到结果中。这里每项占不同区域,所以不会相互覆盖。左移后空出的低位为零,不是把原来的十进制数拼接成字符串。
(uint32_t) 必须在移位之前理解。uint8_t 在表达式中会发生整数提升;先转成三十二位无符号数能避免把高字节左移到有符号 int 不能表示的范围。本节虽使用较小的日期数值,也应知道课程这样写的理由,而不能把它删掉当作多余排版。<<0 不改变数值,课程保留它让四项位置规律一致。
视频约 06:19 把月字段一度口述成“日”,应按数组位置纠正:Byte4 是月,Byte5 才是日。编译不报语法错误只能说明能生成程序,不能单独证明字节位置符合协议。
1.4.2 时间数据的处理与移位操作
时间沿用高位布局,只删除日期的第四项:
接收数组: [2] [3] [4]
业务字段: 时 分 秒
左移位数: 24 16 8
ui_value: [31:24] [23:16] [15:8] [7:0]
内容: 时 分 秒 00
AF 07 10 08 21 AF AF AF 打包为 0x10082100,表示 16 时、8 分、33 秒。最低字节是数值零,不是不确定的“空内存”,也不是从 Byte5 把填充 AF 接入。
在 main.c 追加日期/时间处理时,复制的是转向灯分支的“标志判断→清标志”框架,随后把图标颜色判断全部替换为 RTC 字段解析。日期/时间本身不使用“0 白色、非零红色”的图标逻辑,原笔记将那段旧逻辑写进时间处理不准确。
两个新对象的外部声明放在 can_handle.h,不是在 main.c 再定义一份。若有红线应同时核对包含关系、声明和变量拼写,最终以编译及链接诊断为准。
1.4.3 RTC时间设置函数与结构体
日期用 HAL_RTC_SetDate() 和 RTC_DateTypeDef,时间用 HAL_RTC_SetTime() 和 RTC_TimeTypeDef。GetDate/GetTime 读取当前值,SetDate/SetTime 设置当前值;本节是运行中校时,不是每收一帧就重新初始化所有 RTC 外设配置。
| 调用参数 | 本节含义 |
|---|---|
&hrtc | 本工程 RTC 句柄地址 |
| 日期/时间结构体地址 | HAL 读取的各字段数值 |
RTC_FORMAT_BIN | 结构体字段按普通二进制整数解释,不按 BCD 解释 |
比如 Year=25 在内存里是数值 0x19;如果混用 BCD 格式,把 0x19 当 BCD 就是 19,日期会错。串口助手的 HEX 只是输入显示形式,不会自动要求 RTC 用 BCD。
还要限定器件实现:本工程是 STM32F103 的 F1 HAL。硬件 RTC 以计数器为核心,日期由配套 HAL 的 hrtc.DateToUpdate 软件状态管理,不能套用其他 STM32 系列的硬件日历寄存器讲解。当前 HAL_RTC_SetDate() 读取 Year/Month/Date 并自行计算 WeekDay,所以课程没有赋 WeekDay 并不等同于这个版本一定读取了未初始化星期。RTC_TimeTypeDef 在本工程对应的字段是 Hours/Minutes/Seconds,不能凭其他系列的结构体补进 TimeFormat、DayLightSaving 等成员。
课堂的日期变量未整体初始化,笔记仍保持原样,并按已检查的 F1 HAL 实现解释其实际读取字段。不能把这个局部判断推广成任何 HAL 版本都可以省略结构体初始化。
1.4.4 年月日数据处理与RTC设置
打包后的值是 0x14190714,按课程操作逐个拆出:
| 字段 | 操作 | 中间结果与最终数值 |
|---|---|---|
| Year | value >>16,再 &0xff | 00001419 → 19(HEX)→ 25 |
| Month | value >>8,再 &0xff | 00141907 → 07 → 7 |
| Date | value >>0,再 &0xff | 14190714 → 14 → 20 |
右移把目标八位送到最低位置;0xff 的低八位全为 1,其余为 0,按位与只保留最低八位。这里 & 是按位与,&hrtc 中的 & 则是取地址,要按表达式用途区分。
最高字节 14(数值 20)仍可用右移 24 提取,但本节没有用它设置世纪。当前界面日期文字来自已有 sprintf(...,"20%2u-%2u-%2u",...),固定 20 前缀与年字段拼成 2025。日期被打包为一个数不等于直接把 0x14190714 当年份传给 RTC。
三项赋好再调用 HAL_RTC_SetDate();设置非 HAL_OK 进入错误处理。协议解析没有检查有效年月日,HAL 的参数断言也不能当作完整的应用报文校验。正常测试选有效日期,不能声称程序已拒绝诸如二月三十日等所有非法组合。
1.5 时间处理
1.5.1 结构体类型修改与函数粘贴
从日期分支复制后,先把对象改为 ui_time,类型改为 RTC_TimeTypeDef,设置函数改为 HAL_RTC_SetTime(),再用对应变量 struct_rtc_set_time 的地址作第二个参数。视频通过 Go To 查看接口和类型,目的在于确认这些关联,不是单纯复制一行函数名。
“delta”是 date 的误识别,月份/日期结构体与时分秒结构体确实不同。只把变量名字改成 time 而保留日期类型,不能使结构体自动拥有小时字段。
1.5.2 结构体定义与参数调整
在 main.c 的新时间分支中定义局部 RTC_TimeTypeDef struct_rtc_set_time。成员准确拼写为 Hours、Minutes、Seconds,不是小写 hour/minutes/second。分别从 ui_time.ui_value 提取并赋值,再调用设置函数。
要同步检查对象、清标志对象、字段来源和传参地址是否都属于时间。复制修改容易遗漏其中一处,代码可能能编译却一直读取日期数据,所以“没有红线”不够。
1.5.3 例题1: 移位操作调整
讲师复制日期分支时最初沿用了右移 16/8/0,随后纠正为 24/16/8。原因是日期设置忽略了最高的世纪字节,而时间的小时正占最高字节,两者拆字段位置不同。
用 value=0x10082100 对照:正确操作得到 10/08/21,即 16:08:33;错误的 16/8/0 会得到 08/21/00,即 08:33:00。这个例子直接展示了为何仅改变量名和结构体类型还不够。原笔记只写“改为24/16”漏了秒的右移 8,应补齐三项。
注意 Byte2/3/4 是数组的字节下标,24/16/8 是三十二位整数内部的位移数量。不能把“用三个字节”写成“用三个位”。
1.6 编译并烧写程序
保存新增分支与头文件声明,编译检查类型、字段拼写、括号以及链接定义,再把 ST-Link 接到 ZET6 下载本节第 26 个工程。C8T6 继续运行第 231 篇的八字节串口转发固件。ST-Link/SWD 用于下载,两个 USB 转 TTL 通道用于发指令与收日志,这些硬件接口职责不同。
ZET6 的 RTC 时钟源在配套工程中是 LSE:板上 32.768 kHz 晶体提供低速参考,RTC 预分频后用于秒计数,系统 72 MHz 时钟仍负责 CPU、LVGL 和普通业务。CAN 数据只是更改当前日期/时间数值,并没有通过 CAN 传输一个持续的秒时钟;设定完成后时钟由 RTC 继续推进。具体晶体和备份供电接法以对应 ZET6 原理图为准,本节没有修改电路。
即使备份域有 VBAT 供电,也不能仅由本节推断“收到的新日期断电/复位后一定保留”:F1 HAL 的日历日期在 RAM 句柄中。配套 MX_RTC_Init() 已有备份寄存器 DR1 的 0xA0A0 标记判断,在标记不匹配时才设置固定初始日期/时间并写标记;它并非每次启动都无条件重设。可是这个标记只说明以前执行过初始化,没有保存收到的新年月日,也没有恢复 DateToUpdate 的完整日历状态。本课没有追加日期持久化策略,应区分计数器保持供电、一次初始化标记与完整应用日历恢复。
完成下载后打开两个串口窗口,均为 115200、8N1、无流控;复位 C8T6,通过 C8T6 running 识别 USART1 日志口,在连接 USART2 的窗口按 HEX 发八字节指令。复位不是“加载源码”,而是重新走已烧入 Flash 的启动流程。
1.7 发送并查看效果
视频发送记录确认如下,均含八字节,自动发送附加回车/换行应关闭:
| HEX 指令 | 对应设置 |
|---|---|
AF 06 14 19 07 14 AF AF | 2025 年 7 月 20 日 |
AF 07 10 08 21 AF AF AF | 16:08:33 |
AF 07 10 12 21 AF AF AF | 分钟改为 0x12=18,即 16:18:33 |
AF 07 11 12 21 AF AF AF | 小时改为 0x11=17,即 17:18:33 |
AF 06 14 19 08 19 AF AF | 2025 年 8 月 25 日 |
“16 对应十点、33 对应21秒”“把8点改成12点/18分”是原笔记颠倒字节含义:HEX 10 对应十进制 16 时,08 位于分钟字段,21 对应十进制 33 秒,HEX 12 对应十进制 18 分。日期演示中口述一度提到九月,实际发送记录是 08,屏幕也显示 2025-8-25,应以画面为准。
指令通过 USART2 送到 C8T6,USART1 打印转发副本,ZET6 解析后设置 RTC。已有主循环约每超过 500 ms 读取 RTC、用 sprintf 格式化并更新日期/时间标签,LVGL 另有周期处理,因此界面更新并非 CAN 调用返回瞬间必然完成。秒随后持续增加,实际观察到的秒数比设定值大几秒并不一定是协议错位。
学习时至少自己完成一次“选字节→左移打包→右移加掩码拆出→换算显示”的验证:例如将小时 0x10 改 0x11,只应变成 17 时;将月份 07 改 08,只应变成八月;将日期 14 改 19,20 日应变成 25 日。日志相同、显示变化符合字段位置且时钟继续走,才构成完整链路验证,不能仅凭编译成功下结论。
二、核对依据
- 本节视频与完整转录:00:00~08:20 核对新对象和打包布局;08:21~18:38 核对跨文件声明、RTC 接口及最终时间位移;19:39~21:09 核对串口指令和屏幕日期时间。
- 配套
源码/3.1 CAN、LVGL、IAP固件升级专题/uint3code/26Can_Recv:BSP/Canif/can_handle.c/.h、Core/Src/main.c、rtc.c和Drivers/STM32F1xx_HAL_Driver中 RTC 头文件/实现,核对原样代码、刷新周期、LSE、软件日期与星期计算。 - ST 官方 STM32F1 RTC HAL 源码:对照 SetDate/SetTime 语义与 F1 日期管理;具体课程版本以本地配套源码为准。
- ST RM0008 参考手册:核对 STM32F103 的 RTC 计数器、时钟与备份域,避免套用其他系列硬件日历。