Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ab8264752 | ||
|
|
1f7e697558 | ||
|
|
65c85c2844 | ||
|
|
c38f12b230 |
@@ -0,0 +1,11 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# PyInstaller 工作目录(打包产物 exe 已直接入库, 构建中间物不入库)
|
||||
build/
|
||||
dist/
|
||||
*.spec
|
||||
|
||||
# 发布 zip: 可由 src/package_release.py 重新生成, 只上传 Gitea Release
|
||||
HDMI-Tool/release/*.zip
|
||||
@@ -0,0 +1,105 @@
|
||||
# AGENTS.md — HDMI-Tool 工程档案(供 AI/Agent 使用)
|
||||
|
||||
> **本文档面向 AI/Agent**,是人类文档 [README.md](README.md) 的姊妹篇,两者不重叠:
|
||||
> README 回答"这是什么、怎么用";本文回答"代码怎么组织、怎么构建发布、改动时哪些约束不能破坏"。
|
||||
> 任何 Agent 在修改本目录代码前应先读完本文。
|
||||
|
||||
## 1. 项目定位
|
||||
|
||||
仓库 `LT8619C_Debug_Tools` 是 LT8619C(HDMI→RGB888)→ DLPC3421 投影转接板的**调试上位机工具集**,
|
||||
上游主仓库(固件)为 `hulk/LT8619C_DLPC3421_HDMI`(同 Gitea 实例 `gitea.hulk.wang`)。
|
||||
本工具工作在 HDMI **源端**(PC 显卡侧),与固件侧寄存器调试(I2C-Inject 工具)配合。
|
||||
寄存器语义判读基线(0x606E 位交换 / 0x606D 整组顺序)在上游仓库 `doc/architecture/`,不在本仓库。
|
||||
|
||||
## 2. 目录契约(重要)
|
||||
|
||||
```
|
||||
HDMI-Tool/
|
||||
├── src/ 源代码区 —— 只有这里可以手改(logos/ 改图标源后重跑 make_ico.py)
|
||||
├── assets/ 生成资产区 —— gen_*.py 的产物(PNG 图案),可随时再生成,改动需提交
|
||||
├── release/ 产物区 —— 机器生成,禁止手改:HDMI-Tool.exe(入库)、
|
||||
│ pattern_slots.json(exe 运行配置,入库)、*.zip(gitignore,只上传 Release)
|
||||
├── README.md 人类文档
|
||||
└── AGENTS.md 本文
|
||||
```
|
||||
|
||||
划分原则:**"人写的"在 src/,"生成的"按去向分 assets/(随包分发)与 release/(直接交付)**。
|
||||
新文件必须落对区域;不要在 src/ 外新建源码,不要手改 assets/ 下的 PNG(改生成脚本再跑)。
|
||||
|
||||
### 文件角色速查
|
||||
|
||||
| 文件 | 角色 |
|
||||
|---|---|
|
||||
| `src/hdmi_gui.py` | 单文件主程序:GUI(tkinter) + 命令行,约 1500 行,纯标准库 |
|
||||
| `src/build_exe.py` | PyInstaller 打包 → `release/HDMI-Tool.exe`(onefile/noconsole,内嵌图标) |
|
||||
| `src/package_release.py` | exe+图案+说明 → `release/HDMI-Tool-v<ver>-win64.zip`(校验栏位引用完整性) |
|
||||
| `src/publish_gitea.py` | 建/复用 Gitea Release 并上传 zip(tag 默认取 zip 文件名) |
|
||||
| `src/hdmi_source.ps1/.bat` | 独立 PowerShell 命令行工具(自包含,与 GUI 无代码共享) |
|
||||
| `src/gen_bw_patterns.py` | 生成 `assets/bw_*.png` ×5(640x360 黑白,需 Pillow) |
|
||||
| `src/gen_solid_colors.py` | 生成 `assets/solid_*.png` ×10(800x600 纯色,需 Pillow) |
|
||||
| `src/la_wave_analyzer*.py` | 逻辑分析仪 CSV 波形判读(2/3 通道,40MHz 像素时钟假设) |
|
||||
| `src/logos/make_ico.py` | svg/png → ico(写在其自身目录) |
|
||||
| `release/pattern_slots.json` | 预置栏位:西门子星/波带片/综合测试卡,area=640x360 |
|
||||
|
||||
## 3. 代码不变量(改 hdmi_gui.py 必须维持)
|
||||
|
||||
1. **纯标准库**:hdmi_gui.py 只准 import 标准库(ctypes/winreg/tkinter/…)。Pillow 是
|
||||
*可选*加速(仅扩展自定义图片格式 PNG/GIF 之外的 JPG/BMP/WebP),缺失时必须照常工作。
|
||||
2. **BASE_DIR 语义**:frozen(PyInstaller onefile)时 `BASE_DIR=exe 所在目录`(`sys.executable`),
|
||||
源码运行时 `=hdmi_gui.py 所在目录`(`__file__`)。所有用户数据(pattern_slots.json、
|
||||
自定义图片)都写/读自 BASE_DIR **平铺**存放,不建子目录。
|
||||
3. **自定义栏位 `file` 字段是裸文件名**:加载时 `os.path.join(BASE_DIR, file)` 解析,
|
||||
因此 pattern_slots.json 引用的图片必须与 exe 同目录 —— package_release.py 正是按此平铺打包。
|
||||
4. **exe 自包含**:图标经 `--add-data` 内嵌(运行时从 `_MEIPASS` 读);内置五种图案
|
||||
(红/绿/蓝/棋盘格/网格线)由程序绘制,运行时**不依赖** assets/;solid/bw PNG 只服务自定义栏位。
|
||||
5. **DPI 感知**:进程启动即 `SetProcessDpiAwareness(2)`,图案窗口按物理像素定位,勿移除。
|
||||
6. **启动强制扩展拓扑**:`SetDisplayConfig(SDC_TOPOLOGY_EXTEND|SDC_APPLY)`,静默容错
|
||||
(失败不阻断启动)。动机:LT8619C 热插拔后 Windows 可能停留在"仅电脑屏幕"。
|
||||
7. **分辨率切换是临时的**:走 `CHANGEDISPLAYSETTINGS`(不写注册表),"恢复"用会话开始时快照。
|
||||
8. **EDID 来源是注册表**(`HKLM\SYSTEM\CurrentControlSet\Enum\DISPLAY\...\Device Parameters\EDID`),
|
||||
不是 WMI/DDC;exe 的 noconsole 版无 stdout,`--dump/--edid-fields` 只在源码/控制台版可用。
|
||||
|
||||
## 4. 领域语义(测试值都有含义,勿"顺手改")
|
||||
|
||||
- 纯色值 55/AA/FF:55(01010101) 与 AA(10101010) 互为字节镜像对(检出位序颠倒/位交换),
|
||||
FF(11111111) 全高基准;R/G/B 三行对应 RGB888 三字节组(R=D[23:16]/G=D[15:8]/B=D[7:0])。
|
||||
- 800x600@60:demo 板 shadow EDID 首选时序(solid 系列分辨率与之一致)。
|
||||
- 640x360:测试卡区域默认值(16:9 半分辨率,点对点检查用);full=铺满所选屏。
|
||||
- bw 系列图案各自目的见 `src/gen_bw_patterns.py` 顶部清单注释。
|
||||
|
||||
## 5. 构建 / 发布流水线(全部在 src/ 下执行)
|
||||
|
||||
```powershell
|
||||
python gen_bw_patterns.py ; python gen_solid_colors.py # 仅当改了图案生成逻辑
|
||||
python build_exe.py # → ../release/HDMI-Tool.exe(缺 PyInstaller 会自动 pip install)
|
||||
python build_exe.py --debug # 附加带控制台的 HDMI-Tool-console.exe(调试 --dump 用)
|
||||
python package_release.py <版本> # → ../release/HDMI-Tool-v<版本>-win64.zip
|
||||
python publish_gitea.py HDMI-Tool-v<版本>-win64.zip --replace # 上传(需 GITEA_TOKEN 环境变量或 --token)
|
||||
```
|
||||
|
||||
发布约定:
|
||||
- tag/release 命名 `HDMI-Tool-vX.Y.Z`,zip 命名 `HDMI-Tool-vX.Y.Z-win64.zip`(publish_gitea.py
|
||||
依赖此命名从文件名推 tag);
|
||||
- **exe 与 pattern_slots.json 改动跟随源码提交入库;zip 永不入库**(.gitignore 已挡);
|
||||
- publish_gitea.py 幂等:release 已存在则复用,同名资产默认跳过,`--replace` 删旧重传;
|
||||
- Gitea 实例 `gitea.hulk.wang`,API 走 https(443),git 走 ssh(1234)——从 origin 远程自动解析;
|
||||
自签证书报 SSL 错时加 `--insecure`。
|
||||
|
||||
## 6. 已知坑(历史踩过,勿重蹈)
|
||||
|
||||
- onefile 运行时 `__file__` 指向临时解压目录 `_MEIPASS`,定位 exe 必须用 `sys.executable`(§3.2);
|
||||
- 重新打包前旧 exe 可能被残留进程占用 → build_exe.py 会 taskkill 同名进程,手改打包脚本时保留;
|
||||
- 打包后本机图标缓存不刷新:`ie4uinit -show`,不是产物问题;
|
||||
- 改图标流程:改 svg → `make_ico.py` 生成 ico → 重新打包(ico 同时是文件图标与运行时窗口图标);
|
||||
- GUI 与 hdmi_source.ps1 各自独立实现显示器枚举,行为可能微差(GUI 与 `-List` 序号一致,
|
||||
但与"设置"面板序号无保证);多屏操作前先确认序号;
|
||||
- `pattern_slots.json` 兼容两种格式:新 `{area, slots[]}`,旧纯数组(无 area)——load_slots 已兼容,
|
||||
但写出只应写新格式。
|
||||
|
||||
## 7. 验证清单(改完代码后)
|
||||
|
||||
1. `python hdmi_gui.py --seconds 5` —— GUI 冒烟(自动开关);
|
||||
2. `python hdmi_gui.py --test-pattern Checker --area 800x600 --seconds 3` —— 投图链路;
|
||||
3. `python hdmi_gui.py --dump` —— EDID 解算不抛异常;
|
||||
4. `python build_exe.py` → 双击 release/HDMI-Tool.exe 确认图标/启动/投图;
|
||||
5. `python package_release.py <版本>` —— 不报"引用图片缺失"即栏位完整性 OK。
|
||||
@@ -0,0 +1,181 @@
|
||||
# HDMI-Tool — HDMI 源端调试工具
|
||||
|
||||
> LT8619C(HDMI→RGB888)→ DLPC3421 投影转接板调试配套的 PC 端工具:
|
||||
> 在 HDMI **源端**(电脑显卡输出)完成 EDID 查看、分辨率切换、纯色/测试图案全屏投屏。
|
||||
>
|
||||
> - **只想用工具**:到 [Gitea Releases](https://gitea.hulk.wang/hulk/LT8619C_Debug_Tools/releases)
|
||||
> 下载 `HDMI-Tool-vX.Y.Z-win64.zip`,解压双击 `HDMI-Tool.exe` 即可(Windows 10/11 x64,免安装、无需 Python)。
|
||||
> - **要改代码/重新打包**:见 §7、§8。
|
||||
>
|
||||
> 更新:2026-09-10
|
||||
|
||||
---
|
||||
|
||||
## 1. 功能总览
|
||||
|
||||
| 功能 | 说明 |
|
||||
|---|---|
|
||||
| EDID 查看 | 枚举各显示输出口,从注册表读取 EDID 原始数据,按 EDID 1.4/CEA-861 解算:厂商/型号/尺寸/Gamma/输入类型/支持模式(DTD/已建立/标准/CEA-VIC)/首选时序完整参数/校验和 |
|
||||
| EDID 逐字节解析 | "EDID 原始数据"框右上"逐字节解析"按钮:按偏移逐字段展开基础块与 CTA-861 扩展块,每字段附原始字节 hex;命令行等价 `--edid-fields` |
|
||||
| 分辨率切换 | "分辨率"下拉列出显卡按该屏 EDID 提供的全部模式;**应用**=临时切换(不写注册表,重启/恢复即还原),**恢复**=回到本次会话切改前的模式 |
|
||||
| 测试图案投屏 | 全屏投到所选显示器,测试卡区域可选 `640x360 / 800x600 / full 铺满`;内置 纯红/纯绿/纯蓝/黑白棋盘格/网格线 + R/G/B 三行 55/AA/FF 纯色值按钮 + 3 个自定义图片栏位 |
|
||||
| 显示拓扑控制 | 启动时强制扩展显示拓扑(复制/仅第二屏等模式下保证 demo 板屏幕独立可控) |
|
||||
| 命令行模式 | 全部功能可脚本化调用,供自动化测试(见 §5、§6) |
|
||||
| 波形分析脚本 | 逻辑分析仪 CSV 波形按占空比签名分段,输出行结构/占空比/消隐明细(见 §6.3) |
|
||||
|
||||
## 2. 下载与安装
|
||||
|
||||
1. 打开 [Releases 页面](https://gitea.hulk.wang/hulk/LT8619C_Debug_Tools/releases),取最新的
|
||||
`HDMI-Tool-vX.Y.Z-win64.zip`;
|
||||
2. 解压到**任意可写目录**(自定义图片与栏位配置会写到 exe 旁边);
|
||||
3. 双击 `HDMI-Tool.exe`。首次启动比脚本慢一两秒(onefile 解压),属正常现象。
|
||||
|
||||
包内包含:
|
||||
|
||||
```
|
||||
HDMI-Tool/
|
||||
├── HDMI-Tool.exe 主程序(单文件,自包含)
|
||||
├── pattern_slots.json 预置自定义栏位(指向下面的 bw 图案)
|
||||
├── bw_*.png ×5 640x360 黑白测试图
|
||||
├── solid_*.png ×10 800x600 纯色测试图
|
||||
└── README.md 本说明
|
||||
```
|
||||
|
||||
## 3. GUI 使用
|
||||
|
||||
主窗口自上而下三块区域:
|
||||
|
||||
```
|
||||
┌ 显示器: [GDI ... ▼] [刷新] [拓扑: 扩展 ▼] ┐
|
||||
├ 分辨率: [800x600@60 ▼] [应用] [恢复] ┐
|
||||
├ ── EDID 原始数据 ──────────── [逐字节解析] ─┤
|
||||
│ (hex 文本框) │
|
||||
├ 测试卡区域: (○640x360 ○800x600 ○full) ┤
|
||||
├ [纯红][纯绿][纯蓝][棋盘格][网格线] │ ← 图案按钮
|
||||
├ R: [55][AA][FF] G: [55][AA][FF] B: [55][AA][FF] │ ← 纯色值按钮
|
||||
├ [自定义1][自定义2][自定义3] [⚙] │ ← 自定义栏位
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
1. 顶部显示器下拉选择目标屏(多屏时注意别选成主屏),"刷新"重新枚举;
|
||||
2. **切分辨率**:第二行选模式 → "应用";测完点"恢复";
|
||||
3. **投测试图**:
|
||||
- 先在图案按钮行上方选**测试卡区域**(640x360 / 800x600 / full);
|
||||
- 点任意图案按钮即全屏投放;按 **ESC** 或点击退出;
|
||||
- R/G/B 三行纯色按钮(55/AA/FF)用于 RGB 位交换类测试:55(01010101) 与
|
||||
AA(10101010) 互为字节镜像对,FF(11111111) 为全高基准;
|
||||
- 自定义图片:点"⚙"配置名称与图片文件(图片会自动复制到 exe 旁边),点栏位名投放;
|
||||
4. 测试卡区域与自定义栏位配置自动保存在 exe 旁的 `pattern_slots.json`。
|
||||
|
||||
## 4. 典型测试场景
|
||||
|
||||
```
|
||||
1. HDMI 接 demo 板 → 启动 HDMI-Tool → 刷新 → 选中 demo 板显示器
|
||||
2. 分辨率切 800x600@60(demo 板 shadow EDID 首选)→ 测试卡区域选 800x600
|
||||
3. 点 R/G/B 行纯色按钮投图 → 配合逻辑分析仪/万用表测转接板 RGB888 输出
|
||||
- 纯红/绿/蓝:验证通道映射(R=D[23:16], G=D[15:8], B=D[7:0])
|
||||
- 55/AA 对:验证位交换(0x606E)——图像应为 50% 灰阶占空比
|
||||
4. 换 bw 图案(西门子星/波带片)看极限分辨率与混叠
|
||||
5. 测完"恢复"分辨率
|
||||
```
|
||||
|
||||
寄存器语义(0x606E 位交换 / 0x606D 整组顺序)的判读基线见上游主仓库
|
||||
[LT8619C_DLPC3421_HDMI](https://gitea.hulk.wang/hulk/LT8619C_DLPC3421_HDMI) 的
|
||||
`doc/architecture/`。
|
||||
|
||||
## 5. 命令行模式(hdmi_gui.py)
|
||||
|
||||
exe 版不支持 `--dump/--edid-fields`(noconsole 无控制台输出),请用源码方式跑:
|
||||
|
||||
```
|
||||
python hdmi_gui.py --dump 控制台输出 EDID 解算
|
||||
python hdmi_gui.py --edid-fields 控制台输出 EDID 逐字节字段解析表
|
||||
python hdmi_gui.py --test-pattern Blue --area 800x600 --seconds 5
|
||||
出图 5 秒自动退出
|
||||
python hdmi_gui.py --test-pattern Custom1 投自定义栏位 1
|
||||
python hdmi_gui.py --seconds 5 GUI 冒烟测试, 5 秒自动关
|
||||
--test-pattern {Red,Green,Blue,Checker,Grid,Custom1..3}
|
||||
--area {640x360,800x600,full} --monitor N --seconds S
|
||||
```
|
||||
|
||||
## 6. 辅助脚本(src/ 下)
|
||||
|
||||
### 6.1 hdmi_source.bat / .ps1 — 源端命令行工具
|
||||
|
||||
与 GUI 互补,适合脚本串联:
|
||||
|
||||
```
|
||||
tools\hdmi_source.bat -List 枚举显示器与支持分辨率(验证 EDID)
|
||||
tools\hdmi_source.bat -Monitor 1 -Mode 800x600 切模式
|
||||
tools\hdmi_source.bat -Mode 800x600 -Pattern -Restore
|
||||
切模式→出图案→退出后自动恢复
|
||||
```
|
||||
|
||||
### 6.2 gen_bw_patterns.py / gen_solid_colors.py — 测试图生成
|
||||
|
||||
需 Pillow(`py -m pip install pillow`)。重新生成 `assets/` 下全部图案:
|
||||
|
||||
- `gen_bw_patterns.py` → 640x360 黑白图 ×5:
|
||||
`bw_star.png` 西门子星(极限分辨率)、`bw_sweep.png` 频率扫描条纹(24px→3px)、
|
||||
`bw_checker_sweep.png` 渐变棋盘(2→32px)、`bw_zoneplate.png` 波带片(各向混叠)、
|
||||
`bw_testcard.png` 综合测试卡(灰阶/刻度/细棋盘/线宽组);
|
||||
- `gen_solid_colors.py` → 800x600 纯色图 ×10(R/G/B 各 55/AA/FF + 三原色),
|
||||
与 GUI 内置纯色按钮同源,供自定义栏位或其他工具加载。
|
||||
|
||||
### 6.3 la_wave_analyzer.py / la_wave_analyzer3.py — 逻辑分析仪波形判读
|
||||
|
||||
分析 Kingst VIS 导出的边沿流 CSV(列:`Time[s], 通道A, 通道B[, 通道C]`,
|
||||
10ns 分辨率),按 50ms 窗口占空比签名自动分段,输出每段的签名(FF/55/AA/00)、
|
||||
时长、行周期/有效区宽度/消隐(按 40MHz 像素时钟折算):
|
||||
|
||||
```
|
||||
python la_wave_analyzer.py <capture.csv> # 2 通道(默认判读 B 口 D0/D1)
|
||||
python la_wave_analyzer3.py <capture.csv> # 3 通道(RGB 三组同拍, ch8/9/10)
|
||||
```
|
||||
|
||||
## 7. 目录结构与从源码运行
|
||||
|
||||
```
|
||||
HDMI-Tool/
|
||||
├── src/ # 源代码(人工维护区)
|
||||
│ ├── hdmi_gui.py # 主程序:GUI + 命令行, 纯标准库
|
||||
│ ├── build_exe.py # PyInstaller 打包 → release/
|
||||
│ ├── package_release.py # 组装发布 zip(exe+图案+说明)
|
||||
│ ├── publish_gitea.py # 上传到 Gitea Release
|
||||
│ ├── hdmi_source.ps1/.bat # 源端分辨率/图案命令行工具
|
||||
│ ├── gen_bw_patterns.py # 生成 assets/bw_*.png
|
||||
│ ├── gen_solid_colors.py # 生成 assets/solid_*.png
|
||||
│ ├── la_wave_analyzer*.py # 逻辑分析仪 CSV 波形判读
|
||||
│ └── logos/ # 图标源(svg/png/ico + make_ico.py)
|
||||
├── assets/ # 生成的测试图案(gen_*.py 产物, 随发布包分发)
|
||||
│ ├── bw_*.png ×5 # 640x360 黑白系列
|
||||
│ └── solid_*.png ×10 # 800x600 纯色系列
|
||||
├── release/ # 构建与发布产物(机器生成区, 不手改)
|
||||
│ ├── HDMI-Tool.exe # 打包单文件版(改动跟随源码提交入库)
|
||||
│ ├── pattern_slots.json # exe 运行配置(默认栏位预置 bw 图案)
|
||||
│ └── *.zip # 发布包(不入库, 上传 Gitea Release)
|
||||
├── README.md # 本说明
|
||||
└── AGENTS.md # 面向 AI/Agent 的工程档案(结构/约定/构建规程)
|
||||
```
|
||||
|
||||
从源码运行:`python src/hdmi_gui.py`,需 Python ≥3.10,纯标准库无第三方依赖
|
||||
(自定义图片要 JPG/BMP/WebP 时需可选 Pillow)。
|
||||
|
||||
## 8. 构建与发布
|
||||
|
||||
```powershell
|
||||
cd src
|
||||
python build_exe.py # 打包 → ../release/HDMI-Tool.exe(自动装 PyInstaller)
|
||||
python build_exe.py --debug # 额外出一个带控制台的调试变体(--dump 可用)
|
||||
python package_release.py 0.1.0 # 组装 release/HDMI-Tool-v0.1.0-win64.zip
|
||||
python publish_gitea.py HDMI-Tool-v0.1.0-win64.zip # 上传 Gitea Release(需 GITEA_TOKEN)
|
||||
```
|
||||
|
||||
## 9. 注意事项 / FAQ
|
||||
|
||||
- **exe 放只读目录会怎样**:自定义图片与 `pattern_slots.json` 写在 exe 旁边,只读目录会导致栏位配置存不下;
|
||||
- **重新打包后图标没变**:Windows 图标缓存,`ie4uinit -show` 或改个名即可;
|
||||
- **多屏序号**:切分辨率前先确认显示器序号(GUI 下拉框与 `hdmi_source.bat -List` 输出一致);
|
||||
- **残留进程**:修改源码重新打包前先结束残留的 HDMI-Tool 进程(`build_exe.py` 会自动 taskkill);
|
||||
- **改 logo**:改 `src/logos/logo_ph_orange.svg` → `python src/logos/make_ico.py` → 重新打包;
|
||||
- **配置互不影响**:exe 与源码脚本各自按所在目录保存 `pattern_slots.json`,两份安装互不干扰。
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"area": "640x360",
|
||||
"slots": [
|
||||
{
|
||||
"name": "西门子星",
|
||||
"file": "bw_star.png"
|
||||
},
|
||||
{
|
||||
"name": "BW_ZONE",
|
||||
"file": "bw_zoneplate.png"
|
||||
},
|
||||
{
|
||||
"name": "BW_TEST",
|
||||
"file": "bw_testcard.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/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 可用)
|
||||
|
||||
产物: release/HDMI-Tool.exe(及 --debug 时的 HDMI-Tool-console.exe)
|
||||
首次运行 onefile exe 会先解压到临时目录, 启动比脚本慢一两秒, 属正常现象。
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# 本脚本位于 src/, 打包产物输出到工具根的 release/(与 pattern_slots.json 同目录)
|
||||
SRC = os.path.dirname(os.path.abspath(__file__))
|
||||
BASE = os.path.dirname(SRC)
|
||||
ICO = os.path.join(SRC, '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, 'release')
|
||||
work = os.path.join(SRC, '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(SRC, '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()
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
生成 640x360 黑白测试图, 供 HDMI 源端工具的自定义栏位使用
|
||||
================================================================
|
||||
输出到 ../assets/(仓库资产目录, 发布打包时随 zip 分发); 预置栏位配置
|
||||
release/pattern_slots.json 指向前 3 张, 其余两张可用 "⚙" 换上。
|
||||
|
||||
图案清单(均为 640x360 黑白, 1:1 投屏无缩放):
|
||||
bw_star.png 西门子星: 60 楔放射星, 看极限分辨率与中心偏移
|
||||
bw_sweep.png 频率扫描条纹: 周期 24px 渐减到 3px(上竖线/下横线)
|
||||
bw_checker_sweep.png 渐变棋盘: 格子尺寸 2px 递增到 32px 共 9 段
|
||||
bw_zoneplate.png 同心波带片: 环距向中心递减, 看各向混叠
|
||||
bw_testcard.png 综合测试卡: 灰阶/刻度/细棋盘/粗细线/标记
|
||||
|
||||
用法: python gen_bw_patterns.py
|
||||
"""
|
||||
|
||||
import math
|
||||
import os
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
W, H = 640, 360
|
||||
SRC = os.path.dirname(os.path.abspath(__file__))
|
||||
OUT = os.path.join(os.path.dirname(SRC), 'assets')
|
||||
|
||||
|
||||
def save(img, name):
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
path = os.path.join(OUT, name)
|
||||
img.save(path)
|
||||
print(f'写出 {path}')
|
||||
|
||||
|
||||
def gen_star():
|
||||
"""西门子星: 60 个黑白交替楔形, 边缘楔宽约 17px, 向中心无限加密"""
|
||||
img = Image.new('L', (W, H), 255)
|
||||
d = ImageDraw.Draw(img)
|
||||
cx, cy, R = W // 2, H // 2, 165
|
||||
n = 60
|
||||
for i in range(0, n, 2): # 黑楔在偶数位, 底为白
|
||||
d.pieslice([cx - R, cy - R, cx + R, cy + R],
|
||||
i * 360.0 / n, (i + 1) * 360.0 / n, fill=0)
|
||||
d.ellipse([cx - R - 6, cy - R - 6, cx + R + 6, cy + R + 6],
|
||||
outline=0, width=3)
|
||||
d.rectangle([0, 0, W - 1, H - 1], outline=0, width=2)
|
||||
save(img, 'bw_star.png')
|
||||
|
||||
|
||||
def gen_sweep():
|
||||
"""频率扫描条纹: 左到右黑白条纹周期 24px 渐减到 3px; 上半竖线, 下半横线"""
|
||||
img = Image.new('L', (W, H), 255)
|
||||
px = img.load()
|
||||
# 上半: 竖条纹
|
||||
ph = 0.0
|
||||
for x in range(W):
|
||||
t = x / (W - 1)
|
||||
ph += 1.0 / (24 - 21 * t)
|
||||
if (ph % 1.0) < 0.5:
|
||||
for y in range(H // 2):
|
||||
px[x, y] = 0
|
||||
# 下半: 横条纹
|
||||
ph = 0.0
|
||||
for y in range(H // 2, H):
|
||||
t = (y - H // 2) / (H // 2 - 1)
|
||||
ph += 1.0 / (24 - 21 * t)
|
||||
if (ph % 1.0) < 0.5:
|
||||
for x in range(W):
|
||||
px[x, y] = 0
|
||||
d = ImageDraw.Draw(img)
|
||||
d.line([0, H // 2, W, H // 2], fill=0, width=2)
|
||||
d.rectangle([0, 0, W - 1, H - 1], outline=0, width=2)
|
||||
d.text((8, 6), 'H sweep 24px->3px', 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')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
gen_star()
|
||||
gen_sweep()
|
||||
gen_checker_sweep()
|
||||
gen_zoneplate()
|
||||
gen_testcard()
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
生成 800x600 纯色测试图, 供 RGB 位交换(0x606E)验证使用
|
||||
================================================================
|
||||
配合 demo 板当前 EDID 首选时序 800x600@60, 全屏纯色输出。
|
||||
用法: python gen_solid_colors.py → 输出到 ../assets/solid_*.png
|
||||
在 HDMI-Tool 中通过自定义栏位(⚙ 换图)加载, 发布包随附。
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from PIL import Image
|
||||
|
||||
W, H = 800, 600 # 与 demo 板 EDID 首选时序一致, 全屏铺满
|
||||
SRC = os.path.dirname(os.path.abspath(__file__))
|
||||
OUT = os.path.join(os.path.dirname(SRC), 'assets')
|
||||
|
||||
# (文件名后缀, 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()
|
||||
@@ -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" %*
|
||||
@@ -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 <n> -Mode <WxH[@Hz]> 切换分辨率, 如 -Mode 640x360 或 640x360@60
|
||||
hdmi_source.bat [-Monitor <n>] -Pattern [-PatternName <名>] [-Seconds <n>]
|
||||
全屏测试图案(Bar/GraySteps/Gradient/Checker/Grid/纯色)
|
||||
可选: -Restore 退出图案窗口后恢复切换前的分辨率
|
||||
-Seconds <n> 图案 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
# Kingst VIS 压缩边沿流分析: 数字波形按占空比签名自动分段, 输出行结构/占空比/消隐明细
|
||||
# 用法: python la_wave_analyzer.py <capture.csv>
|
||||
# CSV 列: Time[s], 通道A, 通道B (时间分辨率 10ns; 默认抓 B 通道 D0/D1)
|
||||
# 判读基线见 doc/architecture/rgb_bit_swap_test.md §1.1
|
||||
import csv, statistics, time, sys
|
||||
|
||||
PATH = sys.argv[1] if len(sys.argv) > 1 else \
|
||||
r"E:\Hulk-Coding\LT8619C_DLPC3421_HDMI\tools\wave\B-D0-D7.csv"
|
||||
CLK_NS = 25.0 # 40MHz 像素时钟
|
||||
|
||||
t0 = time.time()
|
||||
rows = []
|
||||
with open(PATH, encoding='utf-8-sig', errors='replace') as f:
|
||||
r = csv.reader(f)
|
||||
next(r) # header
|
||||
for row in r:
|
||||
if len(row) < 3:
|
||||
continue
|
||||
rows.append((float(row[0]) * 1e9, int(row[1]), int(row[2]))) # ns
|
||||
|
||||
n = len(rows)
|
||||
t_start, t_stop = rows[0][0], rows[-1][0] + 1000.0 # 末行状态延续按1us估
|
||||
print(f"rows={n} span={t_stop - t_start / 1e9:.9f}s")
|
||||
|
||||
# ---- 按行区间累计高低时间, 50ms 窗口计算占空比 ----
|
||||
W = 50_000_000.0 # ns
|
||||
win = {'t0': t_start, 'hi0': 0.0, 'hi1': 0.0}
|
||||
wins = []
|
||||
for i in range(n - 1):
|
||||
t, d0, d1 = rows[i]
|
||||
dt = min(rows[i + 1][0], t_stop) - t
|
||||
if dt <= 0:
|
||||
continue
|
||||
win['hi0'] += dt if d0 else 0.0
|
||||
win['hi1'] += dt if d1 else 0.0
|
||||
if rows[i + 1][0] - win['t0'] >= W:
|
||||
span = rows[i + 1][0] - win['t0']
|
||||
wins.append((win['t0'], win['hi0'] / span, win['hi1'] / span))
|
||||
win = {'t0': rows[i + 1][0], 'hi0': 0.0, 'hi1': 0.0}
|
||||
|
||||
# ---- 窗口分类: 依 (d0duty, d1duty) 签名 ----
|
||||
def classify(a, b):
|
||||
hi = lambda x: x > 0.5
|
||||
if hi(a) and hi(b):
|
||||
return 'FF'
|
||||
if hi(a) and not hi(b):
|
||||
return '55'
|
||||
if not hi(a) and hi(b):
|
||||
return 'AA'
|
||||
return '00'
|
||||
|
||||
segs = []
|
||||
for t, a, b in wins:
|
||||
c = classify(a, b)
|
||||
if segs and segs[-1][0] == c:
|
||||
segs[-1][1].append((t, a, b))
|
||||
else:
|
||||
segs.append((c, [(t, a, b)]))
|
||||
|
||||
print(f"\n{'段':<4}{'签名':<6}{'时长(s)':<10}{'D0占空':<10}{'D1占空':<10}")
|
||||
for i, (c, ws) in enumerate(segs):
|
||||
dur = (ws[-1][0] - ws[0][0]) / 1e9
|
||||
a = statistics.mean(x[1] for x in ws)
|
||||
b = statistics.mean(x[2] for x in ws)
|
||||
print(f"{i:<6}{c:<8}{dur:<12.2f}{a:<12.1%}{b:<12.1%}")
|
||||
|
||||
# ---- 每段: 行周期 + 有效区宽度(长高电平段) ----
|
||||
print(f"\n按段结构分析 (时钟数 @40MHz):")
|
||||
for i, (c, ws) in enumerate(segs):
|
||||
seg_t0 = ws[0][0] - W / 2
|
||||
seg_t1 = ws[-1][0] + W / 2
|
||||
ch = 0 if c in ('FF', '55') else 1 # 选有效区为高的通道
|
||||
# 找长高电平段(>=15us)及其起点
|
||||
starts, widths = [], []
|
||||
for j in range(n - 1):
|
||||
t, d0, d1 = rows[j]
|
||||
if t < seg_t0 or t > seg_t1:
|
||||
continue
|
||||
lvl = d0 if ch == 0 else d1
|
||||
if lvl and rows[j + 1][0] - t >= 15_000.0:
|
||||
starts.append(t)
|
||||
widths.append(rows[j + 1][0] - t)
|
||||
if len(starts) < 3:
|
||||
print(f" 段{i} [{c}]: 线段不足({len(starts)}), 跳过")
|
||||
continue
|
||||
periods = [b - a for a, b in zip(starts, starts[1:]) if 20_000 < b - a < 35_000]
|
||||
mp = statistics.median(periods)
|
||||
mw = statistics.median(widths)
|
||||
print(f" 段{i} [{c}]: 行数~{len(starts)} 行周期={mp:.0f}ns={mp/CLK_NS:.1f}clk "
|
||||
f"有效高={mw:.0f}ns={mw/CLK_NS:.1f}clk 消隐={(mp-mw)/CLK_NS:.1f}clk")
|
||||
|
||||
# ---- 每段抽一行: 消隐区的边沿明细 ----
|
||||
print(f"\n消隐区边沿明细(每段第一行, ns 相对行起点, clk=时钟数):")
|
||||
for i, (c, ws) in enumerate(segs):
|
||||
seg_t0 = ws[0][0] - W / 2
|
||||
ch = 0 if c in ('FF', '55') else 1
|
||||
line_start = None
|
||||
for j in range(n - 1):
|
||||
t, d0, d1 = rows[j]
|
||||
if t < seg_t0:
|
||||
continue
|
||||
lvl = d0 if ch == 0 else d1
|
||||
if lvl and rows[j + 1][0] - t >= 15_000.0:
|
||||
line_start = t
|
||||
break
|
||||
if line_start is None:
|
||||
continue
|
||||
print(f" 段{i} [{c}] 行起点+有效区之后:")
|
||||
j0 = next(k for k in range(n - 1) if rows[k][0] >= line_start)
|
||||
out = []
|
||||
for k in range(j0, min(j0 + 30, n - 1)):
|
||||
t, d0, d1 = rows[k]
|
||||
dt = rows[k + 1][0] - t
|
||||
rel = t - line_start
|
||||
out.append(f" +{rel/1000:8.2f}us D0={d0} D1={d1} 持续{dt/CLK_NS:6.1f}clk")
|
||||
if rel > 30_000:
|
||||
break
|
||||
print('\n'.join(out[:18]))
|
||||
|
||||
print(f"\n耗时 {time.time()-t0:.1f}s")
|
||||
@@ -0,0 +1,74 @@
|
||||
# Kingst VIS 3 通道边沿流分段: 按占空比签名分段, 输出各通道占空比
|
||||
# 用法: python la_wave_analyzer3.py <capture.csv> (列: Time[s], ch8, ch9, ch10)
|
||||
import csv, time, statistics
|
||||
|
||||
import sys
|
||||
PATH = sys.argv[1] if len(sys.argv) else r"E:\Hulk-Coding\LT8619C_DLPC3421_HDMI\tools\wave\A1_RGB-SWAP.csv"
|
||||
|
||||
t0 = time.time()
|
||||
rows = []
|
||||
with open(PATH, encoding='utf-8-sig', errors='replace') as f:
|
||||
r = csv.reader(f)
|
||||
hdr = next(r)
|
||||
print('header:', hdr)
|
||||
for row in r:
|
||||
if len(row) < 4:
|
||||
continue
|
||||
rows.append((float(row[0]) * 1e9, int(row[1]), int(row[2]), int(row[3])))
|
||||
n = len(rows)
|
||||
t_last = rows[-1][0] + 1000.0
|
||||
print('rows=%d span=%.2fs' % (n, (t_last - rows[0][0]) / 1e9))
|
||||
|
||||
W = 50_000_000.0
|
||||
wins = []
|
||||
acc = {'t0': rows[0][0], 'hi': [0.0, 0.0, 0.0]}
|
||||
for i in range(n - 1):
|
||||
t, a, b, c = rows[i]
|
||||
dt = min(rows[i + 1][0], t_last) - t
|
||||
if dt <= 0:
|
||||
continue
|
||||
acc['hi'][0] += dt if a else 0.0
|
||||
acc['hi'][1] += dt if b else 0.0
|
||||
acc['hi'][2] += dt if c else 0.0
|
||||
if rows[i + 1][0] - acc['t0'] >= W:
|
||||
span = rows[i + 1][0] - acc['t0']
|
||||
wins.append((acc['t0'], tuple(x / span for x in acc['hi'])))
|
||||
acc = {'t0': rows[i + 1][0], 'hi': [0.0, 0.0, 0.0]}
|
||||
|
||||
|
||||
def sig(d):
|
||||
hi = [x > 0.5 for x in d]
|
||||
return ('D7' if hi[0] else '--') + '/' + ('D15' if hi[1] else '--') + '/' + ('D23' if hi[2] else '--')
|
||||
|
||||
|
||||
segs = []
|
||||
for t, d in wins:
|
||||
s = sig(d)
|
||||
if segs and segs[-1][0] == s:
|
||||
segs[-1][1].append((t, d))
|
||||
else:
|
||||
segs.append((s, [(t, d)]))
|
||||
|
||||
print('\n%-16s %10s %9s %9s %9s' % ('活动组签名', '时长(s)', 'D7占空', 'D15占空', 'D23占空'))
|
||||
for s, ws in segs:
|
||||
dur = ws[-1][0] - ws[0][0]
|
||||
m = [statistics.mean(x[1][k] for x in ws) for k in range(3)]
|
||||
print('%-16s %10.2f %8.1f%% %8.1f%% %8.1f%%' % (s, dur, m[0] * 100, m[1] * 100, m[2] * 100))
|
||||
|
||||
# 每段行结构抽查(第一段一次即可)
|
||||
s0, ws0 = segs[0]
|
||||
ch = 0
|
||||
starts = []
|
||||
seg_t0 = ws0[0][0] - W / 2
|
||||
for j in range(n - 1):
|
||||
t = rows[j][0]
|
||||
if t < seg_t0:
|
||||
continue
|
||||
if rows[j][1] and rows[j + 1][0] - t >= 15000.0:
|
||||
starts.append(t)
|
||||
if len(starts) >= 5 or t > seg_t0 + 0.3e9:
|
||||
break
|
||||
if len(starts) >= 2:
|
||||
p = statistics.median([b - a for a, b in zip(starts, starts[1:])])
|
||||
print('\n行周期抽查: %.0fns (%.1f clk @40MHz)' % (p, p / 25.0))
|
||||
print('耗时 %.1fs' % (time.time() - t0))
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<rect width="512" height="512" fill="#000000"/>
|
||||
<rect x="38" y="64" width="436" height="186" rx="34" fill="#FF9900"/>
|
||||
<text x="256" y="210" font-family="Arial, 'Microsoft YaHei', sans-serif" font-size="146" font-weight="900" fill="#000000" text-anchor="middle">HDMI</text>
|
||||
<text x="256" y="421" font-family="Arial, 'Microsoft YaHei', sans-serif" font-size="146" font-weight="900" fill="#FFFFFF" text-anchor="middle">Tool</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 506 B |
@@ -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}')
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
组装 HDMI-Tool 发布包: exe + 附属资产 → 一个 zip(解压即用)
|
||||
================================================================
|
||||
发布内容(zip 解压后根目录为 HDMI-Tool/, 与 exe 运行目录结构一致):
|
||||
HDMI-Tool.exe 主程序(先运行 build_exe.py 打包)
|
||||
pattern_slots.json 预置自定义栏位(指向前 3 张 bw 图案)
|
||||
bw_*.png 640x360 黑白测试图 ×5(自定义栏位用)
|
||||
solid_*.png 800x600 纯色测试图 ×10(RGB 位交换测试用)
|
||||
README.md 使用说明
|
||||
|
||||
用法:
|
||||
python package_release.py 版本号自动取最近 git tag(HDMI-Tool-v*)
|
||||
python package_release.py 0.1.0 手动指定版本号
|
||||
产物: release/HDMI-Tool-v<版本>-win64.zip(zip 不入库, 上传到 Gitea Release)
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
SRC = os.path.dirname(os.path.abspath(__file__))
|
||||
BASE = os.path.dirname(SRC)
|
||||
RELEASE = os.path.join(BASE, 'release')
|
||||
ASSETS = os.path.join(BASE, 'assets')
|
||||
|
||||
PATTERN_GLOBS = ('bw_*.png', 'solid_*.png')
|
||||
|
||||
|
||||
def detect_version():
|
||||
"""从最近的 HDMI-Tool-v* tag 推断版本号, 无 tag 则报错提示手动指定"""
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
['git', 'describe', '--tags', '--abbrev=0', '--match', 'HDMI-Tool-v*'],
|
||||
cwd=BASE, stderr=subprocess.DEVNULL)
|
||||
return out.decode().strip().removeprefix('HDMI-Tool-v')
|
||||
except Exception:
|
||||
sys.exit('未能从 git tag 推断版本号, 请手动指定: python package_release.py <版本号>')
|
||||
|
||||
|
||||
def main():
|
||||
version = (sys.argv[1] if len(sys.argv) > 1 else detect_version()).lstrip('v')
|
||||
exe = os.path.join(RELEASE, 'HDMI-Tool.exe')
|
||||
slots = os.path.join(RELEASE, 'pattern_slots.json')
|
||||
readme = os.path.join(BASE, 'README.md')
|
||||
if not os.path.exists(exe):
|
||||
sys.exit('缺少 release/HDMI-Tool.exe, 先运行: python build_exe.py')
|
||||
for must in (slots, readme):
|
||||
if not os.path.exists(must):
|
||||
sys.exit(f'缺少 {must}')
|
||||
|
||||
# 依 pattern_slots.json 校验栏位引用的图片都在 assets/ 里
|
||||
slot_files = re.findall(r'"file":\s*"([^"]+)"', open(slots, encoding='utf-8').read())
|
||||
for f in slot_files:
|
||||
if not os.path.exists(os.path.join(ASSETS, f)):
|
||||
sys.exit(f'pattern_slots.json 引用的 {f} 不在 assets/ 中')
|
||||
|
||||
patterns = sorted(p for g in PATTERN_GLOBS
|
||||
for p in os.listdir(ASSETS)
|
||||
if re.fullmatch(g.replace('*', '.*'), p))
|
||||
zip_name = f'HDMI-Tool-v{version}-win64.zip'
|
||||
zip_path = os.path.join(RELEASE, zip_name)
|
||||
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as z:
|
||||
for src, arc in ([(exe, 'HDMI-Tool.exe'), (slots, 'pattern_slots.json'),
|
||||
(readme, 'README.md')]
|
||||
+ [(os.path.join(ASSETS, p), p) for p in patterns]):
|
||||
z.write(src, f'HDMI-Tool/{arc}')
|
||||
print(f'发布包: {zip_path} ({os.path.getsize(zip_path) / 1024 / 1024:.1f} MB)')
|
||||
print('内容: HDMI-Tool.exe, pattern_slots.json, README.md +', f'{len(patterns)} 张图案')
|
||||
print('下一步上传: python publish_gitea.py', zip_name)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
创建/更新 Gitea Release 并上传发布 zip(纯标准库, 无需安装依赖)
|
||||
================================================================
|
||||
用法:
|
||||
python publish_gitea.py HDMI-Tool-v0.1.0-win64.zip
|
||||
python publish_gitea.py <zip> --notes-file release/notes-v0.1.0.md
|
||||
python publish_gitea.py <zip> --tag HDMI-Tool-v0.1.0 --replace
|
||||
|
||||
行为:
|
||||
- tag 与版本默认取 zip 文件名(HDMI-Tool-vX.Y.Z-win64.zip → HDMI-Tool-vX.Y.Z)
|
||||
- release 不存在则创建(tag 不存在时 Gitea 在默认分支头自动建 tag);
|
||||
已存在则复用, 仅上传/更新资产
|
||||
- release 上已有同名资产时: 默认跳过, --replace 删旧再传
|
||||
- API token: --token 参数或环境变量 GITEA_TOKEN(需该仓库写权限)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
DEFAULT_NOTES = """## HDMI-Tool {version}
|
||||
|
||||
HDMI 源端调试工具(LT8619C → DLPC3421 转接板配套): EDID 查看/逐字节解析、
|
||||
分辨率切换、纯色与测试图案全屏投屏。
|
||||
|
||||
### 安装
|
||||
下载 `HDMI-Tool-v{version}-win64.zip`, 解压后双击 `HDMI-Tool.exe`(Windows 10/11 x64, 免安装)。
|
||||
|
||||
### 包内容
|
||||
- `HDMI-Tool.exe` — 主程序
|
||||
- `pattern_slots.json` — 预置自定义栏位(指向 bw_*.png)
|
||||
- `bw_*.png` ×5 / `solid_*.png` ×10 — 测试图案
|
||||
- `README.md` — 完整使用说明
|
||||
"""
|
||||
|
||||
|
||||
def git_remote_info():
|
||||
"""从 origin 远程地址解析 API base 与 owner/repo(ssh 端口不影响 https API)"""
|
||||
url = subprocess.check_output(['git', 'remote', 'get-url', 'origin']).decode().strip()
|
||||
m = re.match(r'(?:ssh://git@|git@)([\w.-]+)(?::\d+)?[:/]([\w.-]+)/([\w.-]+?)(?:\.git)?$', url)
|
||||
if not m:
|
||||
sys.exit(f'无法从 origin 解析 Gitea 地址: {url}')
|
||||
host, owner, repo = m.groups()
|
||||
return f'https://{host}', owner, repo
|
||||
|
||||
|
||||
class Api:
|
||||
def __init__(self, base, owner, repo, token, insecure):
|
||||
self.api = f'{base}/api/v1/repos/{owner}/{repo}'
|
||||
self.headers = {'Authorization': f'token {token}'}
|
||||
self.ctx = ssl._create_unverified_context() if insecure else None
|
||||
|
||||
def request(self, method, path, data=None, headers=None):
|
||||
req = urllib.request.Request(self.api + path, data=data, method=method,
|
||||
headers={**self.headers, **(headers or {})})
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=self.ctx) as resp:
|
||||
body = resp.read()
|
||||
return resp.status, json.loads(body) if body else None
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, json.loads(e.read() or b'{}')
|
||||
|
||||
def get_json(self, path):
|
||||
status, data = self.request('GET', path)
|
||||
return data if status == 200 else None
|
||||
|
||||
def upload_asset(self, release_id, filepath, replace):
|
||||
name = os.path.basename(filepath)
|
||||
status, rel = self.request('GET', f'/releases/{release_id}')
|
||||
if status != 200:
|
||||
sys.exit(f'读取 release {release_id} 失败: {status} {rel}')
|
||||
for asset in (rel.get('assets') or {}).get('items', []):
|
||||
if asset['name'] == name:
|
||||
if not replace:
|
||||
print(f'资产 {name} 已存在, 跳过(覆盖请加 --replace)')
|
||||
return
|
||||
self.request('DELETE', f"/releases/{release_id}/assets/{asset['id']}")
|
||||
print(f'已删除旧资产 {name}')
|
||||
boundary = uuid.uuid4().hex
|
||||
payload = open(filepath, 'rb').read()
|
||||
body = (f'--{boundary}\r\n'
|
||||
f'Content-Disposition: form-data; name="attachment"; filename="{name}"\r\n'
|
||||
f'Content-Type: application/octet-stream\r\n\r\n').encode() + payload + \
|
||||
f'\r\n--{boundary}--\r\n'.encode()
|
||||
status, data = self.request(
|
||||
'POST', f'/releases/{release_id}/assets?name={name}', body,
|
||||
{'Content-Type': f'multipart/form-data; boundary={boundary}'})
|
||||
if status != 201:
|
||||
sys.exit(f'上传资产失败: {status} {data}')
|
||||
print(f'已上传资产: {name} ({len(payload) / 1024 / 1024:.1f} MB)')
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('zipfile_path', help='发布 zip(release/ 下)')
|
||||
ap.add_argument('--tag', help='release 标签, 默认取 zip 文件名去掉 -win64.zip')
|
||||
ap.add_argument('--notes-file', help='release 说明 markdown 文件, 默认用内置模板')
|
||||
ap.add_argument('--token', default=os.environ.get('GITEA_TOKEN'), help='API token')
|
||||
ap.add_argument('--replace', action='store_true', help='覆盖已存在的同名资产')
|
||||
ap.add_argument('--insecure', action='store_true', help='跳过 TLS 证书校验(自签证书)')
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(args.zipfile_path):
|
||||
sys.exit(f'找不到 {args.zipfile_path}')
|
||||
if not args.token:
|
||||
sys.exit('缺少 API token: 传 --token 或设环境变量 GITEA_TOKEN')
|
||||
|
||||
tag = args.tag or re.sub(r'-win64\.zip$', '', os.path.basename(args.zipfile_path))
|
||||
version = tag.removeprefix('HDMI-Tool-v')
|
||||
notes = open(args.notes_file, encoding='utf-8').read() if args.notes_file \
|
||||
else DEFAULT_NOTES.format(version=version)
|
||||
|
||||
base, owner, repo = git_remote_info()
|
||||
api = Api(base, owner, repo, args.token, args.insecure)
|
||||
print(f'目标: {base}/{owner}/{repo} tag={tag}')
|
||||
|
||||
rel = api.get_json(f'/releases/tags/{tag}')
|
||||
if rel:
|
||||
print(f'release 已存在(id={rel["id"]}), 复用')
|
||||
else:
|
||||
status, rel = api.request('POST', '/releases', json.dumps({
|
||||
'tag_name': tag,
|
||||
'name': f'HDMI-Tool v{version}',
|
||||
'body': notes,
|
||||
}).encode(), {'Content-Type': 'application/json'})
|
||||
if status != 201:
|
||||
sys.exit(f'创建 release 失败: {status} {rel}')
|
||||
print(f'release 已创建(id={rel["id"]})')
|
||||
api.upload_asset(rel['id'], args.zipfile_path, args.replace)
|
||||
print(f'完成: {rel.get("html_url", "") or f"{base}/{owner}/{repo}/releases/tag/{tag}"}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,70 @@
|
||||
# I2C-Inject — LT8619C 寄存器择机注入工具
|
||||
|
||||
> 更新:2026-09-03。由两部分组成:**RP2040 注入器固件**(`mcu-rp2040/`,刷入
|
||||
> BPI-Pico-RP2040,MicroPython)+ **PC 端 GUI**(`i2c_inject_gui.py` / 打包版
|
||||
> `I2C-Inject.exe`)。可在 LT8619C demo 板的 I2C 总线上安全地在线读写寄存器。
|
||||
> 固件细节、刷写步骤与串口命令详见 [mcu-rp2040/README.md](mcu-rp2040/README.md)。
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
- 固件分两层:MicroPython 运行时 UF2 + 注入器应用脚本(上传为板上 `main.py` 自启动)
|
||||
——完整刷写步骤、串口命令表、工作机制见 [mcu-rp2040/README.md](mcu-rp2040/README.md)
|
||||
- 固件内置 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` 重新打包) |
|
||||
| `mcu-rp2040/` | RP2040 总线端:注入器固件脚本 + MicroPython 运行时 UF2 + 刷写说明(README) |
|
||||
| `logos/` | 图标源文件与 `make_ico_i2c.py`(改 logo 后重跑再打包) |
|
||||
|
||||
## 6. 故障排查
|
||||
|
||||
- 写入回显 `OK 读回=xx` 但稍后读回变了 → demo 板把它刷回了(见 §4 稳态提醒),先确认 HDMI 已稳定出图
|
||||
- 读寄存器 NAK/失败 → 检查三根接线与共地;用 `s` 命令看边沿计数是否在增长(增长=监听正常,总线有活动)
|
||||
- 串口列表空白 → 换 USB 口/检查设备管理器(应出现 `USB 串行设备`,VID_2E8A)
|
||||
@@ -0,0 +1,101 @@
|
||||
# 0x606E 位交换自动化测试 — 经 RP2040 注入器
|
||||
# ============================================================
|
||||
# 流程: 读基线 -> 写各交换位并校验 -> 读回确认 -> 收尾还原
|
||||
# 注意: LA1010 波形对照按 doc/architecture/rgb_bit_swap_test.md §1.1 手工比对;
|
||||
# 本脚本负责寄存器侧的全部操作与校验, 打印每步期望的 LA 现象。
|
||||
# 用法: python bitswap_auto.py [COM口]
|
||||
# python bitswap_auto.py COM9 full 额外跑 G 通道(0x40)与 R 通道(0x20)
|
||||
|
||||
import serial, sys, time
|
||||
|
||||
PORT = sys.argv[1] if len(sys.argv) > 1 else 'COM9'
|
||||
FULL = len(sys.argv) > 2 and sys.argv[2].lower() == 'full'
|
||||
|
||||
BANK, REG = '60', '6E'
|
||||
STEPS = [ # (写值, 含义, 写入后 HDMI-Tool 应投的图, LA 预期变化 vs 基线)
|
||||
('00', '关闭交换(基准)', 'solid_0000AA / solid_000055',
|
||||
'D0:AA=13%/55=84%, D1:AA=72%/55=0.9% (基线)'),
|
||||
('80', 'B通道镜像', 'solid_0000AA',
|
||||
'与基线互换: D0 应变 ~84%, D1 应变 ~0.9%'),
|
||||
]
|
||||
if FULL:
|
||||
STEPS[1] = ('20', 'R通道镜像(6ch挂D23/D16时测)', 'solid_AA0000',
|
||||
'D23/D16 占空比互换')
|
||||
STEPS.append(('40', 'G通道镜像(6ch挂D15/D8时测)', 'solid_00AA00',
|
||||
'D15/D8 占空比互换'))
|
||||
STEPS.append(('80', 'B通道镜像', 'solid_0000AA',
|
||||
'D0/D1 与基线互换'))
|
||||
|
||||
|
||||
def xact(ser, cmd, wait=3.0):
|
||||
"""发命令收完整回显(注入含重试可能耗时数秒)"""
|
||||
ser.reset_input_buffer()
|
||||
ser.write((cmd + '\n').encode())
|
||||
lines = []
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < wait:
|
||||
ln = ser.readline().decode('utf-8', 'replace').strip()
|
||||
if ln:
|
||||
lines.append(ln)
|
||||
if ln.startswith('=') and ('读回' in ln or ':' in ln):
|
||||
break
|
||||
return lines
|
||||
|
||||
|
||||
def check(lines, expect_val):
|
||||
"""从回显提取 读回=XX 并与期望比较"""
|
||||
for ln in lines:
|
||||
if '读回=' in ln:
|
||||
rb = ln.split('读回=')[1].strip()
|
||||
ok = rb.upper() == expect_val.upper()
|
||||
return ok, rb
|
||||
return False, None
|
||||
|
||||
|
||||
ser = serial.Serial(PORT, timeout=1)
|
||||
time.sleep(0.5)
|
||||
print('== 0x606E 位交换自动化 == 端口 %s' % PORT)
|
||||
print(xact(ser, 's')[0])
|
||||
|
||||
log = []
|
||||
fails = 0
|
||||
try:
|
||||
# 0) 通信预检
|
||||
lines = xact(ser, 'r %s %s' % (BANK, REG))
|
||||
ok, rb = check(lines, '00')
|
||||
print('[预检] 读 0x%s%s -> %s (期望默认 00)' % (BANK, REG, rb))
|
||||
if not ok:
|
||||
print(' !! 预检值不是 00 —— 若刚做过实验未还原请先还原; 继续执行')
|
||||
|
||||
for val, meaning, img, expect in STEPS:
|
||||
print('\n--- 写 %s (%s) ---' % (val, meaning))
|
||||
lines = xact(ser, 'w %s %s %s' % (BANK, REG, val), wait=6.0)
|
||||
for ln in lines:
|
||||
print(' ' + ln)
|
||||
ok, rb = check(lines, val)
|
||||
if not ok:
|
||||
fails += 1
|
||||
print(' !! 校验失败(读回 %s != %s), 跳过此步' % (rb, val))
|
||||
log.append((val, 'FAIL', rb))
|
||||
continue
|
||||
log.append((val, 'OK', rb))
|
||||
print(' [寄存器侧 OK] 读回=%s' % rb)
|
||||
print(' [现在请投] %s' % img)
|
||||
print(' [LA 预期] %s' % expect)
|
||||
if val != '00':
|
||||
input(' >>> 抓完波形按回车继续...')
|
||||
|
||||
# 收尾还原
|
||||
print('\n--- 收尾: 写 00 还原 ---')
|
||||
lines = xact(ser, 'w %s %s 00' % (BANK, REG), wait=6.0)
|
||||
ok, rb = check(lines, '00')
|
||||
print(' 读回=%s %s' % (rb, 'OK' if ok else 'FAIL'))
|
||||
if not ok:
|
||||
fails += 1
|
||||
finally:
|
||||
ser.close()
|
||||
|
||||
print('\n== 结果汇总 ==')
|
||||
for val, st, rb in log:
|
||||
print(' 写%s: %s (读回 %s)' % (val, st, rb))
|
||||
print('失败步骤: %d' % fails)
|
||||
@@ -0,0 +1,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()
|
||||
@@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
I2C-Inject — LT8619C 寄存器择机注入工具
|
||||
================================================
|
||||
配合 RP2040 注入器固件(mcu-rp2040/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)
|
||||
@@ -0,0 +1,117 @@
|
||||
# RP2040 注入器嗅探数据 -> I2C 协议解码
|
||||
# 用法: python i2c_sniff_decode.py [COM口] [采集ms, 默认700]
|
||||
# 帧格式: 固件 'c' 命令回吐 "C <t_us> <val>" 行, val bit0=SDA bit1=SCL
|
||||
import serial, sys, time
|
||||
|
||||
PORT = sys.argv[1] if len(sys.argv) > 1 else 'COM9'
|
||||
MS = int(sys.argv[2]) if len(sys.argv) > 2 else 700
|
||||
|
||||
KNOWN_REGS = {0x44: 'SYS_STATUS', 0x6E: 'TTL_8B_CFG', 0xFF: 'BANK'}
|
||||
|
||||
s = serial.Serial(PORT, timeout=1)
|
||||
time.sleep(0.3)
|
||||
s.reset_input_buffer()
|
||||
s.write(b's\n')
|
||||
time.sleep(0.3)
|
||||
print(s.read(200).decode('utf-8', 'replace').strip())
|
||||
s.write(('c %d\n' % MS).encode())
|
||||
|
||||
edges = []
|
||||
t_deadline = time.time() + MS / 1000 + 60
|
||||
while time.time() < t_deadline:
|
||||
ln = s.readline().decode('ascii', 'replace').strip()
|
||||
if ln == 'C.':
|
||||
break
|
||||
if ln.startswith('C '):
|
||||
try:
|
||||
a, b = ln[2:].split()
|
||||
edges.append((int(a), int(b)))
|
||||
except ValueError:
|
||||
pass
|
||||
elif ln.startswith('='):
|
||||
print(ln)
|
||||
s.close()
|
||||
print('edges:', len(edges))
|
||||
|
||||
# ---------------- I2C 解码 ----------------
|
||||
trans, cur, bits = [], None, []
|
||||
psda = pscl = None
|
||||
|
||||
def flush_byte():
|
||||
global bits
|
||||
if cur is not None and len(bits) == 8:
|
||||
cur['bytes'].append(int(''.join(str(x) for x in bits), 2))
|
||||
bits = []
|
||||
|
||||
for t, v in edges:
|
||||
sda, scl = v & 1, (v >> 1) & 1
|
||||
if pscl is not None:
|
||||
if scl == 1 and pscl == 1: # SCL 高电平期间 SDA 变化
|
||||
if psda == 1 and sda == 0: # START / repeated START
|
||||
if cur is not None:
|
||||
cur['t1'] = t
|
||||
cur['inc'] = bool(bits)
|
||||
trans.append(cur)
|
||||
cur = {'t0': t, 'bytes': [], 'acks': [], 'inc': False}
|
||||
bits = []
|
||||
elif psda == 0 and sda == 1: # STOP
|
||||
if cur is not None:
|
||||
cur['t1'] = t
|
||||
cur['inc'] = bool(bits)
|
||||
trans.append(cur)
|
||||
cur = None
|
||||
bits = []
|
||||
elif scl == 1 and pscl == 0: # SCL 上升沿采样
|
||||
if cur is not None:
|
||||
bits.append(sda)
|
||||
if len(bits) == 8:
|
||||
cur['bytes'].append(int(''.join(str(x) for x in bits), 2))
|
||||
bits = []
|
||||
elif len(bits) == 9:
|
||||
cur['acks'].append(sda) # 0=ACK
|
||||
bits = []
|
||||
psda, pscl = sda, scl
|
||||
if cur is not None:
|
||||
cur['t1'] = edges[-1][0] if edges else 0
|
||||
cur['inc'] = bool(bits)
|
||||
trans.append(cur)
|
||||
|
||||
# ---------------- 打印 ----------------
|
||||
bank = None
|
||||
print(f'\n解码出 {len(trans)} 个事务:')
|
||||
for k, tr in enumerate(trans):
|
||||
d = tr['bytes']
|
||||
if not d:
|
||||
print(' [%2d] t=%dus <空/片段>' % (k, tr['t0']))
|
||||
continue
|
||||
addr7, rw = d[0] >> 1, 'R' if d[0] & 1 else 'W'
|
||||
acks = tr['acks']
|
||||
body = d[1:]
|
||||
ann = []
|
||||
if addr7 == 0x32 and rw == 'W' and body:
|
||||
if body[0] == 0xFF and len(body) >= 2:
|
||||
bank = body[1]
|
||||
ann.append('bank=%02X' % bank)
|
||||
body = body[2:]
|
||||
if body:
|
||||
r = body[0]
|
||||
note = KNOWN_REGS.get(r, '')
|
||||
if r == 0xFF:
|
||||
bank = body[1] if len(body) > 1 else bank
|
||||
note = 'bank=%02X' % bank if len(body) > 1 else note
|
||||
ann.append(('[%02X%02X] %s' % (bank or 0, r, note)) if note else '[%02X?%02X]' % (bank or 0, r))
|
||||
ackstr = ''.join('A' if a == 0 else 'N' for a in acks[1:]) if len(acks) > 1 else ''
|
||||
flag = ' <未收尾>' if tr.get('inc') else ''
|
||||
print(' [%2d] t=%7dus 0x%02X %s: %-30s ACK:%s%s'
|
||||
% (k, tr['t0'], addr7, rw,
|
||||
' '.join('%02X' % x for x in d[1:12]) + (' …' if len(d) > 12 else ''),
|
||||
ackstr[:16], flag))
|
||||
|
||||
# 轮询周期统计(相邻事务起始间隔的主值)
|
||||
if len(trans) > 3:
|
||||
gaps = [b['t0'] - a['t0'] for a, b in zip(trans, trans[1:])]
|
||||
gaps = [g for g in gaps if 1000 < g < 5000000]
|
||||
if gaps:
|
||||
gaps.sort()
|
||||
print('\n相邻事务间隔(us): 中位=%d 最小=%d 最大=%d'
|
||||
% (gaps[len(gaps) // 2], gaps[0], gaps[-1]))
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<rect width="512" height="512" fill="#000000"/>
|
||||
<rect x="38" y="64" width="436" height="186" rx="34" fill="#FF9900"/>
|
||||
<text x="256" y="210" font-family="Arial, 'Microsoft YaHei', sans-serif" font-size="146" font-weight="900" fill="#000000" text-anchor="middle">I2C</text>
|
||||
<text x="256" y="421" font-family="Arial, 'Microsoft YaHei', sans-serif" font-size="146" font-weight="900" fill="#FFFFFF" text-anchor="middle">Inject</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 507 B |
@@ -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}')
|
||||
@@ -0,0 +1,79 @@
|
||||
# mcu-rp2040 — RP2040 注入器固件
|
||||
|
||||
本目录存放 **I2C-Inject 的总线端**:跑在 BPI-Pico-RP2040(RP2040 + MicroPython)上的
|
||||
注入器应用脚本,以及配套的 MicroPython 运行时 UF2。
|
||||
|
||||
> PC 端 GUI 见上级目录 [../README.md](../README.md)。两者通过 USB 虚拟串口通信。
|
||||
|
||||
## 1. 文件说明
|
||||
|
||||
| 文件 | 用途 |
|
||||
|---|---|
|
||||
| `rp2040_inject.py` | 注入器应用(MicroPython 脚本)。上传为板上 `main.py`,上电自启动 |
|
||||
| `RPI_PICO-20260824-v1.29.0.uf2` | MicroPython 运行时(标准 RP2040 构建,v1.29.0,2026-08-24)。首次使用或升级时刷 |
|
||||
|
||||
⚠️ UF2 必须是 **RPI_PICO** 标准构建(RP2040 芯片);`PICO_W`(带 WiFi 芯片)与
|
||||
`RPI_PICO2`(RP2350 芯片)变体不可用。
|
||||
|
||||
## 2. 硬件与接线
|
||||
|
||||
| BPI-Pico-RP2040 | 接 LT8619C 总线 | 说明 |
|
||||
|---|---|---|
|
||||
| GP8(物理脚 11) | SDA | 平时高阻输入只听,注入时才驱动 |
|
||||
| GP9(物理脚 12) | SCL | 同上 |
|
||||
| GND | GND | 必须共地 |
|
||||
|
||||
- **不接 VCC/3V3**(各喂各的电)、**不外加任何上拉电阻**(总线用 demo 板自带上拉)
|
||||
- 动手前确认总线上拉电平为 3.3V(RP2040 不耐 5V)
|
||||
|
||||
## 3. 刷写步骤
|
||||
|
||||
```text
|
||||
① 刷 MicroPython 运行时(首次/升级):
|
||||
按住板上 BOOTSEL 键插入 USB → 出现 RPI-RP2 U 盘
|
||||
→ 把 RPI_PICO-20260824-v1.29.0.uf2 拖入 → 自动重启
|
||||
⚠ 刷 UF2 可能重建板载文件系统, 之后需重传 ② 的 main.py
|
||||
|
||||
② 上传注入器应用(每次刷 UF2 后, 或应用更新后):
|
||||
py -m pip install mpremote
|
||||
py -m mpremote connect COMx cp rp2040_inject.py :main.py
|
||||
py -m mpremote connect COMx reset # 复位, main.py 自启动
|
||||
|
||||
③ 验证: 串口终端(任意波特率)应看到
|
||||
LT8619C 注入器就绪 GP8=SDA GP9=SCL 100kHz addr 0x32
|
||||
```
|
||||
|
||||
**LED 状态**:上电快闪 3 次 = 自检;**常亮 = 总线监听中**。
|
||||
|
||||
## 4. 串口命令(USB CDC,十六进制参数,一行一令)
|
||||
|
||||
| 命令 | 功能 | 回显示例 |
|
||||
|---|---|---|
|
||||
| `w <bank> <reg> <val>` | 择机写 + 读回校验 | `= w 60 6E 80 : OK 读回=80` |
|
||||
| `r <bank> <reg>` | 择机读 | `= r 60 6E : OK 00` |
|
||||
| `s` | 总线边沿计数(接好线后在涨 = 监听正常) | `= 边沿计数 2958` |
|
||||
| `c [ms]` | 嗅探捕获(默认 700ms),输出边沿时间戳供离线解码 I2C | `= capture 1207 edges / 700 ms` + `C` 行 |
|
||||
| `?` | 帮助 | |
|
||||
|
||||
- 回显行首:`=` 正常结果 / `!` 告警(校验不符、总线错误、静置超时)
|
||||
- 读回不符(被 demo 板 MCU 插队)会自动整组重试,最多 3 次
|
||||
|
||||
## 5. 工作机制(择机注入)
|
||||
|
||||
```
|
||||
监听 SDA/SCL 边沿(双沿中断) → 等下一轮总线活动出现
|
||||
→ 活动结束后总线静置满 50ms → 立即发射:
|
||||
START→[bank 写]→START→[reg/数据 写]→RESTART→[读回]→STOP
|
||||
→ 成功/失败(重试) → 立即释放总线回监听
|
||||
```
|
||||
|
||||
- demo 板 MCU 以 ~500ms 周期轮询,注入序列 <2ms,落在周期间空窗内冲突概率趋近零
|
||||
- 嗅探捕获采用 viper 裸指针读 GPIO(~0.27µs/次 ≈ 3MHz 采样),字节码路径仅 ~12µs/次(会混叠)作兜底
|
||||
|
||||
## 6. 已知实测行为(demo 板)
|
||||
|
||||
- 0x606E:bit5(0x20) 镜像 D[7:0](蓝值组)立即生效;板子非稳态时 0.3~0.5s 内会被其固件刷回 0x00
|
||||
→ **先投图等稳态再写,写完立即读回确认**
|
||||
- 0x606D:000/001/010=BGR、011=BRG、100=GBR、101=GRB、110=RBG、111=RGB(字母串=
|
||||
D[23:16]/D[15:8]/D[7:0] 分量顺序),切换实时生效,不影响 HDMI 锁定
|
||||
- 过程数据与完整规程见上游主仓库 `doc/architecture/rgb_bit_swap_test.md`
|
||||
@@ -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 <bank> <reg> <val> 择机写 + 读回校验, 例: w 60 6E 80 (B通道镜像)
|
||||
# r <bank> <reg> 择机读, 例: 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()
|
||||
@@ -1,3 +1,52 @@
|
||||
# LT8619C_Debug_Tools
|
||||
|
||||
本仓库用存储LT8619C调试过程中需要的一些小工具(包括AI生成)
|
||||
LT8619C(HDMI→RGB888)→ DLPC3421 投影转接板调试配套上位机工具集。
|
||||
上游主仓库(固件):[LT8619C_DLPC3421_HDMI](https://gitea.hulk.wang/hulk/LT8619C_DLPC3421_HDMI)。
|
||||
|
||||
## 工具一览
|
||||
|
||||
| 工具 | 用途 | 获取 |
|
||||
|---|---|---|
|
||||
| [HDMI-Tool](HDMI-Tool/) | HDMI 源端:EDID 查看/逐字节解析、分辨率切换、纯色与测试图案全屏投屏 | 到 [Releases](https://gitea.hulk.wang/hulk/LT8619C_Debug_Tools/releases) 下 `HDMI-Tool-vX.Y.Z-win64.zip`,解压即用;详见 [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) |
|
||||
|
||||
## 环境要求
|
||||
|
||||
- 发布版 zip 开箱即用(Windows 10/11 x64,无需安装 Python)
|
||||
- 源码运行:Python ≥3.10;HDMI-Tool 为纯标准库(自定义图片 JPG/BMP 需可选 Pillow);
|
||||
I2C-Inject 需 `py -m pip install pyserial`
|
||||
- 重新打包:`HDMI-Tool/src/build_exe.py`(自动安装 PyInstaller);I2C-Inject 目录内 `python build_exe.py`
|
||||
|
||||
## 目录结构
|
||||
|
||||
每个工具内统一三区:**src/ 源代码(人写)** → **assets/ 生成资产(可再生成)** →
|
||||
**release/ 打包与发布产物(机器生成)**;README.md 面向使用者,AGENTS.md 面向 AI/Agent(工程约定与构建规程)。
|
||||
|
||||
```
|
||||
LT8619C_Debug_Tools/
|
||||
├── HDMI-Tool/
|
||||
│ ├── src/ # 源码 + 打包/发布脚本 + 图标源
|
||||
│ ├── assets/ # 生成的测试图案 PNG(gen_*.py 产物)
|
||||
│ ├── release/ # 打包 exe + 运行配置(入库);发布 zip(不入库)
|
||||
│ ├── README.md # 使用说明
|
||||
│ └── AGENTS.md # AI/Agent 工程档案
|
||||
├── I2C-Inject/ # PC 端 GUI + RP2040 固件 + 打包 exe + 图标 + README
|
||||
├── LICENSE
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 发布流程(HDMI-Tool)
|
||||
|
||||
```powershell
|
||||
cd HDMI-Tool/src
|
||||
python build_exe.py # 打包 → ../release/HDMI-Tool.exe
|
||||
python package_release.py <版本> # → ../release/HDMI-Tool-v<版本>-win64.zip
|
||||
python publish_gitea.py HDMI-Tool-v<版本>-win64.zip # 上传 Gitea Release(需 GITEA_TOKEN)
|
||||
```
|
||||
|
||||
## 备注
|
||||
|
||||
- 源码与打包产物(exe、pattern_slots.json)均入库;发布 zip 不入库,只上传 Gitea Release
|
||||
- 两件工具的寄存器语义(0x606E 位交换 / 0x606D 整组顺序)均经 demo 板逻辑分析仪
|
||||
双色交叉实测,过程数据与规程存于上游主仓库 `doc/architecture/`
|
||||
- 更新:2026-09-10
|
||||
|
||||