feat(I2C-Inject): 归档 RP2040 固件到 mcu-rp2040 并补齐说明
This commit is contained in:
@@ -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`
|
||||
Binary file not shown.
@@ -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