从 fbdev 到 drm/kms
Linux 显示驱动系列
在 Linux 上往屏幕画东西,底层离不开 Framebuffer。从古老的 fbdev 到现代的 DRM/KMS,这条路走了 20 年。
一、Framebuffer 是什么
Framebuffer 直译过来就是"帧缓冲区"——一块内存区域,每个像素对应其中的若干字节。内核或硬件把这块内存的内容持续扫描输出到显示器,就形成了我们看到的画面。
应用程序写入像素 → Framebuffer 内存 → 显示控制器逐行扫描 → 你在屏幕上看到的内容
硬件上,Framebuffer 可以是系统主内存中的一段连续区域(通过 DMA 传输到显示控制器),也可以直接位于显存(VRAM)中。无论是哪种,核心都是一块可以被 CPU/GPU 写入、同时被显示控制器读取的缓冲区。
典型的 Framebuffer 大小计算公式:
// 以 1920×1080、32bpp(RGBA)为例size_t fb_size = width * height * (bpp / 8);// = 1920 × 1080 × 4 ≈ 8.3 MB二、fbdev:经典但过时
早期的 Linux 显示驱动框架是 fbdev(Framebuffer Device),用户空间通过 /dev/fb0 设备文件访问。
核心数据结构
struct fb_info { struct fb_var_screeninfo var; // 可变参数:分辨率、位深、刷新率 struct fb_fix_screeninfo fix; // 固定参数:起始地址、步长、MMIO 长度 void __iomem *screen_base; // Framebuffer 映射基地址 size_t screen_size; // 映射大小 struct fb_ops *fbops; // 操作函数表};用户空间用法
// 打开 fbdev 设备int fd = open("/dev/fb0", O_RDWR);// 获取固定信息structfb_fix_screeninfo fix;ioctl(fd, FBIOGET_FSCREENINFO, &fix);// 获取可变信息(当前分辨率等)struct fb_var_screeninfo var;ioctl(fd, FBIOGET_VSCREENINFO, &var);// mmap 映射帧缓冲区到用户空间size_t screensize = var.xres * var.yres * var.bits_per_pixel / 8;char *fbp = (char *)mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);// 直接写像素:把 (x,y) 画成红色int location = (x + y * var.xres) * var.bits_per_pixel / 8;fbp[location + 0] = 0x00; // Bfbp[location + 1] = 0x00; // Gfbp[location + 2] = 0xFF; // Rfbdev 的痛点
- 单缓冲区:需用
FBIOPAN_DISPLAY模拟双缓冲,且不支持 atomic 更新 - 没有原生 vsync 同步:容易出现撕裂(tearing)
- 不支持多显示器:每个显示器一个 /dev/fbX,配置麻烦
- 缺少统一模型管理:EDID、连接器状态、时钟配置都得手工处理
- GPU 加速困难:与 DRM/GEM 没有原生集成
fbdev 简单直接,适合嵌入式小屏、终端控制台、系统启动阶段。但对于现代桌面、游戏、高刷新率显示器,它力不从心。
三、DRM/KMS:现代显示栈
DRM(Direct Rendering Manager) 是 Linux 内核的现代图形显示框架,其子模块 KMS(Kernel Mode Setting) 负责显示模式设置和输出管理。
用户空间(Wayland / Xorg / 应用)↓DRM ioctl / dumb buffer / GEM↓KMS → CRTC + Encoder + Connector + Plane↓显示硬件
KMS 四大核心对象
| 对象 | 含义 | 类比 |
|---|---|---|
| CRTC | 显示控制器,负责从 Framebuffer 读取数据并生成扫描时序 | "大脑"——掌控扫描时机 |
| Encoder | 将 CRTC 输出的像素流编码为特定接口信号(HDMI、DP、LVDS、DSI) | "翻译官" |
| Connector | 物理连接器,连接显示器。携带 EDID、状态检测等信息 | "插座" |
| Plane | 硬件图层,每个 Plane 可绑定独立的 Framebuffer,支持叠加合成 | "透明胶片" |
一个简单的 KMS 驱动骨架
// 驱动 probe 函数(以虚拟驱动为例)static int my_drm_probe(platform_device *pdev){ struct drm_device *drm; struct drm_connector *connector; struct drm_encoder *encoder; struct drm_crtc *crtc; // 1. 分配 DRM 设备 drm = drm_dev_alloc(&my_drm_driver, &pdev->dev); drm_dev_set_unique(drm, "my_drm_%d", pdev->id); // 2. 初始化模式配置 drm_mode_config_init(drm); drm->mode_config.min_width = 320; drm->mode_config.max_width = 4096; drm->mode_config.min_height = 240; drm->mode_config.max_height = 2160; // 3. 创建 CRTC crtc = kzalloc(sizeof(*crtc), GFP_KERNEL); drm_crtc_init(drm, crtc, &my_crtc_funcs); // 4. 创建 Encoder encoder = kzalloc(sizeof(*encoder), GFP_KERNEL); encoder->possible_crtcs = drm_crtc_mask(crtc); drm_encoder_init(drm, encoder, &my_encoder_funcs, DRM_MODE_ENCODER_DPI, NULL); // 5. 创建 Connector(这个需要实现 get_modes 回调) connector = kzalloc(sizeof(*connector), GFP_KERNEL); drm_connector_init(drm, connector, &my_connector_funcs, DRM_MODE_CONNECTOR_DPI); drm_connector_attach_encoder(connector, encoder); // 6. 注册 DRM 设备 return drm_dev_register(drm, 0);}四、Framebuffer 创建:dumb buffer vs GEM
在 DRM 中,Framebuffer 的本质是 dumb buffer(简单帧缓冲) 或 GEM(Graphics Execution Manager)对象。
Dumb Buffer(CPU 直接访问)
适用于不涉及 GPU 的简单场景——纯 CPU 渲染、嵌入式小屏。
// 用户空间创建 dumb bufferstruct drm_mode_create_dumb arg = { .width = 1920, .height = 1080, .bpp = 32,};ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &arg);// arg.handle 是返回的 GEM handle// arg.pitch 是每行字节数(stride)// arg.size 是缓冲总大小// 映射到用户空间structdrm_mode_map_dumb map = { .handle = arg.handle,};ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map);// map.offset 是 mmap 的偏移void *ptr = mmap(0, arg.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, map.offset);// 绑定为 KMS framebufferuint32_t fb_id;drmModeAddFB(fd, 1920, 1080, 24, 32, arg.pitch, arg.handle, &fb_id);// 把 fb 提交给 CRTC 显示drmModeSetCrtc(fd, crtc_id, fb_id, 0, 0, &connector_id, 1, &mode);GEM(GPU 管理)
当存在 GPU 硬件时,通过 GEM 分配的缓冲可以直接被 GPU 访问、渲染,避免 CPU/GPU 间的拷贝。
// 内核驱动中的 GEM 对象创建struct drm_gem_object *my_gem_create_object(struct drm_device *drm, size_t size){ struct my_gem_object *obj; obj = kzalloc(sizeof(*obj), GFP_KERNEL); if (!obj) return ERR_PTR(-ENOMEM); // 来自 CMA(连续内存分配器)或 IOMMU obj->vaddr = dma_alloc_wc(drm->dev, size, &obj->paddr, GFP_KERNEL); return &obj->base;}// 把 GEM 对象注册为 framebufferstatic int my_dirty_fb(structdrm_framebuffer *fb,struct drm_file *file_priv,unsigned flags, unsigned color,struct drm_clip_rect *clips,unsigned num_clips){ // 这里处理脏矩形更新(比如通过 SPI 或 I2C 更新屏幕) struct my_gem_object *obj = to_my_gem(fb->obj[0]); update_display(obj->vaddr, clips, num_clips); return 0;}五、实际驱动分析:tiny / simpledrm
simpledrm — 最简单的 DRM 驱动
系统启动阶段 UEFI/U-Boot 已经初始化好了显示硬件,simpledrm 直接接管已有的 Framebuffer。
// drivers/gpu/drm/tiny/simpledrm.c(精华版)static int simpledrm_probe(structplatform_device *pdev){ // 读取设备树中的 framebuffer 地址和格式 struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0); void __iomem *base = devm_ioremap_resource(&pdev->dev, res); // 注册一个 DRM 驱动,一个 CRTC + 一个 Plane // 直接使用 bootloader 设置好的模式 drm = devm_drm_dev_alloc(&pdev->dev, &simpledrm_driver, struct simpledrm_device, dev); drm_plane_helper_disable(NULL, NULL); // 单静态 plane return drm_dev_register(drm, 0);}bochs / mgag200 — 虚拟化场景
QEMU 的 bochs 显卡驱动、BMC 的 mgag200 驱动,都是通过一块简单的线性 Framebuffer 让虚拟机或服务器拥有基本显示能力。
六、Framebuffer 驱动的调试
常用工具
| 工具 | 用途 |
|---|---|
modetest |
libdrm 自带的测试工具,枚举/设置显示模式、提交 fb |
kmscube |
使用 DRM/KMS 直接渲染 3D 旋转立方体(依赖 EGL/GBM) |
dmesg |
查看驱动的 probe、mode set 日志 |
cat /sys/kernel/debug/dri/0/name |
查看 DRM 设备信息 |
echo 0x1ff > /sys/module/drm/parameters/debug |
开启 DRM 调试日志 |
常见调试方法
# 查看当前显示状态modetest -M "simpledrm" -c# 手动设置模式并显示颜色条modetest -M "simpledrm" -s 32@0:1920x1080-60 -d# 测试 framebuffer 直写(无需 DRM)dd if=/dev/urandom of=/dev/fb0 bs=1024 count=100七、从驱动角度看 fb 的完整生命周期
1️⃣ probe → 检测硬件、获取物理地址和中断2️⃣ drm_dev_alloc → 分配 DRM 核心结构3️⃣ 初始化 CRTC / Encoder / Connector / Plane4️⃣ drm_dev_register → 暴露 /dev/dri/card0 给用户空间5️⃣ 用户空间调用 DRM_IOCTL_MODE_CREATE_DUMB 分配 fb6️⃣ drmModeSetCrtc → 绑定 fb 到 CRTC → 屏幕亮起7️⃣ 后续通过 page flip 实现 vsync 同步的双缓冲8️⃣ 设备移除 → drm_dev_unregister / drm_dev_put
Page Flip — 告别撕裂
// 用户空间请求页面翻转(vsync 同步)struct drm_mode_crtc_page_flip flip = { .crtc_id = crtc_id, .fb_id = new_fb_id, .flags = DRM_MODE_PAGE_FLIP_EVENT, .user_data = (uint64_t)&event_data,};ioctl(fd, DRM_IOCTL_MODE_PAGE_FLIP, &flip);// 等 DRM 事件 → 翻转完成,可以开始绘制下一帧poll(fds, 1, -1);read(fd, &drm_event, sizeof(drm_event));八、总结:新旧框架对比
| 对比项 | fbdev | DRM/KMS |
|---|---|---|
| 设备节点 | /dev/fb0 | /dev/dri/card0 |
| 多显示器 | 多个 /dev/fbX | Connector 统一管理 |
| 双缓冲/翻页 | FBIOPAN_DISPLAY(无 vsync) | MODE_PAGE_FLIP(硬件 vsync) |
| 硬件图层 | 不支持 | Plane 支持叠加 |
| GPU 集成 | 无 | GEM/dma-buf 原生集成 |
| Atomic 更新 | 不支持 | DRM_IOCTL_MODE_ATOMIC(无闪烁) |
| 适合场景 | 终端控制台、早期启动 | 桌面、游戏、嵌入式现代显示 |
写一个屏幕 fb 驱动,本质上就是告诉内核:我的显示硬件在哪,它支持什么模式,以及怎么把内存里的像素数据送出去。DRM/KMS 框架把所有这一切抽象成了 CRTC、Encoder、Connector、Plane 四个对象——理解它们,就理解了现代 Linux 显示驱动的全部。
— END —