3、显示缓冲区优化策略:详解单缓冲、双缓冲、全屏缓冲与部分缓冲的优劣,教你如何根据RAM大小选择最优方案。
大家好,我是你们的嵌入式UI讲师。
这一章,我们来聊聊显示缓冲区。说白了,就是那块用来存像素数据的内存。很多新手朋友一上来就问:“LVGL是不是必须用双缓冲?” 我的回答是:不一定。关键看你手头的芯片有多少RAM。
我在做一个小型温控器项目时,芯片只有20KB的RAM。当时我硬着头皮上了全屏双缓冲,结果代码直接编译不过去。后来换成部分缓冲,才把界面跑起来。所以,选对策略,比什么都重要。
3.1 单缓冲:最省内存,但有撕裂风险
单缓冲,就是只开一块缓冲区。CPU往里面写数据,LCD控制器同时从里面读数据,往屏幕上刷。
听起来很简单对吧?但这里有个坑。你想想看,如果CPU正在画一个复杂的按钮,画到一半,LCD控制器突然把这块半成品数据刷到屏幕上了。结果就是——屏幕出现撕裂,上半部分是新按钮,下半部分还是旧画面。
我遇到过这种情况。当时客户反馈说屏幕偶尔闪一下,排查了半天,最后发现是单缓冲的时序问题。
适用场景:
- RAM极度紧张(比如<8KB)
- 画面更新不频繁(比如静态仪表盘)
- 对撕裂不敏感(比如纯文字界面)
注意:单缓冲下,LVGL的刷新回调 flush_cb 里,一定要等DMA传输完成再返回。否则撕裂会更严重。
3.2 双缓冲:流畅无撕裂,但吃内存大户
双缓冲,就是开两块缓冲区。一块给CPU画,一块给LCD刷。画完一块,交换一下指针。这样LCD永远刷的是完整的一帧。
我曾经在一个7寸屏的项目上用了双缓冲。效果确实好,滑动列表丝般顺滑。但代价是——光缓冲区就占了800x480x2 = 768KB。加上其他内存,直接干到了1MB以上。
所以,双缓冲不是你想用,想用就能用。你得先看看芯片的RAM够不够。
我的建议:如果你的RAM大于显示分辨率的4倍(比如800x480x4=1.5MB),可以考虑双缓冲。否则,还是看看下面的方案吧。
3.3 全屏缓冲 vs 部分缓冲
这两个概念,很多人容易搞混。我简单解释一下。
全屏缓冲:缓冲区大小等于整个屏幕的像素数。比如320x240的屏,全屏缓冲就是320x240x2=150KB(16位色)。
部分缓冲:缓冲区只占屏幕的一部分。比如只开10行像素的缓冲区,每次刷新10行,分多次刷完整个屏幕。
你可能会问:“部分缓冲会不会很慢?” 嗯,确实会慢一点。因为每次刷完一小块,都要重新触发一次刷新回调。但好处是——内存占用可以降到极低。
| 策略 | 内存占用(320x240, 16位色) | 刷新速度 | 撕裂风险 |
|---|---|---|---|
| 全屏单缓冲 | 150KB | 快 | 有 |
| 全屏双缓冲 | 300KB | 最快 | 无 |
| 部分单缓冲(10行) | 6.4KB | 较慢 | 有 |
| 部分双缓冲(10行) | 12.8KB | 中等 | 无 |
从表格可以看出,部分双缓冲是一个很好的折中方案。内存占用低,又没有撕裂。我在一个STM32F103C8T6(20KB RAM)的项目上,就是用部分双缓冲(8行)跑起来的。
3.4 如何根据RAM大小选择最优方案?
这里我给大家一个实用的决策流程。你可以照着这个来选。
- 先算总账:看看芯片还剩多少RAM。减去系统、协议栈、任务栈等开销,剩下的才是给LVGL的。
- 如果能放下全屏双缓冲:直接上。这是最省心的方案,性能最好。
- 如果只能放下全屏单缓冲:也可以。但要注意刷新频率不要太高,避免撕裂明显。
- 如果全屏单缓冲都放不下:那就用部分缓冲。先试试部分双缓冲,内存不够再降成部分单缓冲。
- 如果部分单缓冲都紧张:那就把缓冲区行数降到最低。LVGL支持最小1行缓冲区,但性能会非常差。我建议至少保留4行。
经验公式:
- RAM > 分辨率x4 → 全屏双缓冲
- RAM > 分辨率x2 → 全屏单缓冲
- RAM > 分辨率x0.5 → 部分双缓冲(建议10-20行)
- RAM < 分辨率x0.5 → 部分单缓冲(建议4-8行)
当然,这只是经验值。具体还要看你界面的复杂度。如果全是静态文本,缓冲区小点也没事。如果有动画或滑动列表,缓冲区就得大一些。
3.5 代码示例:配置部分缓冲
最后,我给大家一个实际配置的代码片段。这是LVGL初始化时设置缓冲区的部分。
/* 定义缓冲区大小:10行像素 */
#define MY_DISP_HOR_RES 320
#define MY_DISP_VER_RES 240
#define MY_BUF_LINES 10
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf1[MY_DISP_HOR_RES * MY_BUF_LINES];
void my_lvgl_init(void)
{
/* 初始化显示缓冲区 */
lv_disp_draw_buf_init(&draw_buf, buf1, NULL, MY_DISP_HOR_RES * MY_BUF_LINES);
/* 初始化显示驱动 */
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.draw_buf = &draw_buf;
disp_drv.flush_cb = my_flush_callback;
disp_drv.hor_res = MY_DISP_HOR_RES;
disp_drv.ver_res = MY_DISP_VER_RES;
/* 注册显示驱动 */
lv_disp_drv_register(&disp_drv);
}
注意看,这里我只传了一个 buf1,第二个缓冲区传了 NULL。这就是单缓冲模式。如果你想用双缓冲,就再定义一个 buf2,两个都传进去。
避坑指南:我曾经犯过一个错误——把缓冲区定义在任务栈里。结果栈溢出,程序跑飞。记住,缓冲区一定要定义成全局变量或者静态变量,或者用动态分配放在堆里。
好了,这一章的内容就到这里。总结一下:
- 单缓冲省内存,但有撕裂风险
- 双缓冲流畅,但吃内存
- 全屏缓冲性能好,部分缓冲更灵活
- 根据RAM大小,按经验公式选择最优方案
下一章,我们会聊聊LVGL的字体和图片优化。到时候我会分享一些压缩技巧,让你在有限的内存里塞下更多资源。我们下次见。