Skip to content

ESP32-S3-RLCD LVGL v8 示例拆解

08_LVGL_V8_Test 演示如何把 LVGL v8 移植到 ESP32-S3-RLCD-4.2 的反射式黑白屏上,并通过一个任务每 1.5 秒切换两张图片。

LVGL 是嵌入式 GUI 库。它负责 UI 对象、布局、事件和绘制,但它不知道具体屏幕怎么接线。因此移植 LVGL 时最关键的是显示端口:

LVGL 计算需要刷新的区域和像素
-> 调用 flush callback
-> 项目代码把像素写到屏幕驱动
-> 写完后调用 lv_disp_flush_ready()

本板是反射式黑白屏,而 LVGL 通常以 RGB565 等彩色格式组织像素。demo 的 flush callback 会把 LVGL 彩色像素按阈值转换成黑或白,再写入 RLCD 缓冲并刷新。

源码阅读入口:

02_ESP-IDF/08_LVGL_V8_Test/main/main.cpp
02_ESP-IDF/08_LVGL_V8_Test/components/app_bsp/lvgl_bsp.cpp
02_ESP-IDF/08_LVGL_V8_Test/components/port_bsp/display_bsp.cpp
02_ESP-IDF/08_LVGL_V8_Test/components/user_app/user_app.cpp
02_ESP-IDF/08_LVGL_V8_Test/components/ui_bsp/generated/

关键硬件配置:

项目配置
屏幕对象DisplayPort RlcdPort(12, 11, 5, 40, 41, 400, 300)
MOSIGPIO12
SCKGPIO11
DCGPIO5
CSGPIO40
RSTGPIO41
分辨率400 x 300
SPI hostSPI3_HOST
SPI clock10 MHz
LVGL 版本^8.4.0
LVGL buffer全屏双缓冲,PSRAM
LVGL taskLVGL,stack 8 * 1024,priority 5,core 0
app_main()
-> UserApp_AppInit()
-> RlcdPort.RLCD_Init()
-> Lvgl_PortInit(400, 300, Lvgl_FlushCallback)
-> lv_init()
-> 分配 draw buffer
-> lv_disp_draw_buf_init()
-> lv_disp_drv_init()
-> 注册 flush_cb
-> lv_disp_drv_register()
-> 启动 tick timer
-> 创建 LVGL task
-> Lvgl_lock(-1)
-> UserApp_UiInit()
-> setup_ui()
-> Lvgl_unlock()
-> UserApp_TaskInit()
-> Lvgl_LoopTask()

刷新链路:

LVGL 需要刷新
-> Lvgl_FlushCallback()
-> 遍历 area 像素
-> RGB565 阈值判断黑/白
-> RlcdPort.RLCD_SetPixel()
-> RlcdPort.RLCD_Display()
-> lv_disp_flush_ready()
函数/方法所在文件作用初学者需要理解的点
app_main()main/main.cpp串起 RLCD、LVGL、UI、任务。显示初始化顺序很重要。
RlcdPort.RLCD_Init()components/port_bsp/display_bsp.cpp初始化反射式屏幕。这是硬件屏幕驱动入口。
Lvgl_PortInit()components/app_bsp/lvgl_bsp.cpp初始化 LVGL v8 显示端口。LVGL 移植核心在这里。
lv_init()LVGL API初始化 LVGL 内核。所有 LVGL API 前先调用。
lv_disp_draw_buf_init()LVGL v8 API初始化显示绘制缓冲。v8 使用 draw buffer + disp drv。
lv_disp_drv_init()LVGL v8 API初始化显示驱动结构体。flush callback 注册到这里。
Lvgl_FlushCallback()main/main.cpp把 LVGL 像素刷新到 RLCD。彩色像素转黑白是重点。
lv_disp_flush_ready()LVGL v8 API告诉 LVGL 本次刷新完成。不调用会导致 LVGL 等待。
setup_ui()components/ui_bsp/generated/创建 SquareLine/生成 UI。generated UI 不建议手改太多。
Lvgl_LoopTask()components/user_app/user_app.cpp定时切换两张图片。这是 demo 业务,不是 LVGL 移植本体。

LVGL v8 移植的核心是三件事:

初始化 LVGL
-> 提供 draw buffer
-> 提供 flush callback

Lvgl_PortInit() 会分配显示缓冲并注册驱动。由于屏幕为 400 x 300,全屏 buffer 体积不小,所以 demo 使用 PSRAM。

Lvgl_FlushCallback() 是硬件和 LVGL 的桥。LVGL 传入区域和像素,callback 负责把像素写到屏幕。因为 RLCD 是黑白显示,demo 使用阈值:

uint8_t color = (*buffer < 0x7fff) ? ColorBlack : ColorWhite;

这不是彩屏刷新,而是“把 RGB565 压成黑/白”。写完区域后,调用:

RlcdPort.RLCD_Display();
lv_disp_flush_ready(drv);

UserApp_UiInit() 调用生成 UI:

setup_ui(&init_ui);

然后 Lvgl_LoopTask() 只做一个很简单的动作:每 1.5 秒切换两张图片的隐藏状态。

烧录运行后,屏幕显示一张图片,并每 1.5 秒在两张图片之间切换。若屏幕不刷新,优先排查 flush callback 是否被调用、SPI/RLCD 初始化是否成功、lv_disp_flush_ready() 是否执行。

现象可能原因排查方式
屏幕空白RLCD 未初始化或 flush 未执行。确认 RlcdPort.RLCD_Init()Lvgl_PortInit() 顺序。
LVGL 卡住flush 后没有调用 lv_disp_flush_ready()检查 callback 结尾。
图片黑白效果不理想阈值转换过于简单。调整 0x7fff 阈值或提前处理图片。
改 UI 后无变化修改了 generated 以外的文件或未重新生成。找到 setup_ui() 创建的对象。
刷新慢RLCD 整屏刷新代价较高。减少高频动画和大面积刷新。

迁移到真实 UI 时,建议把显示系统分成三层:

DisplayPort
-> 负责屏幕硬件初始化和像素刷新
LvglPort
-> 负责 LVGL 初始化、buffer、tick、flush、锁
AppUI
-> 负责页面、状态、图标和交互

不要让业务逻辑直接操作 RLCD_SetPixel(),也不要让显示驱动理解业务状态。这样 UI 迭代和底层屏幕适配才不会互相缠在一起。