/* 按 datasheet 建模的假 PCA9555。宿主机专用。 通用寄存器表(sim_i2c)能测「写下去的字节对不对」,但测不出 A6 真正要拦的那件事: **把一个焊死在地上的脚配成输出**。真板子上这既不报错也不 NAK,只是发烫。 所以这里把三件真器件会做的事补上(见 sim_hal.h 那一节)。 ⚠️ 这个模型是用来证明**代码逻辑**对的。别拿它推翻设计文档 §17.2 的任何一条。 */ #include #include #include "pca9555.h" #include "sim_hal.h" /* 焊死在地上的备用脚(设计文档 §17.2 的注)。 */ static const uint8_t s_gnd_mask[2] = { PCA9555_P0_GND_MASK, PCA9555_P1_GND_MASK }; typedef struct { bool attached; uint8_t addr; uint8_t ext[2]; /* 外部世界驱动的电平;开漏上拉,静息 0xFF */ uint8_t int_latch[2];/* 上一次「被读走」时的输入值,用来判 INT# */ bool int_low; uint32_t int_asserts; uint32_t gnd_driven; uint32_t shorted; } sim_pca9555_t; static sim_pca9555_t s; void sim_pca9555_reset_internal(void); /* 只给 sim_hal_reset() 用 */ void sim_pca9555_reset_internal(void) { memset(&s, 0, sizeof(s)); } /* 输入寄存器 = 逐位选择:配成输入的位取外部电平,配成输出的位取自己的输出锁存器。 接地脚无论配成什么,线上都是 0(它焊在地上,赢的永远是铜)。 */ static uint8_t input_value(const sim_i2c_dev_t *d, 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)((s.ext[port] & cfg) | (out & (uint8_t)~cfg)); v &= (uint8_t)~s_gnd_mask[port]; /* 接地脚读回来永远是 0 */ return (uint8_t)(v ^ pol); /* 极性反转寄存器作用在**输入**上 */ } /* 方向寄存器一变就查一次:有没有把接地脚配成输出,输出的又是不是高电平。 */ static void check_grounded_pins(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) { continue; } s.gnd_driven++; if ((driven & out) != 0) { s.shorted++; /* 真板子上这一刻开始冒烟 */ } } } /* INT#:任何输入位相对「上次被读走的那一帧」发生变化就拉低,读走输入端口才释放。 */ static void update_int(const sim_i2c_dev_t *d) { for (int port = 0; port < 2; port++) { if (input_value(d, port) != s.int_latch[port]) { if (!s.int_low) { s.int_low = true; s.int_asserts++; } return; } } } static void hook(sim_i2c_dev_t *d, sim_i2c_ev_t ev) { if (ev == SIM_I2C_EV_POST_WRITE) { check_grounded_pins(d); update_int(d); return; } /* PRE_READ:把两个输入寄存器合成好再让总线取走。 */ for (int port = 0; port < 2; port++) { d->regs[PCA9555_REG_INPUT0 + port] = input_value(d, port); } /* 读输入端口 = 确认中断。读别的寄存器不清 INT#(真器件就是这个行为, 也是「只靠中断会永远收不到下一个沿」的根源)。 */ if (d->ptr == PCA9555_REG_INPUT0 || d->ptr == PCA9555_REG_INPUT1) { s.int_latch[0] = (uint8_t)d->regs[PCA9555_REG_INPUT0]; s.int_latch[1] = (uint8_t)d->regs[PCA9555_REG_INPUT1]; s.int_low = false; } } void sim_pca9555_attach(uint8_t addr) { assert(!s.attached); /* 板上只有一片 */ sim_i2c_dev_t *d = sim_i2c_add(addr, 1); s.attached = true; s.addr = addr; s.ext[0] = 0xFF; /* FLT# / PGOOD 都是开漏上拉,静息是高 */ s.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; s.int_latch[0] = input_value(d, 0); s.int_latch[1] = input_value(d, 1); d->hook = hook; } static sim_i2c_dev_t *must_dev(void) { assert(s.attached); /* 先 sim_pca9555_attach() */ sim_i2c_dev_t *d = sim_i2c_dev(s.addr); assert(d != NULL); return d; } void sim_pca9555_drive(int port, uint8_t level) { assert(port == 0 || port == 1); s.ext[port] = level; update_int(must_dev()); } uint8_t sim_pca9555_config(int port) { assert(port == 0 || port == 1); return (uint8_t)must_dev()->regs[PCA9555_REG_CONFIG0 + port]; } uint8_t sim_pca9555_output(int port) { assert(port == 0 || port == 1); return (uint8_t)must_dev()->regs[PCA9555_REG_OUTPUT0 + port]; } uint8_t sim_pca9555_polinv(int port) { assert(port == 0 || port == 1); return (uint8_t)must_dev()->regs[PCA9555_REG_POLINV0 + port]; } uint32_t sim_pca9555_gnd_driven(void) { return s.gnd_driven; } uint32_t sim_pca9555_shorted(void) { return s.shorted; } bool sim_pca9555_int_asserted(void) { return s.int_low; } uint32_t sim_pca9555_int_asserts(void) { return s.int_asserts; }