feat: HDMI-Tool 启动时强制扩展显示拓扑 + 收录波形/注入脚本
This commit is contained in:
Binary file not shown.
+3
-1
@@ -1,6 +1,6 @@
|
||||
# HDMI-Tool — HDMI 源端测试工具(LT8619C/DLPC3421 转接板调试)
|
||||
|
||||
> 更新:2026-09-03。单文件 Python GUI(`hdmi_gui.py`),另附命令行分辨率/图案工具
|
||||
> 更新:2026-09-04。单文件 Python GUI(`hdmi_gui.py`),另附命令行分辨率/图案工具
|
||||
> `hdmi_source.bat`。已打包版直接双击 `HDMI-Tool.exe`,无需安装 Python。
|
||||
|
||||
---
|
||||
@@ -10,6 +10,7 @@
|
||||
| 功能 | 说明 |
|
||||
|---|---|
|
||||
| EDID 查看 | 枚举各显示输出口,从注册表读取 EDID 原始数据,按 EDID 1.4/CEA-861 解算:厂商/型号/尺寸/支持模式(DTD/已建立/标准/CEA-VIC)/首选时序完整参数/校验和 |
|
||||
| EDID 逐字节解析 | "EDID 原始数据"框右上"逐字节解析"按钮:按偏移逐字段展开基础块(同步头/厂商/产品/序列/版本/输入类型/尺寸/Gamma/特性位/色度/已建立+标准时序/4×描述符/扩展块数/校验和)与 CTA-861 扩展块(tag/版本/DTD 偏移/标志位/数据块链/扩展 DTD 区/校验和),每字段附原始字节 hex,自动换行;命令行等价 `--edid-fields` |
|
||||
| 分辨率切换 | 第二行"分辨率"下拉列出显卡按该屏 EDID 提供的全部模式;**应用**=临时切换(不写注册表,重启还原),**恢复**=回到本次会话切改前的模式 |
|
||||
| 测试图案 | 全屏投到所选显示器,测试卡区域可选 `640x360 / 800x600 / full 铺满`;内置 纯红/纯绿/纯蓝/棋盘格/网格线 + R/G/B 三行纯色值按钮(55/AA/FF,与 dist 下 solid_*.png 对应)+ 3 个自定义图片栏位 |
|
||||
| 命令行模式 | 自动化测试脚本可直接调用(见 §4) |
|
||||
@@ -39,6 +40,7 @@
|
||||
|
||||
```
|
||||
python hdmi_gui.py --dump 控制台输出 EDID 解算(exe 版不支持)
|
||||
python hdmi_gui.py --edid-fields 控制台输出 EDID 逐字节字段解析表(exe 版不支持)
|
||||
python hdmi_gui.py --test-pattern Blue --area 800x600 --seconds 5
|
||||
出图 5 秒自动退出
|
||||
python hdmi_gui.py --test-pattern Custom1 投自定义栏位 1
|
||||
|
||||
+329
-12
@@ -19,6 +19,7 @@ HDMI EDID 查看器 + 测试图案投屏工具
|
||||
命令行模式(自动化测试用):
|
||||
python hdmi_gui.py 启动 GUI
|
||||
python hdmi_gui.py --dump 控制台输出 EDID 解算结果
|
||||
python hdmi_gui.py --edid-fields 控制台输出 EDID 逐字节字段解析表
|
||||
python hdmi_gui.py --test-pattern Checker --seconds 3
|
||||
直接出图案, 3 秒自动退出
|
||||
python hdmi_gui.py --test-pattern Blue --area 800x600
|
||||
@@ -319,6 +320,24 @@ def apply_mode(device, dm):
|
||||
return r == 0, r # DISP_CHANGE_SUCCESSFUL == 0
|
||||
|
||||
|
||||
def ensure_display_extend():
|
||||
"""把 Windows 显示拓扑强制为"扩展"。
|
||||
|
||||
LT8619C 转接板热插拔后, 若 Windows 停留在"仅电脑屏幕"拓扑(会记住上次
|
||||
Win+P 的选择), 板卡的 EDID 虽已安装、GPU 也在按首选时序输出, 但桌面
|
||||
并未扩展过去, get_displays() 就枚举不到这块"显示器"。
|
||||
|
||||
返回 SetDisplayConfig 的返回码(0=成功); 任何异常静默, 不影响工具启动。
|
||||
"""
|
||||
try:
|
||||
SDC_TOPOLOGY_EXTEND = 0x00000004
|
||||
SDC_APPLY = 0x00000080
|
||||
return ctypes.windll.user32.SetDisplayConfig(
|
||||
0, None, 0, None, SDC_TOPOLOGY_EXTEND | SDC_APPLY)
|
||||
except Exception:
|
||||
return -1
|
||||
|
||||
|
||||
# ============================================================
|
||||
# EDID 1.4 / CEA-861 解析
|
||||
# ============================================================
|
||||
@@ -439,8 +458,8 @@ def parse_edid(raw):
|
||||
if raw[24] & 0x80: feats.append('DPMS 待机')
|
||||
if raw[24] & 0x40: feats.append('DPMS 挂起')
|
||||
if raw[24] & 0x20: feats.append('DPMS 关闭')
|
||||
if raw[24] & 0x08: feats.append('sRGB 特性')
|
||||
if raw[24] & 0x04: feats.append('首选时序=原生')
|
||||
if raw[24] & 0x04: feats.append('sRGB 特性')
|
||||
if raw[24] & 0x02: feats.append('首选时序=原生')
|
||||
e['features'] = ', '.join(feats)
|
||||
|
||||
# 已建立时序位域
|
||||
@@ -600,9 +619,258 @@ def hex_dump(b):
|
||||
for row in range((len(b) + 15) // 16):
|
||||
off = row * 16
|
||||
chunk = b[off:off + 16]
|
||||
hx = ' '.join(f'{v:02X}' for v in chunk)
|
||||
# 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:<48} {asc}')
|
||||
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)
|
||||
|
||||
|
||||
@@ -815,10 +1083,10 @@ def run_gui(auto_close=0):
|
||||
for key in ('Red', 'Green', 'Blue', 'Checker', 'Grid'):
|
||||
ttk.Button(row1, text=PATTERN_NAMES[key], width=12,
|
||||
command=lambda k=key: self._show_pattern(k)).pack(side='left', padx=5)
|
||||
# 纯色测试值三行(R/G/B 按通道分组, 位交换测试用)
|
||||
# 纯色测试值: 保持"通道标签 + 55/AA/FF 按钮"形式, 三组横向一行排布
|
||||
rowf = ttk.Frame(grp)
|
||||
rowf.pack(fill='x', pady=(3, 0))
|
||||
for ch, items in SOLID_ROWS:
|
||||
rowf = ttk.Frame(grp)
|
||||
rowf.pack(fill='x', pady=(3, 0))
|
||||
ttk.Label(rowf, text=f'{ch}: 0x', width=4).pack(side='left')
|
||||
for label, color in items:
|
||||
ttk.Button(rowf, text=label, width=5,
|
||||
@@ -845,9 +1113,13 @@ def run_gui(auto_close=0):
|
||||
ttk.Label(row2, text='自定义: 点击投图, ⚙ 配置名称与图片(图片保存在脚本目录)',
|
||||
foreground='#666666').pack(side='left', padx=10)
|
||||
|
||||
# --- 底部: EDID hex dump ---
|
||||
# --- 底部: EDID hex dump + 逐字节解析按钮 ---
|
||||
bot = ttk.LabelFrame(self, text='EDID 原始数据', padding=4)
|
||||
bot.pack(fill='both', expand=False, padx=8, pady=(6, 8))
|
||||
side = ttk.Frame(bot)
|
||||
side.pack(side='right', fill='y')
|
||||
ttk.Button(side, text='逐字节解析', width=10, command=self._show_edid_fields
|
||||
).pack(side='top', pady=(2, 4))
|
||||
self.txt_hex = tk.Text(bot, height=8, font=('Consolas', 9),
|
||||
state='disabled', wrap='none', background='#101010',
|
||||
foreground='#C8C8C8')
|
||||
@@ -909,6 +1181,41 @@ def run_gui(auto_close=0):
|
||||
self.area = self.area_var.get()
|
||||
save_slots(self.slots, self.area)
|
||||
|
||||
def _show_edid_fields(self):
|
||||
"""逐字节字段解析弹窗(当前所选显示器), 自动换行文本视图"""
|
||||
from tkinter import messagebox
|
||||
idx = self.cbo.current()
|
||||
if idx < 0 or idx >= len(self.displays):
|
||||
return
|
||||
raw = self.displays[idx]['edid']
|
||||
if not raw:
|
||||
messagebox.showinfo('无 EDID', '该显示器注册表中没有 EDID 数据, 无法逐字节解析')
|
||||
return
|
||||
dlg = tk.Toplevel(self)
|
||||
dlg.title(f'EDID 逐字节解析 — {self.displays[idx]["device"]} '
|
||||
f'[{self.displays[idx]["mon_name"]}]')
|
||||
dlg.geometry('1180x720')
|
||||
txt = tk.Text(dlg, font=('Microsoft YaHei UI', 9), wrap='char',
|
||||
background='#101010', foreground='#C8C8C8',
|
||||
padx=10, pady=8)
|
||||
sb = ttk.Scrollbar(dlg, orient='vertical', command=txt.yview)
|
||||
txt.configure(yscrollcommand=sb.set)
|
||||
txt.pack(side='left', fill='both', expand=True)
|
||||
sb.pack(side='right', fill='y')
|
||||
# 字段行加亮, 原始字节补充行暗色, 换行内容自动缩进可读
|
||||
for off, hx, meaning, val, note in edid_field_rows(raw):
|
||||
txt.insert('end', f'{off} {meaning} {val}\n', 'field')
|
||||
if hx:
|
||||
txt.insert('end', f' 原始: {hx}\n', 'raw')
|
||||
if note:
|
||||
txt.insert('end', f' {note}\n', 'note')
|
||||
txt.tag_configure('field', foreground='#E8E8E8')
|
||||
txt.tag_configure('raw', foreground='#7F7F7F')
|
||||
txt.tag_configure('note', foreground='#9FB8CC')
|
||||
txt.configure(state='disabled')
|
||||
ttk.Button(dlg, text='关闭', command=dlg.destroy).pack(pady=(0, 6))
|
||||
dlg.grab_set()
|
||||
|
||||
def _show_solid(self, color):
|
||||
idx = self.cbo.current()
|
||||
if idx < 0 or idx >= len(self.displays):
|
||||
@@ -1041,7 +1348,7 @@ def run_gui(auto_close=0):
|
||||
# ============================================================
|
||||
# 入口
|
||||
# ============================================================
|
||||
def run_dump():
|
||||
def run_dump(edid_fields=False):
|
||||
displays = get_displays()
|
||||
if not displays:
|
||||
print('未找到活动显示器')
|
||||
@@ -1053,6 +1360,9 @@ def run_dump():
|
||||
if not d['edid']:
|
||||
print(' EDID 读取失败(注册表无 EDID 数据)')
|
||||
continue
|
||||
if edid_fields:
|
||||
print(edid_field_text(d['edid']))
|
||||
continue
|
||||
e = parse_edid(d['edid'])
|
||||
for k, v in edid_info_lines(e):
|
||||
print(f' {k:<14}: {v}')
|
||||
@@ -1077,6 +1387,8 @@ def run_dump():
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description='HDMI EDID 查看器 + 测试图案')
|
||||
ap.add_argument('--dump', action='store_true', help='控制台输出 EDID 解算结果, 不开 GUI')
|
||||
ap.add_argument('--edid-fields', action='store_true',
|
||||
help='控制台输出 EDID 逐字节字段解析表(配合 --dump 风格, 不开 GUI); exe 版不支持')
|
||||
ap.add_argument('--test-pattern',
|
||||
choices=list(PATTERN_NAMES) + [f'Custom{i + 1}' for i in range(SLOT_COUNT)],
|
||||
default=None, help='直接全屏出测试图案(不开主窗口; CustomN 为自定义栏位)')
|
||||
@@ -1086,16 +1398,21 @@ def main():
|
||||
ap.add_argument('--seconds', type=int, default=0, help='自动退出秒数(GUI/图案冒烟测试)')
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.dump:
|
||||
# LT8619C 板卡热插拔后可能停留在"仅电脑屏幕"拓扑, 先强制扩展,
|
||||
# 否则 get_displays() 枚举不到板卡的"显示器"(失败静默)
|
||||
ensure_display_extend()
|
||||
|
||||
if args.dump or args.edid_fields:
|
||||
if getattr(sys, 'frozen', False):
|
||||
# --noconsole 打包的 exe 没有控制台可写, 引导用户用源码方式
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
r = tk.Tk()
|
||||
r.withdraw()
|
||||
messagebox.showinfo('不支持', 'exe 版本无控制台输出。\n请用源码方式运行:\npython hdmi_gui.py --dump')
|
||||
messagebox.showinfo('不支持', 'exe 版本无控制台输出。\n请用源码方式运行:\n'
|
||||
'python hdmi_gui.py --edid-fields')
|
||||
sys.exit(1)
|
||||
sys.exit(run_dump())
|
||||
sys.exit(run_dump(edid_fields=args.edid_fields))
|
||||
if args.test_pattern:
|
||||
displays = get_displays()
|
||||
if not displays:
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Kingst VIS 压缩边沿流分析: 数字波形按占空比签名自动分段, 输出行结构/占空比/消隐明细
|
||||
# 用法: python la_wave_analyzer.py <capture.csv>
|
||||
# CSV 列: Time[s], 通道A, 通道B (时间分辨率 10ns; 默认抓 B 通道 D0/D1)
|
||||
# 判读基线见 doc/architecture/rgb_bit_swap_test.md §1.1
|
||||
import csv, statistics, time, sys
|
||||
|
||||
PATH = sys.argv[1] if len(sys.argv) > 1 else \
|
||||
r"E:\Hulk-Coding\LT8619C_DLPC3421_HDMI\tools\wave\B-D0-D7.csv"
|
||||
CLK_NS = 25.0 # 40MHz 像素时钟
|
||||
|
||||
t0 = time.time()
|
||||
rows = []
|
||||
with open(PATH, encoding='utf-8-sig', errors='replace') as f:
|
||||
r = csv.reader(f)
|
||||
next(r) # header
|
||||
for row in r:
|
||||
if len(row) < 3:
|
||||
continue
|
||||
rows.append((float(row[0]) * 1e9, int(row[1]), int(row[2]))) # ns
|
||||
|
||||
n = len(rows)
|
||||
t_start, t_stop = rows[0][0], rows[-1][0] + 1000.0 # 末行状态延续按1us估
|
||||
print(f"rows={n} span={t_stop - t_start / 1e9:.9f}s")
|
||||
|
||||
# ---- 按行区间累计高低时间, 50ms 窗口计算占空比 ----
|
||||
W = 50_000_000.0 # ns
|
||||
win = {'t0': t_start, 'hi0': 0.0, 'hi1': 0.0}
|
||||
wins = []
|
||||
for i in range(n - 1):
|
||||
t, d0, d1 = rows[i]
|
||||
dt = min(rows[i + 1][0], t_stop) - t
|
||||
if dt <= 0:
|
||||
continue
|
||||
win['hi0'] += dt if d0 else 0.0
|
||||
win['hi1'] += dt if d1 else 0.0
|
||||
if rows[i + 1][0] - win['t0'] >= W:
|
||||
span = rows[i + 1][0] - win['t0']
|
||||
wins.append((win['t0'], win['hi0'] / span, win['hi1'] / span))
|
||||
win = {'t0': rows[i + 1][0], 'hi0': 0.0, 'hi1': 0.0}
|
||||
|
||||
# ---- 窗口分类: 依 (d0duty, d1duty) 签名 ----
|
||||
def classify(a, b):
|
||||
hi = lambda x: x > 0.5
|
||||
if hi(a) and hi(b):
|
||||
return 'FF'
|
||||
if hi(a) and not hi(b):
|
||||
return '55'
|
||||
if not hi(a) and hi(b):
|
||||
return 'AA'
|
||||
return '00'
|
||||
|
||||
segs = []
|
||||
for t, a, b in wins:
|
||||
c = classify(a, b)
|
||||
if segs and segs[-1][0] == c:
|
||||
segs[-1][1].append((t, a, b))
|
||||
else:
|
||||
segs.append((c, [(t, a, b)]))
|
||||
|
||||
print(f"\n{'段':<4}{'签名':<6}{'时长(s)':<10}{'D0占空':<10}{'D1占空':<10}")
|
||||
for i, (c, ws) in enumerate(segs):
|
||||
dur = (ws[-1][0] - ws[0][0]) / 1e9
|
||||
a = statistics.mean(x[1] for x in ws)
|
||||
b = statistics.mean(x[2] for x in ws)
|
||||
print(f"{i:<6}{c:<8}{dur:<12.2f}{a:<12.1%}{b:<12.1%}")
|
||||
|
||||
# ---- 每段: 行周期 + 有效区宽度(长高电平段) ----
|
||||
print(f"\n按段结构分析 (时钟数 @40MHz):")
|
||||
for i, (c, ws) in enumerate(segs):
|
||||
seg_t0 = ws[0][0] - W / 2
|
||||
seg_t1 = ws[-1][0] + W / 2
|
||||
ch = 0 if c in ('FF', '55') else 1 # 选有效区为高的通道
|
||||
# 找长高电平段(>=15us)及其起点
|
||||
starts, widths = [], []
|
||||
for j in range(n - 1):
|
||||
t, d0, d1 = rows[j]
|
||||
if t < seg_t0 or t > seg_t1:
|
||||
continue
|
||||
lvl = d0 if ch == 0 else d1
|
||||
if lvl and rows[j + 1][0] - t >= 15_000.0:
|
||||
starts.append(t)
|
||||
widths.append(rows[j + 1][0] - t)
|
||||
if len(starts) < 3:
|
||||
print(f" 段{i} [{c}]: 线段不足({len(starts)}), 跳过")
|
||||
continue
|
||||
periods = [b - a for a, b in zip(starts, starts[1:]) if 20_000 < b - a < 35_000]
|
||||
mp = statistics.median(periods)
|
||||
mw = statistics.median(widths)
|
||||
print(f" 段{i} [{c}]: 行数~{len(starts)} 行周期={mp:.0f}ns={mp/CLK_NS:.1f}clk "
|
||||
f"有效高={mw:.0f}ns={mw/CLK_NS:.1f}clk 消隐={(mp-mw)/CLK_NS:.1f}clk")
|
||||
|
||||
# ---- 每段抽一行: 消隐区的边沿明细 ----
|
||||
print(f"\n消隐区边沿明细(每段第一行, ns 相对行起点, clk=时钟数):")
|
||||
for i, (c, ws) in enumerate(segs):
|
||||
seg_t0 = ws[0][0] - W / 2
|
||||
ch = 0 if c in ('FF', '55') else 1
|
||||
line_start = None
|
||||
for j in range(n - 1):
|
||||
t, d0, d1 = rows[j]
|
||||
if t < seg_t0:
|
||||
continue
|
||||
lvl = d0 if ch == 0 else d1
|
||||
if lvl and rows[j + 1][0] - t >= 15_000.0:
|
||||
line_start = t
|
||||
break
|
||||
if line_start is None:
|
||||
continue
|
||||
print(f" 段{i} [{c}] 行起点+有效区之后:")
|
||||
j0 = next(k for k in range(n - 1) if rows[k][0] >= line_start)
|
||||
out = []
|
||||
for k in range(j0, min(j0 + 30, n - 1)):
|
||||
t, d0, d1 = rows[k]
|
||||
dt = rows[k + 1][0] - t
|
||||
rel = t - line_start
|
||||
out.append(f" +{rel/1000:8.2f}us D0={d0} D1={d1} 持续{dt/CLK_NS:6.1f}clk")
|
||||
if rel > 30_000:
|
||||
break
|
||||
print('\n'.join(out[:18]))
|
||||
|
||||
print(f"\n耗时 {time.time()-t0:.1f}s")
|
||||
@@ -0,0 +1,74 @@
|
||||
# Kingst VIS 3 通道边沿流分段: 按占空比签名分段, 输出各通道占空比
|
||||
# 用法: python la_wave_analyzer3.py <capture.csv> (列: Time[s], ch8, ch9, ch10)
|
||||
import csv, time, statistics
|
||||
|
||||
import sys
|
||||
PATH = sys.argv[1] if len(sys.argv) else r"E:\Hulk-Coding\LT8619C_DLPC3421_HDMI\tools\wave\A1_RGB-SWAP.csv"
|
||||
|
||||
t0 = time.time()
|
||||
rows = []
|
||||
with open(PATH, encoding='utf-8-sig', errors='replace') as f:
|
||||
r = csv.reader(f)
|
||||
hdr = next(r)
|
||||
print('header:', hdr)
|
||||
for row in r:
|
||||
if len(row) < 4:
|
||||
continue
|
||||
rows.append((float(row[0]) * 1e9, int(row[1]), int(row[2]), int(row[3])))
|
||||
n = len(rows)
|
||||
t_last = rows[-1][0] + 1000.0
|
||||
print('rows=%d span=%.2fs' % (n, (t_last - rows[0][0]) / 1e9))
|
||||
|
||||
W = 50_000_000.0
|
||||
wins = []
|
||||
acc = {'t0': rows[0][0], 'hi': [0.0, 0.0, 0.0]}
|
||||
for i in range(n - 1):
|
||||
t, a, b, c = rows[i]
|
||||
dt = min(rows[i + 1][0], t_last) - t
|
||||
if dt <= 0:
|
||||
continue
|
||||
acc['hi'][0] += dt if a else 0.0
|
||||
acc['hi'][1] += dt if b else 0.0
|
||||
acc['hi'][2] += dt if c else 0.0
|
||||
if rows[i + 1][0] - acc['t0'] >= W:
|
||||
span = rows[i + 1][0] - acc['t0']
|
||||
wins.append((acc['t0'], tuple(x / span for x in acc['hi'])))
|
||||
acc = {'t0': rows[i + 1][0], 'hi': [0.0, 0.0, 0.0]}
|
||||
|
||||
|
||||
def sig(d):
|
||||
hi = [x > 0.5 for x in d]
|
||||
return ('D7' if hi[0] else '--') + '/' + ('D15' if hi[1] else '--') + '/' + ('D23' if hi[2] else '--')
|
||||
|
||||
|
||||
segs = []
|
||||
for t, d in wins:
|
||||
s = sig(d)
|
||||
if segs and segs[-1][0] == s:
|
||||
segs[-1][1].append((t, d))
|
||||
else:
|
||||
segs.append((s, [(t, d)]))
|
||||
|
||||
print('\n%-16s %10s %9s %9s %9s' % ('活动组签名', '时长(s)', 'D7占空', 'D15占空', 'D23占空'))
|
||||
for s, ws in segs:
|
||||
dur = ws[-1][0] - ws[0][0]
|
||||
m = [statistics.mean(x[1][k] for x in ws) for k in range(3)]
|
||||
print('%-16s %10.2f %8.1f%% %8.1f%% %8.1f%%' % (s, dur, m[0] * 100, m[1] * 100, m[2] * 100))
|
||||
|
||||
# 每段行结构抽查(第一段一次即可)
|
||||
s0, ws0 = segs[0]
|
||||
ch = 0
|
||||
starts = []
|
||||
seg_t0 = ws0[0][0] - W / 2
|
||||
for j in range(n - 1):
|
||||
t = rows[j][0]
|
||||
if t < seg_t0:
|
||||
continue
|
||||
if rows[j][1] and rows[j + 1][0] - t >= 15000.0:
|
||||
starts.append(t)
|
||||
if len(starts) >= 5 or t > seg_t0 + 0.3e9:
|
||||
break
|
||||
if len(starts) >= 2:
|
||||
p = statistics.median([b - a for a, b in zip(starts, starts[1:])])
|
||||
print('\n行周期抽查: %.0fns (%.1f clk @40MHz)' % (p, p / 25.0))
|
||||
print('耗时 %.1fs' % (time.time() - t0))
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"area": "800x600",
|
||||
"area": "640x360",
|
||||
"slots": [
|
||||
{
|
||||
"name": "西门子星",
|
||||
"file": "bw_star.png"
|
||||
},
|
||||
{
|
||||
"name": "频率条纹",
|
||||
"file": "bw_sweep.png"
|
||||
"name": "BW_ZONE",
|
||||
"file": "bw_zoneplate.png"
|
||||
},
|
||||
{
|
||||
"name": "渐变棋盘",
|
||||
"file": "bw_checker_sweep.png"
|
||||
"name": "BW_TEST",
|
||||
"file": "bw_testcard.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,101 @@
|
||||
# 0x606E 位交换自动化测试 — 经 RP2040 注入器
|
||||
# ============================================================
|
||||
# 流程: 读基线 -> 写各交换位并校验 -> 读回确认 -> 收尾还原
|
||||
# 注意: LA1010 波形对照按 doc/architecture/rgb_bit_swap_test.md §1.1 手工比对;
|
||||
# 本脚本负责寄存器侧的全部操作与校验, 打印每步期望的 LA 现象。
|
||||
# 用法: python bitswap_auto.py [COM口]
|
||||
# python bitswap_auto.py COM9 full 额外跑 G 通道(0x40)与 R 通道(0x20)
|
||||
|
||||
import serial, sys, time
|
||||
|
||||
PORT = sys.argv[1] if len(sys.argv) > 1 else 'COM9'
|
||||
FULL = len(sys.argv) > 2 and sys.argv[2].lower() == 'full'
|
||||
|
||||
BANK, REG = '60', '6E'
|
||||
STEPS = [ # (写值, 含义, 写入后 HDMI-Tool 应投的图, LA 预期变化 vs 基线)
|
||||
('00', '关闭交换(基准)', 'solid_0000AA / solid_000055',
|
||||
'D0:AA=13%/55=84%, D1:AA=72%/55=0.9% (基线)'),
|
||||
('80', 'B通道镜像', 'solid_0000AA',
|
||||
'与基线互换: D0 应变 ~84%, D1 应变 ~0.9%'),
|
||||
]
|
||||
if FULL:
|
||||
STEPS[1] = ('20', 'R通道镜像(6ch挂D23/D16时测)', 'solid_AA0000',
|
||||
'D23/D16 占空比互换')
|
||||
STEPS.append(('40', 'G通道镜像(6ch挂D15/D8时测)', 'solid_00AA00',
|
||||
'D15/D8 占空比互换'))
|
||||
STEPS.append(('80', 'B通道镜像', 'solid_0000AA',
|
||||
'D0/D1 与基线互换'))
|
||||
|
||||
|
||||
def xact(ser, cmd, wait=3.0):
|
||||
"""发命令收完整回显(注入含重试可能耗时数秒)"""
|
||||
ser.reset_input_buffer()
|
||||
ser.write((cmd + '\n').encode())
|
||||
lines = []
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < wait:
|
||||
ln = ser.readline().decode('utf-8', 'replace').strip()
|
||||
if ln:
|
||||
lines.append(ln)
|
||||
if ln.startswith('=') and ('读回' in ln or ':' in ln):
|
||||
break
|
||||
return lines
|
||||
|
||||
|
||||
def check(lines, expect_val):
|
||||
"""从回显提取 读回=XX 并与期望比较"""
|
||||
for ln in lines:
|
||||
if '读回=' in ln:
|
||||
rb = ln.split('读回=')[1].strip()
|
||||
ok = rb.upper() == expect_val.upper()
|
||||
return ok, rb
|
||||
return False, None
|
||||
|
||||
|
||||
ser = serial.Serial(PORT, timeout=1)
|
||||
time.sleep(0.5)
|
||||
print('== 0x606E 位交换自动化 == 端口 %s' % PORT)
|
||||
print(xact(ser, 's')[0])
|
||||
|
||||
log = []
|
||||
fails = 0
|
||||
try:
|
||||
# 0) 通信预检
|
||||
lines = xact(ser, 'r %s %s' % (BANK, REG))
|
||||
ok, rb = check(lines, '00')
|
||||
print('[预检] 读 0x%s%s -> %s (期望默认 00)' % (BANK, REG, rb))
|
||||
if not ok:
|
||||
print(' !! 预检值不是 00 —— 若刚做过实验未还原请先还原; 继续执行')
|
||||
|
||||
for val, meaning, img, expect in STEPS:
|
||||
print('\n--- 写 %s (%s) ---' % (val, meaning))
|
||||
lines = xact(ser, 'w %s %s %s' % (BANK, REG, val), wait=6.0)
|
||||
for ln in lines:
|
||||
print(' ' + ln)
|
||||
ok, rb = check(lines, val)
|
||||
if not ok:
|
||||
fails += 1
|
||||
print(' !! 校验失败(读回 %s != %s), 跳过此步' % (rb, val))
|
||||
log.append((val, 'FAIL', rb))
|
||||
continue
|
||||
log.append((val, 'OK', rb))
|
||||
print(' [寄存器侧 OK] 读回=%s' % rb)
|
||||
print(' [现在请投] %s' % img)
|
||||
print(' [LA 预期] %s' % expect)
|
||||
if val != '00':
|
||||
input(' >>> 抓完波形按回车继续...')
|
||||
|
||||
# 收尾还原
|
||||
print('\n--- 收尾: 写 00 还原 ---')
|
||||
lines = xact(ser, 'w %s %s 00' % (BANK, REG), wait=6.0)
|
||||
ok, rb = check(lines, '00')
|
||||
print(' 读回=%s %s' % (rb, 'OK' if ok else 'FAIL'))
|
||||
if not ok:
|
||||
fails += 1
|
||||
finally:
|
||||
ser.close()
|
||||
|
||||
print('\n== 结果汇总 ==')
|
||||
for val, st, rb in log:
|
||||
print(' 写%s: %s (读回 %s)' % (val, st, rb))
|
||||
print('失败步骤: %d' % fails)
|
||||
@@ -0,0 +1,117 @@
|
||||
# RP2040 注入器嗅探数据 -> I2C 协议解码
|
||||
# 用法: python i2c_sniff_decode.py [COM口] [采集ms, 默认700]
|
||||
# 帧格式: 固件 'c' 命令回吐 "C <t_us> <val>" 行, val bit0=SDA bit1=SCL
|
||||
import serial, sys, time
|
||||
|
||||
PORT = sys.argv[1] if len(sys.argv) > 1 else 'COM9'
|
||||
MS = int(sys.argv[2]) if len(sys.argv) > 2 else 700
|
||||
|
||||
KNOWN_REGS = {0x44: 'SYS_STATUS', 0x6E: 'TTL_8B_CFG', 0xFF: 'BANK'}
|
||||
|
||||
s = serial.Serial(PORT, timeout=1)
|
||||
time.sleep(0.3)
|
||||
s.reset_input_buffer()
|
||||
s.write(b's\n')
|
||||
time.sleep(0.3)
|
||||
print(s.read(200).decode('utf-8', 'replace').strip())
|
||||
s.write(('c %d\n' % MS).encode())
|
||||
|
||||
edges = []
|
||||
t_deadline = time.time() + MS / 1000 + 60
|
||||
while time.time() < t_deadline:
|
||||
ln = s.readline().decode('ascii', 'replace').strip()
|
||||
if ln == 'C.':
|
||||
break
|
||||
if ln.startswith('C '):
|
||||
try:
|
||||
a, b = ln[2:].split()
|
||||
edges.append((int(a), int(b)))
|
||||
except ValueError:
|
||||
pass
|
||||
elif ln.startswith('='):
|
||||
print(ln)
|
||||
s.close()
|
||||
print('edges:', len(edges))
|
||||
|
||||
# ---------------- I2C 解码 ----------------
|
||||
trans, cur, bits = [], None, []
|
||||
psda = pscl = None
|
||||
|
||||
def flush_byte():
|
||||
global bits
|
||||
if cur is not None and len(bits) == 8:
|
||||
cur['bytes'].append(int(''.join(str(x) for x in bits), 2))
|
||||
bits = []
|
||||
|
||||
for t, v in edges:
|
||||
sda, scl = v & 1, (v >> 1) & 1
|
||||
if pscl is not None:
|
||||
if scl == 1 and pscl == 1: # SCL 高电平期间 SDA 变化
|
||||
if psda == 1 and sda == 0: # START / repeated START
|
||||
if cur is not None:
|
||||
cur['t1'] = t
|
||||
cur['inc'] = bool(bits)
|
||||
trans.append(cur)
|
||||
cur = {'t0': t, 'bytes': [], 'acks': [], 'inc': False}
|
||||
bits = []
|
||||
elif psda == 0 and sda == 1: # STOP
|
||||
if cur is not None:
|
||||
cur['t1'] = t
|
||||
cur['inc'] = bool(bits)
|
||||
trans.append(cur)
|
||||
cur = None
|
||||
bits = []
|
||||
elif scl == 1 and pscl == 0: # SCL 上升沿采样
|
||||
if cur is not None:
|
||||
bits.append(sda)
|
||||
if len(bits) == 8:
|
||||
cur['bytes'].append(int(''.join(str(x) for x in bits), 2))
|
||||
bits = []
|
||||
elif len(bits) == 9:
|
||||
cur['acks'].append(sda) # 0=ACK
|
||||
bits = []
|
||||
psda, pscl = sda, scl
|
||||
if cur is not None:
|
||||
cur['t1'] = edges[-1][0] if edges else 0
|
||||
cur['inc'] = bool(bits)
|
||||
trans.append(cur)
|
||||
|
||||
# ---------------- 打印 ----------------
|
||||
bank = None
|
||||
print(f'\n解码出 {len(trans)} 个事务:')
|
||||
for k, tr in enumerate(trans):
|
||||
d = tr['bytes']
|
||||
if not d:
|
||||
print(' [%2d] t=%dus <空/片段>' % (k, tr['t0']))
|
||||
continue
|
||||
addr7, rw = d[0] >> 1, 'R' if d[0] & 1 else 'W'
|
||||
acks = tr['acks']
|
||||
body = d[1:]
|
||||
ann = []
|
||||
if addr7 == 0x32 and rw == 'W' and body:
|
||||
if body[0] == 0xFF and len(body) >= 2:
|
||||
bank = body[1]
|
||||
ann.append('bank=%02X' % bank)
|
||||
body = body[2:]
|
||||
if body:
|
||||
r = body[0]
|
||||
note = KNOWN_REGS.get(r, '')
|
||||
if r == 0xFF:
|
||||
bank = body[1] if len(body) > 1 else bank
|
||||
note = 'bank=%02X' % bank if len(body) > 1 else note
|
||||
ann.append(('[%02X%02X] %s' % (bank or 0, r, note)) if note else '[%02X?%02X]' % (bank or 0, r))
|
||||
ackstr = ''.join('A' if a == 0 else 'N' for a in acks[1:]) if len(acks) > 1 else ''
|
||||
flag = ' <未收尾>' if tr.get('inc') else ''
|
||||
print(' [%2d] t=%7dus 0x%02X %s: %-30s ACK:%s%s'
|
||||
% (k, tr['t0'], addr7, rw,
|
||||
' '.join('%02X' % x for x in d[1:12]) + (' …' if len(d) > 12 else ''),
|
||||
ackstr[:16], flag))
|
||||
|
||||
# 轮询周期统计(相邻事务起始间隔的主值)
|
||||
if len(trans) > 3:
|
||||
gaps = [b['t0'] - a['t0'] for a, b in zip(trans, trans[1:])]
|
||||
gaps = [g for g in gaps if 1000 < g < 5000000]
|
||||
if gaps:
|
||||
gaps.sort()
|
||||
print('\n相邻事务间隔(us): 中位=%d 最小=%d 最大=%d'
|
||||
% (gaps[len(gaps) // 2], gaps[0], gaps[-1]))
|
||||
Reference in New Issue
Block a user