图标颜色修改(2)
223.3.22 图标颜色修改(2)
uint32_t time_count = 0; // 定义循环节拍计数器并清零;每经过一轮约 5 ms 的主循环便加 1
while (1) // 持续运行 LVGL 服务和图标颜色切换逻辑
{ // 主循环开始
lv_timer_handler(); // 处理已经到期的 LVGL 定时器任务,包括输入、动画和待刷新的显示区域
HAL_Delay(5); // 阻塞主循环约 5 ms,使 lv_timer_handler() 保持毫秒级周期调用
time_count++; // 计数器加 1;理想情况下这一拍约对应 5 ms
time_count = time_count %400; // 对 400 取余,使计数器始终在 0~399 之间循环
if(1 == time_count) // 计数值刚变为 1 时,只执行一次红色状态更新
{ // 红色更新分支开始
color_value = lv_color_make(COLOR_RED); // 使用上一节定义的 RGB 参数生成 LVGL 红色值
lv_obj_set_style_img_recolor(ui_ImageLight, color_value, 0); // 设置灯光图标的重着色颜色为红色
lv_obj_set_style_img_recolor_opa(ui_ImageLight, 255, 0); // 把灯光图标的重着色强度设为最大值
lv_obj_set_style_img_recolor(ui_ImageTurnlight, color_value, 0); // 设置转向灯图标的重着色颜色为红色
lv_obj_set_style_img_recolor_opa(ui_ImageTurnlight, 255, 0); // 把转向灯图标的重着色强度设为最大值
lv_obj_set_style_img_recolor(ui_ImageWatertemp, color_value, 0); // 设置水温图标的重着色颜色为红色
lv_obj_set_style_img_recolor_opa(ui_ImageWatertemp, 255, 0); // 把水温图标的重着色强度设为最大值
lv_obj_set_style_img_recolor(ui_ImageSfatyBelt, color_value, 0); // 设置安全带图标的重着色颜色为红色
lv_obj_set_style_img_recolor_opa(ui_ImageSfatyBelt, 255, 0); // 把安全带图标的重着色强度设为最大值
} // 红色更新分支结束
if(201 == time_count) // 再经过约 200 个节拍时,只执行一次白色状态更新
{ // 白色更新分支开始
color_value = lv_color_make(COLOR_WHITE); // 使用上一节定义的 RGB 参数生成 LVGL 白色值
lv_obj_set_style_img_recolor(ui_ImageLight, color_value, 0); // 设置灯光图标的重着色颜色为白色
lv_obj_set_style_img_recolor_opa(ui_ImageLight, 255, 0); // 把灯光图标的重着色强度设为最大值
lv_obj_set_style_img_recolor(ui_ImageTurnlight, color_value, 0); // 设置转向灯图标的重着色颜色为白色
lv_obj_set_style_img_recolor_opa(ui_ImageTurnlight, 255, 0); // 把转向灯图标的重着色强度设为最大值
lv_obj_set_style_img_recolor(ui_ImageWatertemp, color_value, 0); // 设置水温图标的重着色颜色为白色
lv_obj_set_style_img_recolor_opa(ui_ImageWatertemp, 255, 0); // 把水温图标的重着色强度设为最大值
lv_obj_set_style_img_recolor(ui_ImageSfatyBelt, color_value, 0); // 设置安全带图标的重着色颜色为白色
lv_obj_set_style_img_recolor_opa(ui_ImageSfatyBelt, 255, 0); // 把安全带图标的重着色强度设为最大值
} // 白色更新分支结束
} // 主循环结束后立即进入下一轮
上述代码按本节视频最后一次烧写前的画面逐行核对。
COLOR_RED、COLOR_WHITE和color_value沿用第 222 节,本节新增time_count,把改色对象扩展为四个图标,并最终使用1、201两个时刻触发红、白更新。标题下保留了理解本节节拍所必需的既有lv_timer_handler()与HAL_Delay(5);没有加入当前配套工程后来出现的lvtick、指针、RTC 和 Label 更新代码,因为它们属于后续课程。
一、改图标颜色方法
本节从上一节“让一个灯光图标变红”继续扩展,最终让四个状态图标按约 1 秒的节奏在红色与白色之间切换。课堂经历了三个版本:
- 给四个图标设置红色,再给四个图标设置白色;
- 在两组代码之间加入
HAL_Delay(1000),并临时手动调用lv_timer_handler(); - 删除两个 1 秒阻塞延时,使用 5 ms 循环节拍、取余计数器和两个离散触发点完成切换。
前两个版本用于暴露和分析问题,第三个版本才是本节结束时保留的代码。第 224 节从修改仪表指针开始,因此指针角度、RTC 和日期时间显示均不属于本节。
1.1 改图标颜色的方法
四个图标对应的实际对象名为:
| 顺序 | 图标 | 对象名 |
|---|---|---|
| 1 | 灯光 | ui_ImageLight |
| 2 | 转向灯 | ui_ImageTurnlight |
| 3 | 水温 | ui_ImageWatertemp |
| 4 | 安全带 | ui_ImageSfatyBelt |
对象名可以从 SquareLine 控件名称、ui_Screen1.c 的对象创建代码或 ui.h 的 extern 声明中核对。复制改色代码时只替换第一个对象参数,颜色变量、重着色强度和样式选择器保持一致。ui_ImageSfatyBelt 是课程工程实际生成的拼写,虽然正确英文应为 SafetyBelt,但不能只在调用处改正,否则会出现未定义标识符。
每个图标都需要两次样式设置:
lv_obj_set_style_img_recolor()指定要混入的颜色;lv_obj_set_style_img_recolor_opa(..., 255, ...)把重着色强度设到最大。
四个对象共享同一个 color_value。当它由 lv_color_make(COLOR_RED) 生成时,四个图标都被设为红色;当它由 lv_color_make(COLOR_WHITE) 生成时,四个图标都被设为白色。这不是“一条代码同时控制四个对象”,而是依次对四个对象写入相同的样式值。
课堂使用 Ctrl+C/Ctrl+V 批量复制代码,但复制后必须逐项核对对象名。尤其要防止只修改了 recolor 调用中的对象,却漏改相邻 recolor_opa 的对象,这会导致一个图标拿到颜色、另一个图标拿到混合强度,显示结果难以判断。
Keil 的 Go To 一度无法跳转到 COLOR_RED/COLOR_WHITE。这是编辑器浏览信息或工程索引没有及时更新的现象;重新编译可以刷新相关信息,但“能否跳转”不是 C 代码是否有效的判据。编译器在预处理阶段能够展开宏才是构建是否成功的关键。
1.2 添加延时的方法
为了让红白切换肉眼可见,课堂最初在红色代码后和白色代码后各加入一次 HAL_Delay(1000):
设置红色 → 等待 1000 ms → 设置白色 → 等待 1000 ms
从普通顺序程序角度看,这似乎应该得到“一秒红、一秒白”。但 LVGL 的样式设置和真正向 LCD 输出像素不是同一时刻完成的:
- 样式接口修改对象属性,并把相关区域标记为需要重绘;
- 后续调用
lv_timer_handler()时,LVGL 才有机会运行显示刷新定时器、重新绘制无效区域并通过显示驱动提交像素; - 本节当时的
lv_timer_handler()位于主循环顶部,两段改色代码位于它后面; - 红色刚被设置,程序就阻塞 1 秒,随后又把同一批对象改成白色并再阻塞 1 秒;
- 下一次回到循环顶部调用
lv_timer_handler()时,对象的最终样式已经是白色,所以屏幕通常只显示白色。
因此问题不是“颜色切换得太快”,而是两次样式变化之间没有让 LVGL 执行一次刷新。把第二组颜色临时改成黄色后,屏幕最终稳定显示黄色,也验证了“只刷新到最后一次样式状态”的判断。
老师随后在设置红色、设置白色之后各手动调用一次 lv_timer_handler(),于是红白状态都能在相邻的 1 秒延时中显示出来。这个版本能演示原理,却仍不是合理的长期实现:两个 HAL_Delay(1000) 会让主循环每轮阻塞约 2 秒,触摸、动画、输入处理及其他业务都只能长时间等待。
HAL_Delay() 的参数单位是毫秒,所以 1000 表示约 1 秒。它是阻塞式等待,不等于“建立了一个后台定时器”;等待期间主循环不会执行下一条业务代码。中断仍可运行,因此本工程 TIM6 中断里的 lv_tick_inc(1) 可以继续累计 LVGL 时间,但没有再次进入 lv_timer_handler(),到期任务依然不会在主循环中被处理。
1.3 编译错误的处理方法
视频中出现的是烧写/下载阶段卡住或失败,不是 C 源码编译错误:构建已经完成,但新固件没有成功写入 MCU。开发板仍显示上一版本的红色图标,正说明 Flash 中运行的还是旧程序,不能据此判断新逻辑失败。
老师通过任务管理器结束卡死的 Keil,重新打开工程并再次下载,随后成功运行。这里需要纠正原笔记的两点:
- 重新打开已保存的工程通常不需要“手动重写代码”;先确认文件已经保存即可;
- 视频只提出“可能代码太大”的猜测,没有证据证明容量是本次下载卡死的原因。工程当时仍能成功链接且未超出芯片 Flash,下载异常还可能来自 ST-Link、USB/SWD 接触、目标板供电、复位状态或 IDE 调试会话。
排查时应把构建和下载分开:
| 阶段 | 成功标志 | 失败时优先检查 |
|---|---|---|
| 编译/链接 | Build Output 显示 0 Errors,并生成固件 | 语法、对象名、头文件、Flash/RAM 是否溢出 |
| 连接目标 | ST-Link 能识别 MCU | USB/SWD 接线、供电、驱动、复位/BOOT 状态 |
| 擦除与编程 | 出现 Erase Done、Programming Done、Verify OK | 下载算法、连接稳定性、芯片保护状态 |
| 运行新固件 | 复位后呈现新行为 | 是否确实下载了最新构建、程序是否进入预期路径 |
强制结束 IDE 是卡死时的恢复手段,不是所有烧写错误的通用修复。若问题反复出现,应先记录 Keil 下载窗口的具体错误信息,再针对连接或配置排查。
二、检测图形显示状态函数详解
2.1 检测图形显示状态函数的作用
原笔记中的 “time hander”“AverageL”“LV GL” 均指 LVGL 的 lv_timer_handler()。它不只是“检测图形有没有变化”,更准确的职责是运行已经到期的 LVGL 定时器任务,包括显示刷新、输入设备读取、动画和用户创建的 LVGL Timer 等。
LVGL 时间系统需要区分两个环节:
| 环节 | 本工程实现 | 作用 |
|---|---|---|
| 提供时间基准 | TIM6 周期中断中调用 lv_tick_inc(1) | 告诉 LVGL 又过去了 1 ms |
| 执行到期任务 | 主循环调用 lv_timer_handler() | 检查并运行已经到期的刷新、输入和动画任务 |
只增加 Tick 而不运行 Handler,相当于时钟在走,但待办任务无人执行;只频繁调用 Handler 而 Tick 不增长,LVGL 又无法判断定时任务是否到期。两者缺一不可。
LVGL 8.3 官方示例建议在主循环中周期调用 lv_timer_handler(),调用间隔约 5 ms 以保持响应性,但明确指出时间并不要求严格等于 5 ms。本节的 HAL_Delay(5) 是一种最简单的裸机轮询写法,不是 LCD 必须等待 5 ms 才能响应。
本工程 LV_DISP_DEF_REFR_PERIOD 为 30 ms,表示默认显示刷新 Timer 的周期是 30 ms。主循环每约 5 ms 进入一次 Handler,是为了及时运行各种到期任务;它不代表 LCD 必定每 5 ms 全屏刷新一次。
2.2 图形显示状态更新实例
为了验证为什么只看到第二种颜色,课堂暂时把白色分支改为 COLOR_YELLOW。烧写后四个图标稳定显示黄色,验证了如下执行顺序:
设红色 → 阻塞 → 设黄色 → 阻塞 → Handler 处理最终黄色状态
这一步是定位问题的实验,不是本节最终业务需求。实验完成后颜色恢复为 COLOR_WHITE。把一个复杂现象简化成“只改变最后一种颜色”,再观察结果是否随之变化,是嵌入式调试中常用的控制变量方法。
还要注意,样式接口会使对象区域失效,但何时真正绘制取决于显示刷新 Timer 是否到期。一次 Handler 调用并不保证任何情况下都立刻产生一次完整 LCD 刷新;本节实验能够看到目标颜色,是因为经过 1 秒阻塞后刷新任务早已到期。
2.3 图形显示状态更新频率与建议
“约 5 ms 调一次”应理解为 Handler 的服务频率建议,而不是精密定时要求:
- 间隔过长:触摸、动画和界面刷新会变迟钝;
- 间隔明显过短:主循环会更频繁地进入 Handler,增加无效轮询和 CPU 开销;
- 每轮
HAL_Delay(5):实际周期间隔还包含 Handler、样式操作和其他代码的执行时间,所以一定略大于 5 ms; - 要得到准确的业务周期:应依据真实毫秒 Tick 判断时间,而不能永远假定“循环一次恰好 5 ms”。
本节为了演示计数思路,仍按每轮约 5 ms 计算。当前阶段的红白闪烁对精度要求不高;后续功能增多后,主循环时间会变化,单纯按循环次数计时的误差也会随之增大。
2.4 图形显示状态修改与验证
课堂的临时验证版在每组改色代码后主动调用一次 lv_timer_handler():先把红色区域真正刷新到屏幕,再等待 1 秒;随后把白色区域刷新到屏幕,再等待 1 秒。烧写后确实出现了近似一秒红、一秒白。
原笔记中的“l color hander 工具”是识别错误,它不是颜色微调工具,而是函数 lv_timer_handler()。函数本身也不负责决定红色或白色;颜色由前面的样式代码决定,Handler 负责运行到期的刷新任务。
这一实验说明“现象出现了”不代表实现已经合格。阻塞式版本虽然肉眼效果正确,却会阻断主循环中的其他工作。评价嵌入式方案时既要看输出结果,也要看 CPU 是否还能及时处理触摸、通信、传感器和其他界面任务。
2.5 图形显示状态修改延时问题与解决
两个 HAL_Delay(1000) 的主要问题不是“延时数量多”本身,而是连续阻塞时间太长:
- LVGL Handler 两秒左右才能再次获得执行机会;
- 触摸输入最多可能等待很久才响应;
- 后续加入的 CAN、RTC、传感器和控制逻辑也会被主循环阻塞;
- 若通信接收依赖轮询,长阻塞还可能造成数据处理不及时。
本节最终删除两个 1 秒延时,只保留移植时原有的 HAL_Delay(5),再使用计数器判断何时切换颜色。这属于简易的“非长阻塞”轮询:主循环仍有 5 ms 短阻塞,但不会因颜色状态保持而停在某处一整秒。
必须说明,本节方案仍以循环次数估算时间,并不是真正高精度的软件定时器。更稳健的裸机写法通常比较 HAL_GetTick() 的时间差,或者使用 LVGL Timer;不过这些都不是本节最终展示的源码,所以这里只说明方向,不擅自替换课堂实现。
2.6 图形显示状态修改的优化方法
课堂先定义:
uint32_t time_count = 0
然后在 HAL_Delay(5) 后执行自增和取余:
time_count++ → time_count = time_count % 400
取余结果只能是 0~399,不会包含 400。原笔记“让 count 在 0 到 400 之间循环”不严谨。计数序列在接近回绕时是:
397 → 398 → 399 → 0 → 1 → 2 ...
因为 200 个理想节拍约为 200 × 5 ms = 1000 ms,课堂最初用 time_count < 200 显示红色,其余区间显示白色。从条件逻辑看它把两秒周期分成了两半,但实测每种颜色接近 4 秒,明显慢于估算。
原因是两个分支在各自的整个区间内每轮都重写四个对象的两个样式属性:
- 红色阶段约执行 200 次 × 8 个样式设置;
- 白色阶段又约执行 200 次 × 8 个样式设置;
- 即使目标颜色与上一轮相同,LVGL 的本地样式设置仍会刷新样式并使对象区域失效;
- Handler 随后需要重绘失效区域,显示驱动还可能通过 SPI 发送更新像素;
- 这些工作增加了每轮执行时间,所以“一轮等于 5 ms”的假设不再成立。
视频把这条路径概括成“样式接口直接调用 LCD 和 SPI”。更准确地说,样式设置先修改对象并标记重绘,真正绘制和 LCD Flush 主要由之后的 lv_timer_handler() 驱动;最终结果同样是无意义的重复改色造成大量 CPU、渲染和显示传输开销。
优化思路是把“在整个时间区间重复赋值”改为“只在进入新状态的那个瞬间赋值”:
| 计数条件 | 执行次数/两秒周期 | 操作 |
|---|---|---|
time_count == 1 | 1 次 | 四个图标统一设为红色 |
time_count == 201 | 1 次 | 四个图标统一设为白色 |
| 其他计数值 | 398 次 | 不重复设置颜色,只继续服务 LVGL |
这样每个两秒周期只发生两次颜色状态更新,界面节奏便接近预期。真实周期仍应写作:
状态保持时间 ≈ 200 ×(5 ms 阻塞时间 + 每轮其余代码执行时间)
而不是绝对等于 1000 ms。优化后每轮工作量小得多,所以误差明显缩小。
2.7 图形显示状态修改优化实例
优化的第一版使用 time_count == 0 切红色、time_count == 200 切白色,但代码顺序是先 time_count++、再取余、最后判断。上电时初值为 0,第一次判断前已经变成 1,因此不会立即命中红色条件;必须等到约 400 个节拍后计数器回绕到 0,才第一次设置红色。于是复位后素材的初始白色会保持约两秒,显得“第一段白色特别长”。
老师最终没有把 time_count 初值改成 1,也没有改变取值范围,而是把两个触发点整体后移一拍:
1 == time_count:第一次循环就设置红色;201 == time_count:再经过约 200 个节拍后设置白色;- 下一次从 399 回绕到 0,再到 1 时重新设置红色。
所以原笔记“把 count 范围从 0~400 改为 1~201”是错误概括;计数器始终循环于 0~399,变化的只是两个执行事件的阈值。
视频使用常量在左侧的写法 1 == time_count。它与 time_count == 1 判断结果相同;传统 C 代码中这样写还可以避免误把 == 写成 = 后形成合法赋值表达式,因为 1 = time_count 会直接编译报错。两个分支使用独立的 if 也没有冲突,因为一个计数值不可能同时等于 1 和 201。
最终烧写并复位后,四个图标能近似每秒在红、白之间切换。这里“近似”很重要:本节依赖循环节拍,不是精确定时测量;但它已经解决了只显示最后一种颜色、长时间阻塞和每轮重复刷新三个核心问题。
本节没有改变开发板电路。显示负载的完整关系是:
计数条件命中 → 修改四个 LVGL 对象的本地样式 → 对象区域失效 → lv_timer_handler() 运行刷新 Timer → CPU 重绘 RGB565 像素 → 显示驱动通过 LCD 接口发送脏区域 → 屏幕颜色改变
优化减少的是软件重绘和显示传输次数,不会改变 LCD 供电、背光或任何物理灯的颜色。