刷屏策略优化:局部刷新、脏矩形算法、水平/垂直滚动
各位同学,今天我们来聊聊刷屏策略。说实话,这是我在手环项目里踩坑最多的地方。一开始我天真地以为,只要把LCD驱动写对就行了,结果一上电,画面卡得像幻灯片。嗯,那时候我才意识到——刷屏策略,才是决定用户体验的关键。
为什么需要刷屏策略?
你想想看,手环的屏幕虽然小,但刷新频率要求可不低。60fps的流畅度,意味着每16.6ms就要刷一帧。如果每次都是全屏刷新,那CPU和内存带宽的压力非常大。我见过不少新手,上来就搞全屏刷新,结果帧率掉到20fps,动画卡得不行。
说白了,全屏刷新是「笨办法」。你明明只改了一个数字,却要把整个屏幕的数据都重新传一遍。这就像你家里只换了一盏灯,却要把整栋楼的电线都拆了重装——浪费资源。
局部刷新:只改该改的地方
局部刷新的思路很简单:只更新变化的那部分像素。我在做手环表盘时,秒针每秒钟只动一小块区域,完全没必要刷新整个屏幕。
具体怎么做呢?你需要知道LCD的窗口设置命令。大多数LCD控制器都支持设置一个矩形区域,然后只往这个区域写数据。比如ST7789,通过CASET和RASET命令就能设定窗口。
// 设置局部刷新窗口
void lcd_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
// 列地址范围
lcd_write_cmd(0x2A); // CASET
lcd_write_data(x0 >> 8);
lcd_write_data(x0 & 0xFF);
lcd_write_data(x1 >> 8);
lcd_write_data(x1 & 0xFF);
// 行地址范围
lcd_write_cmd(0x2B); // RASET
lcd_write_data(y0 >> 8);
lcd_write_data(y0 & 0xFF);
lcd_write_data(y1 >> 8);
lcd_write_data(y1 & 0xFF);
// 开始写入像素
lcd_write_cmd(0x2C); // RAMWR
}
我在项目中遇到过一个问题:局部刷新时,如果窗口边界没对齐,会出现残影。后来发现是LCD的GRAM在写入时,如果窗口外的数据没被覆盖,就会保留旧内容。所以每次局部刷新前,最好先清一下目标区域。
脏矩形算法:智能合并刷新区域
脏矩形算法,说白了就是「记录哪些地方变了,然后合并成几个矩形去刷新」。我最早做手环通知显示时,每次来消息都要刷新整个通知区域,后来发现其实只有文字部分在变。
算法核心思路是这样的:
- 维护一个脏矩形列表,记录所有需要刷新的区域
- 每次有新变化时,把变化区域加入列表
- 定期合并相邻或重叠的脏矩形
- 按合并后的矩形进行局部刷新
合并策略很关键。我习惯用「贪心合并法」:遍历脏矩形列表,如果两个矩形在水平或垂直方向上有重叠,或者间距小于某个阈值(比如4像素),就把它们合并成一个更大的矩形。
// 脏矩形合并示例
typedef struct {
uint16_t x, y, w, h;
} Rect;
Rect merge_rects(Rect a, Rect b) {
Rect merged;
merged.x = min(a.x, b.x);
merged.y = min(a.y, b.y);
merged.w = max(a.x + a.w, b.x + b.w) - merged.x;
merged.h = max(a.y + a.h, b.y + b.h) - merged.y;
return merged;
}
// 判断两个矩形是否可合并
bool should_merge(Rect a, Rect b, uint16_t threshold) {
// 水平或垂直方向有重叠,或间距小于阈值
if (abs(a.x - b.x) <= threshold || abs(a.y - b.y) <= threshold) {
return true;
}
return false;
}
水平/垂直滚动:硬件加速的利器
这个功能,说实话,很多开发者都忽略了。大多数LCD控制器都内置了硬件滚动功能,你只需要设置滚动起始地址和滚动方向,硬件会自动完成像素移动,完全不需要CPU干预。
我在做手环的「消息列表滚动」时,就用到了垂直滚动。用户滑动屏幕时,只需要更新滚动起始地址,画面就丝滑地滚动了。
// 垂直滚动设置(ST7789示例)
void lcd_vertical_scroll(uint16_t top_fixed, uint16_t scroll_area, uint16_t bottom_fixed) {
lcd_write_cmd(0x33); // VSCRDEF
lcd_write_data(top_fixed >> 8);
lcd_write_data(top_fixed & 0xFF);
lcd_write_data(scroll_area >> 8);
lcd_write_data(scroll_area & 0xFF);
lcd_write_data(bottom_fixed >> 8);
lcd_write_data(bottom_fixed & 0xFF);
}
// 设置滚动起始行
void lcd_scroll_start(uint16_t line) {
lcd_write_cmd(0x37); // VSCRSADD
lcd_write_data(line >> 8);
lcd_write_data(line & 0xFF);
}
水平滚动也类似,不过用得少一些。我只有在做「跑马灯」效果时用过水平滚动,比如手环上的通知滚动条。
三种策略如何选择?
我个人的经验是这样的:
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 秒针/数字变化 | 局部刷新 | 变化区域小,直接刷新最快 |
| 复杂UI动画 | 脏矩形算法 | 多个元素同时变化,合并后减少刷新次数 |
| 列表滚动/跑马灯 | 硬件滚动 | CPU零负担,帧率最高 |
| 全屏切换 | 全屏刷新 | 变化太大,局部刷新反而更慢 |
你可能会问:「能不能三种混着用?」当然可以。我在手环项目里就是混合使用的:表盘用局部刷新,通知列表用脏矩形算法,消息滚动用硬件滚动。关键是做好状态管理,别让它们冲突。
好了,刷屏策略就讲到这里。下一章我们会聊聊帧率优化,到时候会结合这些策略,教你如何把手环的帧率从30fps提升到60fps。嗯,那才是真正考验功底的地方。