/* `i2cbus` —— 带 mux 的 I²C 总线门面(R2 新增,计划 §4.3)。 R2 之后,一次「读 CH3 的电压」在总线上其实是两件事: **先写 TCA9548A 选通道 2,再读 0x40 的寄存器**。 这两件事之间如果被另一个任务插进来选了别的通道,读回来的是**另一台手机的 数据** —— 而且量纲、范围、变化趋势全部正常,没有任何一层会报错。 这是 R2 引入的、最难查的一类静默错误(C-d)。 所以这一层只做一件事:**把「选通道 + 事务」变成一个不可分的动作。** ---- 三条规矩 ------------------------------------------------------------ 1. **应用层不许直接调 `hal_i2c_*`。** 驱动照旧只调 `hal_i2c_*`(它们要能单独 测),但驱动的**调用方**必须经 `i2cbus_xact()`。 2. **`i2cbus_select()` 不是公开接口。** 对外只有 `i2cbus_xact(ch, ...)`: 拿锁 → 必要时选通道 → 跑回调 → 放锁。 3. **通道影子在任何一次选通失败之后作废**(置 `I2CBUS_CH_UNKNOWN`)。 不作废的话,下一次「通道相同、跳过选通」会把事务发到一个其实没切过去的 通道上 —— 这正好是第 1 条要防的那种错,只是由自己制造。 ---- 为什么要有通道影子 -------------------------------------------------- `t_safety` 每 20 ms 跑一圈五路,如果每次 `xact` 都无条件发一遍选通字节, 一圈就多 5 次事务。影子让「通道没变就不重发」,代价是上面第 3 条那个 必须小心处理的不变量。这笔账在计划 §5.5 里算过。 ⚠️ **回调里不许再调 `i2cbus_xact()`**(会嵌套拿同一把锁,直接 HAL_ERR_STATE), 也不许在里面睡 —— 它是持着总线锁跑的。 */ #ifndef I2CBUS_H #define I2CBUS_H #include #include #include "hal_err.h" #include "tca9548a.h" #ifdef __cplusplus extern "C" { #endif /* 主干:所有支路断开。DAC63204 ×2 与输入侧 INA226 挂在这里。 */ #define I2CBUS_CH_TRUNK TCA9548A_CH_NONE /* -1 */ /* 影子失效 —— 下一次 `xact` 无条件重发选通字节。 */ #define I2CBUS_CH_UNKNOWN (-2) /* 一次事务的正文。**持着 I²C 锁、通道已经选好**的情况下被调一次。 返回值原样成为 `i2cbus_xact()` 的返回值(选通道失败时它根本不会被调)。 */ typedef hal_err_t (*i2cbus_fn_t)(void *arg); /* 配 `MUX_RST#` 为输出并拉高(不复位)、认件、把通道全关。 ⚠️ 必须在 `hal_i2c_init()` 与 `hal_lock_init()` 之后调。 认件失败照样返回错误码,**但不 abort**:mux 挂了是运行期的降级情形 (计划 §5.8),不是固件写错了。 */ hal_err_t i2cbus_init(void); /* 三件事一气呵成:拿锁(等 `timeout_ms`)→ 必要时选通道 → 跑 `fn` → 放锁。 返回: HAL_ERR_STATE —— 没 init,或者**嵌套调用**(回调里又调了一次); HAL_ERR_TIMEOUT —— 没拿到锁(这一次什么也没做,调用方自己决定重试还是跳过); 选通道的错误码 —— `fn` 一次都没被调; 否则 = `fn` 的返回值。 */ hal_err_t i2cbus_xact(int ch, uint32_t timeout_ms, i2cbus_fn_t fn, void *arg); /* ---- 观测与恢复 ---------------------------------------------------------- */ int i2cbus_channel(void); /* 当前影子:0~7 / `I2CBUS_CH_TRUNK` / `I2CBUS_CH_UNKNOWN` */ /* **连续**选通失败次数(成功一次就清零)。计划 §5.8 的「mux 失联」判据读它。 ⚠️ 判据用「连续」而不是「累计」:一次总线毛刺不该停掉五台手机。 */ uint32_t i2cbus_select_fail_streak(void); /* 累计量,给遥测与测试用。 */ uint32_t i2cbus_selects(void); /* 真正发出去的选通字节数(影子命中不算) */ uint32_t i2cbus_select_fails(void); /* 累计失败次数 */ uint32_t i2cbus_lock_timeouts(void); uint32_t i2cbus_recoveries(void); /* `MUX_RST#` 拉低一下再放开,然后把通道影子作废。 判据在「五路全断」之前必须先试这一次(计划 §5.8)。 ⚠️ 自己拿锁,**不许在 `xact` 的回调里调**。 */ hal_err_t i2cbus_recover(void); /* 影子作废。给 `i2cbus_recover()` 与测试用;正常路径不需要。 */ void i2cbus_forget_channel(void); #ifdef __cplusplus } #endif #endif /* I2CBUS_H */