文件
phonerail/firmware/main/i2cbus.h
T
zhou yang和Claude Opus 5.5 b8ad69681f 固件: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]>
2026-09-27 10:26:24 +08:00

93 行
4.4 KiB
C
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/* `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 <stdbool.h>
#include <stdint.h>
#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 */