从零写OS内核|347:显示器——从 Framebuffer 到 GPU

摘要:显示器是计算机最直观的输出设备,但背后的 timing、分辨率、帧缓冲、和 GPU 渲染流程鲜有人深究。本篇文章从 CRT timing 原理讲起,详解 DRM/KMS 架构、modetest/xrandr 实战命令,带你理解从像素写入到光子输出的完整链路。


1. CRT Timing:为什么是这些奇怪的分辨率?

1.1 扫描的物理原理

CRT(阴极射线管)显示器通过电子束从左到右、从上到下扫描荧光屏。电子束扫完一行后需要回扫(horizontal retrace),扫完整屏后需要垂直回扫(vertical retrace)。这些回扫时间导致了一堆”边缘”像素不可见。

Bash
←────────────── Visible ──────────────→←─ Front ──→←─ Sync ──→←─ Back ──→
                                  HFP        HS      HBP
 ─────────────────────────────────────────────────────────────────────────────→
 │                               │         │        │                        │
 │   Pixel 0    Pixel 1   ... Pixel N-1    |        |                        │
 │   ←───────── One scanline ──────────→ | ← HSync → |                        │
 │                                                              (下一行)        │
 ──────────────────────────────────────────────────────────────────────────────→↓
                              ↑ Horizontal Retrace (回扫)

核心参数(以 1920×1080@60Hz 为例):

参数 说明
像素时钟 (Pixel Clock) 148.5 MHz 每秒发送 148.5M 个像素
水平 Total 2200 visible 1920 + front 88 + sync 44 + back 148
垂直 Total 1125 visible 1080 + front 4 + sync 5 + back 36
刷新率 60 Hz 148.5M / (2200 × 1125) ≈ 60Hz

1.2 同步与极性

HSync 和 VSync 有极性(正/负)。常见分辨率的 timing 遵循 VESA 标准:

C
// VESA DMT (Display Monitor Timing) 标准 1920x1080@60
static const struct display_timing1920x1080_60 = {
    .pixel_clock_khz = 148500,
    .h_active = 1920,
    .h_blanking = 440,      // front_porch + sync + back_porch
    .v_active = 1080,
    .v_blanking = 45,       // front_porch + sync + back_porch
    .h_sync_offset = 88,    // front_porch
    .h_sync_width = 44,     // sync pulse width
    .v_sync_offset = 4,     // front_porch
    .v_sync_width = 5,      // sync pulse width
    .h_sync_polarity = 1,   // positive
    .v_sync_polarity = 1,   // positive
};

1.3 为什么固定这些分辨率?

这些都是 VESA 标准定义的,GPU 固件和显示器 EDID 会自动协商出双方都支持的 timing。Linux 会读取显示器的 EDID 来获取支持的最佳模式。

Bash
     GPU                      Monitor
      │                          │
      │──── DDC (I2C) 读取 EDID ──► │
      │◄── 支持的 mode 列表 ─────── │
      │                          │
      │── 选择 1920x1080@60 ────► │
      │◄── ACK ─────────────────── │
      │                          │
      │  Pixel Clock = 148.5MHz  │
      │  HSync / VSync 信号输出   │
      ▼                          ▼

2. Framebuffer:像素的”画布”

2.1 帧缓冲结构

Framebuffer 是在内存中分配的一块连续区域,用来存储一帧图像的像素数据。常见格式:

C
// 32bpp RGBA 格式
// 地址计算: offset = y * pitch + x * bytes_per_pixel
struct fb_fix_screeninfo {
    char id[16];              // "mtrr" 或驱动名
    unsigned long smem_start; // 物理地址
    unsigned int smem_len;    // 帧缓冲大小 (bytes)
    unsigned int line_length;  // 一行字节数 (pitch)
};

// 16bpp RGB565 格式示例
// Pixel (x, y) at physical address:
//   phys = fb_va + y * line_length + x * 2
//   *(volatile uint16_t *)(phys) = (r >> 3 << 11) | (g >> 2 << 5) | (b >> 3);

2.2 查看当前 framebuffer 配置

Bash
$ cat /dev/fb0 2>/dev/null && echo "Framebuffer 存在" || echo "无Framebuffer"
Framebuffer 存在

$ fbset
mode "1920x1080"
    dotclock 148.50
    htimings 1920 2008 2052 2200
    vtimings 1080 1084 1089 1125
    flags    +vsync
    rgba 8/16,8/8,8/0,0/0
endmode

$ cat /sys/class/graphics/fb0/virtual_size
1920,1080

2.3 直接写像素到 /dev/fb0

Bash
# 安装 ImageMagick
$ convert -size 100x100 xc:red /tmp/red.png

# 显示图片到 framebuffer(需要 root)
$ sudo fbi -d /dev/fb0 -T 1 /tmp/red.png

或者用 Python 直接写像素:

Python
import struct, os

fb = os.open("/dev/fb0", os.O_RDWR)
# 获取屏幕信息
fi = struct.unpack("I", os.read(fb, 4))[0]  # smem_len
# 假设 1920x1080x4 = 8MB,假设 line_length = 7680

# 写一个红色像素到 (100, 100)
# RGBA: R=255, G=0, B=0, A=255
pixel = struct.pack("I", 0xFF0000FF)  # 小端序
os.lseek(fb, 100 * 7680 + 100 * 4, os.SEEK_SET)
os.write(fb, pixel)
os.close(fb)

3. DRM/KMS:现代 Linux 图形栈

3.1 DRM 子系统架构

DRM(Direct Rendering Manager)最初是为 3D 加速设计的,现在已经演变成 Linux 图形核心子系统:

Bash
用户空间
├── Mesa (OpenGL/Vulkan)
├── libdrm (DRM ioctl 封装)
└── X11/Wayland (合成器)
    ↓ DRM ioctl
