固件: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 行删除
+152
查看文件
@@ -0,0 +1,152 @@
/* 带 mux 的 I²C 门面。契约与三条规矩见 i2cbus.h。 */
#include "i2cbus.h"
#include <stddef.h>
#include "board_pins.h"
#include "hal_gpio.h"
#include "hal_lock.h"
#include "hal_time.h"
#include "tca9548a.h"
/* `MUX_RST#` 的低电平宽度。datasheet 只要求 6 ns,这里给的是「至少 1 ms」。
⚠️ **实际会比 1 ms 长**:ESP 侧 `hal_time_delay_ms()` 是 `vTaskDelay()`,
在 `CONFIG_FREERTOS_HZ=100` 下一个 tick 就是 10 ms(而且它按「至少睡一个
tick」写死)。对这颗器件绰绰有余,但要知道**调用者会被挡住这么久** ——
`i2cbus_recover()` 是 `t_safety` 在放锁之后调的,所以触发恢复的那一圈
巡检会晚一拍。这是可以接受的:走到这条路径时 mux 已经连续失联了,
比起「五路电压电流全看不见还在继续跑」,晚一圈不算什么。 */
#define I2CBUS_RST_MS 1
static bool s_inited;
static bool s_in_xact; /* 嵌套检测。单核上够用,双核上外面还有锁挡着 */
static int s_ch = I2CBUS_CH_UNKNOWN;
static uint32_t s_selects;
static uint32_t s_select_fails;
static uint32_t s_fail_streak;
static uint32_t s_lock_timeouts;
static uint32_t s_recoveries;
int i2cbus_channel(void) { return s_ch; }
uint32_t i2cbus_selects(void) { return s_selects; }
uint32_t i2cbus_select_fails(void) { return s_select_fails; }
uint32_t i2cbus_select_fail_streak(void) { return s_fail_streak; }
uint32_t i2cbus_lock_timeouts(void) { return s_lock_timeouts; }
uint32_t i2cbus_recoveries(void) { return s_recoveries; }
void i2cbus_forget_channel(void) { s_ch = I2CBUS_CH_UNKNOWN; }
/* 选通道。**成功才更新影子**;失败一律把影子作废(规矩 3)。 */
static hal_err_t select_ch(int ch)
{
if (ch == s_ch) {
return HAL_OK; /* 影子命中:一个字节都不发(计划 §5.5 的账就靠这个) */
}
s_selects++;
const hal_err_t err = tca9548a_select(TCA9548A_ADDR, ch);
if (err != HAL_OK) {
s_ch = I2CBUS_CH_UNKNOWN;
s_select_fails++;
if (s_fail_streak < 0xFFFFFFFFu) {
s_fail_streak++;
}
return err;
}
s_ch = ch;
s_fail_streak = 0;
return HAL_OK;
}
hal_err_t i2cbus_init(void)
{
s_inited = false;
s_in_xact = false;
s_ch = I2CBUS_CH_UNKNOWN;
s_selects = 0;
s_select_fails = 0;
s_fail_streak = 0;
s_lock_timeouts = 0;
s_recoveries = 0;
/* `MUX_RST#` 低有效、板上 R611 上拉。先写 1 再配输出会有一个「配成输出时
默认输出 0」的窗口,那就是一次意外复位 —— 所以顺序是:配输出(HAL 两侧
都保证配完是 0)→ 立刻写 1。窗口仍在,但只有几个指令,且此刻还没人用总线。 */
hal_err_t err = hal_gpio_config(BOARD_PIN_MUX_RST_N, HAL_GPIO_OUTPUT);
if (err != HAL_OK) {
return err;
}
err = hal_gpio_write(BOARD_PIN_MUX_RST_N, 1);
if (err != HAL_OK) {
return err;
}
s_inited = true;
/* 认件顺便把通道全关。失败不 abort —— 见头文件。 */
err = hal_lock_take(HAL_LOCK_I2C, HAL_LOCK_FOREVER);
if (err != HAL_OK) {
return err;
}
const hal_err_t probe = tca9548a_probe(TCA9548A_ADDR);
if (probe == HAL_OK) {
s_ch = I2CBUS_CH_TRUNK; /* probe 末尾就是「全关」 */
} else {
s_ch = I2CBUS_CH_UNKNOWN;
}
hal_lock_give(HAL_LOCK_I2C);
return probe;
}
hal_err_t i2cbus_xact(int ch, uint32_t timeout_ms, i2cbus_fn_t fn, void *arg)
{
if (!s_inited || fn == NULL) {
return HAL_ERR_STATE;
}
if (s_in_xact) {
return HAL_ERR_STATE; /* 嵌套:回调里又调了一次(头文件末尾那条警告) */
}
if (ch < I2CBUS_CH_TRUNK || ch >= TCA9548A_CH_COUNT) {
return HAL_ERR_ARG;
}
hal_err_t err = hal_lock_take(HAL_LOCK_I2C, timeout_ms);
if (err != HAL_OK) {
s_lock_timeouts++;
return HAL_ERR_TIMEOUT; /* 这一次什么也没做 —— 调用方决定重试还是跳过 */
}
s_in_xact = true;
err = select_ch(ch);
if (err == HAL_OK) {
err = fn(arg);
}
s_in_xact = false;
hal_lock_give(HAL_LOCK_I2C);
return err;
}
hal_err_t i2cbus_recover(void)
{
if (!s_inited) {
return HAL_ERR_STATE;
}
if (s_in_xact) {
return HAL_ERR_STATE; /* 持着锁去睡 1 ms,会把巡检钉住 */
}
s_recoveries++;
hal_err_t err = hal_gpio_write(BOARD_PIN_MUX_RST_N, 0);
if (err != HAL_OK) {
i2cbus_forget_channel();
return err;
}
hal_time_delay_ms(I2CBUS_RST_MS);
err = hal_gpio_write(BOARD_PIN_MUX_RST_N, 1);
/* ⚠️ **无论成功与否都作废影子**:复位之后器件里的通道是全关的,
而影子还记着上一次选的那一路。不作废 = 下一次事务跳过选通、
发到一个断开的支路上,现象是「所有刀片一起失联」。 */
i2cbus_forget_channel();
return err;
}