3、Linux显示子系统:DRM/KMS框架介绍、Framebuffer设备驱动模型、V4L2与显示的关系

好,我们进入第三章。这一章要聊的东西,是座舱显示驱动开发的核心中的核心。说白了,你搞车载显示,绕不开DRM/KMS,绕不开Framebuffer,也绕不开V4L2。这三兄弟,一个管渲染,一个管显存,一个管摄像头数据流。它们怎么配合?我带你捋一遍。

3.1 DRM/KMS框架:现代Linux显示的中枢神经

DRM,全称Direct Rendering Manager,直接渲染管理器。KMS,Kernel Mode Setting,内核模式设置。这两个词经常绑在一起出现,但干的活不一样。

DRM负责什么? 它管理GPU的显存分配、命令提交、同步。你想想看,一个应用要画一帧画面,它得先跟内核申请一块显存,然后把数据填进去,最后告诉GPU:“哥们,可以扫到屏幕上了。” 这个过程,就是DRM在背后调度。

KMS负责什么? 它管显示控制器的配置。比如分辨率设多少、刷新率多少、连接的是HDMI还是LVDS、要不要做双屏拼接。这些底层硬件寄存器操作,全归KMS管。

我个人习惯把DRM/KMS比作一个“显示调度中心”。DRM是后勤部,管内存和命令;KMS是前台,管屏幕参数。两者缺一不可。

核心数据结构:

  • drm_device:代表一个DRM设备,通常是/dev/dri/card0
  • drm_crtc:显示控制器,负责把framebuffer的数据扫到显示器上
  • drm_encoder:编码器,把数字信号转成显示器能吃的格式(比如LVDS、eDP)
  • drm_connector:物理连接器,代表一个实际的显示接口
  • drm_plane:硬件图层,支持叠加多个画面(比如导航层叠在仪表盘上)

我在项目中遇到过一个问题:客户要求双屏异显,左边仪表盘,右边中控。当时我第一反应就是用两个CRTC,每个CRTC绑定一个connector。但调试时发现,其中一个屏幕死活不亮。查了半天,原来是encoder和connector的匹配关系没配好。嗯,这里要注意:一个CRTC可以连多个encoder,但一个encoder只能连一个connector。这个拓扑关系搞错了,屏幕就是黑的。

3.2 Framebuffer设备驱动模型:显存的那点事

Framebuffer,帧缓冲。这个名字很直白,就是一块内存区域,用来存放一帧画面的像素数据。在Linux里,它被抽象成/dev/fb0这样的设备文件。

你可能会问:“有了DRM,为什么还要Framebuffer?” 好问题。其实在早期的Linux显示系统中,Framebuffer是主力。后来DRM/KMS出现,Framebuffer退居二线,但它依然存在,而且很重要。

Framebuffer的核心作用:

  • 提供一块连续物理内存,供CPU直接读写像素
  • 通过mmap映射到用户空间,应用可以直接操作显存
  • 支持双缓冲、三缓冲机制,防止画面撕裂

我记得有一次调试一个低端座舱方案,GPU性能不够,只能用CPU直接画仪表盘。这时候Framebuffer就派上用场了。应用层直接mmap /dev/fb0,然后往里面填像素数据,简单粗暴。但缺点也很明显:没有硬件加速,全靠CPU扛,帧率上不去

避坑指南: 我曾经在切换Framebuffer分辨率时,忘记重新计算stride(行跨度),导致画面出现奇怪的条纹。stride = 每行像素字节数,它不一定等于width * bpp,因为硬件可能有对齐要求。比如宽度1920,bpp=32,stride可能是7680,也可能是8192(对齐到256字节)。这个值一定要从驱动里读出来,别自己算。

Framebuffer的驱动模型其实很简单:注册一个fb_info结构体,实现fb_ops回调函数。核心回调包括:

