4、显示驱动适配(下):编写显示初始化函数、实现flush_callback回调、配置像素格式与颜色深度、实测验证与性能调优

好,咱们接着上一章往下聊。上回我们把显示驱动的骨架搭好了,也把硬件接口打通了。这一章,我们要让屏幕真正亮起来,显示出东西来。

说白了,就是要把LVGL的绘图指令,变成屏幕上的像素点。这件事做成了,你的嵌入式设备才算有了“脸面”。

4.1 编写显示初始化函数

初始化函数,是屏幕的“开机仪式”。每个屏幕都有自己的脾气,你得按它的规矩来。

我个人习惯把初始化拆成三步:

  1. 硬件复位:拉低RST引脚,等个10ms,再拉高。这一步是让屏幕控制器回到已知状态。
  2. 配置参数:通过SPI或I2C发送初始化指令序列。比如设置扫描方向、像素格式、伽马曲线等。
  3. 唤醒显示:发送SLPOUT(睡眠退出)和DISPON(显示开启)指令。

这里我贴一段典型的初始化代码,以ILI9341为例:

static void lcd_init(void)
{
    // 1. 硬件复位
    lcd_reset_gpio_write(0);
    lcd_delay_ms(10);
    lcd_reset_gpio_write(1);
    lcd_delay_ms(120);

    // 2. 发送初始化序列
    lcd_send_cmd(0x01);   // 软件复位
    lcd_delay_ms(120);

    lcd_send_cmd(0x11);   // 退出睡眠
    lcd_delay_ms(120);

    lcd_send_cmd(0x36);   // 内存访问控制
    lcd_send_data(0x48);  // MX, MV位设置

    lcd_send_cmd(0x3A);   // 像素格式
    lcd_send_data(0x55);  // 16位色(RGB565)

    lcd_send_cmd(0x29);   // 开启显示
    lcd_delay_ms(10);
}

嗯,这里要注意:不同厂商的屏幕,初始化序列差别很大。我遇到过一块便宜的国产屏,官方手册写的序列死活点不亮,后来发现是复位时序太短。把延时从5ms改成20ms,立马就好了。

避坑指南:初始化序列不要照搬网上的代码。一定要对照你手里那块屏幕的数据手册,逐条核对。我曾经因为漏了一条“像素格式设置”,折腾了整整一个下午。

4.2 实现flush_callback回调

这是LVGL和硬件之间的“桥梁”。LVGL画好一帧画面后,会调用这个回调函数,把数据交给你,你再把它刷到屏幕上。

回调函数的原型长这样:

static void my_flush_cb(lv_disp_drv_t * disp_drv, 
                        const lv_area_t * area, 
                        lv_color_t * color_p)
{
    // area->x1, area->y1: 左上角坐标
    // area->x2, area->y2: 右下角坐标
    // color_p: 像素数据缓冲区

    // 1. 设置屏幕的写入窗口
    lcd_set_window(area->x1, area->y1, area->x2, area->y2);

    // 2. 通过SPI/DMA发送像素数据
    lcd_send_pixels(color_p, 
                    (area->x2 - area->x1 + 1) * 
                    (area->y2 - area->y1 + 1));

    // 3. 通知LVGL刷新完成
    lv_disp_flush_ready(disp_drv);
}

你想想看,这个回调会被频繁调用。如果每次都用CPU逐像素发送,性能会很难看。我建议用DMA传输,CPU可以腾出手来做别的事。

性能小技巧:如果MCU支持,把DMA配置成“循环模式”或“双缓冲模式”。这样LVGL在准备下一帧时,DMA还在传输上一帧的数据,流水线作业,帧率能提升30%以上。

4.3 配置像素格式与颜色深度

LVGL支持多种颜色深度:1位、8位、16位、32位。最常用的是16位RGB565,因为它在颜色表现和内存占用之间取得了平衡。

配置方式很简单,在lv_conf.h中设置:

/* 颜色深度设置 */
#define LV_COLOR_DEPTH     16

/* 颜色格式 */
#define LV_COLOR_16_SWAP   0   // 字节序是否交换

这里有个坑:有些屏幕的RGB565字节序是反的。比如你发送0x1F, 0xE0(红色),屏幕却显示蓝色。这就是字节序问题。

我遇到过这种情况,当时排查了很久。后来用了一个简单的方法验证:画一个纯红色的矩形,然后用逻辑分析仪抓SPI数据,对比手册里的像素格式说明,才发现是字节序反了。

解决办法有两个:

  • 设置LV_COLOR_16_SWAP = 1,让LVGL帮你交换字节序
  • 或者在发送数据时手动交换高低字节
颜色深度 每像素字节数 适用场景 内存占用(320x240)
1位 1/8 单色屏、低功耗 9.6 KB
8位 1 低端彩屏 76.8 KB
16位 2 主流彩屏 153.6 KB
32位 4 高画质需求 307.2 KB

4.4 实测验证与性能调优

代码写完了,怎么知道它跑得好不好?我一般做三个测试:

4.4.1 基础功能测试

先跑LVGL自带的benchmark demo。它能显示各种图形、文字、动画。如果画面有撕裂、花屏、颜色不对,说明驱动还有问题。

我习惯先画一个纯色全屏,比如红色0xF800。如果整个屏幕都是均匀的红色,说明数据传输基本正常。然后再画渐变条,检查颜色过渡是否平滑。

4.4.2 帧率测试

帧率是衡量显示性能的关键指标。LVGL提供了lv_disp_get_inactive_time()函数,可以估算帧率。

更直接的方法:在flush_callback里翻转一个GPIO,用示波器看波形。高电平持续时间就是刷一帧的时间。

static void my_flush_cb(...)
{
    gpio_set_level(DEBUG_PIN, 1);  // 开始刷屏

    // ... 传输数据 ...

    gpio_set_level(DEBUG_PIN, 0);  // 刷屏结束
    lv_disp_flush_ready(disp_drv);
}

320x240的屏幕,16位色,SPI时钟40MHz,理论帧率应该在30fps以上。如果低于15fps,就要优化了。

4.4.3 性能调优三板斧

如果帧率不理想,我一般按这个顺序排查:

  1. 开启DMA:这是最有效的优化。CPU不用等SPI传输完成,可以回去继续渲染。
  2. 调整缓冲区大小:LVGL默认用全屏缓冲区,内存不够的话可以改成行缓冲区。但行缓冲区太小会降低效率,我一般用屏幕高度的1/4。
  3. 减少无效刷新:LVGL默认只刷新变化区域。如果你的应用场景变化不大,可以开启LV_DISP_DEF_REFR_PERIOD,降低刷新频率。
我的经验:有一次客户说屏幕卡顿,我排查了半天,发现是SPI时钟配置错了,实际只有10MHz。把时钟提到40MHz后,帧率从12fps直接飙到35fps。所以,先检查硬件配置,再优化软件,这个顺序别搞反了。

4.5 小结

这一章我们走完了显示驱动适配的全流程:从初始化函数,到flush回调,再到颜色配置和性能调优。说白了,就是让LVGL和你的屏幕“对上话”。

下一章,我们会聊输入设备的适配。触摸屏、按键、编码器,这些东西怎么和LVGL配合,让用户能和你的设备交互。到时候见。