3、显示驱动框架:Linux DRM/KMS、Framebuffer 与 RT-Thread Display
显示驱动,说白了就是让 CPU 把像素数据送到屏幕上的那条路。这条路怎么修、用什么材料、走什么协议,直接决定了你的 GUI 跑得顺不顺。
我这些年接触过的嵌入式显示方案,基本可以归为三大类:Linux 下的 DRM/KMS、老牌的 Framebuffer,以及 RT-Thread 的 Display 框架。它们各有各的脾气,也各有各的适用场景。今天咱们就把这三兄弟掰开揉碎了聊一聊。
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 的调用,问题就解决了。嗯,这种坑,踩过一次就记住了。
3.1.1 DRM 驱动适配的关键步骤
适配一个 DRM 驱动,通常需要做这几件事:
- 注册 DRM 设备:调用
drm_dev_alloc和drm_dev_register。 - 实现 CRTC 回调:主要是
mode_set和enable/disable。 - 实现 Encoder 和 Connector:告诉内核你的屏幕支持哪些分辨率、刷新率。
- 实现 Plane:至少实现一个 primary plane,用于显示主图层。
- 实现 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:只能有一个显示层。
- 扩展性差:想加个硬件光标?得自己实现。
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 那种调度延迟。
- 接口简洁:几个回调函数就能搞定。
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 三种框架的核心关系图
下面这张图,帮你理清三种框架在系统中的位置:
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 中断优先级调到最高。
好了,关于显示驱动框架,今天就聊到这里。这三种框架各有千秋,选对工具,你的 GUI 适配工作就成功了一半。