起因

显卡实战篇(《从GOP帧缓冲接管显示输出》)解决的是"接管帧缓冲自己画像素",但顺着往下还有一串问题躲不掉:高分辨率是靠什么实现的?系统怎么"自动适配"显示器?显示器支持哪些模式从哪查?显卡的显存容量、品牌型号从哪读?——“从哪查"这问的标准答案是 EDID,1994 年 VESA 定的规矩:让显示器在 DDC 线上自报家门,交出 128 字节的"我能干什么”(后来扩到 256 字节 E-EDID)。动机很朴素:VGA 时代显示器是哑巴,刷新率给高了轻则黑屏重则伤机器,显卡只能瞎猜。这篇把这些信息问题逐个落到可操作的层面——涉及三块东西:EDID、模式协商、PCI 配置空间。

一、先分清三个角色

“显示"这件事是三方接力,各自约束一环:

  • GPU 渲染核心:把像素算出来写进显存(我们的系统里它闲着,像素我们自己写帧缓冲)
  • 显示引擎(Display Engine):按固定时序逐行扫描帧缓冲,把像素流编码成 HDMI/DP/VGA 信号发出去
  • 显示器:解码信号点亮面板

所以"这台机器支持什么分辨率"从来不是单方面的事,而是三方约束的交集:显示引擎能产生的时序 × 连接线的带宽上限 × 显示器面板能吃的模式。高分辨率的本质:更高的像素时钟 + 更大的显存占用——1920×1080@32bpp 一帧约 8MB,4K 是 33MB,显存(或集显情况下划出的内存)够不够是硬约束。线缆带宽是另一道闸:HDMI 1.4 的 10.2Gbps 撑到 4K30,4K60 要 HDMI 2.0 的 18Gbps——线不够格,时序参数再对也出不了画。

二、“一个显示模式"到底是什么

所谓模式,不是"宽×高"这么简单,是一组扫描时序参数:像素时钟频率 + 水平/垂直的有效像素数、消隐期(blanking)、同步脉冲位置——显示引擎拿这组参数编排扫描节拍。以 1080p60 为例:像素时钟 148.5MHz,每行有效 1920 像素外加消隐共 2200,每帧行数外加消隐共 1125 行,2200×1125×60Hz ≈ 148.5MHz。切换分辨率 = 重写显示引擎的这组时序寄存器。理解了这一点,“显示模式"就从玄学变成了寄存器值。

三、EDID:显示器的身份证

显示器怎么告诉主机"我是谁、我支持什么”?答案是一块叫 EDID 的只读数据,存在显示器内部的 EEPROM 里,通过DDC 通道(Display Data Channel)访问——本质就是一根 I²C 总线,VGA/HDMI 里捎带的 SCL/SDA 两根线,DisplayPort 则走 AUX 通道,器件地址 0x50。主机"自动适配显示器”,第一步就是通过 DDC 把这 128 字节读出来。

EDID 1.x 布局(128 字节):

偏移 字段 说明
0x00 00 FF FF FF FF FF FF 00
0x08 厂商 ID 3 个 5 位压缩的大写字母(如 SAM = 三星),16 位大端
0x0A 产品码 16 位,厂商自定型号编号
0x0C 序列号 + 0x10 制造周/年 出厂身份
0x12 EDID 版本 1.3/1.4
0x14 视频输入定义 数字(HDMI/DP)/模拟(VGA)位标记
0x15 最大尺寸 厘米,对角线兜底信息
0x23 建立时序位图 位标记:640×480@60/72/75/85 等 VGA 经典模式支持与否
0x25 标准时序 8 组 2 字节编码
0x36 起 详细时序描述符(DTD)×4 每个 18 字节——重头戏
0x7E 扩展块数 E-EDID 最多 256 字节
0x7F 校验和 前 128 字节累加 ≡ 0(mod 256),验证读数完好

DTD(18 字节)才是模式清单的核心:像素时钟(2 字节,单位 10kHz)、水平有效/消隐、垂直有效/消隐、同步宽度和偏移、物理尺寸(mm)……第一个 DTD 惯例是显示器的原生模式(preferred timing)——面板的物理分辨率,点对点最清晰。所谓"自动适配"的技术真相:读 EDID → 取首选 DTD → 换算成显示引擎的时序寄存器值。

读回的 128 字节先过两道验证再信:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn edid_ok(e: &[u8; 128]) -> bool {
    e[..8] == [0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0]          // 头
        && e.iter().fold(0u8, |s, &b| s.wrapping_add(b)) == 0    // 0x7F 校验和
}

fn edid_vendor(e: &[u8; 128]) -> String {   // 0x08-0x09 的 16 位大端 → 3 个 5 位字母
    let id = u16::from_be_bytes([e[8], e[9]]);
    let mut s = String::new();
    for shift in [10, 5, 0] {
        s.push((b'@' + ((id >> shift) & 0x1F) as u8) as char);
    }
    s
}

