Files
HW-Debug-Tools/DLPilot/src/edid_parse.py
T

549 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
EDID 1.4 / CEA-861 解析(纯标准库)
================================================================
parse_edid() 原始 EDID 字节 -> 结构化 dict(DTD/已建立/标准/CEA-VIC/数据块)
edid_info_lines() EDID 信息卡片用的 (项目, 值) 行
edid_field_rows()/edid_field_text() 逐字节字段解析
hex_dump() 经典 hex + ASCII 排版
"""
import math
import struct
# ============================================================
# EDID 1.4 / CEA-861 解析
# ============================================================
# CTA-861 VIC 1..64 常用映射(65+ 及扩展 VIC 不展开)
VIC_TABLE = [
'640x480@60', '720x480@60 4:3', '720x480@60 16:9', '1280x720@60', '1920x1080i@60',
'720x480i@60 4:3', '720x480i@60 16:9', '720x240p@60 4:3', '720x240p@60 16:9',
'1440x480i@60 4:3', '1440x480i@60 16:9', '1440x240p@60 4:3', '1440x240p@60 16:9',
'1440x480p@60 4:3', '1440x480p@60 16:9', '1920x1080p@60', '720x576p@50 4:3', '720x576p@50 16:9',
'1280x720p@50', '1920x1080i@50', '720x576i@50 4:3', '720x576i@50 16:9',
'720x288p@50 4:3', '720x288p@50 16:9',
'1440x576i@50 4:3', '1440x576i@50 16:9', '1440x288p@50 4:3', '1440x288p@50 16:9',
'1440x576p@50 4:3', '1440x576p@50 16:9', '1920x1080p@50', '1920x1080p@24',
'1920x1080p@25', '1920x1080p@30',
'2880x480p@60 4:3', '2880x480p@60 16:9', '2880x576p@50 4:3', '2880x576p@50 16:9',
'1920x1080i@50(1250)', '1920x1080i@100', '1280x720p@100',
'720x576p@100 4:3', '720x576p@100 16:9', '720x576i@100 4:3', '720x576i@100 16:9',
'1920x1080i@120', '1280x720p@120',
'720x480p@120 4:3', '720x480p@120 16:9', '720x480i@120 4:3', '720x480i@120 16:9',
'720x576p@200 4:3', '720x576p@200 16:9', '720x576i@200 4:3', '720x576i@200 16:9',
'720x480p@240 4:3', '720x480p@240 16:9', '720x480i@240 4:3', '720x480i@240 16:9',
'1280x720p@24', '1280x720p@25', '1280x720p@30', '1920x1080p@120', '1920x1080p@240',
# VIC 65-88 为 3D/特殊格式, 少见不展开
*[None] * 24,
'2560x1080p@50', '2560x1080p@60', '2560x1080p@100', '2560x1080p@120',
'3840x2160p@24', '3840x2160p@25', '3840x2160p@30',
'3840x2160p@50', '3840x2160p@60', '4096x2160p@24',
]
# 已建立时序位域表(bytes 35-37, 共 24 位, '-' 为保留位)
EST_TABLE = [
'720x400@70', '720x400@88', '640x480@60', '640x480@67', '640x480@72', '640x480@75',
'800x600@56', '800x600@60',
'800x600@72', '800x600@75', '832x624@75', '1024x768@87i', '1024x768@60', '1024x768@70',
'1024x768@75', '1280x1024@75',
'1152x870@75', '-', '-', '-', '-', '-', '-', '-',
]
STD_ASPECT = {0: (10 / 16, '16:10'), 1: (3 / 4, '4:3'), 2: (4 / 5, '5:4'), 3: (9 / 16, '16:9')}
DEPTHS = [0, 6, 8, 10, 12, 14, 16, 0]
INTERFACES = ['未定义', 'DVI', 'HDMI-A', 'HDMI-B', 'MDDI', 'DisplayPort', '保留', '保留']
SYNC_TYPES = ['模拟合成', '双极模拟合成', '数字合成', '数字分离']
# CEA-861 数据块 tag(CTA-861 标准): 1=音频 2=视频 3=厂商专用(VSDB)
# 4=扬声器分配 5=VESA DTC 6=视频格式 7=扩展 tag 集合
EXT_TAG_NAMES = {
0x00: '视频能力', 0x01: '厂商扩展', 0x02: 'VESA 显示设备', 0x04: 'HDMI 4K/8K 视频格式',
0x05: '色度学', 0x06: 'HDR 静态元数据', 0x07: 'HDR 动态元数据', 0x0C: 'YCbCr4:2:0 能力',
0x0E: 'YCbCr4:2:0 视频格式', 0x78: 'HDMI 论坛 SCDB',
}
def parse_dtd(raw, o):
"""解析 18 字节详细时序描述符, 返回 dict"""
t = {}
t['clk_khz'] = struct.unpack_from('<H', raw, o)[0] * 10 # 10kHz 单位
t['hact'] = raw[o + 2] | ((raw[o + 4] & 0xF0) << 4)
t['hblank'] = raw[o + 3] | ((raw[o + 4] & 0x0F) << 8)
t['vact'] = raw[o + 5] | ((raw[o + 7] & 0xF0) << 4)
t['vblank'] = raw[o + 6] | ((raw[o + 7] & 0x0F) << 8)
t['hfront'] = raw[o + 8] | ((raw[o + 11] & 0xC0) << 2)
t['hsync'] = raw[o + 9] | ((raw[o + 11] & 0x30) << 4)
t['vfront'] = ((raw[o + 10] & 0xF0) >> 4) | ((raw[o + 11] & 0x0C) << 2)
t['vsync'] = (raw[o + 10] & 0x0F) | ((raw[o + 11] & 0x03) << 4)
t['him_mm'] = raw[o + 12] | ((raw[o + 14] & 0xF0) << 4)
t['vim_mm'] = raw[o + 13] | ((raw[o + 14] & 0x0F) << 8)
fl = raw[o + 17]
t['interlaced'] = bool(fl & 0x80)
t['sync_type'] = SYNC_TYPES[(fl >> 2) & 3]
t['polarity'] = ('V+' if fl & 0x02 else 'V-') + ' ' + ('H+' if fl & 0x01 else 'H-')
t['htotal'] = t['hact'] + t['hblank']
t['vtotal'] = t['vact'] + t['vblank']
t['refresh'] = round(t['clk_khz'] * 1000.0 / (t['htotal'] * t['vtotal']), 1)
return t
def _ascii(raw, o, max_len):
out = []
for i in range(max_len):
c = raw[o + i]
if c in (0x0A, 0x00):
break
if 32 <= c < 127:
out.append(chr(c))
return ''.join(out)
def parse_edid(raw):
"""解析 EDID(128 或 256 字节), 返回汇总 dict"""
e = {'raw': raw, 'dtds': [], 'est': [], 'std': [], 'cea_video': [], 'cea_blocks': []}
n = len(raw)
e['cksum_base'] = (sum(raw[0:128]) & 0xFF) == 0
# 厂商 ID: 3 个 5bit 压缩大写字母
b0, b1 = raw[8], raw[9]
e['manufacturer'] = ''.join([
chr(((b0 & 0x7C) >> 2) + 64),
chr((((b0 & 0x03) << 3) | ((b1 & 0xE0) >> 5)) + 64),
chr((b1 & 0x1F) + 64)])
e['product'] = struct.unpack_from('<H', raw, 10)[0]
e['serial_num'] = struct.unpack_from('<I', raw, 12)[0]
e['week'] = raw[16]
e['year'] = 1990 + raw[17]
e['ver'] = f'{raw[18]}.{raw[19]}'
if raw[20] & 0x80:
bpc = DEPTHS[(raw[20] >> 4) & 0x7]
e['input'] = '数字 / ' + INTERFACES[raw[20] & 0x7] + \
(f' / {bpc}bpc' if bpc else ' / 色深未定义')
else:
e['input'] = '模拟 (VGA 输入)'
e['wcm'], e['hcm'] = raw[21], raw[22]
e['gamma'] = 0 if raw[23] == 0xFF else (raw[23] + 100) / 100.0
feats = []
if raw[24] & 0x80: feats.append('DPMS 待机')
if raw[24] & 0x40: feats.append('DPMS 挂起')
if raw[24] & 0x20: feats.append('DPMS 关闭')
if raw[24] & 0x04: feats.append('sRGB 特性')
if raw[24] & 0x02: feats.append('首选时序=原生')
e['features'] = ', '.join(feats)
# 已建立时序位域
for i in range(24):
bit = 7 - (i % 8)
if ((raw[35 + i // 8] >> bit) & 1) and EST_TABLE[i] != '-':
e['est'].append(EST_TABLE[i])
# 标准时序(8 组 2 字节, 0x0101 = 未用)
for i in range(38, 54, 2):
a, b = raw[i], raw[i + 1]
if (a == 1 and b == 1) or (a == 0 and b == 0):
continue
h = (a + 31) * 8
v = round(h * STD_ASPECT[(a >> 6) & 3][0])
e['std'].append(f'{h}x{v}@{b + 60}')
# 基础块 4 个 18 字节描述符区: 前两字节非 0 即 DTD, 否则是监视器描述符
e['name'] = e['serial_text'] = e['range_text'] = ''
for d in range(54, 126, 18):
if raw[d] == 0 and raw[d + 1] == 0:
tag = raw[d + 2]
if tag == 0xFC:
e['name'] = _ascii(raw, d + 5, 13)
elif tag == 0xFF:
e['serial_text'] = _ascii(raw, d + 5, 13)
elif tag == 0xFD:
e['range_text'] = (f"V {raw[d+5]}~{raw[d+6]}Hz, H {raw[d+7]}~{raw[d+8]}kHz, "
f"像素时钟上限 {raw[d+9] * 10}MHz")
else:
e['dtds'].append(parse_dtd(raw, d))
# CEA-861 扩展块
e['has_ext'] = raw[126] > 0 and n >= 256
e['has_cea'] = False
e['cea_rev'] = 0
e['cea_caps'] = ''
e['cea_phys'] = ''
e['cksum_ext'] = False
if e['has_ext']:
e['cksum_ext'] = (sum(raw[128:256]) & 0xFF) == 0
if raw[128] == 0x02:
e['has_cea'] = True
e['cea_rev'] = raw[129]
caps = []
if raw[131] & 0x80: caps.append('欠扫描')
if raw[131] & 0x40: caps.append('基本音频')
if raw[131] & 0x20: caps.append('YCbCr4:4:4')
if raw[131] & 0x10: caps.append('YCbCr4:2:2')
e['cea_caps'] = ', '.join(caps)
dtd_off = raw[130]
p = 132
while dtd_off > 4 and (p - 128) < dtd_off and p + 1 < 256:
tag = (raw[p] >> 5) & 7
length = raw[p] & 0x1F
if length == 0:
break
data = raw[p + 1:p + 1 + length]
if tag == 2: # 视频数据块: VIC 列表
for k in range(length):
vic = data[k] & 0x7F
native = ' (原生)' if data[k] & 0x80 else ''
m = (VIC_TABLE[vic - 1] if 1 <= vic <= len(VIC_TABLE) else None) \
or f'VIC#{vic}'
e['cea_video'].append(m + native)
elif tag == 1: # 音频数据块: N 个 3 字节 SAD
e['cea_blocks'].append(f'音频数据块 x{length // 3}')
elif tag == 4:
e['cea_blocks'].append('扬声器分配块')
elif tag == 5:
e['cea_blocks'].append('VESA DTC 块')
elif tag == 6:
e['cea_blocks'].append('视频格式块(VFDB)')
elif tag == 3:
# 厂商专用块: 前 3 字节 IEEE OUI(小端)
oui = bytes(data[0:3])
if oui == b'\x03\x0c\x00': # HDMI 1.x VSDB: OUI 后 4 字节物理地址
if length >= 7:
e['cea_phys'] = '.'.join(str(b) for b in data[3:7])
e['cea_blocks'].append('HDMI VSDB')
elif oui == b'\xd8\x5d\xc4': # HDMI 2.0 HF-VSDB
e['cea_blocks'].append('HDMI 2.0 HF-VSDB')
else:
e['cea_blocks'].append(f'厂商块 OUI={oui.hex(" ")}')
elif tag == 7:
ext = data[0] if length >= 1 else -1
name = EXT_TAG_NAMES.get(ext, f'0x{ext:02X}')
e['cea_blocks'].append(f'扩展块({name})')
else:
e['cea_blocks'].append(f'保留块 tag{tag}')
p += 1 + length
if dtd_off >= 4: # 扩展块 DTD 区
q = 128 + dtd_off
while q + 18 <= n:
if raw[q] == 0 and raw[q + 1] == 0:
break
e['dtds'].append(parse_dtd(raw, q))
q += 18
return e
# ============================================================
# 文本化
# ============================================================
def edid_info_lines(e):
"""[(项目, 值), ...] 摘要行"""
lines = []
inch = 0
if e['wcm'] > 0 and e['hcm'] > 0:
inch = round(math.hypot(e['wcm'], e['hcm']) / 2.54, 1)
lines.append(('厂商 / 产品码', f"{e['manufacturer']} / 0x{e['product']:04X}"))
lines.append(('显示器名称', e['name'] or '(未声明)'))
if e['serial_text']:
lines.append(('序列号(文本)', e['serial_text']))
lines.append(('生产周 / 年', f"第 {e['week']} 周 / {e['year']} 年"))
lines.append(('EDID 版本', e['ver']))
lines.append(('输入类型', e['input']))
if inch > 0:
lines.append(('屏幕尺寸', f"{e['wcm']}cm x {e['hcm']}cm (约 {inch} 英寸)"))
if e['gamma'] > 0:
lines.append(('Gamma', e['gamma']))
if e['features']:
lines.append(('特性', e['features']))
if e['range_text']:
lines.append(('范围限制', e['range_text']))
if e['has_ext']:
ck = 'OK' if e['cksum_ext'] else '错误!'
lines.append(('扩展块', f"CEA-861 rev.{e['cea_rev']} 校验和 {ck}"))
if e['cea_caps']:
lines.append(('CEA 能力', e['cea_caps']))
if e['cea_phys']:
lines.append(('HDMI 物理地址', e['cea_phys']))
else:
lines.append(('扩展块', '无'))
lines.append(('基础块校验和', 'OK (0x00)' if e['cksum_base'] else '错误!'))
# 每个 DTD 的完整时序参数(硬件调试重点)
for k, t in enumerate(e['dtds']):
tag = 'DTD1(首选)' if k == 0 else f'DTD{k + 1}'
lines.append((f'{tag} 模式', f"{t['hact']}x{t['vact']}@{t['refresh']} {t['clk_khz'] / 1000:.3f} MHz"))
lines.append((f'{tag} 总时序', f"{t['htotal']}x{t['vtotal']}"))
lines.append((f'{tag} 水平', f"有效 {t['hact']} | 前沿 {t['hfront']} | 同步 {t['hsync']} | "
f"后沿 {t['htotal'] - t['hact'] - t['hfront'] - t['hsync']}"))
lines.append((f'{tag} 垂直', f"有效 {t['vact']} | 前沿 {t['vfront']} | 同步 {t['vsync']} | "
f"后沿 {t['vtotal'] - t['vact'] - t['vfront'] - t['vsync']}"))
lines.append((f'{tag} 同步', f"{t['sync_type']} {t['polarity']} "
f"{'(隔行)' if t['interlaced'] else '(逐行)'}"))
if t['him_mm'] > 0:
lines.append((f'{tag} 图像尺寸', f"{t['him_mm']}x{t['vim_mm']} mm"))
return lines
def hex_dump(b):
out = []
for row in range((len(b) + 15) // 16):
off = row * 16
chunk = b[off:off + 16]
# 8+8 分组, 组间双空格: 同步头/厂商段一眼分开
hx = ' '.join(f'{v:02X}' for v in chunk[:8]) + ' ' + \
' '.join(f'{v:02X}' for v in chunk[8:])
asc = ''.join(chr(v) if 32 <= v < 127 else '.' for v in chunk)
out.append(f'{off:04X}: {hx:<50} {asc}')
return '\n'.join(out)
# ============================================================
# EDID 逐字节字段解析(调试视图): 基础块 + CTA-861 扩展块
# 每个字段一行: [偏移] 原始字节 | 含义 | 解码值 | 备注
# ============================================================
B_HEAD = '同步头 00 FF FF FF FF FF FF 00'
def _range_limits(d):
"""FD 频率范围描述符(18 字节常规布局) -> V/H 范围与像素时钟上限"""
try:
return (f"V {d[5]}~{d[6]}Hz, H {d[7]}~{d[8]}kHz, "
f"像素时钟上限 {d[9] * 10}MHz")
except (IndexError, ValueError):
return ''
def _bits(b, on, off=None):
return f'{b:02X}: ' + ('; '.join(on) if on else (off or '无置位'))
def edid_field_rows(raw):
"""把 EDID(128/256 字节)逐字段解成 [(偏移, 原始字节, 含义, 解码值, 备注), ...]"""
rows = []
n = len(raw)
def add(off, ln, meaning, val, note=''):
hx = raw[off:off + ln].hex(' ').upper()
if ln > 18: # 超长区域(如零填充)截断, 完整数据看 hex dump
hx = raw[off:off + 18].hex(' ').upper() + f' …(共 {ln} 字节)'
rows.append((f'0x{off:02X}-{off + ln - 1:02X}' if ln > 1 else f'0x{off:02X}',
hx, meaning, str(val), note))
add(0, 8, B_HEAD, 'OK' if raw[0:8] == bytes.fromhex('00FFFFFFFFFFFF00') else '不匹配!',
'不许改, 用于同步识别')
# 8-9 厂商 ID: 16 位大端, 3 组 5bit 压缩大写字母(块内唯一大端字段)
mfr = ''.join([chr(((raw[8] & 0x7C) >> 2) + 64),
chr((((raw[8] & 0x03) << 3) | ((raw[9] & 0xE0) >> 5)) + 64),
chr((raw[9] & 0x1F) + 64)])
add(8, 2, '厂商 ID(大端)', mfr,
f"3×5bit 压缩字母, 原始 0x{(raw[8] << 8) | raw[9]:04X}; 正式产品应使用注册的 PNP ID")
add(10, 2, '产品码(小端)', f"0x{struct.unpack_from('<H', raw, 10)[0]:04X} ({struct.unpack_from('<H', raw, 10)[0]})",
'厂商自定')
add(12, 4, '序列号(小端)', f"0x{struct.unpack_from('<I', raw, 12)[0]:08X}",
'厂商自定')
add(16, 1, '生产周', f"第 {raw[16]} 周", '0 = 不指定')
add(17, 1, '生产年', f"{1990 + raw[17]} 年", '字节值 + 1990')
add(18, 2, 'EDID 版本号', f"{raw[18]}.{raw[19]}", '常用 1.4 / 1.3(即字节 01 04 / 01 03)')
# 字节 0x14 输入类型
if raw[20] & 0x80:
itf, bpc = INTERFACES[raw[20] & 0x7], DEPTHS[(raw[20] >> 4) & 0x7]
val = f'数字 / {itf}' + (f' / {bpc}bpc' if bpc else ' / 色深未定义')
else:
val = '模拟输入'
add(20, 1, '输入类型', val,
'bit7=1 数字; 低 4 位: 1=HDMI-A 2=HDMI-B 4=DP 5=eDP')
add(21, 1, '最大画面宽(cm)', raw[21], '0 = 未知(横竖比由 DTD 图像尺寸给出)')
add(22, 1, '最大画面高(cm)', raw[22], '0 = 未知')
add(23, 1, 'Gamma', f"{(raw[23] + 100) / 100:.2f}" if raw[23] else '(未提供)',
'存储值 = gamma×100 − 100(如 0x78 → 2.20); 0xFF = 不提供')
add(24, 1, '特性位', _bits(raw[24], [
*(['DPMS 待机'] if raw[24] & 0x80 else []),
*(['DPMS 挂起'] if raw[24] & 0x40 else []),
*(['DPMS 关闭'] if raw[24] & 0x20 else []),
*([f"显示色彩 {['RGB 4:4:4', 'RGB+YCrCb 4:4:4', 'RGB+YCrCb 4:2:2', '未定义'][(raw[24] >> 3) & 3]}"]
if raw[24] & 0x80 else []),
*(['sRGB 默认'] if raw[24] & 0x04 else []),
*(['首选时序=原生分辨率'] if raw[24] & 0x02 else []),
*(['连续频率/GTF'] if raw[24] & 0x01 else []),
]), 'bit2 sRGB / bit1 首选时序有效 / bit0 连续频率')
add(25, 10, '色度坐标(CIE xy)', '默认 sRGB 模板' if raw[25:35] == bytes.fromhex(
'78EE95A3554D9EA6544C') else '(打包位段)', '红绿蓝白四点 xy 各 10bit 打包, 通常照抄模板')
est = []
est_bits = 0
for i in range(24):
bit = 7 - (i % 8)
if (raw[35 + i // 8] >> bit) & 1:
est_bits |= 1 << (23 - i)
if EST_TABLE[i] != '-':
est.append(EST_TABLE[i])
if est:
add(35, 3, 'Established Timing 位图', ', '.join(est),
'VESA 老 VGA 时序; 数字屏常写 FF FF 00=全支持')
else:
add(35, 3, 'Established Timing 位图',
'无有效声明' + ('(仅保留位置位)' if est_bits else ''),
'VESA 老 VGA 时序; 数字屏常写 FF FF 00=全支持')
stds = []
for i in range(38, 54, 2):
a, b = raw[i], raw[i + 1]
if (a == 1 and b == 1) or (a == 0 and b == 0):
continue
h = (a + 31) * 8
ar = STD_ASPECT[(a >> 6) & 3][1]
stds.append(f"{h}x{round(h * STD_ASPECT[(a >> 6) & 3][0])}@{b + 60} {ar}")
add(38, 16, '标准时序 ×8', '; '.join(stds) if stds else '8 槽全部未用(01 01)',
'2 字节/槽: 6bit 宽 + 2bit 比例 + 刷新率−60; 未用槽填 01 01')
# 0x36-0x7D: 4 × 18 字节描述符
for k, d in enumerate(range(54, 126, 18)):
grp = raw[d:d + 18]
if grp[0] or grp[1]:
t = parse_dtd(raw, d)
note = (f"{'(隔行)' if t['interlaced'] else '(逐行)'} "
f"{t['sync_type']} {'/'.join(t['polarity'].split())}; "
f"消隐 H{t['hblank']}/V{t['vblank']}, "
f"前/同/后 H{t['hfront']}/{t['hsync']}/{t['htotal'] - t['hact'] - t['hfront'] - t['hsync']} "
f"V{t['vfront']}/{t['vsync']}/{t['vtotal'] - t['vact'] - t['vfront'] - t['vsync']}")
add(d, 18, f'描述符{k + 1}: DTD(详细时序)',
f"{t['hact']}×{t['vact']}@{t['refresh']}Hz "
f"总 {t['htotal']}×{t['vtotal']} {t['clk_khz'] / 1000:.3f}MHz"
+ (' [首选时序]' if k == 0 else ''),
note + ('; 图像尺寸 %dx%d mm' % (t['him_mm'], t['vim_mm']) if t['him_mm'] else ''))
else:
tag = grp[3]
txt = _ascii(raw, d + 5, 13)
if tag == 0xFF:
add(d, 18, f'描述符{k + 1}: 序列号串(0xFF)', repr(txt), 'ASCII, 上限 13 字节')
elif tag == 0xFC:
add(d, 18, f'描述符{k + 1}: 显示器名(0xFC)', repr(txt), 'ASCII, 上限 13 字节')
elif tag == 0xFD:
add(d, 18, f'描述符{k + 1}: 范围限制(0xFD)', _range_limits(grp), '频率范围描述符')
elif tag == 0xFE:
add(d, 18, f'描述符{k + 1}: 无界文本(0xFE)', repr(txt), '')
elif tag == 0x10:
add(d, 18, f'描述符{k + 1}: 数据串(0x10)', grp[5:18].hex(' ').upper(), '二进制用途')
elif tag == 0 and not any(grp):
add(d, 18, f'描述符{k + 1}: 空', '(全 00)', '')
else:
add(d, 18, f'描述符{k + 1}: 未知类型 0x{tag:02X}', grp[5:18].hex(' ').upper(), '')
add(126, 1, '扩展块数量', raw[126], '必须与实际扩展块数一致')
cksum = (256 - sum(raw[0:127])) & 0xFF
add(127, 1, '基础块校验和', f"0x{raw[127]:02X} → " + ('正确' if (sum(raw[0:128]) & 0xFF) == 0 else '错误!'),
f"整块 128 字节和 ≡ 0 (mod 256); 期望 0x{cksum:02X} = 256 − 前 127 字节和")
# ---- CTA-861 扩展块 ----
n_ext = raw[126] if n > 126 else 0
for bi in range(n_ext):
base = 128 + bi * 128
if base + 128 > n:
add(base, 0, f'扩展块{bi + 1}', '(数据不完整)', '')
break
blk = raw[base:base + 128]
add(base, 1, f'扩展块{bi + 1} @0x{base:02X} Tag',
'CTA-861' if blk[0] == 0x02 else f'0x{blk[0]:02X}(非 CTA)',
'固定 0x02 = CTA 扩展; HDMI 必须有')
if blk[0] != 0x02:
add(base + 1, 127, '扩展块数据', blk[1:128].hex(' ').upper(), '未知扩展类型, 跳过')
continue
add(base + 1, 1, '版本', f"rev {blk[1]}", '常用 03')
add(base + 2, 1, 'DTD 起始偏移', f"0x{blk[2]:02X} (= 块内 {blk[2]} 字节处)", '数据块链结束、DTD 区开始的偏移')
add(base + 3, 1, '标志位', _bits(blk[3], [
*(['bit7 支持 YCbCr 4:2:0'] if blk[3] & 0x80 else []),
*(['bit6 支持基础音频'] if blk[3] & 0x40 else []),
*(['bit5 支持 YCbCr 4:2:2'] if blk[3] & 0x20 else []),
*(['bit4 支持 YCbCr 4:4:4'] if blk[3] & 0x10 else []),
*(['bit0 支持欠扫描'] if blk[3] & 0x01 else []),
]), 'bit6=基础音频, bit5=4:2:2, bit7=4:2:0, bit0=欠扫描')
# 数据块链: 每块 1 字节头(高 3 位 tag + 低 5 位长度) + payload
TAG_NAMES = {1: '音频', 2: '视频 VIC', 3: '厂商专用 VSDB', 4: '扬声器分配',
5: 'VESA DTC', 6: '视频格式 VFDB', 7: '扩展 tag'}
p, first = 4, True
while base + p < base + 128 and p < blk[2] and p + 1 < 128:
tag, length = (blk[p] >> 5) & 7, blk[p] & 0x1F
payload = blk[p + 1:p + 1 + length]
if first and blk[p] == 0:
add(base + p, 1, '数据块链', '(无数据块, 直接进 DTD 区)', '4 至 DTD 区前为数据块链')
break
first = False
if tag == 2:
vics = []
for v in payload:
vi = v & 0x7F
name = (VIC_TABLE[vi - 1] if 1 <= vi <= len(VIC_TABLE) else None) or f'VIC#{vi}'
vics.append(name + ('(原生)' if v & 0x80 else ''))
add(base + p, 1 + length, '数据块: 视频', ' '.join(vics) or '(空)', f"tag 2, {length} 字节")
elif tag == 1:
fmts = []
codes = ['保留', 'LPCM', 'AAC', 'AC-3', 'MPEG1', 'MP3', 'MPEG2', 'AAC LC',
'DTS', 'ATRAC', 'DSD', 'E-AC-3', 'DTS-HD', 'MLP', 'DST', 'WMAPro']
for si in range(length // 3):
sad = payload[si * 3:(si + 1) * 3]
ch = (sad[0] & 7) + 1
rates = '/'.join(str(r) for r, b in ((32, 1), (44, 2), (48, 4)) if sad[2] & b) or '?'
mults = ''.join(f'×{m}' for m, b in ((2, 8), (3, 16), (4, 32)) if sad[2] & b)
if (sad[0] >> 3) & 0xF == 1: # LPCM: 位宽在字节1低3位(位图)
bits = '/'.join(str(v) for v, b in ((16, 1), (20, 2), (24, 4)) if sad[1] & b) or '?'
fmts.append(f'LPCM {ch}ch {bits}bit {rates}kHz{mults}')
else: # 压缩格式: 字节1高4位=最高采样率档
fmts.append(f"{codes[(sad[0] >> 3) & 0xF]} {ch}ch {rates}kHz{mults}")
add(base + p, 1 + length, '数据块: 音频', '; '.join(fmts), f"tag 1, {length // 3} × 3 字节 SAD")
elif tag == 3:
oui = bytes(payload[0:3])
names = {b'\x03\x0c\x00': 'HDMI 1.x VSDB', b'\xd8\x5d\xc4': 'HDMI 2.0 HF-VSDB'}
val = names.get(oui, f'OUI={oui.hex(" ").upper()}')
if oui == b'\x03\x0c\x00' and length >= 7:
val += f", 物理地址 {'.'.join(str(b) for b in payload[3:7])}"
add(base + p, 1 + length, '数据块: 厂商专用', val,
f"tag 3, 头 3 字节 IEEE OUI(小端){'; HDMI 2.0 需 HF-VSDB' if oui == b'\x03\x0c\x00' else ''}")
else:
name = TAG_NAMES.get(tag, f'tag{tag}')
add(base + p, 1 + length, f'数据块: {name}',
payload.hex(' ').upper() if length else '(空)', f"{length} 字节 payload")
if length == 0:
break
p += 1 + length
# DTD 区
if blk[2] >= 4:
q, cnt = blk[2], 0
while q + 18 <= 128 and any(blk[q:q + 2]):
t = parse_dtd(blk, q)
add(base + q, 18, f'扩展块 DTD{cnt + 1}',
f"{t['hact']}×{t['vact']}@{t['refresh']}Hz 总 {t['htotal']}×{t['vtotal']} "
f"{t['clk_khz'] / 1000:.3f}MHz",
"前/同/后 H%d/%d/%d V%d/%d/%d %s" % (
t['hfront'], t['hsync'], t['htotal'] - t['hact'] - t['hfront'] - t['hsync'],
t['vfront'], t['vsync'], t['vtotal'] - t['vact'] - t['vfront'] - t['vsync'],
'(隔行)' if t['interlaced'] else '(逐行)'))
cnt += 1
q += 18
if not cnt:
add(base + blk[2], 0, '扩展块 DTD 区', '(空)', '未用空间填 0')
rem = 128 - max(blk[2], 4) - 18 * cnt if blk[2] >= 4 else 0
if blk[2] >= 4 and rem > 0 and not any(blk[128 - rem:128 - 1]):
add(base + 128 - rem, rem - 1, '未用空间', '0 填充', '')
add(base + 127, 1, f'扩展块{bi + 1} 校验和',
f"0x{blk[127]:02X} → " + ('正确' if (sum(blk) & 0xFF) == 0 else '错误!'),
f"期望 0x{(256 - sum(blk[0:127])) & 0xFF:02X} (同基础块规则)")
return rows
def edid_field_text(raw):
"""逐字段解析 -> 多行文本(命令行 / 文本框共用)"""
w = (7, 26, 20)
out = ['%-*s %-*s %-*s %s' % (w[0], '偏移', w[1], '含义', w[2], '解码值', '备注 / 原始字节')]
for off, hx, meaning, val, note in edid_field_rows(raw):
line = '%-*s %-*s %-*s %s' % (w[0], off, w[1], meaning, w[2], val, note)
out.append(line.rstrip())
if hx:
out.append('%-*s %-*s %s %s' % (w[0], '', w[1], '', '原始', hx))
return '\n'.join(out)