2. 显示技术基础

帧缓冲,说白了就是一块内存区域。它存着屏幕上每个像素的颜色值。你往里面写数据,屏幕就显示对应的画面。听起来简单,但实际做起来,坑不少。

我刚开始做嵌入式显示驱动时,以为帧缓冲就是一块显存,直接读写就行。后来发现,在Linux系统里,事情没那么简单。你得跟DRM/KMS打交道,得理解mmap映射,还得处理双缓冲。今天我就把这些经验掰开了讲给你听。

Linux DRM/KMS架构

DRM是Direct Rendering Manager的缩写。它是Linux内核里负责图形显示的核心框架。KMS是Kernel Mode Setting,负责设置显示模式——分辨率、刷新率、像素格式这些。

你想想看,如果没有DRM/KMS,每个应用程序都得自己去操作显卡寄存器。那不乱套了?DRM/KMS把这些底层操作统一管理起来,应用程序只需要通过标准接口跟它沟通就行。

核心概念:DRM把显示流程抽象成几个关键对象:

  • CRTC:显示控制器,负责生成时序信号
  • Encoder:编码器,把像素数据转换成物理接口信号
  • Connector:连接器,代表物理接口(HDMI、DP、LVDS等)
  • Plane:图层,可以叠加多个画面
  • FB:帧缓冲对象,指向实际的内存区域

我做过一个项目,要在嵌入式设备上同时输出两个屏幕。当时就是通过DRM枚举出两个Connector,分别绑定不同的CRTC和FB。嗯,这里要注意:每个Connector只能绑定一个CRTC,但一个CRTC可以驱动多个Connector——前提是它们共用同一套时序。

mmap映射:把内核内存映射到用户空间

帧缓冲在内核空间分配。用户空间的应用程序怎么访问它?答案就是mmap。

mmap把内核空间的物理内存映射到用户空间的虚拟地址。映射之后,应用程序就能像操作普通内存一样读写帧缓冲了。

// 打开DRM设备
int fd = open("/dev/dri/card0", O_RDWR);

// 获取帧缓冲信息
struct drm_mode_fb_cmd fb;
// ... 填充fb结构体 ...

// 获取帧缓冲的偏移量
struct drm_mode_map_dumb mmap_arg;
mmap_arg.handle = fb.handle;
ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &mmap_arg);

// mmap映射
size_t size = fb.width * fb.height * fb.bpp / 8;
void *fb_ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
                    MAP_SHARED, fd, mmap_arg.offset);

// 现在可以直接写fb_ptr了
// 比如把整个屏幕刷成红色
memset(fb_ptr, 0xFF, size);  // 假设是RGB565格式

// 用完记得解除映射
munmap(fb_ptr, size);
close(fd);

我曾经踩过的坑:mmap映射后,直接往里面写数据,发现屏幕闪烁。原因是我没考虑缓存一致性。ARM架构下,CPU写内存会经过Cache,但DMA控制器直接读物理内存。解决办法是使用dma_buf或者手动刷Cache行。在嵌入式Linux里,可以用dma_sync_single_for_device来保证数据同步。

双缓冲机制:告别撕裂

单缓冲的问题很明显:你正在往帧缓冲里写数据,屏幕控制器也在读同一块内存。如果写了一半,屏幕开始刷新,就会出现撕裂——上半部分是旧画面,下半部分是新画面。

双缓冲就是准备两块帧缓冲。一块叫前台缓冲,屏幕控制器正在读它。另一块叫后台缓冲,应用程序往里面写数据。写完后,交换前后台的角色。这个交换操作叫Page Flip。

特性 单缓冲 双缓冲
内存占用 1个FB大小 2个FB大小
撕裂风险 高 低(配合VBlank同步)
实现复杂度 低 中
适用场景 静态画面、低要求 动态画面、视频播放

DRM里实现双缓冲,核心是drmModePageFlip这个ioctl。它会告诉内核:等下一次VBlank(垂直消隐期)到来时,切换显示源到新的FB。

// 创建两个FB
uint32_t fb_id[2];
// ... 通过drmModeAddFB创建 ...

// 当前显示的FB索引
int current = 0;

// 渲染循环
while (running) {
    // 往后台缓冲写数据
    render_to_fb(fb_ptr[1 - current]);

    // 请求Page Flip
    drmModePageFlip(fd, crtc_id, fb_id[1 - current],
                    DRM_MODE_PAGE_FLIP_EVENT, NULL);

    // 等待VBlank事件
    drmHandleEvent(fd, &ev);

    // 交换索引
    current = 1 - current;
}

我的经验:Page Flip是异步操作。你发出请求后,内核会在下一个VBlank到来时执行切换。如果你在切换完成前又发了一次Page Flip,会返回EBUSY。所以一定要等事件通知。我习惯用drmWaitVBlank来同步,但更推荐用事件驱动的方式,避免忙等待浪费CPU。

整体流程:一张图说清楚

DRM/KMS 双缓冲工作流程 用户空间 (Application) 后台缓冲 (Back Buffer) 应用程序写入 前台缓冲 (Front Buffer) 屏幕控制器读取 mmap映射 Page Flip 交换 内核空间 (DRM/KMS) drmModePageFlip VBlank 同步 DMA 传输 硬件层 (Display Controller) 读取前台缓冲 输出到显示器

这张图展示了完整的双缓冲流程。应用程序通过mmap映射访问后台缓冲,写入画面数据。然后调用Page Flip,内核在下一个VBlank到来时切换前后台。切换完成后,原来的前台变成新的后台,应用程序继续往里面写。如此循环,画面流畅无撕裂。

避坑指南

我曾经犯过的错:

  • 忘记同步VBlank:直接连续Page Flip,结果画面闪烁。后来发现是切换太快,屏幕还没完成刷新。解决办法是等VBlank事件确认后再发下一次。
  • mmap大小算错:帧缓冲有对齐要求,实际大小可能比width*height*bpp大。要用drmModeGetFB获取真实的pitch值。
  • 多线程竞争:渲染线程和Page Flip线程同时操作同一个FB,导致数据错乱。我后来用原子操作和双缓冲索引解决了这个问题。

我的建议:刚开始做双缓冲时,先用单色填充测试。确认Page Flip和VBlank同步没问题后,再上复杂的渲染逻辑。这样排查问题会快很多。

总结

帧缓冲是显示系统的基石。DRM/KMS提供了标准化的管理框架,mmap让用户空间能直接操作帧缓冲,双缓冲则解决了画面撕裂的问题。这三者配合起来,才能实现流畅、无撕裂的显示效果。

在实际项目中,我建议你先把单缓冲跑通,确认mmap映射和像素格式没问题。然后再加双缓冲,重点测试Page Flip和VBlank同步。嗯,这样一步步来,不容易出大问题。