LVGL基础框架与显示驱动移植
LVGL的移植,说白了就是让这个图形库能在你的硬件上跑起来。我刚开始接触LVGL时,以为移植就是改几个宏定义,结果折腾了两天才把画面点亮。今天咱们就把这块彻底讲透。
LVGL架构概览
先看看LVGL的整体结构。它分三层:
- 应用层:你写的UI代码,按钮、标签、动画都在这里
- 核心层:对象管理、事件分发、绘图引擎
- 硬件抽象层:显示驱动、输入设备驱动、内存管理
移植工作主要集中在下两层。核心层基本不用动,除非你要改内存分配策略。
关键点:LVGL的硬件抽象层设计得很干净。你只需要实现几个回调函数,剩下的框架帮你搞定。
显示缓冲区配置
显示缓冲区是LVGL性能的关键。配置不对,画面要么撕裂,要么卡顿。
LVGL支持三种缓冲区模式:
| 模式 | 缓冲区大小 | 适用场景 | 性能 |
|---|---|---|---|
| 单缓冲区 | 1 × 屏幕像素 | 低端MCU,内存紧张 | 低,可能撕裂 |
| 部分缓冲区 | 1/10 ~ 1/2 屏幕 | 大多数嵌入式设备 | 中等 |
| 双缓冲区 | 2 × 屏幕像素 | 高性能设备,需要防撕裂 | 高 |
我的建议:如果内存够用,优先用双缓冲区。我在一个项目里用了部分缓冲区,结果滑动列表时总有残影,换成双缓冲区后问题立刻消失。
配置代码长这样:
/* 单缓冲区配置 */
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[LV_HOR_RES_MAX * LV_VER_RES_MAX];
lv_disp_draw_buf_init(&draw_buf, buf, NULL, LV_HOR_RES_MAX * LV_VER_RES_MAX);
/* 双缓冲区配置 */
static lv_color_t buf1[LV_HOR_RES_MAX * LV_VER_RES_MAX];
static lv_color_t buf2[LV_HOR_RES_MAX * LV_VER_RES_MAX];
lv_disp_draw_buf_init(&draw_buf, buf1, buf2, LV_HOR_RES_MAX * LV_VER_RES_MAX);
注意第二个参数是buf1,第三个是buf2。双缓冲区时两个都要传,单缓冲区时第二个传NULL。
Framebuffer 与 Double Buffer
这两个概念容易搞混。我简单解释一下:
- Framebuffer:硬件层面的概念,是显存中一块固定区域,LCD控制器直接从这里读取数据刷新屏幕
- Double Buffer:软件层面的技术,用两个缓冲区交替绘制和显示
在实际项目中,它们经常配合使用。比如:
- LVGL往buf1里画图
- 画完后,把buf1的内容拷贝到Framebuffer
- 同时LVGL开始往buf2里画下一帧
- 如此循环
注意:如果Framebuffer和LVGL的缓冲区是同一块内存,要小心同步问题。我曾经在STM32上遇到过,LVGL正在写缓冲区时LCD控制器突然读取,结果画面出现撕裂。解决办法是用双缓冲区,或者等垂直消隐期再切换。
Flush回调函数实现
flush回调是移植的核心。LVGL画完一帧后,会调用这个函数把数据送到显示器。
标准实现模板:
static void my_flush_cb(lv_disp_drv_t * disp_drv,
const lv_area_t * area,
lv_color_t * color_p)
{
/* 获取刷新区域 */
int32_t x1 = area->x1;
int32_t y1 = area->y1;
int32_t x2 = area->x2;
int32_t y2 = area->y2;
/* 计算像素数量 */
int32_t w = x2 - x1 + 1;
int32_t h = y2 - y1 + 1;
/* 调用底层驱动,把color_p数据写到LCD */
lcd_draw_bitmap(x1, y1, w, h, (uint16_t *)color_p);
/* 通知LVGL刷新完成 */
lv_disp_flush_ready(disp_drv);
}
这里有几个坑:
- 一定要调用
lv_disp_flush_ready,否则LVGL会一直等 - 如果用了DMA传输,要在DMA完成中断里调用
lv_disp_flush_ready - 颜色格式要匹配,LVGL默认是RGB565,如果你的LCD是RGB888需要转换
性能优化技巧:如果LCD支持窗口模式,尽量只刷新脏区域。我见过有人每次都刷新全屏,结果60fps的屏只能跑到20fps。改成局部刷新后直接拉满。
带DMA的flush实现:
static void my_flush_cb(lv_disp_drv_t * disp_drv,
const lv_area_t * area,
lv_color_t * color_p)
{
/* 设置LCD窗口 */
lcd_set_window(area->x1, area->y1, area->x2, area->y2);
/* 启动DMA传输 */
dma_start((uint32_t)color_p, LCD_DATA_ADDR,
(area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1) * 2);
/* DMA传输完成后在中断中调用 lv_disp_flush_ready */
}
void DMA_IRQHandler(void)
{
if(dma_is_transfer_complete()) {
lv_disp_flush_ready(&disp_drv);
}
}
嗯,这里要注意:DMA中断里调用lv_disp_flush_ready时,要确保中断优先级别太高,否则可能影响LVGL的定时器。
最后,注册显示驱动的代码:
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_cb;
disp_drv.hor_res = 320;
disp_drv.ver_res = 240;
lv_disp_drv_register(&disp_drv);
到这里,显示驱动就移植完成了。你想想看,其实就三步:配缓冲区、写flush回调、注册驱动。但每一步都有细节,踩过坑才能记得牢。