3. 显示驱动基础

显示驱动,说白了就是让 CPU 把像素数据送到屏幕上的那条路。这条路怎么修、用什么材料、走什么协议,直接决定了你的 GUI 跑得顺不顺。

我这些年接触过的嵌入式显示方案,基本可以归为三大类:Linux 下的 DRM/KMS、老牌的 Framebuffer,以及 RT-Thread 的 Display 框架。它们各有各的脾气,也各有各的适用场景。今天咱们就把这三兄弟掰开揉碎了聊一聊。

核心观点: 选 Framebuffer 还是 DRM/KMS,取决于你的硬件能力和实时性要求。RT-Thread 的 Display 框架则更像一个轻量级的“中间人”,帮你在资源受限的 MCU 上跑出像样的 GUI。

3.1 Linux DRM/KMS 框架:现代显示驱动的主流选择

DRM(Direct Rendering Manager)加上 KMS(Kernel Mode Setting),是 Linux 内核里负责显示的核心组件。我刚开始接触它的时候,觉得这东西太复杂了——又是 CRTC 又是 Encoder 又是 Connector,绕得人头晕。但用顺手之后,你会发现它其实非常优雅。

DRM/KMS 把显示流程拆成了几个清晰的层次:

  • CRTC(Cathode Ray Tube Controller):负责生成显示时序,说白了就是告诉屏幕“什么时候该刷新哪一行”。
  • Encoder:把 CRTC 输出的像素数据转换成物理接口需要的信号,比如 HDMI、LVDS、MIPI DSI。
  • Connector:物理接口本身,负责和屏幕握手。
  • Plane:这是 DRM 的亮点——每个 Plane 可以独立叠加一层画面,硬件自动做合成,省 CPU。

我曾在某个项目里遇到过一个问题:用 DRM 的 atomic API 做双缓冲切换时,屏幕偶尔会闪一下。查了半天,发现是 CRTC 的 vblank 事件没处理好,导致 buffer 交换和刷新不同步。后来加了个 drm_crtc_vblank_off 的调用,问题就解决了。嗯,这种坑,踩过一次就记住了。

我的建议: 如果你的芯片支持 DRM(比如 i.MX8、RK3399、全志 V系列),优先用 DRM/KMS。它比 Framebuffer 更灵活,而且支持硬件合成、多 Plane 叠加,对 GUI 性能提升很大。

3.1.1 DRM 驱动适配的关键步骤

适配一个 DRM 驱动,通常需要做这几件事:

  1. 注册 DRM 设备:调用 drm_dev_alloc 和 drm_dev_register。
  2. 实现 CRTC 回调:主要是 mode_set 和 enable/disable。
  3. 实现 Encoder 和 Connector:告诉内核你的屏幕支持哪些分辨率、刷新率。
  4. 实现 Plane:至少实现一个 primary plane,用于显示主图层。
  5. 实现 fbdev 兼容层:很多老应用还依赖 /dev/fb0,DRM 可以通过 drm_fbdev_generic_setup 提供兼容。
// 一个简化的 DRM 驱动注册示例
static int my_drm_probe(struct platform_device *pdev)
{
    struct drm_device *drm;
    int ret;

    drm = drm_dev_alloc(&my_drm_driver, &pdev->dev);
    if (IS_ERR(drm))
        return PTR_ERR(drm);

    // 初始化 CRTC、Encoder、Connector
    ret = my_drm_modeset_init(drm);
    if (ret)
        goto err_free;

    // 注册 DRM 设备
    ret = drm_dev_register(drm, 0);
    if (ret)
        goto err_free;

    // 创建 fbdev 兼容层
    drm_fbdev_generic_setup(drm, 32);

    return 0;

err_free:
    drm_dev_put(drm);
    return ret;
}

3.2 Framebuffer 机制:简单但有限的老将

Framebuffer,很多人叫它“帧缓冲”。它的思路很简单:内核在内存里划一块区域,应用层直接往里面写像素数据,内核负责把这块内存的内容刷到屏幕上。

