/* 带 mux 的 I²C 门面。契约与三条规矩见 i2cbus.h。 */ #include "i2cbus.h" #include #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 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_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; } /* 嵌套 = **本任务**已经持着这把锁(回调里又调了一次,头文件末尾那条警告)。 ⚠️ 0.1.11 之前这里看的是一个全局的 `s_in_xact`:别的任务正在事务里时它也是 true, 于是执行器撞上巡检那 ~2.3 ms(每 20 ms 一次,≈12%)就被当成「嵌套」立刻回 STATE —— 现场表现是 `key` 随机回 busy、`vbus` 随机回 io(2026-09-28 板 B)。别的任务持锁 = 该等锁。 */ if (hal_lock_held(HAL_LOCK_I2C)) { 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; /* 这一次什么也没做 —— 调用方决定重试还是跳过 */ } err = select_ch(ch); if (err == HAL_OK) { err = fn(arg); } hal_lock_give(HAL_LOCK_I2C); return err; } hal_err_t i2cbus_recover(void) { if (!s_inited) { return HAL_ERR_STATE; } if (hal_lock_held(HAL_LOCK_I2C)) { 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; }