仪表盘Can接收代码改进(2)
234.4.8 仪表盘Can接收代码改进(2)
/* 整体目的:把 main.c 对 ui_* 对象的直接读取,改为通过 getter 取得本次新数据。 */
/* 下方七个函数是本节在 can_handle.c 实际写出的课程业务;仅去掉原注释并添加逐行解释。 */
/* 接收仍由 Can_Handle 轮询完成,getter 只消费已解析缓存,不负责重新读取 CAN 硬件。 */
uint8_t ui_get_rpm_value(uint32_t *rpm_val) /* 定义指针读取接口;返回 0 表示取得新值,1 表示本次没有新值。 */
{ /* 开始当前指针处理作用域/分支。 */
if(ui_rpm.ui_freshflag == 1) /* 检查指针缓存的待处理标志,只有等于 1 时才写出本批值。 */
{ /* 开始当前指针处理作用域/分支。 */
ui_rpm.ui_freshflag = 0; /* 消费指针标志,让没有新数据的下一次调用返回 1。 */
*rpm_val = ui_rpm.ui_value; /* 解引用输出指针,把缓存的指针值写进调用者的 uint32_t 变量。 */
return 0; /* 表示本次成功取出新数据;它是状态码,不是取出的业务数值。 */
} /* 结束当前指针作用域/分支,返回上一层执行。 */
else /* 未收到新的待处理数据时走这一支。 */
{ /* 开始当前指针处理作用域/分支。 */
return 1; /* 没有待处理的新数据,不修改输出变量原先保存的值。 */
} /* 结束当前指针作用域/分支,返回上一层执行。 */
} /* 结束当前指针作用域/分支,返回上一层执行。 */
uint8_t ui_get_water_temp_value(uint32_t *water_temp_val) /* 定义水温读取接口;返回 0 表示取得新值,1 表示本次没有新值。 */
{ /* 开始当前水温处理作用域/分支。 */
if(ui_water_temp.ui_freshflag == 1) /* 检查水温缓存的待处理标志,只有等于 1 时才写出本批值。 */
{ /* 开始当前水温处理作用域/分支。 */
ui_water_temp.ui_freshflag = 0; /* 消费水温标志,让没有新数据的下一次调用返回 1。 */
*water_temp_val = ui_water_temp.ui_value; /* 解引用输出指针,把缓存的水温值写进调用者的 uint32_t 变量。 */
return 0; /* 表示本次成功取出新数据;它是状态码,不是取出的业务数值。 */
} /* 结束当前水温作用域/分支,返回上一层执行。 */
else /* 未收到新的待处理数据时走这一支。 */
{ /* 开始当前水温处理作用域/分支。 */
return 1; /* 没有待处理的新数据,不修改输出变量原先保存的值。 */
} /* 结束当前水温作用域/分支,返回上一层执行。 */
} /* 结束当前水温作用域/分支,返回上一层执行。 */
uint8_t ui_get_safty_belt_value(uint32_t *safty_belt_val) /* 定义安全带读取接口;返回 0 表示取得新值,1 表示本次没有新值。 */
{ /* 开始当前安全带处理作用域/分支。 */
if(ui_safty_belt.ui_freshflag == 1) /* 检查安全带缓存的待处理标志,只有等于 1 时才写出本批值。 */
{ /* 开始当前安全带处理作用域/分支。 */
ui_safty_belt.ui_freshflag = 0; /* 消费安全带标志,让没有新数据的下一次调用返回 1。 */
*safty_belt_val = ui_safty_belt.ui_value; /* 解引用输出指针,把缓存的安全带值写进调用者的 uint32_t 变量。 */
return 0; /* 表示本次成功取出新数据;它是状态码,不是取出的业务数值。 */
} /* 结束当前安全带作用域/分支,返回上一层执行。 */
else /* 未收到新的待处理数据时走这一支。 */
{ /* 开始当前安全带处理作用域/分支。 */
return 1; /* 没有待处理的新数据,不修改输出变量原先保存的值。 */
} /* 结束当前安全带作用域/分支,返回上一层执行。 */
} /* 结束当前安全带作用域/分支,返回上一层执行。 */
uint8_t ui_get_light_value(uint32_t *light_val) /* 定义灯光读取接口;返回 0 表示取得新值,1 表示本次没有新值。 */
{ /* 开始当前灯光处理作用域/分支。 */
if(ui_light.ui_freshflag == 1) /* 检查灯光缓存的待处理标志,只有等于 1 时才写出本批值。 */
{ /* 开始当前灯光处理作用域/分支。 */
ui_light.ui_freshflag = 0; /* 消费灯光标志,让没有新数据的下一次调用返回 1。 */
*light_val = ui_light.ui_value; /* 解引用输出指针,把缓存的灯光值写进调用者的 uint32_t 变量。 */
return 0; /* 表示本次成功取出新数据;它是状态码,不是取出的业务数值。 */
} /* 结束当前灯光作用域/分支,返回上一层执行。 */
else /* 未收到新的待处理数据时走这一支。 */
{ /* 开始当前灯光处理作用域/分支。 */
return 1; /* 没有待处理的新数据,不修改输出变量原先保存的值。 */
} /* 结束当前灯光作用域/分支,返回上一层执行。 */
} /* 结束当前灯光作用域/分支,返回上一层执行。 */
uint8_t ui_get_turn_light_value(uint32_t *turn_light_val) /* 定义转向灯读取接口;返回 0 表示取得新值,1 表示本次没有新值。 */
{ /* 开始当前转向灯处理作用域/分支。 */
if(ui_turn_light.ui_freshflag == 1) /* 检查转向灯缓存的待处理标志,只有等于 1 时才写出本批值。 */
{ /* 开始当前转向灯处理作用域/分支。 */
ui_turn_light.ui_freshflag = 0; /* 消费转向灯标志,让没有新数据的下一次调用返回 1。 */
*turn_light_val = ui_turn_light.ui_value; /* 解引用输出指针,把缓存的转向灯值写进调用者的 uint32_t 变量。 */
return 0; /* 表示本次成功取出新数据;它是状态码,不是取出的业务数值。 */
} /* 结束当前转向灯作用域/分支,返回上一层执行。 */
else /* 未收到新的待处理数据时走这一支。 */
{ /* 开始当前转向灯处理作用域/分支。 */
return 1; /* 没有待处理的新数据,不修改输出变量原先保存的值。 */
} /* 结束当前转向灯作用域/分支,返回上一层执行。 */
} /* 结束当前转向灯作用域/分支,返回上一层执行。 */
uint8_t ui_get_date_value(uint32_t *date_val) /* 定义日期读取接口;返回 0 表示取得新值,1 表示本次没有新值。 */
{ /* 开始当前日期处理作用域/分支。 */
if(ui_date.ui_freshflag == 1) /* 检查日期缓存的待处理标志,只有等于 1 时才写出本批值。 */
{ /* 开始当前日期处理作用域/分支。 */
ui_date.ui_freshflag = 0; /* 消费日期标志,让没有新数据的下一次调用返回 1。 */
*date_val = ui_date.ui_value; /* 解引用输出指针,把缓存的日期值写进调用者的 uint32_t 变量。 */
return 0; /* 表示本次成功取出新数据;它是状态码,不是取出的业务数值。 */
} /* 结束当前日期作用域/分支,返回上一层执行。 */
else /* 未收到新的待处理数据时走这一支。 */
{ /* 开始当前日期处理作用域/分支。 */
return 1; /* 没有待处理的新数据,不修改输出变量原先保存的值。 */
} /* 结束当前日期作用域/分支,返回上一层执行。 */
} /* 结束当前日期作用域/分支,返回上一层执行。 */
uint8_t ui_get_time_value(uint32_t *time_val) /* 定义时间读取接口;返回 0 表示取得新值,1 表示本次没有新值。 */
{ /* 开始当前时间处理作用域/分支。 */
if(ui_time.ui_freshflag == 1) /* 检查时间缓存的待处理标志,只有等于 1 时才写出本批值。 */
{ /* 开始当前时间处理作用域/分支。 */
ui_time.ui_freshflag = 0; /* 消费时间标志,让没有新数据的下一次调用返回 1。 */
*time_val = ui_time.ui_value; /* 解引用输出指针,把缓存的时间值写进调用者的 uint32_t 变量。 */
return 0; /* 表示本次成功取出新数据;它是状态码,不是取出的业务数值。 */
} /* 结束当前时间作用域/分支,返回上一层执行。 */
else /* 未收到新的待处理数据时走这一支。 */
{ /* 开始当前时间处理作用域/分支。 */
return 1; /* 没有待处理的新数据,不修改输出变量原先保存的值。 */
} /* 结束当前时间作用域/分支,返回上一层执行。 */
} /* 结束当前时间作用域/分支,返回上一层执行。 */
can_handle.h——本节增加的七个函数声明:
uint8_t ui_get_rpm_value(uint32_t *rpm_val); /* 声明指针读取接口,参数是 32 位输出变量的地址,返回值表示是否有新数据。 */
uint8_t ui_get_water_temp_value(uint32_t *water_temp_val); /* 声明水温读取接口,参数是 32 位输出变量的地址,返回值表示是否有新数据。 */
uint8_t ui_get_safty_belt_value(uint32_t *safty_belt_val); /* 声明安全带读取接口,参数是 32 位输出变量的地址,返回值表示是否有新数据。 */
uint8_t ui_get_light_value(uint32_t *light_val); /* 声明灯光读取接口,参数是 32 位输出变量的地址,返回值表示是否有新数据。 */
uint8_t ui_get_turn_light_value(uint32_t *turn_light_val); /* 声明转向灯读取接口,参数是 32 位输出变量的地址,返回值表示是否有新数据。 */
uint8_t ui_get_date_value(uint32_t *date_val); /* 声明日期读取接口,参数是 32 位输出变量的地址,返回值表示是否有新数据。 */
uint8_t ui_get_time_value(uint32_t *time_val); /* 声明时间读取接口,参数是 32 位输出变量的地址,返回值表示是否有新数据。 */
main.c——输出变量声明与本节替换后的主循环业务片段:
/* 第一行放在全局 PV 用户区,Can_Handle 及其后的各分支放在已有 while(1) 内。 */
uint32_t rpm_value,water_temp_value,safty_belt_value,light_value,turn_light_value,date_value,time_value; /* main.c 的 PV 区声明七个输出值变量;仍是全局对象,并未被封装自动消灭。 */
Can_Handle(); /* 主循环沿用轮询解析,将一帧数据写入对应缓存并置刷新标志。 */
if(ui_get_rpm_value(&rpm_value) == 0) /* 传指针输出变量的地址;比较的是 getter 返回状态,0 才进行业务刷新。 */
{ /* 开始当前指针处理作用域/分支。 */
lv_img_set_angle(ui_ImageNeedle, rpm_value*2500/100); /* 指针百分比乘 2500 再除 100,转换成 LVGL 0.1 度单位的角度。 */
} /* 结束当前指针作用域/分支,返回上一层执行。 */
if(ui_get_water_temp_value(&water_temp_value) == 0) /* 传水温输出变量的地址;比较的是 getter 返回状态,0 才进行业务刷新。 */
{ /* 开始当前水温处理作用域/分支。 */
if(water_temp_value == 0) /* 业务值为零时显示白色图标;这个零与 getter 成功状态的零不同。 */
{ /* 开始当前水温处理作用域/分支。 */
color_value = lv_color_make(COLOR_WHITE); /* 按原颜色宏构造 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 /* 有新数据,但业务状态不满足前面的颜色条件时走这一支。 */
{ /* 开始当前水温处理作用域/分支。 */
color_value = lv_color_make(COLOR_RED); /* 按原颜色宏构造 LVGL 颜色值,保持既有状态到颜色的映射。 */
lv_obj_set_style_img_recolor(ui_ImageWatertemp, color_value, 0); /* 把选定颜色应用到这一路既有图标,最后参数沿用原样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageWatertemp, 255, 0); /* 设图像重着色不透明度为 255,使选定的颜色充分作用于图标。 */
} /* 结束当前水温作用域/分支,返回上一层执行。 */
} /* 结束当前水温作用域/分支,返回上一层执行。 */
if(ui_get_safty_belt_value(&safty_belt_value) == 0) /* 传安全带输出变量的地址;比较的是 getter 返回状态,0 才进行业务刷新。 */
{ /* 开始当前安全带处理作用域/分支。 */
if(safty_belt_value == 0) /* 业务值为零时显示白色图标;这个零与 getter 成功状态的零不同。 */
{ /* 开始当前安全带处理作用域/分支。 */
color_value = lv_color_make(COLOR_WHITE); /* 按原颜色宏构造 LVGL 颜色值,保持既有状态到颜色的映射。 */
lv_obj_set_style_img_recolor(ui_ImageSfatyBelt, color_value, 0); /* 把选定颜色应用到这一路既有图标,最后参数沿用原样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageSfatyBelt, 255, 0); /* 设图像重着色不透明度为 255,使选定的颜色充分作用于图标。 */
} /* 结束当前安全带作用域/分支,返回上一层执行。 */
else /* 有新数据,但业务状态不满足前面的颜色条件时走这一支。 */
{ /* 开始当前安全带处理作用域/分支。 */
color_value = lv_color_make(COLOR_RED); /* 按原颜色宏构造 LVGL 颜色值,保持既有状态到颜色的映射。 */
lv_obj_set_style_img_recolor(ui_ImageSfatyBelt, color_value, 0); /* 把选定颜色应用到这一路既有图标,最后参数沿用原样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageSfatyBelt, 255, 0); /* 设图像重着色不透明度为 255,使选定的颜色充分作用于图标。 */
} /* 结束当前安全带作用域/分支,返回上一层执行。 */
} /* 结束当前安全带作用域/分支,返回上一层执行。 */
if(ui_get_light_value(&light_value) == 0) /* 传灯光输出变量的地址;比较的是 getter 返回状态,0 才进行业务刷新。 */
{ /* 开始当前灯光处理作用域/分支。 */
if(light_value == 0) /* 业务值为零时显示白色图标;这个零与 getter 成功状态的零不同。 */
{ /* 开始当前灯光处理作用域/分支。 */
color_value = lv_color_make(COLOR_WHITE); /* 按原颜色宏构造 LVGL 颜色值,保持既有状态到颜色的映射。 */
lv_obj_set_style_img_recolor(ui_ImageLight, color_value, 0); /* 把选定颜色应用到这一路既有图标,最后参数沿用原样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageLight, 255, 0); /* 设图像重着色不透明度为 255,使选定的颜色充分作用于图标。 */
} /* 结束当前灯光作用域/分支,返回上一层执行。 */
else if(light_value == 2) /* 灯光业务状态为 2 时显示红色;其余非零值仍按旧逻辑显示黄色。 */
{ /* 开始当前灯光处理作用域/分支。 */
color_value = lv_color_make(COLOR_RED); /* 按原颜色宏构造 LVGL 颜色值,保持既有状态到颜色的映射。 */
lv_obj_set_style_img_recolor(ui_ImageLight, color_value, 0); /* 把选定颜色应用到这一路既有图标,最后参数沿用原样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageLight, 255, 0); /* 设图像重着色不透明度为 255,使选定的颜色充分作用于图标。 */
} /* 结束当前灯光作用域/分支,返回上一层执行。 */
else /* 有新数据,但业务状态不满足前面的颜色条件时走这一支。 */
{ /* 开始当前灯光处理作用域/分支。 */
color_value = lv_color_make(COLOR_YELLOW); /* 按原颜色宏构造 LVGL 颜色值,保持既有状态到颜色的映射。 */
lv_obj_set_style_img_recolor(ui_ImageLight, color_value, 0); /* 把选定颜色应用到这一路既有图标,最后参数沿用原样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageLight, 255, 0); /* 设图像重着色不透明度为 255,使选定的颜色充分作用于图标。 */
} /* 结束当前灯光作用域/分支,返回上一层执行。 */
} /* 结束当前灯光作用域/分支,返回上一层执行。 */
if(ui_get_turn_light_value(&turn_light_value) == 0) /* 传转向灯输出变量的地址;比较的是 getter 返回状态,0 才进行业务刷新。 */
{ /* 开始当前转向灯处理作用域/分支。 */
if(turn_light_value == 0) /* 业务值为零时显示白色图标;这个零与 getter 成功状态的零不同。 */
{ /* 开始当前转向灯处理作用域/分支。 */
color_value = lv_color_make(COLOR_WHITE); /* 按原颜色宏构造 LVGL 颜色值,保持既有状态到颜色的映射。 */
lv_obj_set_style_img_recolor(ui_ImageTurnlight, color_value, 0); /* 把选定颜色应用到这一路既有图标,最后参数沿用原样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageTurnlight, 255, 0); /* 设图像重着色不透明度为 255,使选定的颜色充分作用于图标。 */
} /* 结束当前转向灯作用域/分支,返回上一层执行。 */
else /* 有新数据,但业务状态不满足前面的颜色条件时走这一支。 */
{ /* 开始当前转向灯处理作用域/分支。 */
color_value = lv_color_make(COLOR_RED); /* 按原颜色宏构造 LVGL 颜色值,保持既有状态到颜色的映射。 */
lv_obj_set_style_img_recolor(ui_ImageTurnlight, color_value, 0); /* 把选定颜色应用到这一路既有图标,最后参数沿用原样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageTurnlight, 255, 0); /* 设图像重着色不透明度为 255,使选定的颜色充分作用于图标。 */
} /* 结束当前转向灯作用域/分支,返回上一层执行。 */
} /* 结束当前转向灯作用域/分支,返回上一层执行。 */
if(ui_get_date_value(&date_value) == 0) /* 传日期输出变量的地址;比较的是 getter 返回状态,0 才进行业务刷新。 */
{ /* 开始当前日期处理作用域/分支。 */
RTC_DateTypeDef struct_rtc_set_date; /* 沿用本课局部日期设置结构体,没有擅自增加整体初始化。 */
struct_rtc_set_date.Year = ((date_value >> 16) & 0xff); /* 从 getter 返回的打包日期中右移并用 0xff 提取该字段,位移保持 16/8/0。 */
struct_rtc_set_date.Month = ((date_value >> 8) & 0xff); /* 从 getter 返回的打包日期中右移并用 0xff 提取该字段,位移保持 16/8/0。 */
struct_rtc_set_date.Date = ((date_value >> 0) & 0xff); /* 从 getter 返回的打包日期中右移并用 0xff 提取该字段,位移保持 16/8/0。 */
if (HAL_RTC_SetDate(&hrtc, &struct_rtc_set_date, RTC_FORMAT_BIN) != HAL_OK) /* 把拆出的普通二进制年月日交给 F1 RTC 日期接口,检查 HAL 状态。 */
{ /* 开始当前日期处理作用域/分支。 */
Error_Handler(); /* 设置失败则按原课程进入错误处理,没有新增重试或回执。 */
} /* 结束当前日期作用域/分支,返回上一层执行。 */
} /* 结束当前日期作用域/分支,返回上一层执行。 */
if(ui_get_time_value(&time_value) == 0) /* 传时间输出变量的地址;比较的是 getter 返回状态,0 才进行业务刷新。 */
{ /* 开始当前时间处理作用域/分支。 */
RTC_TimeTypeDef struct_rtc_set_time; /* 沿用本课局部时间设置结构体,稍后逐项赋时分秒。 */
struct_rtc_set_time.Hours = ((time_value >> 24) & 0xff); /* 从 getter 返回的打包时间中提取该字段,位移保持 24/16/8。 */
struct_rtc_set_time.Minutes = ((time_value >> 16) & 0xff); /* 从 getter 返回的打包时间中提取该字段,位移保持 24/16/8。 */
struct_rtc_set_time.Seconds = ((time_value >> 8) & 0xff); /* 从 getter 返回的打包时间中提取该字段,位移保持 24/16/8。 */
if (HAL_RTC_SetTime(&hrtc, &struct_rtc_set_time, RTC_FORMAT_BIN) != HAL_OK) /* 把拆出的普通二进制时分秒设置到 RTC,检查 HAL 状态。 */
{ /* 开始当前时间处理作用域/分支。 */
Error_Handler(); /* 设置失败则按原课程进入错误处理,没有新增重试或回执。 */
} /* 结束当前时间作用域/分支,返回上一层执行。 */
} /* 结束当前时间作用域/分支,返回上一层执行。 */
代码分别属于
can_handle.c、can_handle.h、main.c,按对应位置收录,不能合成一个新文件。七个 getter 和声明按配套第 28 个工程原样抄录;本课主循环仍直接调用 LVGL,依据视频将上一阶段主循环的标志读取替换为 getter。配套最终main.c中的ui_set_*()属于下一课,不提前抄入。本课沿用已有can_handle.h、ui.h、RTC/LVGL 包含关系、颜色宏和color_value,不重复抄录库代码及自动生成的界面代码。
一、改unit 3 code的28个工程
本课继续修改上课复制出的 uint3code/28Can_Recv_Ok,目标板仍为 ZET6。改动范围是获取已解析 CAN 数据的接口:从 main.c 直接使用 ui_rpm.ui_value/ui_freshflag 等共享成员,转为调用七个 ui_get_*_value()。协议、CAN ID、图标颜色和 RTC 字段布局保持前课的行为。
1.1 函数调用便利性分析
讲师以 canif.c/.h 为例:调用发送时通过普通参数给 ID、长度和缓冲地址,调用接收时通过指针获得 ID/数据并通过返回值得到长度。调用方通过明确接口使用模块,无需记住模块每个内部变量。
这里“返回数据长度”说的是已有 Can_Recv_Msg(),不是本节 getter。本节 getter 的返回值是状态 0/1,输出数值则写入指针所指向的变量。两个接口采用不同约定,不能把返回值统一解释为长度。
封装可以减少调用方对内部结构体布局和标志规则的依赖,移植时也更容易看清所需接口。不过 canif.c 仍依赖 hcan、HAL 和初始化配置,并不是完全没有任何全局依赖;getter 也依赖本文件里的缓存对象。原笔记“变量全消灭、全局变量变少”应理解为减少外部文件直接操作共享对象,不是自动节省内存或自动获得线程安全。
1.2 代码使用便利性分析
前课 main.c 对每个对象重复执行“检查标志、清标志、读值”,同时知道 ui_Typedef 的内部成员名字。本课把这三步放进 can_handle.c,main.c 只要知道“请求最新值,成功再应用”。
原来的 CAN 接收、解析和图标/RTC 设置并没有都塞进 getter:解析还在 Can_Handle(),getter 提供业务值,主循环继续调用 LVGL 或 RTC。这三个环节分工如下:
| 环节 | 本节位置 | 负责的事情 |
|---|---|---|
| 从 CAN 取帧并解析 | Can_Handle() | 筛 ID/AF/功能码,写缓存并置标志 |
| 取出待处理业务值 | ui_get_*_value() | 检查标志、消费标志、通过指针输出 |
| 将业务值应用到仪表 | main.c | 更新图标/指针,或拆字段设置 RTC |
1.3 封函数代替变量
1.3.1 封get rpm value函数
真实函数名为 ui_get_rpm_value,返回类型 uint8_t,参数 uint32_t *rpm_val。前者只保存本节 0/1 状态,后者指向一个三十二位无符号输出对象,能够承接所有现有 ui_value,包括后续日期/时间打包值。
例如 main.c 定义 uint32_t rpm_value,调用 ui_get_rpm_value(&rpm_value):& 取出变量地址,函数形参 rpm_val 收到这个地址,*rpm_val = ui_rpm.ui_value 通过解引用写入原变量。函数退出后,调用方的 rpm_value 已经改变。
不要把这行写成 rpm_val = ui_rpm.ui_value:那会尝试修改保存地址的形参,不能把业务值写给调用者。C 的参数是按值传递;这里传的是地址的副本,再通过该地址修改调用者对象,而不是说 C 自动提供了“引用传递”。
getter 取得的是之前解析好的百分比,不是现场读取硬件指针角度,也不重新从 CAN 接收一帧。没有新数据时返回 1;有新数据时清标志、写输出、返回 0。
1.3.2 函数中判断数据刷新
本节 getter 的契约可以用两次调用解释:
| 调用时缓存状态 | 函数内部动作 | 返回值 | 调用者输出变量 |
|---|---|---|---|
| freshflag=1,value=34 | 清标志、写出34 | 0 | 被更新为34 |
| freshflag=0,没有新帧 | 不写输出 | 1 | 保持上一次值 |
所以它是读取并消费一次待处理更新,不是每调用一次都返回当前缓存。第二次不应重复显示“又来了一个34”。如果主循环只在返回 0 时更新仪表,就能保持原有一次新数据触发一次业务处理的规则。
原笔记将“先清标志再从 CAN 获得数据”写错了:CAN 获取已发生在前面的 Can_Handle();getter 是读保存的 ui_value。没有新数据返回 1 本身不是硬件错误,也不是非法业务值。
七个接口都没有检查输出指针是否为 NULL。调用者必须给一个有效、可写且类型匹配的 uint32_t 对象地址;不要传空指针、未初始化地址或把 uint8_t 的地址强转进来。前课定义的输出变量是全局对象,启动时默认零初始化,但没有新数据时仍应依照返回状态使用它们。
1.3.3 主函数中判断返回值
主循环执行 Can_Handle() 后调用 if(ui_get_rpm_value(&rpm_value) == 0)。比较的是函数调用返回值,不是地址是否为零,也不是 rpm_value 的值是否为零。成功后用 rpm_value*2500/100 更新原 ui_ImageNeedle。
这里有三种不同的“零”:getter 返回 0 表示得到新数据;指针百分比为 0 表示仪表回到起点;图标状态为 0 表示白色。函数完全可以成功取出业务值 0,不能把“业务值0”当作“没有收到数据”。
百分比 34 的例子为 34*2500/100=850,LVGL 以 0.1° 为单位,角度是 85°。当前协议不传实际发动机 RPM,也没有因为 getter 名字含 rpm 就完成物理转速换算。
1.3.4 之前之后的代码对比
原先 main.c 自己检查 ui_rpm.ui_freshflag、清它,再读取 ui_rpm.ui_value。改后只调用 getter,将其返回状态和输出值用于显示。视频先同时放新旧两段方便比较,随后删除旧段,最后留下新段。
1.3.4.1 新代码特征:
新主循环知道一个函数及一个输出变量,不再重复写该对象的内部标志规则。返回值负责流程判断,指针输出负责取值,LVGL 调用负责视觉结果。这种拆分也让模块以后修改内部缓存布局时,调用方更少受影响。
1.3.4.2 新旧代码对比结果:
不是“main.c 与 handle.c 从此只有一个函数”:仪表指针使用一个 getter,七个业务各有自己的接口,另外还调用 Can_Handle()。也不是原来的 ui_* 全局对象被删除,它们仍在 can_handle.c 供解析和 getter 使用,main.c 还新增了七个输出变量。
新旧两段不能长期同时保留。旧段先消费标志后,getter 就会认为没有新数据,导致第二段不执行;反过来也一样。最后应只有一种消费路径,而不是在两个地方清同一个标志。
1.3.5 继续封函数
按照指针 getter 的结构复制,依次为水温、安全带、灯光、转向灯、日期、时间建立接口。每次都核对三处:函数名和参数名、读取对象、写出指针。只改函数名不改对象,会使接口返回其他业务的数据,且可能完全没有编译错误。
视频因中文注释显示问题,进入 Keil Edit → Configuration → Encoding,选择 Chinese GB2312(简体中文)。这是讲师既有源码/编辑器环境的编码设置,配套 can_handle.c 确实包含此类中文注释。本文 Markdown 以 UTF-8 保存;编码设置不改变函数算法,不能把它误解为单片机运行中文代码的功能开关。
建议初学者在理解指针契约后至少手写一个 getter,再对比其余六个的变化位置。复制粘贴可以省重复工作,但不能代替确认每个接口与业务对象是否配对。原笔记“先不声明”只是讲师暂时统一处理声明的写作顺序,最终所有对外使用的接口都需要相应声明。
1.3.6 注释剩余函数
七个接口精确对应:
| 接口 | 缓存对象 | 写入调用方的内容 |
|---|---|---|
| ui_get_rpm_value | ui_rpm | 指针百分比 |
| ui_get_water_temp_value | ui_water_temp | 水温图标状态 |
| ui_get_safty_belt_value | ui_safty_belt | 安全带图标状态 |
| ui_get_light_value | ui_light | 灯光状态 |
| ui_get_turn_light_value | ui_turn_light | 转向灯状态 |
| ui_get_date_value | ui_date | 四字节打包日期 |
| ui_get_time_value | ui_time | 高三个字节打包的时间 |
“注释安全带以确保安全性”是原笔记的误解:注释用于说明这是安全带图标 getter,本课没有用注释实现任何硬件保护机制。代码中的 safty 按课程拼写保存。date 表示日期,不能写成另一个不存在的 data 函数来替换。
本课接收轮询与 getter 消费都发生在同一个主循环,没有同时运行的 CAN 接收中断写这些对象。不过若以后迁移到中断/RTOS,检查标志、清标志和读值不是自动原子的完整事务,需另行处理并发;给外面套函数不会自动使数据同步安全。
1.3.7 指针声明
原标题“指针声明”实际是带指针参数的函数原型声明,不是函数指针变量。例如 uint8_t ui_get_rpm_value(uint32_t *rpm_val); 告诉调用文件函数返回/参数类型,末尾必须有分号;本节并没有声明形如 (*某名字)(...) 的函数指针。
把七个原型放入 can_handle.h,实现放在 .c,调用方已有 #include "can_handle.h"。声明与实现必须一致,不能把 uint32_t 指针参数声明成普通整数来消除红线。头文件保留类型/宏/枚举和 Can_Handle() 声明。
本节强调停止从主函数直接操作共享对象,但没有给这些对象补 static。因此不能说这一步已在 C 链接层强制禁止其他文件访问它们,实际改进是使用方式与接口依赖。视频约14分钟的头文件还保留七个 extern 声明,下一课才移除这些不再需要的外部声明,不能把配套最终文件提前套入本课。
1.4 编译并改主函数
1.4.1 改主函数
在原全局 PV 区定义 rpm_value, water_temp_value, safty_belt_value, light_value, turn_light_value, date_value, time_value,类型均为 uint32_t,对应七个输出指针。保留前面的轮询、RTC 周期显示和 LVGL 服务调用。本节只替换七项更新条件和值来源,不改它们在协议中的编码。
1.4.1.1 改水温部分
外层条件改为 ui_get_water_temp_value(&water_temp_value) == 0,删除主循环中对水温刷新标志的清零。内层改读 water_temp_value:等于 0 显示白色,否则红色。
原笔记“水温值”在这里是报警图标状态,不是摄氏温度。原 LVGL 调用仍设置颜色并把 recolor opacity 设为 255,没有引入真正温度传感器。
1.4.1.2 改安全带部分
外层调用 ui_get_safty_belt_value(&safty_belt_value),检查返回值等于 0;内层使用 safty_belt_value,保持零白色、非零红色。清标志只在 getter 内进行一次。
原笔记“判断地址是否为零来决定刷新”应纠正:&safty_belt_value 是输出地址,程序没有拿它和 0 作更新判断。若误把 if 写成地址条件,就绕过了有无新数据的逻辑。
1.4.1.3 改灯部分
外层由检查 flag==1 改成检查 getter 返回值==0,这不是把灯光业务状态从1改成0。三个颜色条件保持:业务值0白色,业务值2红色,其他非零值黄色,更新对应图标及不透明度。
复制时应把所有旧 ui_light.ui_value 读取都换成 light_value,而不是只换第一处。函数输出参数须传 &light_value,不能漏取地址,也不能错传转向灯变量。
1.4.1.4 改转向灯部分
转向灯使用 ui_get_turn_light_value(&turn_light_value),随后按 turn_light_value 决定颜色。本节保持零白色、非零红色,没有追加自动闪烁定时器。
原笔记中的“将地量变量赋值为零、复制三个相同变量”混入了其他分支的操作:这里删除的是对缓存标志的重复清零,不是先强行把收到的业务值清零。日期的三个字段读取才需要同步替换三个来源表达式。
1.4.1.5 改时间部分
视频先改日期再改时间。日期 getter 输出的是完整打包值,仍用 date_value >>16/8/0 再 &0xff 提取年内数、月、日;最高世纪字节仍忽略。时间 getter 输出布局没有变化,仍用 time_value >>24/16/8 提取时、分、秒,不能套用日期的三个移位数。
两段继续使用 RTC_DateTypeDef / RTC_TimeTypeDef,调用 HAL_RTC_SetDate/SetTime(...,RTC_FORMAT_BIN),失败走原 Error_Handler。Getter 返回0与 HAL 返回 HAL_OK 是两层不同的成功条件,不能只检查取到值就省略 RTC 设置结果。
配套最终主循环在下一课把显示改为 ui_set_*(),本课视频约 21:16 的最终代码仍在 main.c 写 LVGL 与 RTC 操作。顶部按这一阶段收录,不借最终文件把课堂尚未写出的显示封装提前补进来。
二、烧录至板子并测试
保存并编译第 28 个接收工程,把它下载到 ZET6。C8T6 继续用第 27 个串口转发固件。视频左窗为 USART1,复位 C8T6 后出现 C8T6 running;右窗为 USART2,向它按 HEX 发送完整八字节。两口均为115200、8N1,指令不自动加回车/换行。
视频开始发送无响应,关闭再打开串口后恢复,这是该次串口工具/连接现象,不说明所有无响应都可只靠重开解决。调试应分段确认:发送记录、C8T6 转发日志、ZET6 接收解析、getter 有新值和仪表效果。
课堂逐项测试了七个业务,具体发送记录包含沿用日期指令时留下的尾部字节。这些当前未参与前五项功能,但一次仍必须八字节:
| 测试帧(HEX) | 预期行为 |
|---|---|
AF 02 01 19 08 19 AF AF | 水温图标红色 |
AF 03 01 19 08 19 AF AF | 安全带图标红色 |
AF 01 22 19 08 19 AF AF | 0x22=34,指针34%,即85° |
AF 04 01 19 08 19 AF AF | 灯光黄色 |
AF 04 02 19 08 19 AF AF | 灯光红色 |
AF 05 01 19 08 19 AF AF | 转向灯红色 |
AF 06 14 19 08 19 AF AF | 按正确世纪字段设置2025-8-25 |
AF 07 12 19 08 19 AF AF | 0x12=18、0x19=25、0x08=8,设18:25:08 |
日期测试画面实际沿用的是 Byte2=12 的帧,仍显示2025,因为最高世纪字节不参与本课 RTC 设置;表中日期项用协议要求的14说明正确测试输入,不将这个修改冒充视频原发送记录。时间的 Byte5=19 是沿用填充,实际只取 Byte2~4。
讲师还提到 C8T6 的 can send 前缀后忘了冒号,可以自行加。视频本节并未实际修改发送端源码,本文仍保留第231篇原字符串,不把这句建议当作已完成的代码改动。
所有 getter 都改过,因此要逐项测试,不能仅测一个水温接口就认定其余六个复制无误。还可再次调用同一 getter 而不发新帧:第二次返回1、输出值不变、没有再次刷新,验证“消费一次更新”的契约。图标状态0也应能够正常取出并显示白色,帮助区分“返回成功”和“业务值为零”。
三、核对依据
- 本节视频/完整转录:02:08~07:47 核对首个 getter、指针与新旧对照;07:48~14:32 核对其余函数、编码和原型;14:33~21:25 核对全部主循环替换;21:26~23:49 核对七项业务测试与串口现象。
- 配套
28Can_Recv_Ok/BSP/Canif/can_handle.c/.h:七个 getter 与原型逐项按原源码核对。主循环依照视频及前阶段26Can_Recv/Core/Src/main.c核对本节替换,后课显示封装不提前收录。 - C11 委员会草案 N1570,6.5.2.2、6.5.3.2、6.7.6.3:核对函数参数、取地址/解引用及原型的语义。
- LVGL v8.3 图像旋转文档:核对角度以0.1度为单位;具体课程行为同时核对本地 LVGL v8 源码。