简介:《EDID编程规范》面向显示驱动、嵌入式开发及显示器固件调试人员,聚焦扩展显示识别数据从结构理解到实际编写的关键问题。文档以128字节基本块和256字节扩展结构为主线,梳理文件头、生产厂家与产品识别、版本号、基本显示参数、色彩特性、确定与标准显示时序、详细时序描述、显示终端名称及行场频范围限制、扩展标志和CheckSum等字段,并给出V1.3数据编写要点与示例值,便于在EDID烧录、兼容性排查和显示模式适配时快速查阅。资源包共1个doc文件,约493KB,轻量便于随项目归档。已有562人学习,适合需要一份可直接对照字段地址与取值规则的硬件/固件工程师及显示技术学习者。
1. 一根转接线黑屏,问题却出在显示器里那块 256 字节的 EEPROM
一根 DVI 转 HDMI 的转接线,接上电视死活不出图。换线、换口、换播放器,全都没用,很多人第一反应是带宽不够。可把显示器里那块 EEPROM 读出来逐字节看,问题常常出在 EDID 编程规范里最不起眼的两个地方:像素时钟写成了 74 而不是 7425,或者最后一个字节的 Checksum 差了 1。前者让信号源算错了时序,后者让整块数据被直接判为无效,退回 1024×768 的安全模式。
EDID 的反直觉之处在于:显示器不是被动地被识别,而是主动把自己能吃什么信号,预先写在 DDC 通道上,让信号源照着做。搞懂这套从 128 字节扩到 256 字节的结构,你才能在黑屏、闪屏、分辨率上不去这类玄学问题里,第一时间分辨是链路根本没读到,还是读到了但解析错了。
2. EDID 的分区地图:128 字节如何装下一台显示器的身份
2.1 DDC 不是协议,是一条挂在 I²C 上的只读通道
把 EDID 理解成“一个文件”会走偏,它更像一个固定地址的只读寄存器区。显示终端上电后,那颗小容量 EEPROM 就已经把答案准备好了,信号源要做的只是按 I²C 时序把它读出来。这条通道在规范里叫 DDC,物理层就是从 VGA 的 DDC 引脚或 HDMI/DVI 的 DDC/CEC 引脚引出的两根线。
一次完整的读取是标准的 I²C 随机读:主机发出起始条件,用写方向地址 0xA0 选中从机(7 位地址 0x50),送一个字节的子地址 0x00,然后重复起始,换成读方向 0xA1,连续收 128 个字节,最后发 STOP。想读扩展块,就把子地址换成 0x80,再读 128 字节。整个过程中主机不会写任何东西进显示器,所以 EDID 一旦烧录错误,除了重新烧录或者用软件覆盖,没有“运行时纠正”的机会。
注意:早期还有 DDC1 这种单向方案,靠 VSYNC 线把 128 字节一位一位挤出去,速率极低且只在特定条件下有效。今天几乎没有设备再走这条路,但排查老设备时如果发现读出来全是 0x00 或全 0xFF,先确认对方到底支不支持 DDC2B 双向通道。
这套设计的代价是显而易见的:EDID 的内容必须在上电前就确定,无法根据实际接入的信号源动态协商。收益同样明显——信号源在输出第一个像素之前,就已经知道对方的分辨率、刷新率、色度和音频能力,不需要“试探—失败—回退”这种破坏性握手。显示器厂商、信号源厂商、线材厂商三方只要共同遵守这张地址表,互操作就能成立。
2.2 前 8 字节文件头:一个刻意设计的“反随机”常量
第零块的前 8 个字节是固定的00 FF FF FF FF FF FF 00。这个值不是随便挑的,它同时排除了两种最常见的故障读法:如果 DDC 线断了或者从机没响应,主机读到的通常是全 0x00(总线被拉低)或全 0xFF(总线被拉高)。文件头一头一尾各放一个 0x00,中间六个 0xFF,就是为了让这两种情况都能被立刻识别出来,而不是把垃圾数据当成合法 EDID 去解析。
紧接着 08h–09h 是生产厂家名称,用的是三位字母压缩成 15 位的编码。以规范里的例子 PRI 为例:字母按 A=1 递增,P=16、R=18、I=9,拼成 16 位是0_10000_10010_01001,即 0x4249,高字节在前写成42 49。这里藏着一个很容易翻车的细节——厂家 ID 是大端,而紧随其后的产品代码 0Ah–0Bh 和序列号 0Ch–0Fh 是小端。同一份数据里两种字节序并存,手工拼包时把产品代码高低字节写反,信号源读到的就是另一个型号。
10h 是生产周,11h 是生产年份,年份的基准是 1990,所以 2007 年写成11h。12h–13h 是版本号和修订号,01 03对应 EDID 1.3。这两字节决定了后面所有字段的解释方式:1.3 和 1.4 在 0x14 的视频输入定义位、0x7E 的扩展块计数语义上并不完全一致,解析器必须先看版本再决定怎么读。
2.3 基本参数与色彩特性:gamma 那个 0x78 是怎么来的
14h 的视频输入定义是一个典型的状态压缩字节。数字输入时最高位置 1,取 0x80;模拟输入时取 0x0E,低几位分别描述信号电平标准、是否带消隐、是否有 SOG 同步。这意味着“这根口是 HDMI 还是 VGA”这件事,在第一块的第 20 个字节就确定下来了,后面的 CEA 扩展块只是补充能力,不负责表达接口类型。
15h 和 16h 是屏幕最大水平和垂直尺寸,单位厘米。这两个值影响的是信号源如何计算 DPI 和缩放策略,很多产线图省事直接填 0x00,结果在需要按物理尺寸做缩放的场景下,系统只能退回用分辨率猜。
17h 是 gamma,编码方式为(gamma × 100) - 100。规范里 gamma 2.2 的例子算出 120,写成 0x78。这个“减 100”的设计是为了让 1.00 到 3.54 的常用区间刚好落进一个字节,代价是精度只有 0.01。如果填 0x00,语义是“gamma 未定义,由显示端自行决定”,并不是 gamma=1.0,这两个含义在实际设备上表现差别很大。
18h 是支持特性,规范里默认 0xEA,附录的认证样例用的是 0x0A 或 0x0E,差别主要在 DPMS 节能位、首选时序模式、显示类型(RGB 彩色 / 单色 / 灰阶)以及 sRGB 默认色彩空间标志。19h–22h 的 10 个字节是色彩特性,红绿蓝白四组色度坐标,每组用 2 位小数部分加 8 位整数部分拼成 10 位。规范建议一般直接用默认值(色温约 9300K),只有在需要精确描述色域时才值得逐位计算。
2.4 扩展标志与 Checksum:一个字节决定后面还有没有戏
7Eh 是扩展标志。填 0x00 表示只有这 128 字节基本块,填 0x01 表示后面还有一块 128 字节的扩展数据。这个字段是 1.3 时代“最多两块”的简单表达,一旦它非零,信号源就会去读子地址 0x80;如果扩展块本身写坏了,表现出来往往是“基本模式能显示,一切到高分辨率就黑屏”。
7Fh 是第零块 Checksum,规则很朴素:00h–7Eh 这 127 个字节加上校验字节,按字节累加后低 8 位必须为 0。换句话说校验值等于前 127 字节之和的二进制补码低字节:
def fix_block_checksum(block: bytearray) -> int: """把一个 128 字节的 EDID 块最后一位修成合法校验值。 参数: block: 长度必须为 128 的 bytearray,block[0x7F] 会被原地覆盖。 返回: 写入的校验字节。 """ assert len(block) == 128, "EDID 块必须是 128 字节" # sum(block[:0x7F]) 可能超过 255,取负后 & 0xFF 等价于 256 - (sum % 256) block[0x7F] = (-sum(block[:0x7F])) & 0xFF return block[0x7F] def verify_block(block: bytes) -> bool: return len(block) == 128 and sum(block) & 0xFF == 0fix_block_checksum只做一件事:让整块的字节和为 0(模 256)。verify_block是它的逆运算,直接对全 128 字节求和取低 8 位。这段逻辑简单到可以塞进单片机的几行汇编里,这正是它被选中的原因——它不需要 CRC 表,不需要多项式,写入工具用一个累加器就能算完。代价是检错能力弱:任意两个字节同时加 1 和减 1,校验和依然成立。所以规范里那张 18 项的检查表不能省,Checksum 只能挡住“写漏了”“写串了”这类低级错误,挡不住字段语义错误。
扩展块里的 7Fh 是同样的算法,但作用域仅限该块自己的 128 字节,两个块的校验互相独立。改完第一块的任何字段,都必须重算第一块的校验;只改扩展块同理。很多工具图省事只重算一次,结果就是另一块静默失效。
3. DTD 的 18 字节位域:一个视频模式是怎么被拆开又拼回去的
3.1 四个字段跨字节存储:活动像素为什么要拆成两半
第零块的 36h–47h 和 48h–59h 是两个详细的时序描述块,每块 18 字节,负责精确描述一个视频模式。之所以叫“详细”,是因为它能表达任意分辨率和刷新率组合,而不是从预定义表里挑。扩展块里还有四个 DTD 位置(在 HDMI 场景下通常是 1Eh–65h 这段),所以一台设备最多能用六个 DTD 精确描述六个模式。
18 字节要装下像素时钟、水平活动/消隐、垂直活动/消隐、四组同步偏移与宽度、物理尺寸、边框和标志位,必然要压缩。压缩的手法是把 10 位或 12 位的数值拆成“低 8 位单独一个字节 + 高 4 位塞进共享字节的高低半字节”。以 1920×1080i 的规范样例为例,80 18 71这三个字节里:0x80 是水平活动像素低 8 位,0x18 是水平消隐低 8 位,0x71 的高半字节 7 是水平活动的高 4 位,低半字节 1 是水平消隐的高 4 位,拼回来就是 0x780=1920 和 0x118=280。
同样的套路用在垂直方向:1C 16 20里 0x1C 是垂直活动低 8 位,0x16 是垂直消隐低 8 位,0x20 的高半字节 2 和低半字节 0 补齐高位,得到 540 和 22。这里就是交错模式最容易写错的地方——1080i 的活动线数填的是每场 540 行,不是每帧 1080 行;填成 1080 会让信号源算出的场频只有实际的一半,画面要么抖要么直接不锁。
| 偏移 | 字段 | 位宽 | 说明 |
|---|---|---|---|
| 0–1 | 像素时钟 | 16 位小端 | 单位 10 kHz,7425 表示 74.25 MHz |
| 2 | 水平活动像素 | 低 8 位 | 高位在偏移 4 的高半字节 |
| 3 | 水平消隐 | 低 8 位 | 高位在偏移 4 的低半字节 |
| 4 | 高 4 位组合 | 8 位 | 高半字节=水平活动高 4 位,低半字节=水平消隐高 4 位 |
| 5–7 | 垂直活动/消隐 | 同上 | 高位组合在偏移 7 |
| 8–9 | 行同步偏移/宽度 | 低 8 位 | 高位在偏移 11 的 bit7–6 与 bit5–4 |
| 10 | 场同步偏移/宽度 | 低 4 位各半 | 高半字节=偏移,低半字节=宽度 |
| 11 | 同步高 2 位组合 | 8 位 | bit7–6 行偏移,bit5–4 行宽,bit3–2 场偏移,bit1–0 场宽 |
| 12–14 | 图像物理尺寸 | 高低位拆分 | 单位毫米,偏移 14 高半字节=水平高位,低半字节=垂直高位 |
| 15–16 | 水平/垂直边框 | 各 8 位 | 通常为 0 |
| 17 | 标志位 | 8 位 | 交错、立体、同步类型、行场极性 |
这张表值得打印出来贴在工位上,因为几乎每一次手工改 EDID 出错,都发生在偏移 4、7、11、14 这四个“高低位共享字节”上。
3.2 同步偏移与宽度:极性藏在最后一字节的 bit1/bit0
偏移 8 到 11 描述的是同步信号的位置和宽度。行同步偏移 0x58=88、行同步宽度 0x2C=44,这两个值在 1920×1080i 的例子里直接写成低 8 位,是因为它们都没超过 255,高 2 位为 0 时偏移 11 对应位就是 00。场同步偏移 2、宽度 5 更极端,低 4 位就够装,所以偏移 10 写成 0x25——高半字节 2 是偏移,低半字节 5 是宽度。
偏移 17 的标志位决定了信号源用什么方式去“看”这些同步信号。bit7 是交错标志,1080i 必须置 1,否则信号源会把它当成 1080p 的逐行时序去驱动,结果就是垂直方向被压扁或撕裂。bit4–bit3 是同步类型,11表示数字分离同步(行场各自一根线),10表示数字复合同步,01表示数字三电平同步,00表示模拟。bit2 和 bit1 分别是场同步和行同步的极性,1 为正、0 为负。
规范样例里的标志位是 0x9E,拆开是1001 1110:交错=1,立体=00,同步类型=11,场极性=1,行极性=1。而 720×480p 那块的标志位是 0x18,拆开是0001 1000:非交错,数字分离同步,行场都是负极性。这两个值如果写反,屏幕可能还能出图,但画面会偏移或者抖动——这类问题最容易被误判成“线材质量差”,实际是极性位填错了。
3.3 用 30 行代码把 18 字节反解成人能读的时序
手工对着十六进制数位域很容易错位,写个反解函数既能把每台样机的 EDID 变成可读表格,也能在改完之后立刻自检:
def parse_dtd(b: bytes) -> dict: """把 18 字节详细时序描述还原成可读字段。 参数: b: 长度 18 的 bytes,来自第零块 0x36 或扩展块对应偏移。 """ if len(b) < 18: raise ValueError("DTD 必须 18 字节") pixclk_khz = (((b[1] << 8) | b[0]) * 10) # 单位 10kHz -> kHz hact = ((b[4] & 0xF0) << 4) | b[2] # 高 4 位左移 8 再拼低 8 位 hbl = ((b[4] & 0x0F) << 8) | b[3] vact = ((b[7] & 0xF0) << 4) | b[5] vbl = ((b[7] & 0x0F) << 8) | b[6] hoff = ((b[11] & 0xC0) << 2) | b[8] # bit7-6 -> bit9-8 hwid = ((b[11] & 0x30) << 4) | b[9] # bit5-4 -> bit9-8 voff = ((b[11] & 0x0C) << 2) | (b[10] >> 4) # bit3-2 -> bit5-4 vwid = ((b[11] & 0x03) << 2) | (b[10] & 0x0F) htotal, vtotal = hact + hbl, vact + vbl flags = b[17] refresh = pixclk_khz * 1000 / (htotal * vtotal) if flags & 0x80: # 交错时像素时钟按场算 refresh *= 2 return { "pixel_clock_mhz": pixclk_khz / 1000, "h_active": hact, "h_blank": hbl, "h_total": htotal, "v_active": vact, "v_blank": vbl, "v_total": vtotal, "h_sync_offset": hoff, "h_sync_width": hwid, "v_sync_offset": voff, "v_sync_width": vwid, "interlaced": bool(flags & 0x80), "h_polarity": "positive" if flags & 0x02 else "negative", "v_polarity": "positive" if flags & 0x04 else "negative", "refresh_hz": round(refresh, 2), }关键在四个移位常量:0xF0 << 4把高半字节移到 bit11–8,0x0F << 8同理;0xC0 << 2把 bit7–6 移到 bit9–8,0x30 << 4把 bit5–4 移到 bit9–8;0x0C << 2把 bit3–2 移到 bit5–4。参数里b[10]的高半字节是场偏移、低半字节是场宽,这点和偏移 11 的组合顺序是相反的,写反了会得到一个偏移宽度互换但总和不变的错误结果,肉眼很难发现——总像素数没变,刷新率也勉强能算,只是同步位置错了。
交错模式下刷新率要乘 2,是因为像素时钟对应的是场周期,而我们要的是帧率。这段判断如果漏掉,1080i 会被算成 30Hz。
3.4 标准时序表与 Established Timings:为什么还要留着这些“老古董”
如果 DTD 能精确描述任意模式,为什么还要留 23h–25h 的确定时序位图和 26h–35h 的标准时序表?答案很现实:18 字节一个模式,四个 DTD 位置装不下显示器支持的全部模式,而信号源在选模式时需要一份“候选清单”。
23h–25h 是三个字节的位图,每个 bit 对应一个固定的经典模式。规范样例里 0x23 写 0xBF,0x24 写 0xEE,附录的认证样例则用 0xBF、0xEE、0x00,覆盖了 640×480 的 60/67/72/75Hz、800×600 的 56/60/72/75Hz、1024×768 的 60/70/75Hz、832×624@75 等。位图的优势是 3 个字节表达 24 个模式,代价是模式集合被规范写死,想加一个 1366×768 就只能用别的表。
26h–35h 的标准时序识别是 16 字节、8 组、每组 2 字节,编码方式很有代表性:第一个字节存(水平活动像素 / 8) - 31,第二个字节高 2 位存宽高比、低 6 位存刷新率 - 60。640×480@85Hz 的算法是 640/8-31=49(0x31),宽高比 4:3 对应 bit7–6 为 01,刷新率 85-60=25(0x19),拼成 0x59,于是这对字节就是31 59。
这套编码的边界条件也值得记:水平像素必须能被 8 整除且大于 248,否则第一个字节会下溢;刷新率必须落在 60 到 123 Hz 之间,低于 60 无法表达;宽高比只有 16:10、4:3、5:4、16:9 四种,2.35:1 这类比例根本描述不了。规范里把不用填的槽位约定为01 01而不是00 00,就是因为 0x00 会被解析成 248 像素、60Hz 的合法模式,而 0x01 对应的 256 像素同样“不合法但可识别”,解析器看到01 01就知道这是空槽。
三级结构的分工到这里就清楚了:位图负责“大家都认识的经典模式”,标准时序负责“常见但不一定进位图的模式”,DTD 负责“必须精确描述的主模式和设备原生模式”。这是一个典型的表达力换兼容性的取舍——越靠前的表越省字节、越容易被老设备读懂,越靠后的表越精确、越依赖解析器完整实现。
4. CEA-861 扩展块:HDMI 的能力清单为什么要写成一条链
4.1 扩展块头与 d 指针:数据块是一条自描述的链
当 7Eh 填 0x01,信号源会去读 0x80 开始的第二个 128 字节。这个块的头 4 个字节有固定含义:00h 是扩展块标签,0x02 表示 CEA-861;01h 是修订版本,只支持 VGA/DVI 时写 0x01 或 0x04,支持 HDMI 时规范样例写 0x03 对应 861B;02h 是一个叫 d 的指针,指向“后面还有东西”的起始地址。
从 03h 开始到 d-1 是一串数据块,每个数据块自己描述自己:第一个字节的高 3 位是标签代码,低 5 位是紧跟其后的负载字节数。标签 1 是音频数据块,2 是视频数据块,3 是厂商描述数据块(VSDB),4 是喇叭安置数据块,0 和 5–7 保留。解析器读到 03h,取出标签和长度,跳过长度的字节,落到下一个数据块的头部,如此循环直到 d。
这种“标签+长度+负载”的链式结构,和前面那种固定偏移的扁平表是完全不同的设计思路。扁平的 DTD 表要求每个字段位置固定,改一个就得整体重排;链式结构允许厂商只写自己需要的块,不支持的块直接不出现,解析器也不会因为读到不认识的标签就崩掉——它只需要按长度跳过。这是向前兼容的标准做法,代价是解析器必须真的会“走链”,而不能假设视频块一定在 04h。
4.2 视频数据块与 VIC:一个字节代表一整个分辨率
规范样例里 04h 是 0x47,拆开是标签 2(视频)、长度 7。后面 7 个字节85 04 11 02 90 06 14就是 7 个短视频描述符。每个字节的 bit6–0 是视频识别码 VIC,bit7 是 native 标志,表示这是接收端的最佳显示模式。
VIC 5 对应 1920×1080i@59.94/60Hz 16:9,加上 native 标志就是 0x85;VIC 4 是 1280×720p@60,0x04;VIC 17 是 720×576p@50,0x11;VIC 2 是 720×480p@60 4:3,0x02;VIC 16 是 640×480p@60 4:3,加 native 得到 0x90;VIC 6 是 720(1440)×480i@60,0x06;VIC 20 是 1920×1080i@50,0x14。七个字节描述七种模式,平均每个不到一个字节,这就是查表编码的威力。
native 标志的实际作用比看上去大。当信号源要在多个支持模式里挑一个输出时,native 标志是唯一能表达“这是面板原生分辨率”的信号。规范样例里给了两个 native(1080i 和 480p),这在多制式电视上很常见,但严格说 native 应该只有一个。如果所有模式
本文还有配套的精品资源,点击获取