我最早做嵌入式 GUI 的时候,用的就是 Framebuffer。那时候芯片性能弱,DRM 还没普及,Framebuffer 是最直接的选择。你想想看,一个 mmap 把 /dev/fb0 映射到用户空间,然后直接写内存,多简单。

但 Framebuffer 的缺点也很明显:

  • 没有硬件合成:所有图层都得 CPU 自己合成,费 CPU。
  • 没有 vblank 同步:容易出现撕裂(tearing)。
  • 不支持多 Plane:只能有一个显示层。
  • 扩展性差:想加个硬件光标?得自己实现。
避坑指南: 我曾经在一个项目里用 Framebuffer 跑 LVGL,发现刷新率上不去。后来一查,是 Framebuffer 的 pan_display 实现有问题,双缓冲切换时没有等 vblank。解决办法是改用 DRM 的 atomic API,或者自己加一个等待 vblank 的 ioctl。

3.2.1 Framebuffer 驱动适配要点

适配一个 Framebuffer 驱动,核心是填充 struct fb_info:

static struct fb_ops my_fb_ops = {
    .owner        = THIS_MODULE,
    .fb_fillrect  = cfb_fillrect,
    .fb_copyarea  = cfb_copyarea,
    .fb_imageblit = cfb_imageblit,
    .fb_check_var = my_fb_check_var,
    .fb_set_par   = my_fb_set_par,
    .fb_blank     = my_fb_blank,
};

static int my_fb_probe(struct platform_device *pdev)
{
    struct fb_info *fbi;
    void *fb_mem;

    // 分配显存
    fb_mem = dma_alloc_coherent(&pdev->dev, SZ_4M, &dma_addr, GFP_KERNEL);

    // 分配 fb_info
    fbi = framebuffer_alloc(0, &pdev->dev);
    fbi->fbops = &my_fb_ops;
    fbi->screen_base = fb_mem;
    fbi->screen_size = SZ_4M;

    // 设置固定参数和可变参数
    strcpy(fbi->fix.id, "my_fb");
    fbi->fix.smem_start = dma_addr;
    fbi->fix.smem_len = SZ_4M;
    fbi->fix.type = FB_TYPE_PACKED_PIXELS;
    fbi->fix.visual = FB_VISUAL_TRUECOLOR;

    // 注册 framebuffer
    register_framebuffer(fbi);

    return 0;
}

3.3 RT-Thread 的 Display 框架:轻量级的选择

RT-Thread 的 Display 框架,我是在做 STM32 项目时接触到的。它不像 Linux 那么重,但该有的功能都有:图层管理、双缓冲、硬件加速接口。

RT-Thread 的 Display 框架核心是 struct drm_display 和 struct drm_display_ops。你只需要实现几个回调函数:

  • init:初始化 LCD 控制器。
  • set_pixel:设置单个像素(用于软件渲染)。
  • fill_rect:填充矩形区域(硬件加速)。
  • flush:刷新显示缓冲区到屏幕。

我个人习惯用 RT-Thread 的 Display 框架配合 LVGL 做 GUI。它的好处是:

  • 轻量:代码量小,适合 MCU。
  • 实时性好:没有 Linux 那种调度延迟。
  • 接口简洁:几个回调函数就能搞定。
我的经验: 在 RT-Thread 上适配显示驱动时,记得把 flush 函数做成 DMA 方式,别用 CPU 逐像素刷。否则刷新率会很难看。

3.3.1 RT-Thread Display 驱动适配示例

#include <rtdevice.h>

static rt_err_t my_lcd_init(struct drm_display *disp)
{
    // 初始化 LCD 控制器 GPIO、时钟、时序
    lcd_hw_init();
    return RT_EOK;
}

static void my_lcd_set_pixel(struct drm_display *disp, 
                             rt_uint16_t x, rt_uint16_t y, 
                             rt_uint32_t color)
{
    // 设置单个像素
    lcd_write_pixel(x, y, color);
}

static void my_lcd_fill_rect(struct drm_display *disp,
                             rt_uint16_t x, rt_uint16_t y,
                             rt_uint16_t w, rt_uint16_t h,
                             rt_uint32_t color)
{
    // 硬件加速:填充矩形
    lcd_fill(x, y, x + w - 1, y + h - 1, color);
}

