2026-09-16 完成、当时未提交,这里补提交(原样,不含 09-27 上板的修改)。 - 器件层 / 拓扑层整体从 R1 换到 R2:TCA9548A 分通道、五路同址 INA226 0x40 + 输入侧 0x43、 刀片 PCA9555(VBUS_EN 挪到 P0.4)、CH224A 只读驱动、风扇闭环 t_thermal - 新增 i2cbus(mux 原子事务)、slot(刀片 I/O 门面)、host/qtest.sh - 74HC595 链退役(代码与测试保留) - 判据阈值与时间常量未动;19 个 ctest 全绿,idf.py build 通过 Co-Authored-By: Claude Opus 5.5 <[email protected]>
221 行
7.8 KiB
C
221 行
7.8 KiB
C
/* 按 datasheet 建模的假 PCA9555 ×5(R2:每片刀片一颗)。宿主机专用。
|
|
|
|
通用寄存器表(sim_i2c)能测「写下去的字节对不对」,但测不出这一层真正要拦的
|
|
两件事:
|
|
① **把一个焊死在地上的脚配成输出**(P0.5~7 / P1.4~7)——
|
|
真板子上这既不报错也不 NAK,只是发烫;
|
|
② **在输出锁存器还是 0xFF 的时候就把 Port0 配成输出** ——
|
|
那一刻三个键全被按下 + EDL 短接 + VBUS 上电(pca9555.h 那个框)。
|
|
② 在真板上的现象是「五台手机一起进了 Recovery」,而 I²C 从头到尾一切正常。
|
|
|
|
⚠️ 这个模型是用来证明**代码逻辑**对的。别拿它推翻硬件设计的任何一条。 */
|
|
#include <assert.h>
|
|
#include <string.h>
|
|
|
|
#include "pca9555.h"
|
|
#include "sim_hal.h"
|
|
|
|
/* 焊死在地上的备用脚(`_blade_pcb_nets.json` 的 `U4` pin9~11 / pin17~20)。 */
|
|
static const uint8_t s_gnd_mask[2] = { PCA9555_P0_GND_MASK, PCA9555_P1_GND_MASK };
|
|
|
|
typedef struct {
|
|
bool attached;
|
|
int ch; /* 挂在 mux 的哪个通道后面 */
|
|
uint8_t addr;
|
|
uint8_t ext[2]; /* 外部世界驱动的电平;开漏上拉,静息 0xFF */
|
|
uint8_t int_latch[2];/* 上一次「被读走」时的输入值,用来判 INT# */
|
|
bool int_low;
|
|
uint32_t int_asserts;
|
|
} sim_pca_t;
|
|
|
|
static sim_pca_t s[SIM_PCA9555_MAX];
|
|
static uint32_t s_gnd_driven; /* 五片合计 */
|
|
static uint32_t s_shorted;
|
|
static uint32_t s_unsafe_enable;
|
|
|
|
void sim_pca9555_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
|
|
|
void sim_pca9555_reset_internal(void)
|
|
{
|
|
memset(s, 0, sizeof(s));
|
|
s_gnd_driven = 0;
|
|
s_shorted = 0;
|
|
s_unsafe_enable = 0;
|
|
}
|
|
|
|
static sim_pca_t *inst(int idx)
|
|
{
|
|
assert(idx >= 0 && idx < SIM_PCA9555_MAX);
|
|
assert(s[idx].attached); /* 先 sim_pca9555_attach_ch() */
|
|
return &s[idx];
|
|
}
|
|
|
|
/* 从 dev 反查是哪一片:地址相同,靠通道区分(真板上也是这么区分的)。 */
|
|
static sim_pca_t *from_dev(const sim_i2c_dev_t *d)
|
|
{
|
|
for (int i = 0; i < SIM_PCA9555_MAX; i++) {
|
|
if (s[i].attached && s[i].addr == d->addr && s[i].ch == d->bus) {
|
|
return &s[i];
|
|
}
|
|
}
|
|
assert(0 && "hook 挂在了一个没登记的假 PCA9555 上");
|
|
return &s[0];
|
|
}
|
|
|
|
/* 输入寄存器 = 逐位选择:配成输入的位取外部电平,配成输出的位取自己的输出锁存器。
|
|
接地脚无论配成什么,线上都是 0(它焊在地上,赢的永远是铜)。 */
|
|
static uint8_t input_value(const sim_i2c_dev_t *d, const sim_pca_t *p, int port)
|
|
{
|
|
const uint8_t cfg = (uint8_t)d->regs[PCA9555_REG_CONFIG0 + port];
|
|
const uint8_t out = (uint8_t)d->regs[PCA9555_REG_OUTPUT0 + port];
|
|
const uint8_t pol = (uint8_t)d->regs[PCA9555_REG_POLINV0 + port];
|
|
|
|
uint8_t v = (uint8_t)((p->ext[port] & cfg) | (out & (uint8_t)~cfg));
|
|
v &= (uint8_t)~s_gnd_mask[port]; /* 接地脚读回来永远是 0 */
|
|
return (uint8_t)(v ^ pol); /* 极性反转寄存器作用在**输入**上 */
|
|
}
|
|
|
|
/* 方向寄存器一变就查两件事(见文件头 ① ②)。 */
|
|
static void check_dangerous(const sim_i2c_dev_t *d)
|
|
{
|
|
for (int port = 0; port < 2; port++) {
|
|
const uint8_t cfg = (uint8_t)d->regs[PCA9555_REG_CONFIG0 + port];
|
|
const uint8_t out = (uint8_t)d->regs[PCA9555_REG_OUTPUT0 + port];
|
|
const uint8_t driven = (uint8_t)(s_gnd_mask[port] & (uint8_t)~cfg); /* cfg 0 = 输出 */
|
|
if (driven != 0) {
|
|
s_gnd_driven++;
|
|
if ((driven & out) != 0) {
|
|
s_shorted++; /* 真板子上这一刻开始冒烟 */
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ② Port0 上被配成输出、而锁存器里是 1 的位:那一刻它就开始输出高电平。
|
|
本板 Port0 的五个输出(三个键 / EDL / VBUS)全都是**高有效**,
|
|
所以这等于「键被按下 + VBUS 上电」。 */
|
|
const uint8_t cfg0 = (uint8_t)d->regs[PCA9555_REG_CONFIG0];
|
|
const uint8_t out0 = (uint8_t)d->regs[PCA9555_REG_OUTPUT0];
|
|
const uint8_t as_output = (uint8_t)(PCA9555_P0_OUT_MASK & (uint8_t)~cfg0);
|
|
if ((as_output & out0) != 0) {
|
|
s_unsafe_enable++;
|
|
}
|
|
}
|
|
|
|
/* INT#:任何输入位相对「上次被读走的那一帧」发生变化就拉低,读走输入端口才释放。 */
|
|
static void update_int(const sim_i2c_dev_t *d, sim_pca_t *p)
|
|
{
|
|
for (int port = 0; port < 2; port++) {
|
|
if (input_value(d, p, port) != p->int_latch[port]) {
|
|
if (!p->int_low) {
|
|
p->int_low = true;
|
|
p->int_asserts++;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void hook(sim_i2c_dev_t *d, sim_i2c_ev_t ev)
|
|
{
|
|
sim_pca_t *p = from_dev(d);
|
|
|
|
if (ev == SIM_I2C_EV_POST_WRITE) {
|
|
check_dangerous(d);
|
|
update_int(d, p);
|
|
return;
|
|
}
|
|
|
|
/* PRE_READ:把两个输入寄存器合成好再让总线取走。 */
|
|
for (int port = 0; port < 2; port++) {
|
|
d->regs[PCA9555_REG_INPUT0 + port] = input_value(d, p, port);
|
|
}
|
|
/* 读输入端口 = 确认中断。读别的寄存器不清 INT#(真器件就是这个行为,
|
|
也是「只靠中断会永远收不到下一个沿」的根源)。 */
|
|
if (d->ptr == PCA9555_REG_INPUT0 || d->ptr == PCA9555_REG_INPUT1) {
|
|
p->int_latch[0] = (uint8_t)d->regs[PCA9555_REG_INPUT0];
|
|
p->int_latch[1] = (uint8_t)d->regs[PCA9555_REG_INPUT1];
|
|
p->int_low = false;
|
|
}
|
|
}
|
|
|
|
void sim_pca9555_attach_ch(int idx, int ch, uint8_t addr)
|
|
{
|
|
assert(idx >= 0 && idx < SIM_PCA9555_MAX);
|
|
assert(!s[idx].attached);
|
|
sim_i2c_dev_t *d = sim_i2c_add_ch(ch, addr, 1);
|
|
|
|
sim_pca_t *p = &s[idx];
|
|
p->attached = true;
|
|
p->ch = ch;
|
|
p->addr = addr;
|
|
p->ext[0] = 0xFF; /* 开漏上拉,静息是高 */
|
|
p->ext[1] = 0xFF;
|
|
|
|
/* 上电复位值(datasheet 表 6):方向全输入 0xFF,**输出锁存器 0xFF**,极性 0。
|
|
输出的 0xFF 是本器件最阴的一条:配成输出的那一刻就输出高电平。 */
|
|
d->regs[PCA9555_REG_CONFIG0] = 0xFF;
|
|
d->regs[PCA9555_REG_CONFIG1] = 0xFF;
|
|
d->regs[PCA9555_REG_OUTPUT0] = 0xFF;
|
|
d->regs[PCA9555_REG_OUTPUT1] = 0xFF;
|
|
d->regs[PCA9555_REG_POLINV0] = 0x00;
|
|
d->regs[PCA9555_REG_POLINV1] = 0x00;
|
|
|
|
p->int_latch[0] = input_value(d, p, 0);
|
|
p->int_latch[1] = input_value(d, p, 1);
|
|
d->hook = hook;
|
|
}
|
|
|
|
static sim_i2c_dev_t *dev_of(int idx)
|
|
{
|
|
sim_pca_t *p = inst(idx);
|
|
sim_i2c_dev_t *d = sim_i2c_dev_ch(p->ch, p->addr);
|
|
assert(d != NULL);
|
|
return d;
|
|
}
|
|
|
|
void sim_pca9555_drive(int idx, int port, uint8_t level)
|
|
{
|
|
assert(port == 0 || port == 1);
|
|
sim_pca_t *p = inst(idx);
|
|
p->ext[port] = level;
|
|
update_int(dev_of(idx), p);
|
|
}
|
|
|
|
uint8_t sim_pca9555_config(int idx, int port)
|
|
{
|
|
assert(port == 0 || port == 1);
|
|
return (uint8_t)dev_of(idx)->regs[PCA9555_REG_CONFIG0 + port];
|
|
}
|
|
|
|
uint8_t sim_pca9555_output(int idx, int port)
|
|
{
|
|
assert(port == 0 || port == 1);
|
|
return (uint8_t)dev_of(idx)->regs[PCA9555_REG_OUTPUT0 + port];
|
|
}
|
|
|
|
uint8_t sim_pca9555_polinv(int idx, int port)
|
|
{
|
|
assert(port == 0 || port == 1);
|
|
return (uint8_t)dev_of(idx)->regs[PCA9555_REG_POLINV0 + port];
|
|
}
|
|
|
|
int sim_pca9555_pin(int idx, int port, int bit)
|
|
{
|
|
assert(port == 0 || port == 1);
|
|
assert(bit >= 0 && bit < 8);
|
|
const sim_i2c_dev_t *d = dev_of(idx);
|
|
const uint8_t cfg = (uint8_t)d->regs[PCA9555_REG_CONFIG0 + port];
|
|
const uint8_t out = (uint8_t)d->regs[PCA9555_REG_OUTPUT0 + port];
|
|
|
|
if ((cfg & (uint8_t)(1u << bit)) != 0) {
|
|
return -1; /* 配成输入 = 高阻,不是 0 */
|
|
}
|
|
return (out & (uint8_t)(1u << bit)) != 0 ? 1 : 0;
|
|
}
|
|
|
|
uint32_t sim_pca9555_gnd_driven(void) { return s_gnd_driven; }
|
|
uint32_t sim_pca9555_shorted(void) { return s_shorted; }
|
|
uint32_t sim_pca9555_unsafe_enable(void) { return s_unsafe_enable; }
|
|
bool sim_pca9555_int_asserted(int idx) { return inst(idx)->int_low; }
|
|
uint32_t sim_pca9555_int_asserts(int idx) { return inst(idx)->int_asserts; }
|