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大小选择最优方案?

这里我给大家一个实用的决策流程。你可以照着这个来选。

  1. 先算总账:看看芯片还剩多少RAM。减去系统、协议栈、任务栈等开销,剩下的才是给LVGL的。
  2. 如果能放下全屏双缓冲:直接上。这是最省心的方案,性能最好。
  3. 如果只能放下全屏单缓冲:也可以。但要注意刷新频率不要太高,避免撕裂明显。
  4. 如果全屏单缓冲都放不下:那就用部分缓冲。先试试部分双缓冲,内存不够再降成部分单缓冲。
  5. 如果部分单缓冲都紧张:那就把缓冲区行数降到最低。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的字体和图片优化。到时候我会分享一些压缩技巧,让你在有限的内存里塞下更多资源。我们下次见。