从零写OS内核|347:显示器——从 Framebuffer 到 GPU
摘要:显示器是计算机最直观的输出设备,但背后的 timing、分辨率、帧缓冲、和 GPU 渲染流程鲜有人深究。本篇文章从 CRT timing 原理讲起,详解 DRM/KMS 架构、modetest/xrandr 实战命令,带你理解从像素写入到光子输出的完整链路。
1. CRT Timing:为什么是这些奇怪的分辨率?
1.1 扫描的物理原理
CRT(阴极射线管)显示器通过电子束从左到右、从上到下扫描荧光屏。电子束扫完一行后需要回扫(horizontal retrace),扫完整屏后需要垂直回扫(vertical retrace)。这些回扫时间导致了一堆”边缘”像素不可见。
←────────────── 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 标准:
// 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 来获取支持的最佳模式。
GPU Monitor
│ │
│──── DDC (I2C) 读取 EDID ──► │
│◄── 支持的 mode 列表 ─────── │
│ │
│── 选择 1920x1080@60 ────► │
│◄── ACK ─────────────────── │
│ │
│ Pixel Clock = 148.5MHz │
│ HSync / VSync 信号输出 │
▼ ▼2. Framebuffer:像素的”画布”
2.1 帧缓冲结构
Framebuffer 是在内存中分配的一块连续区域,用来存储一帧图像的像素数据。常见格式:
// 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 配置
$ 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,10802.3 直接写像素到 /dev/fb0
# 安装 ImageMagick
$ convert -size 100x100 xc:red /tmp/red.png
# 显示图片到 framebuffer(需要 root)
$ sudo fbi -d /dev/fb0 -T 1 /tmp/red.png或者用 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 图形核心子系统:
用户空间
├── 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
┌────────┐
│ CRTC │ (扫描产生器,产生像素时钟和时序)
└────┬───┘
│ 1:1 关系
┌────▼───┐
│ Encoder│ (编码:LVDS/HDMI/DP/eDP/TV)
└───┬───┘
│ 1:N 关系
┌────▼────▼───┐
│ Connector │ (物理接口:HDMI座、DP座、eDP排线)
└────────────┘Plane(平面):现代 KMS 支持多层合成(主平面 + 覆盖平面),每个 plane 可以独立缩放和定位。
3.3 模式设置(Mode Setting)流程
// 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 设备、显示模式、测试页面填充:
$ 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 0x000000024.2 用 modetest 测试页面填充
$ 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 模式管理)
$ 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 配置
$ 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. 从像素到屏幕:渲染链路
应用 (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
评论