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()必须优化到极致
注意:SPI接口下,如果屏幕分辨率超过320x240,建议别用全屏刷新。否则你会看到明显的刷新撕裂——我踩过这个坑,后来改成局部刷新才解决。

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];
我的经验:对于低内存MCU(比如64KB RAM),用40行双缓冲就够了。缓冲区大小 = 水平分辨率 × 行数 × 2字节(RGB565)。320x240的屏,40行双缓冲只要 320×40×2×2 = 51.2KB。刚好卡在64KB的边缘。

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会把它们的脏区域合并成一个矩形。这个矩形可能比实际变化区域大,但比全屏小得多
重要:如果你的屏幕不支持设置窗口(SetWindow),局部刷新就没法用。这种情况下,要么换屏幕,要么接受全屏刷新。我建议你买屏幕前先确认驱动芯片是否支持窗口模式——ILI9341、ST7789、NT35510都支持,但一些老款驱动芯片不支持。

3.4 实战总结

显示驱动移植,说白了就是三件事:

  1. 搞清楚接口类型:framebuffer、SPI/8080、RGB,适配方式完全不同
  2. 配好lv_port_disp.c:分辨率、颜色深度、缓冲区大小,一个都不能错
  3. 选对刷新策略:低内存MCU必须用局部刷新+双缓冲(行缓冲),别硬上全屏双缓冲

嗯,这里要注意:实际调试时,先用单缓冲把屏幕点亮,再切双缓冲。这样出了问题好定位。我每次移植新屏幕都是这么干的——先点亮,再优化。

下一章咱们讲输入设备移植,触摸屏和按键的适配。到时候我会分享一个我踩过的坑:电容触摸屏的I2C地址跟数据手册不一样,折腾了我一整天。