固件 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]>
这个提交包含在:
+152
-152
@@ -1,152 +1,152 @@
|
||||
/* 带 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;
|
||||
}
|
||||
/* 带 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;
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户