A6:PCA9555 驱动 + 「接地脚永远是输入」的硬约束
U23 @ 0x20,16 个脚全部是输入:5×EFUSE_FLT# + 5×EFUSE_PGOOD + BUCK_PG + INA_ALERT + 4 个焊死在地上的备用脚(P0_7 / P1_5~7)。位表与地址从 .claude/pcb1-parts.json 的真实焊盘网络逐脚核回来,与设计文档 §17.2 一致。 驱动根本不提供「把某个脚配成输出」的接口(和 sr595.h 不提供「抬 OE#」同一条 规矩),方向字一律过 pca9555_dir_safe(),不安全直接 abort。原因是 PCA9555 的 Output Port 寄存器复位值是 0xFF —— 把接地脚配成输出的那一刻它就输出高电平, 而真器件既不报错也不 NAK,只是发烫。 三种极性(FLT# 低有效 / PGOOD 高有效 / INA_ALERT 低有效)各自只在一个函数里 出现一次;上层问「有没有故障」,不许自己去 & 位。 sim 侧新增按 datasheet 建模的假 PCA9555:Output 复位 0xFF、输入按位合成、 接地脚被驱动时记短路、INT# 只被「读输入端口」清除。为此给假 I²C 总线加了一个 窃听钩子(POST_WRITE / PRE_READ)—— 通用寄存器表模拟不了「配成输出就对地 短路」这类事,以后再有这种器件照这个挂。 验收:8 条 ctest(81 用例)全绿;idf.py build 通过,pca9555.c.obj 在。 12 个「一定会被写出来的错」逐个注进去跑过,全部变红。 Co-Authored-By: Claude Opus 5 <[email protected]>
这个提交包含在:
@@ -111,6 +111,9 @@ static hal_err_t do_write(sim_i2c_dev_t *d, const uint8_t *data, size_t len)
|
||||
}
|
||||
d->regs[d->ptr++] = v;
|
||||
}
|
||||
if (d->hook != NULL) {
|
||||
d->hook(d, SIM_I2C_EV_POST_WRITE);
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
@@ -123,6 +126,9 @@ static hal_err_t do_read(sim_i2c_dev_t *d, uint8_t *data, size_t len)
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
d->n_read++;
|
||||
if (d->hook != NULL) {
|
||||
d->hook(d, SIM_I2C_EV_PRE_READ); /* 按 datasheet 建模的器件在这里合成输入寄存器 */
|
||||
}
|
||||
for (size_t i = 0; i < len; i += d->reg_width) {
|
||||
uint16_t v = d->regs[d->ptr++];
|
||||
if (d->reg_width == 2) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
void sim_i2c_reset_internal(void);
|
||||
void sim_gpio_reset_internal(void);
|
||||
void sim_sr595_reset_internal(void);
|
||||
void sim_pca9555_reset_internal(void);
|
||||
void sim_pwm_reset_internal(void);
|
||||
void sim_time_reset_internal(void);
|
||||
|
||||
@@ -13,6 +14,7 @@ void sim_hal_reset(void)
|
||||
sim_i2c_reset_internal();
|
||||
sim_gpio_reset_internal();
|
||||
sim_sr595_reset_internal();
|
||||
sim_pca9555_reset_internal();
|
||||
sim_pwm_reset_internal();
|
||||
sim_time_reset_internal();
|
||||
}
|
||||
|
||||
@@ -28,7 +28,17 @@ void sim_hal_reset(void);
|
||||
#define SIM_I2C_REGS 256
|
||||
#define SIM_I2C_LOG_BYTES 8
|
||||
|
||||
typedef struct {
|
||||
/* 假器件的窃听事件。通用寄存器表模拟不了「配成输出就对地短路」这种事,
|
||||
所以按 datasheet 建模的假器件(sim_pca9555)挂一个钩子进来。 */
|
||||
typedef enum {
|
||||
SIM_I2C_EV_POST_WRITE = 0, /* 一次写事务刚落进寄存器表 */
|
||||
SIM_I2C_EV_PRE_READ, /* 一次读事务即将取寄存器表 —— 在这里合成输入 */
|
||||
} sim_i2c_ev_t;
|
||||
|
||||
struct sim_i2c_dev;
|
||||
typedef void (*sim_i2c_hook_t)(struct sim_i2c_dev *d, sim_i2c_ev_t ev);
|
||||
|
||||
typedef struct sim_i2c_dev {
|
||||
uint8_t addr;
|
||||
uint8_t reg_width; /* 1 或 2 字节。2 字节按**大端**——INA226 / DAC63204 / LM75 都是 */
|
||||
bool present; /* false = 器件失联(A15 降级矩阵用),事务一律 NAK */
|
||||
@@ -39,6 +49,7 @@ typedef struct {
|
||||
uint32_t n_read;
|
||||
uint8_t last_write[SIM_I2C_LOG_BYTES]; /* 最后一次写进去的原始字节 */
|
||||
uint8_t last_write_len;
|
||||
sim_i2c_hook_t hook; /* NULL = 就是一张寄存器表 */
|
||||
} sim_i2c_dev_t;
|
||||
|
||||
/* 往假总线上挂一个器件。地址重复或超出 16 个会 abort(测试写错了,不该悄悄过)。 */
|
||||
@@ -91,6 +102,32 @@ uint32_t sim_sr595_enable_outputs(void);
|
||||
/* 某一位在栅极上的实际电平;OE# 为高时返回 -1(高阻,不是 0)。 */
|
||||
int sim_sr595_pin(int bit);
|
||||
|
||||
/* ---------------- PCA9555(按 datasheet 建模的假器件) ---------------- */
|
||||
|
||||
/* 通用寄存器表证明不了 A6 的核心判据。这个模型只多做三件真器件会做的事:
|
||||
① **Output Port 复位值 0xFF** —— 所以把一个脚配成输出,它下一刻就输出高;
|
||||
② 输入寄存器按位合成:配成输入的位读**外部电平**,配成输出的位读**自己的输出**;
|
||||
③ 四个备用脚 P0_7 / P1_5~7 **焊死在地上** —— 读永远是 0,被驱动成高就是短路。
|
||||
③ 是这块板子上「配错方向」唯一会留下痕迹的地方:真板子上它既不报错也不 NAK。 */
|
||||
void sim_pca9555_attach(uint8_t addr);
|
||||
|
||||
/* 外部世界把某个端口的 8 根线驱动成 level(接地脚不受影响,恒 0)。
|
||||
FLT#/PGOOD 都是开漏上拉,所以「什么都没发生」的静息值是 0xFF。 */
|
||||
void sim_pca9555_drive(int port, uint8_t level);
|
||||
|
||||
uint8_t sim_pca9555_config(int port); /* 方向寄存器现值,1 = 输入 */
|
||||
uint8_t sim_pca9555_output(int port); /* 输出锁存器现值(复位 0xFF) */
|
||||
uint8_t sim_pca9555_polinv(int port);
|
||||
|
||||
/* 接地脚被配成输出过几次(不管输出高低,配了就算 —— 那已经是在对地灌电流)。 */
|
||||
uint32_t sim_pca9555_gnd_driven(void);
|
||||
/* 其中真的输出了高电平、也就是**直接对地短路**过几次。 */
|
||||
uint32_t sim_pca9555_shorted(void);
|
||||
|
||||
/* INT# 现在是不是被拉低(低有效)。输入变化拉低,**读走输入端口才释放**。 */
|
||||
bool sim_pca9555_int_asserted(void);
|
||||
uint32_t sim_pca9555_int_asserts(void); /* 累计拉低次数 */
|
||||
|
||||
/* ---------------- PWM ---------------- */
|
||||
|
||||
int sim_pwm_pin(int ch); /* 未配置返回 -1 */
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/* 按 datasheet 建模的假 PCA9555。宿主机专用。
|
||||
|
||||
通用寄存器表(sim_i2c)能测「写下去的字节对不对」,但测不出 A6 真正要拦的那件事:
|
||||
**把一个焊死在地上的脚配成输出**。真板子上这既不报错也不 NAK,只是发烫。
|
||||
所以这里把三件真器件会做的事补上(见 sim_hal.h 那一节)。
|
||||
|
||||
⚠️ 这个模型是用来证明**代码逻辑**对的。别拿它推翻设计文档 §17.2 的任何一条。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#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; }
|
||||
在新工单中引用
屏蔽一个用户