From c38f12b230a27d930d9c30c492d84ac60de943c4 Mon Sep 17 00:00:00 2001 From: hulk Date: Thu, 3 Sep 2026 17:24:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20HDMI-Tool=20=E4=B8=8E=20I2C-Inject=20?= =?UTF-8?q?=E9=A6=96=E7=89=88=E5=B7=A5=E5=85=B7=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 8 + HDMI-Tool/README.md | 76 ++ HDMI-Tool/build_exe.py | 82 ++ HDMI-Tool/bw_checker_sweep.png | Bin 0 -> 2615 bytes HDMI-Tool/bw_star.png | Bin 0 -> 8044 bytes HDMI-Tool/bw_sweep.png | Bin 0 -> 1808 bytes HDMI-Tool/bw_testcard.png | Bin 0 -> 2506 bytes HDMI-Tool/bw_zoneplate.png | Bin 0 -> 10416 bytes HDMI-Tool/gen_bw_patterns.py | 185 +++++ HDMI-Tool/gen_solid_colors.py | 44 + HDMI-Tool/hdmi_gui.py | 1126 ++++++++++++++++++++++++++ HDMI-Tool/hdmi_source.bat | 7 + HDMI-Tool/hdmi_source.ps1 | 487 +++++++++++ HDMI-Tool/logos/logo_ph_orange.ico | Bin 0 -> 22047 bytes HDMI-Tool/logos/logo_ph_orange.png | Bin 0 -> 7522 bytes HDMI-Tool/logos/logo_ph_orange.svg | 6 + HDMI-Tool/logos/make_ico.py | 48 ++ HDMI-Tool/pattern_slots.json | 17 + HDMI-Tool/solid_000055.png | Bin 0 -> 2788 bytes HDMI-Tool/solid_0000AA.png | Bin 0 -> 2788 bytes HDMI-Tool/solid_0000FF.png | Bin 0 -> 2786 bytes HDMI-Tool/solid_005500.png | Bin 0 -> 2787 bytes HDMI-Tool/solid_00AA00.png | Bin 0 -> 2787 bytes HDMI-Tool/solid_00FF00.png | Bin 0 -> 2786 bytes HDMI-Tool/solid_550000.png | Bin 0 -> 2787 bytes HDMI-Tool/solid_AA0000.png | Bin 0 -> 2787 bytes HDMI-Tool/solid_FF0000.png | Bin 0 -> 2785 bytes I2C-Inject/README.md | 70 ++ I2C-Inject/build_exe.py | 61 ++ I2C-Inject/i2c_inject_gui.py | 272 +++++++ I2C-Inject/logos/logo_i2c_orange.ico | Bin 0 -> 26164 bytes I2C-Inject/logos/logo_i2c_orange.png | Bin 0 -> 12539 bytes I2C-Inject/logos/logo_i2c_orange.svg | 6 + I2C-Inject/logos/make_ico_i2c.py | 58 ++ I2C-Inject/rp2040_inject.py | 192 +++++ README.md | 34 +- 36 files changed, 2778 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 HDMI-Tool/README.md create mode 100644 HDMI-Tool/build_exe.py create mode 100644 HDMI-Tool/bw_checker_sweep.png create mode 100644 HDMI-Tool/bw_star.png create mode 100644 HDMI-Tool/bw_sweep.png create mode 100644 HDMI-Tool/bw_testcard.png create mode 100644 HDMI-Tool/bw_zoneplate.png create mode 100644 HDMI-Tool/gen_bw_patterns.py create mode 100644 HDMI-Tool/gen_solid_colors.py create mode 100644 HDMI-Tool/hdmi_gui.py create mode 100644 HDMI-Tool/hdmi_source.bat create mode 100644 HDMI-Tool/hdmi_source.ps1 create mode 100644 HDMI-Tool/logos/logo_ph_orange.ico create mode 100644 HDMI-Tool/logos/logo_ph_orange.png create mode 100644 HDMI-Tool/logos/logo_ph_orange.svg create mode 100644 HDMI-Tool/logos/make_ico.py create mode 100644 HDMI-Tool/pattern_slots.json create mode 100644 HDMI-Tool/solid_000055.png create mode 100644 HDMI-Tool/solid_0000AA.png create mode 100644 HDMI-Tool/solid_0000FF.png create mode 100644 HDMI-Tool/solid_005500.png create mode 100644 HDMI-Tool/solid_00AA00.png create mode 100644 HDMI-Tool/solid_00FF00.png create mode 100644 HDMI-Tool/solid_550000.png create mode 100644 HDMI-Tool/solid_AA0000.png create mode 100644 HDMI-Tool/solid_FF0000.png create mode 100644 I2C-Inject/README.md create mode 100644 I2C-Inject/build_exe.py create mode 100644 I2C-Inject/i2c_inject_gui.py create mode 100644 I2C-Inject/logos/logo_i2c_orange.ico create mode 100644 I2C-Inject/logos/logo_i2c_orange.png create mode 100644 I2C-Inject/logos/logo_i2c_orange.svg create mode 100644 I2C-Inject/logos/make_ico_i2c.py create mode 100644 I2C-Inject/rp2040_inject.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f6ff9ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Python +__pycache__/ +*.pyc + +# PyInstaller 工作目录(打包产物 exe 已直接入库, 构建中间物不入库) +build/ +dist/ +*.spec diff --git a/HDMI-Tool/README.md b/HDMI-Tool/README.md new file mode 100644 index 0000000..f0d4f0f --- /dev/null +++ b/HDMI-Tool/README.md @@ -0,0 +1,76 @@ +# HDMI-Tool — HDMI 源端测试工具(LT8619C/DLPC3421 转接板调试) + +> 更新:2026-09-03。单文件 Python GUI(`hdmi_gui.py`),另附命令行分辨率/图案工具 +> `hdmi_source.bat`。已打包版直接双击 `HDMI-Tool.exe`,无需安装 Python。 + +--- + +## 1. 功能总览 + +| 功能 | 说明 | +|---|---| +| EDID 查看 | 枚举各显示输出口,从注册表读取 EDID 原始数据,按 EDID 1.4/CEA-861 解算:厂商/型号/尺寸/支持模式(DTD/已建立/标准/CEA-VIC)/首选时序完整参数/校验和 | +| 分辨率切换 | 第二行"分辨率"下拉列出显卡按该屏 EDID 提供的全部模式;**应用**=临时切换(不写注册表,重启还原),**恢复**=回到本次会话切改前的模式 | +| 测试图案 | 全屏投到所选显示器,测试卡区域可选 `640x360 / 800x600 / full 铺满`;内置 纯红/纯绿/纯蓝/棋盘格/网格线 + R/G/B 三行纯色值按钮(55/AA/FF,与 dist 下 solid_*.png 对应)+ 3 个自定义图片栏位 | +| 命令行模式 | 自动化测试脚本可直接调用(见 §4) | + +## 2. 快速上手(GUI) + +1. 双击 `HDMI-Tool.exe`(或 `python hdmi_gui.py`,需 Python ≥3.10 + pyserial 不需要,纯标准库;自定义图片 JPG/BMP 需 Pillow) +2. 顶部显示器下拉选择目标屏(多屏时注意别选成主屏),"刷新"重新枚举 +3. **切分辨率**:第二行选模式 → 点"应用";测完点"恢复" +4. **投测试图**: + - 先在图案按钮行右侧选**测试卡区域**(640x360 / 800x600 / full) + - 点图案按钮即全屏投放;按 **ESC** 退出 + - R/G/B 三行纯色按钮(55/AA/FF)用于位交换类测试:55 与 AA 互为字节镜像对,FF 为全高基准 + - 自定义图片:点"⚙"配置名称与图片文件(图片会复制到本目录),点栏位名投放 +5. 区域与自定义栏位配置自动保存在本目录 `pattern_slots.json` + +## 3. 典型场景(LT8619C demo 板测试) + +``` +1. HDMI 接 demo 板 → 启动 HDMI-Tool → 刷新 → 选中 demo 板显示器 +2. 分辨率切 800x600@60(demo 板 shadow EDID 首选)→ 测试卡区域选 800x600 +3. 点 R/G/B 行的纯色按钮投图 → 配合逻辑分析仪/万用表测 RGB 输出 +4. 测完"恢复"分辨率 +``` + +## 4. 命令行模式 + +``` +python hdmi_gui.py --dump 控制台输出 EDID 解算(exe 版不支持) +python hdmi_gui.py --test-pattern Blue --area 800x600 --seconds 5 + 出图 5 秒自动退出 +python hdmi_gui.py --test-pattern Custom1 投自定义栏位 1 + --area {640x360,800x600,full} --monitor N --seconds S +``` + +**hdmi_source.bat**(PowerShell 源端工具,功能与 GUI 互补): + +``` +tools\hdmi_source.bat -List 枚举显示器与支持分辨率(验证 EDID) +tools\hdmi_source.bat -Monitor 1 -Mode 800x600 切模式 +tools\hdmi_source.bat -Mode 800x600 -Pattern -Restore + 切模式→出图案→退出后自动恢复 +``` + +## 5. 目录内容 + +| 文件 | 用途 | +|---|---| +| `HDMI-Tool.exe` | 打包好的单文件版(--noconsole;`--dump` 请用源码方式跑) | +| `hdmi_gui.py` | 主程序源码 | +| `build_exe.py` | 打包脚本(`python build_exe.py`,自动装 PyInstaller;`--debug` 额外出带控制台版) | +| `hdmi_source.ps1 / .bat` | 源端分辨率/图案命令行工具 | +| `gen_bw_patterns.py` / `gen_solid_colors.py` | 测试图生成脚本(bw 黑白系列 640x360 / solid 纯色系列 800x600) | +| `solid_*.png` | 9 张 800x600 纯色图(R/G/B 各 55/AA/FF,供自定义栏位或脚本使用) | +| `bw_*.png` | 5 张 640x360 黑白测试图(西门子星/频率条纹/渐变棋盘/波带/综合卡) | +| `logos/` | 图标源文件与 `make_ico.py`(改 logo 后重跑再打包) | +| `pattern_slots.json` | 自定义栏位 + 测试卡区域配置(运行后自动生成/更新) | + +## 6. 注意事项 + +- 测试卡区域与自定义栏位配置按 exe/脚本所在目录各自保存,两份安装互不影响 +- 重打包后 Windows 图标缓存可能仍显示旧图标:`ie4uinit -show` 或改名即可 +- 多屏环境切分辨率前先确认显示器序号(下拉框中名称与 `-List` 输出一致) +- 修改源码后重新打包前,先结束残留的 HDMI-Tool 进程(build_exe.py 会自动处理) diff --git a/HDMI-Tool/build_exe.py b/HDMI-Tool/build_exe.py new file mode 100644 index 0000000..b729cbf --- /dev/null +++ b/HDMI-Tool/build_exe.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +把 hdmi_gui.py 打包成单文件 exe(工具名: HDMI-Tool) +================================ +使用 PyInstaller --onefile --noconsole: Python 解释器 + 标准库 + tkinter +全部打进一个 exe, 目标机器无需安装任何依赖。 + +用法: + python build_exe.py 自动安装 PyInstaller(如缺)并打包 + python build_exe.py --debug 额外打一个带控制台的变体(--dump 可用) + +产物: tools/dist/HDMI-Tool.exe(及 --debug 时的 HDMI-Tool-console.exe) +首次运行 onefile exe 会先解压到临时目录, 启动比脚本慢一两秒, 属正常现象。 +""" + +import os +import subprocess +import sys + +BASE = os.path.dirname(os.path.abspath(__file__)) +ICO = os.path.join(BASE, 'logos', 'logo_ph_orange.ico') + + +def ensure_pyinstaller(): + try: + import PyInstaller # noqa: F401 + return + except ImportError: + pass + print('PyInstaller 未安装, 正在安装...') + subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'pyinstaller']) + + +def remove_stale(exe_path): + """删除旧 exe; 若被占用(上次验证的进程还在), 先结束同名进程再重试""" + if not os.path.exists(exe_path): + return + try: + os.remove(exe_path) + return + except PermissionError: + pass + import time + print(f'{exe_path} 被占用, 结束残留进程后重试...') + subprocess.run(['taskkill', '/F', '/IM', os.path.basename(exe_path)], + capture_output=True) + time.sleep(1.0) + os.remove(exe_path) + + +def build(name, extra_args): + dist = os.path.join(BASE, 'dist') + work = os.path.join(BASE, 'build') + remove_stale(os.path.join(dist, name + '.exe')) + cmd = [sys.executable, '-m', 'PyInstaller', + '--onefile', '--clean', '--name', name, + '--distpath', dist, '--workpath', work, '--specpath', work, + ] + extra_args + [os.path.join(BASE, 'hdmi_gui.py')] + if os.path.exists(ICO): + # exe 文件图标 + 运行时窗口图标资源(从 _MEIPASS 读取) + cmd += ['--icon', ICO, '--add-data', f'{ICO};.'] + else: + print('警告: 未找到 logos/logo_ph_orange.ico(先运行 logos/make_ico.py), 本次打包不带图标') + print('\n>>', ' '.join(cmd)) + subprocess.check_call(cmd) + out = os.path.join(dist, name + '.exe') + print(f'完成: {out} ({os.path.getsize(out) / 1024 / 1024:.1f} MB)') + + +def main(): + build_console = '--debug' in sys.argv + ensure_pyinstaller() + # 常规版: 双击即用, 无黑框(GUI/图案/自定义栏位全部可用, --dump 除外) + build('HDMI-Tool', ['--noconsole']) + if build_console: + # 调试版: 带控制台, --dump / 异常栈可见 + build('HDMI-Tool-console', []) + + +if __name__ == '__main__': + main() diff --git a/HDMI-Tool/bw_checker_sweep.png b/HDMI-Tool/bw_checker_sweep.png new file mode 100644 index 0000000000000000000000000000000000000000..24fcfe3cf9c9b3d14252b220f383db2d3c0dc2d3 GIT binary patch literal 2615 zcmb`JeNad{Q;#`#+I@_=_a5fEo;BoI*E4y|r@p6dar)=h0z!HYCHAAU zQPn=w*usFWt@qCZfJQypK5^w)k*^V7G?&#T!Rb5t(+dZTEKP}`aU_kjGd%ViQLUwg z#>2Dmnc~{KW|lN7X(qnAwHxPEpZbiY9ev#>=%fRd^`w~k$?L_X3hnE>?12ZFK$o=^yBR6F(yA4Nrtwg&h(}luoJ%GsR}i-{+U% zNxH%ioiR=z(JFW5XPHJ0lIjJj&NB9wowXV{@F|=)YF83H*XhCc&zQQyo@Rm0`J}v7 z!Jy$wkWs5LJBMlfQqA*B|Rp@ze zy=6rIf=v^RY77cRy9P|`!>w~Z37CramG}8epTBfQ17n;hs;rxpnZ80Y1MrCoQ3fp( zZ_@LBhz9)NipH3o(f;`kF>@35W^QCOtMp+yu7lQ~Pc{D(F7A_#*)^!A50?_N@$k;$ zikY9k{e1dV`y+(cq1BP2v`g8FYYDz|pn0;{g~_T*XKxw;y=n3fKl5z3C_rLU@Jedg z@v`&uMECxop@~m7d(D}im65#zlkw$bZ(r{f8(wAvR2-EP##Vn6w6^X1T~6RL$0Mjm zmQB~NjJWZGPA36w>u%SFtM*@TD%ck39r6mEvjMT;I>xUMrEf|c-t|5_hvtZAa!J~^ z>Gj#s!^LbVm>Ss5&DnuoxN#%3PD$(8D3<=2KHs%=^69)$V)Lb@#SU=+ITi@O}c zO(e048_VMm>T=|S^uPyxNF6Cm{}R5Xn#J7wUK+N;nJ?NtR$tN!Q~Lbdi> zWbYodcK-#kmf*RoW!3&rz^R%PQ#}qxe_{KHag>XJ`&@Vip7R~j;h2l|PKs1Xo_?M@ zj_^uuO@SoQhjX~p_$I> zmnffXwhanm4MHtRN;y}wG~ZBRXud;G=X_Rd0KHaDfR6nW88T>OFo#`}I*DHcAi~w< z{~mW++PD#k{f7;#i;Kp#5vY$ng$q+$unqD>O_LkA*%C^_$8YyR$uXwv3JF%FK6mlA zBDx&2$k3L1voS&ea*{|w(W6`@9UuBQEc#_7#x}8L1GhK|-9SFhlm*+~t(GXSVx1<> zzBSA`RL}w2P4=6is#ArdCKp^}$$g_NX+zV_(UO;c7c+st!Kcg7Yv9Hu$PeDUGAnz!g#Uif@7J}ezCq{zZqi4Ivb`-*L|X8P+jlvX-HV?r{1G158%z-H Hrf~latsCf6 literal 0 HcmV?d00001 diff --git a/HDMI-Tool/bw_star.png b/HDMI-Tool/bw_star.png new file mode 100644 index 0000000000000000000000000000000000000000..de2e9d8376ff04529e07582741a4efa2915d5771 GIT binary patch literal 8044 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 literal 0 HcmV?d00001 diff --git a/HDMI-Tool/bw_sweep.png b/HDMI-Tool/bw_sweep.png new file mode 100644 index 0000000000000000000000000000000000000000..6e9a59aeb368df889cfbccb5bd53b3560fa7881d GIT binary patch literal 1808 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', fill=0) + d.text((8, H // 2 + 6), 'V sweep 24px->3px', fill=0) + save(img, 'bw_sweep.png') + + +def gen_checker_sweep(): + """渐变棋盘: 格子尺寸从 2px 逐段加倍到 32px, 看多大格子开始可分辨""" + img = Image.new('L', (W, H), 0) + d = ImageDraw.Draw(img) + sizes = [2, 3, 4, 6, 8, 12, 16, 24, 32] + seg = W / len(sizes) + for i, s in enumerate(sizes): + x0, x1 = int(round(i * seg)), int(round((i + 1) * seg)) + for yy in range(0, H, s): + for xx in range(x0, x1, s): + if ((xx // s) + (yy // s)) % 2 == 0: + d.rectangle([xx, yy, min(xx + s, x1) - 1, min(yy + s, H) - 1], + fill=255) + d.line([x1, 0, x1, H], fill=255, width=2) + d.text((4, 4), 'checker 2..32px', fill=255) + save(img, 'bw_checker_sweep.png') + + +def gen_zoneplate(): + """同心波带片: 环距中心处约 31px、边缘约 3.5px, 观察各方向混叠""" + img = Image.new('L', (W, H), 255) + px = img.load() + cx, cy, k = W / 2, H / 2, 0.0008 + for y in range(H): + dy = y - cy + for x in range(W): + dx = x - cx + if math.cos(k * (dx * dx + dy * dy)) > 0: + px[x, y] = 0 + d = ImageDraw.Draw(img) + d.rectangle([0, 0, W - 1, H - 1], outline=0, width=2) + save(img, 'bw_zoneplate.png') + + +def gen_testcard(): + """综合测试卡: 灰阶条 + 刻度 + 细棋盘 + 粗细线 + 角标""" + img = Image.new('L', (W, H), 0) + d = ImageDraw.Draw(img) + + # 外框与 40px 刻度(边缘短刻线) + d.rectangle([0, 0, W - 1, H - 1], outline=255, width=2) + for x in range(40, W, 40): + d.line([x, 0, x, 10], fill=255, width=1) + d.line([x, H, x, H - 10], fill=255, width=1) + for y in range(40, H, 40): + d.line([0, y, 10, y], fill=255, width=1) + d.line([W, y, W - 10, y], fill=255, width=1) + + # 顶部 16 级灰阶条 + gw = W // 16 + for i in range(16): + v = round(i * 255 / 15) + d.rectangle([i * gw, 20, (i + 1) * gw - 1, 44], fill=v, outline=255) + + # 左下: 1px 棋盘块 (640/4=160 宽 x 72 高) + bx, by, bs = 16, H - 96, 2 + for yy in range(by, by + 72, bs): + for xx in range(bx, bx + 160, bs): + if ((xx // bs) + (yy // bs)) % 2 == 0: + d.rectangle([xx, yy, xx + bs - 1, yy + bs - 1], fill=255) + d.text((bx, by - 14), '2px checker', fill=255) + + # 右下: 1/2/4px 白线组(横线), 看线条再现 + lx = W - 210 + for row, lw in enumerate((1, 2, 4)): + y0 = H - 100 + row * 26 + for off in range(0, 13, lw * 2): + d.line([lx, y0 + off, lx + 190, y0 + off], fill=255, width=lw) + d.text((lx, H - 114), '1/2/4px lines', fill=255) + + # 中心十字(1px) + cx, cy = W // 2, H // 2 + d.line([cx, 60, cx, H - 60], fill=255, width=1) + d.line([W // 4, cy, W - W // 4, cy], fill=255, width=1) + + # 四角 L 标记 + m, L = 16, 28 + for sx, sy in ((m, m), (W - 1 - m, m), (m, H - 1 - m), (W - 1 - m, H - 1 - m)): + dx = -1 if sx > cx else 1 + dy = -1 if sy > cy else 1 + d.line([sx, sy, sx + dx * L, sy], fill=255, width=2) + d.line([sx, sy, sx, sy + dy * L], fill=255, width=2) + + d.text((W // 2 - 60, 52), '640x360 TEST', fill=255) + save(img, 'bw_testcard.png') + + +def write_slots(): + slots = [ + {'name': '西门子星', 'file': 'bw_star.png'}, + {'name': '频率条纹', 'file': 'bw_sweep.png'}, + {'name': '渐变棋盘', 'file': 'bw_checker_sweep.png'}, + ] + path = os.path.join(OUT, 'pattern_slots.json') + with open(path, 'w', encoding='utf-8') as f: + json.dump(slots, f, ensure_ascii=False, indent=2) + print(f'写出 {path}(前 3 张已配入自定义栏位, 其余两张可用 "⚙" 换上)') + + +if __name__ == '__main__': + gen_star() + gen_sweep() + gen_checker_sweep() + gen_zoneplate() + gen_testcard() + write_slots() diff --git a/HDMI-Tool/gen_solid_colors.py b/HDMI-Tool/gen_solid_colors.py new file mode 100644 index 0000000..b6cc4d8 --- /dev/null +++ b/HDMI-Tool/gen_solid_colors.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +生成 800x600 纯色测试图, 供 RGB 位交换(0x606E)验证使用 +================================================================ +配合 demo 板当前 EDID 首选时序 800x600@60, 全屏纯色输出。 +用法: python gen_solid_colors.py → 输出到 tools/dist/solid_*.png +在 HDMI-Tool 中通过自定义栏位(⚙ 换图)加载。 +""" + +import os + +from PIL import Image + +W, H = 800, 600 # 与 demo 板 EDID 首选时序一致, 全屏铺满 +BASE = os.path.dirname(os.path.abspath(__file__)) +OUT = os.path.join(BASE, 'dist') + +# (文件名后缀, RGB 值, 用途) +COLORS = [ + ('FF0000', (0xFF, 0x00, 0x00), '通道映射: R=D[23:16]'), + ('00FF00', (0x00, 0xFF, 0x00), '通道映射: G=D[15:8]'), + ('0000FF', (0x00, 0x00, 0xFF), '通道映射: B=D[7:0]'), + ('AA0000', (0xAA, 0x00, 0x00), 'R 交换检测: 0xAA(10101010)'), + ('550000', (0x55, 0x00, 0x00), 'R 交换检测: 0x55(01010101)'), + ('00AA00', (0x00, 0xAA, 0x00), 'G 交换检测'), + ('005500', (0x00, 0x55, 0x00), 'G 交换检测'), + ('0000AA', (0x00, 0x00, 0xAA), 'B 交换检测'), + ('000055', (0x00, 0x00, 0x55), 'B 交换检测'), +] + + +def main(): + os.makedirs(OUT, exist_ok=True) + for suffix, rgb, note in COLORS: + img = Image.new('RGB', (W, H), rgb) + path = os.path.join(OUT, f'solid_{suffix}.png') + img.save(path) + print(f'写出 {path} # {note}') + print(f'\n共 {len(COLORS)} 张, 分辨率 {W}x{H}(匹配 demo 板 800x600 EDID)。') + + +if __name__ == '__main__': + main() diff --git a/HDMI-Tool/hdmi_gui.py b/HDMI-Tool/hdmi_gui.py new file mode 100644 index 0000000..ad6cd12 --- /dev/null +++ b/HDMI-Tool/hdmi_gui.py @@ -0,0 +1,1126 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +HDMI EDID 查看器 + 测试图案投屏工具 +================================================ +配合 LT8619C(HDMI 转 RGB888)→ DLPC3421 转接板调试使用。 + +功能: + 1. 枚举各显示输出口接的设备, 从注册表读取其 EDID 原始数据, + 按 EDID 1.4 / CEA-861 标准解算: 厂商/产品/名称/尺寸/Gamma/输入类型、 + 支持的全部模式(详细时序 DTD / 已建立时序 / 标准时序 / CEA VIC)、 + 首选时序完整参数(像素时钟/消隐/同步极性)、CEA 扩展能力、校验和。 + 2. 向所选屏幕全屏投测试图案(黑底 1:1 居中, 测试卡区域可选 + 640x360 / 800x600 / full 铺满): 纯红 / 纯绿 / 纯蓝 / 黑白棋盘格 / + 网格线 + 3 个自定义图片栏位, 点击或 ESC 退出。 + +仅用标准库(ctypes / winreg / tkinter), 无需 pip 安装任何依赖。 + +命令行模式(自动化测试用): + python hdmi_gui.py 启动 GUI + python hdmi_gui.py --dump 控制台输出 EDID 解算结果 + python hdmi_gui.py --test-pattern Checker --seconds 3 + 直接出图案, 3 秒自动退出 + python hdmi_gui.py --test-pattern Blue --area 800x600 + 指定测试卡区域(full=铺满所选屏) + python hdmi_gui.py --seconds 5 GUI 5 秒自动关闭(冒烟测试) +""" + +import argparse +import ctypes +import ctypes.wintypes as wt +import json +import math +import os +import shutil +import struct +import sys +import winreg + +# 进程级 DPI 感知: 保证图案窗口按物理像素精确定位/绘制 +try: + ctypes.windll.shcore.SetProcessDpiAwareness(2) +except Exception: + pass + +# 可选 Pillow: 装了则支持 JPEG/BMP/WebP, 没装则用 tkinter 原生(仅 PNG/GIF) +try: + from PIL import Image, ImageTk + HAS_PIL = True +except ImportError: + HAS_PIL = False + +# ============================================================ +# 自定义图案栏位: 配置存于脚本目录 pattern_slots.json, +# 图片文件同样保存在脚本目录下 +# 注: PyInstaller onefile 打包后 __file__ 指向临时解压目录, +# 配置必须落在 exe 旁边, 故 frozen 时改用 sys.executable 定位 +# ============================================================ +if getattr(sys, 'frozen', False): + BASE_DIR = os.path.dirname(os.path.abspath(sys.executable)) +else: + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +SLOT_COUNT = 3 +SLOT_CONFIG = os.path.join(BASE_DIR, 'pattern_slots.json') +IMG_EXTS = ['.png', '.gif'] + (['.jpg', '.jpeg', '.bmp', '.webp'] if HAS_PIL else []) + + +def load_slots(): + """读 pattern_slots.json, 返回 (slots, area)。 + + 新格式为 {'area': 'WxH'|'full', 'slots': [...]}; 兼容旧版纯数组格式(无 area)。 + """ + slots = [{'name': f'自定义{i + 1}', 'file': None} for i in range(SLOT_COUNT)] + try: + with open(SLOT_CONFIG, encoding='utf-8') as f: + data = json.load(f) + except Exception: + return slots, DEFAULT_AREA + if isinstance(data, dict): + area = str(data.get('area') or DEFAULT_AREA) + items = data.get('slots') or [] + else: + area, items = DEFAULT_AREA, data + for i, item in enumerate(items[:SLOT_COUNT]): + if isinstance(item, dict): + slots[i]['name'] = str(item.get('name') or slots[i]['name']) + slots[i]['file'] = item.get('file') or None + return slots, area + + +def save_slots(slots, area): + with open(SLOT_CONFIG, 'w', encoding='utf-8') as f: + json.dump({'area': area, 'slots': slots}, f, ensure_ascii=False, indent=2) + + +def copy_image_into_script_dir(src_path): + """把任意路径的图片复制到脚本目录, 重名自动加序号, 返回文件名""" + dst_name = os.path.basename(src_path) + dst = os.path.join(BASE_DIR, dst_name) + if os.path.normcase(os.path.abspath(src_path)) == os.path.normcase(dst): + return dst_name + stem, ext = os.path.splitext(dst_name) + n = 1 + while os.path.exists(dst): + dst_name = f'{stem}_{n}{ext}' + dst = os.path.join(BASE_DIR, dst_name) + n += 1 + shutil.copy2(src_path, dst) + return dst_name + + +def load_image_raw(path): + """按原始像素加载图片(不缩放不裁剪; PIL 仅用于支持 JPG/BMP/WebP 等格式)""" + import tkinter as tk + if HAS_PIL: + return ImageTk.PhotoImage(Image.open(path).convert('RGB')) + return tk.PhotoImage(file=path) + + +def get_icon_path(): + """工具图标: 源码模式取 logos/ 下, 打包模式取 exe 内嵌资源(_MEIPASS)""" + if getattr(sys, 'frozen', False): + p = os.path.join(getattr(sys, '_MEIPASS', ''), 'logo_ph_orange.ico') + else: + p = os.path.join(BASE_DIR, 'logos', 'logo_ph_orange.ico') + return p if os.path.exists(p) else None + + +# ============================================================ +# Win32 显示 API +# ============================================================ +user32 = ctypes.windll.user32 + +ENUM_CURRENT_SETTINGS = 0xFFFFFFFF # (DWORD)-1 + + +class DISPLAY_DEVICEW(ctypes.Structure): + _fields_ = [ + ('cb', wt.DWORD), + ('DeviceName', ctypes.c_wchar * 32), + ('DeviceString', ctypes.c_wchar * 128), + ('StateFlags', wt.DWORD), + ('DeviceID', ctypes.c_wchar * 128), + ('DeviceKey', ctypes.c_wchar * 128), + ] + + +class DEVMODEW(ctypes.Structure): + _fields_ = [ + ('dmDeviceName', ctypes.c_wchar * 32), + ('dmSpecVersion', wt.WORD), + ('dmDriverVersion', wt.WORD), + ('dmSize', wt.WORD), + ('dmDriverExtra', wt.WORD), + ('dmFields', wt.DWORD), + ('dmPositionX', wt.LONG), + ('dmPositionY', wt.LONG), + ('dmDisplayOrientation', wt.DWORD), + ('dmDisplayFixedOutput', wt.DWORD), + ('dmColor', wt.WORD), + ('dmDuplex', wt.WORD), + ('dmYResolution', wt.WORD), + ('dmTTOption', wt.WORD), + ('dmCollate', wt.WORD), + ('dmFormName', ctypes.c_wchar * 32), + ('dmLogPixels', wt.WORD), + ('dmBitsPerPel', wt.DWORD), + ('dmPelsWidth', wt.DWORD), + ('dmPelsHeight', wt.DWORD), + ('dmDisplayFlags', wt.DWORD), + ('dmDisplayFrequency', wt.DWORD), + ('dmICMMethod', wt.DWORD), + ('dmICMIntent', wt.DWORD), + ('dmMediaType', wt.DWORD), + ('dmDitherType', wt.DWORD), + ('dmReserved1', wt.DWORD), + ('dmReserved2', wt.DWORD), + ('dmPanningWidth', wt.DWORD), + ('dmPanningHeight', wt.DWORD), + ] + + +user32.EnumDisplayDevicesW.argtypes = [ctypes.c_wchar_p, wt.DWORD, + ctypes.POINTER(DISPLAY_DEVICEW), wt.DWORD] +user32.EnumDisplayDevicesW.restype = wt.BOOL +user32.EnumDisplaySettingsW.argtypes = [ctypes.c_wchar_p, wt.DWORD, + ctypes.POINTER(DEVMODEW)] +user32.EnumDisplaySettingsW.restype = wt.BOOL +user32.ChangeDisplaySettingsExW.argtypes = [ctypes.c_wchar_p, ctypes.POINTER(DEVMODEW), + ctypes.c_void_p, wt.DWORD, ctypes.c_void_p] +user32.ChangeDisplaySettingsExW.restype = ctypes.c_long + +DM_BITSPERPEL = 0x00040000 +DM_PELSWIDTH = 0x00080000 +DM_PELSHEIGHT = 0x00100000 +DM_DISPLAYFREQUENCY = 0x00400000 + + +def read_edid_from_registry(device_id): + """monitor DeviceID 'MONITOR\\\\' → 注册表 EDID 原始字节 + + 注意: 间接显示器/虚拟显示驱动返回的 DeviceID 实例段是 '{类GUID}\\NNNN' + 格式, 注册表中并不存在该路径; 因此精确匹配失败后按 PNP ID 枚举其下 + 全部实例, 取第一份 EDID(同一物理屏的重复实例 EDID 相同)。 + """ + if not device_id or not device_id.startswith('MONITOR\\'): + return None + parts = device_id.split('\\') + if len(parts) < 3: + return None + pnp = parts[1] + inst = device_id.split('\\', 2)[2] + + def _read(path): + try: + key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path) + try: + val, _typ = winreg.QueryValueEx(key, 'EDID') + if isinstance(val, (bytes, bytearray)): + return bytes(val) + if isinstance(val, list): # 某些系统返回 int 列表 + return bytes(val) + finally: + winreg.CloseKey(key) + except OSError: + pass + return None + + # 1) 精确实例路径(常规真实显示器) + raw = _read(rf'SYSTEM\CurrentControlSet\Enum\DISPLAY\{pnp}\{inst}\Device Parameters') + if raw: + return raw + + # 2) 按 PNP ID 遍历全部实例(间接显示器/实例名不一致) + try: + pnp_root = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, + rf'SYSTEM\CurrentControlSet\Enum\DISPLAY\{pnp}') + except OSError: + return None + try: + for i in range(winreg.QueryInfoKey(pnp_root)[0]): + sub = winreg.EnumKey(pnp_root, i) + raw = _read(rf'SYSTEM\CurrentControlSet\Enum\DISPLAY\{pnp}\{sub}\Device Parameters') + if raw: + return raw + finally: + winreg.CloseKey(pnp_root) + return None + + +def get_displays(): + """枚举活动显示器: 设备名/显卡/监视器名/当前模式/屏幕矩形/EDID 字节""" + out = [] + i = 0 + while True: + dd = DISPLAY_DEVICEW() + dd.cb = ctypes.sizeof(dd) + if not user32.EnumDisplayDevicesW(None, i, ctypes.byref(dd), 0): + break + if dd.StateFlags & 0x1: # ATTACHED_TO_DESKTOP + # 找该输出口上活动的 monitor 条目 + mon = None + j = 0 + while True: + m = DISPLAY_DEVICEW() + m.cb = ctypes.sizeof(m) + if not user32.EnumDisplayDevicesW(dd.DeviceName, j, ctypes.byref(m), 0): + break + if m.StateFlags & 0x1: + mon = m + break + j += 1 + + cur = '' + rect = None + dm = DEVMODEW() + dm.dmSize = ctypes.sizeof(dm) + if user32.EnumDisplaySettingsW(dd.DeviceName, ENUM_CURRENT_SETTINGS, + ctypes.byref(dm)): + cur = f'{dm.dmPelsWidth}x{dm.dmPelsHeight}@{dm.dmDisplayFrequency}' + rect = (dm.dmPositionX, dm.dmPositionY, dm.dmPelsWidth, dm.dmPelsHeight) + + out.append({ + 'device': dd.DeviceName, # \\.\DISPLAY1 + 'adapter': dd.DeviceString, # 显卡名 + 'mon_name': mon.DeviceString if mon else '', + 'edid': read_edid_from_registry(mon.DeviceID) if mon else None, + 'cur_mode': cur, + 'rect': rect, # (x, y, w, h) 物理像素 + }) + i += 1 + return out + + +def enum_modes(device): + """枚举 device(\\\\.\\DISPLAYn) 可用模式 —— 显卡按该屏 EDID 提供, + 返回 (['WxH@Hz', ...] 按像素数降序, {'WxH@Hz': DEVMODEW})""" + seen = {} + i = 0 + while True: + dm = DEVMODEW() + dm.dmSize = ctypes.sizeof(dm) + if not user32.EnumDisplaySettingsW(device, i, ctypes.byref(dm)): + break + i += 1 + if dm.dmBitsPerPel != 32: + continue + key = f'{dm.dmPelsWidth}x{dm.dmPelsHeight}@{dm.dmDisplayFrequency}' + seen.setdefault(key, dm) + keys = sorted(seen, key=lambda k: tuple(int(v) for v in k.replace('@', 'x').split('x')), + reverse=True) + return keys, seen + + +def apply_mode(device, dm): + """把 device 切到 dm 模式(临时切换, 不写注册表, 重启还原), 返回 (成功?, 返回码)""" + dm.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY + r = user32.ChangeDisplaySettingsExW(device, ctypes.byref(dm), None, 0, None) + return r == 0, r # DISP_CHANGE_SUCCESSFUL == 0 + + +# ============================================================ +# 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('> 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('> 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] & 0x08: feats.append('sRGB 特性') + if raw[24] & 0x04: 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] + hx = ' '.join(f'{v:02X}' for v in chunk) + asc = ''.join(chr(v) if 32 <= v < 127 else '.' for v in chunk) + out.append(f'{off:04X}: {hx:<48} {asc}') + return '\n'.join(out) + + +# ============================================================ +# 测试图案窗口(tkinter) — 测试卡区域可选 640x360/800x600/全屏 +# ============================================================ +PAT_W, PAT_H = 640, 360 +AREA_OPTIONS = ('640x360', '800x600', 'full') +DEFAULT_AREA = AREA_OPTIONS[0] +PATTERN_NAMES = {'Red': '纯红', 'Green': '纯绿', 'Blue': '纯蓝', + 'Checker': '黑白棋盘格', 'Grid': '网格线'} +# 纯色测试值(位交换测试用, 与 tools/dist/solid_*.png 对应): +# 每通道 55/AA 互为镜像对(检测对), FF 全高基准 +SOLID_ROWS = ( + ('R', (('55', '#550000'), ('AA', '#AA0000'), ('FF', '#FF0000'))), + ('G', (('55', '#005500'), ('AA', '#00AA00'), ('FF', '#00FF00'))), + ('B', (('55', '#000055'), ('AA', '#0000AA'), ('FF', '#0000FF'))), +) + + +def _area_box(spec, w, h): + """区域选项 -> (x0, y0, aw, ah): 'full' 铺满整个屏幕, 'WxH' 黑底居中""" + if spec == 'full': + return 0, 0, w, h + try: + aw, ah = (int(v) for v in spec.lower().split('x', 1)) + except Exception: + aw, ah = PAT_W, PAT_H + return (w - aw) // 2, (h - ah) // 2, aw, ah + + +def _open_fullscreen(parent, rect, auto_close=0): + """创建覆盖 rect(x,y,w,h) 的无边框全屏黑底窗口, 返回 (top, cv) + + 只支持 ESC 退出: overrideredirect 窗口必须 focus_force 强取键盘焦点, + 否则收不到按键(Windows 上该组合有效)。 + """ + import tkinter as tk + rx, ry, rw, rh = rect + top = tk.Toplevel(parent) + top.overrideredirect(True) + top.geometry(f'{rw}x{rh}+{rx}+{ry}') + top.attributes('-topmost', True) + top.configure(bg='black') + + cv = tk.Canvas(top, bg='black', highlightthickness=0, bd=0) + cv.pack(fill='both', expand=True) + + def close(_evt=None): + top.destroy() + + top.bind('', close) + if auto_close > 0: + top.after(auto_close * 1000, close) + top.focus_force() + return top, cv + + +def show_pattern(parent, rect, kind, auto_close=0, area=DEFAULT_AREA): + """在 rect 指定的屏幕上全屏出居中测试卡(无任何 OSD 修饰), 区域由 area 指定""" + top, cv = _open_fullscreen(parent, rect, auto_close) + + def draw(_evt=None): + cv.delete('all') + w, h = max(cv.winfo_width(), rect[2]), max(cv.winfo_height(), rect[3]) + x0, y0, aw, ah = _area_box(area, w, h) + + if kind.startswith('#'): # 纯色测试值: 直接填充指定颜色 + cv.create_rectangle(x0, y0, x0 + aw, y0 + ah, fill=kind, outline='') + elif kind == 'Red': + cv.create_rectangle(x0, y0, x0 + aw, y0 + ah, fill='#FF0000', outline='') + elif kind == 'Green': + cv.create_rectangle(x0, y0, x0 + aw, y0 + ah, fill='#00FF00', outline='') + elif kind == 'Blue': + cv.create_rectangle(x0, y0, x0 + aw, y0 + ah, fill='#0000FF', outline='') + elif kind == 'Checker': + # 40px 黑白格, 格数随区域尺寸自适应(640x360 = 16x9 格) + for gy in range((ah + 39) // 40): + for gx in range((aw + 39) // 40): + if (gx + gy) % 2 == 0: + cv.create_rectangle(x0 + gx * 40, y0 + gy * 40, + x0 + (gx + 1) * 40, y0 + (gy + 1) * 40, + fill='#FFFFFF', outline='') + elif kind == 'Grid': + # 20px 白色网格线, 黑底, 无其它修饰 + for k in range(aw // 20 + 1): + cv.create_line(x0 + k * 20, y0, x0 + k * 20, y0 + ah, fill='#FFFFFF') + for k in range(ah // 20 + 1): + cv.create_line(x0, y0 + k * 20, x0 + aw, y0 + k * 20, fill='#FFFFFF') + + top.bind('', draw) + top.after(50, draw) + top.wait_window(top) + + +def show_pattern_image(parent, rect, path, auto_close=0, area=DEFAULT_AREA): + """全屏黑底, 图片 1:1 居中于 area 指定的测试卡区域: + 小于区域 -> 真实像素居中, 四周黑边; + 大于区域 -> 等效从中心截取(区域外用黑矩形遮挡)""" + top, cv = _open_fullscreen(parent, rect, auto_close) + + def draw(_evt=None): + cv.delete('all') + w, h = max(cv.winfo_width(), rect[2]), max(cv.winfo_height(), rect[3]) + x0, y0, aw, ah = _area_box(area, w, h) + photo = load_image_raw(path) + top._photo = photo # 挂在窗口上防止被垃圾回收 + iw, ih = photo.width(), photo.height() + cv.create_image(x0 + (aw - iw) // 2, y0 + (ah - ih) // 2, + image=photo, anchor='nw') + # 四块黑矩形盖住测试卡区域之外 -> 大图等效中心裁剪 + cv.create_rectangle(-2, -2, w + 2, y0, fill='black', outline='') + cv.create_rectangle(-2, y0 + ah, w + 2, h + 2, fill='black', outline='') + cv.create_rectangle(-2, y0, x0, y0 + ah, fill='black', outline='') + cv.create_rectangle(x0 + aw, y0, w + 2, y0 + ah, fill='black', outline='') + + top.bind('', draw) + top.after(50, draw) + top.wait_window(top) + + +# ============================================================ +# 主 GUI +# ============================================================ +def run_gui(auto_close=0): + import tkinter as tk + from tkinter import ttk + + # 让任务栏图标独立显示(不归并到 Python/通用组), 须在创建窗口前调用 + try: + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID('hulk.HDMI-Tool') + except Exception: + pass + + class App(tk.Tk): + def __init__(self): + super().__init__() + self.title('HDMI-Tool — EDID 查看器 + 测试图案 (LT8619C 转接板调试)') + self.geometry('960x880') + ic = get_icon_path() + if ic: + try: + self.iconbitmap(ic) # 窗口标题栏图标 + except Exception: + pass + self.displays = [] + self.slots, self.area = load_slots() + self._modes = {} # 'WxH@Hz' -> DEVMODEW(当前所选屏) + self._orig_modes = {} # device -> 切改前的原始 DEVMODEW + self._build() + + def _build(self): + # --- 顶部: 显示器选择 --- + top = ttk.Frame(self, padding=(8, 6)) + top.pack(fill='x') + ttk.Label(top, text='显示器:').pack(side='left') + self.cbo = ttk.Combobox(top, state='readonly', width=62) + self.cbo.pack(side='left', padx=6) + self.cbo.bind('<>', self._on_select) + ttk.Button(top, text='刷新', command=self.load_displays, width=8).pack(side='left') + self.lbl_cur = ttk.Label(top, text='') + self.lbl_cur.pack(side='left', padx=12) + + # --- 顶部第二行: 分辨率切换 --- + top2 = ttk.Frame(self, padding=(8, 2)) + top2.pack(fill='x') + ttk.Label(top2, text='分辨率:').pack(side='left') + self.cbo_mode = ttk.Combobox(top2, state='readonly', width=16) + self.cbo_mode.pack(side='left', padx=6) + ttk.Button(top2, text='应用', width=8, command=self._apply_mode).pack(side='left') + ttk.Button(top2, text='恢复', width=8, command=self._restore_mode).pack(side='left', padx=6) + ttk.Label(top2, text='列表为显卡按该屏 EDID 提供的模式; 切换临时生效, 重启还原', + foreground='#666666').pack(side='left', padx=10) + + # --- 中部: 模式列表 + EDID 信息 --- + mid = ttk.Frame(self, padding=(8, 0)) + mid.pack(fill='both', expand=True) + + left = ttk.LabelFrame(mid, text='支持的模式 (EDID 解算)', padding=4) + left.pack(side='left', fill='both', expand=True) + self.tv_modes = ttk.Treeview(left, columns=('mode', 'hz', 'clk', 'src'), + show='headings', height=12) + for col, txt, w, anchor in (('mode', '模式', 110, 'w'), ('hz', '刷新率', 65, 'e'), + ('clk', '像素时钟', 80, 'e'), ('src', '来源', 90, 'w')): + self.tv_modes.heading(col, text=txt) + self.tv_modes.column(col, width=w, anchor=anchor) + sb1 = ttk.Scrollbar(left, orient='vertical', command=self.tv_modes.yview) + self.tv_modes.configure(yscrollcommand=sb1.set) + self.tv_modes.pack(side='left', fill='both', expand=True) + sb1.pack(side='right', fill='y') + + right = ttk.LabelFrame(mid, text='EDID 信息', padding=4) + right.pack(side='left', fill='both', expand=True, padx=(6, 0)) + self.tv_info = ttk.Treeview(right, columns=('k', 'v'), show='headings', height=12) + self.tv_info.heading('k', text='项目') + self.tv_info.heading('v', text='值') + self.tv_info.column('k', width=150, anchor='w') + self.tv_info.column('v', width=420, anchor='w') + sb2 = ttk.Scrollbar(right, orient='vertical', command=self.tv_info.yview) + self.tv_info.configure(yscrollcommand=sb2.set) + self.tv_info.pack(side='left', fill='both', expand=True) + sb2.pack(side='right', fill='y') + + # --- 测试图案按钮组 --- + grp = ttk.LabelFrame(self, text='测试图案 — 全屏投至所选显示器, 黑底 1:1 居中, 按 ESC 退出', + padding=(10, 6)) + grp.pack(fill='x', padx=8, pady=(6, 0)) + row1 = ttk.Frame(grp) + row1.pack(fill='x') + 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 按通道分组, 位交换测试用) + 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, + command=lambda c=color: self._show_solid(c) + ).pack(side='left', padx=3) + ttk.Label(row1, text='测试卡区域:').pack(side='left', padx=(16, 2)) + self.area_var = tk.StringVar(value=self.area) + cbo_area = ttk.Combobox(row1, textvariable=self.area_var, width=8, + state='readonly', values=list(AREA_OPTIONS)) + cbo_area.pack(side='left') + cbo_area.bind('<>', lambda _e: self._save_area()) + row2 = ttk.Frame(grp) + row2.pack(fill='x', pady=(4, 0)) + self.slot_buttons = [] + for i in range(SLOT_COUNT): + cell = ttk.Frame(row2) + cell.pack(side='left', padx=(5, 0)) + btn = ttk.Button(cell, text=self.slots[i]['name'], width=12, + command=lambda i=i: self._show_custom(i)) + btn.pack(side='left') + ttk.Button(cell, text='⚙', width=3, + command=lambda i=i: self._configure_slot(i)).pack(side='left', padx=(4, 0)) + self.slot_buttons.append(btn) + ttk.Label(row2, text='自定义: 点击投图, ⚙ 配置名称与图片(图片保存在脚本目录)', + foreground='#666666').pack(side='left', padx=10) + + # --- 底部: EDID hex dump --- + bot = ttk.LabelFrame(self, text='EDID 原始数据', padding=4) + bot.pack(fill='both', expand=False, padx=8, pady=(6, 8)) + self.txt_hex = tk.Text(bot, height=8, font=('Consolas', 9), + state='disabled', wrap='none', background='#101010', + foreground='#C8C8C8') + sb3 = ttk.Scrollbar(bot, orient='vertical', command=self.txt_hex.yview) + self.txt_hex.configure(yscrollcommand=sb3.set) + self.txt_hex.pack(side='left', fill='both', expand=True) + sb3.pack(side='right', fill='y') + + def load_displays(self): + keep = self.cbo.current() + self.displays = get_displays() + self.cbo['values'] = [f"{d['device']} {d['adapter']} [{d['mon_name']}]" for d in self.displays] + if self.displays: + self.cbo.current(max(0, min(keep, len(self.displays) - 1))) + self._on_select() + else: + self.lbl_cur.config(text='未找到活动显示器') + + def _on_select(self, _evt=None): + idx = self.cbo.current() + if idx < 0 or idx >= len(self.displays): + return + d = self.displays[idx] + self.lbl_cur.config(text=f"当前输出: {d['cur_mode']}") + # 分辨率下拉: 该屏可用模式, 默认选中当前模式 + keys, modes = enum_modes(d['device']) + self._modes = modes + self.cbo_mode['values'] = keys + if d['cur_mode'] in modes: + self.cbo_mode.set(d['cur_mode']) + elif keys: + self.cbo_mode.set(keys[0]) + for tv in (self.tv_modes, self.tv_info): + tv.delete(*tv.get_children()) + if not d['edid']: + self.tv_info.insert('', 'end', values=('EDID', '读取失败(注册表无 EDID 数据)')) + return + e = parse_edid(d['edid']) + for k, v in edid_info_lines(e): + self.tv_info.insert('', 'end', values=(k, v)) + for k, t in enumerate(e['dtds']): + src = 'DTD 首选' if k == 0 else 'DTD' + self.tv_modes.insert('', 'end', values=( + f"{t['hact']}x{t['vact']}", f"{t['refresh']}Hz", + f"{t['clk_khz'] / 1000:.2f}MHz", src)) + for m in e['cea_video']: + wh, _, hz = m.partition('@') + self.tv_modes.insert('', 'end', values=(wh, hz.split(' ')[0] + 'Hz', '', 'CEA-VIC')) + for m in e['est'] + e['std']: + wh, _, hz = m.partition('@') + src = '已建立时序' if m in e['est'] else '标准时序' + self.tv_modes.insert('', 'end', values=(wh, hz + 'Hz', '', src)) + self.txt_hex.configure(state='normal') + self.txt_hex.delete('1.0', 'end') + self.txt_hex.insert('1.0', hex_dump(d['edid'])) + self.txt_hex.configure(state='disabled') + + def _save_area(self): + self.area = self.area_var.get() + save_slots(self.slots, self.area) + + def _show_solid(self, color): + idx = self.cbo.current() + if idx < 0 or idx >= len(self.displays): + return + d = self.displays[idx] + if not d['rect']: + return + show_pattern(self, d['rect'], color, area=self.area_var.get()) + + def _apply_mode(self): + from tkinter import messagebox + idx, mi = self.cbo.current(), self.cbo_mode.current() + if idx < 0 or idx >= len(self.displays) or mi < 0: + return + dm = self._modes.get(self.cbo_mode.get()) + if dm is None: + return + dev = self.displays[idx]['device'] + if dev not in self._orig_modes: # 记住首次切改前的原始模式, 供"恢复"用 + cur = DEVMODEW() + cur.dmSize = ctypes.sizeof(cur) + if user32.EnumDisplaySettingsW(dev, ENUM_CURRENT_SETTINGS, ctypes.byref(cur)): + self._orig_modes[dev] = cur + ok, r = apply_mode(dev, dm) + if not ok: + messagebox.showerror('切换失败', f'ChangeDisplaySettingsEx 返回码 {r}') + return + self.after(500, self.load_displays) # 等模式生效后刷新矩形/EDID 面板 + + def _restore_mode(self): + from tkinter import messagebox + idx = self.cbo.current() + if idx < 0 or idx >= len(self.displays): + return + dev = self.displays[idx]['device'] + dm = self._orig_modes.pop(dev, None) + if dm is None: + messagebox.showinfo('无可恢复', '该显示器没有记录切改前的模式') + return + apply_mode(dev, dm) + self.after(500, self.load_displays) + + def _show_pattern(self, kind): + idx = self.cbo.current() + if idx < 0 or idx >= len(self.displays): + return + d = self.displays[idx] + if not d['rect']: + return + show_pattern(self, d['rect'], kind, area=self.area_var.get()) + + def _show_custom(self, idx): + from tkinter import messagebox + slot = self.slots[idx] + if not slot['file']: + messagebox.showinfo('未配置图片', + f'请先点击"⚙"为"{slot["name"]}"配置图片。') + return + path = os.path.join(BASE_DIR, slot['file']) + if not os.path.exists(path): + messagebox.showerror('图片不存在', path) + return + di = self.cbo.current() + if di < 0 or di >= len(self.displays) or not self.displays[di]['rect']: + return + show_pattern_image(self, self.displays[di]['rect'], path, + area=self.area_var.get()) + + def _configure_slot(self, idx): + from tkinter import filedialog, messagebox + dlg = tk.Toplevel(self) + dlg.title(f'配置自定义栏位 {idx + 1}') + dlg.resizable(False, False) + dlg.grab_set() + + name_var = tk.StringVar(value=self.slots[idx]['name']) + file_var = tk.StringVar(value=self.slots[idx]['file'] or '(未设置)') + + body = ttk.Frame(dlg, padding=10) + body.pack() + ttk.Label(body, text='按钮名称:').grid(row=0, column=0, sticky='w', pady=4) + ttk.Entry(body, textvariable=name_var, width=30).grid(row=0, column=1, columnspan=2, + sticky='w', padx=6) + ttk.Label(body, text='图片文件:').grid(row=1, column=0, sticky='w', pady=4) + ttk.Label(body, textvariable=file_var, foreground='#0066CC').grid( + row=1, column=1, columnspan=2, sticky='w', padx=6) + if not HAS_PIL: + ttk.Label(body, text='(未安装 Pillow, 仅支持 PNG/GIF; 安装 pillow 后可用 JPG/BMP/WebP)', + foreground='#B05000').grid(row=2, column=0, columnspan=3, sticky='w') + + def choose(): + exts = ' '.join(f'*{e}' for e in IMG_EXTS) + path = filedialog.askopenfilename(parent=dlg, title='选择图片', + filetypes=[('图片文件', exts), ('所有文件', '*.*')]) + if not path: + return + try: + file_var.set(copy_image_into_script_dir(path)) + except OSError as ex: + messagebox.showerror('复制图片失败', str(ex), parent=dlg) + + def clear(): + file_var.set('(未设置)') + + def save(): + name = name_var.get().strip() or f'自定义{idx + 1}' + fname = file_var.get() + self.slots[idx] = {'name': name, + 'file': None if fname.startswith('(') else fname} + save_slots(self.slots, self.area_var.get()) + self.slot_buttons[idx].configure(text=name) + dlg.destroy() + + row = 3 if not HAS_PIL else 2 + ttk.Button(body, text='选择图片...', command=choose).grid( + row=row, column=0, sticky='w', pady=(8, 0)) + ttk.Button(body, text='清除图片', command=clear).grid( + row=row, column=1, sticky='w', padx=6, pady=(8, 0)) + ttk.Button(body, text='保存', command=save).grid( + row=row + 1, column=0, sticky='w', pady=(8, 0)) + ttk.Button(body, text='取消', command=dlg.destroy).grid( + row=row + 1, column=1, sticky='w', padx=6, pady=(8, 0)) + + app = App() + if auto_close > 0: + app.after(auto_close * 1000, app.destroy) + app.mainloop() + + +# ============================================================ +# 入口 +# ============================================================ +def run_dump(): + displays = get_displays() + if not displays: + print('未找到活动显示器') + return 1 + for i, d in enumerate(displays, 1): + print() + print(f'== 显示器 {i} {d["device"]} 显卡[{d["adapter"]}] ' + f'监视器[{d["mon_name"]}] 当前输出 {d["cur_mode"]}') + if not d['edid']: + print(' EDID 读取失败(注册表无 EDID 数据)') + continue + e = parse_edid(d['edid']) + for k, v in edid_info_lines(e): + print(f' {k:<14}: {v}') + print(' ---- 支持的模式 ----') + for k, t in enumerate(e['dtds']): + src = 'DTD1(首选)' if k == 0 else f'DTD{k + 1}' + print(f" [{src}] {t['hact']}x{t['vact']}@{t['refresh']} " + f"{t['clk_khz'] / 1000:.3f}MHz 总{t['htotal']}x{t['vtotal']}") + for m in e['cea_video']: + print(f' [CEA-VIC] {m}') + for m in e['cea_blocks']: + print(f' [CEA-块] {m}') + for m in e['est']: + print(f' [已建立] {m}') + for m in e['std']: + print(f' [标准] {m}') + print(' ---- EDID 原始数据 ----') + print(hex_dump(d['edid'])) + return 0 + + +def main(): + ap = argparse.ArgumentParser(description='HDMI EDID 查看器 + 测试图案') + ap.add_argument('--dump', action='store_true', help='控制台输出 EDID 解算结果, 不开 GUI') + ap.add_argument('--test-pattern', + choices=list(PATTERN_NAMES) + [f'Custom{i + 1}' for i in range(SLOT_COUNT)], + default=None, help='直接全屏出测试图案(不开主窗口; CustomN 为自定义栏位)') + ap.add_argument('--area', choices=list(AREA_OPTIONS), default=DEFAULT_AREA, + help='测试卡区域: WxH 黑底居中 或 full 铺满所选显示器(默认 640x360)') + ap.add_argument('--monitor', type=int, default=1, help='--test-pattern 用的显示器序号(默认 1)') + ap.add_argument('--seconds', type=int, default=0, help='自动退出秒数(GUI/图案冒烟测试)') + args = ap.parse_args() + + if args.dump: + 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') + sys.exit(1) + sys.exit(run_dump()) + if args.test_pattern: + displays = get_displays() + if not displays: + print('未找到活动显示器') + sys.exit(1) + d = displays[min(args.monitor, len(displays)) - 1] + if not d['rect']: + print('无法获取显示器矩形') + sys.exit(1) + import tkinter as tk + root = tk.Tk() + root.withdraw() + if args.test_pattern.startswith('Custom'): + slots, _ = load_slots() + slot = slots[int(args.test_pattern[6:]) - 1] + if not slot['file']: + print(f"错误: {args.test_pattern}({slot['name']})未配置图片") + sys.exit(1) + show_pattern_image(root, d['rect'], os.path.join(BASE_DIR, slot['file']), + args.seconds, area=args.area) + else: + show_pattern(root, d['rect'], args.test_pattern, args.seconds, area=args.area) + sys.exit(0) + run_gui(args.seconds) + + +if __name__ == '__main__': + main() diff --git a/HDMI-Tool/hdmi_source.bat b/HDMI-Tool/hdmi_source.bat new file mode 100644 index 0000000..0cd1787 --- /dev/null +++ b/HDMI-Tool/hdmi_source.bat @@ -0,0 +1,7 @@ +@echo off +rem HDMI source test tool launcher (bypasses PowerShell execution policy) +rem Examples: +rem hdmi_source.bat -List +rem hdmi_source.bat -Mode 640x360 +rem hdmi_source.bat -Mode 640x360 -Pattern -Restore +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0hdmi_source.ps1" %* diff --git a/HDMI-Tool/hdmi_source.ps1 b/HDMI-Tool/hdmi_source.ps1 new file mode 100644 index 0000000..9804adb --- /dev/null +++ b/HDMI-Tool/hdmi_source.ps1 @@ -0,0 +1,487 @@ +<# +.SYNOPSIS + HDMI 源端测试工具 —— 枚举/切换输出分辨率 + 全屏测试图案 + +.DESCRIPTION + 配合 LT8619C(HDMI 转 RGB888)→ DLPC3421 转接板调试使用。 + + 原理:电脑显卡通过 HDMI DDC 读取转接板 EDID 来枚举可用分辨率, + 本脚本调用 Win32 API 完成模式枚举与切换,再用 WinForms 全屏窗口 + 输出测试图案。板载 EDID 只声明了 640x360@60(25.2MHz 像素时钟), + 插上 HDMI 后显卡应只能发这一个模式;-List 可直接验证 EDID 是否生效。 + + 测试图案用途: + Bars 100% 彩条 —— 肉眼/示波器验证 RGB 通道顺序与极性 + GraySteps 16 级灰阶 —— 验证 RGB888 各 bit 连通性 + Gradient R/G/B 三段渐变 —— 验证每通道 8bit 线性度 + Checker 黑白棋盘格 —— 验证像素时钟 PCLK 与 DE 对位 + Grid 网格+中心十字 —— 验证像素映射/偏移/缩放 + Red/Green/Blue/White/Black/Gray50 纯色 —— 万用表/示波器测单通道电平 + + 全屏窗口操作:空格或→下一图案,←上一图案,ESC 退出。 + +.EXAMPLE + tools\hdmi_source.bat -List + 枚举所有活动显示器与显卡支持的分辨率(验证 EDID) + +.EXAMPLE + tools\hdmi_source.bat -Monitor 1 -Mode 640x360 + 把显示器 1 切到 640x360(缺省 60Hz,可写 640x360@60) + +.EXAMPLE + tools\hdmi_source.bat -Mode 640x360 -Pattern -Restore + 切到 640x360 并全屏出图案,退出图案后恢复原分辨率 + +.EXAMPLE + tools\hdmi_source.bat -Pattern -PatternName Checker -Seconds 5 + 当前分辨率下全屏棋盘格,5 秒后自动退出(自动化测试用) +#> + +param( + [switch]$List, + [int]$Monitor = 1, + [string]$Mode = '', + [switch]$Pattern, + [ValidateSet('', 'Bars', 'GraySteps', 'Gradient', 'Checker', 'Grid', + 'Red', 'Green', 'Blue', 'White', 'Black', 'Gray50')] + [string]$PatternName = '', + [int]$Seconds = 0, + [switch]$Restore +) + +$ErrorActionPreference = 'Stop' + +# ---------------- Win32 显示 API ---------------- +Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; + +namespace HdmiSrc +{ + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + public struct DISPLAY_DEVICE + { + public int cb; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string DeviceName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceString; + public int StateFlags; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceID; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceKey; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + public struct DEVMODE + { + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string dmDeviceName; + public short dmSpecVersion; + public short dmDriverVersion; + public short dmSize; + public short dmDriverExtra; + public int dmFields; + public int dmPositionX; + public int dmPositionY; + public int dmDisplayOrientation; + public int dmDisplayFixedOutput; + public short dmColor; + public short dmDuplex; + public short dmYResolution; + public short dmTTOption; + public short dmCollate; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string dmFormName; + public short dmLogPixels; + public int dmBitsPerPel; + public int dmPelsWidth; + public int dmPelsHeight; + public int dmDisplayFlags; + public int dmDisplayFrequency; + public int dmICMMethod; + public int dmICMIntent; + public int dmMediaType; + public int dmDitherType; + public int dmReserved1; + public int dmReserved2; + public int dmPanningWidth; + public int dmPanningHeight; + } + + public static class Native + { + [DllImport("user32.dll", CharSet = CharSet.Ansi)] + public static extern bool EnumDisplayDevices(string lpDevice, uint iDevNum, + ref DISPLAY_DEVICE lpDisplayDevice, uint dwFlags); + + // PowerShell 会把 $null marshal 成空串而非 NULL,由 C# 侧保证传 NULL + public static bool EnumDisplayAdapters(uint iDevNum, ref DISPLAY_DEVICE lpDisplayDevice) + { + return EnumDisplayDevices(null, iDevNum, ref lpDisplayDevice, 0); + } + + [DllImport("user32.dll", CharSet = CharSet.Ansi)] + public static extern bool EnumDisplaySettings(string lpszDeviceName, int iModeNum, + ref DEVMODE lpDevMode); + + [DllImport("user32.dll", CharSet = CharSet.Ansi)] + public static extern int ChangeDisplaySettingsEx(string lpszDeviceName, + ref DEVMODE lpDevMode, IntPtr hwnd, int dwflags, IntPtr lParam); + } +} +"@ + +# 板载 EDID 声明的目标模式,-List 时用于自检 +$TargetW = 640 +$TargetH = 360 + +function New-DevMode { + $dm = New-Object HdmiSrc.DEVMODE + $dm.dmDeviceName = '' + $dm.dmFormName = '' + $dm.dmSize = [System.Runtime.InteropServices.Marshal]::SizeOf($dm) + return $dm +} + +function New-DisplayDevice { + $dd = New-Object HdmiSrc.DISPLAY_DEVICE + $dd.DeviceName = '' + $dd.DeviceString = '' + $dd.DeviceID = '' + $dd.DeviceKey = '' + $dd.cb = [System.Runtime.InteropServices.Marshal]::SizeOf($dd) + return $dd +} + +# 枚举所有"已连接到桌面"的显示器(即插着 HDMI 的输出口) +function Get-Displays { + $list = @() + $i = 0 + while ($true) { + $dd = New-DisplayDevice + if (-not [HdmiSrc.Native]::EnumDisplayAdapters($i, [ref]$dd)) { break } + # StateFlags bit0 = ATTACHED_TO_DESKTOP + if ($dd.StateFlags -band 0x1) { + $mon = New-DisplayDevice + [void][HdmiSrc.Native]::EnumDisplayDevices($dd.DeviceName, 0, [ref]$mon, 0) + $list += [pscustomobject]@{ + Index = $list.Count + 1 + Device = $dd.DeviceName + Adapter = $dd.DeviceString + MonName = $mon.DeviceString + } + } + $i++ + } + return $list +} + +# 显卡枚举到的全部显示模式(去重) +function Get-Modes { + param([string]$Device) + $modes = @() + $i = 0 + while ($true) { + $dm = New-DevMode + if (-not [HdmiSrc.Native]::EnumDisplaySettings($Device, $i, [ref]$dm)) { break } + $modes += [pscustomobject]@{ + W = $dm.dmPelsWidth + H = $dm.dmPelsHeight + Hz = $dm.dmDisplayFrequency + Bpp = $dm.dmBitsPerPel + } + $i++ + } + return ($modes | Sort-Object W, H, Hz, Bpp -Unique) +} + +function Get-CurrentDevMode { + param([string]$Device) + $dm = New-DevMode + [void][HdmiSrc.Native]::EnumDisplaySettings($Device, -1, [ref]$dm) # ENUM_CURRENT_SETTINGS + return $dm +} + +# 应用一个 DEVMODE(先 CDS_TEST 再真正生效),返回 $true/$false +function Set-DevMode { + param([string]$Device, $dm) + $dm.dmFields = 0x00040000 -bor 0x00080000 -bor 0x00100000 -bor 0x00400000 + # DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY + $rc = [HdmiSrc.Native]::ChangeDisplaySettingsEx($Device, [ref]$dm, [IntPtr]::Zero, 2, [IntPtr]::Zero) + if ($rc -ne 0) { + Write-Host (" 错误: 显卡拒绝该模式 (CDS_TEST 返回 {0})" -f $rc) -ForegroundColor Red + return $false + } + $rc = [HdmiSrc.Native]::ChangeDisplaySettingsEx($Device, [ref]$dm, [IntPtr]::Zero, 0, [IntPtr]::Zero) + if ($rc -ne 0) { + Write-Host (" 错误: 模式应用失败 (返回 {0})" -f $rc) -ForegroundColor Red + return $false + } + return $true +} + +# 在显卡枚举的模式里找 W x H [@Hz] 并切换 +function Switch-Mode { + param([string]$Device, [int]$W, [int]$H, [int]$Hz) + $all = @(Get-Modes $Device) + $hit = @($all | Where-Object { $_.W -eq $W -and $_.H -eq $H -and ($Hz -le 0 -or $_.Hz -eq $Hz) }) + if (-not $hit) { + Write-Host (" 错误: 显卡未枚举到 {0}x{1} 模式" -f $W, $H) -ForegroundColor Red + Write-Host " 可能原因:" -ForegroundColor Yellow + Write-Host " 1. 转接板 EDID 未被读取(检查 HPD / DDC / LT8619C 供电)" + Write-Host " 2. EDID 中未包含该分辨率(当前板载 EDID 只有 640x360@60)" + Write-Host " 3. 显卡驱动拒收该 DTD(可用 CRU 手动添加自定义分辨率)" + Write-Host " 先运行 -List 查看显卡实际枚举到了哪些模式。" + return $false + } + $dm = New-DevMode + $dm.dmPelsWidth = $W + $dm.dmPelsHeight = $H + $dm.dmDisplayFrequency = $hit[0].Hz + $dm.dmBitsPerPel = 32 + $ok = Set-DevMode $Device $dm + if ($ok) { + Write-Host (" 已切换 {0} -> {1}x{2}@{3}" -f $Device, $W, $H, $hit[0].Hz) -ForegroundColor Green + } + return $ok +} + +# ---------------- 全屏测试图案 ---------------- +function Show-PatternWindow { + param( + [string]$Device = '', + [string]$StartPattern = '', + [int]$AutoSeconds = 0 + ) + + Add-Type -AssemblyName System.Windows.Forms + Add-Type -AssemblyName System.Drawing + + # 窗口放到目标设备名对应的屏幕上(如 \\.\DISPLAY2),找不到则回退主屏 + $screen = [System.Windows.Forms.Screen]::AllScreens | + Where-Object { $_.DeviceName -eq $Device } | + Select-Object -First 1 + if (-not $screen) { $screen = [System.Windows.Forms.Screen]::PrimaryScreen } + + $script:patterns = @('Bars', 'GraySteps', 'Gradient', 'Checker', 'Grid', + 'Red', 'Green', 'Blue', 'White', 'Black', 'Gray50') + $script:pi = 0 + if ($StartPattern) { + $idx = [array]::IndexOf($script:patterns, $StartPattern) + if ($idx -ge 0) { $script:pi = $idx } + } + + $script:form = New-Object System.Windows.Forms.Form + $script:form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::None + $script:form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual + $script:form.Bounds = $screen.Bounds + $script:form.TopMost = $true + $script:form.KeyPreview = $true + # 双缓冲避免棋盘格/网格闪烁 + $script:form.GetType().GetProperty('DoubleBuffered', + [System.Reflection.BindingFlags]'Instance,NonPublic').SetValue($script:form, $true, $null) + + $osdFont = New-Object System.Drawing.Font('Consolas', 11) + + $paintHandler = { + param($s, $e) + $g = $e.Graphics + $w = $e.ClipRectangle.Width + $h = $e.ClipRectangle.Height + $name = $script:patterns[$script:pi] + + switch ($name) { + 'Bars' { + # 100% 饱和度竖彩条:白黄青绿品红红蓝黑 + $c = @(@(255,255,255), @(255,255,0), @(0,255,255), @(0,255,0), + @(255,0,255), @(255,0,0), @(0,0,255), @(0,0,0)) + $n = $c.Count + for ($k = 0; $k -lt $n; $k++) { + $bw = [Math]::Ceiling($w / $n) + $brush = New-Object System.Drawing.SolidBrush ( + [System.Drawing.Color]::FromArgb($c[$k][0], $c[$k][1], $c[$k][2])) + $g.FillRectangle($brush, [int]($k * $bw), 0, [int]$bw + 1, $h) + $brush.Dispose() + } + } + 'GraySteps' { + # 16 级灰阶,验证 RGB888 各 bit + $n = 16 + for ($k = 0; $k -lt $n; $k++) { + $v = [int]($k * 255 / ($n - 1)) + $bw = [Math]::Ceiling($w / $n) + $brush = New-Object System.Drawing.SolidBrush ( + [System.Drawing.Color]::FromArgb($v, $v, $v)) + $g.FillRectangle($brush, [int]($k * $bw), 0, [int]$bw + 1, $h) + $brush.Dispose() + } + } + 'Gradient' { + # R/G/B 三段水平渐变,验证每通道 8bit 线性度 + $seg = [Math]::Ceiling($w / 3.0) + $rgbIdx = @(0, 1, 2) + foreach ($ch in $rgbIdx) { + for ($x = 0; $x -lt $seg; $x++) { + $v = [int](255 * $x / [Math]::Max(1.0, ($seg - 1))) + $r = 0; $gg = 0; $b = 0 + if ($ch -eq 0) { $r = $v } elseif ($ch -eq 1) { $gg = $v } else { $b = $v } + $brush = New-Object System.Drawing.SolidBrush ( + [System.Drawing.Color]::FromArgb($r, $gg, $b)) + $g.FillRectangle($brush, [int]($ch * $seg + $x), 0, 1, $h) + $brush.Dispose() + } + } + } + 'Checker' { + # 黑白棋盘格,验证 PCLK/DE 对位 + $g.FillRectangle([System.Drawing.Brushes]::Black, 0, 0, $w, $h) + $cell = 16 + for ($y = 0; $y -lt $h; $y += $cell) { + for ($x = 0; $x -lt $w; $x += $cell) { + if ((([int]($x / $cell) + [int]($y / $cell)) % 2) -eq 0) { + $g.FillRectangle([System.Drawing.Brushes]::White, $x, $y, $cell, $cell) + } + } + } + } + 'Grid' { + # 黑底 + 40px 网格 + 中心十字 + 白色边框,验证像素映射 + $g.FillRectangle([System.Drawing.Brushes]::Black, 0, 0, $w, $h) + $pen = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(0, 128, 0), 1) + for ($x = 40; $x -lt $w; $x += 40) { $g.DrawLine($pen, $x, 0, $x, $h) } + for ($y = 40; $y -lt $h; $y += 40) { $g.DrawLine($pen, 0, $y, $w, $y) } + $pen.Dispose() + $wp = New-Object System.Drawing.Pen ([System.Drawing.Color]::White, 2) + $cx = [int]($w / 2); $cy = [int]($h / 2) + $g.DrawLine($wp, $cx, 0, $cx, $h) + $g.DrawLine($wp, 0, $cy, $w, $cy) + $g.DrawRectangle($wp, 1, 1, $w - 3, $h - 3) + $wp.Dispose() + } + 'Red' { $g.FillRectangle([System.Drawing.Brushes]::Red, 0, 0, $w, $h) } + 'Green' { $g.FillRectangle([System.Drawing.Brushes]::Green, 0, 0, $w, $h) } + 'Blue' { $g.FillRectangle([System.Drawing.Brushes]::Blue, 0, 0, $w, $h) } + 'White' { $g.FillRectangle([System.Drawing.Brushes]::White, 0, 0, $w, $h) } + 'Black' { $g.FillRectangle([System.Drawing.Brushes]::Black, 0, 0, $w, $h) } + 'Gray50' { + $brush = New-Object System.Drawing.SolidBrush ( + [System.Drawing.Color]::FromArgb(128, 128, 128)) + $g.FillRectangle($brush, 0, 0, $w, $h) + $brush.Dispose() + } + } + + # 左上角 OSD:图案名 + 分辨率 + 按键提示 + $text = "[{0}] {1}x{2} 空格/→:下一图案 ←:上一图案 ESC:退出" -f $name, $w, $h + $sz = $g.MeasureString($text, $osdFont) + $bg = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(150, 0, 0, 0)) + $g.FillRectangle($bg, 6, 6, $sz.Width + 12, $sz.Height + 8) + $bg.Dispose() + $g.DrawString($text, $osdFont, [System.Drawing.Brushes]::White, 12, 10) + } + $script:form.Add_Paint($paintHandler) + + $script:form.Add_KeyDown({ + param($s, $e) + switch ($e.KeyCode) { + 'Escape' { $s.Close() } + 'Space' { $script:pi = ($script:pi + 1) % $script:patterns.Count; $s.Invalidate() } + 'Right' { $script:pi = ($script:pi + 1) % $script:patterns.Count; $s.Invalidate() } + 'Left' { $script:pi = ($script:pi - 1 + $script:patterns.Count) % $script:patterns.Count; $s.Invalidate() } + } + }) + + $script:form.Add_Activated({ [System.Windows.Forms.Cursor]::Hide() }) + $script:form.Add_FormClosed({ [System.Windows.Forms.Cursor]::Show() }) + + if ($AutoSeconds -gt 0) { + $timer = New-Object System.Windows.Forms.Timer + $timer.Interval = $AutoSeconds * 1000 + $timer.Add_Tick({ $script:form.Close() }) + $timer.Start() + } + + Write-Host ("全屏图案已启动于 {0} ({1}x{2})" -f $screen.DeviceName, $screen.Bounds.Width, $screen.Bounds.Height) + Write-Host "空格/→:下一图案 ←:上一图案 ESC:退出" + [void]$script:form.ShowDialog() +} + +function Show-Usage { + Write-Host @" +用法: + hdmi_source.bat -List 枚举显示器与可用分辨率(验证 EDID) + hdmi_source.bat -Monitor -Mode 切换分辨率, 如 -Mode 640x360 或 640x360@60 + hdmi_source.bat [-Monitor ] -Pattern [-PatternName <名>] [-Seconds ] + 全屏测试图案(Bar/GraySteps/Gradient/Checker/Grid/纯色) + 可选: -Restore 退出图案窗口后恢复切换前的分辨率 + -Seconds 图案 n 秒后自动退出(自动化测试) + +典型调试流程: + 1. HDMI 插上转接板 -> -List 确认出现 640x360(EDID 生效) + 2. -Mode 640x360 -Pattern -> 全屏图案, 逐项验证 RGB 通道/位序/像素映射 +"@ +} + +# ---------------- 主流程 ---------------- +$displays = @(Get-Displays) # @() 保证单显示器时不退化为标量 +if ($displays.Count -eq 0) { + Write-Host "错误: 未找到活动显示器" -ForegroundColor Red + exit 1 +} + +if ($List) { + foreach ($d in $displays) { + Write-Host "" + Write-Host ("== 显示器 {0} {1} 显卡[{2}] 监视器[{3}]" -f $d.Index, $d.Device, $d.Adapter, $d.MonName) -ForegroundColor Cyan + $cur = Get-CurrentDevMode $d.Device + Write-Host (" 当前模式: {0}x{1}@{2} {3}bpp" -f $cur.dmPelsWidth, $cur.dmPelsHeight, $cur.dmDisplayFrequency, $cur.dmBitsPerPel) + $modes = Get-Modes $d.Device + $modes | Format-Table W, H, Hz, Bpp -AutoSize + $hasTarget = $modes | Where-Object { $_.W -eq $TargetW -and $_.H -eq $TargetH } + if ($hasTarget) { + Write-Host (" [OK] 找到板载 EDID 目标模式 {0}x{1} -> EDID 已被显卡读取" -f $TargetW, $TargetH) -ForegroundColor Green + } else { + Write-Host (" [--] 未找到 {0}x{1} (若此口接的是转接板, 说明 EDID/HPD 未生效)" -f $TargetW, $TargetH) -ForegroundColor Yellow + } + } + exit 0 +} + +if (-not $Mode -and -not $Pattern) { + Show-Usage + exit 0 +} + +if ($Monitor -gt $displays.Count) { + Write-Host ("错误: -Monitor 超出范围(共 {0} 个活动显示器, 运行 -List 查看)" -f $displays.Count) -ForegroundColor Red + exit 1 +} +$disp = $displays[$Monitor - 1] +Write-Host ("目标: 显示器 {0} = {1}" -f $disp.Index, $disp.Device) + +$prevMode = $null +if ($Mode) { + if ($Mode -notmatch '^\s*(\d+)\s*[xX]\s*(\d+)\s*(?:@\s*(\d+)\s*)?$') { + Write-Host "错误: -Mode 格式应为 WxH 或 WxH@Hz, 例如 640x360 或 640x360@60" -ForegroundColor Red + exit 1 + } + $w = [int]$Matches[1]; $h = [int]$Matches[2] + $hz = 0 + if ($Matches[3]) { $hz = [int]$Matches[3] } + + $prevMode = Get-CurrentDevMode $disp.Device + $ok = Switch-Mode $disp.Device $w $h $hz + if (-not $ok) { exit 1 } + Start-Sleep -Milliseconds 300 # 等显卡/显示器完成模式切换 +} + +if ($Pattern) { + if ($Restore -and -not $prevMode) { + Write-Host "提示: -Restore 仅在与 -Mode 同用时生效" -ForegroundColor Yellow + } + try { + Show-PatternWindow -Device $disp.Device -StartPattern $PatternName -AutoSeconds $Seconds + } + finally { + if ($Restore -and $prevMode) { + Write-Host ("恢复原分辨率 {0}x{1}@{2} ..." -f $prevMode.dmPelsWidth, $prevMode.dmPelsHeight, $prevMode.dmDisplayFrequency) + [void](Set-DevMode $disp.Device $prevMode) + } + } +} diff --git a/HDMI-Tool/logos/logo_ph_orange.ico b/HDMI-Tool/logos/logo_ph_orange.ico new file mode 100644 index 0000000000000000000000000000000000000000..3713f836f27448aabeec4aa56f5ef533aac9f8a8 GIT binary patch literal 22047 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 literal 0 HcmV?d00001 diff --git a/HDMI-Tool/logos/logo_ph_orange.svg b/HDMI-Tool/logos/logo_ph_orange.svg new file mode 100644 index 0000000..af4af1f --- /dev/null +++ b/HDMI-Tool/logos/logo_ph_orange.svg @@ -0,0 +1,6 @@ + + + + HDMI + Tool + diff --git a/HDMI-Tool/logos/make_ico.py b/HDMI-Tool/logos/make_ico.py new file mode 100644 index 0000000..6536889 --- /dev/null +++ b/HDMI-Tool/logos/make_ico.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +把 logo_ph_orange.svg 同款设计栅格化为多尺寸 .ico(供 exe 图标) +================================================================ +设计参数与 SVG 严格一致: 黑底 512, 橙色圆角矩形 x38 y64 w436 h186 rx34, +HDMI 146px Arial Bold 黑字(框内居中), Tool 146px 白字(基线 421)。 + +输出: logos/logo_ph_orange.png(512, 预览/复用) + logos/logo_ph_orange.ico(16~256 七档, exe 图标用) + +用法: python make_ico.py +""" + +import os + +from PIL import Image, ImageDraw, ImageFont + +BASE = os.path.dirname(os.path.abspath(__file__)) +FONT = r'C:\Windows\Fonts\arialbd.ttf' # Arial Bold, Windows 标配 + + +def render(size=512): + s = size / 512.0 + img = Image.new('RGB', (size, size), (0, 0, 0)) + d = ImageDraw.Draw(img) + d.rounded_rectangle([38 * s, 64 * s, (38 + 436) * s, (64 + 186) * s], + radius=34 * s, fill=(255, 153, 0)) + if not os.path.exists(FONT): + raise SystemExit(f'未找到字体 {FONT}, 无法栅格化') + f = ImageFont.truetype(FONT, max(8, round(146 * s))) + cx = size / 2 + # HDMI: 居中于橙色框 + d.text((cx, 64 * s + 186 * s / 2), 'HDMI', font=f, fill=(0, 0, 0), anchor='mm') + # Tool: 与 SVG 基线 421 对齐(无下伸字母, 中心 = 421 - cap/2 ~= 368) + d.text((cx, 368 * s), 'Tool', font=f, fill=(255, 255, 255), anchor='mm') + return img + + +if __name__ == '__main__': + img = render(512) + png = os.path.join(BASE, 'logo_ph_orange.png') + ico = os.path.join(BASE, 'logo_ph_orange.ico') + img.save(png) + img.save(ico, sizes=[(16, 16), (24, 24), (32, 32), (48, 48), + (64, 64), (128, 128), (256, 256)]) + print(f'写出 {png}') + print(f'写出 {ico}') diff --git a/HDMI-Tool/pattern_slots.json b/HDMI-Tool/pattern_slots.json new file mode 100644 index 0000000..f49fce4 --- /dev/null +++ b/HDMI-Tool/pattern_slots.json @@ -0,0 +1,17 @@ +{ + "area": "800x600", + "slots": [ + { + "name": "西门子星", + "file": "bw_star.png" + }, + { + "name": "频率条纹", + "file": "bw_sweep.png" + }, + { + "name": "渐变棋盘", + "file": "bw_checker_sweep.png" + } + ] +} \ No newline at end of file diff --git a/HDMI-Tool/solid_000055.png b/HDMI-Tool/solid_000055.png new file mode 100644 index 0000000000000000000000000000000000000000..557398088f681ad51f4722c693b20cfe03d8d49a GIT binary patch literal 2788 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i0*Z)=h^jL%aIN-qaSW-L^Y(_JAcFx9 z^TsVQr{{Zl{+Zy&^x)_D^k=*b40EOP7#S2cFfdff=`b|RIKafv!8ytp4TRB@Kv6Cb zh+tsYT?x!qNle@f3Ti`!;gG<{@aEaU(NbWvC>*T@Msoo;7Yx<1YzP&*Bi^aYzB2x3 XXJvh{Yr%bB>y^RN)z4*}Q$iB}so}d2 literal 0 HcmV?d00001 diff --git a/HDMI-Tool/solid_0000AA.png b/HDMI-Tool/solid_0000AA.png new file mode 100644 index 0000000000000000000000000000000000000000..5a4bf499f8de43b9cb9a91aba8feed8fb7186ab0 GIT binary patch literal 2788 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i0*Z)=h^jL%aIN-qaSW-L^Y(_JAcFx9 z^TsWICW@qb{+Zy&^x)_D^k=*b40EOP7#S2cFfdff=`b|RIKafv!8ytp4TRB@Kv6Cb zh+tsYT?x!qNle@f3Ti`!;gG<{@aEaU(NbWvC>*T@Msoo;7Yx<1YzP&*Bi^aYzB1l) XW{tAb{|RizGB9|$`njxgN@xNA)djiF literal 0 HcmV?d00001 diff --git a/HDMI-Tool/solid_0000FF.png b/HDMI-Tool/solid_0000FF.png new file mode 100644 index 0000000000000000000000000000000000000000..87d7f56891c555f2562b32c2f54072211efb20ce GIT binary patch literal 2786 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i0*Z)=h^jL%aIN%oaSW-L^Y(_JAcFx9 z^TsWIEVrxFols!>@aMVlXEuh0bG+Lb7z8317;O2)7#w^O7#WVRj50<8VKgOBlnXd? z7#i-{0JD{0BP)Y|@Q`6BY+zt0`#f3-j24BX)xc;jfaHRqT9yr=Vt2$l_42Qs|5?}X U@=nSFwp|%KUHx3vIVCg!0Pmp!5&!@I literal 0 HcmV?d00001 diff --git a/HDMI-Tool/solid_005500.png b/HDMI-Tool/solid_005500.png new file mode 100644 index 0000000000000000000000000000000000000000..1a3f9164175168f25bf3209078597e29431434e1 GIT binary patch literal 2787 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i0*Z)=h^jL%aINxmaSW-L^Y(_JAcFx9 z^TsVQr{{Zl{+Zy&^x)_D^m_~p0=u3{05x$lC_F#N#L&U1!_Y9}z$jxh5Jpo1CAom1 zk(FV?Y<6I_Qrp15kOazKLxaI0#&94{ZnP8_Eec1gfzez5&ILoZEE___?nrlPLX`ZE XJFKi9($k*-8?OwWu6{1-oD!Mi0*Z)=h^jL%aINxmaSW-L^Y(_JAcFx9 z^TsWICW@qb{+Zy&^x)_D^m_~p0=u3{05x$lC_F#N#L&U1!_Y9}z$jxh5Jpo1CAom1 zk(FV?Y<6I_Qrp15kOazKLxaI0#&94{ZnP8_Eec1gfzez5&ILoZEE___?nrlPLX`Z? Xx2#c*%gtqhjaLRwS3j3^P6i0*Z)=h^jL%aIN%oaSW-L^Y(_JAcFx9 z^TsWIEVrxFols!>@aMU4J_7^C-69?ahDKI~37-=f8IG`sF*x{)GDZVoG$l}!3mOhE zG2A%Y%*r4j9Kpa~2+UtYgu%qkkYLU~S_+I7g`?HLXf6Qff+1U$4WVLpq&wAME&r~6 Wtm_$8`fvc-t_+^8elF{r5}E*7;Q=@R literal 0 HcmV?d00001 diff --git a/HDMI-Tool/solid_550000.png b/HDMI-Tool/solid_550000.png new file mode 100644 index 0000000000000000000000000000000000000000..efcd1b20a24526df3e87a6b73a378776b303551c GIT binary patch literal 2787 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i0*Z)=h^jL%aINxmaSW-L^Y(_JAcFx9 z^TsVQr{{Zl{+Zy&^x)_D=e!IIOJ}cRWKh_^z>p=U!_Y9}024z8=O|+|5Jpo1MY%vA zf`MUoB`{khF>x~}s0|s0Ljohin`Z|{OM%g%aI_j2%?03GFjUL3Ayn*+c&9G=%J|ce Wl{MdDVF9r5%HZkh=d#Wzp$P!jd%2?k literal 0 HcmV?d00001 diff --git a/HDMI-Tool/solid_AA0000.png b/HDMI-Tool/solid_AA0000.png new file mode 100644 index 0000000000000000000000000000000000000000..7be3495248696b360bd2b7898a543fa856c24ebc GIT binary patch literal 2787 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i0*Z)=h^jL%aINxmaSW-L^Y(_JAcFx9 z^TsWICW@qb{+Zy&^x)_D=e!IIOJ}cRWKh_^z>p=U!_Y9}024z8=O|+|5Jpo1MY%vA zf`MUoB`{khF>x~}s0|s0Ljohin`Z|{OM%g%aI_j2%?03GFjUL3Ayn*+c&9G=%6Mx! WYm{E?^ip8smBG{1&t;ucLK6TZ*1B{6 literal 0 HcmV?d00001 diff --git a/HDMI-Tool/solid_FF0000.png b/HDMI-Tool/solid_FF0000.png new file mode 100644 index 0000000000000000000000000000000000000000..8f5a718e6d6c2edf678e0f16ce879bbee7a60272 GIT binary patch literal 2785 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i0*Z)=h^jL%aINrkaSW-L^Y(_JAcFx9 z^TsWIEVrxFols!>@aOqwHim{vzSkKT1R@w1O!>ta9DEWO8IG`wGDZVoG$l}!3pjKb z8t&Qvvz1{ZD}#XWkYOlnU|=ZwJX#8j7KNkLz-TUjpO@~_>0S=Zma TY_uQPbY<{#^>bP0l+XkKk?#Wr literal 0 HcmV?d00001 diff --git a/I2C-Inject/README.md b/I2C-Inject/README.md new file mode 100644 index 0000000..46d422f --- /dev/null +++ b/I2C-Inject/README.md @@ -0,0 +1,70 @@ +# I2C-Inject — LT8619C 寄存器择机注入工具 + +> 更新:2026-09-03。由两部分组成:**RP2040 注入器固件**(`rp2040_inject.py`,刷入 +> BPI-Pico-RP2040,MicroPython)+ **PC 端 GUI**(`i2c_inject_gui.py` / 打包版 +> `I2C-Inject.exe`)。可在 LT8619C demo 板的 I2C 总线上安全地在线读写寄存器。 + +--- + +## 1. 工作原理(为什么不会和 demo 板冲突) + +demo 板 MCU 每 ~500ms 轮询一次 LT8619C。注入器的时序: + +``` +监听总线边沿 -> 等下一轮活动结束 -> 总线静置 50ms -> 发射 +(切 bank -> 写 -> 读回校验), 失败自动重试 3 次 -> 立即释放总线回监听 +``` + +静置窗口落在两次轮询之间的 ~490ms 空窗内,单次注入序列 <2ms,冲突概率趋近零; +即使撞上也表现为读回不符,自动重试覆盖。板载 LED:**常亮 = 监听中**,上电快闪 3 次 = 自检。 + +## 2. 硬件准备 + +| RP2040 板 | 接 LT8619C 总线 | +|---|---| +| GP8(物理脚 11) | SDA | +| GP9(物理脚 12) | SCL | +| GND | GND | + +- **不接 VCC、不外加干扰**:总线由 demo 板自带上拉(先确认上拉为 3.3V) +- 固件刷写:板子插 USB → 已有 MicroPython 则直接 + `py -m pip install mpremote && py -m mpremote connect COMx cp rp2040_inject.py :main.py` + → 复位后自启动(刷新 MicroPython UF2 会清掉 main.py,重传一次即可) +- 固件内置 viper 编译的总线嗅探(`c [ms]` 命令,~3MHz 采样),串口回吐边沿数据可离线解码 I2C + +## 3. GUI 使用 + +双击 `I2C-Inject.exe`(或 `python i2c_inject_gui.py [COMx]`): + +1. **第一排左侧**:串口下拉 →"刷新"扫描 → 选中注入器端口 →"连接"(日志出现"已连接") +2. **第一排右侧**:输入 Bank/寄存器/值(十六进制)→ **写入**(自动读回校验)/**读取** +3. **0x606E 快捷块**(位交换,语义为 demo 板 LA 实测): + - `00`=关交换;`20`=镜像 D[7:0](载蓝色分量的组,实测立即生效) + - `40`/`80`=另外两组(demo 板未单独实测,自板点亮时验证) +4. **0x606D 快捷块**(RGB 整组顺序,蓝+红双色实测=手册 naive 读法): + - `00 BGR / 03 BRG / 04 GBR / 05 GRB / 06 RBG / 07 RGB` + - 字母串顺序 = D[23:16]/D[15:8]/D[7:0] 携带的分量;切换实时生效,不影响 HDMI 锁定 +5. 回显格式:`=` 正常结果(绿色,含"读回=xx"校验值)、`!` 告警/失败(橙色) +6. 收尾:把改过的寄存器写回原值(0x606E 原值 00;0x606D 原值 07) + +## 4. 已实测的寄存器事实速查(demo 板) + +| 寄存器 | 实测结论 | +|---|---| +| 0x606E | bit5(0x20)=镜像 D[7:0](蓝值组),立即生效、含消隐码;bit7(0x80) 对该组无效;稳态下写入可持续(实测 6.7s+),板子非稳态时 0.3~0.5s 内被刷回 00 → **先投图等稳态再写,写完立即读回确认** | +| 0x606D | 000/001/010=BGR、011=BRG、100=GBR、101=GRB、110=RBG、111=RGB(字母串=D[23:16]/D[15:8]/D[7:0]),切换实时生效;demo 板 init 值 07 | + +## 5. 目录内容 + +| 文件 | 用途 | +|---|---| +| `I2C-Inject.exe` | 打包好的单文件 PC 端 GUI(需 pyserial,已打包在内) | +| `i2c_inject_gui.py` | PC 端 GUI 源码(`python build_exe.py` 重新打包) | +| `rp2040_inject.py` | RP2040 注入器固件(MicroPython,刷成 main.py 自启动) | +| `logos/` | 图标源文件与 `make_ico_i2c.py`(改 logo 后重跑再打包) | + +## 6. 故障排查 + +- 写入回显 `OK 读回=xx` 但稍后读回变了 → demo 板把它刷回了(见 §4 稳态提醒),先确认 HDMI 已稳定出图 +- 读寄存器 NAK/失败 → 检查三根接线与共地;用 `s` 命令看边沿计数是否在增长(增长=监听正常,总线有活动) +- 串口列表空白 → 换 USB 口/检查设备管理器(应出现 `USB 串行设备`,VID_2E8A) diff --git a/I2C-Inject/build_exe.py b/I2C-Inject/build_exe.py new file mode 100644 index 0000000..244ea22 --- /dev/null +++ b/I2C-Inject/build_exe.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +把 i2c_inject_gui.py 打包成单文件 exe(工具名: I2C-Inject) +================================ +使用 PyInstaller --onefile --noconsole, 目标机器无需安装 Python/pyserial。 + +用法: + python build_exe.py 自动安装 PyInstaller(如缺)并打包 + +产物: 本目录 dist/I2C-Inject.exe +""" + +import os +import subprocess +import sys + +BASE = os.path.dirname(os.path.abspath(__file__)) +ICO = os.path.join(BASE, 'logos', 'logo_i2c_orange.ico') + + +def ensure_pyinstaller(): + try: + import PyInstaller # noqa: F401 + return + except ImportError: + pass + print('PyInstaller 未安装, 正在安装...') + subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'pyinstaller']) + + +def main(): + ensure_pyinstaller() + exe = os.path.join(BASE, 'dist', 'I2C-Inject.exe') + if os.path.exists(exe): + try: + os.remove(exe) + except PermissionError: + subprocess.run(['taskkill', '/F', '/IM', 'I2C-Inject.exe'], + capture_output=True) + import time + time.sleep(1.0) + os.remove(exe) + cmd = [sys.executable, '-m', 'PyInstaller', + '--onefile', '--clean', '--name', 'I2C-Inject', + '--distpath', os.path.join(BASE, 'dist'), + '--workpath', os.path.join(BASE, 'build'), + '--specpath', os.path.join(BASE, 'build'), + '--noconsole', os.path.join(BASE, 'i2c_inject_gui.py')] + if os.path.exists(ICO): + # exe 文件图标 + 运行时窗口图标资源(从 _MEIPASS 读取) + cmd += ['--icon', ICO, '--add-data', f'{ICO};.'] + else: + print('警告: 未找到 logos/logo_i2c_orange.ico(先运行 logos/make_ico_i2c.py), 本次打包不带图标') + print('>>', ' '.join(cmd)) + subprocess.check_call(cmd) + print('完成: %s (%.1f MB)' % (exe, os.path.getsize(exe) / 1024 / 1024)) + + +if __name__ == '__main__': + main() diff --git a/I2C-Inject/i2c_inject_gui.py b/I2C-Inject/i2c_inject_gui.py new file mode 100644 index 0000000..6d65b74 --- /dev/null +++ b/I2C-Inject/i2c_inject_gui.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +I2C-Inject — LT8619C 寄存器择机注入工具 +================================================ +配合 RP2040 注入器固件(tools/rp2040_inject.py, 刷入 BPI-Pico-RP2040)使用, +在 LT8619C demo 板的 I2C 总线上择机读写寄存器。 + +特性: + - 串口选择/连接 与 寄存器操作 并列第一排, 手动输入 bank/寄存器/值 即刻注入 + - 择机机制: 等总线一轮活动结束 -> 总线静置 50ms -> 发射(切bank->写->读回校验), + 失败自动重试 3 次, 完成后立即释放总线回监听 + - 0x606E(位交换)/0x606D(整组顺序) 快捷按钮, 全部为 demo 板 LA 实测确认的语义 + +依赖: pyserial(py -m pip install pyserial) + tkinter 标准库 +用法: python i2c_inject_gui.py (也可带默认串口参数: python i2c_inject_gui.py COM9) +打包: python build_exe.py (生成 I2C-Inject.exe) +""" + +import serial +import serial.tools.list_ports +import os +import sys +import threading +import queue +import time +import tkinter as tk +from tkinter import ttk, messagebox + +if getattr(sys, 'frozen', False): + BASE_DIR = os.path.dirname(os.path.abspath(sys.executable)) +else: + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def get_icon_path(): + """工具图标: 源码模式取 logos/ 下, 打包模式取 exe 内嵌资源(_MEIPASS)""" + if getattr(sys, 'frozen', False): + p = os.path.join(getattr(sys, '_MEIPASS', ''), 'logo_i2c_orange.ico') + else: + p = os.path.join(BASE_DIR, 'logos', 'logo_i2c_orange.ico') + return p if os.path.exists(p) else None + + +class Injector: + """串口工作线程: 目标端口动态切换; 命令队列 -> RP2040, 回显 -> 消息队列""" + + def __init__(self, on_line, on_status): + self.q = queue.Queue() + self.on_line, self.on_status = on_line, on_status + self._target = None # 想要连接的端口名(None=断开) + self.ser = None + self._lock = threading.Lock() + threading.Thread(target=self._run, daemon=True).start() + + def connect(self, port): + with self._lock: + self._target = port + + def disconnect(self): + with self._lock: + self._target = None + + def send(self, cmd): + with self._lock: + if self.ser is None: + self.on_status('未连接串口, 命令被丢弃: ' + cmd) + return + self.q.put(cmd) + + def is_connected(self): + with self._lock: + return self.ser is not None + + def _run(self): + buf = b'' + while True: + with self._lock: + target = self._target + if target != (self.ser.port if self.ser else None): + if self.ser is not None: + try: + self.ser.close() + except Exception: + pass + self.ser = None + self.on_status('已断开') + if target: + try: + self.ser = serial.Serial(target, timeout=0.2) + buf = b'' + time.sleep(0.3) # 等 Pico 复位横幅打完 + self.ser.reset_input_buffer() + self.on_status('已连接 ' + target) + except Exception as e: + self.ser = None + with self._lock: + self._target = None + self.on_status('连接 %s 失败: %s' % (target, e)) + if self.ser is not None: + try: + cmd = None + try: + cmd = self.q.get_nowait() + except queue.Empty: + pass + if cmd is not None: + self.ser.write((cmd + '\n').encode()) + buf += self.ser.read(256) + while b'\n' in buf: + ln, buf = buf.split(b'\n', 1) + txt = ln.decode('utf-8', 'replace').strip() + if txt: + self.on_line(txt) + except OSError: + self.ser = None + with self._lock: + self._target = None + self.on_status('串口丢失(设备拔出?), 请重新连接') + time.sleep(0.02) + + +def run_gui(default_port=None): + root = tk.Tk() + root.title('I2C-Inject — LT8619C 寄存器择机注入器 (静置50ms)') + root.geometry('860x520') + ic = get_icon_path() + if ic: + try: + root.iconbitmap(ic) # 窗口标题栏图标 + except Exception: + pass + + log_q = queue.Queue() + + # ============ 第一排: 串口选择 + 寄存器操作(并列) ============ + row1 = ttk.LabelFrame(root, text='连接与寄存器操作 (十六进制)', padding=(8, 4)) + row1.pack(fill='x', padx=8, pady=(8, 4)) + + # --- 左半: 串口选择 --- + ttk.Label(row1, text='串口:').grid(row=0, column=0) + cbo_port = ttk.Combobox(row1, state='readonly', width=8, values=[]) + cbo_port.grid(row=0, column=1, padx=(2, 6)) + + inj = Injector(on_line=lambda s: log_q.put((s, 'rx')), + on_status=lambda s: log_q.put((s, 'st'))) + + def refresh_ports(): + ports = [p.device for p in serial.tools.list_ports.comports()] + cbo_port['values'] = ports + if ports: + cbo_port.current(0) + else: + cbo_port.set('') + inj.on_status('串口列表: %s' % (', '.join(ports) if ports else '无')) + + def toggle_connect(): + if inj.is_connected(): + inj.disconnect() + btn_conn.config(text='连接') + else: + port = cbo_port.get().strip() + if not port: + messagebox.showerror('未选择串口', '请先选择串口(或点刷新扫描)') + return + inj.connect(port) + btn_conn.config(text='断开') + + ttk.Button(row1, text='刷新', width=6, command=refresh_ports).grid(row=0, column=2, padx=(0, 4)) + btn_conn = ttk.Button(row1, text='连接', width=6, command=toggle_connect) + btn_conn.grid(row=0, column=3, padx=(0, 14)) + + # --- 右半: 寄存器操作 --- + ttk.Label(row1, text='Bank:').grid(row=0, column=4) + e_bank = ttk.Entry(row1, width=5); e_bank.insert(0, '60'); e_bank.grid(row=0, column=5, padx=(2, 8)) + ttk.Label(row1, text='寄存器:').grid(row=0, column=6) + e_reg = ttk.Entry(row1, width=5); e_reg.insert(0, '6E'); e_reg.grid(row=0, column=7, padx=(2, 8)) + ttk.Label(row1, text='值:').grid(row=0, column=8) + e_val = ttk.Entry(row1, width=5); e_val.insert(0, '00'); e_val.grid(row=0, column=9, padx=(2, 8)) + + def _hex_ok(s): + try: + return 0 <= int(s, 16) <= 0xFF + except ValueError: + return False + + def do_write(): + b, r, v = e_bank.get().strip(), e_reg.get().strip(), e_val.get().strip() + if not (_hex_ok(b) and _hex_ok(r) and _hex_ok(v)): + messagebox.showerror('输入错误', 'bank/寄存器/值 都必须是 00-FF 的十六进制') + return + inj.send('w %s %s %s' % (b, r, v)) + + def do_read(): + b, r = e_bank.get().strip(), e_reg.get().strip() + if not (_hex_ok(b) and _hex_ok(r)): + messagebox.showerror('输入错误', 'bank/寄存器 必须是 00-FF 的十六进制') + return + inj.send('r %s %s' % (b, r)) + + ttk.Button(row1, text='写入', width=8, command=do_write).grid(row=0, column=10, padx=2) + ttk.Button(row1, text='读取', width=8, command=do_read).grid(row=0, column=11, padx=2) + ttk.Label(row1, text='写=择机写入后自动读回校验; 失败自动重试3次', + foreground='#666666').grid(row=1, column=0, columnspan=12, + sticky='w', pady=(4, 0)) + + # ============ 0x606E 快捷块(位交换) ============ + qk = ttk.LabelFrame(root, text='0x606E 快捷操作 (位交换)', padding=(8, 4)) + qk.pack(fill='x', padx=8, pady=4) + for i, v in enumerate(('00', '20', '40', '80')): + ttk.Button(qk, text='写 ' + v, width=8, + command=lambda v=v: (e_val.delete(0, 'end'), e_val.insert(0, v), do_write()) + ).grid(row=0, column=i, padx=4) + ttk.Button(qk, text='读 0x606E', width=10, + command=do_read).grid(row=0, column=5, padx=4) + ttk.Label(qk, text='00=关交换 20=镜像D[7:0](LA实测) 40/80=另外两组(待自板单测)', + foreground='#666666').grid(row=1, column=0, columnspan=7, + sticky='w', pady=(3, 0)) + + # ============ 0x606D 快捷块(整组顺序) ============ + qk2 = ttk.LabelFrame(root, text='0x606D 快捷操作 (RGB 整组顺序, 双色实测=手册naive读法)', + padding=(8, 4)) + qk2.pack(fill='x', padx=8, pady=4) + for i, (code, name) in enumerate((('00', 'BGR'), ('03', 'BRG'), ('04', 'GBR'), + ('05', 'GRB'), ('06', 'RBG'), ('07', 'RGB'))): + ttk.Button(qk2, text='%s %s' % (code, name), width=8, + command=lambda c=code: inj.send('w 60 6D ' + c) + ).grid(row=0, column=i, padx=3) + ttk.Button(qk2, text='读 0x606D', width=10, + command=lambda: inj.send('r 60 6D')).grid(row=0, column=6, padx=3) + ttk.Label(qk2, text='字母串=D[23:16]/D[15:8]/D[7:0] 的分量顺序; 切换实时生效, 不影响HDMI锁定', + foreground='#666666').grid(row=1, column=0, columnspan=7, + sticky='w', pady=(3, 0)) + + # ============ 底部: 日志 ============ + frm = ttk.LabelFrame(root, text='串口日志', padding=(6, 4)) + frm.pack(fill='both', expand=True, padx=8, pady=(4, 8)) + txt_log = tk.Text(frm, height=10, font=('Consolas', 9), state='disabled', + background='#101010', foreground='#C8C8C8') + txt_log.pack(side='left', fill='both', expand=True) + sb = ttk.Scrollbar(frm, orient='vertical', command=txt_log.yview) + txt_log.configure(yscrollcommand=sb.set) + sb.pack(side='right', fill='y') + for tag, color in (('rx', '#C8C8C8'), ('st', '#88AAFF'), ('ok', '#66CC66'), + ('warn', '#E0A030')): + txt_log.tag_configure(tag, foreground=color) + + def poll_log(): + try: + while True: + ln, tag = log_q.get_nowait() + txt_log.configure(state='normal') + txt_log.insert('end', ln + '\n', tag) + if ln.startswith('='): + hot = 'warn' if ('FAIL' in ln or '丢弃' in ln) else 'ok' + txt_log.tag_add(hot, 'end-2l linestart', 'end-2l lineend') + txt_log.see('end') + txt_log.configure(state='disabled') + except queue.Empty: + pass + root.after(80, poll_log) + + refresh_ports() + if default_port: + cbo_port.set(default_port) + root.after(80, poll_log) + root.protocol('WM_DELETE_WINDOW', root.destroy) + root.mainloop() + + +if __name__ == '__main__': + run_gui(sys.argv[1] if len(sys.argv) > 1 else None) diff --git a/I2C-Inject/logos/logo_i2c_orange.ico b/I2C-Inject/logos/logo_i2c_orange.ico new file mode 100644 index 0000000000000000000000000000000000000000..24cd781671090b25af3ec39488850df0babd2163 GIT binary patch literal 26164 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 literal 0 HcmV?d00001 diff --git a/I2C-Inject/logos/logo_i2c_orange.png b/I2C-Inject/logos/logo_i2c_orange.png new file mode 100644 index 0000000000000000000000000000000000000000..5d1e980ff8b1029f79a6760871a0fea7a211ccbc GIT binary patch literal 12539 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 literal 0 HcmV?d00001 diff --git a/I2C-Inject/logos/logo_i2c_orange.svg b/I2C-Inject/logos/logo_i2c_orange.svg new file mode 100644 index 0000000..453ee51 --- /dev/null +++ b/I2C-Inject/logos/logo_i2c_orange.svg @@ -0,0 +1,6 @@ + + + + I2C + Inject + diff --git a/I2C-Inject/logos/make_ico_i2c.py b/I2C-Inject/logos/make_ico_i2c.py new file mode 100644 index 0000000..7e4baed --- /dev/null +++ b/I2C-Inject/logos/make_ico_i2c.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +把 logo_i2c_orange.svg 同款设计栅格化为多尺寸 .ico(供 I2C-Inject exe 图标) +================================================================ +设计参数与 logo_ph_orange 同风格: 黑底 512, 橙色圆角矩形 x38 y64 w436 h186 rx34, +框内 "I2C" 146px Arial Bold 黑字(居中), 下方 "Inject" 白字(基线 421)。 +"Inject" 比原版 "Tool" 长, 自动缩放到内容宽度 <= 85% 画布(436px)。 + +输出: logos/logo_i2c_orange.png(512, 预览/复用) + logos/logo_i2c_orange.ico(16~256 七档, exe 图标用) + +用法: python make_ico_i2c.py +""" + +import os + +from PIL import Image, ImageDraw, ImageFont + +BASE = os.path.dirname(os.path.abspath(__file__)) +FONT = r'C:\Windows\Fonts\arialbd.ttf' # Arial Bold, Windows 标配 +MAX_W = 436 # 内容宽度上限(85% 画布) + + +def render(size=512): + s = size / 512.0 + img = Image.new('RGB', (size, size), (0, 0, 0)) + d = ImageDraw.Draw(img) + d.rounded_rectangle([38 * s, 64 * s, (38 + 436) * s, (64 + 186) * s], + radius=34 * s, fill=(255, 153, 0)) + if not os.path.exists(FONT): + raise SystemExit(f'未找到字体 {FONT}, 无法栅格化') + px = max(8, round(146 * s)) + f = ImageFont.truetype(FONT, px) + cx = size / 2 + # I2C: 居中于橙色框(146px 下宽度远小于 436, 无需缩放) + d.text((cx, 64 * s + 186 * s / 2), 'I2C', font=f, fill=(0, 0, 0), anchor='mm') + # Inject: 基线 421 对齐(中心 ~= 368), 超宽则缩字号 + bottom_px = px + while bottom_px > 8: + w = d.textlength('Inject', font=ImageFont.truetype(FONT, bottom_px)) + if w <= MAX_W * s: + break + bottom_px -= 2 + f_bottom = ImageFont.truetype(FONT, bottom_px) + d.text((cx, 368 * s), 'Inject', font=f_bottom, fill=(255, 255, 255), anchor='mm') + return img + + +if __name__ == '__main__': + img = render(512) + png = os.path.join(BASE, 'logo_i2c_orange.png') + ico = os.path.join(BASE, 'logo_i2c_orange.ico') + img.save(png) + img.save(ico, sizes=[(16, 16), (24, 24), (32, 32), (48, 48), + (64, 64), (128, 128), (256, 256)]) + print(f'写出 {png}') + print(f'写出 {ico}') diff --git a/I2C-Inject/rp2040_inject.py b/I2C-Inject/rp2040_inject.py new file mode 100644 index 0000000..ed4f263 --- /dev/null +++ b/I2C-Inject/rp2040_inject.py @@ -0,0 +1,192 @@ +# LT8619C I2C 注入器 — BPI-Pico-RP2040 / MicroPython +# ============================================================ +# 用途: 在 demo 板(LT8619C, 7bit 地址 0x32)的 I2C 总线上择机注入寄存器写入, +# 避开 demo 板 MCU 的 ~500ms 轮询: 等一轮活动结束 + 静置 15ms 后快速 +# "切bank→写→读回校验", 失败自动重试, 完成后立即释放总线回监听。 +# 接线: GP8(物理脚11)=SDA, GP9(物理脚12)=SCL, GND 共地; 不接 VCC, 不加上拉。 +# 串口: USB CDC, 命令均十六进制, 一行一回: +# w 择机写 + 读回校验, 例: w 60 6E 80 (B通道镜像) +# r 择机读, 例: r 60 6E +# s 总线边沿计数统计 +# ? 帮助 +# 返回行以 '=' 开头为正常结果, '!' 为告警/失败。 + +import machine, time, sys, select, array, micropython + +SDA, SCL = 8, 9 +I2C_ADDR = 0x32 +FREQ = 100_000 +QUIET_MS = 50 # 静置判定窗口(总线空闲该时长后才注入) +ARM_TIMEOUT_MS = 1500 # 武装后等下一轮总线活动的超时(超过则硬闯并告警) +RETRY = 3 +CAP_EDGES = 8000 # 嗅探最大边沿数(内存 ~40KB) + +# ---- 嗅探: 紧循环读 GPIO_IN(0xD0000004) 位8/9, 变化即记录时间戳(TIMERAWL, us) ---- +# val 编码: bit0=SDA(GP8), bit1=SCL(GP9) +# 性能: 字节码 machine.mem32 ~12us/读(混叠), viper 裸指针 ~0.27us/读(~3MHz 采样) —— 必须用 viper +def _cap_edges_bc(ts, vv, max_n, timeout_us): + n = 0 + last = (machine.mem32[0xD0000004] >> 8) & 3 + tend = machine.mem32[0x40054028] + timeout_us + while n < max_n: + v = (machine.mem32[0xD0000004] >> 8) & 3 + if v != last: + ts[n] = machine.mem32[0x40054028] + vv[n] = v + n += 1 + last = v + elif machine.mem32[0x40054028] >= tend: + break + return n + + +def _cap_edges_viper(ts, vv, max_n: int, timeout_us: int) -> int: + p_gpio = ptr32(0xD0000004) + p_timer = ptr32(0x40054028) + p_ts = ptr32(ts) + p_vv = ptr8(vv) + last = int(p_gpio[0]) >> 8 & 3 + tend = int(p_timer[0]) + timeout_us + n = 0 + while n < max_n: + v = int(p_gpio[0]) >> 8 & 3 + if v != last: + p_ts[n] = int(p_timer[0]) + p_vv[n] = v + n += 1 + last = v + elif int(p_timer[0]) >= tend: + break + return n + + +_cap_edges = _cap_edges_bc +try: + _cap_edges_viper(array.array('i', bytes(4)), array.array('b', bytes(1)), 0, 0) + _cap_edges = _cap_edges_viper # viper 可用则用(~3MHz 采样) +except Exception: + pass # 退回字节码(仅够解码 <15kHz 的总线) + +last_edge_us = time.ticks_us() +edges = 0 +LED = machine.Pin(25, machine.Pin.OUT) # 板载 LED: 常亮 = 注入器监听中 + + +def _on_edge(_pin): + global last_edge_us, edges + last_edge_us = time.ticks_us() + edges += 1 + + +def bus_listen(): + """两脚浮空输入 + 双沿中断(只听不驱)""" + for p in (SDA, SCL): + pin = machine.Pin(p, machine.Pin.IN, None) + pin.irq(trigger=machine.Pin.IRQ_RISING | machine.Pin.IRQ_FALLING, + handler=_on_edge) + + +def wait_window(): + """等一轮总线活动结束再静置 QUIET_MS; 返回 True=正常等到, False=超时硬闯""" + saw = edges + t0 = time.ticks_ms() + while edges == saw: # 等下一轮活动出现(demo ~500ms 一轮) + if time.ticks_diff(time.ticks_ms(), t0) > ARM_TIMEOUT_MS: + print('! 无总线活动 %.0fms, 直接注入' % ARM_TIMEOUT_MS) + return False + time.sleep_ms(2) + t0 = time.ticks_ms() + while True: # 等静置 + if time.ticks_diff(time.ticks_us(), last_edge_us) >= QUIET_MS * 1000: + return True + if time.ticks_diff(time.ticks_ms(), t0) > 2000: + print('! 静置超时, 直接注入') + return False + time.sleep_us(200) + + +def inject(bank, reg, val=None): + """注入并校验; val=None 只读。返回 (ok, readback)""" + for attempt in range(1, RETRY + 1): + wait_window() + i2c = machine.I2C(0, sda=machine.Pin(SDA), scl=machine.Pin(SCL), freq=FREQ) + try: + if val is not None: + i2c.writeto(I2C_ADDR, bytes([0xFF, bank])) # 切 bank + i2c.writeto(I2C_ADDR, bytes([reg, val])) # 写寄存器 + rb = i2c.readfrom_mem(I2C_ADDR, reg, 1)[0] # repeated-start 读回 + except OSError as e: + print('! 第%d次总线错误 %s' % (attempt, e)) + rb = None + finally: + bus_listen() # 立即释放总线 + if rb is None: + continue + if val is None or rb == val: + return True, rb + print('! 第%d次校验不符: 写%02X 读%02X' % (attempt, val, rb)) + return False, None + + +def main(): + bus_listen() + for _ in range(3): # 上电自检: 快闪 3 次 + LED.on(); time.sleep_ms(80); LED.off(); time.sleep_ms(80) + LED.on() # 常亮 = 监听中 + print('LT8619C 注入器就绪 GP8=SDA GP9=SCL %dkHz addr 0x%02X' % (FREQ // 1000, I2C_ADDR)) + poll = select.poll() + poll.register(sys.stdin, select.POLLIN) + buf = b'' + while True: + try: + if poll.poll(50): + buf += sys.stdin.read(1) + if buf[-1:] not in (b'\n', b'\r'): + continue + line = buf.strip().decode() + buf = b'' + parts = line.split() + if not parts: + continue + cmd = parts[0].lower() + if cmd == 'w' and len(parts) == 4: + bank, reg, val = (int(x, 16) for x in parts[1:]) + ok, rb = inject(bank, reg, val) + print('= w %02X %02X %02X : %s 读回=%s' % + (bank, reg, val, 'OK' if ok else 'FAIL', + '%02X' % rb if rb is not None else '--')) + elif cmd == 'r' and len(parts) == 3: + bank, reg = (int(x, 16) for x in parts[1:]) + ok, rb = inject(bank, reg) + print('= r %02X %02X : %s %s' % + (bank, reg, 'OK' if ok else 'FAIL', + '%02X' % rb if rb is not None else '--')) + elif cmd == 's': + print('= 边沿计数 %d' % edges) + elif cmd == 'c': + ms = int(parts[1]) if len(parts) > 1 else 700 + for p in (SDA, SCL): # 捕获期间暂停监听中断 + machine.Pin(p, machine.Pin.IN, None).irq(handler=None) + ts = array.array('i', bytes(4 * CAP_EDGES)) + vv = array.array('b', bytes(CAP_EDGES)) + n = _cap_edges(ts, vv, CAP_EDGES, ms * 1000) + bus_listen() + print('= capture %d edges / %d ms' % (n, ms)) + t0 = ts[0] if n else 0 + out = [] + for i in range(n): + out.append('%d %d' % (ts[i] - t0, vv[i])) + if len(out) >= 100: + print('C ' + '\nC '.join(out)) + out = [] + if out: + print('C ' + '\nC '.join(out)) + print('C.') + else: + print('? w bank reg val | r bank reg | s | c [ms]') + except KeyboardInterrupt: + break + print('退出') + + +main() diff --git a/README.md b/README.md index 9d2a91a..c436b3c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,35 @@ # LT8619C_Debug_Tools -本仓库用存储LT8619C调试过程中需要的一些小工具(包括AI生成) \ No newline at end of file +LT8619C(HDMI→RGB888)→ DLPC3421 投影转接板调试配套上位机工具集。 +上游主仓库(固件):[LT8619C_DLPC3421_HDMI](https://gitea.hulk.wang/hulk/LT8619C_DLPC3421_HDMI)。 + +## 工具一览 + +| 工具 | 用途 | 快速开始 | +|---|---|---| +| [HDMI-Tool](HDMI-Tool/) | HDMI 源端:EDID 查看/解算、分辨率切换、纯色与测试图案全屏投屏(R/G/B 纯色值按钮用于位交换类测试) | 双击 `HDMI-Tool.exe`;详见 [HDMI-Tool/README.md](HDMI-Tool/README.md) | +| [I2C-Inject](I2C-Inject/) | LT8619C 寄存器在线读写:RP2040 注入器在 demo 板 I2C 总线上择机注入(避让主控轮询,写后自动读回校验) | 需配合 RP2040 固件 `rp2040_inject.py`;详见 [I2C-Inject/README.md](I2C-Inject/README.md) | + +## 环境要求 + +- 打包版 exe 开箱即用(Windows 10/11 x64,无需安装 Python) +- 源码运行:Python ≥3.10;HDMI-Tool 为纯标准库(自定义图片 JPG/BMP 需 Pillow); + I2C-Inject 需 `py -m pip install pyserial` +- 重新打包:各工具目录内 `python build_exe.py`(自动安装 PyInstaller) + +## 目录结构 + +``` +LT8619C_Debug_Tools/ +├── HDMI-Tool/ # 源码 + 打包 exe + 测试图资产 + 图标 + README +├── I2C-Inject/ # PC 端 GUI + RP2040 固件 + 打包 exe + 图标 + README +├── LICENSE +└── README.md +``` + +## 备注 + +- 源码与打包产物均入库:exe 改动跟随源码提交更新 +- 两件工具的寄存器语义(0x606E 位交换 / 0x606D 整组顺序)均经 demo 板逻辑分析仪 + 双色交叉实测,过程数据与规程存于上游主仓库 `doc/architecture/` +- 更新:2026-09-03