ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

VC++读取注册表EDID:解析显示器厂商型号与最大分辨率

VC++读取注册表EDID:解析显示器厂商型号与最大分辨率

简介:面向VC++开发者的示例工程,演示如何通过注册表读取并解析显示器的EDID数据,从而识别显示器制造商、型号、最大分辨率等设备信息。资料包内含完整工程源码,可在Visual Studio环境中直接打开编译,较适合具备一定C++基础、希望深入Windows系统编程或从事硬件诊断相关工作的开发者参考。

包体共21个文件,压缩包约2.38MB,以头文件与源文件为主(6个.h、5个.cpp),覆盖对话框界面、注册表封装、EDID解析等模块,并附有工程配置、资源文件及说明文档,目录结构完整清晰,便于对照学习;目前已有1713人浏览学习。

项目中具体呈现了使用RegOpenKeyEx、RegQueryValueEx等API定位显示器注册表项并读取二进制EDID数据的过程,也展示了按EDID规范解析关键字段并整理为可读信息的思路,对于理解Windows视频子系统、扩展显示标识协议以及VC++二进制数据处理方法都有不错的参考意义。

1. 注册表里的EDID:显示器在操作系统中留下的身份证,你只需要学会解析

在VC里解析一台显示器的厂商、型号和最大分辨率,多数人的第一反应是去调显卡驱动接口,但真正省事的入口藏在注册表里:一段名叫EDID的REG_BINARY数据。EDID是显示器厂商烧录在EEPROM里的128字节结构,操作系统的PnP管理器在设备枚举阶段通过DDC通道把它读回来,原样缓存到注册表。之后任何用户态程序都只需要RegQueryValueEx就能拿到这份原始字节。可问题也随之而来:字节是拿到了,但协议规范里全是位域——哪个偏移是厂商ID、哪个字节是分辨率、哪里可能藏着校验和,不把机制拆开,你看到的只是一堆十六进制。

2. 为什么是注册表:设备枚举、DDC通道和EDID缓存的黑匣子

2.1 显示器不是被发现的,而是被上报的

显示器与显卡之间除了视频信号线,还有一条低速的DDC通道,物理上就是I2C总线。显示器端的EEPROM挂在I2C地址0x50上,里面就是EDID数据块。显示适配器驱动在检测到显示器热插拔或系统启动时,会通过DDC2B协议发起读序列:先写0x50地址,再从0x00开始逐字节读回EDID。这个过程涉及I2C控制器操作、时序延迟和应答重试,属于硬件驱动层的活。

关键是这一步在操作系统枚举设备时就已经做完了。显卡驱动把读回的EDID挂到显示器设备节点上,作为该设备的一个属性值。注册表路径形如HKLM\SYSTEM\CurrentControlSet\Enum\DISPLAY<显示器名><实例ID>\Device Parameters\EDID,类型是REG_BINARY。换句话说,你读注册表拿到的是驱动上一次枚举成功后的快照,不是实时去I2C总线上现读的。这既是优点也是坑:优点是开发成本极低、不需要碰显卡私有API;缺点是缓存可能陈旧,和显示器当前固件里的数据不一致。

2.2 存储结构:为什么一个显示器会对应多个EDID键

DISPLAY这个枚举分支不是一层到底的。第一层子键按显示器型号命名,通常长得很像"MON1234"这类由驱动自动生成的ID;第二层才是实例ID,因为同一型号可能接了两台,或者同一个显示器在多显卡环境下被不同驱动枚举过。EDID值就挂在第二层子键下的Device Parameters里。所以代码里必须做两层遍历,而不是写死某个键名。

还有个细节经常被忽略:EDID值的大小不固定。基础块固定128字节,但现代4K、HDR显示器会在第126个字节声明扩展块数量,常见的是1或2,于是注册表里的值实际是256或384字节。只按128字节读,后面那些包含VIC时序和HDR元数据的扩展块全部丢弃,解析结果自然缺胳膊少腿。

