屏幕 Framebuffer 驱动

屏幕Framebuffer驱动
\n

从 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;     // R

fbdev 的痛点

  • 单缓冲区:需用 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 —

 

暂无评论,快来发表第一条评论吧!

📮 需求咨询