3. 开发环境搭建
显示驱动移植,说白了就是让LVGL能真正把像素点画到屏幕上。这一步卡住了很多人——明明LVGL跑起来了,屏幕却一片漆黑,或者画面撕裂、闪烁。我刚开始做移植时也踩过这些坑,后来才明白,核心就三件事:搞懂两个回调函数、写对底层硬件接口、用好双缓冲。
这一章我们就把这三件事拆开揉碎,讲清楚。
3.1 显示驱动接口分析:flush_cb 与 rounder_cb
LVGL的显示驱动核心是一个结构体:lv_disp_drv_t。你不需要填满所有成员,但有两个回调函数是必须实现的——flush_cb 和 rounder_cb。
3.1.1 flush_cb:真正的“画图”函数
flush_cb 是LVGL把像素数据推送到显示器的出口。每次LVGL渲染完一个局部区域,就会调用这个函数,把一块矩形区域的像素数据交给你。
它的原型长这样:
static void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);
三个参数的含义:
- disp_drv:显示驱动结构体指针,可以用来获取用户自定义数据
- area:需要刷新的矩形区域,包含 x1/y1/x2/y2 四个坐标
- color_p:像素数据的缓冲区指针,数据格式由
lv_color_t决定
你在回调里要做的,就是把 color_p 里的数据,按 area 指定的位置,写入到显示器的帧缓冲区中。写完后,必须调用 lv_disp_flush_ready(disp_drv) 通知LVGL刷新完成。
lv_disp_flush_ready,LVGL会认为显示器还在忙,不会再发送新的刷新请求。整个界面就会卡死。我曾经见过有人调试了一整天,最后发现是忘了这行代码。
一个典型的实现框架:
static void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
int32_t x, y;
uint32_t width = lv_area_get_width(area);
uint32_t height = lv_area_get_height(area);
lv_color_t * buf = color_p;
for(y = area->y1; y <= area->y2; y++) {
for(x = area->x1; x <= area->x2; x++) {
// 将 buf 中的像素写入到屏幕坐标 (x, y)
put_pixel(x, y, *buf);
buf++;
}
}
lv_disp_flush_ready(disp_drv); // 通知LVGL刷新完成
}
3.1.2 rounder_cb:对齐刷新区域
rounder_cb 是个可选回调,但建议实现。它的作用是把LVGL请求的刷新区域,对齐到硬件更友好的边界上。
为什么需要对齐?很多显示控制器(比如LTDC)要求帧缓冲区的地址和宽度是4字节或8字节对齐的。如果LVGL请求的区域不对齐,硬件可能会报错,或者性能下降。
实现起来很简单:
static void my_rounder_cb(lv_disp_drv_t * disp_drv, lv_area_t * area)
{
// 将区域对齐到 4 像素边界
area->x1 = (area->x1 / 4) * 4;
area->x2 = ((area->x2 + 3) / 4) * 4 - 1;
area->y1 = (area->y1 / 4) * 4;
area->y2 = ((area->y2 + 3) / 4) * 4 - 1;
}
3.2 基于STM32的LTDC/FMC驱动实现
STM32系列中,带TFT-LCD控制器的型号(如STM32F429、STM32F746、STM32H743)通常使用LTDC外设。而如果用的是不带LTDC的型号(如STM32F103),则可以通过FMC/FSMC接口驱动并口屏。
3.2.1 LTDC驱动实现
LTDC(LCD-TFT Display Controller)是STM32内部集成的显示控制器,可以直接驱动RGB接口的屏幕。它的核心配置包括:
- 时钟配置:LTDC时钟来自PLL,需要根据屏幕分辨率计算像素时钟
- 时序配置:HBP、HFP、VBP、VFP、HSYNC、VSYNC 等参数
- 层配置:LTDC支持2层叠加,每层可以独立配置颜色格式、透明度、窗口位置
- 帧缓冲区地址:告诉LTDC从哪里读取像素数据
一个简化的LTDC初始化流程:
void ltdc_init(void)
{
// 1. 配置LTDC时钟
RCC->PLLSAICFGR = ...; // 根据屏幕参数计算
RCC->DCKCFGR |= RCC_DCKCFGR_PLLSAIDIVR_2;
// 2. 配置时序
LTDC->SSCR = (HSYNC << 16) | VSYNC;
LTDC->BPCR = (HBP << 16) | VBP;
LTDC->AWCR = (ACTIVE_W << 16) | ACTIVE_H;
LTDC->TWCR = (TOTAL_W << 16) | TOTAL_H;
// 3. 配置层1
LTDC_Layer1->WHPCR = (WINDOW_X2 << 16) | WINDOW_X1;
LTDC_Layer1->WVPCR = (WINDOW_Y2 << 16) | WINDOW_Y1;
LTDC_Layer1->PFCR = LTDC_PFCR_PF_ARGB8888; // 颜色格式
LTDC_Layer1->CFBAR = (uint32_t)frame_buffer; // 帧缓冲区地址
LTDC_Layer1->CFBLR = (PIXEL_WIDTH * 4) << 16 | (PIXEL_WIDTH * 4 + 3);
LTDC_Layer1->CFBLNR = ACTIVE_H;
// 4. 使能LTDC
LTDC->GCR |= LTDC_GCR_LTDCEN;
LTDC->SRCR = LTDC_SRCR_IMR;
}
配置好LTDC后,你的 flush_cb 就变得非常简单——直接把 color_p 拷贝到帧缓冲区对应位置即可。因为LTDC会自动从帧缓冲区读取数据并刷新屏幕。
3.2.2 FMC驱动实现
对于并口屏(如ILI9341、SSD1963),通常使用FMC(Flexible Memory Controller)的NOR/PSRAM模式来驱动。FMC可以把屏幕映射到STM32的地址空间,读写就像操作内存一样简单。
关键配置:
- 总线宽度:8位、16位或32位,取决于屏幕接口
- 时序参数:地址建立时间、数据建立时间、总线周转时间等
- 片选映射:FMC有多个片选区域,每个区域对应不同的地址范围
配置好FMC后,写像素就变成了内存写操作:
// 假设屏幕映射到 0x60000000
#define LCD_BASE ((uint32_t)0x60000000)
#define LCD_WRITE_CMD(cmd) (*(volatile uint16_t *)(LCD_BASE)) = (cmd)
#define LCD_WRITE_DATA(data) (*(volatile uint16_t *)(LCD_BASE + 0x10000)) = (data)
static void put_pixel(uint16_t x, uint16_t y, uint16_t color)
{
LCD_WRITE_CMD(0x2A); // 设置列地址
LCD_WRITE_DATA(x);
LCD_WRITE_CMD(0x2B); // 设置行地址
LCD_WRITE_DATA(y);
LCD_WRITE_CMD(0x2C); // 写像素
LCD_WRITE_DATA(color);
}
flush_cb 的性能瓶颈往往在逐像素写入上。建议在 flush_cb 中先设置窗口(通过命令设置显示区域),然后连续写入整块数据,而不是逐像素设置坐标。
3.3 双缓冲机制原理与配置
双缓冲是解决画面撕裂和闪烁的经典方案。说白了,就是准备两个缓冲区:一个用于LVGL渲染,另一个用于显示器刷新。两者交替使用,互不干扰。
3.3.1 为什么需要双缓冲?
单缓冲模式下,LVGL直接在显示器的帧缓冲区上绘制。如果显示器正在刷新某一帧,而LVGL同时修改了缓冲区,就会出现撕裂——屏幕上部分显示旧帧,部分显示新帧。
双缓冲的流程:
- LVGL在后台缓冲区(Back Buffer)中绘制
- 绘制完成后,交换前后台缓冲区(或者拷贝数据)
- 显示器从新的前台缓冲区(Front Buffer)读取数据
- LVGL继续在后台缓冲区绘制下一帧
3.3.2 LVGL中的双缓冲配置
LVGL支持三种缓冲模式,通过 lv_disp_buf_init 配置:
| 模式 | 缓冲区数量 | 特点 | 适用场景 |
|---|---|---|---|
| 单缓冲 | 1个 | 内存占用最小,但可能撕裂 | 低端MCU,内存紧张 |
| 部分双缓冲 | 2个(小) | 每次刷新部分屏幕,内存适中 | 大多数STM32项目 |
| 全双缓冲 | 2个(全屏) | 性能最好,无撕裂,内存占用大 | 带外部SDRAM的STM32 |
配置代码示例:
// 定义两个缓冲区
#define DISP_BUF_SIZE (LV_HOR_RES_MAX * LV_VER_RES_MAX / 10) // 部分双缓冲
static lv_color_t buf1[DISP_BUF_SIZE];
static lv_color_t buf2[DISP_BUF_SIZE];
// 初始化显示缓冲区
static lv_disp_buf_t disp_buf;
lv_disp_buf_init(&disp_buf, buf1, buf2, DISP_BUF_SIZE);
// 注册到显示驱动
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.buffer = &disp_buf;
disp_drv.flush_cb = my_flush_cb;
disp_drv.rounder_cb = my_rounder_cb;
lv_disp_drv_register(&disp_drv);
3.3.3 双缓冲与LTDC的配合
使用LTDC时,双缓冲的实现更优雅——你只需要切换LTDC的帧缓冲区地址,而不需要实际拷贝数据。这就是所谓的“页面翻转”(Page Flipping)。
实现思路:
- 分配两个帧缓冲区:fb1 和 fb2
- LTDC当前指向 fb1(前台缓冲区)
- LVGL在 fb2(后台缓冲区)中绘制
- 绘制完成后,将LTDC的帧缓冲区地址改为 fb2
- 下次绘制时,LVGL在 fb1 中绘制,LTDC从 fb2 读取
在 flush_cb 中实现页面翻转:
static uint32_t active_buf = 0;
static lv_color_t fb1[LV_HOR_RES_MAX * LV_VER_RES_MAX];
static lv_color_t fb2[LV_HOR_RES_MAX * LV_VER_RES_MAX];
static void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
// 判断当前color_p指向哪个缓冲区
if(color_p == fb1) {
active_buf = 1;
LTDC_Layer1->CFBAR = (uint32_t)fb1;
} else {
active_buf = 2;
LTDC_Layer1->CFBAR = (uint32_t)fb2;
}
// 触发LTDC重载
LTDC->SRCR = LTDC_SRCR_IMR;
lv_disp_flush_ready(disp_drv);
}
LTDC_IT_LI(Line Interrupt),可以在消隐期触发中断,在中断中切换缓冲区。不过对于大多数应用,直接在 flush_cb 中切换也够用,因为LVGL的刷新频率通常低于屏幕刷新率。
3.4 本章知识体系
下面这张图总结了显示驱动移植的核心脉络:
从图中可以看出,整个显示驱动移植的脉络很清晰:LVGL通过两个回调函数与底层硬件交互,底层硬件可以是LTDC或FMC,而双缓冲机制则贯穿其中,保证显示质量。
嗯,这一章的内容就到这里。显示驱动移植是LVGL移植中最关键的一步,也是问题最多的一步。如果你在移植过程中遇到屏幕不亮、花屏、撕裂等问题,回头检查一下这三个方面:flush_cb有没有调用ready、LTDC时序对不对、双缓冲配置是否合理。大部分问题都能在这三个地方找到答案。