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;
}
💡 我的习惯: 即使硬件不强制要求对齐,我也会实现 rounder_cb。因为对齐后的区域更大,单次刷新的数据量更多,可以减少 flush_cb 的调用次数,整体性能反而更好。

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);
}
🔑 关键点: 用FMC驱动并口屏时,flush_cb 的性能瓶颈往往在逐像素写入上。建议在 flush_cb 中先设置窗口(通过命令设置显示区域),然后连续写入整块数据,而不是逐像素设置坐标。

3.3 双缓冲机制原理与配置

双缓冲是解决画面撕裂和闪烁的经典方案。说白了,就是准备两个缓冲区:一个用于LVGL渲染,另一个用于显示器刷新。两者交替使用,互不干扰。

3.3.1 为什么需要双缓冲?

单缓冲模式下,LVGL直接在显示器的帧缓冲区上绘制。如果显示器正在刷新某一帧,而LVGL同时修改了缓冲区,就会出现撕裂——屏幕上部分显示旧帧,部分显示新帧。

双缓冲的流程:

  1. LVGL在后台缓冲区(Back Buffer)中绘制
  2. 绘制完成后,交换前后台缓冲区(或者拷贝数据)
  3. 显示器从新的前台缓冲区(Front Buffer)读取数据
  4. 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);
💡 我的建议: 如果内存允许,优先使用全双缓冲。STM32F429搭配外部SDRAM时,分配两个800x480的ARGB8888缓冲区(约3MB)完全没问题。全双缓冲下,LVGL的渲染速度会明显提升,因为不需要频繁拷贝数据。

3.3.3 双缓冲与LTDC的配合

使用LTDC时,双缓冲的实现更优雅——你只需要切换LTDC的帧缓冲区地址,而不需要实际拷贝数据。这就是所谓的“页面翻转”(Page Flipping)。

实现思路:

  1. 分配两个帧缓冲区:fb1 和 fb2
  2. LTDC当前指向 fb1(前台缓冲区)
  3. LVGL在 fb2(后台缓冲区)中绘制
  4. 绘制完成后,将LTDC的帧缓冲区地址改为 fb2
  5. 下次绘制时,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);
}
⚠️ 注意: 页面翻转必须在垂直消隐期(VBlank)进行,否则会出现撕裂。LTDC提供了中断标志位 LTDC_IT_LI(Line Interrupt),可以在消隐期触发中断,在中断中切换缓冲区。不过对于大多数应用,直接在 flush_cb 中切换也够用,因为LVGL的刷新频率通常低于屏幕刷新率。

3.4 本章知识体系

下面这张图总结了显示驱动移植的核心脉络:

显示驱动移植核心知识体系 LVGL 显示驱动 (lv_disp_drv_t) flush_cb 像素数据推送 + 刷新完成通知 rounder_cb 刷新区域对齐(可选但建议实现) LTDC 驱动 RGB接口屏 / 内部控制器 时钟·时序·层·帧缓冲区 FMC 驱动 并口屏(ILI9341等) 地址映射·时序·窗口写入 双缓冲机制 单缓冲 / 部分双缓冲 / 全双缓冲 · 页面翻转 无撕裂 · 高性能 · 流畅显示

从图中可以看出,整个显示驱动移植的脉络很清晰:LVGL通过两个回调函数与底层硬件交互,底层硬件可以是LTDC或FMC,而双缓冲机制则贯穿其中,保证显示质量。

嗯,这一章的内容就到这里。显示驱动移植是LVGL移植中最关键的一步,也是问题最多的一步。如果你在移植过程中遇到屏幕不亮、花屏、撕裂等问题,回头检查一下这三个方面:flush_cb有没有调用ready、LTDC时序对不对、双缓冲配置是否合理。大部分问题都能在这三个地方找到答案。