feat: HDMI-Tool 与 I2C-Inject 首版工具集
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
# I2C-Inject — LT8619C 寄存器择机注入工具
|
||||
|
||||
> 更新:2026-09-03。由两部分组成:**RP2040 注入器固件**(`rp2040_inject.py`,刷入
|
||||
> BPI-Pico-RP2040,MicroPython)+ **PC 端 GUI**(`i2c_inject_gui.py` / 打包版
|
||||
> `I2C-Inject.exe`)。可在 LT8619C demo 板的 I2C 总线上安全地在线读写寄存器。
|
||||
|
||||
---
|
||||
|
||||
## 1. 工作原理(为什么不会和 demo 板冲突)
|
||||
|
||||
demo 板 MCU 每 ~500ms 轮询一次 LT8619C。注入器的时序:
|
||||
|
||||
```
|
||||
监听总线边沿 -> 等下一轮活动结束 -> 总线静置 50ms -> 发射
|
||||
(切 bank -> 写 -> 读回校验), 失败自动重试 3 次 -> 立即释放总线回监听
|
||||
```
|
||||
|
||||
静置窗口落在两次轮询之间的 ~490ms 空窗内,单次注入序列 <2ms,冲突概率趋近零;
|
||||
即使撞上也表现为读回不符,自动重试覆盖。板载 LED:**常亮 = 监听中**,上电快闪 3 次 = 自检。
|
||||
|
||||
## 2. 硬件准备
|
||||
|
||||
| RP2040 板 | 接 LT8619C 总线 |
|
||||
|---|---|
|
||||
| GP8(物理脚 11) | SDA |
|
||||
| GP9(物理脚 12) | SCL |
|
||||
| GND | GND |
|
||||
|
||||
- **不接 VCC、不外加干扰**:总线由 demo 板自带上拉(先确认上拉为 3.3V)
|
||||
- 固件刷写:板子插 USB → 已有 MicroPython 则直接
|
||||
`py -m pip install mpremote && py -m mpremote connect COMx cp rp2040_inject.py :main.py`
|
||||
→ 复位后自启动(刷新 MicroPython UF2 会清掉 main.py,重传一次即可)
|
||||
- 固件内置 viper 编译的总线嗅探(`c [ms]` 命令,~3MHz 采样),串口回吐边沿数据可离线解码 I2C
|
||||
|
||||
## 3. GUI 使用
|
||||
|
||||
双击 `I2C-Inject.exe`(或 `python i2c_inject_gui.py [COMx]`):
|
||||
|
||||
1. **第一排左侧**:串口下拉 →"刷新"扫描 → 选中注入器端口 →"连接"(日志出现"已连接")
|
||||
2. **第一排右侧**:输入 Bank/寄存器/值(十六进制)→ **写入**(自动读回校验)/**读取**
|
||||
3. **0x606E 快捷块**(位交换,语义为 demo 板 LA 实测):
|
||||
- `00`=关交换;`20`=镜像 D[7:0](载蓝色分量的组,实测立即生效)
|
||||
- `40`/`80`=另外两组(demo 板未单独实测,自板点亮时验证)
|
||||
4. **0x606D 快捷块**(RGB 整组顺序,蓝+红双色实测=手册 naive 读法):
|
||||
- `00 BGR / 03 BRG / 04 GBR / 05 GRB / 06 RBG / 07 RGB`
|
||||
- 字母串顺序 = D[23:16]/D[15:8]/D[7:0] 携带的分量;切换实时生效,不影响 HDMI 锁定
|
||||
5. 回显格式:`=` 正常结果(绿色,含"读回=xx"校验值)、`!` 告警/失败(橙色)
|
||||
6. 收尾:把改过的寄存器写回原值(0x606E 原值 00;0x606D 原值 07)
|
||||
|
||||
## 4. 已实测的寄存器事实速查(demo 板)
|
||||
|
||||
| 寄存器 | 实测结论 |
|
||||
|---|---|
|
||||
| 0x606E | bit5(0x20)=镜像 D[7:0](蓝值组),立即生效、含消隐码;bit7(0x80) 对该组无效;稳态下写入可持续(实测 6.7s+),板子非稳态时 0.3~0.5s 内被刷回 00 → **先投图等稳态再写,写完立即读回确认** |
|
||||
| 0x606D | 000/001/010=BGR、011=BRG、100=GBR、101=GRB、110=RBG、111=RGB(字母串=D[23:16]/D[15:8]/D[7:0]),切换实时生效;demo 板 init 值 07 |
|
||||
|
||||
## 5. 目录内容
|
||||
|
||||
| 文件 | 用途 |
|
||||
|---|---|
|
||||
| `I2C-Inject.exe` | 打包好的单文件 PC 端 GUI(需 pyserial,已打包在内) |
|
||||
| `i2c_inject_gui.py` | PC 端 GUI 源码(`python build_exe.py` 重新打包) |
|
||||
| `rp2040_inject.py` | RP2040 注入器固件(MicroPython,刷成 main.py 自启动) |
|
||||
| `logos/` | 图标源文件与 `make_ico_i2c.py`(改 logo 后重跑再打包) |
|
||||
|
||||
## 6. 故障排查
|
||||
|
||||
- 写入回显 `OK 读回=xx` 但稍后读回变了 → demo 板把它刷回了(见 §4 稳态提醒),先确认 HDMI 已稳定出图
|
||||
- 读寄存器 NAK/失败 → 检查三根接线与共地;用 `s` 命令看边沿计数是否在增长(增长=监听正常,总线有活动)
|
||||
- 串口列表空白 → 换 USB 口/检查设备管理器(应出现 `USB 串行设备`,VID_2E8A)
|
||||
@@ -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 注入器固件(tools/rp2040_inject.py, 刷入 BPI-Pico-RP2040)使用,
|
||||
在 LT8619C demo 板的 I2C 总线上择机读写寄存器。
|
||||
|
||||
特性:
|
||||
- 串口选择/连接 与 寄存器操作 并列第一排, 手动输入 bank/寄存器/值 即刻注入
|
||||
- 择机机制: 等总线一轮活动结束 -> 总线静置 50ms -> 发射(切bank->写->读回校验),
|
||||
失败自动重试 3 次, 完成后立即释放总线回监听
|
||||
- 0x606E(位交换)/0x606D(整组顺序) 快捷按钮, 全部为 demo 板 LA 实测确认的语义
|
||||
|
||||
依赖: pyserial(py -m pip install pyserial) + tkinter 标准库
|
||||
用法: python i2c_inject_gui.py (也可带默认串口参数: python i2c_inject_gui.py COM9)
|
||||
打包: python build_exe.py (生成 I2C-Inject.exe)
|
||||
"""
|
||||
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import queue
|
||||
import time
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(sys.executable))
|
||||
else:
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def get_icon_path():
|
||||
"""工具图标: 源码模式取 logos/ 下, 打包模式取 exe 内嵌资源(_MEIPASS)"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
p = os.path.join(getattr(sys, '_MEIPASS', ''), 'logo_i2c_orange.ico')
|
||||
else:
|
||||
p = os.path.join(BASE_DIR, 'logos', 'logo_i2c_orange.ico')
|
||||
return p if os.path.exists(p) else None
|
||||
|
||||
|
||||
class Injector:
|
||||
"""串口工作线程: 目标端口动态切换; 命令队列 -> RP2040, 回显 -> 消息队列"""
|
||||
|
||||
def __init__(self, on_line, on_status):
|
||||
self.q = queue.Queue()
|
||||
self.on_line, self.on_status = on_line, on_status
|
||||
self._target = None # 想要连接的端口名(None=断开)
|
||||
self.ser = None
|
||||
self._lock = threading.Lock()
|
||||
threading.Thread(target=self._run, daemon=True).start()
|
||||
|
||||
def connect(self, port):
|
||||
with self._lock:
|
||||
self._target = port
|
||||
|
||||
def disconnect(self):
|
||||
with self._lock:
|
||||
self._target = None
|
||||
|
||||
def send(self, cmd):
|
||||
with self._lock:
|
||||
if self.ser is None:
|
||||
self.on_status('未连接串口, 命令被丢弃: ' + cmd)
|
||||
return
|
||||
self.q.put(cmd)
|
||||
|
||||
def is_connected(self):
|
||||
with self._lock:
|
||||
return self.ser is not None
|
||||
|
||||
def _run(self):
|
||||
buf = b''
|
||||
while True:
|
||||
with self._lock:
|
||||
target = self._target
|
||||
if target != (self.ser.port if self.ser else None):
|
||||
if self.ser is not None:
|
||||
try:
|
||||
self.ser.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.ser = None
|
||||
self.on_status('已断开')
|
||||
if target:
|
||||
try:
|
||||
self.ser = serial.Serial(target, timeout=0.2)
|
||||
buf = b''
|
||||
time.sleep(0.3) # 等 Pico 复位横幅打完
|
||||
self.ser.reset_input_buffer()
|
||||
self.on_status('已连接 ' + target)
|
||||
except Exception as e:
|
||||
self.ser = None
|
||||
with self._lock:
|
||||
self._target = None
|
||||
self.on_status('连接 %s 失败: %s' % (target, e))
|
||||
if self.ser is not None:
|
||||
try:
|
||||
cmd = None
|
||||
try:
|
||||
cmd = self.q.get_nowait()
|
||||
except queue.Empty:
|
||||
pass
|
||||
if cmd is not None:
|
||||
self.ser.write((cmd + '\n').encode())
|
||||
buf += self.ser.read(256)
|
||||
while b'\n' in buf:
|
||||
ln, buf = buf.split(b'\n', 1)
|
||||
txt = ln.decode('utf-8', 'replace').strip()
|
||||
if txt:
|
||||
self.on_line(txt)
|
||||
except OSError:
|
||||
self.ser = None
|
||||
with self._lock:
|
||||
self._target = None
|
||||
self.on_status('串口丢失(设备拔出?), 请重新连接')
|
||||
time.sleep(0.02)
|
||||
|
||||
|
||||
def run_gui(default_port=None):
|
||||
root = tk.Tk()
|
||||
root.title('I2C-Inject — LT8619C 寄存器择机注入器 (静置50ms)')
|
||||
root.geometry('860x520')
|
||||
ic = get_icon_path()
|
||||
if ic:
|
||||
try:
|
||||
root.iconbitmap(ic) # 窗口标题栏图标
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
log_q = queue.Queue()
|
||||
|
||||
# ============ 第一排: 串口选择 + 寄存器操作(并列) ============
|
||||
row1 = ttk.LabelFrame(root, text='连接与寄存器操作 (十六进制)', padding=(8, 4))
|
||||
row1.pack(fill='x', padx=8, pady=(8, 4))
|
||||
|
||||
# --- 左半: 串口选择 ---
|
||||
ttk.Label(row1, text='串口:').grid(row=0, column=0)
|
||||
cbo_port = ttk.Combobox(row1, state='readonly', width=8, values=[])
|
||||
cbo_port.grid(row=0, column=1, padx=(2, 6))
|
||||
|
||||
inj = Injector(on_line=lambda s: log_q.put((s, 'rx')),
|
||||
on_status=lambda s: log_q.put((s, 'st')))
|
||||
|
||||
def refresh_ports():
|
||||
ports = [p.device for p in serial.tools.list_ports.comports()]
|
||||
cbo_port['values'] = ports
|
||||
if ports:
|
||||
cbo_port.current(0)
|
||||
else:
|
||||
cbo_port.set('')
|
||||
inj.on_status('串口列表: %s' % (', '.join(ports) if ports else '无'))
|
||||
|
||||
def toggle_connect():
|
||||
if inj.is_connected():
|
||||
inj.disconnect()
|
||||
btn_conn.config(text='连接')
|
||||
else:
|
||||
port = cbo_port.get().strip()
|
||||
if not port:
|
||||
messagebox.showerror('未选择串口', '请先选择串口(或点刷新扫描)')
|
||||
return
|
||||
inj.connect(port)
|
||||
btn_conn.config(text='断开')
|
||||
|
||||
ttk.Button(row1, text='刷新', width=6, command=refresh_ports).grid(row=0, column=2, padx=(0, 4))
|
||||
btn_conn = ttk.Button(row1, text='连接', width=6, command=toggle_connect)
|
||||
btn_conn.grid(row=0, column=3, padx=(0, 14))
|
||||
|
||||
# --- 右半: 寄存器操作 ---
|
||||
ttk.Label(row1, text='Bank:').grid(row=0, column=4)
|
||||
e_bank = ttk.Entry(row1, width=5); e_bank.insert(0, '60'); e_bank.grid(row=0, column=5, padx=(2, 8))
|
||||
ttk.Label(row1, text='寄存器:').grid(row=0, column=6)
|
||||
e_reg = ttk.Entry(row1, width=5); e_reg.insert(0, '6E'); e_reg.grid(row=0, column=7, padx=(2, 8))
|
||||
ttk.Label(row1, text='值:').grid(row=0, column=8)
|
||||
e_val = ttk.Entry(row1, width=5); e_val.insert(0, '00'); e_val.grid(row=0, column=9, padx=(2, 8))
|
||||
|
||||
def _hex_ok(s):
|
||||
try:
|
||||
return 0 <= int(s, 16) <= 0xFF
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def do_write():
|
||||
b, r, v = e_bank.get().strip(), e_reg.get().strip(), e_val.get().strip()
|
||||
if not (_hex_ok(b) and _hex_ok(r) and _hex_ok(v)):
|
||||
messagebox.showerror('输入错误', 'bank/寄存器/值 都必须是 00-FF 的十六进制')
|
||||
return
|
||||
inj.send('w %s %s %s' % (b, r, v))
|
||||
|
||||
def do_read():
|
||||
b, r = e_bank.get().strip(), e_reg.get().strip()
|
||||
if not (_hex_ok(b) and _hex_ok(r)):
|
||||
messagebox.showerror('输入错误', 'bank/寄存器 必须是 00-FF 的十六进制')
|
||||
return
|
||||
inj.send('r %s %s' % (b, r))
|
||||
|
||||
ttk.Button(row1, text='写入', width=8, command=do_write).grid(row=0, column=10, padx=2)
|
||||
ttk.Button(row1, text='读取', width=8, command=do_read).grid(row=0, column=11, padx=2)
|
||||
ttk.Label(row1, text='写=择机写入后自动读回校验; 失败自动重试3次',
|
||||
foreground='#666666').grid(row=1, column=0, columnspan=12,
|
||||
sticky='w', pady=(4, 0))
|
||||
|
||||
# ============ 0x606E 快捷块(位交换) ============
|
||||
qk = ttk.LabelFrame(root, text='0x606E 快捷操作 (位交换)', padding=(8, 4))
|
||||
qk.pack(fill='x', padx=8, pady=4)
|
||||
for i, v in enumerate(('00', '20', '40', '80')):
|
||||
ttk.Button(qk, text='写 ' + v, width=8,
|
||||
command=lambda v=v: (e_val.delete(0, 'end'), e_val.insert(0, v), do_write())
|
||||
).grid(row=0, column=i, padx=4)
|
||||
ttk.Button(qk, text='读 0x606E', width=10,
|
||||
command=do_read).grid(row=0, column=5, padx=4)
|
||||
ttk.Label(qk, text='00=关交换 20=镜像D[7:0](LA实测) 40/80=另外两组(待自板单测)',
|
||||
foreground='#666666').grid(row=1, column=0, columnspan=7,
|
||||
sticky='w', pady=(3, 0))
|
||||
|
||||
# ============ 0x606D 快捷块(整组顺序) ============
|
||||
qk2 = ttk.LabelFrame(root, text='0x606D 快捷操作 (RGB 整组顺序, 双色实测=手册naive读法)',
|
||||
padding=(8, 4))
|
||||
qk2.pack(fill='x', padx=8, pady=4)
|
||||
for i, (code, name) in enumerate((('00', 'BGR'), ('03', 'BRG'), ('04', 'GBR'),
|
||||
('05', 'GRB'), ('06', 'RBG'), ('07', 'RGB'))):
|
||||
ttk.Button(qk2, text='%s %s' % (code, name), width=8,
|
||||
command=lambda c=code: inj.send('w 60 6D ' + c)
|
||||
).grid(row=0, column=i, padx=3)
|
||||
ttk.Button(qk2, text='读 0x606D', width=10,
|
||||
command=lambda: inj.send('r 60 6D')).grid(row=0, column=6, padx=3)
|
||||
ttk.Label(qk2, text='字母串=D[23:16]/D[15:8]/D[7:0] 的分量顺序; 切换实时生效, 不影响HDMI锁定',
|
||||
foreground='#666666').grid(row=1, column=0, columnspan=7,
|
||||
sticky='w', pady=(3, 0))
|
||||
|
||||
# ============ 底部: 日志 ============
|
||||
frm = ttk.LabelFrame(root, text='串口日志', padding=(6, 4))
|
||||
frm.pack(fill='both', expand=True, padx=8, pady=(4, 8))
|
||||
txt_log = tk.Text(frm, height=10, font=('Consolas', 9), state='disabled',
|
||||
background='#101010', foreground='#C8C8C8')
|
||||
txt_log.pack(side='left', fill='both', expand=True)
|
||||
sb = ttk.Scrollbar(frm, orient='vertical', command=txt_log.yview)
|
||||
txt_log.configure(yscrollcommand=sb.set)
|
||||
sb.pack(side='right', fill='y')
|
||||
for tag, color in (('rx', '#C8C8C8'), ('st', '#88AAFF'), ('ok', '#66CC66'),
|
||||
('warn', '#E0A030')):
|
||||
txt_log.tag_configure(tag, foreground=color)
|
||||
|
||||
def poll_log():
|
||||
try:
|
||||
while True:
|
||||
ln, tag = log_q.get_nowait()
|
||||
txt_log.configure(state='normal')
|
||||
txt_log.insert('end', ln + '\n', tag)
|
||||
if ln.startswith('='):
|
||||
hot = 'warn' if ('FAIL' in ln or '丢弃' in ln) else 'ok'
|
||||
txt_log.tag_add(hot, 'end-2l linestart', 'end-2l lineend')
|
||||
txt_log.see('end')
|
||||
txt_log.configure(state='disabled')
|
||||
except queue.Empty:
|
||||
pass
|
||||
root.after(80, poll_log)
|
||||
|
||||
refresh_ports()
|
||||
if default_port:
|
||||
cbo_port.set(default_port)
|
||||
root.after(80, poll_log)
|
||||
root.protocol('WM_DELETE_WINDOW', root.destroy)
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_gui(sys.argv[1] if len(sys.argv) > 1 else None)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
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,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()
|
||||
Reference in New Issue
Block a user