/* PCA9555 驱动(R2:每片刀片一颗,P0.0~P0.4 是输出)。 只经 hal_i2c_*,不碰 mux、不碰任何平台头文件。契约见 pca9555.h。 */ #include "pca9555.h" #include #include #include "hal_i2c.h" /* 键 → Port0 位号。写成表而不是 `key + 0`:哪天硬件改一根线,改这里一处就够。 */ static const uint8_t s_key_bit[PCA9555_KEY_COUNT] = { PCA9555_BIT_KEY_PWR, PCA9555_BIT_KEY_VUP, PCA9555_BIT_KEY_VDN, }; /* 五片各自的 Port0 输出影子。**写成功才更新**(见 pca9555_write_out0)。 */ static uint8_t s_out0[PCA9555_SLOT_COUNT]; int pca9555_key_bit(pca9555_key_t key) { assert(key >= 0 && key < PCA9555_KEY_COUNT); /* 越界是调用方算错了 */ return s_key_bit[key]; } uint8_t pca9555_key_mask(void) { uint8_t m = 0; for (int k = 0; k < PCA9555_KEY_COUNT; k++) { m |= (uint8_t)(1u << s_key_bit[k]); } return m; } bool pca9555_map_ok(void) { uint8_t p0 = 0; /* 三个键互不重复。 */ for (int k = 0; k < PCA9555_KEY_COUNT; k++) { const uint8_t b = (uint8_t)(1u << s_key_bit[k]); if ((p0 & b) != 0) { return false; } p0 |= b; } /* EDL 与 VBUS 也在 Port0,同样不许撞。 */ const uint8_t edl = (uint8_t)(1u << PCA9555_BIT_EDL_EN); const uint8_t vbus = (uint8_t)(1u << PCA9555_BIT_VBUS_EN); if ((p0 & edl) != 0 || (p0 & vbus) != 0 || edl == vbus) { return false; } p0 |= (uint8_t)(edl | vbus); /* 五个输出位必须正好等于「允许配成输出」的那五位: 少一位 = 有个输出永远发不出去;多一位 = 方向字会把接地脚驱起来。 */ if (p0 != PCA9555_P0_OUT_MASK) { return false; } if ((p0 & PCA9555_P0_GND_MASK) != 0) { return false; } /* Port1 的四个输入位互不重复、都不落在接地脚上。 */ const uint8_t in_bits[] = { (uint8_t)(1u << PCA9555_BIT_EFUSE_FLT), (uint8_t)(1u << PCA9555_BIT_BUCK_PG), (uint8_t)(1u << PCA9555_BIT_INA_ALERT), (uint8_t)(1u << PCA9555_BIT_EFUSE_PG), }; uint8_t p1 = 0; for (size_t i = 0; i < sizeof(in_bits); i++) { if ((p1 & in_bits[i]) != 0) { return false; } p1 |= in_bits[i]; } if (p1 != PCA9555_P1_IN_MASK || (p1 & PCA9555_P1_GND_MASK) != 0) { return false; } /* 两个方向字必须与位分配自洽:Port0 上「不是输入」的正好是那五个输出位。 */ if ((uint8_t)~PCA9555_CONFIG0_INIT != PCA9555_P0_OUT_MASK) { return false; } if (PCA9555_CONFIG1_INIT != PCA9555_DIR_ALL_INPUT) { return false; } return true; } bool pca9555_addr_ok(void) { /* 主干上的器件(计划 §5.2)。刀片支路上另有 0x40 的 INA226,与 0x20 不冲突。 */ static const uint8_t others[] = { 0x40, 0x43, 0x4A, 0x4B, 0x70 }; 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) { /* 1 = 输入。 */ if (port == 0) { /* 接地脚必须是输入,一位都不能少。bit0~4 随意(它们就是要当输出的)。 */ return (dir & PCA9555_P0_GND_MASK) == PCA9555_P0_GND_MASK; } if (port == 1) { /* Port1 **一位都不许输出**:bit0~3 接的是 eFuse / Buck / INA 的开漏输出, 配成输出就是两个驱动器对打;bit4~7 焊死在地上。 */ return dir == PCA9555_DIR_ALL_INPUT; } return false; } /* ---- 寄存器读写 --------------------------------------------------------- PCA9555 的命令字节在**成对**的寄存器之间 toggle(0/1、2/3、4/5、6/7), 不是无限自增。本驱动只在一对之内连读 2 字节,落在 datasheet 保证的范围里。 */ 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; } return (c0 == PCA9555_DIR_ALL_INPUT && c1 == PCA9555_DIR_ALL_INPUT) ? HAL_OK : HAL_ERR_BUS; } hal_err_t pca9555_init(int slot) { if (slot < 0 || slot >= PCA9555_SLOT_COUNT) { return HAL_ERR_ARG; } /* 方向字的合法性是编译期就能判的事,assert 拦的是「哪天有人顺手改了常量」。 */ assert(pca9555_dir_safe(0, PCA9555_CONFIG0_INIT)); assert(pca9555_dir_safe(1, PCA9555_CONFIG1_INIT)); /* ⛔ 顺序就是安全本身(头文件那个框):**输出锁存器先清零,方向最后改**。 复位值 0xFF 的输出锁存器 + 先改方向 = 三个键全按下 + VBUS 上电。 */ hal_err_t err = write_verify(PCA9555_REG_OUTPUT0, 0x00); if (err != HAL_OK) { return err; } err = write_verify(PCA9555_REG_OUTPUT1, 0x00); if (err != HAL_OK) { return err; } /* 极性反转清零。复位值本来就是 0,但「本来就是」不值得赌 —— 看门狗复位 只复位 ESP32,刀片上的 PCA9555 是不掉电的,上一条命的残留值会原样留着。 */ err = write_verify(PCA9555_REG_POLINV0, PCA9555_POLINV_NONE); if (err != HAL_OK) { return err; } err = write_verify(PCA9555_REG_POLINV1, PCA9555_POLINV_NONE); if (err != HAL_OK) { return err; } /* Port1 先(它本来就该保持全输入),Port0 最后 —— 这一句之后五个输出脚 才真的开始驱动,而它们的锁存器在上面已经是 0 了。 */ err = write_verify(PCA9555_REG_CONFIG1, PCA9555_CONFIG1_INIT); if (err != HAL_OK) { return err; } err = write_verify(PCA9555_REG_CONFIG0, PCA9555_CONFIG0_INIT); if (err != HAL_OK) { return err; } s_out0[slot] = 0; return HAL_OK; } 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; } hal_err_t pca9555_write_out0(int slot, uint8_t mask, uint8_t value) { if (slot < 0 || slot >= PCA9555_SLOT_COUNT) { return HAL_ERR_ARG; } if ((mask & ~PCA9555_P0_OUT_MASK) != 0) { /* 掩码碰到了接地脚或者别的端口。不 abort:这多半是上层算错了掩码, 返回错误码能让它记一条事件;真正会冒烟的是**方向**,那条走 abort。 */ return HAL_ERR_ARG; } const uint8_t next = (uint8_t)((s_out0[slot] & ~mask) | (value & mask)); const hal_err_t err = write_reg(PCA9555_REG_OUTPUT0, next); if (err == HAL_OK) { s_out0[slot] = next; /* 影子只在写成功之后更新 */ } return err; } uint8_t pca9555_shadow(int slot) { assert(slot >= 0 && slot < PCA9555_SLOT_COUNT); return s_out0[slot]; } void pca9555_shadow_reset(void) { for (int i = 0; i < PCA9555_SLOT_COUNT; i++) { s_out0[i] = 0; } } /* ---- 语义判读(极性各出现一次,见头文件) -------------------------------- */ bool pca9555_efuse_fault(const pca9555_in_t *in) { assert(in != NULL); return (in->port1 & (uint8_t)(1u << PCA9555_BIT_EFUSE_FLT)) == 0; /* FLT# 低有效 */ } bool pca9555_efuse_pg(const pca9555_in_t *in) { assert(in != NULL); return (in->port1 & (uint8_t)(1u << PCA9555_BIT_EFUSE_PG)) != 0; /* 高 = 良好 */ } bool pca9555_buck_pg(const pca9555_in_t *in) { assert(in != NULL); return (in->port1 & (uint8_t)(1u << PCA9555_BIT_BUCK_PG)) != 0; /* 高 = 正常 */ } bool pca9555_ina_alert(const pca9555_in_t *in) { assert(in != NULL); return (in->port1 & (uint8_t)(1u << PCA9555_BIT_INA_ALERT)) == 0; /* 低有效 */ }