static struct fb_ops myfb_ops = {
    .owner          = THIS_MODULE,
    .fb_open        = myfb_open,
    .fb_release     = myfb_release,
    .fb_read        = myfb_read,
    .fb_write       = myfb_write,
    .fb_fillrect    = myfb_fillrect,   // 填充矩形
    .fb_copyarea    = myfb_copyarea,   // 区域拷贝
    .fb_imageblit   = myfb_imageblit,  // 位块传输
    .fb_setcolreg   = myfb_setcolreg,  // 设置颜色寄存器
    .fb_setpar      = myfb_setpar,     // 设置显示参数
    .fb_check_var   = myfb_check_var,  // 检查可变参数
    .fb_set_var     = myfb_set_var,    // 设置可变参数
};

你想想看,这些回调函数,说白了就是让内核知道:怎么读写显存、怎么画矩形、怎么拷贝区域。如果你的硬件有2D加速引擎,可以在fb_fillrectfb_copyarea里调用硬件加速,否则就用软件模拟。

3.3 V4L2与显示的关系:摄像头数据怎么上屏?

V4L2,Video for Linux 2,是Linux的视频采集框架。它跟显示有什么关系?关系大了去了。座舱里经常有倒车影像、360环视、DMS驾驶员监控。这些摄像头数据,最终都要显示到屏幕上。

V4L2的核心流程:

  1. 打开摄像头设备(/dev/video0)
  2. 设置格式(分辨率、像素格式,比如YUYV、NV12)
  3. 申请缓冲区(通常用mmap)
  4. 启动数据流(stream on)
  5. 从队列里取出帧(dequeue buffer)
  6. 处理数据(比如转成RGB,然后送给DRM显示)
  7. 把缓冲区放回队列(queue buffer)

这里有个关键点:V4L2采集到的数据,通常是YUV格式,而DRM显示需要RGB格式。所以中间必须有一个格式转换环节。我见过两种做法:

  • 软件转换:用libyuv或者ffmpeg,在CPU上把YUV转成RGB。简单,但费CPU,帧率一高就卡。
  • 硬件转换:用GPU的着色器或者专门的ISP模块,直接硬件转换。效率高,但驱动复杂。

注意: 在座舱项目中,倒车影像的延迟要求非常严格。从摄像头采集到屏幕显示,整个链路延迟不能超过100ms,否则驾驶员会感觉画面滞后。我曾经在一个项目里,因为V4L2的缓冲区队列设置太小,导致丢帧,画面一卡一卡的。后来把缓冲区数量从2个增加到4个,问题解决。嗯,缓冲区数量不是越多越好,但太少肯定不行。

V4L2和DRM的配合,通常通过DMA-BUF机制实现。V4L2采集到一帧数据后,不拷贝,而是把缓冲区对应的DMA-BUF句柄传给DRM。DRM直接把这个DMA-BUF作为framebuffer的显存来源。这样零拷贝,延迟最低。

说白了,V4L2是数据生产者,DRM是数据消费者。中间用DMA-BUF搭一座桥,数据从摄像头直接流到屏幕,不经过CPU。这才是座舱显示该有的样子。

3.4 三者的协作关系:一张图说清楚

我习惯用一张表格来总结它们的分工:

组件 职责 典型设备节点 数据流向
DRM/KMS 管理GPU显存、配置显示控制器、控制图层叠加 /dev/dri/card0 framebuffer → CRTC → encoder → connector → 屏幕
Framebuffer 提供显存区域,支持CPU直接读写像素 /dev/fb0 CPU → 显存 → 显示控制器
V4L2 采集摄像头数据,提供视频流 /dev/video0 摄像头 → 缓冲区 → 用户空间(或直接DMA-BUF给DRM)

你想想看,一个典型的倒车影像场景:V4L2从摄像头采集YUV数据,通过DMA-BUF传给DRM,DRM把它作为overlay plane叠加到主画面上。整个过程,CPU只负责控制流,数据流完全走硬件。这才是嵌入式座舱显示的正确打开方式。

总结一下:

  • DRM/KMS是显示框架的核心,管渲染和配置
  • Framebuffer是显存的抽象,适合简单场景
  • V4L2是视频采集的入口,通过DMA-BUF与DRM零拷贝对接
  • 三者配合,才能实现座舱里复杂的显示需求

好,这一章就到这里。下一章我们开始动手写一个简单的DRM驱动,从零点亮一块屏幕。到时候你会看到,这些概念是怎么落到代码里的。