存储格式再强调一遍:REG_BINARY,原始字节序,没有额外头、没有长度前缀。你从RegQueryValueEx拿到的第一个字节就是EDID的Header首字节0x00。有些程序习惯先查一遍"00 FF FF FF FF FF FF 00"这个头部特征,但注册表数据一般已经可信,查一遍当作防御也不是坏事。

2.3 自己上I2C读EDID:一条更险、更得不偿失的路

理论上你可以绕过注册表,直接访问显卡的I2C控制器去读显示器的EEPROM。但这条路的成本非常高:显卡厂商各自封装了I2C/GPIOC访问接口,没有统一API;同一台机器上显卡驱动版本、显示输出端口(HDMI、DP、DVI)不同,总线复用方式也不同;热插拔瞬间I2C总线可能被驱动独占,你读到的可能是半截数据。

对比维度注册表读EDID直接I2C读EDID
开发难度只依赖Win32注册表API需要厂商SDK或驱动IOCTL
硬件依赖无强依赖具体显卡和端口
数据时效枚举时的缓存快照实时但可能因总线冲突失败
权限要求普通用户可读需要驱动级权限,未经驱动同意很可能蓝屏

注册表方案输在时效性上,却赢得开箱即用。对大多数需要显示器信息清单的VC项目来说,读注册表是性价比最高的起点。它把最难的I2C时序和DDC协议都封装进了系统内部,你要做的只是拿一份已经读好的字节流,然后搞懂EDID协议本身。

3. 128字节的显示器身份证:从位域还原厂商、型号和时序

3.1 厂商ID的5bit字母压缩:两个字节装三个大写字母

EDID基础块的字节8和字节9存厂商ID,但并不是ASCII。VESA把每个字母压成5bit,取值范围1到26,映射到大写字母,两个字节共16bit塞下三个字母,剩下的bit保留。解码算法是固定的:把两个字节拼成一个16位数,高位那组5bit是第一个字母,中间的5bit是第二个,低位5bit是第三个,然后每个值加上0x40也就是'@',就得到'A'到'Z'。

// 假设 pEDID 指向 128 字节基础块 WORD wMfg = (WORD)((pEDID[8] << 8) | pEDID[9]); char szMfg[4]; szMfg[0] = (char)(((wMfg >> 10) & 0x1F) + '@'); szMfg[1] = (char)(((wMfg >> 5) & 0x1F) + '@'); szMfg[2] = (char)((wMfg & 0x1F) + '@'); szMfg[3] = '\0';

这段逻辑值得说透:先移位到对应区间,再用0x1F掩码取5bit,最后加'@'。比如5bit的值是1,加0x40得到0x41正好是'A';值是2就是'B'。这样三个字母只占10个有效bit,厂商ID字段巧妙避开了ASCII对大小写和字符集的依赖。字节10、11是产品码,字节12、13是序列号,都是小端序,直接按WORD读即可,不需要这种位运算。

3.2 详细时序描述符(DTD):首选时序和最大分辨率藏在哪

从字节54开始,连续四段18字节的详细时序描述符,每段都描述一种具体时序。一个真正的时序DTD的结构是:前两个字节为像素时钟,单位10kHz,小端序;随后是水平有效像素、水平消隐、垂直有效像素、垂直消隐,各自有低8位和高4位拼接;再往后是同步偏移和脉宽。如果显示器厂商不老实,描述符也可能是空的,前三个字节全是0,这时它不再是时序,而是显示器描述符,用来写产品名、序列号或范围限制。

提取分辨率的代码看起来简单,但位域的拼接特别容易错:

// pDTD 指向 18 字节的详细时序描述符 UINT uPClk = (UINT)((pDTD[1] << 8) | pDTD[0]) * 10000; // 10kHz 单位,转成 Hz UINT uHA = pDTD[2] | ((UINT)(pDTD[4] & 0xF0) << 4); UINT uHB = pDTD[3] | ((UINT)(pDTD[4] & 0x0F) << 8); UINT uVA = pDTD[5] | ((UINT)(pDTD[7] & 0xF0) << 4); UINT uVB = pDTD[6] | ((UINT)(pDTD[7] & 0x0F) << 8); // 水平总时序 = 有效 + 消隐;刷新率 = 像素时钟 / (水平总时序 * 垂直总时序) double hFreq = (double)uPClk / (uHA + uHB); double vFreq = hFreq / (uVA + uVB);

