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]>
153 行
5.1 KiB
C
153 行
5.1 KiB
C
/* 带 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;
|
||
}
|