文件
phonerail/firmware/main/i2cbus.c
T
zhou yang和Claude Opus 5.5 9fb5bba367 固件 0.1.5~0.1.13-c5:刷机模式、多组 WiFi、热插拔初始化、总线锁误判、硬重启断 VBUS
- 0.1.5/0.1.6:刷机模式(flash_mode,最多 2 路,60 分钟可加钟)——窗口内顶高按 4450 mV 绝对值判、不断 VBUS
- 0.1.7/0.1.8:反灌断 VBUS 阈值放宽到 -300 mA(adb reboot bootloader 过渡实测 -208 mA)
- 0.1.9:WiFi 最多存 4 组,连不上轮换;NVS v1 自动迁移并双写(OTA 回滚仍能上网)
- 0.1.10:热插拔进来的刀片重新初始化 PCA9555 + INA226(否则 VBUS 常开、电流恒读 0)
- 0.1.11:i2cbus 嵌套检测改看本任务持锁(原全局标志让 key 随机回 busy、vbus 随机回 io)
- 0.1.12:硬重启先断 VBUS,开机校验通过 60 s 后再接回(否则假重启 / 误判没开机 / 进关机充电)
- 0.1.13:这一路被安全判据断电时一并退出刷机模式(事件 21 arg 254)

26 ctest 全绿;新增用例均先在旧代码上确认变红。板 B 已 OTA 到 0.1.13-c5 并实测。

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-29 08:16:21 +08:00

153 行
5.2 KiB
C
原始文件 Blame 文件历史

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