
简介这是一份面向显示驱动开发、硬件调试与显示器固件工程师的EDID编程规范文档围绕扩展显示识别数据这一数据结构展开帮助读者理解显示器如何通过DDC通道向信号发生器告知自身识别信息与显示能力从而解决信号适配与兼容性问题。文档以EDID 1.3规范为主线逐段拆解128字节核心数据结构包括固定文件头、生产厂家与产品识别、版本号、基本显示参数与特性、色彩特性、确定显示时序、标准时序识别、四组详细时序描述块、显示终端名称与行场频范围、扩展标志及CheckSum校验并对每一地址区间给出默认值与填写示例便于对照编写。资源包内含1个doc文档大小约493KB结构紧凑、查阅方便。目前已有562人学习下载适合需要快速掌握EDID字段含义、校验计算与规范配置方法的中初级开发者作为案头参考。1. 一块 128 字节、只读的“自我介绍”显示器从不需要和主机协商把一台显示器接到主机上几毫秒之内主机就知道该输出 1920×1080 还是 640×480知道对面有没有喇叭、支不支持 YCbCr 4:2:2甚至知道这块面板物理宽度是 708mm。整个过程里没有一次双向协商显示器从头到尾只是被人从两根线上把 128 个字节读走一个字节都不会多说。这 128 字节就是 EDID。它不是协议是一份自我介绍主机并不“问”显示器能干什么而是“读”显示器早就写好的自述。搞懂 EDID 的底层机制本质上就是搞懂三件事这 128 字节怎么划分地址空间、18 字节的时序块怎么把一堆位域拼起来、以及为什么校验和对上了画面还是黑的。这三件事串起来才是显示链路里真正的黑匣子。2. DDC 上的单向约定128 字节边界与 I2C 读取时序EDID 之所以能以这种“读取即完成”的方式工作取决于它在物理层依赖的 DDC 通道。这部分是整个机制的地基也是很多“改了 EDID 但不生效”问题的根源你以为在改数据其实在改一段显示器上电时就被主机抢读的只读存储。2.1 两根线上的一次整块读取I2C 地址 0x50显示数据通道在物理上通常就是两根线一条时钟、一条数据走的是 I2C 电气规范。EDID 在这个总线上的从机地址是 0x50主机先写一个字节的偏移量 0x00然后连续读 128 字节一次读完中间不需要显示器参与任何判断。常见的实现思路是主机侧读 EDID 是一个“写偏移 连续读”的固定事务偏移量只有 8 bit所以单次寻址天然只能覆盖 256 字节空间。这一点非常关键它解释了下文所有的地址分配第零块占 00h–7Fh第一块占 00h–7Fh在同一段偏移空间里通过块内结构区分再往后如果还要加块就必须靠额外的段指针寄存器来切换。协议的设计从来不是先有“128 字节”这个数字而是先有 8 bit 偏移这个物理约束再从里面划出两个 128 字节的窗口。提示读 EDID 失败时先别怀疑数据内容。总线层面上“读不到”和“读到了但内容不对”是两类完全不同的故障前者通常停在应答位ACK上后者是数据本身的问题。正因为是单向读取EDID 里没有“能力协商”字段。所有描述都是陈述句我支持这些模式、我最大能跑这么高的点频、我有哪些接口。主机只能选择相信或者不相信没有讨价还价的空间。这个设计取舍带来极高的兼容性——十年前的显卡也能读今天的显示器——但代价是EDID 写错了主机不会报错它只会安静地输出一个你以为不存在的模式。2.2 7Fh 那个字节为什么是求和取补而不是 CRC第零块的最后 1 个字节7Fh是校验值规则朴素得有点土00h 到 7Eh 共 127 个字节相加再加上校验值结果必须让低 8 位为 0。换成代码就是“求和取补”。def fix_checksum(block): 按 EDID 规则重算并写回第 128 个字节 assert len(block) 128, 一个块必须刚好 128 字节 # 前 127 字节求和取低 8 位的二进制补码使整块累加 ≡ 0 (mod 256) block[127] (-sum(block[:127])) 0xFF return block这段函数的参数是一个长度必须为 128 的可变序列通常是 bytearray返回同一个对象。它只做两件事对前 127 字节求和、把补码写进最后一位。 0xFF用来截断成单字节-sum(...)是 Python 里求补码最省事的写法。为什么不用 CRC因为在 128 字节这个量级、这个带宽、这个年代求和取补已经够用了。它对任何单字节错误 100% 敏感对任意奇数个比特翻转敏感而且计算成本近乎为零——显示器端可能只是一个 8 位单片机在主循环里顺手加一遍就行。这是个典型的嵌入式取舍校验能力的上限不需要达到“可纠错”只需要达到“不合格就别用”。但这里藏着一个后面会反复出现的隐患校验和只证明这 128 字节在传输和存储过程中没有发生意外翻转它完全不证明这些字节的语义是对的。你把 gamma 从 2.2 写成 3.2重算校验和照样全绿只有实物看上去发灰你才知道出事了。2.3 128 字节装不下之后扩展标志与第二块第零块能塞的东西其实很少文件头 8 字节、厂商与产品信息 10 字节、版本 2 字节、基本参数 5 字节、色彩特性 10 字节、三张时序表合计 31672 字节、扩展标志 1 字节、校验 1 字节。72 字节的详细时序区看着宽敞其实只够放 4 个块而且通常已经被“详细时序 1”“详细时序 2”“显示器名称”“行场频范围限制”占满了。于是 7Eh 这一个字节成了分水岭值为 00h 表示只有这 128 字节值为 01h 表示后面还跟着一个 128 字节的扩展块。扩展块同样以 00h 开头、以 7Fh 的校验值结尾自成一体。主机读 EDID 的标准动作因此变成两步先读 128 字节看 7Eh如果非零再用段指针或者在某些实现里直接续读把第二块也读进来。这个“块 块内校验”的结构有个非常现实的好处一个块坏掉不会污染另一个块。第零块的校验挂了主机至少还能拿扩展块里的详细时序去点屏反过来扩展块校验挂了基本显示能力还在。它把“全有或全无”拆成了“部分可用”这在消费电子的兼容性策略里是相当常见的一手。注意扩展标志与实际内容必须一致。7Eh 写了 01h 却没有任何扩展数据或者明明写了扩展结构却忘了把 7Eh 置 01h主机侧的表现完全不同——前者读第二块时拿到一片 0xFF后者干脆当扩展不存在。3. 第零块逐段拆解厂商 ID 的 5bit 压缩与三张时序表的冗余第零块真正考验理解深度的地方不在地址表而在几个“压缩得过分”的字段和几张功能重叠的时序表。看懂这些你才算知道为什么 EDID 解析器里总有那么多硬编码的例外分支。3.1 08h–11h厂商码的 5bit 压缩与 1990 这个基准年08h–09h 这两个字节存的是三字母厂商码但只给两个字节怎么装三个字母答案是每个字母只用 5 bitA 对应 1B 对应 2一直到 Z 对应 26最高位留空。三个字母 15 bit加上一个留空的最高位正好 16 bit。def decode_vendor_id(b8, b9): 把 08h/09h 还原成三字母码示例数据 0x10B6 - DEV raw (b8 8) | b9 chars [] # bit15 保留为 0其后依次是 char1(bit14-10)、char2(bit9-5)、char3(bit4-0) for shift in (10, 5, 0): code (raw shift) 0x1F chars.append(chr(ord(A) code - 1) if code else ?) return .join(chars)示例里用的是一个虚构厂商码 0x10B6拆成二进制是0001 0000 1011 0110去掉最高位后按 5 bit 切成00100 / 00101 / 10110分别等于 4、5、22映射回字母就是 D、E、V。函数里的三个 shift 值就是三个字母在 16 bit 里的起始位置 0x1F负责只保留 5 bit。code为 0 时返回问号因为 0 不是合法字母值。再往后看0Ah–0Bh 是产品代码0Ch–0Fh 是序列号10h 是生产周11h 是生产年份。年份这个字段很有意思它存的是“年份减去 1990”。写 11h就是 1990172007 年。用相对值而不是绝对值是为了让一个字节能覆盖 1990 到 2245 年这是典型的老派嵌入式思维——反正规范是 1990 年代定的够用一辈子。代价是今天所有解析器里都得硬编码一个 1990 的偏移量谁忘了这个减法就会看到一批“1911 年生产”的显示器。这三个字段产品代码、序列号、生产周在规范里明确写着“一般不用默认值为 00h / 01h”。这句话的潜台词是主机端并不依赖它们做匹配它们更多是给产线追溯和售后识使用的。你在做兼容性调试时不要指望改序列号能让主机重新识别主机的缓存键通常是厂商码加产品码甚至只是物理接口。3.2 14h–18h输入类型、gamma 与特性位里藏着的语义14h 这一个字节是整个 EDID 里最容易写错、也最容易引发“完全没画面”的字段。最高位区分数字输入还是模拟输入数字信号输入填 0x80模拟信号输入填 0x0E。这个差别不是风格问题是电气问题——主机读到模拟标志就会去按模拟时序驱动 DDC 之外的信号线读到数字标志就走纯数字链路。填反了数据全部正确画面完全没有。15h 和 16h 是屏幕最大水平与垂直尺寸单位是厘米。规范允许填 0表示“我不告诉你”。但这里有个坑这两个字节和详细时序块里的图像尺寸单位毫米描述的是同一块面板两者不一致时主机通常信详细时序块因为那里的精度更高。所以填 0 是安全的乱填反而不如不填。17h 是 gamma编码方式是“gamma×100−100”。gamma 2.2 就是 220−100120写成 0x78。这个偏移 100 的设计意图很直接绝大多数面板的 gamma 落在 1.0 到 3.54 之间减去 100 之后刚好把常用区间压进 0 到 254既不浪费取值空间又留出整数精度0.01 一档。这是那种“一眼看不出道理、算一下才发现刚好卡住”的经典位宽设计。18h 是特性支持位示例值 0xEA。高三位对应三种低功耗状态的能力标志待机、挂起、主动关断中间几位描述显示类型和色彩模式。这一位的语义在 1.3 和 1.4 之间有过调整所以解析器通常不深究只取高位做电源管理判断。真正影响点屏的是前面那几个字段。3.3 三张时序表Established、Standard、Detailed 为什么并存23h–25h 是“确定的显示时序”3 个字节本质是一张位图每一位固定对应一个历史上的经典模式640×48060、800×60060、1024×76860 这些。26h–35h 是“标准的显示时序识别”16 个字节能放 8 组每组 2 字节用公式编码任意常见分辨率。36h–7Dh 是“详细的时序描述”72 字节放 4 个 18 字节块能完整描述点频、消隐、同步偏移、物理尺寸。三张表并存不是设计者没想到合并而是历史层层叠加的结果。最早只有第一张位图够用是因为当年显示模式就那么几种后来模式爆炸位图放不下于是加了公式编码的第二张再后来需要描述非标准时序和面板物理尺寸才有了第三张。主机端的解析顺序也因此固定下来优先用详细时序其次标准时序最后才是位图。第二张表的编码方式值得单独说因为它是最容易算错的地方。每组两个字节第一个字节是“行活动像素除以 8 再减 31”第二个字节的高 2 bit 是宽高比00 表示 16:1001 表示 4:310 表示 5:411 表示 16:9低 6 bit 是“刷新率减 60”。举个例子要表达 640×48085Hz而 85Hz 不在位图里第一个字节 640/8−31 49 0x31宽高比 640:480 4:3对应高 2 bit 是 01刷新率 85−60 25 0x19填进低 6 bit。合起来第二个字节 0x40 | 0x19 0x59。所以这一组就是 0x31、0x59。def encode_standard_timing(hact, vact, refresh): 按 8 像素 / 60Hz 偏移的规则编码一组标准时序 assert hact % 8 0 and 256 hact 2288, 行像素必须是 8 的倍数且在取值范围内 ratio hact / vact # 宽高比只有四档超出这四档就不能用标准时序表达必须走详细时序块 aspect 0b01 if abs(ratio - 4/3) 0.01 else 0b11 if abs(ratio - 16/9) 0.01 \ else 0b00 if abs(ratio - 16/10) 0.01 else 0b10 b1 hact // 8 - 31 b2 (aspect 6) | ((refresh - 60) 0x3F) return b1, b2函数里hact // 8 - 31是硬性公式行像素不是 8 的倍数就没法编码宽高比只能在四档里挑这也解释了为什么 21:9 这类比例在标准时序表里永远表达不出来只能落到详细时序块。(refresh - 60) 0x3F里的掩码保证刷新率只占低 6 bit超出 60–123Hz 区间的刷新率同样编不进去。未使用的组规范要求填 0x01、0x01而不是 0x00、0x00。这个细节很坑0x00 在数值上表示“行像素 248、16:10、60Hz”是一个看似合法的模式而 0x01 对应的行像素是 256宽高比是 16:10、刷新率 61Hz同样“合法”但显然不是真实模式。规范选 0x01 作为填充值纯粹是因为它容易和全 0 的空白存储区分开。解析器看到 0x01、0x01 就直接跳过这是硬编码的例外。3.4 18 字节块Detailed Timing 与描述符共用一套布局36h 到 7Dh 这 72 字节被切成 4 个 18 字节块。这 4 个块长着同一副骨架但填的内容可以不一样前两个通常是真正的详细时序比如 1920×1080i 和 720×480p后两个往往是“显示器名称”和“行场频范围限制”这两个描述符。def parse_dtd(d): 解析 18 字节详细时序块d 为长度 18 的字节序列 pixclk ((d[1] 8) | d[0]) * 10_000 # 单位 10kHz hact d[2] | ((d[4] 0xF0) 4) # 行活动像素高 4 位在 d[4] hblank d[3] | ((d[4] 0x0F) 8) # 行消隐高 4 位在 d[4] vact d[5] | ((d[7] 0xF0) 4) # 场活动线数 vblank d[6] | ((d[7] 0x0F) 8) # 场消隐 hoff d[8] | ((d[11] 0xC0) 2) # 行同步偏移 hwid d[9] | ((d[11] 0x30) 4) # 行同步宽度 voff (d[10] 4) | ((d[11] 0x0C) 2) # 场同步偏移 vwid (d[10] 0x0F) | ((d[11] 0x03) 4) # 场同步宽度 hsize d[12] | ((d[14] 0xF0) 4) # 图像水平尺寸毫米 vsize d[13] | ((d[14] 0x0F) 8) # 图像垂直尺寸毫米 inter (d[17] 7) 0x01 # 隔行标志 return dict(pixclkpixclk, hacthact, hblankhblank, vactvact, vblankvblank, hoffhoff, hwidhwid, voffvoff, vwidvwid, hsizehsize, vsizevsize, interlacedinter)拿一组真实数据验证一下01 1D 80 18 71 1C 16 20 58 2C 25 00 C4 8E 21 00 00 9E。点频是 0x1D01×10kHz 74.25MHz行活动像素 0x80 | (0x710xF0)4 0x780 1920行消隐 0x18 | (0x710x0F)8 280场活动线数 0x1C | (0x200xF0)4 540场消隐 0x16 22行同步偏移 88、宽度 44场同步偏移 0x254 2、宽度 5物理尺寸 708mm × 398mm最后那个 0x9E 的最高位是 1说明这是隔行模式。这套解析逻辑里最需要留神的是那些“高位藏在别的字节里”的字段。行活动像素只有 8 bit 放不下 1920需要 11 bit于是高 4 位被塞进 d[4] 的高半字节而行消隐的高 4 位则占 d[4] 的低半字节——两个字段挤在一个字节里靠掩码区分。d[11] 更夸张一个字节承载了四个同步参数的高位扩展。这就是为什么手写 EDID 时最容易翻车的地方永远在这几个字节上。再看描述符复用。5Ah–6Bh 是显示器名称格式固定为00 00 00 FC 00开头后面是 ASCII 名称以 0Ah 结束剩余空间填 20h空格。6Ch–7Dh 是行场频范围限制头是00 00 00 FD 00后面依次是最低场频、最高场频、最低行频、最高行频、最大点频。它们和详细时序块共享同样的 18 字节槽位靠第三个字节到第五个字节的特征值来区分全 0 就是详细时序FC 就是名称FD 就是范围限制。这种“同一地址、不同结构、靠特征字节区分”的做法在解析器里必须写成先判类型再解析的分支。一个常见的错误是写 EDID 时把名称描述符放进了第一个槽位36h而主机默认第一个详细时序块必须是真实时序于是它拿 “00 00 00 FC” 去算点频得到 0整个显示模式协商直接失效。注意4 个槽位里至少要有一个真正的详细时序块且通常要求放在第一个。名称和范围限制可以没有但不能占据唯一的时序槽位。4. 扩展块与 CEA-861链式数据块和 HDMI 厂商描述第零块解决的是“这块屏能显示什么”扩展块解决的是“这个接口还能传什么”。前者是显示能力后者是接口能力两者混在一起才是完整的 EDID。4.1 一个扩展块里两种写法纯视频时序与 HDMI 能力集扩展块的 00h–03h 是文件头但根据接口类型这 4 个字节填法完全不同。只支持 VGA 或 DVI 的机型头是02 01 04 0002h 表示这是 CEA 扩展头01h 是 EIA/CEA-861 的修订版本号04h 是固定值00h 未使用。这种情况下扩展块的结构非常朴素04h–15h 放第三个详细时序块通常是 1280×720p16h–7Eh 全部填 00h 保留7Fh 是校验。支持 HDMI 的机型头变成02 03修订版本号升级到 03h表示按 EIA/CEA-861B 组织数据。02h 这个位置不再闲置而是变成了一个指针记作 d——它指向“HDMI 支持音频信息标志”所在的地址。整个扩展块的布局随之变成03h 一个能力字节04h 到 d−1h 是一串数据块d 之后是 4 个详细时序块72 字节再往后是保留区最后 7Fh 校验。这个 d 的设计是个典型的“自描述偏移”因为数据块区的长度取决于你写了多少个数据块视频、音频、喇叭、厂商描述都可能写也可能不写所以详细时序块的起始地址不固定必须靠这个指针来定位。03h 那个能力字节的低 4 bit 还同时声明了详细时序块的数量主机拿到它就知道要在 d 之后按 18 字节一跨跳几次。03h 的高 4 bit 是 DTV 能力标志bit7 表示是否支持欠扫描bit6 表示是否支持基本的声音格式bit5 和 bit4 分别表示是否支持 YCbCr 4:4:4 和 4:2:2。示例值 0x53 拆开是0101 0011bit61 表示支持基本音频bit41 表示支持 4:2:2低 4 bit 是 3 表示后面跟着 3 个详细时序块。4.2 Data Block 的 tag/length 链3 bit 标签 5 bit 长度数据块区是一个单向链表但没有指针——每个块自己带长度读完就往后跳。每个块的第 1 个字节被拆成两部分高 3 bit 是标签代码低 5 bit 是负载长度。标签代码数据块类型典型负载长度1音频数据块3 的倍数每个描述符 3 字节2视频数据块每个视频描述符 1 字节3厂商描述数据块5 字节起含 24bit 注册标识4喇叭安置数据块3 字节0 / 5 / 6 / 7保留不得使用def walk_data_blocks(ext, d): 从 04h 走到 d按 tag/length 逐块遍历 i 4 while i d: tag (ext[i] 5) 0x07 # 高 3 bit块类型 length ext[i] 0x1F # 低 5 bit负载字节数 payload ext[i 1: i 1 length] yield tag, payload i 1 length # 头 1 字节 负载 length 字节这段生成器的参数 ext 是整个扩展块128 字节d 是 02h 里那个偏移。循环从 04h 开始每次读出一个字节拆成 tag 和 length切出负载然后正好跳过1 length个字节。yield让它能按需迭代调试时可以直接 print 出每个块的偏移肉眼比对。拿一组实测数据结构验证这条链的自洽性03h 是 0x533 个详细时序块04h 是 0x47拆开 tag2视频块、length7于是跳到 0417 0Ch0Ch 是 0x26tag1音频块、length6跳到 13h13h 是 0x83tag4喇叭安置、length3跳到 17h17h 是 0x65tag3厂商描述、length5跳到 1Dh。而 02h 里写的 d 正好是 0x1D。这里出现了整个 EDID 中最漂亮也最脆弱的一处算术d 必须等于链表走完后的落点。中间任何一个 length 少写 1链条就会错位一个字节后面所有块全部解析成垃圾而校验和依然是正确的——因为校验和只关心字节总和。这就是我在后面避坑章要重点说的那类故障。视频数据块里每个字节是一个视频模式确认代码VIC最高位是 Native 标志表示这是接收端的最佳显示模式。比如 0x85 表示 VIC 51920×1080i且是首选0x04 表示 VIC 41280×720p非首选。主机在挑选输出模式时会优先选 Native 标记的那个。音频数据块的结构更有意思它的负载是若干个 3 字节描述符每个描述符描述一种音频格式。第 1 字节高 4 bit 是音频格式代码低 4 bit 是最大通道数减 1后两个字节是采样率位图和比特深度位图。非压缩的线性 PCM 是一种格式代码压缩格式如 AC-3是另一种如果机型支持对 HDMI 通道的压缩音频解码就必须额外加一个描述符。4.3 VSDB 与 Supports_AI音频信息标志为什么要求静音厂商描述数据块VSDB是 HDMI 扩展块里唯一带“品牌身份”的块。它的负载至少 5 个字节第 1 个字节是 3 字节的 24bit 注册标识用来声明这是哪套规范定义的扩展第 2 个字节在单 HDMI 通道时填 0x65有两个及以上通道时填 0x66第 3 个字节是通道编号第一个通道填 0x10第二个填 0x20依次类推。这里的设计意图很明确一台电视可能有多个 HDMI 口每个口的能力可以不同VSDB 用“通道编号 物理地址”的方式把能力绑定到具体的物理接口上。0x65 与 0x66 的区别不是长度而是语义——0x66 意味着后面还要跟着每个通道的物理地址列表。最后是 Supports_AI。这个标志位于 02h 指针 d 所指向的地址上值为 0x80 表示 HDMI本文还有配套的精品资源点击获取