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]>
这个提交包含在:
@@ -6,5 +6,6 @@ idf_component_register(
|
||||
SRCS "ina226.c"
|
||||
"dac63204.c"
|
||||
"sr595.c"
|
||||
"pca9555.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES phonerail_hal)
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/* PCA9555(U23,地址 0x20)—— 板上唯一的 I/O 扩展器,**16 个脚全部是输入**。
|
||||
|
||||
挂的是五路 eFuse 的 FLT#/PGOOD、Buck 的线或 PWRGD、六片 INA226 的线或 ALERT,
|
||||
共 12 个信号 + 4 个直接接地的备用脚(设计文档 §17.2)。
|
||||
|
||||
⚠️ **本驱动不提供「把某个脚配成输出」的接口,一个都不提供。**
|
||||
四个备用脚 P0_7 / P1_5~7 在板上**直接接地、不串限流电阻**(§17.2 的注,
|
||||
图纸上已标)。而 PCA9555 的 Output Port 寄存器**复位值是 0xFF**——
|
||||
所以只要把其中一个配成输出,它下一刻就输出高电平,**直接对地短路**。
|
||||
配错既不会报错也不会 NAK,只会发烫。所以方向字一律走 pca9555_dir_safe(),
|
||||
不安全直接 abort。这跟 sr595.h 不提供「抬 OE#」是同一条规矩。
|
||||
|
||||
§10.10 的降级行为:这片不应答 = 失去 FAULT / PG 可见性,**不关输出**,
|
||||
改用 INA226 的电压判据兜底并持续告警。所以本驱动的读失败一律只是返回错误码,
|
||||
不做任何动作。
|
||||
|
||||
本驱动只经 hal_i2c_*,宿主机上由 sim 的假 PCA9555(`sim_pca9555_*`)全部测完。 */
|
||||
#ifndef PCA9555_H
|
||||
#define PCA9555_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- 地址与引脚 ---------------------------------------------------------- */
|
||||
|
||||
/* A0 = A1 = A2 = GND(U23 的 pin21 / pin2 / pin3,已按 PCB 焊盘网络核过)。 */
|
||||
#define PCA9555_ADDR 0x20
|
||||
|
||||
/* `IOEXP_INT#`:开漏、低有效、板上 R608 10 kΩ 上拉,直连 ESP32 IO38(§8.1)。
|
||||
⚠️ **中断只是加速路径,不是主路径**:§10.3 的 `t_safety` 每 20 ms 无条件轮询
|
||||
读一次两个输入端口。原因是 PCA9555 的 INT 要**读走输入端口才会释放**,而且
|
||||
输入自己变回原值时它也会释放 —— 只靠中断的话,漏掉一次读就再也等不到下一个沿。
|
||||
A6 只留这个宏,读它是 A8/A10 的活。 */
|
||||
#define PCA9555_PIN_INT_N 38
|
||||
|
||||
/* ---- 寄存器(全部 8 位) ------------------------------------------------- */
|
||||
|
||||
#define PCA9555_REG_INPUT0 0x00
|
||||
#define PCA9555_REG_INPUT1 0x01
|
||||
#define PCA9555_REG_OUTPUT0 0x02
|
||||
#define PCA9555_REG_OUTPUT1 0x03
|
||||
#define PCA9555_REG_POLINV0 0x04
|
||||
#define PCA9555_REG_POLINV1 0x05
|
||||
#define PCA9555_REG_CONFIG0 0x06
|
||||
#define PCA9555_REG_CONFIG1 0x07
|
||||
|
||||
/* 方向寄存器:**1 = 输入**(复位值 0xFF)。本板全输入,所以只会写这一个值。 */
|
||||
#define PCA9555_DIR_ALL_INPUT 0xFFu
|
||||
|
||||
/* 极性反转寄存器必须是 0。写成非 0 的现象是「五路同时报故障」或者更糟的
|
||||
「五路故障全都看不见」,而 I²C 一切正常 —— init 会写 0 并回读校验。 */
|
||||
#define PCA9555_POLINV_NONE 0x00u
|
||||
|
||||
/* ---- 位分配(从 `.claude/pcb1-parts.json` 的真实焊盘网络逐脚核对) -------
|
||||
|
||||
Port0 bit0~4 = EFUSE_FLT1~5 bit5 = BUCK_PG bit6 = INA_ALERT bit7 = 接地
|
||||
Port1 bit0~4 = EFUSE_PG1~5 bit5~7 = 接地
|
||||
|
||||
这一次两张表**确实是顺排的**(`bit = rail - 1`),和 INA226 地址、595 位号
|
||||
那两张表不一样。所以这里最容易写出来的错不是「算式」,而是
|
||||
**把 FLT 和 PG 的端口搞反** —— 两个端口的 bit0~4 长得一模一样,
|
||||
搞反之后「有故障」会被读成「电源良好」,方向正好相反。
|
||||
所以取位号一律走下面两个函数,不要在别处写 `1 << (rail - 1)`。 */
|
||||
|
||||
#define PCA9555_RAIL_COUNT 5
|
||||
|
||||
#define PCA9555_BIT_BUCK_PG 5 /* Port0,五路 Buck 的 PWRGD 线或(都是开漏) */
|
||||
#define PCA9555_BIT_INA_ALERT 6 /* Port0,六片 INA226 的 ALERT 线或 */
|
||||
|
||||
/* 接地的备用脚。**永远、永远是输入。** */
|
||||
#define PCA9555_P0_GND_MASK 0x80u /* P0_7 */
|
||||
#define PCA9555_P1_GND_MASK 0xE0u /* P1_5 P1_6 P1_7 */
|
||||
|
||||
/* rail 是 **1~5**,越界 abort。返回的是**位号**(0~4),不是掩码。 */
|
||||
int pca9555_flt_bit(int rail); /* Port0 */
|
||||
int pca9555_pg_bit(int rail); /* Port1 */
|
||||
|
||||
/* 12 个信号位互不重复、都没落到接地脚上、FLT 与 PG 分属两个端口。
|
||||
启动自检调一次(A7,和 ina226_addr_table_ok() / sr595_map_ok() 一起)。 */
|
||||
bool pca9555_map_ok(void);
|
||||
|
||||
/* 0x20 不与 INA226(0x40/41/44/45/42/43) / DAC63204(0x4A/0x4B) / LM75(0x48) 撞车。 */
|
||||
bool pca9555_addr_ok(void);
|
||||
|
||||
/* dir 里 1 = 输入。返回 false = 这个方向字会把某个**接地脚**配成输出。
|
||||
驱动内部走到 false 直接 abort:悄悄放过去的后果是板子发烫,不是功能不好用。 */
|
||||
bool pca9555_dir_safe(int port, uint8_t dir);
|
||||
|
||||
/* ---- 采样 ---------------------------------------------------------------- */
|
||||
|
||||
typedef struct {
|
||||
uint8_t port0;
|
||||
uint8_t port1;
|
||||
} pca9555_in_t;
|
||||
|
||||
/* 认件:读回两个方向寄存器,全 0xFF(复位值)才算是它。
|
||||
不应答 → HAL_ERR_NAK;应答了但方向寄存器不是复位值 → HAL_ERR_BUS
|
||||
(这个地址上有东西,但不是一片刚上电的 PCA9555)。 */
|
||||
hal_err_t pca9555_probe(void);
|
||||
|
||||
/* 写方向(全输入)与极性反转(全 0)并**逐个回读校验**,回读不一致 → HAL_ERR_BUS。
|
||||
⚠️ Output Port 一个字节都不写:写它没有意义(没有输出脚),而写错方向的
|
||||
后果见文件头。 */
|
||||
hal_err_t pca9555_init(void);
|
||||
|
||||
/* 巡检主路径:**一次事务**读两个输入端口(§10.3 的 0.12 ms 预算就是按一次算的;
|
||||
PCA9555 的读指针在 0x00 / 0x01 之间 toggle,这两个字节是 datasheet 保证的)。 */
|
||||
hal_err_t pca9555_read_inputs(pca9555_in_t *out);
|
||||
|
||||
/* ---- 语义判读 ------------------------------------------------------------
|
||||
四个信号**三种极性**,这是本文件第二个容易写反的地方。极性只在下面四个函数里
|
||||
各出现一次(和 sr595_set_hub_reset() 里那个反相同一个道理:多一处就多一处写反
|
||||
的机会)。上层一律问「有没有故障」,不许自己去 & 位。 */
|
||||
|
||||
/* TPS25940A 的 `FLT#`:开漏、板上 R113 47 kΩ 上拉 ⇒ **低 = 有故障**。 */
|
||||
bool pca9555_rail_fault(const pca9555_in_t *in, int rail);
|
||||
|
||||
/* TPS25940A 的 `PGOOD`:开漏、R115 47 kΩ 上拉,输出 2.97 V 以上才释放
|
||||
⇒ **高 = 电源良好**。 */
|
||||
bool pca9555_rail_pg(const pca9555_in_t *in, int rail);
|
||||
|
||||
/* 五路 Buck 的 `PWRGD` 线或(开漏)⇒ **高 = 五路都正常**;低只说明「有某一路
|
||||
不正常」,看不出是哪一路(§16.1)。 */
|
||||
bool pca9555_buck_pg(const pca9555_in_t *in);
|
||||
|
||||
/* 六片 INA226 的 `ALERT` 线或:开漏、低有效 ⇒ **低 = 有片子在告警**。 */
|
||||
bool pca9555_ina_alert(const pca9555_in_t *in);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PCA9555_H */
|
||||
@@ -0,0 +1,194 @@
|
||||
/* PCA9555 驱动。只经 hal_i2c_*,不碰任何平台头文件。 */
|
||||
#include "pca9555.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "hal_i2c.h"
|
||||
|
||||
/* 位表见 pca9555.h。这两张表**这次确实是顺排的**,但仍然写成表:
|
||||
哪天硬件改一根线,改这里一处就够,而不是满工程找 `1 << (rail - 1)`。 */
|
||||
static const uint8_t s_flt_bit[PCA9555_RAIL_COUNT] = { 0, 1, 2, 3, 4 }; /* Port0 */
|
||||
static const uint8_t s_pg_bit[PCA9555_RAIL_COUNT] = { 0, 1, 2, 3, 4 }; /* Port1 */
|
||||
|
||||
int pca9555_flt_bit(int rail)
|
||||
{
|
||||
assert(rail >= 1 && rail <= PCA9555_RAIL_COUNT); /* 越界是调用方算错了,不许悄悄返回 */
|
||||
return s_flt_bit[rail - 1];
|
||||
}
|
||||
|
||||
int pca9555_pg_bit(int rail)
|
||||
{
|
||||
assert(rail >= 1 && rail <= PCA9555_RAIL_COUNT);
|
||||
return s_pg_bit[rail - 1];
|
||||
}
|
||||
|
||||
bool pca9555_map_ok(void)
|
||||
{
|
||||
uint8_t p0 = 0, p1 = 0;
|
||||
|
||||
for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) {
|
||||
const uint8_t f = (uint8_t)(1u << s_flt_bit[rail - 1]);
|
||||
const uint8_t g = (uint8_t)(1u << s_pg_bit[rail - 1]);
|
||||
if ((p0 & f) != 0 || (p1 & g) != 0) {
|
||||
return false; /* 两路占了同一位 */
|
||||
}
|
||||
p0 |= f;
|
||||
p1 |= g;
|
||||
}
|
||||
|
||||
const uint8_t buck = (uint8_t)(1u << PCA9555_BIT_BUCK_PG);
|
||||
const uint8_t alrt = (uint8_t)(1u << PCA9555_BIT_INA_ALERT);
|
||||
if ((p0 & buck) != 0 || (p0 & alrt) != 0 || buck == alrt) {
|
||||
return false; /* 线或的两根撞到某路 FLT 上,或者撞到一起 */
|
||||
}
|
||||
p0 |= (uint8_t)(buck | alrt);
|
||||
|
||||
/* 一个信号都不许落到接地脚上 —— 落上去就等于永远读到 0(FLT 永远报故障,
|
||||
PG 永远报「不良」),而 I²C 一切正常。 */
|
||||
if ((p0 & PCA9555_P0_GND_MASK) != 0 || (p1 & PCA9555_P1_GND_MASK) != 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool pca9555_addr_ok(void)
|
||||
{
|
||||
/* 同一条 I²C 上的其它器件(§8.2)。 */
|
||||
static const uint8_t others[] = { 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x48, 0x4A, 0x4B };
|
||||
|
||||
if (PCA9555_ADDR > 0x7f) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < sizeof(others); i++) {
|
||||
if (others[i] == PCA9555_ADDR) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool pca9555_dir_safe(int port, uint8_t dir)
|
||||
{
|
||||
if (port != 0 && port != 1) {
|
||||
return false;
|
||||
}
|
||||
/* 1 = 输入。接地脚对应的位必须是 1,一位都不能少。 */
|
||||
const uint8_t gnd = (port == 0) ? PCA9555_P0_GND_MASK : PCA9555_P1_GND_MASK;
|
||||
return (dir & gnd) == gnd;
|
||||
}
|
||||
|
||||
/* ---- 寄存器读写 ---------------------------------------------------------
|
||||
PCA9555 的寄存器是 8 位的,命令字节在**成对**的寄存器之间 toggle
|
||||
(0/1、2/3、4/5、6/7),不是无限自增。本驱动只在一对之内连读 2 字节,
|
||||
落在 datasheet 保证的范围里;跨对连读(比如从 0x06 读 4 字节)行为不同,
|
||||
别那么写。 */
|
||||
|
||||
static hal_err_t read_reg(uint8_t reg, uint8_t *val)
|
||||
{
|
||||
return hal_i2c_write_read(PCA9555_ADDR, ®, 1, val, 1);
|
||||
}
|
||||
|
||||
static hal_err_t write_reg(uint8_t reg, uint8_t val)
|
||||
{
|
||||
const uint8_t wr[2] = { reg, val };
|
||||
return hal_i2c_write(PCA9555_ADDR, wr, sizeof(wr));
|
||||
}
|
||||
|
||||
/* 写完立刻回读比对。回读不一致 = 总线上有东西但不是我们以为的那片。 */
|
||||
static hal_err_t write_verify(uint8_t reg, uint8_t val)
|
||||
{
|
||||
hal_err_t err = write_reg(reg, val);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
uint8_t rb = 0;
|
||||
err = read_reg(reg, &rb);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
return (rb == val) ? HAL_OK : HAL_ERR_BUS;
|
||||
}
|
||||
|
||||
hal_err_t pca9555_probe(void)
|
||||
{
|
||||
uint8_t c0 = 0, c1 = 0;
|
||||
hal_err_t err = read_reg(PCA9555_REG_CONFIG0, &c0);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
err = read_reg(PCA9555_REG_CONFIG1, &c1);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
/* 刚上电的 PCA9555 两个方向寄存器都是 0xFF。不是的话,要么这地址上蹲着别的
|
||||
器件,要么它已经被谁配过了 —— 两种都要在启动时就叫出来。 */
|
||||
return (c0 == PCA9555_DIR_ALL_INPUT && c1 == PCA9555_DIR_ALL_INPUT) ? HAL_OK : HAL_ERR_BUS;
|
||||
}
|
||||
|
||||
hal_err_t pca9555_init(void)
|
||||
{
|
||||
/* 方向必须先于一切:这一步写错就是四个接地脚被驱动成高电平。
|
||||
全输入是唯一合法值,assert 拦的是「哪天有人顺手改了这个常量」。 */
|
||||
assert(pca9555_dir_safe(0, PCA9555_DIR_ALL_INPUT));
|
||||
assert(pca9555_dir_safe(1, PCA9555_DIR_ALL_INPUT));
|
||||
|
||||
hal_err_t err = write_verify(PCA9555_REG_CONFIG0, PCA9555_DIR_ALL_INPUT);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
err = write_verify(PCA9555_REG_CONFIG1, PCA9555_DIR_ALL_INPUT);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
/* 极性反转清零。复位值本来就是 0,但「本来就是」不值得赌 —— 看门狗复位
|
||||
只复位 ESP32,PCA9555 是不掉电的,上一条命的残留值会原样留着。 */
|
||||
err = write_verify(PCA9555_REG_POLINV0, PCA9555_POLINV_NONE);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
return write_verify(PCA9555_REG_POLINV1, PCA9555_POLINV_NONE);
|
||||
}
|
||||
|
||||
hal_err_t pca9555_read_inputs(pca9555_in_t *out)
|
||||
{
|
||||
if (out == NULL) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
const uint8_t reg = PCA9555_REG_INPUT0;
|
||||
uint8_t rd[2] = { 0, 0 };
|
||||
hal_err_t err = hal_i2c_write_read(PCA9555_ADDR, ®, 1, rd, sizeof(rd));
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
out->port0 = rd[0];
|
||||
out->port1 = rd[1];
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* ---- 语义判读(极性各出现一次,见头文件) -------------------------------- */
|
||||
|
||||
bool pca9555_rail_fault(const pca9555_in_t *in, int rail)
|
||||
{
|
||||
assert(in != NULL);
|
||||
return (in->port0 & (uint8_t)(1u << pca9555_flt_bit(rail))) == 0; /* FLT# 低有效 */
|
||||
}
|
||||
|
||||
bool pca9555_rail_pg(const pca9555_in_t *in, int rail)
|
||||
{
|
||||
assert(in != NULL);
|
||||
return (in->port1 & (uint8_t)(1u << pca9555_pg_bit(rail))) != 0; /* PGOOD 高 = 良好 */
|
||||
}
|
||||
|
||||
bool pca9555_buck_pg(const pca9555_in_t *in)
|
||||
{
|
||||
assert(in != NULL);
|
||||
return (in->port0 & (uint8_t)(1u << PCA9555_BIT_BUCK_PG)) != 0; /* 高 = 五路都正常 */
|
||||
}
|
||||
|
||||
bool pca9555_ina_alert(const pca9555_in_t *in)
|
||||
{
|
||||
assert(in != NULL);
|
||||
return (in->port0 & (uint8_t)(1u << PCA9555_BIT_INA_ALERT)) == 0; /* ALERT 低有效 */
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户