内核 DRM Core (drm.ko)
├── DRM KMS (Kernel Mode Setting)
├── GEM/TTM (显存管理)
└── 驱动层 (i915, amdgpu, nouveau, virtio-gpu...)
    ↓
GPU 硬件

3.2 KMS 核心概念:CRTC、Encoder、Connector、Plane

Bash
      ┌────────┐
      │  CRTC  │  (扫描产生器,产生像素时钟和时序)
      └────┬───┘
           │  1:1 关系
      ┌────▼───┐
      │ Encoder│  (编码:LVDS/HDMI/DP/eDP/TV)
      └───┬───┘
         │  1:N 关系
    ┌────▼────▼───┐
    │ Connector  │  (物理接口:HDMI座、DP座、eDP排线)
    └────────────┘

Plane(平面):现代 KMS 支持多层合成(主平面 + 覆盖平面),每个 plane 可以独立缩放和定位。

3.3 模式设置(Mode Setting)流程

C
// 1. 打开 DRM 设备
int fd = open("/dev/dri/card0", O_RDWR);
drmModeRes *res = drmModeGetResources(fd);

// 2. 找到 connector
drmModeConnector *conn = drmModeGetConnector(fd, res->connectors[0]);

// 3. 选择支持的最佳模式
drmModeModeInfo *mode = &conn->modes[0]; // 通常选最高分辨率

// 4. 获取 CRTC 和 Encoder
drmModeCrtc *crtc = drmModeGetCrtc(fd, res->crtcs[0]);

// 5. 设置模式 (mode setting!)
drmModeSetCrtc(fd, crtc->crtc_id, 
                fb_id,    // framebuffer id
                0, 0,     // x, y
                &conn->connector_id, 1,  // 关联 connector
                mode);    // timing 参数

// 6. 创建 GBM 缓冲区 (用于 Mesa)
struct gbm_device *gbm = gbm_create_device(fd);
struct gbm_surface *surface = gbm_surface_create(gbm, 
    mode->hdisplay, mode->vdisplay,
    GBM_FORMAT_XRGB8888,
    GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);

4. 实战:modetest 和 xrandr

4.1 modetest(DRM 调试工具)

modetest 是 libdrm 自带的测试工具,可以列出所有 DRM 设备、显示模式、测试页面填充:

Bash
$ modetest -M i915
Connector 0: 1920x1080+0+0 "HDMI-A-1"
  Encoder 0: "HDMI 1"
  CRTC 0: 1920x1080@60.0
  CRTC 1: unavailable

$ modetest -M i915 -c  # 列出 connectors
Connectors:
  id      encoder status          name            size (mm)       modes
  35      34      connected       HDMI-A-1        530x300        21
  37      0       disconnected    DP-1            0x0            0

$ modetest -M i915 -p  # 列出 planes
Planes:
  id      crtc    type    possible_crtcs
  27      26      primary 0x00000001
  28      0       overlay 0x00000001
  30      0       overlay 0x00000002

4.2 用 modetest 测试页面填充

Bash
$ modetest -M i915 -s 35@26:1920x1080  # 在 connector 35 的 CRTC 26 上显示 1920x1080
# 屏幕会显示彩色条纹或渐变

$ modetest -M i915 -P 27@26:1920x1080@0,0  # 设置 primary plane
# 在 plane 27 上测试

4.3 xrandr(X11 模式管理)

Bash
$ xrandr --listmonitors
Monitors: 1
 0: +HDMI-1 1920x1080+0+0 530x300mm

$ xrandr --output HDMI-1 --mode 1920x1080 --rate 60
# 设置 1080p 60Hz

$ xrandr --output HDMI-1 --mode 1280x720 --rate 60
# 切换到 720p

$ xrandr --output HDMI-1 --primary
# 设置为主显示器

4.4 读取当前 display 配置

Bash
$ cat /sys/class/drm/card0-HDMI-A-1/status
connected

$ cat /sys/class/drm/card0-HDMI-A-1/modes
1920x1080
1920x1080i
1600x900
1280x720
...

$ cat /sys/class/drm/card0-HDMI-A-1/edid | xxd | head -20
# EDID 原始数据(显示器 capabilities)

5. 从像素到屏幕:渲染链路

Bash
应用 (Firefox / gimp / 游戏)
    ↓ glReadPixels / Vulkan / OpenGL
Mesa (软件/硬件渲染器)
    ↓
DRM/GEM (显存分配)
    ↓
KMS (CRTC scanout)
    ↓
Encoder (TMDS/LVDS/DP 编码)
    ↓
Connector (物理输出)
    ↓
显示器 (DDC/EDID 读取)
    ↓
Panel/STMLCD/CRT (电光转换)
    ↓
光子 → 视网膜

6. wandos 对比:Framebuffer 简化实现

wandos 的显示模型更接近 BIOS 文本模式或简单的线性帧缓冲。没有完整的 DRM 子系统,但 wandos 有基础的 framebuffer 驱动——一个固定地址的线性缓冲区 + 简单的像素格式配置。wandos 的下一步目标是实现基本的 VBE (VESA BIOS Extension) 模式设置接口,这样可以在保护模式下设置高分辨率图形模式。


公众号底部

📢 关注公众号【内核栈】,获取更多 OS/内核干货

💡 本文属于「从零写OS内核」系列,欢迎回顾:

  • 篇 346:《键盘——按键是怎么变成字符的》
  • 篇 345:《USB 协议》
  • 篇 344:《时钟与复位》

🔗 回复「显示」获取 VESA Timing 标准参考表


作者:内核栈 | 原创出品
本文永久链接:https://kernelstack.dev/347-display

最后修改: 2024年7月15日

作者

评论

发表评论

您的邮箱地址不会被公开。