固件:R2 迁移 R-0 ~ R-6(底板 + 5 片同构刀片)

2026-09-16 完成、当时未提交,这里补提交(原样,不含 09-27 上板的修改)。

- 器件层 / 拓扑层整体从 R1 换到 R2:TCA9548A 分通道、五路同址 INA226 0x40 + 输入侧 0x43、
  刀片 PCA9555(VBUS_EN 挪到 P0.4)、CH224A 只读驱动、风扇闭环 t_thermal
- 新增 i2cbus(mux 原子事务)、slot(刀片 I/O 门面)、host/qtest.sh
- 74HC595 链退役(代码与测试保留)
- 判据阈值与时间常量未动;19 个 ctest 全绿,idf.py build 通过

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-27 10:26:24 +08:00
共同撰写人 Claude Opus 5.5
父节点 5a80d8a872
当前提交 b8ad69681f
共修改 51 个文件,包含 10882 行新增和 4068 行删除
+149 -58
查看文件
@@ -1,4 +1,5 @@
/* PCA9555 驱动。只经 hal_i2c_*,不碰任何平台头文件。 */
/* PCA9555 驱动(R2:每片刀片一颗,P0.0~P0.4 是输出)。
只经 hal_i2c_*,不碰 mux、不碰任何平台头文件。契约见 pca9555.h。 */
#include "pca9555.h"
#include <assert.h>
@@ -6,47 +7,83 @@
#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 */
/* 键 → 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,
};
int pca9555_flt_bit(int rail)
/* 五片各自的 Port0 输出影子。**写成功才更新**(见 pca9555_write_out0)。 */
static uint8_t s_out0[PCA9555_SLOT_COUNT];
int pca9555_key_bit(pca9555_key_t key)
{
assert(rail >= 1 && rail <= PCA9555_RAIL_COUNT); /* 越界是调用方算错了,不许悄悄返回 */
return s_flt_bit[rail - 1];
assert(key >= 0 && key < PCA9555_KEY_COUNT); /* 越界是调用方算错了 */
return s_key_bit[key];
}
int pca9555_pg_bit(int rail)
uint8_t pca9555_key_mask(void)
{
assert(rail >= 1 && rail <= PCA9555_RAIL_COUNT);
return s_pg_bit[rail - 1];
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, p1 = 0;
uint8_t p0 = 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; /* 两路占了同一位 */
/* 三个键互不重复。 */
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 |= f;
p1 |= g;
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;
}
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 上,或者撞到一起 */
/* 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;
}
p0 |= (uint8_t)(buck | alrt);
/* 一个信号都不许落到接地脚上 —— 落上去就等于永远读到 0(FLT 永远报故障,
PG 永远报「不良」),而 I²C 一切正常。 */
if ((p0 & PCA9555_P0_GND_MASK) != 0 || (p1 & PCA9555_P1_GND_MASK) != 0) {
/* 两个方向字必须与位分配自洽: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;
@@ -54,8 +91,8 @@ bool pca9555_map_ok(void)
bool pca9555_addr_ok(void)
{
/* 同一条 I²C 上的其它器件(§8.2)。 */
static const uint8_t others[] = { 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x48, 0x4A, 0x4B };
/* 主干上的器件(计划 §5.2)。刀片支路上另有 0x40 的 INA226,与 0x20 不冲突。 */
static const uint8_t others[] = { 0x40, 0x43, 0x4A, 0x4B, 0x70 };
if (PCA9555_ADDR > 0x7f) {
return false;
@@ -70,19 +107,22 @@ bool pca9555_addr_ok(void)
bool pca9555_dir_safe(int port, uint8_t dir)
{
if (port != 0 && port != 1) {
return false;
/* 1 = 输入。 */
if (port == 0) {
/* 接地脚必须是输入,一位都不能少。bit0~4 随意(它们就是要当输出的)。 */
return (dir & PCA9555_P0_GND_MASK) == PCA9555_P0_GND_MASK;
}
/* 1 = 输入。接地脚对应的位必须是 1,一位都不能少。 */
const uint8_t gnd = (port == 0) ? PCA9555_P0_GND_MASK : PCA9555_P1_GND_MASK;
return (dir & gnd) == gnd;
if (port == 1) {
/* Port1 **一位都不许输出**:bit0~3 接的是 eFuse / Buck / INA 的开漏输出,
配成输出就是两个驱动器对打;bit4~7 焊死在地上。 */
return dir == PCA9555_DIR_ALL_INPUT;
}
return false;
}
/* ---- 寄存器读写 ---------------------------------------------------------
PCA9555 的寄存器是 8 位的,命令字节在**成对**的寄存器之间 toggle
(0/1、2/3、4/5、6/7),不是无限自增。本驱动只在一对之内连读 2 字节,
落在 datasheet 保证的范围里;跨对连读(比如从 0x06 读 4 字节)行为不同,
别那么写。 */
PCA9555 的命令字节在**成对**的寄存器之间 toggle(0/1、2/3、4/5、6/7),
不是无限自增。本驱动只在一对之内连读 2 字节,落在 datasheet 保证的范围里。 */
static hal_err_t read_reg(uint8_t reg, uint8_t *val)
{
@@ -121,34 +161,53 @@ hal_err_t pca9555_probe(void)
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)
hal_err_t pca9555_init(int slot)
{
/* 方向必须先于一切:这一步写错就是四个接地脚被驱动成高电平。
全输入是唯一合法值,assert 拦的是「哪天有人顺手改了这个常量」。 */
assert(pca9555_dir_safe(0, PCA9555_DIR_ALL_INPUT));
assert(pca9555_dir_safe(1, PCA9555_DIR_ALL_INPUT));
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));
hal_err_t err = write_verify(PCA9555_REG_CONFIG0, PCA9555_DIR_ALL_INPUT);
/* ⛔ 顺序就是安全本身(头文件那个框):**输出锁存器先清零,方向最后改**。
复位值 0xFF 的输出锁存器 + 先改方向 = 三个键全按下 + VBUS 上电。 */
hal_err_t err = write_verify(PCA9555_REG_OUTPUT0, 0x00);
if (err != HAL_OK) {
return err;
}
err = write_verify(PCA9555_REG_CONFIG1, PCA9555_DIR_ALL_INPUT);
err = write_verify(PCA9555_REG_OUTPUT1, 0x00);
if (err != HAL_OK) {
return err;
}
/* 极性反转清零。复位值本来就是 0,但「本来就是」不值得赌 —— 看门狗复位
只复位 ESP32,PCA9555 是不掉电的,上一条命的残留值会原样留着。 */
只复位 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);
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)
@@ -167,28 +226,60 @@ hal_err_t pca9555_read_inputs(pca9555_in_t *out)
return HAL_OK;
}
/* ---- 语义判读(极性各出现一次,见头文件) -------------------------------- */
bool pca9555_rail_fault(const pca9555_in_t *in, int rail)
hal_err_t pca9555_write_out0(int slot, uint8_t mask, uint8_t value)
{
assert(in != NULL);
return (in->port0 & (uint8_t)(1u << pca9555_flt_bit(rail))) == 0; /* FLT# 低有效 */
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;
}
bool pca9555_rail_pg(const pca9555_in_t *in, int rail)
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_pg_bit(rail))) != 0; /* PGOOD 高 = 良好 */
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->port0 & (uint8_t)(1u << PCA9555_BIT_BUCK_PG)) != 0; /* 高 = 五路都正常 */
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->port0 & (uint8_t)(1u << PCA9555_BIT_INA_ALERT)) == 0; /* ALERT 低有效 */
return (in->port1 & (uint8_t)(1u << PCA9555_BIT_INA_ALERT)) == 0; /* 低有效 */
}