细节全在高4位上:字节4的高4位是水平有效像素的高4位,低4位却属于水平消隐;字节7同理。所以拼回去时要先左移4位放进高位,再用按位或合并。别小看这个左移,见过不止一个解析器在Android/Linux移植时漏掉高4位,结果所有分辨率都被强行截断成低8位的模,4K屏报出个3840的低字节刚好64,于是显示成64x64。

产品名描述符则完全不同:当DTD的像素时钟和水平有效位都是0时,第四个字节如果是0xFC,说明这段是产品名;如果是0xFE是普通字符串,0xFF是序列号,0xFD是范围限制。字符串内容从偏移5开始,约13个字符,通常以0x0A结尾,后面的字节又是0x20。做解析时必须按这个规则截取,不能一路读到18字节结束。

3.3 校验和与扩展块:哪些错误可以让步,哪些诈胡必须拦下

EDID最后一个字节是校验和,保证前128字节累加后模256的结果为0。这个校验是EDID解析的第一道防线,能拦住总线错位、Flash半擦除这类硬件问题。但实际项目里你会发现,有些低端显示器固件算出的校验和就是不对,显示器本身却正常工作。

更关键的是扩展块。字节126表示后面的扩展块数量,为0就表示没有扩展;为1通常是一份CTA-861块,里面携带SVD列表,常见的4K@60Hz的VIC 97、HDR静态元数据都在这里。如果注册表里的值本身只有128字节,说明显示器只烧录了基础块,那4K能力很可能就不在DTD里体现。反过来,如果值有256字节而你只解析前128,那4K分辨率也会静默丢失。解析器的正确姿势是先判断缓冲区长度,不够就按扩展块数量尝试读取,而不是默认只解析基础块。

4. VC落地:RegQueryValueEx拿到裸字节,再自己解码

4.1 遍历DISPLAY子键,定位带EDID的实例

注册表读取的骨架不复杂,但坑在路径的选择:不能写死具体的显示器型号子键,因为不同机器、不同显卡驱动的键名差异很大。正确的做法是从DISPLAY根键开始,用RegEnumKeyEx遍历两层:

HKEY hEnum = NULL; if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Enum\\DISPLAY", 0, KEY_READ | KEY_ENUMERATE_SUB_KEYS, &hEnum) != ERROR_SUCCESS) { return; // 读取失败,多半是枚举路径被精简过 } for (DWORD i = 0; ; i++) { WCHAR szMon[256] = {0}, szInst[256] = {0}; DWORD cMon = 256, cInst = 256; if (RegEnumKeyExW(hEnum, i, szMon, &cMon, NULL, NULL, NULL, NULL) != ERROR_SUCCESS) break; HKEY hMon = NULL; if (RegOpenKeyExW(hEnum, szMon, 0, KEY_READ, &hMon) != ERROR_SUCCESS) continue; for (DWORD j = 0; ; j++) { if (RegEnumKeyExW(hMon, j, szInst, &cInst, NULL, NULL, NULL, NULL) != ERROR_SUCCESS) break; WCHAR szParams[300]; wsprintfW(szParams, L"%s\\%s\\Device Parameters", szMon, szInst); HKEY hParams = NULL; if (RegOpenKeyExW(hEnum, szParams, 0, KEY_READ, &hParams) == ERROR_SUCCESS) { // hParams 之后用于读取 EDID 值 } } }

两层遍历的用意是把同一型号的多台实例全部暴露出来。参数szParams的拼接必须是完整路径,注意基键是hEnum而不是hMon,因为RegOpenKeyEx要的是绝对路径。64位系统上编译32位程序容易出现重定向问题,打开路径时建议加上KEY_WOW64_64KEY,否则可能被重定向到SysWOW64视图,找到的Enum分支不一致。

