3、显示驱动移植实战:framebuffer与TFT接口的适配
好,咱们直接进入正题。显示驱动移植,说白了就是让LVGL能跟你的屏幕「说上话」。很多新手卡在这一步,其实没那么玄乎。我最早做STM32F103+ILI9341的时候,光是点亮屏幕就折腾了两天——后来发现是SPI时序里一个极小的延迟没加够。
今天咱们把三种主流接口(framebuffer、SPI/8080、RGB)的适配思路讲透,再聊聊lv_port_disp.c里那些坑,最后给出双缓冲和局部刷新的实战配置。
3.1 三种显示接口的适配思路
先搞清楚你的屏幕是什么接口。这决定了你的底层怎么写。
3.1.1 Framebuffer接口
这种最常见于Linux或带LCD控制器的MCU(比如STM32F429的LTDC)。说白了,你有一块内存区域,往里面写像素数据,屏幕自动刷新。
适配要点:
- 你只需要提供
flush_cb函数,把LVGL的绘制缓冲区拷贝到framebuffer地址 - 注意字节对齐。我遇到过RGB565格式下,framebuffer起始地址没4字节对齐,结果颜色全错位
- 如果硬件支持DMA2D,记得用——性能能翻好几倍
// framebuffer刷新回调示例
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
uint32_t w = lv_area_get_width(area);
uint32_t h = lv_area_get_height(area);
lv_color_t * fb = (lv_color_t *)LCD_FRAME_BUFFER;
// 直接拷贝到framebuffer对应位置
for(uint32_t y = area->y1; y <= area->y2; y++) {
memcpy(&fb[y * LV_HOR_RES + area->x1],
color_p,
w * sizeof(lv_color_t));
color_p += w;
}
lv_disp_flush_ready(disp_drv);
}
3.1.2 SPI/8080并行接口
这是小尺寸屏幕(3.5寸以下)的标配。SPI接口线少但慢,8080并行接口快但占引脚多。
我个人习惯:
- SPI接口:优先用硬件SPI+DMA,别用GPIO模拟。我曾经在GD32F103上试过模拟SPI刷320x240的屏,帧率只有8fps,换成硬件SPI+DMA直接飙到35fps
- 8080接口:注意读写时序。很多国产屏的时序参数跟数据手册有出入,我建议你实际用示波器抓一下
- 关键函数:
LCD_WriteData()和LCD_SetWindow()必须优化到极致
3.1.3 RGB接口
RGB接口常见于4.3寸以上的大屏,需要MCU有LTDC或RGB控制器。数据线多(16位或18位),但刷新速度最快。
适配要点:
- 时钟频率要算准。比如480x272的屏,60Hz刷新率,像素时钟大约9MHz
- HBP、HFP、VBP、VFP这些参数别乱填。我见过有人把HBP设成0,结果屏幕左边缺了一列像素
- 如果MCU没有硬件LTDC,别硬上RGB接口——你会被时序搞疯的
3.2 lv_port_disp.c的配置详解
这个文件是LVGL显示驱动的「总开关」。配置不对,后面全白搭。
3.2.1 核心配置项
| 配置项 | 说明 | 我的建议值 |
|---|---|---|
LV_HOR_RES_MAX |
水平分辨率 | 屏幕实际宽度 |
LV_VER_RES_MAX |
垂直分辨率 | 屏幕实际高度 |
LV_COLOR_DEPTH |
颜色深度 | 16(RGB565)最省内存 |
LV_DPI |
每英寸像素数 | 3.5寸屏设96,7寸屏设120 |
LV_DISP_DEF_REFR_PERIOD |
刷新周期(ms) | 30(30fps够用) |
3.2.2 缓冲区配置
这是最关键的。缓冲区大小直接决定内存占用和刷新效率。
// 单缓冲(最省内存,但可能有撕裂)
static lv_color_t buf_1[LV_HOR_RES_MAX * 40]; // 40行缓冲区
// 双缓冲(推荐,无撕裂)
static lv_color_t buf_1[LV_HOR_RES_MAX * 40];
static lv_color_t buf_2[LV_HOR_RES_MAX * 40];
// 全屏双缓冲(性能最好,但内存大)
static lv_color_t buf_1[LV_HOR_RES_MAX * LV_VER_RES_MAX];
static lv_color_t buf_2[LV_HOR_RES_MAX * LV_VER_RES_MAX];
3.3 双缓冲与局部刷新策略
为什么需要双缓冲?说白了就是防止屏幕刷新到一半时,LVGL又在往缓冲区写数据——结果就是画面撕裂,看着特别难受。
3.3.1 双缓冲的实现
在lv_port_disp.c里这样配:
// 初始化显示驱动
static void disp_init(void)
{
// 你的屏幕初始化代码
LCD_Init();
}
// 刷新回调
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
// 设置窗口
LCD_SetWindow(area->x1, area->y1, area->x2, area->y2);
// 发送像素数据(用DMA加速)
LCD_WriteDataMultiple((uint16_t *)color_p,
lv_area_get_width(area) * lv_area_get_height(area));
// 通知LVGL刷新完成
lv_disp_flush_ready(disp_drv);
}
// 在disp_drv初始化中
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.flush_cb = disp_flush;
disp_drv.buffer_1 = buf_1;
disp_drv.buffer_2 = buf_2; // 双缓冲的关键
disp_drv.full_refresh = 0; // 0表示局部刷新,1表示全屏刷新
lv_disp_drv_register(&disp_drv);
3.3.2 局部刷新策略
局部刷新是低内存MCU的救命稻草。它只刷新变化的部分,而不是整个屏幕。
为什么有效?你想想看,一个UI界面通常只有按钮、滑块在动,背景基本不变。全屏刷新纯属浪费。
disp_drv.full_refresh = 0; // 启用局部刷新disp_drv.rounder_cb = NULL; // 不需要对齐,让LVGL自己决定刷新区域
我曾经在一个项目里,用STM32F103C8T6(只有20KB RAM)驱动320x240的SPI屏。全屏刷新时帧率只有12fps,而且内存不够用。改成局部刷新后,帧率稳定在28fps,内存占用从18KB降到6KB。
3.3.3 避坑指南
- 别用全屏双缓冲:对于低内存MCU,全屏双缓冲就是自杀。320x240的RGB565全屏缓冲区要 320×240×2 = 153.6KB,两个就是307.2KB——大部分MCU都没这么多RAM
- DMA传输完成后记得回调:我遇到过DMA还在传输,LVGL就开始写下一个缓冲区了。解决办法是在DMA传输完成中断里调用
lv_disp_flush_ready() - 局部刷新时注意脏区域合并:如果多个控件同时变化,LVGL会把它们的脏区域合并成一个矩形。这个矩形可能比实际变化区域大,但比全屏小得多
3.4 实战总结
显示驱动移植,说白了就是三件事:
- 搞清楚接口类型:framebuffer、SPI/8080、RGB,适配方式完全不同
- 配好lv_port_disp.c:分辨率、颜色深度、缓冲区大小,一个都不能错
- 选对刷新策略:低内存MCU必须用局部刷新+双缓冲(行缓冲),别硬上全屏双缓冲
嗯,这里要注意:实际调试时,先用单缓冲把屏幕点亮,再切双缓冲。这样出了问题好定位。我每次移植新屏幕都是这么干的——先点亮,再优化。
下一章咱们讲输入设备移植,触摸屏和按键的适配。到时候我会分享一个我踩过的坑:电容触摸屏的I2C地址跟数据手册不一样,折腾了我一整天。