仪表盘Can接收代码改进(3)
235.4.9 仪表盘Can接收代码改进(3)
#include "../ui.h" /* 沿用屏幕源文件已有的 UI 头文件,取得控件对象、LVGL 类型和函数声明。 */
#define COLOR_RED 0xff,0x0,0x0 /* 红色:RGB 三个分量为 255、0、0。 宏展开成三个逗号分隔的参数,不带分号。 */
#define COLOR_WHITE 0xff,0xff,0xff /* 白色:RGB 三个分量都是 255。 宏展开成三个逗号分隔的参数,不带分号。 */
#define COLOR_YELLOW 0xff,0xff,0x0 /* 黄色:RGB 分量为 255、255、0。 宏展开成三个逗号分隔的参数,不带分号。 */
void ui_set_date_time_value(uint8_t *date_str, uint8_t * time_str) /* 日期和时间字符串以指针传入;void 表示没有返回值,不能当成返回数值 0。 */
{ /* 开始当前函数或条件分支的作用域。 */
lv_label_set_text(ui_LabelTime, time_str); /* 复制对应的以空字符结尾的字符串到标签内部;本课保留 uint8_t* 传参写法。 */
lv_label_set_text(ui_LabelDate, date_str); /* 复制对应的以空字符结尾的字符串到标签内部;本课保留 uint8_t* 传参写法。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
void ui_set_rpm_angle(uint32_t angle) /* 按值传入指针百分比;参数虽叫 angle,调用者传的是百分比而不是度数。 */
{ /* 开始当前函数或条件分支的作用域。 */
lv_img_set_angle(ui_ImageNeedle, angle*2500/100); /* 先乘 2500 再除以 100,把百分比换为 LVGL 的 0.1 度单位;100 对应 250 度。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
void ui_set_water_temp_color( uint32_t color) /* 按值传入水温图标状态编号,用于选择颜色,不是传 RGB 数值或变量地址。 */
{ /* 开始当前函数或条件分支的作用域。 */
lv_color_t color_value; /* 在本次函数调用的局部作用域中保存 LVGL 颜色对象。 */
if(color == 0) /* 状态编号为 0 时选择白色,并非读取函数的返回状态。 */
{ /* 开始当前函数或条件分支的作用域。 */
color_value = lv_color_make(COLOR_WHITE); /* 把对应 RGB 宏的三个分量转换成 LVGL 颜色对象。 */
lv_obj_set_style_img_recolor(ui_ImageWatertemp, color_value, 0); /* 把 color_value 设置为对应图标的重着色颜色,保留原控件名称和默认样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageWatertemp, 255, 0); /* 将该图标的重着色不透明度设为 255,即全量重着色;末尾 0 是样式选择器。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
else /* 其余状态沿用课堂分支;二色图标显示红色,三色灯光显示黄色。 */
{ /* 开始当前函数或条件分支的作用域。 */
color_value = lv_color_make(COLOR_RED); /* 把对应 RGB 宏的三个分量转换成 LVGL 颜色对象。 */
lv_obj_set_style_img_recolor(ui_ImageWatertemp, color_value, 0); /* 把 color_value 设置为对应图标的重着色颜色,保留原控件名称和默认样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageWatertemp, 255, 0); /* 将该图标的重着色不透明度设为 255,即全量重着色;末尾 0 是样式选择器。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
void ui_set_safty_belt_color( uint32_t color) /* 按值传入安全带图标状态编号,用于选择颜色,不是传 RGB 数值或变量地址。 */
{ /* 开始当前函数或条件分支的作用域。 */
lv_color_t color_value; /* 在本次函数调用的局部作用域中保存 LVGL 颜色对象。 */
if(color == 0) /* 状态编号为 0 时选择白色,并非读取函数的返回状态。 */
{ /* 开始当前函数或条件分支的作用域。 */
color_value = lv_color_make(COLOR_WHITE); /* 把对应 RGB 宏的三个分量转换成 LVGL 颜色对象。 */
lv_obj_set_style_img_recolor(ui_ImageSfatyBelt, color_value, 0); /* 把 color_value 设置为对应图标的重着色颜色,保留原控件名称和默认样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageSfatyBelt, 255, 0); /* 将该图标的重着色不透明度设为 255,即全量重着色;末尾 0 是样式选择器。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
else /* 其余状态沿用课堂分支;二色图标显示红色,三色灯光显示黄色。 */
{ /* 开始当前函数或条件分支的作用域。 */
color_value = lv_color_make(COLOR_RED); /* 把对应 RGB 宏的三个分量转换成 LVGL 颜色对象。 */
lv_obj_set_style_img_recolor(ui_ImageSfatyBelt, color_value, 0); /* 把 color_value 设置为对应图标的重着色颜色,保留原控件名称和默认样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageSfatyBelt, 255, 0); /* 将该图标的重着色不透明度设为 255,即全量重着色;末尾 0 是样式选择器。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
void ui_set_light_color( uint32_t color) /* 按值传入灯光图标状态编号,用于选择颜色,不是传 RGB 数值或变量地址。 */
{ /* 开始当前函数或条件分支的作用域。 */
lv_color_t color_value; /* 在本次函数调用的局部作用域中保存 LVGL 颜色对象。 */
if(color == 0) /* 状态编号为 0 时选择白色,并非读取函数的返回状态。 */
{ /* 开始当前函数或条件分支的作用域。 */
color_value = lv_color_make(COLOR_WHITE); /* 把对应 RGB 宏的三个分量转换成 LVGL 颜色对象。 */
lv_obj_set_style_img_recolor(ui_ImageLight, color_value, 0); /* 把 color_value 设置为对应图标的重着色颜色,保留原控件名称和默认样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageLight, 255, 0); /* 将该图标的重着色不透明度设为 255,即全量重着色;末尾 0 是样式选择器。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
else if(color == 2) /* 灯光状态编号为 2 时选择红色;其余非零编号由最后的 else 显示黄色。 */
{ /* 开始当前函数或条件分支的作用域。 */
color_value = lv_color_make(COLOR_RED); /* 把对应 RGB 宏的三个分量转换成 LVGL 颜色对象。 */
lv_obj_set_style_img_recolor(ui_ImageLight, color_value, 0); /* 把 color_value 设置为对应图标的重着色颜色,保留原控件名称和默认样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageLight, 255, 0); /* 将该图标的重着色不透明度设为 255,即全量重着色;末尾 0 是样式选择器。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
else /* 其余状态沿用课堂分支;二色图标显示红色,三色灯光显示黄色。 */
{ /* 开始当前函数或条件分支的作用域。 */
color_value = lv_color_make(COLOR_YELLOW); /* 把对应 RGB 宏的三个分量转换成 LVGL 颜色对象。 */
lv_obj_set_style_img_recolor(ui_ImageLight, color_value, 0); /* 把 color_value 设置为对应图标的重着色颜色,保留原控件名称和默认样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageLight, 255, 0); /* 将该图标的重着色不透明度设为 255,即全量重着色;末尾 0 是样式选择器。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
void ui_set_turn_light_color( uint32_t color) /* 按值传入转向灯图标状态编号,用于选择颜色,不是传 RGB 数值或变量地址。 */
{ /* 开始当前函数或条件分支的作用域。 */
lv_color_t color_value; /* 在本次函数调用的局部作用域中保存 LVGL 颜色对象。 */
if(color == 0) /* 状态编号为 0 时选择白色,并非读取函数的返回状态。 */
{ /* 开始当前函数或条件分支的作用域。 */
color_value = lv_color_make(COLOR_WHITE); /* 把对应 RGB 宏的三个分量转换成 LVGL 颜色对象。 */
lv_obj_set_style_img_recolor(ui_ImageTurnlight, color_value, 0); /* 把 color_value 设置为对应图标的重着色颜色,保留原控件名称和默认样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageTurnlight, 255, 0); /* 将该图标的重着色不透明度设为 255,即全量重着色;末尾 0 是样式选择器。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
else /* 其余状态沿用课堂分支;二色图标显示红色,三色灯光显示黄色。 */
{ /* 开始当前函数或条件分支的作用域。 */
color_value = lv_color_make(COLOR_RED); /* 把对应 RGB 宏的三个分量转换成 LVGL 颜色对象。 */
lv_obj_set_style_img_recolor(ui_ImageTurnlight, color_value, 0); /* 把 color_value 设置为对应图标的重着色颜色,保留原控件名称和默认样式选择器。 */
lv_obj_set_style_img_recolor_opa(ui_ImageTurnlight, 255, 0); /* 将该图标的重着色不透明度设为 255,即全量重着色;末尾 0 是样式选择器。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
**代码位置和收录范围:**上面是本课在 midware/ui/screens/ui_Screen1.c 末尾新增的三个颜色宏、六个显示接口,开头保留该文件已有的 #include "../ui.h"。只收录课堂业务,不抄整套自动生成的界面创建代码或 LVGL 库实现。代码按配套 28Can_Recv_Ok 保留,仅调整排版、替换逐行注释;包括课程已有的 safty、Sfaty 拼写和 uint8_t * 字符串参数,不擅自改名或加类型转换。
midware/ui/ui.h:本课新增的六个声明,放在已有头文件保护范围内。
void ui_set_date_time_value(uint8_t *date_str, uint8_t * time_str); /* 在 ui.h 中声明接口;末尾分号必需。日期和时间字符串以指针传入;void 表示没有返回值,不能当成返回数值 0。 */
void ui_set_rpm_angle(uint32_t angle); /* 在 ui.h 中声明接口;末尾分号必需。按值传入指针百分比;参数虽叫 angle,调用者传的是百分比而不是度数。 */
void ui_set_water_temp_color( uint32_t color); /* 在 ui.h 中声明接口;末尾分号必需。按值传入水温图标状态编号,用于选择颜色,不是传 RGB 数值或变量地址。 */
void ui_set_safty_belt_color( uint32_t color); /* 在 ui.h 中声明接口;末尾分号必需。按值传入安全带图标状态编号,用于选择颜色,不是传 RGB 数值或变量地址。 */
void ui_set_light_color( uint32_t color); /* 在 ui.h 中声明接口;末尾分号必需。按值传入灯光图标状态编号,用于选择颜色,不是传 RGB 数值或变量地址。 */
void ui_set_turn_light_color( uint32_t color); /* 在 ui.h 中声明接口;末尾分号必需。按值传入转向灯图标状态编号,用于选择颜色,不是传 RGB 数值或变量地址。 */
Core/Src/main.c:本课清理后的用户变量区。
RTC_TimeTypeDef struct_rtc_time; /* 保存 RTC 读出的时、分、秒等字段,供字符串格式化使用。 */
RTC_DateTypeDef struct_rtc_date; /* 保存 RTC 读出的年、月、日字段,供字符串格式化使用。 */
uint8_t time_buff[15] = {0}; /* 保存时间字符串的 15 字节缓冲区,初始清零,含结尾空字符的空间。 */
uint8_t date_buff[15] = {0}; /* 保存日期字符串的 15 字节缓冲区,初始清零。 */
uint32_t timetick = 0; /* 记录上次刷新日期时间显示的 HAL 毫秒时刻。 */
uint32_t lvtick = 0; /* 记录上次执行 LVGL 任务处理的 HAL 毫秒时刻。 */
uint32_t rpm_value,water_temp_value,safty_belt_value,light_value,turn_light_value,date_value,time_value; /* 保留七个 getter 的 32 位输出变量;本课没有擅自改成局部变量。 */
**main.c:本课保留的外设初始化调用和用户初始化片段。**这些语句处于已有 main() 内、时钟配置之后,不是独立程序。保留课堂注释掉的触摸初始化行,展示“关闭输入、保留显示”的具体位置。
MX_GPIO_Init(); /* 沿用 CubeMX 生成的 MX_GPIO_Init 外设初始化调用;TIM1、TIM3 已在配置中关闭。 */
MX_SPI1_Init(); /* 沿用 CubeMX 生成的 MX_SPI1_Init 外设初始化调用;TIM1、TIM3 已在配置中关闭。 */
MX_USART1_UART_Init(); /* 沿用 CubeMX 生成的 MX_USART1_UART_Init 外设初始化调用;TIM1、TIM3 已在配置中关闭。 */
MX_TIM6_Init(); /* 沿用 CubeMX 生成的 MX_TIM6_Init 外设初始化调用;TIM1、TIM3 已在配置中关闭。 */
MX_RTC_Init(); /* 沿用 CubeMX 生成的 MX_RTC_Init 外设初始化调用;TIM1、TIM3 已在配置中关闭。 */
MX_CAN_Init(); /* 沿用 CubeMX 生成的 MX_CAN_Init 外设初始化调用;TIM1、TIM3 已在配置中关闭。 */
HAL_TIM_Base_Start_IT(&htim6); /* 启动仍需保留的 TIM6 周期中断,为 LVGL 提供毫秒节拍。 */
Can_Filter_Init(); /* 沿用 CAN 过滤器及启动配置,使接收端能取得业务帧。 */
lv_init(); /* 初始化 LVGL 核心,随后注册显示驱动并创建 UI 控件。 */
lv_port_disp_init(); /* 注册显示驱动;关闭触摸并不关闭屏幕输出。 */
//lv_port_indev_init(); /* 本课关闭触摸输入注册;显示驱动初始化仍保留。 */
ui_init(); /* 创建和加载课堂仪表盘界面;setter 在这些控件存在后调用。 */
**main.c:显示封装完成后的主循环片段。**这里没有补写 main()、系统时钟函数、HAL 库或下一节业务。RTC 设置沿用上一课,只将 LVGL 显示调用换为本课 setter。
while (1) /* 主循环持续处理 LVGL、RTC 显示和 CAN 新数据。 */
{ /* 开始当前函数或条件分支的作用域。 */
if(HAL_GetTick() - lvtick > 5) /* 超过 5 毫秒才处理 LVGL;这是主循环中的时间差判断,不是严格硬实时周期。 */
{ /* 开始当前函数或条件分支的作用域。 */
lvtick = HAL_GetTick(); /* 保存本次处理时刻,作为下一次 LVGL 时间差的基准。 */
lv_timer_handler(); /* 执行 LVGL 的定时任务及刷新调度;封装显示接口后此调用仍必需。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
if(HAL_GetTick() - timetick >500) /* 超过 500 毫秒才刷新一次 RTC 文本,沿用本课的大于号。 */
{ /* 开始当前函数或条件分支的作用域。 */
timetick = HAL_GetTick(); /* 保存本次文本刷新时刻,避免每轮循环都调用标签更新。 */
HAL_RTC_GetTime(&hrtc, &struct_rtc_time, RTC_FORMAT_BIN); /* 读取 RTC 当前时间,RTC_FORMAT_BIN 表示普通二进制字段。 */
sprintf((char *)(time_buff),"%2u:%2u:%2u", struct_rtc_time.Hours, struct_rtc_time.Minutes, struct_rtc_time.Seconds); /* 把时分秒写成文本;%2u 最小宽度 2、默认补空格,课堂没有改为 %02u。 */
HAL_RTC_GetDate(&hrtc, &struct_rtc_date, RTC_FORMAT_BIN); /* 读取 F1 HAL 管理的日期字段,仍在 main.c 中完成。 */
sprintf((char *)date_buff,"20%2u-%2u-%2u", struct_rtc_date.Year, struct_rtc_date.Month, struct_rtc_date.Date); /* 把两位年份加固定 20 前缀后拼为日期文本;仍未使用 CAN 传来的世纪字段。 */
ui_set_date_time_value(date_buff, time_buff); /* 把两个字符串缓冲区的首地址交给显示接口,顺序是日期、时间。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
Can_Handle(); /* 轮询并解析 CAN 业务,把接收值和刷新标志写入 can_handle.c 的缓存。 */
if(ui_get_rpm_value(&rpm_value) == 0) /* 传出参地址,让 getter 写回指针值;返回 0 表示本次取得新数据。 */
{ /* 开始当前函数或条件分支的作用域。 */
ui_set_rpm_angle(rpm_value); /* 把 getter 已写回的业务数值按值交给显示接口,这里不加取地址符 &。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
if(ui_get_water_temp_value(&water_temp_value) == 0) /* 传出参地址,让 getter 写回水温图标值;返回 0 表示本次取得新数据。 */
{ /* 开始当前函数或条件分支的作用域。 */
ui_set_water_temp_color(water_temp_value); /* 把 getter 已写回的业务数值按值交给显示接口,这里不加取地址符 &。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
if(ui_get_safty_belt_value(&safty_belt_value) == 0) /* 传出参地址,让 getter 写回安全带图标值;返回 0 表示本次取得新数据。 */
{ /* 开始当前函数或条件分支的作用域。 */
ui_set_safty_belt_color(safty_belt_value); /* 把 getter 已写回的业务数值按值交给显示接口,这里不加取地址符 &。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
if(ui_get_light_value(&light_value) == 0) /* 传出参地址,让 getter 写回灯光图标值;返回 0 表示本次取得新数据。 */
{ /* 开始当前函数或条件分支的作用域。 */
ui_set_light_color(light_value); /* 把 getter 已写回的业务数值按值交给显示接口,这里不加取地址符 &。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
if(ui_get_turn_light_value(&turn_light_value) == 0) /* 传出参地址,让 getter 写回转向灯图标值;返回 0 表示本次取得新数据。 */
{ /* 开始当前函数或条件分支的作用域。 */
ui_set_turn_light_color(turn_light_value); /* 把 getter 已写回的业务数值按值交给显示接口,这里不加取地址符 &。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
if(ui_get_date_value(&date_value) == 0) /* 传出参地址,让 getter 写回日期值;返回 0 表示本次取得新数据。 */
{ /* 开始当前函数或条件分支的作用域。 */
RTC_DateTypeDef struct_rtc_set_date; /* 沿用课堂日期设置结构体;RTC 设置没有迁入显示文件。 */
struct_rtc_set_date.Year = ((date_value >> 16) & 0xff); /* 按日期打包位置右移 16/8/0 位并与 0xff,提取年月日字段。 */
struct_rtc_set_date.Month = ((date_value >> 8) & 0xff); /* 按日期打包位置右移 16/8/0 位并与 0xff,提取年月日字段。 */
struct_rtc_set_date.Date = ((date_value >> 0) & 0xff); /* 按日期打包位置右移 16/8/0 位并与 0xff,提取年月日字段。 */
if (HAL_RTC_SetDate(&hrtc, &struct_rtc_set_date, RTC_FORMAT_BIN) != HAL_OK) /* 把年月日设置到 RTC,使用普通二进制格式,并检查 HAL 返回状态。 */
{ /* 开始当前函数或条件分支的作用域。 */
Error_Handler(); /* RTC 设置失败时进入原课程的错误处理。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
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); /* 按时间打包位置右移 24/16/8 位并与 0xff,提取时分秒字段。 */
struct_rtc_set_time.Minutes = ((time_value >> 16) & 0xff); /* 按时间打包位置右移 24/16/8 位并与 0xff,提取时分秒字段。 */
struct_rtc_set_time.Seconds = ((time_value >> 8) & 0xff); /* 按时间打包位置右移 24/16/8 位并与 0xff,提取时分秒字段。 */
if (HAL_RTC_SetTime(&hrtc, &struct_rtc_set_time, RTC_FORMAT_BIN) != HAL_OK) /* 把时分秒设置到 RTC,使用普通二进制格式,并检查 HAL 返回状态。 */
{ /* 开始当前函数或条件分支的作用域。 */
Error_Handler(); /* RTC 设置失败时进入原课程的错误处理。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
} /* 结束当前作用域,随后执行外层剩余代码。 */
一、仪器仪表Can接收代码改进
本课接着 234 的 getter 接口继续整理接收工程:上一课解决“怎样读出 CAN 缓存的新数据”,本课解决“拿到数据后怎样显示”。完成后对应配套工程 28Can_Recv_Ok。本课约 00:00~13:40 封装显示、13:40~15:57 下载调试、15:59~20:23 清理无用依赖、20:24~22:16 比较工程和总结调用方式。
1.1 继续改进代码
1.1.1 代码结构调整与优化
原笔记的 lgl、Y1、me.c 等应分别核对为 LVGL、main.c、ui_Screen1.c。工程中的 ui_Screen1.c 是屏幕文件,原来已有 ui_Screen1_screen_init() 创建控件;教师在这个 文件的末尾另写函数,并不是把所有运行期刷新代码塞进屏幕初始化函数。
本课的职责划分如下:
| 位置 | 保留的职责 | 本课变化 |
|---|---|---|
canif.c | 底层 CAN 收发 | 本课未重写其库/驱动逻辑 |
can_handle.c | CAN 类型判断、数据缓存、七个 getter | 保留 233~234 已有逻辑 |
main.c | 调度、检查 getter 状态、RTC 设置及文本格式化 | 用显示接口替代直接操作图标、标签的代码 |
ui_Screen1.c | 界面控件和具体显示操作 | 新增六个 ui_set_* 接口及颜色宏 |
ui.h | 对外提供 UI 声明 | 新增六个接口声明 |
这样查看 main.c 时,可以直接读出“有新水温状态就更新水温图标”,不用在主循环里反复阅读 RGB 和重着色设置。程序仍执行同样的显示逻辑;模块化首先改善理解和维护,不能仅凭源码行数减少就断言 CPU 更快或 Flash 一定变小。课堂通过鼠标定位文件、选中缩进等编辑动作,目的都是把相关功能集中并保持括号层次清楚。
1.1.2 函数封装与代码复用
教师先封装水温图标,再复制相似的代码建立安全带、灯光、转向灯接口。每次复制都必须改 函数名、调用参数、控件对象、对应颜色分支。水温/安全带/转向灯是二色分支,灯光是三色分支,不能随意用二色函数覆盖灯光第三种状态。
以 ui_set_water_temp_color(uint32_t color) 为例:返回类型 void 表示 没有返回值,不是转录所说“返回值为 0”;形参 color 接收一个业务状态编号。调用 ui_set_water_temp_color(water_temp_value) 时,传入的是变量中保存的数值。参数名虽叫颜色,1 并不是红色 RGB 编码,而是由函数内部按协议映射成红色。
| 显示接口 | 操作对象 | 状态到颜色/角度的映射 |
|---|---|---|
ui_set_water_temp_color | ui_ImageWatertemp | 0 白色,其他值红色 |
ui_set_safty_belt_color | ui_ImageSfatyBelt | 0 白色,其他值红色 |
ui_set_light_color | ui_ImageLight | 0 白色,2 红色,其余值黄色 |
ui_set_turn_light_color | ui_ImageTurnlight | 0 白色,其他值红色 |
ui_set_rpm_angle | ui_ImageNeedle | 输入百分比乘 2500 再除以 100 |
ui_set_date_time_value | 时间、日期标签 | 先更新时间标签,再更新日期标签 |
这些状态是课程演示的显示协议。本节没有把水温值换算为摄氏度,也没有测量实际安全带开关,更没有用这些 setter 去驱动车灯硬件。“亮红/黄灯”在这里是 屏幕上图标变色。
1.1.3 变量定义与宏定义处理
color_value 的实际类型是 lv_color_t。教师把原先 main.c 中的颜色临时变量移动到每个颜色 setter 内;于是它成为函数局部变量,只在这次调用里用来保存选中的颜色。如果搬来代码却漏掉这个定义,函数体内使用 color_value 就会找不到声明。
三个颜色宏也从 main.c 移到 ui_Screen1.c:COLOR_RED 展开成 0xff,0x0,0x0,用于 lv_color_make(COLOR_RED) 时,相当于给该函数传三个 RGB 分量。0xff 是 255,0x0 是 0;白色三个分量都是 255,黄色是红、绿分量 255、蓝分量 0。这些宏不是一个打包的 24 位整数,末尾也不能加分号,否则会破坏调用表达式。
lv_obj_set_style_img_recolor(..., color_value, 0) 指定图像的重着色颜色;对应 recolor_opa(...,255,0) 设为全量重着色。最后参数 0 是默认状态、主体部分对应的样式选择器,不是图标颜色、不透明度或坐标。图像本身仍保留轮廓与透明区域,不能把“全量重着色”理解为把整个矩形背景涂满。此处依据配套 LVGL 8.3 源码和 LVGL 8.3 图像文档。
课堂的 Ctrl+X 是剪切移动代码;Ctrl+C/Ctrl+V 是复制。移动后需检查旧位置是否还有无用变量,并保持每个函数自己的局部变量定义。旧变量变成未使用时,编译器可能给警告;但是否有警告还取决于编译设置,不能靠“没警告”证明完全没有冗余。
1.1.4 函数声明与调用
原笔记的 ur.h 应为 ui.h。教师在已有 ui.h 中声明函数;定义位于 .c 文件、声明以分号结尾、调用者通过 #include "ui.h" 得到声明。这三个位置的函数名、参数类型和顺序必须一致。
最容易混淆的是以下两步:ui_get_light_value(&light_value) 需要输出变量的 地址,因为 getter 要把收到的值写回;ui_set_light_color(light_value) 需要 值,因为 setter 只读这个状态决定颜色。教师约 07:15 特别把显示调用中多余的取地址符去掉,不能把 getter 的 & 机械复制到 setter。
外层 if(ui_get_light_value(&light_value) == 0) 检查有没有新数据;里面 ui_set_light_color(light_value) 才更新显示。getter 返回 0 是成功状态,light_value 等于 0 则是业务含义“显示白色”,两者含义不同。
课堂将多行图标代码收成“判断、作用域、调用”形式,是缩短调用处的表达;原显示逻辑实际迁移到了函数里,并未凭空消失。约 04:23 教师修正水温名字多写 w 的问题,必须同步检查定义、头文件声明和 main.c 调用。复制后还要逐项确认 ui_ImageWatertemp 没有误用于安全带或转向灯,以及灯光的 else if(color == 2) 没有丢失。
1.1.5 仪表盘与指针⻆度设置
指针接口名为 ui_set_rpm_angle(uint32_t angle),本课保留这个名字和参数。**调用者传入的 rpm_value 是百分比,angle 形参名并不改变数值单位。**按 LVGL 8.3 的角度单位,lv_img_set_angle 的 2500 表示 250.0°,不是 2500°。因此:
[ A_{\text{LVGL}}=\left\lfloor\frac{P\times2500}{100}\right\rfloor,\qquad \theta=A_{\text{LVGL}}/10;\text{度} ]
例如 33% 得到 825,即 82.5°;65% 得到 1625,即 162.5°。不能先把 33 除以 100 做整数除法,否则提前得到 0。本课函数没有添加范围限制,收到超过 100 的数值仍按原表达式计算,不应把未写出的限幅逻辑补进代码。
日期和时间的 显示 一起封装为 ui_set_date_time_value(date_str,time_str),参数是两个字符串首地址。顺序是日期在前、时间在后;函数体先写 ui_LabelTime 再写 ui_LabelDate,不能因为执行顺序不同就把实参颠倒。sprintf 在主循环准备文本,并附带结尾 \0。%2u 为最小两字符宽度、默认前面补空格,和 %02u 补零不同;本课没有改格式字符串。年份前缀仍固定为 20,不是通过世纪字段自动生成。
lv_label_set_text 会复制字符串到标签的内部缓冲区,因此这两个参数不是让标签永久借用外部数组。该 API 的文本参数实际为 const char *,课程 setter 却用 uint8_t * 直接传入,视频 19:20 附近能看到类型不兼容警告 #167-D。逐行源码保留原写法,在笔记里指出接口类型问题,不能悄悄替教师加转换然后声称是原代码。LVGL 8.3 标签文档
约 09:02,教师明确保留 HAL_RTC_SetDate/SetTime 在 main.c:修改计时数据和把数据画到标签上是两件事。CAN 设置 RTC 后,周期读取 RTC、格式化字符串、再调用显示接口;不是收到时间包就仅改一个不再走时的字符串。
1.1.6 串口通信与数据发送
调试链路延续 231:电脑串口助手按 HEX 发送 8 字节 → C8T6 的 USART2 接收 → C8T6 转发 CAN → ZET6 仪表盘解析 → getter 写回业务值 → setter 改屏幕。烧录接收工程后,打开相关串口、复位发送端,再逐项发送并观察屏幕和发送端打印。本节未新增串口转发代码。
视频约 13:43~15:57 的数据可整理如下;这些都是 十六进制字节,不是按 ASCII 发送的字符:
| 演示报文 | 含义 |
|---|---|
AF 07 12 11 00 19 AF AF | 类型 07:时 0x12=18,分 0x11=17,秒 00;第六字节 19 不参与时间打包 |
AF 06 12 11 03 19 AF AF | 类型 06:年 0x11=17、月 03、日 0x19=25,界面显示 2017 年 3 月 25 日;世纪字节 12 没有进入设置年份逻辑 |
AF 05 01 11 03 19 AF AF | 类型 05:转向灯状态 1,图标红色 |
AF 04 01 11 03 19 AF AF | 类型 04:灯光状态 1,图标黄色 |
AF 04 02 11 03 19 AF AF | 类型 04:灯光状态 2,图标红色 |
AF 03 01 11 03 19 AF AF | 类型 03:安全带状态 1,图标红色 |
AF 02 01 11 03 19 AF AF | 类型 02:水温状态 1,图标红色 |
AF 01 21 11 03 19 AF AF | 类型 01:0x21=33%,指针角 82.5° |
AF 01 21 41 03 19 AF AF | 教师一度改错位置:第三字节仍是 21,第四字节变 41,指针值仍是 33% |
AF 01 41 41 03 19 AF AF | 改对第三字节后,0x41=65%,指针角 162.5° |
原笔记的“转向灯代码 55”是误识别,业务类型实际是 0x05。数据区只由对应类型解释,本课不把末尾所有 AF 都当成严格校验尾;接收代码检查帧 ID、首字节及类型,并不是完整的防错协议。
教师遇到无响应时关闭再打开串口窗口,恢复后再发送,这只是这次演示的排查过程,不能据此确定所有无响应都由串口软件造成。复现时还需核对 HEX 模式、8 字节长度、115200 波特率、设备端口、发送端转发日志、CAN 接收条件和实际修改的字节位置。本课视频显示封装后日期、时间、指针和图标均可继续变化;这是课堂实机观察,不是本轮精校在真实硬件上重新运行的结果。
1.2 删除用不到的变量
约 16:07 教师从 can_handle.h 移除原来七个 extern ui_Typedef ui_*; 声明:调用者已经使用 getter,不再需要通过头文件访问内部缓存。**删除的是对外声明,不是 can_handle.c 内实际接收缓存的定义。**缓存对象仍用于解析和 getter,误删定义会破坏程序。
课程没有把这些对象加上 static;因此头文件不再公开它们,只是减少公开接口,并不等于从 C 链接层面强制禁止其他文件访问。这里不替教师追加 static。同样,七个 *_value 输出变量仍在主文件全局区,不是本课已经消除了全部全局变量。
教师检查并删除旧演示的 needle_angle、time_count 等无用变量;不确定时先注释、编译并搜索引用,再确认是否可删。保留 timetick 和 lvtick,它们分别控制日期时间刷新及 LVGL 调度。检查范围应覆盖 .h 声明、.c 定义、调用点和工程文件;不要因为一个文件内看不到调用就判定整个工程不用。
1.3 删除用不到的定时器
本课废弃的是 TIM3 的风扇 PWM 和 TIM1 的 DHT11 微秒延时依赖。旧初始化区的 HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_2) 不再用于当前 CAN 仪表盘,先移除对应启动调用;再在 CubeMX 配置中关闭 TIM1、TIM3,重新生成代码,使句柄、初始化和相关配置一起更新。
不能把“当前工程两个定时器不用”推广为所有定时器都能删。最终工程仍有 TIM6,HAL_TIM_Base_Start_IT(&htim6) 保留;配套 tim.c 使用预分频 71、自动重装 999,72 MHz 定时器时钟下计数频率为 1 MHz、1000 次计数产生 1 ms 周期,已有回调调用 lv_tick_inc(1)。这给 LVGL 时间基准,主循环的 lv_timer_handler() 则处理实际任务,两者承担不同职责。
仅注释定时器启动调用并不自动从工程删除生成的初始化函数;仅在 CubeMX 关掉外设也不自动清除其他用户模块对句柄的引用。后面的 htim1 链接错误正说明需要一起检查依赖。
1.4 删除用不到的触摸屏
原笔记“LV ds 中的音 DV 输入”“imax”“DC 包”“q 工程”分别应理解为 LVGL 输入设备初始化、CubeMX、Disable、Keil 工程。教师将 lv_port_indev_init() 注释掉,当前仪表盘由 CAN 更新,无需触摸操作。保留 lv_port_disp_init()、lv_init() 和 ui_init(),所以屏幕继续显示。
indev 是 input device(输入设备),disp 是 display(显示)。触摸输入驱动负责把触摸坐标和按下/松开状态送给 LVGL;显示驱动负责把绘制结果送到液晶。关闭前者不会自动关闭后者,也不等于从磁盘删除触摸驱动文件或改变板上触摸电路。
课堂随后打开 CubeMX,关闭 TIM1、TIM3 的相关启用配置并重新生成,返回 Keil 重新加载工程、编译。此过程的正确判断依据是生成文件和编译结果;原笔记将 Flash 变化都归因于“删触摸屏”过于简单,本课同时做了显示封装、变量清理、外设关闭和无用模块移除。
1.5 编译时发现错误从而改进代码
视频约 19:20 的确切错误是 链接错误:Error: L6218E: Undefined symbol htim1 (referred from dht11.o)。含义是 dht11.c 已编译为 dht11.o,其代码引用了 TIM1 句柄,但当前定时器配置已不再提供 htim1 的定义。它不是简单的“DHT11 头文件找不到”。
教师先注释 main.c 中 #include "dht11.h",再编译仍报相同错误。原因是 Keil 工程仍把 dht11.c 作为待编译源文件:头文件控制本文件可见的声明,不决定其他 .c 是否参加构建。最终从工程树对 dht11.c 执行 Remove,再次构建后链接通过。
Remove 表示从当前工程构建列表移除,不能写成“彻底删除磁盘上的 .c/.h 文件”;配套目录中是否还保留源码与是否编译是两个问题。也不能据此教成所有 Undefined symbol 都可以靠删模块解决:本次模块确实是旧 DHT11 演示残留,当前业务不需要,才适合移除;仍需业务则要恢复正确依赖。
需要区分 错误和警告。19:20 附近的全量构建同时显示字符串指针类型等警告;后面只重编部分改动文件时显示 0 Error(s),0 Warning(s),不代表先前 UI 文件的类型问题已被修复。课堂没有展示修正 uint8_t * 到 const char * 的提交,笔记也不声称警告彻底消失。完整验证应看全量重建涉及的文件及信息,而不只看增量构建最后一行。
1.6 之前工程代码占用内存情况
这里的“26”“28”是目录 26Can_Recv、28Can_Recv_Ok,不是“同时打开 26 个工程”。教师约 20:24~21:10 打开旧工程进行编译比较,使用画面中的 Arm Compiler V5.06 update 7(build 960)。原笔记的“VC5”不准确;“312”是约 312 KB 的 Flash 占用,不是新工程版本号。
下表按视频编译输出的内存条核对(课程工具以 1024 字节换算并显示 KB):
| 编译对象 | RAM 占用 / 总量 | RAM 剩余 | Flash 占用 / 总量 | Flash 剩余 |
|---|---|---|---|---|
旧 26Can_Recv,约 21:00 | 61.88 KB / 64 KB,96.68% | 2176 B | 333.20 KB / 512 KB,65.08% | 183096 B |
新 28Can_Recv_Ok,约 20:25~21:15 | 61.73 KB / 64 KB,96.46% | 2320 B | 311.82 KB / 512 KB,60.90% | 204998 B |
Flash 剩余增加 21902 B,约 21.39 KB;RAM 剩余增加 144 B,约 0.14 KB。显示数字保留两位小数,所以直接用 61.88−61.73 得到 0.15 KB 是四舍五入后的表面差,字节栏更精确。封装中途约 13:44 还出现过 333.33 KB Flash、61.90 KB RAM,不应把这个中间状态和最终清理后的结果混为一谈。
Flash 可以理解为烧录后断电仍保留程序和常量的空间;SRAM 是运行时存放可写数据的工作空间,普通断电不保持。二者不是同一池内存,空出的 Flash 不能自动变成更多 SRAM。表格是链接结果统计,不能仅凭它推断屏幕帧率提升。
二、Can接收代码优化
2.1 内存优化
原文“从 61.88 降至 6”是转录截断导致的严重错误。实际是 61.88 KB → 61.73 KB,减少很小,工程仍占用了约 96.46% 的 64 KB RAM。不能写成优化后只剩 6 KB,也不能把这次封装当成解决了全部 RAM 容量问题。
视频新工程还显示 Program Size: Code=156608 RO-data=162692 RW-data=696 ZI-data=62520。其中 Code 是机器指令等代码内容,RO-data 是只读数据(本工程含图像等资源),RW-data 是需初值的可写数据,ZI-data 是启动时清零的可写空间。在常见 Flash 执行布局下,Flash 需求按 Code+RO+RW、静态 RAM 需求按 RW+ZI 理解;实际还需结合 MAP、链接布局及初值压缩,不能把几个整数相加后强行替换课程工具的 Flash 数字。此处 RAM 校验为 696+62520=63216 B,总量 65536 B 减去后恰好剩 2320 B。Arm 编译器用户指南:映像大小字段与统计
当前源码的 LVGL 内存池、绘制缓冲等运行空间仍存在。MAP 中若已保留堆、栈或 LVGL 内存池,它们也占据 RAM 区,内部动态分配不能再机械重复相加;内存条剩余空间也不能当成已经实测的运行栈峰值或 LVGL 池内空闲值。进一步优化需要定位实际大对象和运行需求,本课没有完成这一步。
本课最值得掌握的调用链是:Can_Handle() 更新缓存 → getter 返回 0 并通过地址写回业务值 → main.c 将数值传给显示 setter → setter 操作 LVGL 对象。日期和时间还有 RTC 设置这一步,随后由周期显示逻辑读取当前 RTC。这样主循环可以用返回值判断是否需要刷新,既减少跨模块直接访问,也让每段代码的职责清楚。
学习检查:解释为什么 getter 有 & 而颜色 setter 没有;指出 void 与返回 0 的区别;按十六进制解释 41 为什么是 65;判断改第四字节为何不改指针;说明删除 extern 为何不能删除缓存定义;区分关闭触摸、关闭显示和关闭 TIM6;解释为什么删主文件头文件后 dht11.o 仍会造成链接错误。能逐项说清这些问题,才能真正理解本节封装和清理的目的。
**核对依据:**本节视频与完整转录;配套 28Can_Recv_Ok 的 ui_Screen1.c、ui.h、main.c、can_handle.c/.h、tim.c、LVGL 8.3 标签及图像源码;旧 26Can_Recv 的对应工程。重点画面为约 13:44 主循环、15:40~15:54 串口字节位置纠错、19:20~20:06 htim1 链接错误、20:25 新工程和 21:00 旧工程的内存统计。新增源码逐行注释,并核对去注释后的代码与本节配套片段一致。