4.2 解析器骨架:先验校验和,再解位域

拿到hParams之后,用RegGetValue读取EDID,它可以直接指定为REG_BINARY类型,省去RegQueryValueEx的两段式长度查询。缓冲区长度的预判很关键:先声明512字节,读回来后再根据实际长度判断是只有基础块还是带扩展块。

BYTE byEDID[512] = {0}; DWORD cbEDID = sizeof(byEDID); if (RegGetValueW(hParams, NULL, L"EDID", RRF_RT_REG_BINARY, NULL, byEDID, &cbEDID) != ERROR_SUCCESS || cbEDID < 128) { continue; // 这个实例没有可用的 EDID } // 校验和:前128字节累加模256必须为0 BYTE sum = 0; for (DWORD k = 0; k < 128; k++) sum = (BYTE)(sum + byEDID[k]); if (sum != 0) { /* 记录告警,但不中断解析 */ } UINT uYr = 1990 + byEDID[15]; // 生产年份,1990是基准 UINT uW = byEDID[21], uH = byEDID[22]; // 屏幕物理尺寸,单位厘米

校验和的写法用一个BYTE循环累加,利用自然的溢出回绕。如果求和结果不为0,说明这组数据来自一台固件异常的显示器,但分辨率字段往往仍然可信,所以记录告警即可。生产年份的基准是1990,这是EDID协议的历史遗留:设计当年定义这个字段的人没想到显示器会被用超过一百年,后来只能靠高位补丁。物理尺寸字段只在基础块字节21和22,单位是厘米,很多程序拿它算PPI,注意这里不是像素尺寸,别跟分辨率混淆。

4.3 输出参数表与验证方法

把前面两节拼起来,一个最小可用的解析程序就能输出下面的清单。字段务必按整数类型打印,尤其像素时钟要先乘以10000,否则刷新率会差四个数量级。

输出字段数据来源类型
厂商ID字节8/9解码后的三字母字符串
产品码字节10/11小端无符号整数
生产年份字节15+1990无符号整数
物理尺寸字节21/22无符号整数(厘米)
首选分辨率第一段非空DTD宽x高
最大刷新率所有DTD计算后取最大浮点数
扩展块数量字节126无符号整数

验证这套解析的最快办法是拿系统自带的显示器信息交叉比对:打开显示设置里的高级信息页,看分辨率是否和首选时序一致;再找个已知A级面板的显示器,厂商ID应该能对应到具体品牌。如果解析出的厂商ID是三个乱码字符,先不要怀疑解码算法,多半是字节序拼反了——常见的错误是把字节8直接当WORD高8位、字节9当低8位,解法是先用BYTE数组原样读入,再手工拼接,别依赖WORD指针强制转换。

5. 避坑指南:EDID解析中容易翻车的五类场景

5.1 现象:注册表里根本没有EDID键

现象:代码遍历DISPLAY子键两层,每个实例下的Device Parameters都是空的,找不到名为EDID的REG_BINARY值。原因:这类机器多半走的是转接器或虚拟显卡输出,比如HDMI转VGA的被动转接头自己不提供DDC回路,显示器固件读不到;又或者显卡驱动处于一种"假枚举"状态,把显示器识别成了通用即插即用监视器,却跳过了EDID上报。解决:先插一条直连DP或HDMI线验证;再尝试读取WMI命名空间里的显示器类数据,它的底层数据源仍然是EDID,但可能由另一条驱动路径填好。如果两条路都为空,说明这台设备的EDID从硬件层面就不存在,物理链路需要先查线。

5.2 现象:只读出128字节,4K显示器最大只报1080p

现象:显示器物理面板是4K,但拿到注册表EDID解析后,首选时序和最大分辨率都停在1920x1080。原因:这类4K屏把4K@60Hz的时序放在了CTA-861扩展块的SVD列表里,而不是基础块DTD中;如果你的代码按固定128字节解析,或注册表值实际只有128字节,自然看不到。解决:先RegGetValue返回的cbEDID,若大于128,用字节126的数值读取后续扩展块,在CTA-861块中扫描SVD的VIC码。若注册表值本身就是128字节,则是固件或驱动版本过旧,插拔一次让驱动重新枚举,再对比长度。

5.3 现象:校验和不通过,显示器却工作正常

现象:所有字段都合理,厂商ID和分辨率都能对上,唯独累加校验和不为0。原因:生产阶段固件写入时校验计算bug,或某些参数在产线上被售后工具改写过,但覆盖写入时没重算校验和。把锅甩给显示器固件比甩给代码常见得多。解决:校验和只做告警处理,不阻断解析;同时把这份异常EDID留个二进制快照,遇到显示异常可以回查是不是源头数据就脏。不要为了解决"校验和不通过"去强行改字节127——那属于伪造数据,违背读取EDID的初衷。

5.4 现象:型号字符串带着0x0A,怎么截都截不干净

现象:产品名打印出来后面总跟着一个换行,或者尾部一堆空格,用strlen和trim都处理不干净。原因:EDID的字符串描述符是定长区域,标准做法是字符串后放0x0A表示结束,剩余字节填0x20空格;部分显示器还会在0x0A后再补一个0x20。解决:从描述符偏移5开始读,最多13字节,遇到0x0A或0x20立即截断,然后再清一次尾部空格。不要在截断后再调用依赖本地代码页的字符串清理函数,二进制里的这些字节不是编码文本,只是ASCII约定。

5.5 现象:中文型号解析出来全是乱码

现象:明明是某国产品牌,型号却显示成"鍝佺墝"之类的诡异字符。原因:EDID协议里的字符串是纯ASCII/Latin-1,一个字节一个字符,压根没有UTF-8或GBK编码空间;你多半在解析后用宽字符转换函数按本地代码页处理了二进制数据。解决:产品名只做字节级拷贝,按ASCII解释,非ASCII字节要么显示为'?',要么保留原字节交由上层做映射。EDID不是给你放中文的地方,厂商真要在描述符里塞非ASCII字符,结果就是跨平台全部乱码,这是协议本身的限制,不是解析器的错。

6. 设计取舍:吃缓存省下的,是你自己写I2C驱动的时间

6.1 注册表EDID的可用边界与实时性问题

读注册表拿EDID本质是在吃PnP枚举的缓存,这个设计把硬件时序、总线冲突、驱动权限全部封装掉了,换来的是普通用户态程序也能稳定解析到显示器信息。你不能要求它实时反馈显示器当前的状态:如果用户通过OSD或DDC/CI改了亮度、换了显示模式,注册表里的EDID不会自动更新,因为EDID描述的是"这台显示器支持什么",而不是"它现在正处在什么模式"。理解了这一点,就能辨别什么功能该用注册表EDID,什么该走DDC/CI或直接读显卡状态。

进阶做法是拿注册表EDID做基线,再叠加实时监控:用RegNotifyChangeKeyValue监听EDID键的变化,热插拔瞬间重新读取并解析;或者订阅设备接口通知,收到显示设备到达事件后再去读注册表。两相结合,既避开I2C总线裸访问的坑,又追上了缓存时效的短板。

至于扩展块的深度解析,比如CTA-861里的HDR亮度、色彩深度、VIC列表,注册表数据一样能解,前提是你先把基础块的位域和解码算法彻底做对。EDID的乐趣在于它是一份被精确压缩过的说明书,字节0到127没有任何浪费;解析它最大的教训是:显示协议让你看到什么,你就只能信什么——厂商ID会骗人,校验和可以不通过,分辨率字段也可能被驱动改写。从那以后,我每次拿到EDID都强制走一遍校验和、数一遍扩展块、列一遍全部DTD,再跟系统显示设置交叉核对一遍才敢交差。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进