feat: HDMI-Tool 与 I2C-Inject 首版工具集

This commit is contained in:
2026-09-03 17:24:33 +08:00
parent 6243b8c262
commit d0cefeebf3
38 changed files with 2778 additions and 1 deletions
Binary file not shown.
+70
View File
@@ -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)
+61
View File
@@ -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()
+272
View File
@@ -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

+6
View File
@@ -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

+58
View File
@@ -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}')
+192
View File
@@ -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()