/* 按 datasheet 建模的假 PCA9555 ×5(R2:每片刀片一颗)。宿主机专用。 通用寄存器表(sim_i2c)能测「写下去的字节对不对」,但测不出这一层真正要拦的 两件事: ① **把一个焊死在地上的脚配成输出**(P0.5~7 / P1.4~7)—— 真板子上这既不报错也不 NAK,只是发烫; ② **在输出锁存器还是 0xFF 的时候就把 Port0 配成输出** —— 那一刻三个键全被按下 + EDL 短接 + VBUS 上电(pca9555.h 那个框)。 ② 在真板上的现象是「五台手机一起进了 Recovery」,而 I²C 从头到尾一切正常。 ⚠️ 这个模型是用来证明**代码逻辑**对的。别拿它推翻硬件设计的任何一条。 */ #include #include #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; }