资讯动态

Windows下EDID解析:从注册表到CEA-861扩展块

发布时间:2026/10/26 6:27:54 来源:尧图企业网站定制
简介面向Windows平台C/MFC开发者与硬件诊断爱好者的VC示例源码聚焦从系统注册表读取显示器EDID并解析设备参数。项目以HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Video下的显示设备子键为入口演示RegOpenKeyEx、RegQueryValueEx等API读取“EDID”二进制值再按128字节结构拆分固定头部、基本显示信息、附加显示信息与校验块提取制造商、型号、物理尺寸、色深、刷新率及最大水平/垂直分辨率等规格并以可读形式呈现帮助理解Windows显示设备信息的组织方式。压缩包共21个文件约2.38MB包含6个h头文件、5个cpp源文件以及vcproj与sln工程文件、rc资源脚本、ico图标、txt说明和aps/ncb/suo等VC辅助文件可直接打开工程对照学习MFC对话框与注册表封装代码。已有1713人学习适合借此掌握注册表访问、二进制数据转换与EDID字段解析并用于系统监测、硬件诊断或显示器配置管理等场景。1. 从一块「不认识自己」的屏幕说起EDID 为什么是显示器信息的源头把一台陌生的显示器插到 Windows 机器上不装任何驱动系统就能报出它的型号、支持的分辨率、甚至生产年份。多数人以为系统「问」了显示器实际上不是——系统只是从一条两线制的 I2C 总线上把显示器内部一颗 EEPROM 里的 128 字节原样读出来再缓存进注册表。这 128 字节就是 EDIDExtended Display Identification Data。在 VC 项目里做显示器信息采集绕不开的就是从 Monitor 注册表里把这 128 字节取出来再按 EDID 规范逐字段解析。它之所以值得搞懂是因为一切「免驱」的显示能力协商都压在这段二进制上分辨率上限、刷新率、色域、物理尺寸、甚至显示器名字全是从里面解出来的。2. DDC 与 I2C128 字节是怎么从显示器搬进注册表的2.1 DDC 通道与 I2C 地址 0x50EDID 存放在显示器侧的一颗 EEPROM 里访问通道叫 DDCDisplay Data Channel。DDC 是 VESA 定义的一组协议族底层跑在 I2C 上。VGA 时代用 DDC2BDVI、HDMI 沿用了同一套语义DisplayPort 换成了 AUX 通道但「读一块固定地址的 EEPROM」这件事没变。I2C 是两根线SCL 时钟、SDA 数据。主机是 master显示器 EEPROM 是 slave7 位地址固定为0x50写成 8 位就是写0xA0、读0xA1。读 EDID 的过程就是一个标准的 I2C EEPROM 随机读序列主机发 START发0xA0地址 写发0x00字地址EDID 起始偏移发重复 START发0xA1地址 读连续读 128 字节每字节主机回 ACK最后一字节回 NACK发 STOP这个序列跟普通 24Cxx EEPROM 读一块数据没有区别VESA 只是把「偏移 0x00 开始读 128 字节」这个用法规范化了。注意有些 EEPROM 页大小就是 128 字节连续读跨页会回卷到页首。Windows 的显示驱动在读取时会自己处理跨页但如果是在嵌入式侧或自研板卡上直接操作 I2C 控制器就要在 128 字节边界重新设置字地址否则读回来的后半段是前半段的重复。2.2 从 HPD 到注册表Windows 的 PnP 缓存流程显示器插入或上电时接口上的 HPDHot Plug Detect引脚电平会变化。端口驱动或显示驱动收到这个信号后发起一次 DDC 读取拿到 128 字节基础块校验通过后从中解出厂商 ID、产品代码和序列号交给 PnP 管理器创建设备节点。最终落盘的位置是注册表HKLM\SYSTEM\CurrentControlSet\Enum\DISPLAY\PnP ID\实例ID\Device Parameters\EDID拆开看每一级DISPLAY是设备类对应GUID_DEVCLASS_MONITORPnP ID由 EDID 里的三字母厂商 ID 加四位产品代码拼成比如三个字母加1234实例ID通常带总线路径和 UID形如52a3b4c5d0UID4352格式跟 Windows 版本和总线类型有关不能硬编码Device Parameters下的EDID是REG_BINARY内容就是原始字节不做任何转码这条链路的关键在于注册表里存的是「原始 EDID」不是「解析后的字段」。Windows 自己解析一遍用于 PnP 匹配但把原始数据留了下来等于给上层应用留了一个可以自己重新解释的口子。VC 项目读注册表的价值就在这里——拿到的是一手数据解析逻辑完全可控。2.3 为什么注册表里的 EDID 可能和显示器对不上这里有几个反复踩过的坑。KVM 切换器是第一大类。不少 KVM 会自己伪造一份 EDID 给主机或者透传失败时返回固定值结果系统读到的是 KVM 的「假身份」跟实际接的显示器对不上。扩展坞也类似部分扩展坞会缓存 EDID显示器换到另一个口后读到旧值。系统缓存是第二类。Windows 有一套显示器缓存机制历史连接过的设备节点会保留在Enum\DISPLAY下不删除。如果只是遍历这个键会把几年前插过的显示器也枚举出来。DIGCF_PRESENT标志就是用来过滤掉这些不在线设备的。第三类是显示器固件本身的问题。个别型号 EDID 更新后系统没有重新读取必须拔插一次或重启才刷新睡眠唤醒后 DDC 读取失败也会沿用旧缓存。这属于玄学范畴工程上能做的是在解析前做校验和检查校验失败时降级而不是直接崩。3. EDID 二进制解剖128 字节里到底编码了什么3.1 头部与厂商 ID 的 5-bit 编码基础块的布局是固定的按偏移量列表最清楚偏移长度含义0-78固定头00 FF FF FF FF FF FF 008-92厂商 ID5-bit × 3大端10-112产品代码小端12-154序列号小端161制造周171制造年199018-192EDID 版本 / 修订201视频输入定义21-222水平 / 垂直尺寸cm231Gamma241特性支持位25-3410色度坐标35-373Established Timings38-5316Standard Timings8 组54-125724 个 18 字节描述符1261扩展块数量1271校验和厂商 ID 的编码是最容易被忽略的一处。三个字母 A-Z 映射到 1-26压进 15 个 bitbit14-10 是第一字母bit9-5 是第二字母bit4-0 是第三字母bit15 保留为 0。比如三个字母分别是 1、2、3拼出来就是0b0_00001_00010_00011即0x0843在 EDID 里按大端存成08 43。// 从 EDID 第 8、9 字节解出三字母 PnP 厂商 ID // EDID 里是大端存储x86 上不能直接按 WORD 读 void DecodeManufacturerId(const BYTE* edid, char out[4]) { WORD raw (WORD)((edid[8] 8) | edid[9]); // 手动拼大端 out[0] (char)(((raw 10) 0x1F) A - 1); out[1] (char)(((raw 5) 0x1F) A - 1); out[2] (char)((raw 0x1F) A - 1); out[3] \0; }这段代码里raw 10取的是 bit14-10raw 5取 bit9-5最低 5 位直接掩码。三个字母都减 1 再加A是因为编码从 1 开始而不是 0。参数没有别的入口edid必须至少 128 字节且已经通过头部校验。3.2 基本参数与标准时序制造日期两字节周范围 1-53年要加 1990。这两个字段在很多工具里显示成「第 32 周 / 2021 年」这种形式比时间戳更贴近出厂信息。第 20 字节的视频输入定义决定了后面怎么解释bit7 为 1 表示数字输入为 0 表示模拟。数字输入时bit6-4 是位深0016bit、0108bit、01110bitbit3-0 是接口类型HDMI-a、HDMI-b、MDDI、DisplayPort 等。模拟输入时这几个位的含义完全不同混着读会得到荒谬结果。第 21、22 字节是可见区域的物理尺寸单位厘米。注意这是面板可见区不是外框尺寸不能用它去反推显示器的外形。第 24 字节的特性位里有 DPMS 支持、首选时序类型、sRGB 默认色域、持续频率等标志做显示能力判断时会用到。Established Timings 是三字节位图覆盖 640×48060、800×60060、1024×76860 这类老分辨率。Standard Timings 是 8 组、每组 2 字节编码方式是水平像素 (byte1 31) * 8范围 248 到 2288byte2 的 bit7-6 是宽高比0016:10014:3105:41116:9byte2 的 bit5-0 是刷新率减 60所以一组0x81 0x80解出来是(0x81 31) * 8 1216像素宽、16:10、60Hz对应 1280×800 这一档。字段都是无符号整数没有负数偏移。3.3 显示器描述符名字、序列号和范围限制从偏移 54 开始的 72 字节被切成 4 个 18 字节块每个块有两种可能Detailed Timing DescriptorDTD或者 Monitor Descriptor。判断依据在前两个字节如果0x0000就是 Monitor Descriptor否则是 DTD前两字节是像素时钟单位 10kHz小端。Monitor Descriptor 的第 3 字节是 tag0xFC显示器名称0xFF序列号字符串0xFE任意 ASCII 字符串0xFD范围限制最小/最大垂直频率、水平频率、最大像素时钟0xFB、0xFA附加标准时序名称描述符的内容区从第 5 字节开始13 字节 ASCII通常以0x0A换行结尾不足的部分补空格。解析时要同时处理「换行截断」和「尾部空格裁剪」两种情况否则拿到的名字后面会拖一串空白。DTD 的 18 字节里塞了像素时钟、水平/垂直有效像素、消隐、前沿、同步脉宽、图像尺寸和标志位。水平方向的有效像素低 8 位在第 2 字节高 4 位在第 4 字节的高半字节垂直方向同理低 8 位在第 5 字节高 4 位在第 7 字节。这种高低位拆开存的写法贯穿整个 DTD逐字段读的时候要非常小心位偏移。3.4 扩展块与 CEA-861第 126 字节是扩展块数量。每个扩展块也是 128 字节紧跟在基础块后面。扩展块的第 0 字节是 tagtag类型0x02CEA-8610x10VTB-EXT0x40DI-EXT0x50LS-EXT0x70DisplayID0xF0块映射CEA-861 是现代显示器最关键的一块。它的第 2 字节是 DTD 起始偏移第 3 字节是标志位underscan、音频、YUV 等从第 4 字节开始是一串「数据块」视频数据块VDB、音频数据块ADB、扬声器分配块、VESA DTC 块以及 HDMI 特有的 HDR 静态元数据块tag0x06、HDMI VSDBtag0x03。4K60、HDR、音频回传这些能力基本都在 CEA 扩展块的数据块里。只解析基础块的程序在一台 4K 显示器上会得到「最大 1920×1080」的错误结论——基础块里的 Standard Timings 编码范围根本装不下 4K 的像素数。4. VC 解析实现从注册表读取到结构体映射的完整链路4.1 用 SetupAPI 定位在线显示器直接遍历Enum\DISPLAY能拿到数据但会混进历史缓存。更规范的做法是走 SetupAPI用GUID_DEVCLASS_MONITOR过滤设备类用DIGCF_PRESENT只取当前在线的。#include windows.h #include setupapi.h #include devguid.h #include vector #pragma comment(lib, setupapi.lib) // 枚举当前在线的显示器逐个读 Device Parameters\EDID void EnumMonitorEdid() { HDEVINFO devInfo SetupDiGetClassDevs( GUID_DEVCLASS_MONITOR, // 显示器设备类 NULL, NULL, DIGCF_PRESENT); // 只要当前存在的过滤历史缓存 if (devInfo INVALID_HANDLE_VALUE) return; SP_DEVINFO_DATA devData { sizeof(SP_DEVINFO_DATA) }; for (DWORD i 0; SetupDiEnumDeviceInfo(devInfo, i, devData); i) { // 打开该设备的 Device Parameters 键 HKEY hKey SetupDiOpenDevRegKey( devInfo, devData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ); if (hKey INVALID_HANDLE_VALUE) continue; DWORD type 0, size 0; // 第一次调用只查大小避免缓冲区不够 if (RegQueryValueExA(hKey, EDID, NULL, type, NULL, size) ERROR_SUCCESS type REG_BINARY size 128) { std::vectorBYTE edid(size); if (RegQueryValueExA(hKey, EDID, NULL, type, edid.data(), size) ERROR_SUCCESS) { // 到这里 edid 里就是原始字节交给后面的解析函数 } } RegCloseKey(hKey); } SetupDiDestroyDeviceInfoList(devInfo); }GUID_DEVCLASS_MONITOR定义在devguid.h对应的是{4d36e96e-e325-11ce-bfc1-08002be10318}。SetupDiOpenDevRegKey的DICS_FLAG_GLOBAL表示打开全局硬件配置而不是当前用户配置DIREG_DEV表示目标键是Device Parameters。如果换成DIREG_DRV打开的是Driver键里面没有EDID值——这是最容易写错的一处。4.2 读取二进制值与长度判断注册表值名就是EDID四个大写字母类型是REG_BINARY。大小写不敏感但工程上建议统一写大写。长度上基础块固定 128 字节带扩展块时可能是 256、384、512 甚至更多。size 128是硬性下限但真正决定能解析多少的是edid[126]这个扩展块数量字段// 按扩展块数量校验实际数据是否完整 bool CheckEdidLength(const std::vectorBYTE edid) { if (edid.size() 128) return false; int extCount edid[126]; size_t need 128 (size_t)extCount * 128; return edid.size() need; }这段逻辑的意义在于如果驱动只写入了 128 字节而edid[126]声称有 2 个扩展块说明数据被截断了后面按扩展块解析会越界。宁可在这里判定数据不完整也不要让后面的循环去踩内存。4.3 描述符与字符串解析4 个描述符块从偏移 54 开始每块 18 字节。找显示器名称就是遍历这四块匹配 tag 为0xFC的那一块。// 从 4 个描述符块中提取显示器名称tag 0xFC bool ParseMonitorName(const BYTE* edid, char out[14]) { for (int i 0; i 4; i) { const BYTE* d edid 54 i * 18; // Monitor Descriptor 特征前两字节为 0第 3 字节为 0第 4 字节是 tag if (d[0] 0 d[1] 0 d[2] 0 d[3] 0xFC) { int n 0; for (int j 5; j 18 n 13; j) { if (d[j] 0x0A || d[j] 0x00) break; // 换行或终止符 out[n] (char)d[j]; } while (n 0 out[n - 1] ) --n; // 裁掉尾部空格填充 out[n] \0; return true; } } return false; }out缓冲区至少 14 字节因为内容区最长 13 字符再加结束符。j从 5 开始是因为前 5 字节是标识区。如果四块里都没有0xFC函数返回false调用方应该准备一个兜底名称比如用 PnP ID 拼一个而不是显示空字符串。4.4 头部与校验和验证EDID 的校验规则很简单基础块 128 字节求和低 8 位必须为 0每个扩展块同理各自 128 字节独立校验。// 校验 EDID 头部魔数和每个 128 字节块的校验和 bool ValidateEdid(const BYTE* edid, size_t size) { if (size 128) return false; // 头部必须是 00 FF FF FF FF FF FF 00 if (edid[0] ! 0x00 || edid[7] ! 0x00) return false; for (int i 1; i 7; i) if (edid[i] ! 0xFF) return false; // 基础块校验和 int sum 0; for (int i 0; i 128; i) sum edid[i]; if ((sum 0xFF) ! 0) return false; // 逐个扩展块校验 int extCount edid[126]; if (128 (size_t)extCount * 128 size) return false; for (int e 0; e extCount; e) { const BYTE* blk edid 128 e * 128; int s 0; for (int i 0; i 128; i) s blk[i]; if ((s 0xFF) ! 0) return false; } return true; }sum 0xFF 0是 EDID 校验的定义不是「接近 0」也不是「等于 256」必须整除。扩展块数量字段在第 126 字节这是解析前必须读的元信息不读它就没法判断数据边界。5. 避坑与排查EDID 解析最容易翻车的五个点5.1 只读了 128 字节4K 信息全丢现象一台支持 4K60 和 HDR 的显示器解析出来的最大分辨率却只有 1920×1080。原因只处理了基础块。基础块里的 Standard Timings 编码范围装不下 3840 这种像素数4K 时序和 HDR 元数据全在 CEA-861 扩展块里。解决读完基础块后检查edid[126]的扩展块数量按 128 字节一块继续读遇到 tag 为0x02的块再解析 VDB 和 HDR 数据块。不做扩展块解析就不要声称自己「读懂了 EDID」。5.2 厂商 ID 解出来是乱码现象厂商 ID 位置显示成K或类似乱码。原因字节序搞反了。edid[8]是高字节、edid[9]是低字节如果在 x86 上直接把这两个字节当WORD读小端会把它反过来。解决不要用*(WORD*)edid[8]手动(edid[8] 8) | edid[9]拼。产品代码和序列号相反是标准小端直接读即可。同一个结构里两种字节序并存是 EDID 最容易翻车的地方之一。5.3 枚举出五六台显示器实际只接了一台现象机器上只有一台显示器程序却报告了五六个设备。原因Enum\DISPLAY下保留了历史连接过的设备节点Windows 不会主动清理。直接遍历这个键拿到的是「曾经存在过」的全集。解决用SetupDiGetClassDevs加DIGCF_PRESENT或者在直接遍历注册表时结合EnumDisplayDevices做交叉验证。只按 PnP ID 匹配也不够——同型号多台显示器的 PnP ID 相同必须用实例 ID 区分。5.4 远程桌面和虚拟机里拿不到 EDID现象在远程会话或虚拟机里运行程序EDID值不存在或者长度异常。原因虚拟显示适配器没有真实的 DDC 通道EDID 要么是驱动伪造的固定值要么干脆没有。远程桌面会话下本地显示器信息根本不会传进来。解决用GetSystemMetrics(SM_REMOTESESSION)判断会话类型远程会话直接跳过 EDID 读取走 UI 提示或兜底配置。虚拟机场景下可以检测设备实例路径里是否包含虚拟总线特征作为降级依据。5.5 校验和失败但显示器工作正常现象ValidateEdid返回false但显示器显示一切正常。原因部分显示器或转接器的 EDID 校验和本身就是错的出厂就没算对还有一种是读取过程中被截断导致求和结果偏离。校验和只是完整性提示不是「数据可用」的充分条件。解决校验失败时记录一条警告继续按偏移解析能解出的字段不要直接丢弃整份数据。但如果连续多个块都校验失败就要怀疑数据源本身有问题——比如 KVM 返回的是伪造值这时候解析出来的型号可能是错的需要在 UI 上标注「数据完整性存疑」。6. 注册表、WMI 与 DXGI三条取数路径的取舍VC 项目里拿显示器信息实际有三条路可走各自的定位不一样。路径数据来源能拿到什么代价注册表Enum\DISPLAY驱动缓存的原始 EDID完整 128 字节 扩展块要自己解析、含历史缓存、需要读权限WMIroot\wmiWMI 类封装语义化字段如WmiMonitorID、WmiMonitorBasicDisplayParams字段有限、依赖 WMI 服务、单次查询较慢DXGI /EnumDisplayDevices显示驱动当前状态当前活动输出、桌面坐标、连接状态不给原始 EDID只给当前模式WMI 的WmiMonitorID里ManufacturerName、ProductCodeID、SerialNumberID、UserFriendlyName都是UInt16数组解码方式是每个元素取低字节转 ASCII这跟直接解 EDID 里的字符串是一回事只是中间多了一层封装。它的好处是字段名自带语义不用记偏移量坏处是字段覆盖不全而且某些型号上UserFriendlyName会是空数组。DXGI 的IDXGIOutput::GetDesc返回DXGI_OUTPUT_DESC里面有DeviceName、DesktopCoordinates、AttachedToDesktop、Monitor但它描述的是「当前输出状态」不是「显示器能力」。想知道这台显示器最大支持多少刷新率DXGI 只能给出驱动当前协商出来的模式列表而不是 EDID 里声明的完整能力集。所以合理的组合是用 DXGI 或EnumDisplayDevices确定「当前有哪些活动输出」用 SetupAPI 定位对应的设备实例再读注册表里的原始 EDID 做完整解析最后用 WMI 的语义字段做一次交叉校验。单走任何一条都会缺一块。我印象最深的一次是某开发者在一个模拟项目 X 里用 WMI 拿显示器名称本地测试全过到客户现场某个型号上UserFriendlyName返回空数组UI 上直接显示空白。排查半天才确认是那批显示器的 WMI 提供程序没填这个字段最后回退到注册表解析 EDID 的0xFC描述符才拿到名字。从那以后我每次做显示器信息采集都强制走一遍「头部校验 → 基础块解析 → 扩展块数量检查 → 0xFC 名称兜底」这条链路WMI 只当辅助不当主数据源。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