厂商 ID 的编码是每字母 5 位(1=A……26=Z),两个字节装三个字母——SAM 三星、DEL 戴尔、AOC 冠捷,现场一眼认出面板是谁家的。校验和不过的 EDID 常见于劣质转接线,别硬解,按 1024×768 安全模式兜底。

四、自动适配在我们系统里是怎么发生的

分层看这段流程归谁做:

  • 固件已经做了一遍:UEFI 的 DXE 阶段,GOP 驱动读 EDID、协商模式、初始化显示引擎——所以移交清单里的帧缓冲五元组,就是"协商已完成"的结果。我们的"开机选原生分辨率然后写死"策略之所以成立,是因为脏活累活固件干完了
  • QueryMode 枚举的是固件准备好的模式清单gop.mode.max_mode 个,逐个 QueryMode(i) 看 info),典型内容是原生模式 + 几个 VESA 安全降级模式——注意这是协商后的可用清单,不是显示器全部能力。全能力要看 EDID 原文
  • 什么时候需要自己下场:固件清单满足不了(比如固件只给了 1024×768 但面板是 1080p)、或者要多屏/热插拔——那就得自己走一遍"DDC 读 EDID → 解析 DTD → 编显示引擎时序寄存器”。最后一步要查 Intel 显示引擎的寄存器文档(Intel 有公开的显示编程文档可查),这属于后续扩展项:工控单屏固定场景,基础篇吃固件现成饭,够用且稳

QEMU 实验路径:-device VGA,edid=on 生成虚拟 EDID,-device VGA,xres=1920,yres=1080 直接指定模式,配合我们内核把 EDID 头和厂商 ID 打到串口,验读数正确。

五、显存怎么查

两条路径,按显卡形态分:

  • Intel 集显:没有独立显存,是共享系统内存(UMA)——固件开机从物理内存里"偷"一块 stolen memory 划给显示用,frame_buffer_base 指向的就是它。这块的踪迹在移交的内存映射里是保留类型描述符,不在可用内存之列(显卡篇第三节的排除清单就是为它准备的)——所以集显平台问"显存多大"其实问错了对象,要问的是帧缓冲需求:pitch × height × 4,1080p 约 8MB,stolen memory 一般 32MB 以上,绰绰有余
  • 独显(以后扩展用):显存映射在 BAR 里,容量 = PCIe 篇"写全 1 探测"读回的 BAR 尺寸——那套 BAR 探测代码在显示这就是显存容量探测代码,一块砖两处用

六、品牌型号信息:PCI 配置空间的认亲

显卡的品牌、型号、修订,全部写在 PCIe 配置空间里,枚举篇的偏移表直接查:

偏移 字段 说明
0x00 Vendor ID 0x8086 = Intel,0x10DE = NVIDIA,0x1002/0x1022 = AMD
0x02 Device ID 具体型号编号,对照厂商设备表查名字
0x08 Revision + Class Code 类码 0x03 显示控制器,子类 0x00 VGA 兼容 / 0x02 显示其它
0x40 子系统厂商/设备 ID 整卡厂(OEM)信息

配合 EDID 里的显示器厂商 ID(SAM 三星、DEL 戴尔、AOC……),整条显示链的"户口本"都能打印到工控系统的硬件信息页:显卡 vendor/device + 显示器厂商/产品码/原生模式 + EDID 物理尺寸——现场排障第一步"知道自己插的是啥"就有了。

七、汇总一张表

你想知道 去哪查 我们的系统怎么拿
当前分辨率/帧缓冲 GOP ModeInfo 移交清单五元组(固件已协商)
显示器支持的全部模式 EDID 的 DTD 列表 DDC/AUX 读 0x50 器件(需要时自己实现,QEMU 可验)
显示器原生模式 EDID 首选 DTD 同上
显示器厂商/型号 EDID 0x08-0x0B 同上
显存容量 集显=UMA(内存映射);独显=BAR 尺寸 内存映射 / PCIe 枚举现成代码
显卡品牌型号 PCI 配置空间 0x00/0x02/0x40 PCIe 枚举清单直接认亲
切换分辨率 显示引擎时序寄存器重编程 基础篇不做(固定原生模式),扩展项

总结

显示子系统的情报体系三层:EDID 是显示器的自述(128 字节、DDC 通道 0x50 器件、DTD 列表是模式清单、首个 DTD 即原生模式,读回先验头和校验和);模式协商是三方交集的取舍(固件已替我们完成,GOP 清单是协商结果不是全部能力);身份信息在 PCI 配置空间(显卡)和 EDID 头(显示器)。显存这件事,集显平台是 UMA + stolen memory,问帧缓冲够不够就行;独显平台就是 BAR 尺寸,枚举代码直接复用。到这里,显示这条线从"能画像素"到"知道自己为什么能画"就完整了。

参考链接