static void my_lcd_flush(struct drm_display *disp,
                         rt_uint16_t x, rt_uint16_t y,
                         rt_uint16_t w, rt_uint16_t h,
                         rt_uint8_t *pixels)
{
    // 刷新缓冲区到屏幕,建议用 DMA
    lcd_dma_send(x, y, w, h, pixels);
}

static struct drm_display_ops my_lcd_ops = {
    .init      = my_lcd_init,
    .set_pixel = my_lcd_set_pixel,
    .fill_rect = my_lcd_fill_rect,
    .flush     = my_lcd_flush,
};

int my_lcd_drv_init(void)
{
    struct drm_display *disp;

    disp = rt_malloc(sizeof(struct drm_display));
    disp->width  = 480;
    disp->height = 272;
    disp->pixel_format = DRM_PIXEL_FORMAT_RGB565;
    disp->ops = &my_lcd_ops;

    drm_display_register(disp, "lcd0");
    return 0;
}
INIT_DEVICE_EXPORT(my_lcd_drv_init);

3.4 三种框架的对比与选择

为了让你看得更清楚,我整理了一张对比表:

特性 DRM/KMS Framebuffer RT-Thread Display
适用平台 Linux(ARM64、x86) Linux(ARM、MIPS) RT-Thread(MCU)
硬件合成 支持(多 Plane) 不支持 部分支持(需硬件配合)
vblank 同步 原生支持 需手动实现 需手动实现
代码复杂度 高 低 中
实时性 一般(受 Linux 调度影响) 一般 好
典型应用 高端嵌入式、多媒体设备 低端 Linux 设备、快速原型 MCU GUI、工控屏

选哪个?我的建议很简单:

  • 如果你跑 Linux,芯片又支持 DRM,别犹豫,用 DRM/KMS。
  • 如果芯片太老,或者只是做个快速原型,Framebuffer 也能凑合。
  • 如果是 MCU 项目,RT-Thread 的 Display 框架是最省心的选择。

3.5 三种框架的核心关系图

下面这张图,帮你理清三种框架在系统中的位置:

三种显示驱动框架关系图 三种显示驱动框架关系图 GUI 应用层(LVGL / Qt / 自研) DRM/KMS Framebuffer RT-Thread Display DRM 驱动 + KMS fbdev 驱动 LCD 控制器驱动 显示硬件(LCD 控制器 / GPU / 显示接口) 说明: • DRM/KMS 和 Framebuffer 都运行在 Linux 内核空间,RT-Thread Display 运行在 RTOS 内核空间。 • 三种框架最终都通过 LCD 控制器驱动与硬件交互,只是抽象层次不同。 图:三种显示驱动框架在系统中的位置与关系

3.6 避坑指南:我踩过的那些坑

最后,分享几个我实际项目中遇到的坑,希望能帮你少走弯路:

  • DRM 的 atomic 提交失败:我曾经在切换分辨率时,atomic check 一直报错。后来发现是 CRTC 的 mode_valid 回调没实现,导致内核认为新模式不合法。解决办法:实现 mode_valid,返回 MODE_OK。
  • Framebuffer 的缓存一致性问题:在 ARM 平台上,CPU 和 DMA 之间可能有缓存不一致的问题。我遇到过屏幕显示花屏,后来加了 dma_map_single 和 dma_sync_single_for_device 才解决。
  • RT-Thread 的 DMA 中断冲突:有一次用 RT-Thread 的 Display 框架做 DMA 刷新,发现屏幕一直闪。查了半天,是 DMA 中断优先级太低,被其他中断抢占了。解决办法:把 DMA 中断优先级调到最高。
重要提醒: 不管你用哪种框架,一定要先确认你的硬件是否支持硬件合成和 vblank 同步。如果不支持,就别硬上 DRM,老老实实用 Framebuffer 或 RT-Thread Display,否则性能反而更差。

好了,关于显示驱动框架,今天就聊到这里。这三种框架各有千秋,选对工具,你的 GUI 适配工作就成功了一半。