固件 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]>
这个提交包含在:
+89
-1
@@ -1,5 +1,93 @@
|
|||||||
# 交接
|
# 交接
|
||||||
更新:2026-09-28 固件 **0.1.4-c5**(整片刀片不应答 ⇒ 关路);板 B 经 OTA 推上
|
更新:2026-09-28 23:20 固件 **0.1.13-c5**(断电自动退出刷机模式);板 B 经 OTA 推上;rack 已部署
|
||||||
|
|
||||||
|
## 0.1.13:这一路断了电 ⇒ 刷机模式一并退出(2026-09-28)
|
||||||
|
- 起因:第 2 槽刀片带电拔走后,刷机模式还挂了约 48 分钟、占着「最多 2 路」的名额。`check_present` / SR-5 / 刀片不应答 /
|
||||||
|
mux 失联全断都是 t_safety 直接关 eFuse,不经过 `power` 命令,而只有 `power off` 会顺带退刷机模式。
|
||||||
|
- 修:`cmd_chgmode_tick()`(执行器任务,chgmode 的唯一写者)每拍看一遍:刷机模式开着 + eFuse 关着 + 执行器不忙 ⇒
|
||||||
|
退出,记事件 21 arg = **254 `CHG_EV_DROPPED`**。执行器忙(硬重启 HR_OFF 那几秒 eFuse 本来就关)不算。
|
||||||
|
- ⚠️ 254 带 0x80 位:rack `api.py` 事件命名里「断电自动退出」必须排在「加钟」前面判(已改 + 测试)。
|
||||||
|
- 测试:`test_cmd` +1(硬重启中不退 / 关电后下一拍退、只记一次、名额让出);反向验证:去掉判断 ⇒ 红。26 ctest 全绿;rack 48 例全绿并已部署。
|
||||||
|
- ⏳ 真板未专门验证(需要带电拔一次开着刷机模式的刀片)。
|
||||||
|
|
||||||
|
## 0.1.11 / 0.1.12:命令随机 busy / io;硬重启带着 VBUS(2026-09-28 刷机时发现)
|
||||||
|
- **总线锁误判(0.1.11)**:`i2cbus_xact()` 拿锁前查全局 `s_in_xact` 防嵌套 —— 别的任务在事务里时它也是 true。
|
||||||
|
执行器撞上巡检那 ~2.3 ms(每 20 ms,≈12%)就被判成「嵌套」立刻回 STATE ⇒ `key` 随机回 **busy**、`vbus` 随机回 **io**
|
||||||
|
(反过来巡检撞上执行器也会白跑一圈)。改成 `hal_lock_held(HAL_LOCK_I2C)`(**本任务**持锁才算嵌套)。
|
||||||
|
真板:第 3 路连开关 VBUS 40 次,0 失败。测试 `test_i2cbus` +1(另一任务持锁 ⇒ TIMEOUT 而不是 STATE;旧代码红)。
|
||||||
|
- **硬重启先断 VBUS(0.1.11)+ 延后接回(0.1.12)**:VBUS 通着做 `power_cycle` 的三个后果现场全见过 ——
|
||||||
|
手机靠 USB 撑着没断电(假重启);直供电流 ≈0 ⇒ 开机校验判「没开起来」再按两下;断电那刻带着 USB ⇒ 进关机充电,一拔 USB 就关机。
|
||||||
|
现在 `t_actuator_hard_reset()` 先断 VBUS,**开机校验通过后再等 `ACT_HR_VBUS_BACK_MS` = 60 s** 才接回
|
||||||
|
(0.1.11 是校验一过就接回 ⇒ 手机还在 bootloader、关充电补丁没生效 ⇒ 13 s 顶高 4394 mV、SW-8 断 VBUS)。
|
||||||
|
等待期间执行器空闲;`vbus` / `power` 命令、再次硬重启、EDL、abort、这一路被关电 ⇒ 作废。没开起来 / 出错 ⇒ 不接回。
|
||||||
|
真板:第 3 路 VBUS 通 → 硬重启:hr-off 时 vb=false、14 s 校验通过、72 s 接回,无顶高事件。
|
||||||
|
测试 `test_t_actuator` +3(先断后接回、没开机不接回 / abort 不接回 / 本来没通不打开、等待期间被取消的三种)。26 ctest 全绿。
|
||||||
|
- ~~没修的:第 2 槽刀片被拔走时刷机模式不退出~~ ⇒ 0.1.13 已修。
|
||||||
|
|
||||||
|
## 0.1.10:热插拔进来的刀片重新初始化(2026-09-28)
|
||||||
|
- 现象(板 B,新主板 + 没刷补丁):刀片**带电插**进槽 ⇒ C67(VBUS)= 5 V,换 U5 也一样;`vb:false`。
|
||||||
|
没刷补丁的手机被充到 4.4 V ⇒ SR-5 过压关路(反复 7 次)。**插好再给底板上电就正常**。
|
||||||
|
- 根因:`slot_init()` / `ina226_init()` 只在开机 bringup 做。热插拔时 PCA9555 上电复位不干净(SDA/SCL 经上拉寄生供电),
|
||||||
|
`VBUS_EN` 被配成输出高、影子却是 off ⇒ SW-8 不武装;INA226 CALIB = 0 ⇒ **电流恒读 0**(第 2 路那段数据正是整 0)。
|
||||||
|
- 修:`t_safety` 认 `PRSNT#` 下降沿,在位连续 `T_SAFETY_INSERT_N` = 5 圈(100 ms)后在本路锁内 `slot_init_locked()` + `ina226_init()`;
|
||||||
|
失败隔 5 圈重试;拔走作废;开机在位的不重做。status `safety.inits` = 成功次数。
|
||||||
|
- 测试 +2(`test_t_safety`:乱寄存器插入 ⇒ 第 5 圈恢复、VBUS 断、键全松、CALIB 写回、只做一次;NAK 重试 + 插一半拔走不碰空槽);
|
||||||
|
反向验证:不置 need_init ⇒ 两例红。26 ctest 全绿。
|
||||||
|
- ✅ 真板验证(用户现场):带电插刀片后 C67 = 0 V、`safety.inits` 0 → 1。
|
||||||
|
|
||||||
|
## 0.1.9:多组 WiFi(2026-09-28,用户要求)
|
||||||
|
- 起因:板 B 只存一组 WiFi(CANBOX-5G),热点一关就离线;而改配置只收 USB。
|
||||||
|
- `netcfg_t.wifi[NETCFG_WIFI_N=4]`。`wifi` 命令 = **同名改口令、否则追加**(回 `"n":组数`;满了 `state`);
|
||||||
|
`{"cmd":"wifi","ssid":"X","del":true}` 删(没有这组回 `range`)。仍然只收 USB。
|
||||||
|
- `net` 回应:`ssid` / `pass_set` = 正在连的那一组(`net_status_t.wifi_idx`),新增 `"ssids":[…]` 整张表。
|
||||||
|
- 轮换(mlink_esp.c):没连上 ⇒ 换下一组;连上过又掉 ⇒ 先重试同一组;退避按「轮完一圈」算(`attempt / n`)。
|
||||||
|
改配置时正连着的那一组没被删、口令没变、broker 没变 ⇒ **WiFi / MQTT 都不断**(加备用热点不掉线)。
|
||||||
|
- NVS:新键 `net2`(ver 2);读不到就读旧键 `net`(ver 1)迁移。**写时两个键都写**,`net` 放表里第一组 ——
|
||||||
|
OTA 回滚到 ≤0.1.8 仍连得上网。
|
||||||
|
- 测试 +3(`test_net` 10 → 13 例:加 / 改同名、删 + 满表、NVS 空位压实);26 ctest 全绿。
|
||||||
|
- 真板:烧录后 `ssids:["CANBOX-5G"]`(迁移成功)→ 加 `AAAAAABBBCCC`(用户给的名字,周围扫不到)→
|
||||||
|
加 `AAABBBCCC`(本机扫到的实际热点,同口令)⇒ 连上 10.5.5.58、MQTT up、服务器显示在线。
|
||||||
|
- ⚠️ USB esptool 烧录 = 非计划重启 ⇒ 五路全断(第 1 路手机掉电)。第一次连接报 `ClearCommError`,COM5 重新枚举后重试即成。
|
||||||
|
|
||||||
|
## 0.1.7 / 0.1.8:反灌断 VBUS 阈值 −50 → −150 → −300 mA(2026-09-28,用户要求)
|
||||||
|
- 起因:刷机时重启过渡的瞬间反灌误断 VBUS,fastboot 断连。0.1.6 下实测 −59 mA;0.1.7(−150 mA)下 `adb reboot bootloader` 实测 **−208.5 mA** 仍断。
|
||||||
|
- `T_SAFETY_REV_UA` = −300000;判据本身(一圈就断、不做确认窗口、不碰 eFuse、刷机模式不豁免)不变。
|
||||||
|
- 测试:注入 −60 → −310 mA,「小电流不算反灌」−40 → −250 mA;26 ctest 全绿。0.1.8 下 `adb reboot bootloader` 过渡没再断 USB。
|
||||||
|
|
||||||
|
## 0.1.6:刷机模式默认 30 → 60 分钟 + 加钟(2026-09-28,用户要求)
|
||||||
|
- `CHG_MODE_DEFAULT_MIN` = 60。
|
||||||
|
- 加钟:`{"cmd":"flash_mode","rail":1,"add":30}`(1~120,与 on / min 互斥)—— `chgmode_extend()` 只给**开着的**窗口加
|
||||||
|
(没开 / 宽限期里回 `state`:那是重新开,不是加钟),加完剩余截断到 120 分钟;回 `"left":秒`。
|
||||||
|
事件 21 arg = `CHG_EV_EXTEND`(0x80)| 分钟。rack:「更多 → 刷机模式加钟 30 分钟」(只在开着时出现)。
|
||||||
|
- 测试 +2(`test_cmd` 加钟全路径、`test_t_safety_rules` 截断与宽限期拒绝),rack 48 例。真板:没开加钟被拒 / 开 3600 / 加钟后 5399 / 关。
|
||||||
|
|
||||||
|
## 0.1.5:刷机模式(2026-09-28,用户定「做,最多同时 2 路」)
|
||||||
|
- **起因**:fastboot / bootloader 阶段内核补丁还没起来,手机充电器把直供节点顶到浮充 ~4.4 V(板 B 实测 4395 mV),
|
||||||
|
SW-8 立刻断 VBUS ⇒ fastboot 刷到一半断连。历史 5 次顶高全是只断 VBUS、**0 次**关路。
|
||||||
|
- **依据**:每路 eFuse TPS25940A 自带反向阻断(V(IN)−V(OUT) < −9.3 mV 断开),顶高时能量进不了 Buck / 12V 母线;
|
||||||
|
板 B 五次顶高实测电流 0 ~ −0.5 mA。
|
||||||
|
- **新模块 `chgmode.{h,c}`**:每路一个截止时刻。**只有执行器任务写**(命令 + `cmd_chgmode_tick()` 到期清理),
|
||||||
|
巡检只读;tick 必须清标志,否则 32 位毫秒时钟 ~24.8 天后过期窗口会「复活」。
|
||||||
|
- **判据(`t_safety.c judge_locked`)**:窗口内「顶高断 VBUS」与 SR-5 过压都按绝对值 `CHG_MODE_MAX_MV` = 4450 判;
|
||||||
|
窗口后 1 s 宽限期里 SW-8 回到按设定值(立刻断 VBUS),SR-5 仍按绝对值(节点往回落的几 ms 不许误关路)。
|
||||||
|
反灌(< −50 mA)与欠压判据**不变**。
|
||||||
|
- **命令 14 `flash_mode`**:`{"rail":1,"on":true,"min":60}`(默认 60、1~120 分钟;0.1.6 由 30 改 60),回 `"left":秒`;
|
||||||
|
**最多同时 2 路**(第 3 路回 `state`);关着电不许开(`state`);`power` 关电时一并退出。status 每路多 `"chg":秒`。
|
||||||
|
- **事件 21 `LOG_EV_CHG_MODE`**:arg = 分钟(开)/ 0(手动关)/ 255(到期)。
|
||||||
|
- rack:白名单 + 事件名 + 单板页「更多 → 刷机模式(1 小时)」(带确认),状态标签显示剩余分钟;模拟板同步。
|
||||||
|
- 测试:`test_t_safety_rules` +10(绝对上限、反灌 / 欠压不变、按路、到期断 VBUS 不关路、宽限期后恢复、2 路上限、时钟回绕),
|
||||||
|
`test_cmd` +3;26 ctest 全绿;rack 46 例。反向验证:去掉窗口内豁免 → 4 例红;tick 不清标志 → 回绕用例红。
|
||||||
|
- ✅ **台架实测(2026-09-28,板 B 第 1 路,0.1.6-c5)**,原始数据 `10.5.5.13:/root/phonerail-src/bench-flashmode-20260928.tsv`:
|
||||||
|
· 开模式 + VBUS 通 → `adb reboot bootloader`,6 s 进 fastboot,**fastboot 下确实在充电**:节点 3864~4395 mV 锯齿
|
||||||
|
(充到浮充即停、手机耗下来再充,周期 3~4 s),**VBUS 全程没断**、fastboot 一直在线(`getvar` 正常);
|
||||||
|
· 该路电流 −1.0 ~ +1.5 mA(eFuse 反向阻断干净),12V 母线 12.029~12.031 V 不动,输入电流 160 → 230 mA(+0.8 W,5V 负担很小);
|
||||||
|
· **FLT 在反向阻断期间一直为 true**(与 RCB 同进同退,节点跌到 3864 仍为 true = 要 +100 mV 才重新导通),
|
||||||
|
固件对 FLT 只上报不动作 ⇒ 无事件。是 TPS25940A 的 RCB 指示,不是故障。rack 已改:chg>0 时这一路显示蓝色「刷机模式 · 充电中」、FLT 标签变中性「反向阻断」(模式外 FLT 仍标红);
|
||||||
|
· 对照:充电中关模式 → 当圈 PUSHED_UP 断 VBUS(读数 4034 mV),**不关路**(宽限期),Buck 立刻接手 3917 mV / 240 mA;FLT 同时消失;
|
||||||
|
· 重开模式 + VBUS → 2 s 回 fastboot。`fastboot getvar unlocked: yes`(BL 确已解锁);
|
||||||
|
· ⚠️ **`fastboot reboot` 回 MIUI 的开机过程中出现 −59 mA 反灌 ⇒ SW-8 断 VBUS**(事件 290)。机理:开机大负载时 Buck 在连续导通,
|
||||||
|
节点只比设定高几 mV、没到 eFuse 的 −9.3 mV 阻断门限,充电器灌进来的电被 Buck 反向吸收。12V 母线没动(12.029~12.031)。
|
||||||
|
**反灌判据保留是对的**;实际影响只是「重启过渡时 USB 可能被断,进系统后再开 VBUS」。
|
||||||
|
|
||||||
## 0.1.4:整片刀片不应答 ⇒ 关这一路(2026-09-28,用户拍板)
|
## 0.1.4:整片刀片不应答 ⇒ 关这一路(2026-09-28,用户拍板)
|
||||||
- `t_safety` 每路锁内:eFuse 开着、本路 INA226 **和** PCA9555 同时 NAK,连续 `T_SAFETY_BLADE_MUTE_N` = 10 圈(200 ms)
|
- `t_safety` 每路锁内:eFuse 开着、本路 INA226 **和** PCA9555 同时 NAK,连续 `T_SAFETY_BLADE_MUTE_N` = 10 圈(200 ms)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ set(PHONERAIL_CORE_SRCS
|
|||||||
${FW_ROOT}/main/slot.c
|
${FW_ROOT}/main/slot.c
|
||||||
${FW_ROOT}/main/sched.c
|
${FW_ROOT}/main/sched.c
|
||||||
${FW_ROOT}/main/tasks.c
|
${FW_ROOT}/main/tasks.c
|
||||||
|
${FW_ROOT}/main/chgmode.c
|
||||||
${FW_ROOT}/main/t_safety.c
|
${FW_ROOT}/main/t_safety.c
|
||||||
${FW_ROOT}/main/t_actuator.c
|
${FW_ROOT}/main/t_actuator.c
|
||||||
${FW_ROOT}/main/t_thermal.c
|
${FW_ROOT}/main/t_thermal.c
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ idf_component_register(
|
|||||||
"slot.c"
|
"slot.c"
|
||||||
"sched.c"
|
"sched.c"
|
||||||
"tasks.c"
|
"tasks.c"
|
||||||
|
"chgmode.c"
|
||||||
"t_safety.c"
|
"t_safety.c"
|
||||||
"t_actuator.c"
|
"t_actuator.c"
|
||||||
"t_thermal.c"
|
"t_thermal.c"
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ static void task_actuator(void *arg)
|
|||||||
static char reply[CMD_OUT_MAX]; /* 静态:1.5 KB 放栈上会把 4 KB 的栈吃掉一大半 */
|
static char reply[CMD_OUT_MAX]; /* 静态:1.5 KB 放栈上会把 4 KB 的栈吃掉一大半 */
|
||||||
cmd_msg_t m;
|
cmd_msg_t m;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
cmd_chgmode_tick(hal_time_now_ms()); /* 刷机模式到期清理 + 记事件(chgmode.h:只在本任务写) */
|
||||||
const TickType_t t = pdMS_TO_TICKS(t_actuator_step());
|
const TickType_t t = pdMS_TO_TICKS(t_actuator_step());
|
||||||
/* ⚠️ `CONFIG_FREERTOS_HZ=100` ⇒ 一个 tick 是 10 ms,`pdMS_TO_TICKS(5)`
|
/* ⚠️ `CONFIG_FREERTOS_HZ=100` ⇒ 一个 tick 是 10 ms,`pdMS_TO_TICKS(5)`
|
||||||
是 **0**,而「等 0 tick」只是查一眼就回来 —— 那会让这个中优先级任务
|
是 **0**,而「等 0 tick」只是查一眼就回来 —— 那会让这个中优先级任务
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/* 刷机模式的窗口表。契约与线程约定在 chgmode.h。 */
|
||||||
|
#include "chgmode.h"
|
||||||
|
|
||||||
|
#include "phonerail.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
volatile uint32_t until; /* 窗口截止时刻(ms,回绕安全比较) */
|
||||||
|
volatile uint8_t set; /* 1 = 窗口或宽限期还没被 tick 清掉 */
|
||||||
|
uint8_t by_cmd; /* 1 = 命令关掉的(已记过事件,tick 不再报「到期」);只有执行器任务读写 */
|
||||||
|
} chg_t;
|
||||||
|
|
||||||
|
static chg_t s_chg[PHONERAIL_RAIL_COUNT];
|
||||||
|
|
||||||
|
static bool rail_ok(int rail) { return rail >= 1 && rail <= PHONERAIL_RAIL_COUNT; }
|
||||||
|
|
||||||
|
/* `a` 在 `b` 之前(回绕安全)。 */
|
||||||
|
static bool before(uint32_t a, uint32_t b) { return (int32_t)(a - b) < 0; }
|
||||||
|
|
||||||
|
bool chgmode_active(int rail, uint32_t now)
|
||||||
|
{
|
||||||
|
return rail_ok(rail) && s_chg[rail - 1].set && before(now, s_chg[rail - 1].until);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool chgmode_relaxed(int rail, uint32_t now)
|
||||||
|
{
|
||||||
|
return rail_ok(rail) && s_chg[rail - 1].set &&
|
||||||
|
before(now, s_chg[rail - 1].until + CHG_MODE_GRACE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t chgmode_left_s(int rail, uint32_t now)
|
||||||
|
{
|
||||||
|
if (!chgmode_active(rail, now)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (s_chg[rail - 1].until - now + 999u) / 1000u;
|
||||||
|
}
|
||||||
|
|
||||||
|
int chgmode_count(uint32_t now)
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
for (int r = 1; r <= PHONERAIL_RAIL_COUNT; r++) {
|
||||||
|
n += chgmode_active(r, now) ? 1 : 0;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
hal_err_t chgmode_set(int rail, uint32_t minutes, uint32_t now)
|
||||||
|
{
|
||||||
|
if (!rail_ok(rail) || minutes > CHG_MODE_MAX_MIN) {
|
||||||
|
return HAL_ERR_ARG;
|
||||||
|
}
|
||||||
|
chg_t *c = &s_chg[rail - 1];
|
||||||
|
if (minutes == 0) {
|
||||||
|
if (chgmode_active(rail, now)) {
|
||||||
|
c->until = now; /* 立刻结束窗口,宽限期从现在算 */
|
||||||
|
c->by_cmd = 1;
|
||||||
|
}
|
||||||
|
return HAL_OK;
|
||||||
|
}
|
||||||
|
if (!chgmode_active(rail, now) && chgmode_count(now) >= CHG_MODE_MAX_RAILS) {
|
||||||
|
return HAL_ERR_STATE;
|
||||||
|
}
|
||||||
|
/* 先写时刻、后置标志:巡检任务读到 set=1 时 until 一定是新的。 */
|
||||||
|
c->until = now + minutes * 60000u;
|
||||||
|
c->by_cmd = 0;
|
||||||
|
c->set = 1;
|
||||||
|
return HAL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
hal_err_t chgmode_extend(int rail, uint32_t minutes, uint32_t now)
|
||||||
|
{
|
||||||
|
if (!rail_ok(rail) || minutes < 1 || minutes > CHG_MODE_MAX_MIN) {
|
||||||
|
return HAL_ERR_ARG;
|
||||||
|
}
|
||||||
|
if (!chgmode_active(rail, now)) {
|
||||||
|
return HAL_ERR_STATE;
|
||||||
|
}
|
||||||
|
chg_t *c = &s_chg[rail - 1];
|
||||||
|
const uint32_t cap = CHG_MODE_MAX_MIN * 60000u;
|
||||||
|
uint32_t left = c->until - now + minutes * 60000u; /* 窗口内 until − now 为正 */
|
||||||
|
if (left > cap) {
|
||||||
|
left = cap;
|
||||||
|
}
|
||||||
|
c->until = now + left; /* set 已是 1;单字写,巡检看到的要么旧要么新 */
|
||||||
|
return HAL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t chgmode_tick(uint32_t now)
|
||||||
|
{
|
||||||
|
uint32_t ended = 0;
|
||||||
|
for (int r = 1; r <= PHONERAIL_RAIL_COUNT; r++) {
|
||||||
|
chg_t *c = &s_chg[r - 1];
|
||||||
|
if (c->set && !before(now, c->until + CHG_MODE_GRACE_MS)) {
|
||||||
|
c->set = 0;
|
||||||
|
if (!c->by_cmd) {
|
||||||
|
ended |= 1u << (r - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ended;
|
||||||
|
}
|
||||||
|
|
||||||
|
void chgmode_reset(void)
|
||||||
|
{
|
||||||
|
for (int r = 0; r < PHONERAIL_RAIL_COUNT; r++) {
|
||||||
|
s_chg[r].set = 0;
|
||||||
|
s_chg[r].until = 0;
|
||||||
|
s_chg[r].by_cmd = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/* 刷机模式(0.1.5,2026-09-28 用户定):某一路 VBUS 通着时**允许手机充电、不断 USB**。
|
||||||
|
|
||||||
|
为什么要有它:fastboot / bootloader 阶段我们的内核补丁还没起来,手机充电器会把电池端
|
||||||
|
(我们的直供节点)顶到它的浮充电压 ~4.4 V。平时 SW-8 会立刻断 VBUS(事件 PUSHED_UP),
|
||||||
|
刷到一半的 fastboot 就断了。
|
||||||
|
|
||||||
|
为什么放得开:
|
||||||
|
· 每路 eFuse(TPS25940A)自带**反向阻断**(V(IN)−V(OUT) < −9.3 mV 断开)——
|
||||||
|
充电器顶高节点时 eFuse 自己断开,能量进不了 Buck、到不了 12V 母线。
|
||||||
|
2026-09-28 板 B 五次顶高实测电流 0 ~ −0.5 mA,印证了这一点;
|
||||||
|
· 4.4 V 是小米8 高压电芯 + 手机充电芯片自己的设计点,保护板已旁路。
|
||||||
|
|
||||||
|
刷机模式下(这一路、窗口内)t_safety 的判据改成:
|
||||||
|
| 判据 | 平时 | 刷机模式 |
|
||||||
|
|---|---|---|
|
||||||
|
| SW-8 顶高断 VBUS | > 设定 +100 mV | > CHG_MODE_MAX_MV(绝对值) |
|
||||||
|
| SR-5 过压关路 | > 设定 +100 mV 持续 20 ms | > CHG_MODE_MAX_MV 持续 20 ms |
|
||||||
|
| SW-8 反灌断 VBUS | I < −300 mA | **不变**(eFuse 之外的第二道) |
|
||||||
|
| SR-5 欠压关路 | < 设定 −250 mV | **不变** |
|
||||||
|
窗口结束后再留 CHG_MODE_GRACE_MS 的「过压仍按绝对值判」:窗口一过 SW-8 立刻断 VBUS,
|
||||||
|
节点从 4.4 V 往回落要几毫秒,不能被 SR-5 当成过压把手机关掉。
|
||||||
|
|
||||||
|
**最多同时 CHG_MODE_MAX_RAILS 路**:下行 VBUS 由板上一片 3 A 的 5V(U30)供,
|
||||||
|
一台充电 0.5 A(SDP)~1.5 A(CDP),五台一起会把 5V 拉垮、全部 USB 掉线。
|
||||||
|
|
||||||
|
---- 线程 ----------------------------------------------------------------
|
||||||
|
**只有执行器任务写**(`chgmode_set()` 由命令处理器调,`chgmode_tick()` 在执行器循环里调,
|
||||||
|
两者同一个任务),巡检任务(t_safety)**只读**。每路状态是一个 32 位截止时刻 + 一个字节标志,
|
||||||
|
单字读写本身原子;「先写时刻、后置标志」保证巡检不会读到半截。
|
||||||
|
为什么要 tick 清标志:时钟是 32 位毫秒,过期的窗口放着不管,~24.8 天后
|
||||||
|
`(int32_t)(until - now)` 会翻回正数,窗口「复活」。 */
|
||||||
|
#ifndef CHGMODE_H
|
||||||
|
#define CHGMODE_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "hal_err.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define CHG_MODE_MAX_RAILS 2
|
||||||
|
#define CHG_MODE_DEFAULT_MIN 60 /* 2026-09-28 用户改:30 → 60 */
|
||||||
|
#define CHG_MODE_MAX_MIN 120
|
||||||
|
#define CHG_MODE_MAX_MV 4450 /* 手机充电器浮充 ~4.4 V(实测 4395)+ 余量;超过 = 充电器异常 */
|
||||||
|
#define CHG_MODE_GRACE_MS 1000
|
||||||
|
|
||||||
|
/* LOG_EV_CHG_MODE 的 arg(开的时候是分钟数 1~120)。⚠️ 进 flash,冻结。
|
||||||
|
加钟:CHG_EV_EXTEND | 加的分钟数(1~120 ⇒ 129~248)。 */
|
||||||
|
#define CHG_EV_OFF 0u
|
||||||
|
#define CHG_EV_EXTEND 0x80u
|
||||||
|
#define CHG_EV_EXPIRED 255u
|
||||||
|
#define CHG_EV_DROPPED 254u /* 0.1.13:这一路断了电(刀片拔走 / 安全判据关路),自动退出 */
|
||||||
|
|
||||||
|
/* 开 / 关。minutes = 0 关(进入宽限期),1~CHG_MODE_MAX_MIN 开(覆盖原窗口)。
|
||||||
|
HAL_ERR_ARG —— 路号 / 分钟越界;
|
||||||
|
HAL_ERR_STATE —— 要开第 CHG_MODE_MAX_RAILS+1 路(本路已开着的续期不算新增)。 */
|
||||||
|
hal_err_t chgmode_set(int rail, uint32_t minutes, uint32_t now);
|
||||||
|
|
||||||
|
/* 加钟(0.1.6):在**正开着**的窗口上再加 minutes(1~CHG_MODE_MAX_MIN)。
|
||||||
|
加完剩余时间超过 CHG_MODE_MAX_MIN 的按它截断(「最长 120 分钟」是剩余时间的上限,不是一次窗口的)。
|
||||||
|
HAL_ERR_ARG —— 路号 / 分钟越界;
|
||||||
|
HAL_ERR_STATE —— 这一路没开着(过期了 / 宽限期里也算没开:要重新开,不是加钟)。 */
|
||||||
|
hal_err_t chgmode_extend(int rail, uint32_t minutes, uint32_t now);
|
||||||
|
|
||||||
|
/* 过期清理(执行器循环里调,每拍一次即可)。返回本次**自然到期**的路的位图(bit0 = CH1),
|
||||||
|
调用方据此记事件;被命令关掉的不在其中(关的那一刻命令层已经记过)。宽限期结束才清标志。 */
|
||||||
|
uint32_t chgmode_tick(uint32_t now);
|
||||||
|
|
||||||
|
/* 窗口内:放开「顶高断 VBUS」。 */
|
||||||
|
bool chgmode_active(int rail, uint32_t now);
|
||||||
|
/* 窗口内或宽限期内:过压按 CHG_MODE_MAX_MV 绝对值判。 */
|
||||||
|
bool chgmode_relaxed(int rail, uint32_t now);
|
||||||
|
/* 剩余秒数(不在窗口内为 0)。status 上报用。 */
|
||||||
|
uint32_t chgmode_left_s(int rail, uint32_t now);
|
||||||
|
/* 窗口内的路数。 */
|
||||||
|
int chgmode_count(uint32_t now);
|
||||||
|
|
||||||
|
void chgmode_reset(void); /* 全关(上电 / 宿主机测试) */
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* CHGMODE_H */
|
||||||
+2
-2
@@ -13,7 +13,7 @@ static const char *const k_names[CMD_COUNT] = {
|
|||||||
[CMD_POWER_CYCLE] = "power_cycle", [CMD_KEY] = "key", [CMD_EDL] = "edl",
|
[CMD_POWER_CYCLE] = "power_cycle", [CMD_KEY] = "key", [CMD_EDL] = "edl",
|
||||||
[CMD_VBUS] = "vbus", [CMD_FAN] = "fan", [CMD_EVENTS] = "events",
|
[CMD_VBUS] = "vbus", [CMD_FAN] = "fan", [CMD_EVENTS] = "events",
|
||||||
[CMD_POWER] = "power", [CMD_WIFI] = "wifi", [CMD_MQTT] = "mqtt",
|
[CMD_POWER] = "power", [CMD_WIFI] = "wifi", [CMD_MQTT] = "mqtt",
|
||||||
[CMD_NET] = "net", [CMD_OTA] = "ota",
|
[CMD_NET] = "net", [CMD_OTA] = "ota", [CMD_FLASH_MODE] = "flash_mode",
|
||||||
};
|
};
|
||||||
|
|
||||||
static const char *const k_errs[CMD_ERR_COUNT] = {
|
static const char *const k_errs[CMD_ERR_COUNT] = {
|
||||||
@@ -105,7 +105,7 @@ static const handler_t k_handlers[CMD_COUNT] = {
|
|||||||
[CMD_POWER_CYCLE] = do_power_cycle, [CMD_KEY] = do_key, [CMD_EDL] = do_edl,
|
[CMD_POWER_CYCLE] = do_power_cycle, [CMD_KEY] = do_key, [CMD_EDL] = do_edl,
|
||||||
[CMD_VBUS] = do_vbus, [CMD_FAN] = do_fan, [CMD_EVENTS] = do_events,
|
[CMD_VBUS] = do_vbus, [CMD_FAN] = do_fan, [CMD_EVENTS] = do_events,
|
||||||
[CMD_POWER] = do_power, [CMD_WIFI] = do_wifi, [CMD_MQTT] = do_mqtt,
|
[CMD_POWER] = do_power, [CMD_WIFI] = do_wifi, [CMD_MQTT] = do_mqtt,
|
||||||
[CMD_NET] = do_net, [CMD_OTA] = do_ota,
|
[CMD_NET] = do_net, [CMD_OTA] = do_ota, [CMD_FLASH_MODE] = do_flash_mode,
|
||||||
};
|
};
|
||||||
|
|
||||||
static cmd_id_t lookup(const char *name)
|
static cmd_id_t lookup(const char *name)
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ typedef enum {
|
|||||||
CMD_MQTT = 11, /* A14/C1:写 broker 地址 / 账号 / 遥测周期(只收 USB) */
|
CMD_MQTT = 11, /* A14/C1:写 broker 地址 / 账号 / 遥测周期(只收 USB) */
|
||||||
CMD_NET = 12, /* A14:网络状态(只读,不回显密码) */
|
CMD_NET = 12, /* A14:网络状态(只读,不回显密码) */
|
||||||
CMD_OTA = 13, /* C2:从 URL 下载固件、校验 sha256、切启动分区(ota.h) */
|
CMD_OTA = 13, /* C2:从 URL 下载固件、校验 sha256、切启动分区(ota.h) */
|
||||||
|
CMD_FLASH_MODE = 14, /* 0.1.5:刷机模式——VBUS 通着时允许手机充电、不断 USB(chgmode.h) */
|
||||||
CMD_COUNT,
|
CMD_COUNT,
|
||||||
} cmd_id_t;
|
} cmd_id_t;
|
||||||
|
|
||||||
@@ -138,6 +139,10 @@ typedef struct {
|
|||||||
} cmd_stats_t;
|
} cmd_stats_t;
|
||||||
void cmd_stats(cmd_stats_t *st);
|
void cmd_stats(cmd_stats_t *st);
|
||||||
|
|
||||||
|
/* 刷机模式到期清理 + 记事件(LOG_EV_CHG_MODE,arg = CHG_EV_EXPIRED)。**只许在执行器任务里调**
|
||||||
|
(chgmode.h:窗口表只有这个任务写),app_main 每转一圈调一次。 */
|
||||||
|
void cmd_chgmode_tick(uint32_t now);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
判重、名字表、回应拼装在 cmd.c;status / events 的正文在 cmd_report.c。 */
|
判重、名字表、回应拼装在 cmd.c;status / events 的正文在 cmd_report.c。 */
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "chgmode.h"
|
||||||
#include "cmd_priv.h"
|
#include "cmd_priv.h"
|
||||||
#include "evlog.h"
|
#include "evlog.h"
|
||||||
|
#include "hal_time.h"
|
||||||
#include "i2cbus.h"
|
#include "i2cbus.h"
|
||||||
#include "rail.h"
|
#include "rail.h"
|
||||||
#include "rail_cfg.h"
|
#include "rail_cfg.h"
|
||||||
@@ -209,6 +211,7 @@ cmd_err_t do_vbus(ctx_t *c)
|
|||||||
if (on && !rail_efuse_is_on(c->rail)) {
|
if (on && !rail_efuse_is_on(c->rail)) {
|
||||||
return reject(c, CMD_ERR_STATE, 1);
|
return reject(c, CMD_ERR_STATE, 1);
|
||||||
}
|
}
|
||||||
|
t_actuator_cancel_vbus_back(c->rail); /* 人动了 VBUS:硬重启排的「过会儿接回」作废 */
|
||||||
return from_bus(slot_vbus_enable(c->rail, on));
|
return from_bus(slot_vbus_enable(c->rail, on));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,6 +237,7 @@ cmd_err_t do_power(ctx_t *c)
|
|||||||
if (t_actuator_busy(c->rail)) {
|
if (t_actuator_busy(c->rail)) {
|
||||||
return CMD_ERR_BUSY; /* 硬重启走到一半再开关一次是自作主张(t_actuator_abort 同理) */
|
return CMD_ERR_BUSY; /* 硬重启走到一半再开关一次是自作主张(t_actuator_abort 同理) */
|
||||||
}
|
}
|
||||||
|
t_actuator_cancel_vbus_back(c->rail); /* 人动了电:硬重启排的「过会儿接回 VBUS」作废 */
|
||||||
if (on) {
|
if (on) {
|
||||||
const hal_err_t ee = rail_efuse_enable(c->rail, true);
|
const hal_err_t ee = rail_efuse_enable(c->rail, true);
|
||||||
if (ee == HAL_OK) {
|
if (ee == HAL_OK) {
|
||||||
@@ -248,10 +252,113 @@ cmd_err_t do_power(ctx_t *c)
|
|||||||
const hal_err_t ee = rail_efuse_enable(c->rail, false);
|
const hal_err_t ee = rail_efuse_enable(c->rail, false);
|
||||||
if (ee == HAL_OK) {
|
if (ee == HAL_OK) {
|
||||||
note_intent(c->rail, false); /* 直供确实断了:下次开机也别开它 */
|
note_intent(c->rail, false); /* 直供确实断了:下次开机也别开它 */
|
||||||
|
/* 这一路停了,刷机模式也一并退出(不占 CHG_MODE_MAX_RAILS 的名额)。 */
|
||||||
|
if (chgmode_active(c->rail, hal_time_now_ms())) {
|
||||||
|
(void)chgmode_set(c->rail, 0, hal_time_now_ms());
|
||||||
|
evlog_put((uint8_t)c->rail, LOG_EV_CHG_MODE, CHG_EV_OFF, now_mv(c->rail), now_ua(c->rail));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return from_bus(ee != HAL_OK ? ee : ev);
|
return from_bus(ee != HAL_OK ? ee : ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 0.1.5 刷机模式(chgmode.h):`{"cmd":"flash_mode","rail":1,"on":true,"min":60}`。
|
||||||
|
min 省略 = CHG_MODE_DEFAULT_MIN;关不带 min。回应带 `"left":秒`。
|
||||||
|
0.1.6 加钟:`{"cmd":"flash_mode","rail":1,"add":30}`(与 on / min 互斥)—— 只给开着的窗口加,
|
||||||
|
剩余最多 CHG_MODE_MAX_MIN 分钟(超出截断);没开着回 state。
|
||||||
|
⚠️ 只改判据,**不动 VBUS**:开 VBUS 仍用 `vbus` 命令(顺序无所谓:先开模式再开 VBUS,
|
||||||
|
或 VBUS 被 SW-8 断了之后开模式再重开 VBUS)。 */
|
||||||
|
cmd_err_t do_flash_mode(ctx_t *c)
|
||||||
|
{
|
||||||
|
cmd_err_t e = need_rail(c);
|
||||||
|
if (e != CMD_OK) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
/* 加钟(0.1.6):`{"cmd":"flash_mode","rail":1,"add":30}`,与 on 二选一。 */
|
||||||
|
int64_t add = 0;
|
||||||
|
const int ga = cj_get_int(c->o, "add", &add);
|
||||||
|
if (ga < 0) {
|
||||||
|
return CMD_ERR_ARG;
|
||||||
|
}
|
||||||
|
if (ga == 1) {
|
||||||
|
bool on_probe = false;
|
||||||
|
int64_t min_probe = 0;
|
||||||
|
if (cj_get_bool(c->o, "on", &on_probe) != 0 || cj_get_int(c->o, "min", &min_probe) != 0) {
|
||||||
|
return CMD_ERR_ARG; /* add 不和 on / min 混用 */
|
||||||
|
}
|
||||||
|
if (add < 1 || add > CHG_MODE_MAX_MIN) {
|
||||||
|
return reject(c, CMD_ERR_RANGE, add);
|
||||||
|
}
|
||||||
|
const uint32_t now = hal_time_now_ms();
|
||||||
|
const hal_err_t he = chgmode_extend(c->rail, (uint32_t)add, now);
|
||||||
|
if (he == HAL_ERR_STATE) {
|
||||||
|
return reject(c, CMD_ERR_STATE, 0); /* 没开着:要重新开,不是加钟 */
|
||||||
|
}
|
||||||
|
if (he != HAL_OK) {
|
||||||
|
return reject(c, CMD_ERR_RANGE, add);
|
||||||
|
}
|
||||||
|
evlog_put((uint8_t)c->rail, LOG_EV_CHG_MODE, (uint8_t)(CHG_EV_EXTEND | (uint32_t)add),
|
||||||
|
now_mv(c->rail), now_ua(c->rail));
|
||||||
|
cj_w(c->w, ",\"left\":%lu", (unsigned long)chgmode_left_s(c->rail, now));
|
||||||
|
return CMD_OK;
|
||||||
|
}
|
||||||
|
bool on = false;
|
||||||
|
if (cj_get_bool(c->o, "on", &on) != 1) {
|
||||||
|
return CMD_ERR_ARG;
|
||||||
|
}
|
||||||
|
int64_t min = CHG_MODE_DEFAULT_MIN;
|
||||||
|
const int g = cj_get_int(c->o, "min", &min);
|
||||||
|
if (g < 0 || (!on && g == 1)) {
|
||||||
|
return CMD_ERR_ARG;
|
||||||
|
}
|
||||||
|
const uint32_t now = hal_time_now_ms();
|
||||||
|
if (!on) {
|
||||||
|
const bool was = chgmode_active(c->rail, now);
|
||||||
|
(void)chgmode_set(c->rail, 0, now);
|
||||||
|
if (was) {
|
||||||
|
evlog_put((uint8_t)c->rail, LOG_EV_CHG_MODE, CHG_EV_OFF, now_mv(c->rail), now_ua(c->rail));
|
||||||
|
}
|
||||||
|
cj_w(c->w, ",\"left\":0");
|
||||||
|
return CMD_OK;
|
||||||
|
}
|
||||||
|
if (min < 1 || min > CHG_MODE_MAX_MIN) {
|
||||||
|
return reject(c, CMD_ERR_RANGE, min);
|
||||||
|
}
|
||||||
|
/* 关着电的路开刷机模式没有意义(手机没电),与 vbus 命令同一条规矩。 */
|
||||||
|
if (!rail_efuse_is_on(c->rail)) {
|
||||||
|
return reject(c, CMD_ERR_STATE, 1);
|
||||||
|
}
|
||||||
|
const hal_err_t he = chgmode_set(c->rail, (uint32_t)min, now);
|
||||||
|
if (he == HAL_ERR_STATE) {
|
||||||
|
return reject(c, CMD_ERR_STATE, chgmode_count(now)); /* 已有 CHG_MODE_MAX_RAILS 路开着 */
|
||||||
|
}
|
||||||
|
if (he != HAL_OK) {
|
||||||
|
return reject(c, CMD_ERR_RANGE, min);
|
||||||
|
}
|
||||||
|
evlog_put((uint8_t)c->rail, LOG_EV_CHG_MODE, (uint8_t)min, now_mv(c->rail), now_ua(c->rail));
|
||||||
|
cj_w(c->w, ",\"left\":%lu", (unsigned long)chgmode_left_s(c->rail, now));
|
||||||
|
return CMD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cmd_chgmode_tick(uint32_t now)
|
||||||
|
{
|
||||||
|
const uint32_t ended = chgmode_tick(now);
|
||||||
|
for (int r = 1; r <= PHONERAIL_RAIL_COUNT; r++) {
|
||||||
|
if ((ended & (1u << (r - 1))) != 0) {
|
||||||
|
evlog_put((uint8_t)r, LOG_EV_CHG_MODE, CHG_EV_EXPIRED, now_mv(r), now_ua(r));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 0.1.13:这一路已经断了电(刀片被拔走 / SR-5 等判据关路 / mux 失联全断 —— 都是 t_safety 关的 eFuse,
|
||||||
|
不经过 `power` 命令)⇒ 刷机模式一并退出。不退的话它一直占着 CHG_MODE_MAX_RAILS 的名额
|
||||||
|
(2026-09-28 板 B:第 2 槽刀片拔走后还挂着 48 分钟),插回来上电时还带着放宽的判据。
|
||||||
|
硬重启那几秒 eFuse 本来就关着,不算(执行器忙)。 */
|
||||||
|
for (int r = 1; r <= PHONERAIL_RAIL_COUNT; r++) {
|
||||||
|
if (chgmode_active(r, now) && !rail_efuse_is_on(r) && !t_actuator_busy(r)) {
|
||||||
|
(void)chgmode_set(r, 0, now);
|
||||||
|
evlog_put((uint8_t)r, LOG_EV_CHG_MODE, CHG_EV_DROPPED, now_mv(r), now_ua(r));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cmd_err_t do_fan(ctx_t *c)
|
cmd_err_t do_fan(ctx_t *c)
|
||||||
{
|
{
|
||||||
int64_t duty = 0;
|
int64_t duty = 0;
|
||||||
|
|||||||
+38
-4
@@ -33,14 +33,38 @@ cmd_err_t do_wifi(ctx_t *c)
|
|||||||
}
|
}
|
||||||
const char *ssid = NULL;
|
const char *ssid = NULL;
|
||||||
const char *pass = NULL;
|
const char *pass = NULL;
|
||||||
if (get_str(c, "ssid", true, &ssid) != CMD_OK || get_str(c, "pass", false, &pass) != CMD_OK) {
|
bool del = false;
|
||||||
|
if (get_str(c, "ssid", true, &ssid) != CMD_OK || get_str(c, "pass", false, &pass) != CMD_OK ||
|
||||||
|
cj_get_bool(c->o, "del", &del) < 0) {
|
||||||
return CMD_ERR_ARG;
|
return CMD_ERR_ARG;
|
||||||
}
|
}
|
||||||
|
netcfg_t cfg;
|
||||||
|
netcfg_get(&cfg);
|
||||||
|
hal_err_t e;
|
||||||
|
if (del) {
|
||||||
|
/* 删:`{"cmd":"wifi","ssid":"X","del":true}`。没有这一组 = range(记现有组数)。 */
|
||||||
|
if (netcfg_wifi_find(&cfg, ssid) < 0) {
|
||||||
|
c->bad = netcfg_wifi_count(&cfg);
|
||||||
|
return CMD_ERR_RANGE;
|
||||||
|
}
|
||||||
|
e = netcfg_del_wifi(ssid);
|
||||||
|
} else {
|
||||||
if (!netcfg_wifi_ok(ssid, pass)) {
|
if (!netcfg_wifi_ok(ssid, pass)) {
|
||||||
c->bad = (int64_t)strlen(pass); /* 最常见的错是口令长度(<8),记长度不记内容 */
|
c->bad = (int64_t)strlen(pass); /* 最常见的错是口令长度(<8),记长度不记内容 */
|
||||||
return CMD_ERR_RANGE;
|
return CMD_ERR_RANGE;
|
||||||
}
|
}
|
||||||
return netcfg_set_wifi(ssid, pass) == HAL_OK ? CMD_OK : CMD_ERR_IO;
|
if (netcfg_wifi_find(&cfg, ssid) < 0 && netcfg_wifi_count(&cfg) >= NETCFG_WIFI_N) {
|
||||||
|
c->bad = NETCFG_WIFI_N; /* 满了:先删一组 */
|
||||||
|
return CMD_ERR_STATE;
|
||||||
|
}
|
||||||
|
e = netcfg_set_wifi(ssid, pass);
|
||||||
|
}
|
||||||
|
if (e != HAL_OK) {
|
||||||
|
return CMD_ERR_IO;
|
||||||
|
}
|
||||||
|
netcfg_get(&cfg);
|
||||||
|
cj_w(c->w, ",\"n\":%d", netcfg_wifi_count(&cfg));
|
||||||
|
return CMD_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_err_t do_mqtt(ctx_t *c)
|
cmd_err_t do_mqtt(ctx_t *c)
|
||||||
@@ -91,9 +115,19 @@ cmd_err_t do_net(ctx_t *c)
|
|||||||
|
|
||||||
cj_w(w, ",\"id\":");
|
cj_w(w, ",\"id\":");
|
||||||
w_str(w, st.id);
|
w_str(w, st.id);
|
||||||
|
/* `ssid` / `pass_set` = 正在连 / 已连上的那一组;`ssids` = 整张表(按轮换顺序)。 */
|
||||||
|
const int n = netcfg_wifi_count(&cfg);
|
||||||
|
const netcfg_wifi_t *cur = (st.wifi_idx < n) ? &cfg.wifi[st.wifi_idx] : &cfg.wifi[0];
|
||||||
cj_w(w, ",\"wifi\":\"%s\",\"ssid\":", net_state_str(st.wifi));
|
cj_w(w, ",\"wifi\":\"%s\",\"ssid\":", net_state_str(st.wifi));
|
||||||
w_str(w, cfg.ssid);
|
w_str(w, cur->ssid);
|
||||||
cj_w(w, ",\"pass_set\":%s,\"ip\":", cfg.pass[0] ? "true" : "false");
|
cj_w(w, ",\"pass_set\":%s,\"ssids\":[", cur->pass[0] ? "true" : "false");
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
cj_w(w, ",");
|
||||||
|
}
|
||||||
|
w_str(w, cfg.wifi[i].ssid);
|
||||||
|
}
|
||||||
|
cj_w(w, "],\"ip\":");
|
||||||
w_str(w, st.ip);
|
w_str(w, st.ip);
|
||||||
cj_w(w, ",\"rssi\":%d,\"wifi_reason\":%u,\"mqtt\":\"%s\",\"uri\":", (int)st.rssi,
|
cj_w(w, ",\"rssi\":%d,\"wifi_reason\":%u,\"mqtt\":\"%s\",\"uri\":", (int)st.rssi,
|
||||||
(unsigned)st.wifi_reason, net_state_str(st.mqtt));
|
(unsigned)st.wifi_reason, net_state_str(st.mqtt));
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ cmd_err_t do_edl(ctx_t *c);
|
|||||||
cmd_err_t do_vbus(ctx_t *c);
|
cmd_err_t do_vbus(ctx_t *c);
|
||||||
cmd_err_t do_power(ctx_t *c);
|
cmd_err_t do_power(ctx_t *c);
|
||||||
cmd_err_t do_fan(ctx_t *c);
|
cmd_err_t do_fan(ctx_t *c);
|
||||||
|
cmd_err_t do_flash_mode(ctx_t *c);
|
||||||
|
|
||||||
/* cmd_net.c:网络配置(只收 USB)与网络状态。 */
|
/* cmd_net.c:网络配置(只收 USB)与网络状态。 */
|
||||||
cmd_err_t do_wifi(ctx_t *c);
|
cmd_err_t do_wifi(ctx_t *c);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
#include "board_pins.h"
|
#include "board_pins.h"
|
||||||
|
#include "chgmode.h"
|
||||||
#include "cmd_priv.h"
|
#include "cmd_priv.h"
|
||||||
#include "evlog.h"
|
#include "evlog.h"
|
||||||
#include "hal_gpio.h"
|
#include "hal_gpio.h"
|
||||||
@@ -69,6 +70,8 @@ static void rail_obj(cj_writer_t *w, int rail, const safety_snap_t *s)
|
|||||||
cj_w(w, ",\"pg\":null,\"flt\":null");
|
cj_w(w, ",\"pg\":null,\"flt\":null");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* 0.1.5:刷机模式剩余秒数(0 = 没开)。 */
|
||||||
|
cj_w(w, ",\"chg\":%lu", (unsigned long)chgmode_left_s(rail, hal_time_now_ms()));
|
||||||
cj_w(w, ",\"st\":\"%s\",\"res\":\"%s\"}", t_actuator_state_str(t_actuator_state(rail)),
|
cj_w(w, ",\"st\":\"%s\",\"res\":\"%s\"}", t_actuator_state_str(t_actuator_state(rail)),
|
||||||
t_actuator_result_str(t_actuator_last_result(rail)));
|
t_actuator_result_str(t_actuator_last_result(rail)));
|
||||||
}
|
}
|
||||||
@@ -131,9 +134,10 @@ void cmd_report_status(cj_writer_t *w, const cmd_env_t *env)
|
|||||||
thermal_obj(w, env->thermal);
|
thermal_obj(w, env->thermal);
|
||||||
}
|
}
|
||||||
if (s != NULL) {
|
if (s != NULL) {
|
||||||
cj_w(w, ",\"safety\":{\"cycles\":%lu,\"skipped\":%lu,\"i2c_err\":%lu,\"events\":%lu}",
|
cj_w(w, ",\"safety\":{\"cycles\":%lu,\"skipped\":%lu,\"i2c_err\":%lu,\"events\":%lu,\"inits\":%lu}",
|
||||||
(unsigned long)s->cycles, (unsigned long)s->skipped,
|
(unsigned long)s->cycles, (unsigned long)s->skipped,
|
||||||
(unsigned long)s->i2c_errors, (unsigned long)s->events);
|
(unsigned long)s->i2c_errors, (unsigned long)s->events,
|
||||||
|
(unsigned long)s->blade_inits);
|
||||||
}
|
}
|
||||||
ota_obj(w);
|
ota_obj(w);
|
||||||
evlog_stats_t es;
|
evlog_stats_t es;
|
||||||
|
|||||||
@@ -575,6 +575,7 @@ const char *evlog_ev_str(log_ev_t ev)
|
|||||||
case LOG_EV_PG_TIMEOUT: return "上电等不到PG";
|
case LOG_EV_PG_TIMEOUT: return "上电等不到PG";
|
||||||
case LOG_EV_OTA: return "固件升级";
|
case LOG_EV_OTA: return "固件升级";
|
||||||
case LOG_EV_BOOT_CHECK: return "开机重按";
|
case LOG_EV_BOOT_CHECK: return "开机重按";
|
||||||
|
case LOG_EV_CHG_MODE: return "刷机模式";
|
||||||
case LOG_EV_BLADE_MUTE: return "刀片不应答关路";
|
case LOG_EV_BLADE_MUTE: return "刀片不应答关路";
|
||||||
case LOG_EV_COUNT: break;
|
case LOG_EV_COUNT: break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ typedef enum {
|
|||||||
出现 = 这一槽 `PRSNT#` 假在位,或刀片的 I²C 整个断了。 */
|
出现 = 这一槽 `PRSNT#` 假在位,或刀片的 I²C 整个断了。 */
|
||||||
LOG_EV_BLADE_MUTE = 20,
|
LOG_EV_BLADE_MUTE = 20,
|
||||||
|
|
||||||
|
/* 0.1.5 新增:刷机模式(chgmode.h)开 / 关 / 到期。arg = 分钟数(1~120,开),
|
||||||
|
0 = 命令关(CHG_EV_OFF),255 = 到期(CHG_EV_EXPIRED)。bus_mv / current_ua 是当时这一路的读数。 */
|
||||||
|
LOG_EV_CHG_MODE = 21,
|
||||||
|
|
||||||
LOG_EV_COUNT,
|
LOG_EV_COUNT,
|
||||||
} log_ev_t;
|
} log_ev_t;
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -19,7 +19,6 @@
|
|||||||
#define I2CBUS_RST_MS 1
|
#define I2CBUS_RST_MS 1
|
||||||
|
|
||||||
static bool s_inited;
|
static bool s_inited;
|
||||||
static bool s_in_xact; /* 嵌套检测。单核上够用,双核上外面还有锁挡着 */
|
|
||||||
static int s_ch = I2CBUS_CH_UNKNOWN;
|
static int s_ch = I2CBUS_CH_UNKNOWN;
|
||||||
static uint32_t s_selects;
|
static uint32_t s_selects;
|
||||||
static uint32_t s_select_fails;
|
static uint32_t s_select_fails;
|
||||||
@@ -60,7 +59,6 @@ static hal_err_t select_ch(int ch)
|
|||||||
hal_err_t i2cbus_init(void)
|
hal_err_t i2cbus_init(void)
|
||||||
{
|
{
|
||||||
s_inited = false;
|
s_inited = false;
|
||||||
s_in_xact = false;
|
|
||||||
s_ch = I2CBUS_CH_UNKNOWN;
|
s_ch = I2CBUS_CH_UNKNOWN;
|
||||||
s_selects = 0;
|
s_selects = 0;
|
||||||
s_select_fails = 0;
|
s_select_fails = 0;
|
||||||
@@ -102,8 +100,12 @@ hal_err_t i2cbus_xact(int ch, uint32_t timeout_ms, i2cbus_fn_t fn, void *arg)
|
|||||||
if (!s_inited || fn == NULL) {
|
if (!s_inited || fn == NULL) {
|
||||||
return HAL_ERR_STATE;
|
return HAL_ERR_STATE;
|
||||||
}
|
}
|
||||||
if (s_in_xact) {
|
/* 嵌套 = **本任务**已经持着这把锁(回调里又调了一次,头文件末尾那条警告)。
|
||||||
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) {
|
if (ch < I2CBUS_CH_TRUNK || ch >= TCA9548A_CH_COUNT) {
|
||||||
return HAL_ERR_ARG;
|
return HAL_ERR_ARG;
|
||||||
@@ -115,12 +117,10 @@ hal_err_t i2cbus_xact(int ch, uint32_t timeout_ms, i2cbus_fn_t fn, void *arg)
|
|||||||
return HAL_ERR_TIMEOUT; /* 这一次什么也没做 —— 调用方决定重试还是跳过 */
|
return HAL_ERR_TIMEOUT; /* 这一次什么也没做 —— 调用方决定重试还是跳过 */
|
||||||
}
|
}
|
||||||
|
|
||||||
s_in_xact = true;
|
|
||||||
err = select_ch(ch);
|
err = select_ch(ch);
|
||||||
if (err == HAL_OK) {
|
if (err == HAL_OK) {
|
||||||
err = fn(arg);
|
err = fn(arg);
|
||||||
}
|
}
|
||||||
s_in_xact = false;
|
|
||||||
|
|
||||||
hal_lock_give(HAL_LOCK_I2C);
|
hal_lock_give(HAL_LOCK_I2C);
|
||||||
return err;
|
return err;
|
||||||
@@ -131,7 +131,7 @@ hal_err_t i2cbus_recover(void)
|
|||||||
if (!s_inited) {
|
if (!s_inited) {
|
||||||
return HAL_ERR_STATE;
|
return HAL_ERR_STATE;
|
||||||
}
|
}
|
||||||
if (s_in_xact) {
|
if (hal_lock_held(HAL_LOCK_I2C)) {
|
||||||
return HAL_ERR_STATE; /* 持着锁去睡 1 ms,会把巡检钉住 */
|
return HAL_ERR_STATE; /* 持着锁去睡 1 ms,会把巡检钉住 */
|
||||||
}
|
}
|
||||||
s_recoveries++;
|
s_recoveries++;
|
||||||
|
|||||||
+116
-22
@@ -45,8 +45,28 @@
|
|||||||
static const char *TAG = "mlink";
|
static const char *TAG = "mlink";
|
||||||
|
|
||||||
#define NVS_NS "phonerail"
|
#define NVS_NS "phonerail"
|
||||||
#define NVS_KEY "net"
|
#define NVS_KEY "net2"
|
||||||
#define NVS_VERSION 1 /* netcfg_t 布局一变就加一:旧版本的 blob 直接当没配置 */
|
#define NVS_VERSION 2 /* netcfg_t 布局一变就加一:旧版本的 blob 直接当没配置 */
|
||||||
|
|
||||||
|
/* ≤0.1.8 的配置(只有一组 WiFi)存在 "net" 键里。0.1.9 起:
|
||||||
|
· 读:没有 "net2" 就读 "net" 迁过来(上架的板子升级后不用重新配网);
|
||||||
|
· 写:两个键**都写**,"net" 里放表里第一组 —— OTA 回滚到老固件照样连得上网。 */
|
||||||
|
#define NVS_KEY_V1 "net"
|
||||||
|
#define NVS_VERSION_V1 1
|
||||||
|
|
||||||
|
typedef struct { /* ≤0.1.8 的 netcfg_t,一个字节都不能动 */
|
||||||
|
char ssid[NETCFG_SSID_MAX + 1];
|
||||||
|
char pass[NETCFG_PASS_MAX + 1];
|
||||||
|
char uri[NETCFG_URI_MAX + 1];
|
||||||
|
char user[NETCFG_USER_MAX + 1];
|
||||||
|
char mpass[NETCFG_MPASS_MAX + 1];
|
||||||
|
uint32_t tele_ms;
|
||||||
|
} netcfg_v1_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t ver;
|
||||||
|
netcfg_v1_t cfg;
|
||||||
|
} nvs_blob_v1_t;
|
||||||
|
|
||||||
/* evse-ca(home 服务器上那个 mosquitto 的自签 CA,公钥证书,不是机密)。
|
/* evse-ca(home 服务器上那个 mosquitto 的自签 CA,公钥证书,不是机密)。
|
||||||
`main/CMakeLists.txt` 的 EMBED_TXTFILES 编进来,结尾带 0。只在 mqtts:// 时用。 */
|
`main/CMakeLists.txt` 的 EMBED_TXTFILES 编进来,结尾带 0。只在 mqtts:// 时用。 */
|
||||||
@@ -63,6 +83,8 @@ static SemaphoreHandle_t s_mtx; /* 护 s_st 与 s_mqtt 句柄 */
|
|||||||
static net_status_t s_st;
|
static net_status_t s_st;
|
||||||
static esp_mqtt_client_handle_t s_mqtt;
|
static esp_mqtt_client_handle_t s_mqtt;
|
||||||
static netcfg_t s_cur; /* 当前生效的配置(只在 t_link 里读写) */
|
static netcfg_t s_cur; /* 当前生效的配置(只在 t_link 里读写) */
|
||||||
|
static volatile uint8_t s_idx; /* 正在连 / 已连上的是 s_cur.wifi 第几组 */
|
||||||
|
static volatile bool s_leaving; /* wifi_apply 主动断开了一条好好的连接:那次断开事件不算失败 */
|
||||||
static volatile bool s_dirty; /* 配置变了,等 t_link 来重配 */
|
static volatile bool s_dirty; /* 配置变了,等 t_link 来重配 */
|
||||||
static esp_timer_handle_t s_retry;
|
static esp_timer_handle_t s_retry;
|
||||||
static uint32_t s_attempt;
|
static uint32_t s_attempt;
|
||||||
@@ -106,8 +128,22 @@ static hal_err_t nvs_save(void *ctx, const netcfg_t *cfg)
|
|||||||
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK) {
|
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK) {
|
||||||
return HAL_ERR_BUS;
|
return HAL_ERR_BUS;
|
||||||
}
|
}
|
||||||
const nvs_blob_t b = { .ver = NVS_VERSION, .cfg = *cfg };
|
static nvs_blob_t b; /* ≈560 B,不放栈上(只有执行器任务进来) */
|
||||||
|
static nvs_blob_v1_t v1;
|
||||||
|
b.ver = NVS_VERSION;
|
||||||
|
b.cfg = *cfg;
|
||||||
|
memset(&v1, 0, sizeof(v1));
|
||||||
|
v1.ver = NVS_VERSION_V1;
|
||||||
|
memcpy(v1.cfg.ssid, cfg->wifi[0].ssid, sizeof(v1.cfg.ssid));
|
||||||
|
memcpy(v1.cfg.pass, cfg->wifi[0].pass, sizeof(v1.cfg.pass));
|
||||||
|
memcpy(v1.cfg.uri, cfg->uri, sizeof(v1.cfg.uri));
|
||||||
|
memcpy(v1.cfg.user, cfg->user, sizeof(v1.cfg.user));
|
||||||
|
memcpy(v1.cfg.mpass, cfg->mpass, sizeof(v1.cfg.mpass));
|
||||||
|
v1.cfg.tele_ms = cfg->tele_ms;
|
||||||
esp_err_t e = nvs_set_blob(h, NVS_KEY, &b, sizeof(b));
|
esp_err_t e = nvs_set_blob(h, NVS_KEY, &b, sizeof(b));
|
||||||
|
if (e == ESP_OK) {
|
||||||
|
e = nvs_set_blob(h, NVS_KEY_V1, &v1, sizeof(v1));
|
||||||
|
}
|
||||||
if (e == ESP_OK) {
|
if (e == ESP_OK) {
|
||||||
e = nvs_commit(h);
|
e = nvs_commit(h);
|
||||||
}
|
}
|
||||||
@@ -121,14 +157,29 @@ static bool nvs_load(netcfg_t *out)
|
|||||||
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK) {
|
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
nvs_blob_t b;
|
static nvs_blob_t b; /* ≈560 B,不放栈上(只在启动时进来一次) */
|
||||||
size_t n = sizeof(b);
|
size_t n = sizeof(b);
|
||||||
const bool ok = nvs_get_blob(h, NVS_KEY, &b, &n) == ESP_OK && n == sizeof(b) &&
|
bool ok = nvs_get_blob(h, NVS_KEY, &b, &n) == ESP_OK && n == sizeof(b) &&
|
||||||
b.ver == NVS_VERSION;
|
b.ver == NVS_VERSION;
|
||||||
nvs_close(h);
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
*out = b.cfg;
|
*out = b.cfg;
|
||||||
|
} else {
|
||||||
|
static nvs_blob_v1_t v1;
|
||||||
|
n = sizeof(v1);
|
||||||
|
ok = nvs_get_blob(h, NVS_KEY_V1, &v1, &n) == ESP_OK && n == sizeof(v1) &&
|
||||||
|
v1.ver == NVS_VERSION_V1;
|
||||||
|
if (ok) {
|
||||||
|
memset(out, 0, sizeof(*out));
|
||||||
|
memcpy(out->wifi[0].ssid, v1.cfg.ssid, sizeof(v1.cfg.ssid));
|
||||||
|
memcpy(out->wifi[0].pass, v1.cfg.pass, sizeof(v1.cfg.pass));
|
||||||
|
memcpy(out->uri, v1.cfg.uri, sizeof(v1.cfg.uri));
|
||||||
|
memcpy(out->user, v1.cfg.user, sizeof(v1.cfg.user));
|
||||||
|
memcpy(out->mpass, v1.cfg.mpass, sizeof(v1.cfg.mpass));
|
||||||
|
out->tele_ms = v1.cfg.tele_ms;
|
||||||
|
ESP_LOGI(TAG, "旧版网络配置(一组 WiFi)已迁移;下次改配置时写成新版");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
nvs_close(h);
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,10 +331,25 @@ static void mqtt_stop(void)
|
|||||||
|
|
||||||
/* ---- WiFi 事件(系统事件循环) ------------------------------------------- */
|
/* ---- WiFi 事件(系统事件循环) ------------------------------------------- */
|
||||||
|
|
||||||
|
/* 把 STA 配成表里第 idx 组(idx 必须 < count)。 */
|
||||||
|
static void wifi_set_sta(uint8_t idx)
|
||||||
|
{
|
||||||
|
const netcfg_wifi_t *w = &s_cur.wifi[idx];
|
||||||
|
wifi_config_t wc = { 0 };
|
||||||
|
memcpy(wc.sta.ssid, w->ssid, strlen(w->ssid));
|
||||||
|
memcpy(wc.sta.password, w->pass, strlen(w->pass));
|
||||||
|
wc.sta.threshold.authmode = w->pass[0] ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN;
|
||||||
|
esp_wifi_set_config(WIFI_IF_STA, &wc);
|
||||||
|
ST_EDIT(s_st.wifi_idx = idx);
|
||||||
|
}
|
||||||
|
|
||||||
static void retry_cb(void *arg)
|
static void retry_cb(void *arg)
|
||||||
{
|
{
|
||||||
(void)arg;
|
(void)arg;
|
||||||
if (s_st.wifi != NET_UP && s_cur.ssid[0] != '\0') {
|
const int n = netcfg_wifi_count(&s_cur);
|
||||||
|
if (s_st.wifi != NET_UP && n > 0) {
|
||||||
|
const uint8_t i = s_idx < n ? s_idx : 0;
|
||||||
|
wifi_set_sta(i);
|
||||||
esp_wifi_connect();
|
esp_wifi_connect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,13 +359,24 @@ static void on_wifi(void *arg, esp_event_base_t base, int32_t id, void *data)
|
|||||||
(void)arg;
|
(void)arg;
|
||||||
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
|
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||||
const wifi_event_sta_disconnected_t *d = data;
|
const wifi_event_sta_disconnected_t *d = data;
|
||||||
ESP_LOGW(TAG, "WiFi 断开,原因 %u,%u ms 后重试", (unsigned)d->reason,
|
const int n = netcfg_wifi_count(&s_cur);
|
||||||
(unsigned)mlink_backoff_ms(s_attempt));
|
const bool was_up = s_st.wifi == NET_UP;
|
||||||
ST_EDIT(s_st.wifi_reason = d->reason;if (s_st.wifi == NET_UP) { s_st.wifi_drops++; } s_st.wifi =
|
ST_EDIT(s_st.wifi_reason = d->reason; if (was_up) { s_st.wifi_drops++; } s_st.wifi =
|
||||||
s_cur.ssid[0] ? NET_CONNECTING : NET_OFF;
|
n > 0 ? NET_CONNECTING : NET_OFF;
|
||||||
s_st.ip[0] = '\0'; s_st.rssi = 0);
|
s_st.ip[0] = '\0'; s_st.rssi = 0);
|
||||||
if (s_cur.ssid[0] != '\0') {
|
if (s_leaving) {
|
||||||
const uint32_t ms = mlink_backoff_ms(s_attempt++);
|
s_leaving = false; /* wifi_apply 自己断的,它紧接着就去连了 */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (n > 0) {
|
||||||
|
/* 连上过又掉的:先重试同一组;没连上的:换下一组。退避按「轮完一整圈」算 ——
|
||||||
|
几组全连不上也是 1 s、2 s、4 s …… 地拉长,而不是每组各退各的。 */
|
||||||
|
if (!was_up && n > 1) {
|
||||||
|
s_idx = (uint8_t)((s_idx + 1) % n);
|
||||||
|
}
|
||||||
|
const uint32_t ms = mlink_backoff_ms(s_attempt++ / (uint32_t)n);
|
||||||
|
ESP_LOGW(TAG, "WiFi 断开,原因 %u,%u ms 后连「%s」", (unsigned)d->reason, (unsigned)ms,
|
||||||
|
s_cur.wifi[s_idx < n ? s_idx : 0].ssid);
|
||||||
esp_timer_stop(s_retry);
|
esp_timer_stop(s_retry);
|
||||||
esp_timer_start_once(s_retry, (uint64_t)ms * 1000u);
|
esp_timer_start_once(s_retry, (uint64_t)ms * 1000u);
|
||||||
}
|
}
|
||||||
@@ -308,7 +385,7 @@ static void on_wifi(void *arg, esp_event_base_t base, int32_t id, void *data)
|
|||||||
s_attempt = 0;
|
s_attempt = 0;
|
||||||
ST_EDIT(s_st.wifi = NET_UP;
|
ST_EDIT(s_st.wifi = NET_UP;
|
||||||
snprintf(s_st.ip, sizeof(s_st.ip), IPSTR, IP2STR(&e->ip_info.ip)));
|
snprintf(s_st.ip, sizeof(s_st.ip), IPSTR, IP2STR(&e->ip_info.ip)));
|
||||||
ESP_LOGI(TAG, "WiFi 已连上,IP %s", s_st.ip);
|
ESP_LOGI(TAG, "WiFi「%s」已连上,IP %s", s_cur.wifi[s_idx].ssid, s_st.ip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,20 +393,22 @@ static void on_wifi(void *arg, esp_event_base_t base, int32_t id, void *data)
|
|||||||
static void wifi_apply(void)
|
static void wifi_apply(void)
|
||||||
{
|
{
|
||||||
esp_timer_stop(s_retry);
|
esp_timer_stop(s_retry);
|
||||||
|
s_leaving = s_st.wifi == NET_UP;
|
||||||
esp_wifi_disconnect();
|
esp_wifi_disconnect();
|
||||||
s_attempt = 0;
|
s_attempt = 0;
|
||||||
if (s_cur.ssid[0] == '\0') {
|
const int n = netcfg_wifi_count(&s_cur);
|
||||||
|
if (n == 0) {
|
||||||
|
s_leaving = false;
|
||||||
ST_EDIT(s_st.wifi = NET_OFF);
|
ST_EDIT(s_st.wifi = NET_OFF);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
wifi_config_t wc = { 0 };
|
if (s_idx >= n) {
|
||||||
memcpy(wc.sta.ssid, s_cur.ssid, strlen(s_cur.ssid));
|
s_idx = 0;
|
||||||
memcpy(wc.sta.password, s_cur.pass, strlen(s_cur.pass));
|
}
|
||||||
wc.sta.threshold.authmode = s_cur.pass[0] ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN;
|
wifi_set_sta(s_idx);
|
||||||
esp_wifi_set_config(WIFI_IF_STA, &wc);
|
|
||||||
ST_EDIT(s_st.wifi = NET_CONNECTING);
|
ST_EDIT(s_st.wifi = NET_CONNECTING);
|
||||||
esp_wifi_connect();
|
esp_wifi_connect();
|
||||||
ESP_LOGI(TAG, "连 WiFi「%s」", s_cur.ssid); /* 只打 SSID,不打口令 */
|
ESP_LOGI(TAG, "连 WiFi「%s」(%u/%d)", s_cur.wifi[s_idx].ssid, (unsigned)s_idx + 1, n); /* 只打 SSID,不打口令 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 对外 ---------------------------------------------------------------- */
|
/* ---- 对外 ---------------------------------------------------------------- */
|
||||||
@@ -391,10 +470,25 @@ void mlink_esp_poll(void)
|
|||||||
}
|
}
|
||||||
if (s_dirty) {
|
if (s_dirty) {
|
||||||
s_dirty = false;
|
s_dirty = false;
|
||||||
|
/* 加 / 删的不是正连着的那一组、口令也没变:WiFi 不断;broker 也没变:MQTT 也不断。
|
||||||
|
经 USB 往表里加一组备用热点,不该让板子掉线一次。 */
|
||||||
|
static netcfg_t next; /* ≈560 B,不放栈上(只有 t_link 进来) */
|
||||||
|
netcfg_get(&next);
|
||||||
|
const int j = (s_st.wifi == NET_UP) ? netcfg_wifi_find(&next, s_cur.wifi[s_idx].ssid) : -1;
|
||||||
|
const bool wifi_same = j >= 0 && strcmp(next.wifi[j].pass, s_cur.wifi[s_idx].pass) == 0;
|
||||||
|
const bool mqtt_same = strcmp(next.uri, s_cur.uri) == 0 && strcmp(next.user, s_cur.user) == 0 &&
|
||||||
|
strcmp(next.mpass, s_cur.mpass) == 0;
|
||||||
|
if (!(wifi_same && mqtt_same)) {
|
||||||
mqtt_stop();
|
mqtt_stop();
|
||||||
netcfg_get(&s_cur);
|
}
|
||||||
|
s_cur = next;
|
||||||
|
if (wifi_same) {
|
||||||
|
s_idx = (uint8_t)j;
|
||||||
|
ST_EDIT(s_st.wifi_idx = s_idx);
|
||||||
|
} else {
|
||||||
wifi_apply();
|
wifi_apply();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/* MQTT 的生命周期只在这里管:WiFi 有地址、配了 broker、还没建客户端 ⇒ 建。
|
/* MQTT 的生命周期只在这里管:WiFi 有地址、配了 broker、还没建客户端 ⇒ 建。
|
||||||
之后掉线重连由 esp-mqtt 自己做(network.reconnect_timeout_ms,默认 10 s)。 */
|
之后掉线重连由 esp-mqtt 自己做(network.reconnect_timeout_ms,默认 10 s)。 */
|
||||||
if (s_mqtt == NULL && s_st.wifi == NET_UP && s_cur.uri[0] != '\0') {
|
if (s_mqtt == NULL && s_st.wifi == NET_UP && s_cur.uri[0] != '\0') {
|
||||||
|
|||||||
+57
-7
@@ -18,9 +18,20 @@ void netcfg_init(const netcfg_t *loaded, netcfg_save_fn save, netcfg_changed_fn
|
|||||||
if (loaded != NULL) {
|
if (loaded != NULL) {
|
||||||
s_cfg = *loaded;
|
s_cfg = *loaded;
|
||||||
}
|
}
|
||||||
/* 从 flash 读回来的东西不可信:强制收尾,免得一个没带 0 的字段把后面的读穿。 */
|
/* 从 flash 读回来的东西不可信:强制收尾,免得一个没带 0 的字段把后面的读穿;
|
||||||
s_cfg.ssid[NETCFG_SSID_MAX] = '\0';
|
空位挤到末尾(「有效的就是 [0, n)」是 count / 轮换的前提)。 */
|
||||||
s_cfg.pass[NETCFG_PASS_MAX] = '\0';
|
int n = 0;
|
||||||
|
for (int i = 0; i < NETCFG_WIFI_N; i++) {
|
||||||
|
netcfg_wifi_t w = s_cfg.wifi[i];
|
||||||
|
w.ssid[NETCFG_SSID_MAX] = '\0';
|
||||||
|
w.pass[NETCFG_PASS_MAX] = '\0';
|
||||||
|
if (w.ssid[0] != '\0') {
|
||||||
|
s_cfg.wifi[n++] = w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = n; i < NETCFG_WIFI_N; i++) {
|
||||||
|
memset(&s_cfg.wifi[i], 0, sizeof(s_cfg.wifi[i]));
|
||||||
|
}
|
||||||
s_cfg.uri[NETCFG_URI_MAX] = '\0';
|
s_cfg.uri[NETCFG_URI_MAX] = '\0';
|
||||||
s_cfg.user[NETCFG_USER_MAX] = '\0';
|
s_cfg.user[NETCFG_USER_MAX] = '\0';
|
||||||
s_cfg.mpass[NETCFG_MPASS_MAX] = '\0';
|
s_cfg.mpass[NETCFG_MPASS_MAX] = '\0';
|
||||||
@@ -90,6 +101,25 @@ static hal_err_t commit(const netcfg_t *next)
|
|||||||
return HAL_OK;
|
return HAL_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int netcfg_wifi_count(const netcfg_t *cfg)
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
while (n < NETCFG_WIFI_N && cfg->wifi[n].ssid[0] != '\0') {
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
int netcfg_wifi_find(const netcfg_t *cfg, const char *ssid)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < netcfg_wifi_count(cfg); i++) {
|
||||||
|
if (strcmp(cfg->wifi[i].ssid, ssid) == 0) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
hal_err_t netcfg_set_wifi(const char *ssid, const char *pass)
|
hal_err_t netcfg_set_wifi(const char *ssid, const char *pass)
|
||||||
{
|
{
|
||||||
if (!netcfg_wifi_ok(ssid, pass)) {
|
if (!netcfg_wifi_ok(ssid, pass)) {
|
||||||
@@ -97,10 +127,30 @@ hal_err_t netcfg_set_wifi(const char *ssid, const char *pass)
|
|||||||
}
|
}
|
||||||
netcfg_t next;
|
netcfg_t next;
|
||||||
netcfg_get(&next);
|
netcfg_get(&next);
|
||||||
memset(next.ssid, 0, sizeof(next.ssid));
|
int i = netcfg_wifi_find(&next, ssid);
|
||||||
memset(next.pass, 0, sizeof(next.pass));
|
if (i < 0) {
|
||||||
strcpy(next.ssid, ssid);
|
i = netcfg_wifi_count(&next);
|
||||||
strcpy(next.pass, pass);
|
if (i >= NETCFG_WIFI_N) {
|
||||||
|
return HAL_ERR_STATE; /* 满了:先删一组。不悄悄挤掉最老的 —— 那可能正是唯一连得上的 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
memset(&next.wifi[i], 0, sizeof(next.wifi[i]));
|
||||||
|
strcpy(next.wifi[i].ssid, ssid);
|
||||||
|
strcpy(next.wifi[i].pass, pass);
|
||||||
|
return commit(&next);
|
||||||
|
}
|
||||||
|
|
||||||
|
hal_err_t netcfg_del_wifi(const char *ssid)
|
||||||
|
{
|
||||||
|
netcfg_t next;
|
||||||
|
netcfg_get(&next);
|
||||||
|
const int i = ssid != NULL ? netcfg_wifi_find(&next, ssid) : -1;
|
||||||
|
if (i < 0) {
|
||||||
|
return HAL_ERR_ARG;
|
||||||
|
}
|
||||||
|
const int n = netcfg_wifi_count(&next);
|
||||||
|
memmove(&next.wifi[i], &next.wifi[i + 1], (size_t)(n - 1 - i) * sizeof(next.wifi[0]));
|
||||||
|
memset(&next.wifi[n - 1], 0, sizeof(next.wifi[0]));
|
||||||
return commit(&next);
|
return commit(&next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-5
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
IF-19 原话:「凭据与服务端地址存 NVS,**不编译进固件** —— 新板上架时先用 IF-13 的
|
IF-19 原话:「凭据与服务端地址存 NVS,**不编译进固件** —— 新板上架时先用 IF-13 的
|
||||||
USB 链路配网」。所以:
|
USB 链路配网」。所以:
|
||||||
· 配置由两条命令写入:`wifi`(SSID / 密码)与 `mqtt`(broker 地址 / 账号 / 遥测周期),
|
· 配置由两条命令写入:`wifi`(SSID / 密码,**最多存 NETCFG_WIFI_N 组**,0.1.9)与
|
||||||
|
`mqtt`(broker 地址 / 账号 / 遥测周期),
|
||||||
**只收 USB 链路来的**(cmd.c 回 `link`)—— 经 WiFi 改 WiFi 配置,一次手误就把
|
**只收 USB 链路来的**(cmd.c 回 `link`)—— 经 WiFi 改 WiFi 配置,一次手误就把
|
||||||
唯一的远程通道弄断了,而那时只能拆机插 USB;
|
唯一的远程通道弄断了,而那时只能拆机插 USB;
|
||||||
· 本模块只管「校验 + 在内存里持有一份 + 调存储后端 + 通知网络那一侧」,
|
· 本模块只管「校验 + 在内存里持有一份 + 调存储后端 + 通知网络那一侧」,
|
||||||
@@ -14,8 +15,13 @@
|
|||||||
|
|
||||||
---- 跨任务 --------------------------------------------------------------
|
---- 跨任务 --------------------------------------------------------------
|
||||||
写者是执行器任务(命令处理器),读者是链路任务(网络胶水)。配置读写都在
|
写者是执行器任务(命令处理器),读者是链路任务(网络胶水)。配置读写都在
|
||||||
`hal_crit` 里整块拷贝(≈260 B 的 memcpy,关中断几微秒),读者永远拿到一份完整的。
|
`hal_crit` 里整块拷贝(≈560 B 的 memcpy,关中断十几微秒),读者永远拿到一份完整的。
|
||||||
网络状态反过来:链路任务写、命令处理器读,同样整块拷贝。 */
|
网络状态反过来:链路任务写、命令处理器读,同样整块拷贝。
|
||||||
|
|
||||||
|
---- 多组 WiFi(0.1.9,2026-09-28 用户要求)-------------------------------
|
||||||
|
`wifi` 命令 = **加一组或改同名那组的口令**,`"del":true` = 删同名那组;满了回 `state`。
|
||||||
|
连不上就按表里的顺序轮着换下一组(mlink_esp.c)。表里的空位(ssid 为空)一律跳过,
|
||||||
|
`netcfg_init` 会把它们挤到末尾。 */
|
||||||
#ifndef NETCFG_H
|
#ifndef NETCFG_H
|
||||||
#define NETCFG_H
|
#define NETCFG_H
|
||||||
|
|
||||||
@@ -40,10 +46,16 @@ extern "C" {
|
|||||||
#define NETCFG_TELE_MIN_MS 200
|
#define NETCFG_TELE_MIN_MS 200
|
||||||
#define NETCFG_TELE_MAX_MS 60000
|
#define NETCFG_TELE_MAX_MS 60000
|
||||||
|
|
||||||
|
#define NETCFG_WIFI_N 4 /* 存几组 WiFi */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char ssid[NETCFG_SSID_MAX + 1]; /* 空 = 空位 */
|
||||||
|
char pass[NETCFG_PASS_MAX + 1];
|
||||||
|
} netcfg_wifi_t;
|
||||||
|
|
||||||
/* ⚠️ 这个结构体**原样存进 NVS**(mlink_esp.c 带版本号存)。改字段就升版本号。 */
|
/* ⚠️ 这个结构体**原样存进 NVS**(mlink_esp.c 带版本号存)。改字段就升版本号。 */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char ssid[NETCFG_SSID_MAX + 1];
|
netcfg_wifi_t wifi[NETCFG_WIFI_N]; /* 按加入的顺序,空位都在末尾 */
|
||||||
char pass[NETCFG_PASS_MAX + 1];
|
|
||||||
char uri[NETCFG_URI_MAX + 1];
|
char uri[NETCFG_URI_MAX + 1];
|
||||||
char user[NETCFG_USER_MAX + 1];
|
char user[NETCFG_USER_MAX + 1];
|
||||||
char mpass[NETCFG_MPASS_MAX + 1];
|
char mpass[NETCFG_MPASS_MAX + 1];
|
||||||
@@ -61,10 +73,18 @@ void netcfg_init(const netcfg_t *loaded, netcfg_save_fn save, netcfg_changed_fn
|
|||||||
void netcfg_get(netcfg_t *out);
|
void netcfg_get(netcfg_t *out);
|
||||||
|
|
||||||
/* 校验 → 存 → 改内存 → 通知。校验不过 HAL_ERR_ARG,存不下是 save 的错误码。
|
/* 校验 → 存 → 改内存 → 通知。校验不过 HAL_ERR_ARG,存不下是 save 的错误码。
|
||||||
|
`set_wifi`:同名的改口令,否则加到末尾;表满了 HAL_ERR_STATE。
|
||||||
|
`del_wifi`:没有同名的 HAL_ERR_ARG。
|
||||||
`mqtt` 的 tele_ms 传 0 = 用默认值。 */
|
`mqtt` 的 tele_ms 传 0 = 用默认值。 */
|
||||||
hal_err_t netcfg_set_wifi(const char *ssid, const char *pass);
|
hal_err_t netcfg_set_wifi(const char *ssid, const char *pass);
|
||||||
|
hal_err_t netcfg_del_wifi(const char *ssid);
|
||||||
hal_err_t netcfg_set_mqtt(const char *uri, const char *user, const char *mpass, uint32_t tele_ms);
|
hal_err_t netcfg_set_mqtt(const char *uri, const char *user, const char *mpass, uint32_t tele_ms);
|
||||||
|
|
||||||
|
/* 表里有几组(空位在末尾,所以有效的就是 [0, n))。 */
|
||||||
|
int netcfg_wifi_count(const netcfg_t *cfg);
|
||||||
|
/* 名为 ssid 的那组在第几个,没有 −1。 */
|
||||||
|
int netcfg_wifi_find(const netcfg_t *cfg, const char *ssid);
|
||||||
|
|
||||||
/* 纯校验,给命令处理器先判一遍好回准确的错误。 */
|
/* 纯校验,给命令处理器先判一遍好回准确的错误。 */
|
||||||
bool netcfg_wifi_ok(const char *ssid, const char *pass);
|
bool netcfg_wifi_ok(const char *ssid, const char *pass);
|
||||||
bool netcfg_mqtt_ok(const char *uri, const char *user, const char *mpass, uint32_t tele_ms);
|
bool netcfg_mqtt_ok(const char *uri, const char *user, const char *mpass, uint32_t tele_ms);
|
||||||
@@ -88,6 +108,7 @@ typedef struct {
|
|||||||
uint32_t tele_skipped; /* 该发遥测时命令队列满 / 上一条还没发完 */
|
uint32_t tele_skipped; /* 该发遥测时命令队列满 / 上一条还没发完 */
|
||||||
uint8_t wifi_reason; /* 最近一次断开的原因码(wifi_err_reason_t;201 = 找不到 AP,
|
uint8_t wifi_reason; /* 最近一次断开的原因码(wifi_err_reason_t;201 = 找不到 AP,
|
||||||
202 = 认证失败,15 = 四次握手超时 ≈ 口令错),0 = 没断过 */
|
202 = 认证失败,15 = 四次握手超时 ≈ 口令错),0 = 没断过 */
|
||||||
|
uint8_t wifi_idx; /* 正在连 / 已连上的是表里第几组(net 回应的 ssid 取它) */
|
||||||
char id[13]; /* 板子 ID = STA MAC 的 12 位小写十六进制,topic 用它 */
|
char id[13]; /* 板子 ID = STA MAC 的 12 位小写十六进制,topic 用它 */
|
||||||
} net_status_t;
|
} net_status_t;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -339,7 +339,7 @@ void ota_esp_poll(void)
|
|||||||
.pending = true,
|
.pending = true,
|
||||||
.now_ms = now,
|
.now_ms = now,
|
||||||
.boot_done = s_boot_done,
|
.boot_done = s_boot_done,
|
||||||
.wifi_set = cfg.ssid[0] != '\0',
|
.wifi_set = netcfg_wifi_count(&cfg) > 0,
|
||||||
.wifi_up = ns.wifi == NET_UP,
|
.wifi_up = ns.wifi == NET_UP,
|
||||||
.mqtt_set = cfg.uri[0] != '\0',
|
.mqtt_set = cfg.uri[0] != '\0',
|
||||||
.mqtt_up = ns.mqtt == NET_UP,
|
.mqtt_up = ns.mqtt == NET_UP,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#define PHONERAIL_H
|
#define PHONERAIL_H
|
||||||
|
|
||||||
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
||||||
#define PHONERAIL_FW_VERSION "0.1.4-c5"
|
#define PHONERAIL_FW_VERSION "0.1.13-c5"
|
||||||
|
|
||||||
/* 通道数。五路直供,见设计文档 §2。 */
|
/* 通道数。五路直供,见设计文档 §2。 */
|
||||||
#define PHONERAIL_RAIL_COUNT 5
|
#define PHONERAIL_RAIL_COUNT 5
|
||||||
|
|||||||
@@ -107,6 +107,14 @@ hal_err_t slot_init(int rail)
|
|||||||
return i2cbus_xact(slot_mux_ch(rail), SLOT_LOCK_TIMEOUT_MS, do_init, &r);
|
return i2cbus_xact(slot_mux_ch(rail), SLOT_LOCK_TIMEOUT_MS, do_init, &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hal_err_t slot_init_locked(int rail)
|
||||||
|
{
|
||||||
|
if (!rail_valid(rail)) {
|
||||||
|
return HAL_ERR_ARG;
|
||||||
|
}
|
||||||
|
return pca9555_init(rail - 1);
|
||||||
|
}
|
||||||
|
|
||||||
hal_err_t slot_init_keep_vbus(int rail)
|
hal_err_t slot_init_keep_vbus(int rail)
|
||||||
{
|
{
|
||||||
if (!rail_valid(rail)) {
|
if (!rail_valid(rail)) {
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ hal_err_t slot_init(int rail);
|
|||||||
这片掉过电(方向寄存器不是我们写的)就不留,等同 slot_init()(pca9555_init_keep)。 */
|
这片掉过电(方向寄存器不是我们写的)就不留,等同 slot_init()(pca9555_init_keep)。 */
|
||||||
hal_err_t slot_init_keep_vbus(int rail);
|
hal_err_t slot_init_keep_vbus(int rail);
|
||||||
|
|
||||||
|
/* 已持锁版本的 slot_init():给 `t_safety` 在热插拔进来的刀片上用(0.1.10)——
|
||||||
|
它本来就在这一路的 `xact` 里。 */
|
||||||
|
hal_err_t slot_init_locked(int rail);
|
||||||
|
|
||||||
/* ---- VBUS(PCA9555 P0.4 → TPS22918) ------------------------------------- */
|
/* ---- VBUS(PCA9555 P0.4 → TPS22918) ------------------------------------- */
|
||||||
|
|
||||||
hal_err_t slot_vbus_enable(int rail, bool on);
|
hal_err_t slot_vbus_enable(int rail, bool on);
|
||||||
|
|||||||
+76
-4
@@ -24,6 +24,9 @@ typedef struct {
|
|||||||
uint8_t good_n; /* HR_VERIFY:连续几个样本电流达标 */
|
uint8_t good_n; /* HR_VERIFY:连续几个样本电流达标 */
|
||||||
bool ua_seen; /* HR_VERIFY:这一轮窗口里读到过电流没有 */
|
bool ua_seen; /* HR_VERIFY:这一轮窗口里读到过电流没有 */
|
||||||
int32_t last_ua; /* HR_VERIFY:最近一个读到的样本 */
|
int32_t last_ua; /* HR_VERIFY:最近一个读到的样本 */
|
||||||
|
bool vbus_back; /* HR:开始时断了 VBUS,开机校验通过后要接回(0.1.11) */
|
||||||
|
bool vbus_pending; /* 已开机,等到 vbus_at 再接回 VBUS(ACT_HR_VBUS_BACK_MS) */
|
||||||
|
uint32_t vbus_at;
|
||||||
} act_rail_t;
|
} act_rail_t;
|
||||||
|
|
||||||
static act_rail_t s_rail[PHONERAIL_RAIL_COUNT];
|
static act_rail_t s_rail[PHONERAIL_RAIL_COUNT];
|
||||||
@@ -118,6 +121,7 @@ static void boot_report(act_rail_t *r, int rail, bool booted)
|
|||||||
按下去过东西就必须经 CLEANUP,那是「键一定会松开」的唯一保证。 */
|
按下去过东西就必须经 CLEANUP,那是「键一定会松开」的唯一保证。 */
|
||||||
static void finish(act_rail_t *r, act_result_t res, uint32_t now)
|
static void finish(act_rail_t *r, act_result_t res, uint32_t now)
|
||||||
{
|
{
|
||||||
|
r->vbus_back = false; /* 只有 hr_ok() 会接回 VBUS;别的结局一律让它断着 */
|
||||||
r->result = res;
|
r->result = res;
|
||||||
r->done++;
|
r->done++;
|
||||||
if (r->release_mask != 0) {
|
if (r->release_mask != 0) {
|
||||||
@@ -128,6 +132,39 @@ static void finish(act_rail_t *r, act_result_t res, uint32_t now)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* HR 成功收尾:开始时断过 VBUS 的,排一个 ACT_HR_VBUS_BACK_MS 之后的「接回」
|
||||||
|
(t_actuator_step 里做,那时执行器已空闲)。 */
|
||||||
|
static void hr_ok(act_rail_t *r, int rail, uint32_t now)
|
||||||
|
{
|
||||||
|
(void)rail;
|
||||||
|
if (r->vbus_back) {
|
||||||
|
r->vbus_pending = true;
|
||||||
|
r->vbus_at = now + ACT_HR_VBUS_BACK_MS;
|
||||||
|
}
|
||||||
|
finish(r, ACT_RES_OK, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 到点了就接回 VBUS。返回下次最晚多久再来。
|
||||||
|
这一路被关了电(安全判据 / 拔刀片)就作废;写失败下一拍再试(影子如实留在 off)。 */
|
||||||
|
static uint32_t vbus_back_step(act_rail_t *r, int rail, uint32_t now)
|
||||||
|
{
|
||||||
|
if (!r->vbus_pending || r->state != ACT_ST_IDLE) {
|
||||||
|
return ACT_IDLE_WAIT_MS;
|
||||||
|
}
|
||||||
|
const int32_t left = (int32_t)(r->vbus_at - now);
|
||||||
|
if (left > 0) {
|
||||||
|
return (uint32_t)left;
|
||||||
|
}
|
||||||
|
if (!rail_efuse_is_on(rail)) {
|
||||||
|
r->vbus_pending = false;
|
||||||
|
return ACT_IDLE_WAIT_MS;
|
||||||
|
}
|
||||||
|
if (slot_vbus_enable(rail, true) == HAL_OK) {
|
||||||
|
r->vbus_pending = false;
|
||||||
|
}
|
||||||
|
return ACT_IDLE_WAIT_MS;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- 状态机本体:一个状态一段,返回「还要多久再来」 ---------------------- */
|
/* ---- 状态机本体:一个状态一段,返回「还要多久再来」 ---------------------- */
|
||||||
|
|
||||||
static uint32_t state_step(act_rail_t *r, int rail, uint32_t now)
|
static uint32_t state_step(act_rail_t *r, int rail, uint32_t now)
|
||||||
@@ -181,7 +218,7 @@ static uint32_t state_step(act_rail_t *r, int rail, uint32_t now)
|
|||||||
return r->hold_ms - el;
|
return r->hold_ms - el;
|
||||||
}
|
}
|
||||||
if (s_ua == NULL) {
|
if (s_ua == NULL) {
|
||||||
finish(r, ACT_RES_OK, now); /* 不校验(0.1.1 行为):CLEANUP 松键 */
|
hr_ok(r, rail, now); /* 不校验(0.1.1 行为):CLEANUP 松键 */
|
||||||
return ACT_CLEANUP_RETRY_MS;
|
return ACT_CLEANUP_RETRY_MS;
|
||||||
}
|
}
|
||||||
/* 要校验:先在这里松键(松不掉就走 CLEANUP 的「不放弃」,不再校验)。 */
|
/* 要校验:先在这里松键(松不掉就走 CLEANUP 的「不放弃」,不再校验)。 */
|
||||||
@@ -206,7 +243,7 @@ static uint32_t state_step(act_rail_t *r, int rail, uint32_t now)
|
|||||||
r->good_n = ua >= ACT_HR_BOOT_UA_MIN ? (uint8_t)(r->good_n + 1) : 0;
|
r->good_n = ua >= ACT_HR_BOOT_UA_MIN ? (uint8_t)(r->good_n + 1) : 0;
|
||||||
if (r->good_n >= ACT_HR_VERIFY_GOOD_N) {
|
if (r->good_n >= ACT_HR_VERIFY_GOOD_N) {
|
||||||
boot_report(r, rail, true);
|
boot_report(r, rail, true);
|
||||||
finish(r, ACT_RES_OK, now);
|
hr_ok(r, rail, now);
|
||||||
return ACT_IDLE_WAIT_MS;
|
return ACT_IDLE_WAIT_MS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,7 +253,7 @@ static uint32_t state_step(act_rail_t *r, int rail, uint32_t now)
|
|||||||
}
|
}
|
||||||
if (!r->ua_seen) {
|
if (!r->ua_seen) {
|
||||||
/* 整个窗口都读不到电流:判不了,**不重按**(ACT_HR_BOOT_TRIES 那段注释)。 */
|
/* 整个窗口都读不到电流:判不了,**不重按**(ACT_HR_BOOT_TRIES 那段注释)。 */
|
||||||
finish(r, ACT_RES_OK, now);
|
hr_ok(r, rail, now);
|
||||||
return ACT_IDLE_WAIT_MS;
|
return ACT_IDLE_WAIT_MS;
|
||||||
}
|
}
|
||||||
if (r->presses < ACT_HR_BOOT_TRIES) {
|
if (r->presses < ACT_HR_BOOT_TRIES) {
|
||||||
@@ -330,8 +367,25 @@ hal_err_t t_actuator_hard_reset(int rail)
|
|||||||
(void)out_write(rail, km, 0); /* 失败也照断电:断电永远比不断电安全 */
|
(void)out_write(rail, km, 0); /* 失败也照断电:断电永远比不断电安全 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 0.1.11(2026-09-28 板 B):VBUS 通着就**先断**,开机校验通过后再接回(hr_ok)。
|
||||||
|
不断的三个后果,现场全见过:
|
||||||
|
· 手机靠 USB 撑着,根本没断电 —— 硬重启是假的;
|
||||||
|
· 手机吃 USB 的电,直供电流 ≈ 0 ⇒ 开机校验判「没开起来」、再按两下电源键;
|
||||||
|
· 断电那一刻 USB 还在 ⇒ 被当成「插充电器开机」,进关机充电模式,一拔 USB 就关机。
|
||||||
|
断不掉也照样断电(断电永远比不断电安全),只是事后不接回。 */
|
||||||
|
r->vbus_back = false;
|
||||||
|
r->vbus_pending = false; /* 上一次硬重启排的接回作废 */
|
||||||
|
const bool had_vbus = slot_vbus_is_on(rail);
|
||||||
|
if (had_vbus && slot_vbus_enable(rail, false) == HAL_OK) {
|
||||||
|
r->vbus_back = true;
|
||||||
|
}
|
||||||
|
|
||||||
const hal_err_t err = rail_efuse_enable(rail, false);
|
const hal_err_t err = rail_efuse_enable(rail, false);
|
||||||
if (err != HAL_OK) {
|
if (err != HAL_OK) {
|
||||||
|
if (r->vbus_back) {
|
||||||
|
(void)slot_vbus_enable(rail, true); /* 序列没开始:恢复原样 */
|
||||||
|
r->vbus_back = false;
|
||||||
|
}
|
||||||
return err; /* 序列没开始 */
|
return err; /* 序列没开始 */
|
||||||
}
|
}
|
||||||
r->seq = ACT_SEQ_HARD_RESET;
|
r->seq = ACT_SEQ_HARD_RESET;
|
||||||
@@ -396,6 +450,7 @@ hal_err_t t_actuator_edl(int rail)
|
|||||||
return HAL_ERR_STATE;
|
return HAL_ERR_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r->vbus_pending = false; /* EDL 自己管 VBUS */
|
||||||
const uint8_t mask = (uint8_t)(1u << PCA9555_BIT_EDL_EN);
|
const uint8_t mask = (uint8_t)(1u << PCA9555_BIT_EDL_EN);
|
||||||
const hal_err_t err = out_write(rail, mask, mask);
|
const hal_err_t err = out_write(rail, mask, mask);
|
||||||
|
|
||||||
@@ -417,6 +472,7 @@ void t_actuator_abort(int rail)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
act_rail_t *r = at(rail);
|
act_rail_t *r = at(rail);
|
||||||
|
r->vbus_pending = false; /* 被打断的路(多半是安全判据关了电)不再接回 VBUS */
|
||||||
if (r->state == ACT_ST_IDLE || r->state == ACT_ST_CLEANUP) {
|
if (r->state == ACT_ST_IDLE || r->state == ACT_ST_CLEANUP) {
|
||||||
return; /* CLEANUP 已经在收尾了,别把它的结果覆盖掉 */
|
return; /* CLEANUP 已经在收尾了,别把它的结果覆盖掉 */
|
||||||
}
|
}
|
||||||
@@ -424,6 +480,18 @@ void t_actuator_abort(int rail)
|
|||||||
(void)rail_step(rail, hal_time_now_ms()); /* 立刻试一次收尾,别等下一拍 */
|
(void)rail_step(rail, hal_time_now_ms()); /* 立刻试一次收尾,别等下一拍 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void t_actuator_cancel_vbus_back(int rail)
|
||||||
|
{
|
||||||
|
if (s_inited && rail_valid(rail)) {
|
||||||
|
at(rail)->vbus_pending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool t_actuator_vbus_back_pending(int rail)
|
||||||
|
{
|
||||||
|
return s_inited && rail_valid(rail) && at(rail)->vbus_pending;
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t t_actuator_step(void)
|
uint32_t t_actuator_step(void)
|
||||||
{
|
{
|
||||||
if (!s_inited) {
|
if (!s_inited) {
|
||||||
@@ -433,7 +501,11 @@ uint32_t t_actuator_step(void)
|
|||||||
uint32_t wait = ACT_IDLE_WAIT_MS;
|
uint32_t wait = ACT_IDLE_WAIT_MS;
|
||||||
|
|
||||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||||
const uint32_t w = rail_step(rail, now);
|
uint32_t w = rail_step(rail, now);
|
||||||
|
const uint32_t v = vbus_back_step(at(rail), rail, now);
|
||||||
|
if (v < w) {
|
||||||
|
w = v;
|
||||||
|
}
|
||||||
if (w < wait) {
|
if (w < wait) {
|
||||||
wait = w;
|
wait = w;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,13 @@ extern "C" {
|
|||||||
|
|
||||||
/* CLEANUP 重试间隔,以及空闲时建议任务睡多久(A12 会换成「带超时地等队列」)。 */
|
/* CLEANUP 重试间隔,以及空闲时建议任务睡多久(A12 会换成「带超时地等队列」)。 */
|
||||||
#define ACT_CLEANUP_RETRY_MS 5
|
#define ACT_CLEANUP_RETRY_MS 5
|
||||||
|
|
||||||
|
/* 0.1.11:硬重启开始时断掉的 VBUS,开机校验通过后**再等这么久**才接回(执行器此时已空闲,
|
||||||
|
不挡别的命令)。校验通过时手机才起来几秒,还在 bootloader / 内核早期 —— 关充电的内核补丁
|
||||||
|
没生效,这时接回 USB,充电器立刻把节点顶到 4.4 V、SW-8 当场断 VBUS(2026-09-28 板 B
|
||||||
|
实测:硬重启后 13 s「顶高断 VBUS」4394 mV)。60 s 时 Android 内核早已 probe 完充电驱动。
|
||||||
|
没刷补丁的手机接回后照样会被 SW-8 断掉 —— 那是对的。 */
|
||||||
|
#define ACT_HR_VBUS_BACK_MS 60000
|
||||||
#define ACT_IDLE_WAIT_MS 100
|
#define ACT_IDLE_WAIT_MS 100
|
||||||
|
|
||||||
/* ---- 按键:逻辑键位,不是器件上的位号 -----------------------------------
|
/* ---- 按键:逻辑键位,不是器件上的位号 -----------------------------------
|
||||||
@@ -235,6 +242,12 @@ hal_err_t t_actuator_edl(int rail);
|
|||||||
或者再开一次都是自作主张;断电是 A10 的判据和上位机命令的事。 */
|
或者再开一次都是自作主张;断电是 A10 的判据和上位机命令的事。 */
|
||||||
void t_actuator_abort(int rail);
|
void t_actuator_abort(int rail);
|
||||||
|
|
||||||
|
/* 取消「过一会儿把 VBUS 接回去」(ACT_HR_VBUS_BACK_MS)。有人手动动了这一路的
|
||||||
|
VBUS / 电(`vbus` / `power` 命令)就该调:人的意图优先,别 60 s 后又被改回去。
|
||||||
|
硬重启 / EDL / abort 自己会取消。 */
|
||||||
|
void t_actuator_cancel_vbus_back(int rail);
|
||||||
|
bool t_actuator_vbus_back_pending(int rail);
|
||||||
|
|
||||||
/* 推一步。返回**下次最晚多久再来**(ms):五路里最近的那个截止时刻。
|
/* 推一步。返回**下次最晚多久再来**(ms):五路里最近的那个截止时刻。
|
||||||
全空闲返回 ACT_IDLE_WAIT_MS。醒早了没关系 —— 每个状态都是拿绝对时刻算的,
|
全空闲返回 ACT_IDLE_WAIT_MS。醒早了没关系 —— 每个状态都是拿绝对时刻算的,
|
||||||
早来就再返回一个剩余时间。(ESP-IDF 默认 `CONFIG_FREERTOS_HZ=100`,
|
早来就再返回一个剩余时间。(ESP-IDF 默认 `CONFIG_FREERTOS_HZ=100`,
|
||||||
|
|||||||
+45
-4
@@ -4,6 +4,7 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "chgmode.h"
|
||||||
#include "hal_i2c_acct.h"
|
#include "hal_i2c_acct.h"
|
||||||
#include "hal_time.h"
|
#include "hal_time.h"
|
||||||
#include "i2cbus.h"
|
#include "i2cbus.h"
|
||||||
@@ -125,25 +126,33 @@ static void judge_locked(safety_snap_t *s, int rail, uint32_t now)
|
|||||||
const int32_t ua = s->rail[rail - 1].current_ua;
|
const int32_t ua = s->rail[rail - 1].current_ua;
|
||||||
const int32_t dev = mv - (int32_t)rail_setpoint_mv(rail);
|
const int32_t dev = mv - (int32_t)rail_setpoint_mv(rail);
|
||||||
|
|
||||||
|
/* 刷机模式(chgmode.h,0.1.5):窗口内放开「顶高断 VBUS」;窗口内 + 宽限期内,
|
||||||
|
「过高」按 CHG_MODE_MAX_MV 绝对值判(手机充电器浮充 ~4.4 V 是预期的,超过才是异常)。
|
||||||
|
反灌(电流 < −300 mA)与欠压判据不受影响。 */
|
||||||
|
const bool thru = chgmode_active(rail, now);
|
||||||
|
const bool high = chgmode_relaxed(rail, now) ? (mv > CHG_MODE_MAX_MV) : (dev > T_SAFETY_DEV_MV);
|
||||||
|
|
||||||
/* ---- SW-8(先跑):只在 VBUS 通着的时候武装 -------------------------
|
/* ---- SW-8(先跑):只在 VBUS 通着的时候武装 -------------------------
|
||||||
反灌的能量只可能从 VBUS 那一侧来,断开之后判据自然就解除武装了。
|
反灌的能量只可能从 VBUS 那一侧来,断开之后判据自然就解除武装了。
|
||||||
先跑它是因为过压那种情况两条判据都成立:该先撤掉能量来源(VBUS),
|
先跑它是因为过压那种情况两条判据都成立:该先撤掉能量来源(VBUS),
|
||||||
20 ms 后 SR-5 再决定要不要连这一路一起关掉。 */
|
20 ms 后 SR-5 再决定要不要连这一路一起关掉。
|
||||||
|
刷机模式窗口内「顶高」同样按绝对值判(`high`);窗口一过(宽限期里)回到按设定值判 ——
|
||||||
|
那一刻手机还在充,节点在 4.4 V,就在这里立刻断 VBUS,而 SR-5 仍按绝对值判、不会误关路。 */
|
||||||
if (slot_vbus_is_on(rail)) {
|
if (slot_vbus_is_on(rail)) {
|
||||||
if (ua < T_SAFETY_REV_UA) {
|
if (ua < T_SAFETY_REV_UA) {
|
||||||
cut_vbus_locked(s, rail, SAFETY_EV_REVERSE);
|
cut_vbus_locked(s, rail, SAFETY_EV_REVERSE);
|
||||||
} else if (dev > T_SAFETY_DEV_MV) {
|
} else if (thru ? high : (dev > T_SAFETY_DEV_MV)) {
|
||||||
cut_vbus_locked(s, rail, SAFETY_EV_PUSHED_UP);
|
cut_vbus_locked(s, rail, SAFETY_EV_PUSHED_UP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- SR-5:+100 / −250 mV 连着 T_SAFETY_DEV_HOLD_CYCLES 圈(= 持续 20 ms)---- */
|
/* ---- SR-5:+100 / −250 mV 连着 T_SAFETY_DEV_HOLD_CYCLES 圈(= 持续 20 ms)---- */
|
||||||
if (dev > T_SAFETY_DEV_MV || dev < -T_SAFETY_UNDER_MV) {
|
if (high || dev < -T_SAFETY_UNDER_MV) {
|
||||||
if (st->dev_n < 0xFF) {
|
if (st->dev_n < 0xFF) {
|
||||||
st->dev_n++;
|
st->dev_n++;
|
||||||
}
|
}
|
||||||
if (st->dev_n >= T_SAFETY_DEV_HOLD_CYCLES) {
|
if (st->dev_n >= T_SAFETY_DEV_HOLD_CYCLES) {
|
||||||
trip_rail_locked(s, rail, dev > 0 ? SAFETY_EV_OVER_VOLT : SAFETY_EV_UNDER_VOLT);
|
trip_rail_locked(s, rail, high ? SAFETY_EV_OVER_VOLT : SAFETY_EV_UNDER_VOLT);
|
||||||
/* ⚠️ 这里**故意不**把 `dev_n` 清零。关掉之后下一圈 `en` 就是 false,
|
/* ⚠️ 这里**故意不**把 `dev_n` 清零。关掉之后下一圈 `en` 就是 false,
|
||||||
上面那道闸自己会清;而万一那次写引脚失败了(影子留在 on),
|
上面那道闸自己会清;而万一那次写引脚失败了(影子留在 on),
|
||||||
下一圈 `dev_n` 已经够数,会**立刻再关一次**。 */
|
下一圈 `dev_n` 已经够数,会**立刻再关一次**。 */
|
||||||
@@ -163,6 +172,23 @@ static hal_err_t cycle_rail(void *arg)
|
|||||||
const int rail = a->rail;
|
const int rail = a->rail;
|
||||||
hal_err_t first = HAL_OK;
|
hal_err_t first = HAL_OK;
|
||||||
|
|
||||||
|
/* 热插拔进来的刀片:先把它初始化成开机时的样子,再读、再判(T_SAFETY_INSERT_N 的理由)。 */
|
||||||
|
safety_rail_t *ist = &s->st[rail - 1];
|
||||||
|
if (ist->need_init && ++ist->ins_n >= T_SAFETY_INSERT_N) {
|
||||||
|
ist->ins_n = 0;
|
||||||
|
hal_err_t ie = slot_init_locked(rail);
|
||||||
|
if (ie == HAL_OK) {
|
||||||
|
ie = ina226_init(rail_cfg(rail)->ina_addr);
|
||||||
|
}
|
||||||
|
if (ie == HAL_OK) {
|
||||||
|
ist->need_init = false;
|
||||||
|
s->blade_inits++;
|
||||||
|
} else {
|
||||||
|
s->i2c_errors++;
|
||||||
|
first = ie;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ina226_vi_t vi;
|
ina226_vi_t vi;
|
||||||
hal_err_t e = ina226_read_vi(rail_cfg(rail)->ina_addr, &vi);
|
hal_err_t e = ina226_read_vi(rail_cfg(rail)->ina_addr, &vi);
|
||||||
s->rail_ok[rail - 1] = (e == HAL_OK);
|
s->rail_ok[rail - 1] = (e == HAL_OK);
|
||||||
@@ -231,6 +257,7 @@ void t_safety_init(safety_snap_t *s)
|
|||||||
memset(s, 0, sizeof(*s));
|
memset(s, 0, sizeof(*s));
|
||||||
for (int i = 0; i < PHONERAIL_RAIL_COUNT; i++) {
|
for (int i = 0; i < PHONERAIL_RAIL_COUNT; i++) {
|
||||||
s->present[i] = true; /* 与 slot.c 的初值一致:读到之前当它在位 */
|
s->present[i] = true; /* 与 slot.c 的初值一致:读到之前当它在位 */
|
||||||
|
s->st[i].was_present = true; /* 开机时在位的刀片 bringup 已经初始化过,不算「插进来」 */
|
||||||
}
|
}
|
||||||
/* 游标对齐到「现在」,否则第一圈会把启动期间(器件初始化)的事务算进来。 */
|
/* 游标对齐到「现在」,否则第一圈会把启动期间(器件初始化)的事务算进来。 */
|
||||||
s->bus_cursor = hal_i2c_acct_bits();
|
s->bus_cursor = hal_i2c_acct_bits();
|
||||||
@@ -359,6 +386,20 @@ hal_err_t t_safety_step(safety_snap_t *s)
|
|||||||
在位脚是纯 GPIO,先 poll 再按它挑。`check_present()` 在后面照旧再 poll 一次、照旧负责断电。 */
|
在位脚是纯 GPIO,先 poll 再按它挑。`check_present()` 在后面照旧再 poll 一次、照旧负责断电。 */
|
||||||
slot_prsnt_poll();
|
slot_prsnt_poll();
|
||||||
|
|
||||||
|
/* 认「插进来」:上一圈不在、这一圈在 ⇒ 这片要重新初始化(在 cycle_rail 里、锁内做)。
|
||||||
|
拔走就作废 —— 下次插进来重新数。 */
|
||||||
|
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||||
|
safety_rail_t *st = &s->st[rail - 1];
|
||||||
|
const bool here = slot_present(rail);
|
||||||
|
if (here && !st->was_present) {
|
||||||
|
st->need_init = true;
|
||||||
|
st->ins_n = 0;
|
||||||
|
} else if (!here) {
|
||||||
|
st->need_init = false;
|
||||||
|
}
|
||||||
|
st->was_present = here;
|
||||||
|
}
|
||||||
|
|
||||||
/* 五路,一路一次 `xact`(通道查表,**不许算** —— rail_cfg.h 那条禁令)。 */
|
/* 五路,一路一次 `xact`(通道查表,**不许算** —— rail_cfg.h 那条禁令)。 */
|
||||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||||
if (!slot_present(rail)) {
|
if (!slot_present(rail)) {
|
||||||
|
|||||||
+24
-3
@@ -98,8 +98,11 @@ extern "C" {
|
|||||||
|
|
||||||
/* SW-8 的反灌判据:设计文档 §15.2 对策 2 原话「if I < −50 mA → 立即拉低 VBUS_EN」。
|
/* SW-8 的反灌判据:设计文档 §15.2 对策 2 原话「if I < −50 mA → 立即拉低 VBUS_EN」。
|
||||||
离噪声很远:INA226 电流 LSB 是 0.5 mA,分流失调 ±10 µV / 5 mΩ ≈ ±2 mA,
|
离噪声很远:INA226 电流 LSB 是 0.5 mA,分流失调 ±10 µV / 5 mΩ ≈ ±2 mA,
|
||||||
−50 mA 是它的 25 倍。§12.1 的验收注入 −60 mA,落在阈值外侧。 */
|
−50 mA 是它的 25 倍。§12.1 的验收注入 −60 mA,落在阈值外侧。
|
||||||
#define T_SAFETY_REV_UA (-50000)
|
2026-09-28 用户定:放宽到 −150 mA(刷机重启过渡时实测 −59 mA 误断 VBUS);
|
||||||
|
同日再放宽到 −300 mA(0.1.7 下 adb reboot bootloader 实测 −208.5 mA 仍误断)。
|
||||||
|
测试注入相应改为 −310 mA。 */
|
||||||
|
#define T_SAFETY_REV_UA (-300000)
|
||||||
|
|
||||||
/* 反灌**不做确认窗口**(一圈看见就断),这和 20 ms 的过压判据是有意不对称的:
|
/* 反灌**不做确认窗口**(一圈看见就断),这和 20 ms 的过压判据是有意不对称的:
|
||||||
· 误断 VBUS 的代价是丢一次 USB 数据,手机还由直供电着,没有任何风险;
|
· 误断 VBUS 的代价是丢一次 USB 数据,手机还由直供电着,没有任何风险;
|
||||||
@@ -128,6 +131,18 @@ extern "C" {
|
|||||||
10 圈 = 200 ms:本板实测单路 9 万圈 0 次 NAK,连续 10 次不是毛刺;空槽带 12 V 多 200 ms 无所谓。 */
|
10 圈 = 200 ms:本板实测单路 9 万圈 0 次 NAK,连续 10 次不是毛刺;空槽带 12 V 多 200 ms 无所谓。 */
|
||||||
#define T_SAFETY_BLADE_MUTE_N 10
|
#define T_SAFETY_BLADE_MUTE_N 10
|
||||||
|
|
||||||
|
/* 热插拔(0.1.10,2026-09-28 板 B 实测):刀片**带电插进来**,只有开机 bringup 才做的那套初始化
|
||||||
|
没人做 ——
|
||||||
|
· PCA9555:金手指触点先后接触,SDA/SCL 经上拉先于 +3V3 给它寄生供电,上电复位不干净,
|
||||||
|
寄存器是乱的。实测 `VBUS_EN` 被抬高、TPS22918 常开(C67 = 5 V),而固件的影子是 off ⇒
|
||||||
|
没刷补丁的手机被充到 4.4 V、SR-5 过压关路,SW-8 却从没武装(它看的是影子);
|
||||||
|
同一片刀片插好再给底板上电则一切正常。侧键 / EDL 同理可能被按住。
|
||||||
|
· INA226:CALIB 是复位值 0 ⇒ **电流恒读 0**,反灌判据与开机电流校验全瞎。
|
||||||
|
所以 `PRSNT#` 从高变低、连着在位 T_SAFETY_INSERT_N 圈(金手指插到底、弹片不再抖)之后,
|
||||||
|
在这一路的锁内重做一遍:PCA9555 输出清零 + 方向(VBUS 断、键全松),INA226 配置 + 校准。
|
||||||
|
没成功就再等 T_SAFETY_INSERT_N 圈重试,直到成功或刀片被拔走。开机时就在位的刀片不重做。 */
|
||||||
|
#define T_SAFETY_INSERT_N 5
|
||||||
|
|
||||||
/* ---- A10:事件(§10.7 的「单条」形状) -----------------------------------
|
/* ---- A10:事件(§10.7 的「单条」形状) -----------------------------------
|
||||||
A11 把这条记录塞进环形缓冲,这里只负责**产生**它。 */
|
A11 把这条记录塞进环形缓冲,这里只负责**产生**它。 */
|
||||||
|
|
||||||
@@ -135,7 +150,7 @@ typedef enum {
|
|||||||
SAFETY_EV_NONE = 0,
|
SAFETY_EV_NONE = 0,
|
||||||
SAFETY_EV_OVER_VOLT, /* SR-5:高于设定 >100 mV 持续 20 ms → 关这一路 */
|
SAFETY_EV_OVER_VOLT, /* SR-5:高于设定 >100 mV 持续 20 ms → 关这一路 */
|
||||||
SAFETY_EV_UNDER_VOLT, /* SR-5:低于设定 >100 mV 持续 20 ms → 关这一路 */
|
SAFETY_EV_UNDER_VOLT, /* SR-5:低于设定 >100 mV 持续 20 ms → 关这一路 */
|
||||||
SAFETY_EV_REVERSE, /* SW-8:电流转负(I < −50 mA)→ 断这一路 VBUS */
|
SAFETY_EV_REVERSE, /* SW-8:电流转负(I < −300 mA)→ 断这一路 VBUS */
|
||||||
SAFETY_EV_PUSHED_UP, /* SW-8:VBUS 期间电压被顶高 >+100 mV → 断这一路 VBUS */
|
SAFETY_EV_PUSHED_UP, /* SW-8:VBUS 期间电压被顶高 >+100 mV → 断这一路 VBUS */
|
||||||
/* ⭐ R2 新增两条(计划 §5.8)。放在**末尾**:`evlog` 那边的数字是 flash 格式。 */
|
/* ⭐ R2 新增两条(计划 §5.8)。放在**末尾**:`evlog` 那边的数字是 flash 格式。 */
|
||||||
SAFETY_EV_MUX_LOST, /* TCA9548A 连续选不中 ⇒ 五路全部断电(单点) */
|
SAFETY_EV_MUX_LOST, /* TCA9548A 连续选不中 ⇒ 五路全部断电(单点) */
|
||||||
@@ -167,6 +182,11 @@ typedef struct {
|
|||||||
uint8_t dev_n; /* 连着几圈越界了。回到范围内 / 这一路被关掉就清零 */
|
uint8_t dev_n; /* 连着几圈越界了。回到范围内 / 这一路被关掉就清零 */
|
||||||
uint8_t mute_n; /* 开着电、整片刀片连着几圈不应答。任一颗答应了 / 这一路关着就清零 */
|
uint8_t mute_n; /* 开着电、整片刀片连着几圈不应答。任一颗答应了 / 这一路关着就清零 */
|
||||||
|
|
||||||
|
/* 热插拔(T_SAFETY_INSERT_N 的理由)。 */
|
||||||
|
bool was_present; /* 上一圈的 `PRSNT#`,认「插进来」那一下 */
|
||||||
|
bool need_init; /* 插进来了、还没初始化成功 */
|
||||||
|
uint8_t ins_n; /* 距上次尝试(或插入)过了几圈 */
|
||||||
|
|
||||||
/* 本圈锁内判据留给锁外的两件事(规矩 3:VBUS 锁内断,eFuse 锁外关)。 */
|
/* 本圈锁内判据留给锁外的两件事(规矩 3:VBUS 锁内断,eFuse 锁外关)。 */
|
||||||
uint8_t pend_ev; /* safety_ev_t,0 = 没有 */
|
uint8_t pend_ev; /* safety_ev_t,0 = 没有 */
|
||||||
bool pend_efuse; /* 锁外要把 `EFUSE_EN` 拉低 */
|
bool pend_efuse; /* 锁外要把 `EFUSE_EN` 拉低 */
|
||||||
@@ -196,6 +216,7 @@ typedef struct {
|
|||||||
uint32_t cycles; /* 跑过几圈(含被跳过的) */
|
uint32_t cycles; /* 跑过几圈(含被跳过的) */
|
||||||
uint32_t skipped; /* 其中至少有一路因为拿不到锁被跳过的圈数 */
|
uint32_t skipped; /* 其中至少有一路因为拿不到锁被跳过的圈数 */
|
||||||
uint32_t i2c_errors; /* 累计器件级失败次数(一圈里可能不止一次) */
|
uint32_t i2c_errors; /* 累计器件级失败次数(一圈里可能不止一次) */
|
||||||
|
uint32_t blade_inits; /* 热插拔进来的刀片成功重新初始化过几次 */
|
||||||
uint32_t mux_recovers; /* 脉冲过几次 `MUX_RST#` */
|
uint32_t mux_recovers; /* 脉冲过几次 `MUX_RST#` */
|
||||||
uint32_t mux_trips; /* 因 mux 失联全断过几次 */
|
uint32_t mux_trips; /* 因 mux 失联全断过几次 */
|
||||||
/* mux 失联判据走到哪一步了:0 = 没事 / 1 = 这一轮复位过了 / 2 = 这一轮已经全断。
|
/* mux 失联判据走到哪一步了:0 = 没事 / 1 = 这一轮复位过了 / 2 = 这一轮已经全断。
|
||||||
|
|||||||
+147
-1
@@ -11,6 +11,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "board_pins.h"
|
#include "board_pins.h"
|
||||||
|
#include "chgmode.h"
|
||||||
#include "cmd.h"
|
#include "cmd.h"
|
||||||
#include "dac63204.h"
|
#include "dac63204.h"
|
||||||
#include "evlog.h"
|
#include "evlog.h"
|
||||||
@@ -112,6 +113,7 @@ void setUp(void)
|
|||||||
t_thermal_set_floor(0);
|
t_thermal_set_floor(0);
|
||||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||||
t_actuator_init(NULL, NULL);
|
t_actuator_init(NULL, NULL);
|
||||||
|
chgmode_reset();
|
||||||
cmd_init(NULL);
|
cmd_init(NULL);
|
||||||
|
|
||||||
/* 启动期独占,之后任何一次 I²C 事务没持锁就记违规 —— `set_voltage` 那一笔
|
/* 启动期独占,之后任何一次 I²C 事务没持锁就记违规 —— `set_voltage` 那一笔
|
||||||
@@ -186,7 +188,8 @@ static void test_cmd_numbers_are_frozen(void)
|
|||||||
TEST_ASSERT_EQUAL_INT(11, CMD_MQTT);
|
TEST_ASSERT_EQUAL_INT(11, CMD_MQTT);
|
||||||
TEST_ASSERT_EQUAL_INT(12, CMD_NET);
|
TEST_ASSERT_EQUAL_INT(12, CMD_NET);
|
||||||
TEST_ASSERT_EQUAL_INT(13, CMD_OTA); /* C2 */
|
TEST_ASSERT_EQUAL_INT(13, CMD_OTA); /* C2 */
|
||||||
TEST_ASSERT_EQUAL_INT(14, CMD_COUNT);
|
TEST_ASSERT_EQUAL_INT(14, CMD_FLASH_MODE); /* 0.1.5 刷机模式 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(15, CMD_COUNT);
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL_INT(0, CMD_OK);
|
TEST_ASSERT_EQUAL_INT(0, CMD_OK);
|
||||||
TEST_ASSERT_EQUAL_INT(1, CMD_ERR_PARSE);
|
TEST_ASSERT_EQUAL_INT(1, CMD_ERR_PARSE);
|
||||||
@@ -690,6 +693,144 @@ static void test_small_output_buffer(void)
|
|||||||
TEST_ASSERT_EQUAL_UINT16(300, t_thermal_floor());
|
TEST_ASSERT_EQUAL_UINT16(300, t_thermal_floor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- 0.1.5 刷机模式(flash_mode) ----------------------------------------- */
|
||||||
|
|
||||||
|
static void test_flash_mode_command(void)
|
||||||
|
{
|
||||||
|
/* 关着电的路不许开(与 vbus 同一条规矩),记拒绝事件。 */
|
||||||
|
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true}"), "\"err\":\"state\"");
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, count_events(LOG_EV_CMD_REJECT));
|
||||||
|
|
||||||
|
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"power\",\"rail\":1,\"on\":true}"), "\"ok\":true");
|
||||||
|
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true}"), "\"left\":3600"); /* 默认 60 分钟 */
|
||||||
|
TEST_ASSERT_TRUE(chgmode_active(1, hal_time_now_ms()));
|
||||||
|
evlog_rec_t r;
|
||||||
|
TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, 0, &r));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(1, r.rail);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(CHG_MODE_DEFAULT_MIN, r.arg);
|
||||||
|
|
||||||
|
/* status 每一路报剩余秒数 */
|
||||||
|
ASSERT_HAS(send("{\"cmd\":\"status\"}"), "\"chg\":3600");
|
||||||
|
|
||||||
|
/* 越界 / 参数错 */
|
||||||
|
ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true,\"min\":0}"), "\"err\":\"range\"");
|
||||||
|
ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true,\"min\":121}"), "\"err\":\"range\"");
|
||||||
|
ASSERT_HAS(send("{\"seq\":6,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":false,\"min\":5}"), "\"err\":\"arg\"");
|
||||||
|
ASSERT_HAS(send("{\"seq\":7,\"cmd\":\"flash_mode\",\"rail\":1}"), "\"err\":\"arg\"");
|
||||||
|
|
||||||
|
/* 续期:本路再开一次不占新名额 */
|
||||||
|
ASSERT_HAS(send("{\"seq\":8,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true,\"min\":120}"), "\"left\":7200");
|
||||||
|
|
||||||
|
/* 第三路被拒 */
|
||||||
|
ASSERT_HAS(send("{\"seq\":9,\"cmd\":\"power\",\"rail\":2,\"on\":true}"), "\"ok\":true");
|
||||||
|
ASSERT_HAS(send("{\"seq\":10,\"cmd\":\"power\",\"rail\":3,\"on\":true}"), "\"ok\":true");
|
||||||
|
ASSERT_HAS(send("{\"seq\":11,\"cmd\":\"flash_mode\",\"rail\":2,\"on\":true,\"min\":10}"), "\"ok\":true");
|
||||||
|
ASSERT_HAS(send("{\"seq\":12,\"cmd\":\"flash_mode\",\"rail\":3,\"on\":true}"), "\"err\":\"state\"");
|
||||||
|
TEST_ASSERT_FALSE(chgmode_active(3, hal_time_now_ms()));
|
||||||
|
|
||||||
|
/* 关:回 left 0、记一条 arg=0;腾出的名额第三路就能用了 */
|
||||||
|
ASSERT_HAS(send("{\"seq\":13,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":false}"), "\"left\":0");
|
||||||
|
TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, 3, &r)); /* 开1、续期、开2、关1 */
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(CHG_EV_OFF, r.arg);
|
||||||
|
ASSERT_HAS(send("{\"seq\":14,\"cmd\":\"flash_mode\",\"rail\":3,\"on\":true}"), "\"ok\":true");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 关这一路电 = 刷机模式一并退出(不白占名额),记一条 arg=0。 */
|
||||||
|
static void test_power_off_ends_flash_mode(void)
|
||||||
|
{
|
||||||
|
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"power\",\"rail\":4,\"on\":true}"), "\"ok\":true");
|
||||||
|
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"flash_mode\",\"rail\":4,\"on\":true}"), "\"ok\":true");
|
||||||
|
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"power\",\"rail\":4,\"on\":false}"), "\"ok\":true");
|
||||||
|
TEST_ASSERT_FALSE(chgmode_active(4, hal_time_now_ms()));
|
||||||
|
TEST_ASSERT_EQUAL_INT(2, count_events(LOG_EV_CHG_MODE));
|
||||||
|
evlog_rec_t r;
|
||||||
|
TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, 1, &r));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(CHG_EV_OFF, r.arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自然到期:执行器循环里的 cmd_chgmode_tick 记**一条** arg=255,之后不再重复记。 */
|
||||||
|
static void test_flash_mode_expiry_is_logged_once(void)
|
||||||
|
{
|
||||||
|
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"power\",\"rail\":5,\"on\":true}"), "\"ok\":true");
|
||||||
|
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"flash_mode\",\"rail\":5,\"on\":true,\"min\":1}"), "\"left\":60");
|
||||||
|
cmd_chgmode_tick(hal_time_now_ms());
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, count_events(LOG_EV_CHG_MODE));
|
||||||
|
|
||||||
|
sim_time_advance_ms(60000 + CHG_MODE_GRACE_MS);
|
||||||
|
cmd_chgmode_tick(hal_time_now_ms());
|
||||||
|
cmd_chgmode_tick(hal_time_now_ms() + 100);
|
||||||
|
TEST_ASSERT_EQUAL_INT(2, count_events(LOG_EV_CHG_MODE));
|
||||||
|
evlog_rec_t r;
|
||||||
|
TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, 1, &r));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(5, r.rail);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(CHG_EV_EXPIRED, r.arg);
|
||||||
|
ASSERT_HAS(send("{\"cmd\":\"status\"}"), "\"chg\":0");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 0.1.13:安全判据 / 拔刀片把这一路关了(不经过 power 命令)⇒ 下一次 tick 退出刷机模式、记 arg=254、
|
||||||
|
让出名额;硬重启中间 eFuse 关着的那几秒不算。 */
|
||||||
|
static void test_flash_mode_ends_when_the_rail_loses_power(void)
|
||||||
|
{
|
||||||
|
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"power\",\"rail\":1,\"on\":true}"), "\"ok\":true");
|
||||||
|
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"power\",\"rail\":2,\"on\":true}"), "\"ok\":true");
|
||||||
|
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"power\",\"rail\":3,\"on\":true}"), "\"ok\":true");
|
||||||
|
ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true}"), "\"ok\":true");
|
||||||
|
ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"flash_mode\",\"rail\":2,\"on\":true}"), "\"ok\":true");
|
||||||
|
ASSERT_HAS(send("{\"seq\":6,\"cmd\":\"flash_mode\",\"rail\":3,\"on\":true}"), "\"err\":\"state\"");
|
||||||
|
|
||||||
|
/* 硬重启:HR_OFF 期间 eFuse 关着,但这是序列的一部分 —— 不退。 */
|
||||||
|
ASSERT_HAS(send("{\"seq\":7,\"cmd\":\"power_cycle\",\"rail\":1}"), "\"ok\":true");
|
||||||
|
TEST_ASSERT_FALSE(rail_efuse_is_on(1));
|
||||||
|
cmd_chgmode_tick(hal_time_now_ms());
|
||||||
|
TEST_ASSERT_TRUE(chgmode_active(1, hal_time_now_ms()));
|
||||||
|
|
||||||
|
/* 第 2 路被 t_safety 关了(这里直接拉低 EN 扮演):下一次 tick 退出。 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(2, false));
|
||||||
|
const int before = count_events(LOG_EV_CHG_MODE);
|
||||||
|
cmd_chgmode_tick(hal_time_now_ms());
|
||||||
|
TEST_ASSERT_FALSE(chgmode_active(2, hal_time_now_ms()));
|
||||||
|
TEST_ASSERT_EQUAL_INT(before + 1, count_events(LOG_EV_CHG_MODE));
|
||||||
|
evlog_rec_t r;
|
||||||
|
TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, before, &r));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(2, r.rail);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(CHG_EV_DROPPED, r.arg);
|
||||||
|
cmd_chgmode_tick(hal_time_now_ms() + 100);
|
||||||
|
TEST_ASSERT_EQUAL_INT(before + 1, count_events(LOG_EV_CHG_MODE)); /* 只记一次 */
|
||||||
|
|
||||||
|
/* 名额让出来了。 */
|
||||||
|
ASSERT_HAS(send("{\"seq\":8,\"cmd\":\"flash_mode\",\"rail\":3,\"on\":true}"), "\"ok\":true");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 0.1.6 加钟:只给开着的窗口加;剩余最多 120 分钟(超出截断);与 on / min 互斥;事件 arg = 0x80 | 分钟。 */
|
||||||
|
static void test_flash_mode_extend(void)
|
||||||
|
{
|
||||||
|
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"power\",\"rail\":2,\"on\":true}"), "\"ok\":true");
|
||||||
|
/* 没开着 ⇒ state(要重新开,不是加钟) */
|
||||||
|
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":30}"), "\"err\":\"state\"");
|
||||||
|
|
||||||
|
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"flash_mode\",\"rail\":2,\"on\":true,\"min\":20}"), "\"left\":1200");
|
||||||
|
sim_time_advance_ms(5 * 60000); /* 过了 5 分钟,还剩 15 */
|
||||||
|
ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":30}"), "\"left\":2700"); /* 15 + 30 */
|
||||||
|
evlog_rec_t r;
|
||||||
|
TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, 1, &r));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(CHG_EV_EXTEND | 30u, r.arg);
|
||||||
|
|
||||||
|
/* 截断到 120 分钟 */
|
||||||
|
ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":120}"), "\"left\":7200");
|
||||||
|
|
||||||
|
/* 越界 / 混用 */
|
||||||
|
ASSERT_HAS(send("{\"seq\":6,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":0}"), "\"err\":\"range\"");
|
||||||
|
ASSERT_HAS(send("{\"seq\":7,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":121}"), "\"err\":\"range\"");
|
||||||
|
ASSERT_HAS(send("{\"seq\":8,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":10,\"on\":true}"), "\"err\":\"arg\"");
|
||||||
|
ASSERT_HAS(send("{\"seq\":9,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":10,\"min\":10}"), "\"err\":\"arg\"");
|
||||||
|
ASSERT_HAS(send("{\"seq\":10,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":true}"), "\"err\":\"arg\"");
|
||||||
|
|
||||||
|
/* 加钟不占新名额:两路都开着时照样能加 */
|
||||||
|
ASSERT_HAS(send("{\"seq\":11,\"cmd\":\"power\",\"rail\":3,\"on\":true}"), "\"ok\":true");
|
||||||
|
ASSERT_HAS(send("{\"seq\":12,\"cmd\":\"flash_mode\",\"rail\":3,\"on\":true}"), "\"ok\":true");
|
||||||
|
ASSERT_HAS(send("{\"seq\":13,\"cmd\":\"flash_mode\",\"rail\":3,\"add\":10}"), "\"left\":4200");
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
@@ -709,6 +850,11 @@ int main(void)
|
|||||||
RUN_TEST(test_busy_rail_refuses_a_second_sequence);
|
RUN_TEST(test_busy_rail_refuses_a_second_sequence);
|
||||||
RUN_TEST(test_reboots_are_logged_with_kind_and_link);
|
RUN_TEST(test_reboots_are_logged_with_kind_and_link);
|
||||||
RUN_TEST(test_vbus_needs_the_rail_powered);
|
RUN_TEST(test_vbus_needs_the_rail_powered);
|
||||||
|
RUN_TEST(test_flash_mode_command);
|
||||||
|
RUN_TEST(test_power_off_ends_flash_mode);
|
||||||
|
RUN_TEST(test_flash_mode_expiry_is_logged_once);
|
||||||
|
RUN_TEST(test_flash_mode_ends_when_the_rail_loses_power);
|
||||||
|
RUN_TEST(test_flash_mode_extend);
|
||||||
RUN_TEST(test_power_off_drops_vbus_and_always_drops_the_efuse);
|
RUN_TEST(test_power_off_drops_vbus_and_always_drops_the_efuse);
|
||||||
RUN_TEST(test_power_records_intent_only_when_it_took_effect);
|
RUN_TEST(test_power_records_intent_only_when_it_took_effect);
|
||||||
RUN_TEST(test_fan_floor);
|
RUN_TEST(test_fan_floor);
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ static void test_flash_format_is_frozen(void)
|
|||||||
TEST_ASSERT_EQUAL_INT(18, LOG_EV_OTA); /* C2 */
|
TEST_ASSERT_EQUAL_INT(18, LOG_EV_OTA); /* C2 */
|
||||||
TEST_ASSERT_EQUAL_INT(19, LOG_EV_BOOT_CHECK); /* 0.1.2 开机校验 */
|
TEST_ASSERT_EQUAL_INT(19, LOG_EV_BOOT_CHECK); /* 0.1.2 开机校验 */
|
||||||
TEST_ASSERT_EQUAL_INT(20, LOG_EV_BLADE_MUTE); /* 0.1.4 刀片不应答 */
|
TEST_ASSERT_EQUAL_INT(20, LOG_EV_BLADE_MUTE); /* 0.1.4 刀片不应答 */
|
||||||
TEST_ASSERT_EQUAL_INT(21, LOG_EV_COUNT);
|
TEST_ASSERT_EQUAL_INT(21, LOG_EV_CHG_MODE); /* 0.1.5 刷机模式 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(22, LOG_EV_COUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* §10.7 的「必记事件」有八类,A10 只产生其中两类。剩下的号先占住,
|
/* §10.7 的「必记事件」有八类,A10 只产生其中两类。剩下的号先占住,
|
||||||
|
|||||||
@@ -65,6 +65,19 @@ static hal_err_t cb_recover_inside(void *arg)
|
|||||||
return HAL_OK;
|
return HAL_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 事务进行中,**另一个任务**也来要总线(sim 的假任务号 1 扮演执行器)。 */
|
||||||
|
static hal_err_t cb_other_task(void *arg)
|
||||||
|
{
|
||||||
|
ctx_t *c = (ctx_t *)arg;
|
||||||
|
c->calls++;
|
||||||
|
static ctx_t inner_ctx;
|
||||||
|
inner_ctx.addr = BLADE_INA;
|
||||||
|
sim_lock_set_task(1);
|
||||||
|
c->inner = i2cbus_xact(1, 5, cb_read_mv, &inner_ctx);
|
||||||
|
sim_lock_set_task(0);
|
||||||
|
return HAL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
/* 在事务里再调一次 `i2cbus_xact()` —— 头文件末尾那条警告。 */
|
/* 在事务里再调一次 `i2cbus_xact()` —— 头文件末尾那条警告。 */
|
||||||
static hal_err_t cb_nested(void *arg)
|
static hal_err_t cb_nested(void *arg)
|
||||||
{
|
{
|
||||||
@@ -444,6 +457,23 @@ static void test_recover_is_rejected_inside_a_transaction(void)
|
|||||||
不拦的话会在同一个任务里第二次 take 同一把锁 —— sim 上是 HAL_ERR_STATE,
|
不拦的话会在同一个任务里第二次 take 同一把锁 —— sim 上是 HAL_ERR_STATE,
|
||||||
FreeRTOS 上(非递归的 mutex)就是那个任务当场停住,
|
FreeRTOS 上(非递归的 mutex)就是那个任务当场停住,
|
||||||
现象是 `t_safety` 整个不跑了,而看门狗之外没有任何日志。 */
|
现象是 `t_safety` 整个不跑了,而看门狗之外没有任何日志。 */
|
||||||
|
/* ⭐ 0.1.11:别的任务正持着锁 ≠ 嵌套。它该**等锁**(超时回 TIMEOUT),而不是回 STATE。
|
||||||
|
原来的全局标志把两者混为一谈,执行器撞上巡检就回 STATE ⇒ `key` 随机 busy、`vbus` 随机 io。 */
|
||||||
|
static void test_another_tasks_xact_waits_instead_of_looking_nested(void)
|
||||||
|
{
|
||||||
|
mount_ready();
|
||||||
|
ctx_t c = { BLADE_INA, 0, -1, HAL_OK };
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_xact(0, 10, cb_other_task, &c));
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(1, c.calls);
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_ERR_TIMEOUT, c.inner);
|
||||||
|
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C));
|
||||||
|
/* 锁放了之后,那个任务再来就拿得到。 */
|
||||||
|
sim_lock_set_task(1);
|
||||||
|
ctx_t d = { BLADE_INA, 0, -1, HAL_OK };
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_xact(1, 5, cb_read_mv, &d));
|
||||||
|
sim_lock_set_task(0);
|
||||||
|
}
|
||||||
|
|
||||||
static void test_a_nested_xact_is_rejected_and_does_not_deadlock(void)
|
static void test_a_nested_xact_is_rejected_and_does_not_deadlock(void)
|
||||||
{
|
{
|
||||||
mount_ready();
|
mount_ready();
|
||||||
@@ -604,6 +634,7 @@ int main(void)
|
|||||||
RUN_TEST(test_recover_pulses_mux_rst_and_forgets_the_channel);
|
RUN_TEST(test_recover_pulses_mux_rst_and_forgets_the_channel);
|
||||||
RUN_TEST(test_recover_is_rejected_inside_a_transaction);
|
RUN_TEST(test_recover_is_rejected_inside_a_transaction);
|
||||||
RUN_TEST(test_a_nested_xact_is_rejected_and_does_not_deadlock);
|
RUN_TEST(test_a_nested_xact_is_rejected_and_does_not_deadlock);
|
||||||
|
RUN_TEST(test_another_tasks_xact_waits_instead_of_looking_nested);
|
||||||
RUN_TEST(test_a_lock_timeout_changes_nothing_at_all);
|
RUN_TEST(test_a_lock_timeout_changes_nothing_at_all);
|
||||||
RUN_TEST(test_xact_rejects_bad_arguments_before_taking_the_lock);
|
RUN_TEST(test_xact_rejects_bad_arguments_before_taking_the_lock);
|
||||||
RUN_TEST(test_forget_channel_only_drops_the_shadow);
|
RUN_TEST(test_forget_channel_only_drops_the_shadow);
|
||||||
|
|||||||
+104
-6
@@ -97,8 +97,8 @@ static void test_config_over_usb_is_saved_and_applied(void)
|
|||||||
"\"ok\":true");
|
"\"ok\":true");
|
||||||
TEST_ASSERT_EQUAL_INT(2, s_saves);
|
TEST_ASSERT_EQUAL_INT(2, s_saves);
|
||||||
TEST_ASSERT_EQUAL_INT(2, s_changed);
|
TEST_ASSERT_EQUAL_INT(2, s_changed);
|
||||||
TEST_ASSERT_EQUAL_STRING("farm-2g", s_saved.ssid);
|
TEST_ASSERT_EQUAL_STRING("farm-2g", s_saved.wifi[0].ssid);
|
||||||
TEST_ASSERT_EQUAL_STRING("s3cret-pass", s_saved.pass); /* 第二次存的是整份:WiFi 那部分还在 */
|
TEST_ASSERT_EQUAL_STRING("s3cret-pass", s_saved.wifi[0].pass); /* 第二次存的是整份:WiFi 那部分还在 */
|
||||||
TEST_ASSERT_EQUAL_STRING("mqtt://10.5.5.13:1883", s_saved.uri);
|
TEST_ASSERT_EQUAL_STRING("mqtt://10.5.5.13:1883", s_saved.uri);
|
||||||
TEST_ASSERT_EQUAL_UINT32(2000, s_saved.tele_ms);
|
TEST_ASSERT_EQUAL_UINT32(2000, s_saved.tele_ms);
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ static void test_config_validation(void)
|
|||||||
pass[63] = '\0';
|
pass[63] = '\0';
|
||||||
snprintf(line, sizeof(line), "{\"seq\":11,\"cmd\":\"wifi\",\"ssid\":\"x\",\"pass\":\"%s\"}", pass);
|
snprintf(line, sizeof(line), "{\"seq\":11,\"cmd\":\"wifi\",\"ssid\":\"x\",\"pass\":\"%s\"}", pass);
|
||||||
ASSERT_HAS(send_on(CMD_LINK_USB, line), "\"ok\":true");
|
ASSERT_HAS(send_on(CMD_LINK_USB, line), "\"ok\":true");
|
||||||
TEST_ASSERT_EQUAL_STRING(pass, s_saved.pass);
|
TEST_ASSERT_EQUAL_STRING(pass, s_saved.wifi[1].pass);
|
||||||
TEST_ASSERT_EQUAL_INT(3, s_saves);
|
TEST_ASSERT_EQUAL_INT(3, s_saves);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ static void test_unsaved_config_does_not_take_effect(void)
|
|||||||
"\"err\":\"io\"");
|
"\"err\":\"io\"");
|
||||||
netcfg_t c;
|
netcfg_t c;
|
||||||
netcfg_get(&c);
|
netcfg_get(&c);
|
||||||
TEST_ASSERT_EQUAL_STRING("old", c.ssid);
|
TEST_ASSERT_EQUAL_STRING("old", c.wifi[0].ssid);
|
||||||
TEST_ASSERT_EQUAL_INT(1, s_changed);
|
TEST_ASSERT_EQUAL_INT(1, s_changed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,11 +197,11 @@ static void test_loaded_config_is_sanitised(void)
|
|||||||
netcfg_t junk;
|
netcfg_t junk;
|
||||||
memset(&junk, 'A', sizeof(junk));
|
memset(&junk, 'A', sizeof(junk));
|
||||||
junk.tele_ms = 5;
|
junk.tele_ms = 5;
|
||||||
memcpy(junk.ssid, "a\"b\\c\n", 6);
|
memcpy(junk.wifi[0].ssid, "a\"b\\c\n", 6);
|
||||||
netcfg_init(&junk, NULL, NULL, NULL);
|
netcfg_init(&junk, NULL, NULL, NULL);
|
||||||
netcfg_t c;
|
netcfg_t c;
|
||||||
netcfg_get(&c);
|
netcfg_get(&c);
|
||||||
TEST_ASSERT_EQUAL_size_t(NETCFG_SSID_MAX, strlen(c.ssid));
|
TEST_ASSERT_EQUAL_size_t(NETCFG_SSID_MAX, strlen(c.wifi[0].ssid));
|
||||||
TEST_ASSERT_EQUAL_size_t(NETCFG_URI_MAX, strlen(c.uri));
|
TEST_ASSERT_EQUAL_size_t(NETCFG_URI_MAX, strlen(c.uri));
|
||||||
TEST_ASSERT_EQUAL_UINT32(NETCFG_TELE_DEFAULT_MS, c.tele_ms);
|
TEST_ASSERT_EQUAL_UINT32(NETCFG_TELE_DEFAULT_MS, c.tele_ms);
|
||||||
|
|
||||||
@@ -210,6 +210,101 @@ static void test_loaded_config_is_sanitised(void)
|
|||||||
TEST_ASSERT_EQUAL_CHAR('}', r[strlen(r) - 1]);
|
TEST_ASSERT_EQUAL_CHAR('}', r[strlen(r) - 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- 多组 WiFi(0.1.9)--------------------------------------------------- */
|
||||||
|
|
||||||
|
/* ⭐ 加第二组不挤掉第一组;同名再发 = 改口令、不多占一格;回应带组数。 */
|
||||||
|
static void test_wifi_adds_and_updates_by_name(void)
|
||||||
|
{
|
||||||
|
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":1,\"cmd\":\"wifi\",\"ssid\":\"home\",\"pass\":\"pass-home-1\"}"),
|
||||||
|
"\"n\":1");
|
||||||
|
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":2,\"cmd\":\"wifi\",\"ssid\":\"hotspot\",\"pass\":\"pass-hot-1\"}"),
|
||||||
|
"\"n\":2");
|
||||||
|
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":3,\"cmd\":\"wifi\",\"ssid\":\"home\",\"pass\":\"pass-home-2\"}"),
|
||||||
|
"\"n\":2");
|
||||||
|
netcfg_t c;
|
||||||
|
netcfg_get(&c);
|
||||||
|
TEST_ASSERT_EQUAL_INT(2, netcfg_wifi_count(&c));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("home", c.wifi[0].ssid);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("pass-home-2", c.wifi[0].pass);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("hotspot", c.wifi[1].ssid);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("pass-hot-1", c.wifi[1].pass);
|
||||||
|
TEST_ASSERT_EQUAL_MEMORY(&s_saved, &c, sizeof(c)); /* 存的是整张表 */
|
||||||
|
|
||||||
|
/* net:ssid = 正在连的那组(状态里 wifi_idx),ssids = 整张表;任何一组的口令都不出现。 */
|
||||||
|
net_status_t st;
|
||||||
|
memset(&st, 0, sizeof(st));
|
||||||
|
st.wifi_idx = 1;
|
||||||
|
net_status_set(&st);
|
||||||
|
const char *r = send_on(CMD_LINK_WIFI, "{\"cmd\":\"net\"}");
|
||||||
|
ASSERT_HAS(r, "\"ssid\":\"hotspot\",\"pass_set\":true,\"ssids\":[\"home\",\"hotspot\"]");
|
||||||
|
ASSERT_LACKS(r, "pass-home");
|
||||||
|
ASSERT_LACKS(r, "pass-hot");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ⭐ 删一组后面的往前补;删没有的 = range;满了再加新名字 = state、表不变;满了改已有的照样行。 */
|
||||||
|
static void test_wifi_delete_and_full_table(void)
|
||||||
|
{
|
||||||
|
char line[128];
|
||||||
|
for (int i = 0; i < NETCFG_WIFI_N; i++) {
|
||||||
|
snprintf(line, sizeof(line), "{\"seq\":%d,\"cmd\":\"wifi\",\"ssid\":\"n%d\",\"pass\":\"12345678\"}",
|
||||||
|
i + 1, i);
|
||||||
|
ASSERT_HAS(send_on(CMD_LINK_USB, line), "\"ok\":true");
|
||||||
|
}
|
||||||
|
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":20,\"cmd\":\"wifi\",\"ssid\":\"extra\",\"pass\":\"12345678\"}"),
|
||||||
|
"\"err\":\"state\"");
|
||||||
|
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":21,\"cmd\":\"wifi\",\"ssid\":\"n3\",\"pass\":\"87654321\"}"),
|
||||||
|
"\"ok\":true");
|
||||||
|
TEST_ASSERT_EQUAL_INT(NETCFG_WIFI_N + 1, s_saves);
|
||||||
|
|
||||||
|
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":22,\"cmd\":\"wifi\",\"ssid\":\"n1\",\"del\":true}"),
|
||||||
|
"\"n\":3");
|
||||||
|
netcfg_t c;
|
||||||
|
netcfg_get(&c);
|
||||||
|
TEST_ASSERT_EQUAL_INT(NETCFG_WIFI_N - 1, netcfg_wifi_count(&c));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("n0", c.wifi[0].ssid);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("n2", c.wifi[1].ssid);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("n3", c.wifi[2].ssid);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("87654321", c.wifi[2].pass);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", c.wifi[3].ssid);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", c.wifi[3].pass); /* 挪走之后末尾清干净,不留口令 */
|
||||||
|
|
||||||
|
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":23,\"cmd\":\"wifi\",\"ssid\":\"n1\",\"del\":true}"),
|
||||||
|
"\"err\":\"range\"");
|
||||||
|
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":24,\"cmd\":\"wifi\",\"ssid\":\"n1\",\"del\":1}"),
|
||||||
|
"\"err\":\"arg\"");
|
||||||
|
/* 删也只收 USB。 */
|
||||||
|
ASSERT_HAS(send_on(CMD_LINK_WIFI, "{\"seq\":25,\"cmd\":\"wifi\",\"ssid\":\"n0\",\"del\":true}"),
|
||||||
|
"\"err\":\"link\"");
|
||||||
|
/* 删到空:net 回应仍是合法 JSON,ssids 是空数组。 */
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
snprintf(line, sizeof(line), "{\"seq\":%d,\"cmd\":\"wifi\",\"ssid\":\"%s\",\"del\":true}", 30 + i,
|
||||||
|
i == 0 ? "n0" : i == 1 ? "n2" : "n3");
|
||||||
|
ASSERT_HAS(send_on(CMD_LINK_USB, line), "\"ok\":true");
|
||||||
|
}
|
||||||
|
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"cmd\":\"net\"}"), "\"ssid\":\"\",\"pass_set\":false,\"ssids\":[]");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NVS 读回来的表中间有空位:挤到前面,count 才算得对、轮换才不会停在空位上。 */
|
||||||
|
static void test_loaded_table_is_compacted(void)
|
||||||
|
{
|
||||||
|
netcfg_t in;
|
||||||
|
memset(&in, 0, sizeof(in));
|
||||||
|
strcpy(in.wifi[1].ssid, "b");
|
||||||
|
strcpy(in.wifi[1].pass, "12345678");
|
||||||
|
strcpy(in.wifi[3].ssid, "d");
|
||||||
|
strcpy(in.wifi[2].pass, "orphan-pass"); /* 空 SSID 带着口令:整格丢掉 */
|
||||||
|
netcfg_init(&in, NULL, NULL, NULL);
|
||||||
|
netcfg_t c;
|
||||||
|
netcfg_get(&c);
|
||||||
|
TEST_ASSERT_EQUAL_INT(2, netcfg_wifi_count(&c));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("b", c.wifi[0].ssid);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("12345678", c.wifi[0].pass);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("d", c.wifi[1].ssid);
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, netcfg_wifi_find(&c, "d"));
|
||||||
|
TEST_ASSERT_EQUAL_INT(-1, netcfg_wifi_find(&c, ""));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", c.wifi[2].pass);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- MQTT 链路的平台无关部分 --------------------------------------------- */
|
/* ---- MQTT 链路的平台无关部分 --------------------------------------------- */
|
||||||
|
|
||||||
static void test_topics_follow_the_mac(void)
|
static void test_topics_follow_the_mac(void)
|
||||||
@@ -287,6 +382,9 @@ int main(void)
|
|||||||
RUN_TEST(test_config_validation);
|
RUN_TEST(test_config_validation);
|
||||||
RUN_TEST(test_unsaved_config_does_not_take_effect);
|
RUN_TEST(test_unsaved_config_does_not_take_effect);
|
||||||
RUN_TEST(test_loaded_config_is_sanitised);
|
RUN_TEST(test_loaded_config_is_sanitised);
|
||||||
|
RUN_TEST(test_wifi_adds_and_updates_by_name);
|
||||||
|
RUN_TEST(test_wifi_delete_and_full_table);
|
||||||
|
RUN_TEST(test_loaded_table_is_compacted);
|
||||||
RUN_TEST(test_topics_follow_the_mac);
|
RUN_TEST(test_topics_follow_the_mac);
|
||||||
RUN_TEST(test_inbound_messages);
|
RUN_TEST(test_inbound_messages);
|
||||||
RUN_TEST(test_backoff_doubles_and_caps);
|
RUN_TEST(test_backoff_doubles_and_caps);
|
||||||
|
|||||||
@@ -1231,6 +1231,113 @@ static void test_boot_check_first_press_boots(void)
|
|||||||
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C));
|
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ⭐ 0.1.11(板 B 实测):VBUS 通着做硬重启 —— 手机靠 USB 撑着没断电、直供电流 ≈ 0 ⇒
|
||||||
|
校验判「没开起来」再按两下、断电时带着 USB 进关机充电。现在:断电前先断 VBUS,
|
||||||
|
整个开机 + 校验期间都断着,开机校验通过才接回。 */
|
||||||
|
static void test_hard_reset_cuts_vbus_first_and_restores_it_after_boot(void)
|
||||||
|
{
|
||||||
|
const int rail = 4;
|
||||||
|
s_pg_val[rail - 1] = true;
|
||||||
|
phone_up(1, false);
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(rail, true));
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN)); /* 断电之前就断了 */
|
||||||
|
run_phone_ms(rail, HR_TO_PRESS_MS + ACT_HR_BOOT_KEY_MS + 10);
|
||||||
|
TEST_ASSERT_EQUAL_INT(ACT_ST_HR_VERIFY, t_actuator_state(rail));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN)); /* 校验期间也断着 */
|
||||||
|
|
||||||
|
for (int i = 0; i < 1000 && t_actuator_busy(rail); i++) {
|
||||||
|
run_phone_ms(rail, 10); /* 走到校验刚结束的那一刻,从这里起算 60 s */
|
||||||
|
}
|
||||||
|
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
|
||||||
|
TEST_ASSERT_TRUE(s_boot_booted);
|
||||||
|
/* 开起来了,但**还不接回**:这时手机在 bootloader / 内核早期,关充电补丁没生效,
|
||||||
|
接回 USB 就被充到 4.4 V(板 B 实测 13 s 顶高断 VBUS)。执行器已空闲、不挡命令。 */
|
||||||
|
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
||||||
|
TEST_ASSERT_TRUE(t_actuator_vbus_back_pending(rail));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN));
|
||||||
|
run_phone_ms(rail, ACT_HR_VBUS_BACK_MS - 1000);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN));
|
||||||
|
run_phone_ms(rail, 1500);
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_VBUS_EN)); /* 到点:原样接回 */
|
||||||
|
TEST_ASSERT_TRUE(slot_vbus_is_on(rail));
|
||||||
|
TEST_ASSERT_FALSE(t_actuator_vbus_back_pending(rail));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 等接回的这 60 s 里:有人手动动了 VBUS / 电,或者这一路被关了电 ⇒ 不再接回。 */
|
||||||
|
static void test_a_pending_vbus_restore_is_dropped_when_someone_else_acts(void)
|
||||||
|
{
|
||||||
|
const int rail = 1;
|
||||||
|
s_pg_val[rail - 1] = true;
|
||||||
|
|
||||||
|
/* ① 人手动断 VBUS(cmd 层会调 cancel)。 */
|
||||||
|
phone_up(1, false);
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(rail, true));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
|
||||||
|
run_phone_ms(rail, HR_TO_PRESS_MS + ONE_TRY_MS + 100);
|
||||||
|
TEST_ASSERT_TRUE(t_actuator_vbus_back_pending(rail));
|
||||||
|
t_actuator_cancel_vbus_back(rail);
|
||||||
|
run_phone_ms(rail, ACT_HR_VBUS_BACK_MS + 1000);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN));
|
||||||
|
|
||||||
|
/* ② 这一路在等的时候被关了电(安全判据 / 拔刀片):到点也不接。 */
|
||||||
|
phone_up(1, false);
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(rail, true));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
|
||||||
|
run_phone_ms(rail, HR_TO_PRESS_MS + ONE_TRY_MS + 100);
|
||||||
|
TEST_ASSERT_TRUE(t_actuator_vbus_back_pending(rail));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(rail, false));
|
||||||
|
run_phone_ms(rail, ACT_HR_VBUS_BACK_MS + 1000);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN));
|
||||||
|
TEST_ASSERT_FALSE(t_actuator_vbus_back_pending(rail));
|
||||||
|
|
||||||
|
/* ③ 再来一次硬重启:上一次排的接回作废(新的一次从头算)。 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(rail, true));
|
||||||
|
phone_up(1, false);
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(rail, true));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
|
||||||
|
run_phone_ms(rail, HR_TO_PRESS_MS + ONE_TRY_MS + 100);
|
||||||
|
TEST_ASSERT_TRUE(t_actuator_vbus_back_pending(rail));
|
||||||
|
phone_up(1, false);
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail)); /* VBUS 这时是断的 */
|
||||||
|
TEST_ASSERT_FALSE(t_actuator_vbus_back_pending(rail));
|
||||||
|
run_phone_ms(rail, HR_TO_PRESS_MS + ONE_TRY_MS + ACT_HR_VBUS_BACK_MS + 1000);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 没开起来 / 被打断:VBUS **不接回** —— 接回去就是让一台没开机的手机进关机充电。
|
||||||
|
开始时本来就没通的,成功了也不许凭空打开。 */
|
||||||
|
static void test_hard_reset_leaves_vbus_off_unless_the_phone_booted(void)
|
||||||
|
{
|
||||||
|
const int rail = 2;
|
||||||
|
s_pg_val[rail - 1] = true;
|
||||||
|
phone_up(0, false);
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(rail, true));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
|
||||||
|
run_phone_ms(rail, HR_TO_PRESS_MS + 3 * ONE_TRY_MS + 1000);
|
||||||
|
TEST_ASSERT_EQUAL_INT(ACT_RES_NO_BOOT, t_actuator_last_result(rail));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN));
|
||||||
|
TEST_ASSERT_FALSE(slot_vbus_is_on(rail));
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(rail, true));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
|
||||||
|
run_phone_ms(rail, ACT_HR_OFF_MS / 2);
|
||||||
|
t_actuator_abort(rail);
|
||||||
|
run_phone_ms(rail, ONE_TRY_MS);
|
||||||
|
TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, t_actuator_last_result(rail));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN));
|
||||||
|
|
||||||
|
const int r5 = 5;
|
||||||
|
s_pg_val[r5 - 1] = true;
|
||||||
|
phone_up(1, false);
|
||||||
|
TEST_ASSERT_FALSE(slot_vbus_is_on(r5));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(r5));
|
||||||
|
run_phone_ms(r5, HR_TO_PRESS_MS + ONE_TRY_MS + 1000);
|
||||||
|
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(r5));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, pin(r5, PCA9555_BIT_VBUS_EN));
|
||||||
|
}
|
||||||
|
|
||||||
/* ⭐ 这次的主症状:第一下没压上,第二下开了。 */
|
/* ⭐ 这次的主症状:第一下没压上,第二下开了。 */
|
||||||
static void test_boot_check_presses_again_when_the_phone_stays_off(void)
|
static void test_boot_check_presses_again_when_the_phone_stays_off(void)
|
||||||
{
|
{
|
||||||
@@ -1375,6 +1482,9 @@ int main(void)
|
|||||||
RUN_TEST(test_actuator_and_safety_share_one_lock_without_starving);
|
RUN_TEST(test_actuator_and_safety_share_one_lock_without_starving);
|
||||||
|
|
||||||
RUN_TEST(test_boot_check_first_press_boots);
|
RUN_TEST(test_boot_check_first_press_boots);
|
||||||
|
RUN_TEST(test_hard_reset_cuts_vbus_first_and_restores_it_after_boot);
|
||||||
|
RUN_TEST(test_hard_reset_leaves_vbus_off_unless_the_phone_booted);
|
||||||
|
RUN_TEST(test_a_pending_vbus_restore_is_dropped_when_someone_else_acts);
|
||||||
RUN_TEST(test_boot_check_presses_again_when_the_phone_stays_off);
|
RUN_TEST(test_boot_check_presses_again_when_the_phone_stays_off);
|
||||||
RUN_TEST(test_boot_check_gives_up_after_three_presses_and_keeps_power);
|
RUN_TEST(test_boot_check_gives_up_after_three_presses_and_keeps_power);
|
||||||
RUN_TEST(test_boot_check_does_not_press_again_when_current_is_unreadable);
|
RUN_TEST(test_boot_check_does_not_press_again_when_current_is_unreadable);
|
||||||
|
|||||||
@@ -543,6 +543,99 @@ static void test_empty_slots_are_never_touched_and_the_input_still_reads(void)
|
|||||||
TEST_ASSERT_EQUAL_INT32(12000, g_snap.input.bus_mv);
|
TEST_ASSERT_EQUAL_INT32(12000, g_snap.input.bus_mv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 把第 rail 槽的刀片拔走 / 插回(PRSNT# + 两颗器件一起)。 */
|
||||||
|
static void seat_blade(int rail, bool in)
|
||||||
|
{
|
||||||
|
sim_gpio_drive(rail_cfg(rail)->prsnt_n, in ? 0 : 1);
|
||||||
|
sim_i2c_set_present_ch(ch_of(rail), rail_cfg(rail)->ina_addr, in);
|
||||||
|
sim_i2c_set_present_ch(ch_of(rail), PCA9555_ADDR, in);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ⭐ 热插拔(0.1.10,板 B 实测):带电插进来的刀片寄存器是乱的 —— VBUS_EN 被配成输出高
|
||||||
|
(TPS22918 常开、C67 = 5 V,影子却是 off),INA226 的 CALIB 是 0(电流恒读 0)。
|
||||||
|
插到底、稳住 T_SAFETY_INSERT_N 圈之后,巡检必须把它重做成开机时的样子;只做一次。 */
|
||||||
|
static void test_a_hot_plugged_blade_is_initialised_like_at_boot(void)
|
||||||
|
{
|
||||||
|
const int rail = 3;
|
||||||
|
const int ch = ch_of(rail);
|
||||||
|
const uint8_t ina = rail_cfg(rail)->ina_addr;
|
||||||
|
|
||||||
|
/* 开机就在位的刀片:跑多少圈都不重做。 */
|
||||||
|
for (int i = 0; i < 3 * T_SAFETY_INSERT_N; i++) {
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||||||
|
}
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(0, g_snap.blade_inits);
|
||||||
|
|
||||||
|
seat_blade(rail, false);
|
||||||
|
(void)t_safety_step(&g_snap);
|
||||||
|
|
||||||
|
/* 插回来的是一片「上电复位没做干净」的刀片。 */
|
||||||
|
sim_i2c_set_reg_ch(ch, PCA9555_ADDR, PCA9555_REG_OUTPUT0, 0xFF);
|
||||||
|
sim_i2c_set_reg_ch(ch, PCA9555_ADDR, PCA9555_REG_CONFIG0, 0xEF); /* P0.4 = 输出 */
|
||||||
|
sim_i2c_set_reg_ch(ch, ina, INA226_REG_CALIB, 0);
|
||||||
|
seat_blade(rail, true);
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, sim_pca9555_pin(rail - 1, 0, PCA9555_BIT_VBUS_EN));
|
||||||
|
TEST_ASSERT_FALSE(slot_vbus_is_on(rail)); /* 影子说断着 —— 这正是危险所在 */
|
||||||
|
|
||||||
|
for (int i = 0; i < T_SAFETY_INSERT_N - 1; i++) {
|
||||||
|
(void)t_safety_step(&g_snap);
|
||||||
|
}
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(0, g_snap.blade_inits); /* 还在等金手指插稳 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, sim_pca9555_pin(rail - 1, 0, PCA9555_BIT_VBUS_EN));
|
||||||
|
/* 乱寄存器本身就是假器件认的危险态(锁存器 0xFF + Port0 有输出位),它一直在记数;
|
||||||
|
要的是初始化之后**不再**增加。 */
|
||||||
|
TEST_ASSERT_TRUE(sim_pca9555_unsafe_enable() > 0);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||||||
|
const uint32_t unsafe0 = sim_pca9555_unsafe_enable();
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(1, g_snap.blade_inits);
|
||||||
|
TEST_ASSERT_EQUAL_HEX8(PCA9555_CONFIG0_INIT, sim_pca9555_config(rail - 1, 0));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, sim_pca9555_pin(rail - 1, 0, PCA9555_BIT_VBUS_EN));
|
||||||
|
for (int bit = 0; bit < 4; bit++) {
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, sim_pca9555_pin(rail - 1, 0, bit)); /* 三个键 + EDL 全松 */
|
||||||
|
}
|
||||||
|
TEST_ASSERT_EQUAL_HEX16(INA226_CALIB, sim_i2c_get_reg_ch(ch, ina, INA226_REG_CALIB));
|
||||||
|
|
||||||
|
for (int i = 0; i < 3 * T_SAFETY_INSERT_N; i++) {
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap));
|
||||||
|
}
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(1, g_snap.blade_inits); /* 只做一次 */
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(unsafe0, sim_pca9555_unsafe_enable());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 初始化那一下没成(弹片还在抖、NAK):再等 T_SAFETY_INSERT_N 圈重试,而不是就此放弃;
|
||||||
|
插到一半又拔走的,作废,不对空槽发字节。 */
|
||||||
|
static void test_a_failed_blade_init_is_retried_and_dropped_when_pulled(void)
|
||||||
|
{
|
||||||
|
const int rail = 2;
|
||||||
|
seat_blade(rail, false);
|
||||||
|
(void)t_safety_step(&g_snap);
|
||||||
|
seat_blade(rail, true);
|
||||||
|
for (int i = 0; i < T_SAFETY_INSERT_N - 1; i++) {
|
||||||
|
(void)t_safety_step(&g_snap);
|
||||||
|
}
|
||||||
|
sim_i2c_inject_nak_ch(ch_of(rail), PCA9555_ADDR, 1); /* 落在初始化的第一个字节上 */
|
||||||
|
(void)t_safety_step(&g_snap);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(0, g_snap.blade_inits);
|
||||||
|
for (int i = 0; i < T_SAFETY_INSERT_N; i++) {
|
||||||
|
(void)t_safety_step(&g_snap);
|
||||||
|
}
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(1, g_snap.blade_inits);
|
||||||
|
|
||||||
|
/* 插进来没稳就拔走:need_init 作废,空槽一个字节也不发。 */
|
||||||
|
seat_blade(rail, false);
|
||||||
|
(void)t_safety_step(&g_snap);
|
||||||
|
seat_blade(rail, true);
|
||||||
|
(void)t_safety_step(&g_snap);
|
||||||
|
seat_blade(rail, false);
|
||||||
|
const uint32_t err0 = g_snap.i2c_errors;
|
||||||
|
for (int i = 0; i < 3 * T_SAFETY_INSERT_N; i++) {
|
||||||
|
(void)t_safety_step(&g_snap);
|
||||||
|
}
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(err0, g_snap.i2c_errors);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(1, g_snap.blade_inits);
|
||||||
|
}
|
||||||
|
|
||||||
/* 读失败时保留**上一圈的值**并把 ok 标成 false —— 不清零。
|
/* 读失败时保留**上一圈的值**并把 ok 标成 false —— 不清零。
|
||||||
清零的话,判据会看到一个 0 V 的「正常数」;而 `ok` 正是让它先看一眼
|
清零的话,判据会看到一个 0 V 的「正常数」;而 `ok` 正是让它先看一眼
|
||||||
「这数是不是新的」。拿一个没更新的电压去判过压是最难查的那种错。 */
|
「这数是不是新的」。拿一个没更新的电压去判过压是最难查的那种错。 */
|
||||||
@@ -652,6 +745,8 @@ int main(void)
|
|||||||
|
|
||||||
RUN_TEST(test_one_dead_ina226_does_not_stop_the_other_four);
|
RUN_TEST(test_one_dead_ina226_does_not_stop_the_other_four);
|
||||||
RUN_TEST(test_empty_slots_are_never_touched_and_the_input_still_reads);
|
RUN_TEST(test_empty_slots_are_never_touched_and_the_input_still_reads);
|
||||||
|
RUN_TEST(test_a_hot_plugged_blade_is_initialised_like_at_boot);
|
||||||
|
RUN_TEST(test_a_failed_blade_init_is_retried_and_dropped_when_pulled);
|
||||||
RUN_TEST(test_a_failed_read_keeps_the_stale_value_and_flags_it);
|
RUN_TEST(test_a_failed_read_keeps_the_stale_value_and_flags_it);
|
||||||
RUN_TEST(test_a_dead_pca9555_does_not_spoil_the_same_rails_ina226);
|
RUN_TEST(test_a_dead_pca9555_does_not_spoil_the_same_rails_ina226);
|
||||||
RUN_TEST(test_the_beat_survives_a_dead_blade);
|
RUN_TEST(test_the_beat_survives_a_dead_blade);
|
||||||
|
|||||||
+206
-12
@@ -28,6 +28,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "board_pins.h"
|
#include "board_pins.h"
|
||||||
|
#include "chgmode.h"
|
||||||
#include "hal_gpio.h"
|
#include "hal_gpio.h"
|
||||||
#include "hal_i2c.h"
|
#include "hal_i2c.h"
|
||||||
#include "hal_i2c_acct.h"
|
#include "hal_i2c_acct.h"
|
||||||
@@ -238,6 +239,7 @@ static void board_up(void)
|
|||||||
t_safety_set_event_sink(catch_event, NULL);
|
t_safety_set_event_sink(catch_event, NULL);
|
||||||
|
|
||||||
t_safety_init(&g_snap);
|
t_safety_init(&g_snap);
|
||||||
|
chgmode_reset(); /* 窗口表是静态的,用例之间不许串 */
|
||||||
sched_periodic_init(&g_tick, task_desc(TASK_SAFETY)->period_ms);
|
sched_periodic_init(&g_tick, task_desc(TASK_SAFETY)->period_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,6 +262,7 @@ void tearDown(void)
|
|||||||
/* 一拍 = 真板上两个任务在这 20 ms 里干的事。执行器要先推,因为判据打断的正是它。 */
|
/* 一拍 = 真板上两个任务在这 20 ms 里干的事。执行器要先推,因为判据打断的正是它。 */
|
||||||
static void beat(void)
|
static void beat(void)
|
||||||
{
|
{
|
||||||
|
(void)chgmode_tick(hal_time_now_ms()); /* app_main:执行器任务每转一圈清一次过期窗口 */
|
||||||
(void)t_actuator_step();
|
(void)t_actuator_step();
|
||||||
(void)t_safety_step(&g_snap);
|
(void)t_safety_step(&g_snap);
|
||||||
sched_periodic_wait(&g_tick);
|
sched_periodic_wait(&g_tick);
|
||||||
@@ -306,8 +309,8 @@ static void test_thresholds_match_the_documents(void)
|
|||||||
TEST_ASSERT_EQUAL_UINT32((T_SAFETY_DEV_HOLD_CYCLES - 1) * task_desc(TASK_SAFETY)->period_ms,
|
TEST_ASSERT_EQUAL_UINT32((T_SAFETY_DEV_HOLD_CYCLES - 1) * task_desc(TASK_SAFETY)->period_ms,
|
||||||
(uint32_t)T_SAFETY_DEV_HOLD_MS);
|
(uint32_t)T_SAFETY_DEV_HOLD_MS);
|
||||||
|
|
||||||
/* SW-8 的两个数:设计文档 §15.2 对策 2 的 −50 mA,需求 SW-8 的 200 ms 预算。 */
|
/* SW-8 的两个数:设计文档 §15.2 对策 2 的 −50 mA(2026-09-28 放宽到 −300 mA),需求 SW-8 的 200 ms 预算。 */
|
||||||
TEST_ASSERT_EQUAL_INT32(-50000, T_SAFETY_REV_UA);
|
TEST_ASSERT_EQUAL_INT32(-300000, T_SAFETY_REV_UA);
|
||||||
TEST_ASSERT_EQUAL_INT(200, T_SAFETY_REV_BUDGET_MS);
|
TEST_ASSERT_EQUAL_INT(200, T_SAFETY_REV_BUDGET_MS);
|
||||||
/* R2 里断 VBUS 多了一次 I²C 事务(≈0.16 ms),判据延迟变成「一圈 + 一次事务」。
|
/* R2 里断 VBUS 多了一次 I²C 事务(≈0.16 ms),判据延迟变成「一圈 + 一次事务」。
|
||||||
它必须离 200 ms 的预算远得看得出来 —— 计划 §5.4 说的「仍有 10 倍余量」。 */
|
它必须离 200 ms 的预算远得看得出来 —— 计划 §5.4 说的「仍有 10 倍余量」。 */
|
||||||
@@ -324,7 +327,7 @@ static void test_thresholds_match_the_documents(void)
|
|||||||
否则验收测的是「阈值刚好卡在注入值上」。 */
|
否则验收测的是「阈值刚好卡在注入值上」。 */
|
||||||
TEST_ASSERT_TRUE(150 > T_SAFETY_DEV_MV);
|
TEST_ASSERT_TRUE(150 > T_SAFETY_DEV_MV);
|
||||||
TEST_ASSERT_TRUE(300 > T_SAFETY_UNDER_MV); /* 欠压注入 −300 mV */
|
TEST_ASSERT_TRUE(300 > T_SAFETY_UNDER_MV); /* 欠压注入 −300 mV */
|
||||||
TEST_ASSERT_TRUE(-60000 < T_SAFETY_REV_UA);
|
TEST_ASSERT_TRUE(-310000 < T_SAFETY_REV_UA);
|
||||||
|
|
||||||
/* ⭐ R2 新增:mux 连续选不中多少次才算失联。
|
/* ⭐ R2 新增:mux 连续选不中多少次才算失联。
|
||||||
1 的话一次总线毛刺就停五台手机;太大的话失联之后还有一大截时间在拿旧数据
|
1 的话一次总线毛刺就停五台手机;太大的话失联之后还有一大截时间在拿旧数据
|
||||||
@@ -813,7 +816,7 @@ static void test_reverse_current_drops_vbus(void)
|
|||||||
vbus_on(1);
|
vbus_on(1);
|
||||||
watch_reset();
|
watch_reset();
|
||||||
const uint32_t en_w = en_writes(1);
|
const uint32_t en_w = en_writes(1);
|
||||||
inject(1, NOMINAL_MV, -60000); /* §12.1 的 −60 mA */
|
inject(1, NOMINAL_MV, -310000); /* §12.1 的 −60 mA,阈值放宽后改 −310 mA */
|
||||||
|
|
||||||
const uint32_t t0 = hal_time_now_ms();
|
const uint32_t t0 = hal_time_now_ms();
|
||||||
const uint32_t takes_before = sim_lock_takes(HAL_LOCK_I2C);
|
const uint32_t takes_before = sim_lock_takes(HAL_LOCK_I2C);
|
||||||
@@ -823,7 +826,7 @@ static void test_reverse_current_drops_vbus(void)
|
|||||||
TEST_ASSERT_EQUAL_UINT32(en_w, en_writes(1)); /* eFuse 那根线一次都没被碰 */
|
TEST_ASSERT_EQUAL_UINT32(en_w, en_writes(1)); /* eFuse 那根线一次都没被碰 */
|
||||||
TEST_ASSERT_EQUAL_INT(1, g_n_ev);
|
TEST_ASSERT_EQUAL_INT(1, g_n_ev);
|
||||||
TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_REVERSE, g_ev[0].type);
|
TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_REVERSE, g_ev[0].type);
|
||||||
TEST_ASSERT_EQUAL_INT32(-60000, g_ev[0].current_ua);
|
TEST_ASSERT_EQUAL_INT32(-310000, g_ev[0].current_ua);
|
||||||
|
|
||||||
/* ⭐ 这一写同样是锁内、本路通道上的(SW-8 走的是和 SR-5 同一条路)。 */
|
/* ⭐ 这一写同样是锁内、本路通道上的(SW-8 走的是和 SR-5 同一条路)。 */
|
||||||
TEST_ASSERT_EQUAL_UINT32(1, g_w[0].n);
|
TEST_ASSERT_EQUAL_UINT32(1, g_w[0].n);
|
||||||
@@ -844,19 +847,19 @@ static void test_reverse_current_is_not_debounced(void)
|
|||||||
{
|
{
|
||||||
power_up(1);
|
power_up(1);
|
||||||
vbus_on(1);
|
vbus_on(1);
|
||||||
inject(1, NOMINAL_MV, -60000);
|
inject(1, NOMINAL_MV, -310000);
|
||||||
beat(); /* 就一圈 */
|
beat(); /* 就一圈 */
|
||||||
TEST_ASSERT_EQUAL_INT(0, vbus_pin(1));
|
TEST_ASSERT_EQUAL_INT(0, vbus_pin(1));
|
||||||
TEST_ASSERT_EQUAL_INT(1, g_n_ev);
|
TEST_ASSERT_EQUAL_INT(1, g_n_ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 没到 −50 mA 不算。手机在直供下本来就会有小幅的双向电流。 */
|
/* 没到 −300 mA 不算(−250 mA 也不算)。手机在直供下本来就会有小幅的双向电流。 */
|
||||||
static void test_a_small_negative_current_is_not_a_backfeed(void)
|
static void test_a_small_negative_current_is_not_a_backfeed(void)
|
||||||
{
|
{
|
||||||
power_up(1);
|
power_up(1);
|
||||||
vbus_on(1);
|
vbus_on(1);
|
||||||
watch_reset();
|
watch_reset();
|
||||||
inject(1, NOMINAL_MV, -40000);
|
inject(1, NOMINAL_MV, -250000);
|
||||||
beats(50);
|
beats(50);
|
||||||
TEST_ASSERT_EQUAL_INT(1, vbus_pin(1));
|
TEST_ASSERT_EQUAL_INT(1, vbus_pin(1));
|
||||||
TEST_ASSERT_EQUAL_INT(0, g_n_ev);
|
TEST_ASSERT_EQUAL_INT(0, g_n_ev);
|
||||||
@@ -882,7 +885,7 @@ static void test_a_backfeed_does_not_refire_every_cycle(void)
|
|||||||
{
|
{
|
||||||
power_up(1);
|
power_up(1);
|
||||||
vbus_on(1);
|
vbus_on(1);
|
||||||
inject(1, NOMINAL_MV, -60000);
|
inject(1, NOMINAL_MV, -310000);
|
||||||
beat();
|
beat();
|
||||||
TEST_ASSERT_EQUAL_INT(1, g_n_ev);
|
TEST_ASSERT_EQUAL_INT(1, g_n_ev);
|
||||||
|
|
||||||
@@ -923,7 +926,7 @@ static void test_a_running_edl_sequence_cannot_bring_vbus_back(void)
|
|||||||
TEST_ASSERT_EQUAL_INT(1, vbus_pin(1));
|
TEST_ASSERT_EQUAL_INT(1, vbus_pin(1));
|
||||||
TEST_ASSERT_EQUAL_INT(ACT_ST_EDL_ENUM, (int)t_actuator_state(1));
|
TEST_ASSERT_EQUAL_INT(ACT_ST_EDL_ENUM, (int)t_actuator_state(1));
|
||||||
|
|
||||||
inject(1, NOMINAL_MV, -60000);
|
inject(1, NOMINAL_MV, -310000);
|
||||||
beat();
|
beat();
|
||||||
TEST_ASSERT_EQUAL_INT(0, vbus_pin(1));
|
TEST_ASSERT_EQUAL_INT(0, vbus_pin(1));
|
||||||
TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, (int)t_actuator_last_result(1));
|
TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, (int)t_actuator_last_result(1));
|
||||||
@@ -942,7 +945,7 @@ static void test_a_backfeed_does_not_abort_an_unrelated_sequence(void)
|
|||||||
vbus_on(1);
|
vbus_on(1);
|
||||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(1, ACT_KEYS_VUP, ACT_KEY_MS_ON));
|
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_key(1, ACT_KEYS_VUP, ACT_KEY_MS_ON));
|
||||||
|
|
||||||
inject(1, NOMINAL_MV, -60000);
|
inject(1, NOMINAL_MV, -310000);
|
||||||
beat();
|
beat();
|
||||||
TEST_ASSERT_EQUAL_INT(0, vbus_pin(1));
|
TEST_ASSERT_EQUAL_INT(0, vbus_pin(1));
|
||||||
TEST_ASSERT_TRUE(t_actuator_busy(1));
|
TEST_ASSERT_TRUE(t_actuator_busy(1));
|
||||||
@@ -959,7 +962,7 @@ static void test_backfeed_is_per_rail(void)
|
|||||||
vbus_on(2);
|
vbus_on(2);
|
||||||
vbus_on(5);
|
vbus_on(5);
|
||||||
|
|
||||||
inject(5, NOMINAL_MV, -60000);
|
inject(5, NOMINAL_MV, -310000);
|
||||||
beats(5);
|
beats(5);
|
||||||
TEST_ASSERT_EQUAL_INT(0, vbus_pin(5));
|
TEST_ASSERT_EQUAL_INT(0, vbus_pin(5));
|
||||||
TEST_ASSERT_EQUAL_INT(1, vbus_pin(2));
|
TEST_ASSERT_EQUAL_INT(1, vbus_pin(2));
|
||||||
@@ -1268,6 +1271,185 @@ static void test_the_cut_happens_even_with_no_event_sink(void)
|
|||||||
TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_OVER_VOLT, g_snap.last.type);
|
TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_OVER_VOLT, g_snap.last.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==== 0.1.5 刷机模式(chgmode.h)========================================
|
||||||
|
刷机时手机充电器会把节点顶到浮充 ~4.4 V(2026-09-28 板 B 实测 4395 mV)。
|
||||||
|
模式窗口里:不因为这个断 VBUS、也不关路;超过 CHG_MODE_MAX_MV 才算充电器异常。 */
|
||||||
|
|
||||||
|
#define CHARGER_FLOAT_MV 4395 /* 板 B 实测 PUSHED_UP 时的读数 */
|
||||||
|
|
||||||
|
static void flash_mode_on(int rail, uint32_t min)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, chgmode_set(rail, min, hal_time_now_ms()));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_flash_mode_lets_the_charger_push_the_rail_up(void)
|
||||||
|
{
|
||||||
|
power_up(1);
|
||||||
|
flash_mode_on(1, 30);
|
||||||
|
vbus_on(1);
|
||||||
|
inject(1, CHARGER_FLOAT_MV, 0); /* eFuse 反向阻断:电流 0 */
|
||||||
|
beats(500); /* 10 s */
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, vbus_pin(1)); /* USB 没断 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, en_level(1)); /* 直供也没关 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, g_n_ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 超过绝对上限 = 充电器本身不对:和平时一样分两层 —— 当圈断 VBUS,20 ms 后还高就关路。 */
|
||||||
|
static void test_flash_mode_still_cuts_above_the_absolute_ceiling(void)
|
||||||
|
{
|
||||||
|
power_up(1);
|
||||||
|
flash_mode_on(1, 30);
|
||||||
|
vbus_on(1);
|
||||||
|
inject(1, CHG_MODE_MAX_MV + 10, 0);
|
||||||
|
beat();
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, vbus_pin(1));
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, en_level(1));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_PUSHED_UP, g_ev[0].type);
|
||||||
|
beat();
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, en_level(1));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_OVER_VOLT, g_ev[1].type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 正好等于上限不算越界(与「正好 +100 mV 还在范围内」同一个口径)。 */
|
||||||
|
static void test_flash_mode_ceiling_itself_is_still_in_range(void)
|
||||||
|
{
|
||||||
|
power_up(1);
|
||||||
|
flash_mode_on(1, 30);
|
||||||
|
vbus_on(1);
|
||||||
|
inject(1, CHG_MODE_MAX_MV, 0);
|
||||||
|
beats(50);
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, vbus_pin(1));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, g_n_ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 反灌判据不受刷机模式影响(eFuse 反向阻断之外的第二道)。 */
|
||||||
|
static void test_flash_mode_keeps_the_backfeed_rule(void)
|
||||||
|
{
|
||||||
|
power_up(1);
|
||||||
|
flash_mode_on(1, 30);
|
||||||
|
vbus_on(1);
|
||||||
|
inject(1, CHARGER_FLOAT_MV, -310000);
|
||||||
|
beat();
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, vbus_pin(1));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_REVERSE, g_ev[0].type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 欠压判据也不受影响。 */
|
||||||
|
static void test_flash_mode_keeps_the_undervoltage_rule(void)
|
||||||
|
{
|
||||||
|
power_up(1);
|
||||||
|
flash_mode_on(1, 30);
|
||||||
|
inject(1, NOMINAL_MV - T_SAFETY_UNDER_MV - 50, 1000000);
|
||||||
|
beats(2);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, en_level(1));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_UNDER_VOLT, g_ev[0].type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 模式只管自己那一路:CH2 没开模式,照样一顶高就断 VBUS。 */
|
||||||
|
static void test_flash_mode_is_per_rail(void)
|
||||||
|
{
|
||||||
|
power_up(1);
|
||||||
|
power_up(2);
|
||||||
|
flash_mode_on(1, 30);
|
||||||
|
vbus_on(1);
|
||||||
|
vbus_on(2);
|
||||||
|
inject(1, CHARGER_FLOAT_MV, 0);
|
||||||
|
inject(2, CHARGER_FLOAT_MV, 0);
|
||||||
|
beat();
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, vbus_pin(1));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, vbus_pin(2));
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, g_n_ev);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(2, g_ev[0].rail);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ⭐ 窗口到期那一刻手机还在充、节点在 4.4 V:SW-8 立刻断 VBUS(回到平时),
|
||||||
|
但宽限期里 SR-5 仍按绝对值判 —— 节点往回落的那几毫秒不许把手机关掉。 */
|
||||||
|
static void test_flash_mode_expiry_drops_vbus_but_not_the_rail(void)
|
||||||
|
{
|
||||||
|
power_up(1);
|
||||||
|
flash_mode_on(1, 1);
|
||||||
|
vbus_on(1);
|
||||||
|
inject(1, CHARGER_FLOAT_MV, 0);
|
||||||
|
beats(60000 / 20 - 5); /* 窗口里 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, vbus_pin(1));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, g_n_ev);
|
||||||
|
|
||||||
|
beats(10); /* 跨过到期 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, vbus_pin(1));
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, en_level(1));
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, g_n_ev);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_PUSHED_UP, g_ev[0].type);
|
||||||
|
|
||||||
|
inject(1, NOMINAL_MV, 200000); /* USB 断了,节点回到直供 */
|
||||||
|
beats(200);
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, en_level(1));
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, g_n_ev);
|
||||||
|
TEST_ASSERT_FALSE(chgmode_relaxed(1, hal_time_now_ms())); /* 宽限期过了,标志也被清掉 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 宽限期过了节点还高(比如 VBUS 根本没断掉)⇒ 回到平时的 SR-5,关路。 */
|
||||||
|
static void test_after_the_grace_period_the_normal_rule_is_back(void)
|
||||||
|
{
|
||||||
|
power_up(1);
|
||||||
|
flash_mode_on(1, 1);
|
||||||
|
inject(1, CHARGER_FLOAT_MV, 0); /* VBUS 没开:模拟「断掉了但节点还被别的东西撑着」 */
|
||||||
|
beats(60000 / 20 - 5);
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, en_level(1));
|
||||||
|
beats((CHG_MODE_GRACE_MS / 20) + 20);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, en_level(1));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(SAFETY_EV_OVER_VOLT, g_ev[0].type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 最多同时 CHG_MODE_MAX_RAILS 路;本路续期不占新名额;关掉一路后腾出名额。 */
|
||||||
|
static void test_flash_mode_allows_at_most_two_rails(void)
|
||||||
|
{
|
||||||
|
const uint32_t now = hal_time_now_ms();
|
||||||
|
TEST_ASSERT_EQUAL_INT(2, CHG_MODE_MAX_RAILS);
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, chgmode_set(1, 30, now));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, chgmode_set(3, 30, now));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, chgmode_set(5, 30, now));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, chgmode_set(3, 60, now)); /* 续期 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, chgmode_set(1, 0, now));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, chgmode_set(5, 30, now));
|
||||||
|
TEST_ASSERT_EQUAL_INT(2, chgmode_count(now));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, chgmode_set(1, CHG_MODE_MAX_MIN + 1, now));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, chgmode_set(0, 30, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 过期的窗口不许「复活」:32 位毫秒时钟 ~24.8 天后 (int32_t)(until - now) 会翻正,
|
||||||
|
所以 tick 必须把标志清掉。这里直接把时钟拨到 25 天后验证。 */
|
||||||
|
static void test_an_expired_window_does_not_come_back_after_the_clock_wraps(void)
|
||||||
|
{
|
||||||
|
const uint32_t now = hal_time_now_ms();
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, chgmode_set(1, 1, now));
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(1u, chgmode_tick(now + 60000u + CHG_MODE_GRACE_MS)); /* 自然到期 */
|
||||||
|
const uint32_t later = now + 25u * 24u * 3600u * 1000u;
|
||||||
|
TEST_ASSERT_FALSE(chgmode_active(1, later));
|
||||||
|
TEST_ASSERT_FALSE(chgmode_relaxed(1, later));
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(0, chgmode_tick(later));
|
||||||
|
|
||||||
|
/* 命令关掉的,tick 不再报到期(关的时候命令层已经记过事件)。 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, chgmode_set(2, 5, later));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, chgmode_set(2, 0, later));
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(0, chgmode_tick(later + CHG_MODE_GRACE_MS));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 0.1.6 加钟:剩余时间截断在 CHG_MODE_MAX_MIN;宽限期里 / 过期后不能加(要重新开)。 */
|
||||||
|
static void test_flash_mode_extend_caps_and_needs_an_open_window(void)
|
||||||
|
{
|
||||||
|
const uint32_t now = hal_time_now_ms();
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, chgmode_extend(1, 10, now));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, chgmode_set(1, 100, now));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, chgmode_extend(1, 60, now));
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(CHG_MODE_MAX_MIN * 60u, chgmode_left_s(1, now));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, chgmode_extend(1, 0, now));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, chgmode_extend(1, CHG_MODE_MAX_MIN + 1, now));
|
||||||
|
|
||||||
|
const uint32_t over = now + CHG_MODE_MAX_MIN * 60000u + 10u; /* 窗口刚过,宽限期里 */
|
||||||
|
TEST_ASSERT_TRUE(chgmode_relaxed(1, over));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, chgmode_extend(1, 10, over));
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
@@ -1321,5 +1503,17 @@ int main(void)
|
|||||||
RUN_TEST(test_the_lock_check_itself_can_fail);
|
RUN_TEST(test_the_lock_check_itself_can_fail);
|
||||||
RUN_TEST(test_a_skipped_cycle_runs_no_rules);
|
RUN_TEST(test_a_skipped_cycle_runs_no_rules);
|
||||||
RUN_TEST(test_the_cut_happens_even_with_no_event_sink);
|
RUN_TEST(test_the_cut_happens_even_with_no_event_sink);
|
||||||
|
RUN_TEST(test_flash_mode_lets_the_charger_push_the_rail_up);
|
||||||
|
RUN_TEST(test_flash_mode_still_cuts_above_the_absolute_ceiling);
|
||||||
|
RUN_TEST(test_flash_mode_ceiling_itself_is_still_in_range);
|
||||||
|
RUN_TEST(test_flash_mode_keeps_the_backfeed_rule);
|
||||||
|
RUN_TEST(test_flash_mode_keeps_the_undervoltage_rule);
|
||||||
|
RUN_TEST(test_flash_mode_is_per_rail);
|
||||||
|
RUN_TEST(test_flash_mode_expiry_drops_vbus_but_not_the_rail);
|
||||||
|
RUN_TEST(test_after_the_grace_period_the_normal_rule_is_back);
|
||||||
|
RUN_TEST(test_flash_mode_allows_at_most_two_rails);
|
||||||
|
RUN_TEST(test_an_expired_window_does_not_come_back_after_the_clock_wraps);
|
||||||
|
RUN_TEST(test_flash_mode_extend_caps_and_needs_an_open_window);
|
||||||
|
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -69,13 +69,14 @@ Rinj 107k→150k,输出上限 4.22V→**4.01V**(§4.3 / §5)。P2-CHANNEL
|
|||||||
| 9 | **侧键接 3 个**(PWR / VOL+ / VOL−)×5 台 = 15 路 MOS | §13.3 |
|
| 9 | **侧键接 3 个**(PWR / VOL+ / VOL−)×5 台 = 15 路 MOS | §13.3 |
|
||||||
| 10 | **尾插装原厂小板**(不自制 BM24 转接板) | §13.5 方案 A |
|
| 10 | **尾插装原厂小板**(不自制 BM24 转接板) | §13.5 方案 A |
|
||||||
| 11 | **USB 仅接电脑 / Hub(恒 5V),不接快充充电器** | §15.1 |
|
| 11 | **USB 仅接电脑 / Hub(恒 5V),不接快充充电器** | §15.1 |
|
||||||
| 12 | USB 三条对策全做:软件关充电 + VBUS 负载开关 + 12V 母线泄放 | §15.2 |
|
| 12 | USB 三条对策全做:软件关充电 + VBUS 负载开关 + 12V 母线泄放。**例外:刷机模式**(固件 0.1.5,2026-09-28)—— 按路限时、最多 2 路,允许充电不断 USB;靠 eFuse 反向阻断兜住反灌,见需求 SR-10 | §15.2 |
|
||||||
| 13 | 15 路按键经 **74HC595 ×3**(24 位链,含 5 路 EDL_EN + HUB_RST)驱动,EN / VBUS_EN 保持 GPIO 直连。**2026-09-06 起三片 595 + 15 颗按键 MOS 整窝在载板**(§26.23) | §8.1 / §26.23 |
|
| 13 | 15 路按键经 **74HC595 ×3**(24 位链,含 5 路 EDL_EN + HUB_RST)驱动,EN / VBUS_EN 保持 GPIO 直连。**2026-09-06 起三片 595 + 15 颗按键 MOS 整窝在载板**(§26.23) | §8.1 / §26.23 |
|
||||||
| 14 | **侧键接触改中框式焊盘,顶针取消**(2026-09-06):载板顶层 4ר1.5 裸铜盘(ENIG、钢网不开口),螺丝把主板压在载板上、间隙 **1.0 mm**,主板自带弹片直压铜盘。定位孔 **H5 不打**(每台 6 颗螺钉)。 | §22.3e |
|
| 14 | **侧键接触改中框式焊盘,顶针取消**(2026-09-06):载板顶层 4ר1.5 裸铜盘(ENIG、钢网不开口),螺丝把主板压在载板上、间隙 **1.0 mm**,主板自带弹片直压铜盘。定位孔 **H5 不打**(每台 6 颗螺钉)。 | §22.3e |
|
||||||
| 15 | **走线方向规则(2026-09-07):顶层水平、底层纵向**。两块板共用的栅格布线器按此计价(软规则,逆向走法 2 倍代价) | §10.1b |
|
| 15 | **走线方向规则(2026-09-07):顶层水平、底层纵向**。两块板共用的栅格布线器按此计价(软规则,逆向走法 2 倍代价) | §10.1b |
|
||||||
| 16 | **风扇改 FAN-R2(2026-09-07)**:五个 3 针风扇 → **两只 4 线 PWM 风扇**(山洋 9G0612P4S001,一进一出),高边 P-MOS 默认关电 + 独立 PWM 开漏 + **两路独立测速**。连带把 12V 预算从 8.3 A 顶到 **≈10 A**,`IO5=FAN_EN` / `IO4=FAN_TACH_OUT` | §22.6 |
|
| 16 | **风扇改 FAN-R2(2026-09-07)**:五个 3 针风扇 → **两只 4 线 PWM 风扇**(~~山洋 9G0612P4S001~~ ⇒ **2026-09-13 改 60×60×10 薄风扇**(广诚散热科技、小 4PIN = PC 标准 4 线口、孔距 50.2,配机箱自攻螺丝;**MPN / 电压 / 电流 / PWM·TACH 规格待补**),一进一出;为缩短 R2 机箱,见 R2 §3.3),高边 P-MOS 默认关电 + 独立 PWM 开漏 + **两路独立测速**。连带把 12V 预算从 8.3 A 顶到 **≈10 A**,`IO5=FAN_EN` / `IO4=FAN_TACH_OUT` | §22.6 |
|
||||||
| 18 | ~~**U32 的 `PRT_SWP1~5` 各 100k 下拉**:`R900~R904`~~ **已被 #19 取代(同日)** —— 五颗里有三颗(CH1/CH2/CH3)在载板上**布不出线**,见 §21.6a 末尾 | §21.6a |
|
| 18 | ~~**U32 的 `PRT_SWP1~5` 各 100k 下拉**:`R900~R904`~~ **已被 #19 取代(同日)** —— 五颗里有三颗(CH1/CH2/CH3)在载板上**布不出线**,见 §21.6a 末尾 | §21.6a |
|
||||||
| 19 | **U32 改 `CFG_SEL[2:0]=110`(2026-09-07,取代 #18)**:Table 5-2 的「Internal Default Configuration • **Strap Options are Disabled**」——十个 strap 脚根本不采样,`R900~R904` 一颗不装。`CFG_SEL2`(13) 是纯输入(`I`),由硬接 GND 改**硬接 `+3V3_HUB`**(不加电阻);`CFG_SEL1` 的 `R712` 由下拉改上拉;`CFG_SEL0` 的 `R711` 不动。**必须连带删掉 `R714`/`R715`**(否则口 6/7 变幽灵口)。BOM 净 −7 颗 | §21.6b |
|
| 19 | **U32 改 `CFG_SEL[2:0]=110`(2026-09-07,取代 #18)**:Table 5-2 的「Internal Default Configuration • **Strap Options are Disabled**」——十个 strap 脚根本不采样,`R900~R904` 一颗不装。`CFG_SEL2`(13) 是纯输入(`I`),由硬接 GND 改**硬接 `+3V3_HUB`**(不加电阻);`CFG_SEL1` 的 `R712` 由下拉改上拉;`CFG_SEL0` 的 `R711` 不动。**必须连带删掉 `R714`/`R715`**(否则口 6/7 变幽灵口)。BOM 净 −7 颗 | §21.6b |
|
||||||
|
| 19a | **R2 底板(刀片式)2026-09-10 改回 `CFG_SEL=000`**:`PRT_SWP1~3` 各 100k 上拉 = 口 1~3 D+/D− 片内对调(底板上北去三对不再 U 形掉头),`PRT_SWP4~6` 各 100k 下拉;`CFG_SEL2` 接 GND、`R712` 改下拉。**只改底板,R1 载板仍按 #19**。理由与接法见 `.claude/r2/a4_hub.py` 文件头 | R2 |
|
||||||
| 17 | **C737 换固态钽(2026-09-07)**:AMS1117-3.3 的 22 µF 输出电容按手册 §Stability 用 **KEMET T491A226K010AT**(C116575),不再用陶瓷。**1 脚是正极** | §26.25 |
|
| 17 | **C737 换固态钽(2026-09-07)**:AMS1117-3.3 的 22 µF 输出电容按手册 §Stability 用 **KEMET T491A226K010AT**(C116575),不再用陶瓷。**1 脚是正极** | §26.25 |
|
||||||
| 20 | **12V 输入座改圆形 DC 电源座(2026-09-08)**:`J1` 由 XT30PW-M 换成 **`DC-005-5.5*2.5`**(C9900017043,封装 `DC-IN-TH_DC-005-2`)。理由是成品 12V/10A 适配器出厂就是 5.5×2.5 圆头,装配上顺手。⚠️ **代价是插座超额定**:家族额定 2~5 A、本板最坏 9.99 A,发热在**插头内芯与插座弹片的接触面**,板上加铜救不了 —— **用户已明确接受、不降载**。板端唯一缓解:`pin2`/`pin3` 两个负极触点**都接地并联**,接触电阻减半。引脚:1=中心正极 / 2=负极静触点 / 3=负极动触点(2-3 无插头时短接)。⚠️ 装配前用万用表核:无插头时 2-3 通、1 对 2/3 断 | §1 / §22.6 |
|
| 20 | **12V 输入座改圆形 DC 电源座(2026-09-08)**:`J1` 由 XT30PW-M 换成 **`DC-005-5.5*2.5`**(C9900017043,封装 `DC-IN-TH_DC-005-2`)。理由是成品 12V/10A 适配器出厂就是 5.5×2.5 圆头,装配上顺手。⚠️ **代价是插座超额定**:家族额定 2~5 A、本板最坏 9.99 A,发热在**插头内芯与插座弹片的接触面**,板上加铜救不了 —— **用户已明确接受、不降载**。板端唯一缓解:`pin2`/`pin3` 两个负极触点**都接地并联**,接触电阻减半。引脚:1=中心正极 / 2=负极静触点 / 3=负极动触点(2-3 无插头时短接)。⚠️ 装配前用万用表核:无插头时 2-3 通、1 对 2/3 断 | §1 / §22.6 |
|
||||||
|
|
||||||
@@ -1163,7 +1164,7 @@ GND 靠地平面 + 散热盘阵列一个就够。出线宽度 1.0 mm ——
|
|||||||
**不阻塞打样,但要定**
|
**不阻塞打样,但要定**
|
||||||
|
|
||||||
- [ ] 主板机械固定方式(建议连中框,见 §13.6)。
|
- [ ] 主板机械固定方式(建议连中框,见 §13.6)。
|
||||||
- [ ] `input_suspend` 节点在目标 ROM 上的实际路径(§15.2 对策 1)。
|
- [x] ~~`input_suspend` 节点在目标 ROM 上的实际路径(§15.2 对策 1)~~ → **2026-09-28 小米8(MIUI / Android 10)实测:`/sys/class/power_supply/battery/input_suspend`**,写 1 后 `status=Discharging`、`usb/online=0`,VBUS 常开不再顶高母线。由 Magisk 模块 `phone/magisk-phonerail/`(service.sh,日志 `/data/local/tmp/phonerail_chg.log`)开机自动写入 = 需求 SW-7。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -2593,6 +2594,7 @@ Table 7.1:06h Config Data Byte 1 内部默认 ROM = 9Bh = 1001_1011b
|
|||||||
| **R708** | **12.0 kΩ ±1%**(C25752) | `RBIAS`(63) → GND | Table 5.1:「A 12.0kΩ (+/-1%) resistor…to set the transceiver's internal bias」。**这个值不能动** |
|
| **R708** | **12.0 kΩ ±1%**(C25752) | `RBIAS`(63) → GND | Table 5.1:「A 12.0kΩ (+/-1%) resistor…to set the transceiver's internal bias」。**这个值不能动** |
|
||||||
| **Y1** | **24 MHz ±10ppm, CL=9pF**(C2762192) | `XTAL1`(61) / `XTAL2`(60) | §9.1:「Parallel Resonant, Fundamental Mode, **24 MHz ±350ppm**」。选 ±10ppm 余量很大 |
|
| **Y1** | **24 MHz ±10ppm, CL=9pF**(C2762192) | `XTAL1`(61) / `XTAL2`(60) | §9.1:「Parallel Resonant, Fundamental Mode, **24 MHz ±350ppm**」。选 ±10ppm 余量很大 |
|
||||||
| **C740 / C741** | **12 pF C0G**(C1547)×2 | 晶振两端 → GND | 按 Figure 9.2:`CL = (C1+CS1)(C2+CS2)/(C1+CS1+C2+CS2)`,取杂散 ~3pF、CL=9pF → C1=C2≈12pF |
|
| **C740 / C741** | **12 pF C0G**(C1547)×2 | 晶振两端 → GND | 按 Figure 9.2:`CL = (C1+CS1)(C2+CS2)/(C1+CS1+C2+CS2)`,取杂散 ~3pF、CL=9pF → C1=C2≈12pF |
|
||||||
|
| **1 MΩ 跨接**(⚠ 2026-09-10 补,R1 起就漏了)| **1 MΩ 0402**(`0402WGF1004TCE` / C26083)| `XTAL1`(61) ↔ `XTAL2`(60) | USB2517 Hardware Design Checklist p9 原文:「A 1 MΩ resistor connected across XTAL1 and XTAL2 is **required**. Failure to place this resistor will result in unstable crystal operation.」数据手册 p52 Figure 9-1 也画着 1Meg。R2 底板 = `R723`;**R1 载板待补**(用户 2026-09-10 定:两块都补) |
|
||||||
| **C742 / C743** | **1 µF 0402**(C52923)×2 | `VDD18`(25)、`VDD18PLL`(62) → GND | Table 5.1 两处都写「must have a **1.0µF (or greater) ±20% (ESR <0.1Ω)** capacitor to VSS」 |
|
| **C742 / C743** | **1 µF 0402**(C52923)×2 | `VDD18`(25)、`VDD18PLL`(62) → GND | Table 5.1 两处都写「must have a **1.0µF (or greater) ±20% (ESR <0.1Ω)** capacitor to VSS」 |
|
||||||
| **R709 / R710** | 100 kΩ / 100 kΩ | 上游 VBUS → 分压 → `VBUS_DET`(44) | §7.1.2 + Table 5.1:「When designing a **detachable** hub, this pin must be connected to the VBUS power pin of the USB port that is upstream」。我们的上游是可插拔的 USB-C,**属于 detachable**,所以要真接、不能拉到 3.3V。该脚是 3.3V 逻辑,5V 必须分压 |
|
| **R709 / R710** | 100 kΩ / 100 kΩ | 上游 VBUS → 分压 → `VBUS_DET`(44) | §7.1.2 + Table 5.1:「When designing a **detachable** hub, this pin must be connected to the VBUS power pin of the USB port that is upstream」。我们的上游是可插拔的 USB-C,**属于 detachable**,所以要真接、不能拉到 3.3V。该脚是 3.3V 逻辑,5V 必须分压 |
|
||||||
| **R711~R713** | 10 kΩ ×3 | `CFG_SEL[2:0]` → GND | §21.3 |
|
| **R711~R713** | 10 kΩ ×3 | `CFG_SEL[2:0]` → GND | §21.3 |
|
||||||
@@ -3343,7 +3345,7 @@ U33: QA=EDL_EN1 QB=EDL_EN2 QC=EDL_EN3 QD=EDL_EN4 QE=EDL_EN5 QF/QG/QH 备用
|
|||||||
|
|
||||||
| 项 | 现行 |
|
| 项 | 现行 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 风扇 | **山洋 SANYO DENKI 9G0612P4S001 ×2**(`C2920592`),60×60×25,12V 四线 PWM |
|
| 风扇 | ~~山洋 SANYO DENKI 9G0612P4S001 ×2(`C2920592`),60×60×25~~ ⇒ **2026-09-13 改:60×60×10 薄风扇 ×2**(广诚散热科技,小 4PIN / PC 4 线:1 GND、2 +12V、3 TACH、4 PWM —— 与 J26/J27 针序一致;孔距 50.2 / 孔 Ø4.4,配机箱自攻螺丝)。⚠ **额定电压、电流、风量、静压、PWM/TACH 规格均未取得**,12 V 预算里的风扇电流沿用旧值、待核 |
|
||||||
| 布置 | **一进一出**:J26 = 进风、J27 = 排风,分列左右两侧;原顶部排风孔取消 |
|
| 布置 | **一进一出**:J26 = 进风、J27 = 排风,分列左右两侧;原顶部排风孔取消 |
|
||||||
| 额定 | 100% PWM 时每只 0.67 A / 8.04 W、11000 RPM;**0% PWM 仍转 3300 RPM**、每只 0.07 A |
|
| 额定 | 100% PWM 时每只 0.67 A / 8.04 W、11000 RPM;**0% PWM 仍转 3300 RPM**、每只 0.07 A |
|
||||||
| 供电 | 高边 P-MOS **默认关电**:`+12V --Q7(AO3401A)--> FAN_12V_SW` |
|
| 供电 | 高边 P-MOS **默认关电**:`+12V --Q7(AO3401A)--> FAN_12V_SW` |
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ USB 通断」这些原本要人手做的动作全部电气化,交给板上 MCU
|
|||||||
| **SR-7** | 放电 MOS 与 eFuse EN **硬件反相联锁**,物理上不可能同时导通 | §7.3 |
|
| **SR-7** | 放电 MOS 与 eFuse EN **硬件反相联锁**,物理上不可能同时导通 | §7.3 |
|
||||||
| **SR-8** | **输入保护**:保险丝 + TVS + P-MOS 反接保护 | §2 |
|
| **SR-8** | **输入保护**:保险丝 + TVS + P-MOS 反接保护 | §2 |
|
||||||
| **SR-9** | **12 V 母线泄放**:母线 >13.2 V 时导通泄放,防 USB 反灌或突卸能量抬高母线 | §15.2 |
|
| **SR-9** | **12 V 母线泄放**:母线 >13.2 V 时导通泄放,防 USB 反灌或突卸能量抬高母线 | §15.2 |
|
||||||
| **SR-10** | **USB 反灌三条对策全做,一条都不能省**:① 软件关充电 ② VBUS 负载开关 ③ 母线泄放 | 决策 12 / §13.5 |
|
| **SR-10** | **USB 反灌三条对策全做,一条都不能省**:① 软件关充电 ② VBUS 负载开关 ③ 母线泄放。**唯一的有意例外:刷机模式**(固件 0.1.5,2026-09-28 用户定)——按路、带时限(默认 60 分钟,最长 120)、最多同时 2 路,窗口内允许手机充电、「顶高断 VBUS」与过压改按绝对值 4.45 V 判,反灌与欠压判据不变;依据是每路 eFuse(TPS25940A)自带反向阻断,充电器顶高节点时能量进不了 Buck / 12V 母线(板 B 五次顶高实测电流 0 ~ −0.5 mA) | 决策 12 / §13.5 / firmware `chgmode.h` |
|
||||||
| **SR-11** | 上游 USB 不做 PD 协商(CC 双 5.1 kΩ 下拉,纯 UFP),杜绝 VBUS 被抬到 9 / 12 V | §15.1 |
|
| **SR-11** | 上游 USB 不做 PD 协商(CC 双 5.1 kΩ 下拉,纯 UFP),杜绝 VBUS 被抬到 9 / 12 V | §15.1 |
|
||||||
| **SR-12** | 五台手机的地**不得**在载板上并成一根:定位孔做 **NPTH**,立柱用**尼龙** | §25.2 |
|
| **SR-12** | 五台手机的地**不得**在载板上并成一根:定位孔做 **NPTH**,立柱用**尼龙** | §25.2 |
|
||||||
| **SR-13** | 固件**不得**提供任何能让输出超过 4.2 V 的接口 | §8.3 |
|
| **SR-13** | 固件**不得**提供任何能让输出超过 4.2 V 的接口 | §8.3 |
|
||||||
@@ -283,7 +283,7 @@ USB 通断」这些原本要人手做的动作全部电气化,交给板上 MCU
|
|||||||
| **SW-5** | 侧键序列:短按 100 ms 唤醒 / 长按 5 s 开机(小米8 实测 2 s 不够,2026-09-28)/ 长按 10 s 强制重启 / VOL−+PWR 进 Fastboot / VOL++PWR 进 Recovery。硬重启末尾的开机键**按完看电流**:松键后 3~5 s 内 < 100 mA 就重按,最多 3 次,仍不开记事件 19「开机重按」并保持供电(固件 0.1.2,起因是电源键弹片接触不良) |
|
| **SW-5** | 侧键序列:短按 100 ms 唤醒 / 长按 5 s 开机(小米8 实测 2 s 不够,2026-09-28)/ 长按 10 s 强制重启 / VOL−+PWR 进 Fastboot / VOL++PWR 进 Recovery。硬重启末尾的开机键**按完看电流**:松键后 3~5 s 内 < 100 mA 就重按,最多 3 次,仍不开记事件 19「开机重按」并保持供电(固件 0.1.2,起因是电源键弹片接触不良) |
|
||||||
| **SW-6** | EDL 时序**固定为** `EDL_EN=1` → `VBUS_EN=1` → 等枚举 → `EDL_EN=0`(先短接再插 USB) |
|
| **SW-6** | EDL 时序**固定为** `EDL_EN=1` → `VBUS_EN=1` → 等枚举 → `EDL_EN=0`(先短接再插 USB) |
|
||||||
| **SW-7** | 开机脚本执行「关充电」(写 `input_suspend` 类节点),路径需在首台机上确认后写死 |
|
| **SW-7** | 开机脚本执行「关充电」(写 `input_suspend` 类节点),路径需在首台机上确认后写死 |
|
||||||
| **SW-8** | USB 反灌保护:检测到该路电流转负或母线上升,**200 ms 内**断开该路 VBUS |
|
| **SW-8** | USB 反灌保护:检测到该路电流转负或母线上升,**200 ms 内**断开该路 VBUS。刷机模式窗口内「顶高」按绝对值 4.45 V 判(SR-10 的例外),电流转负照断 |
|
||||||
| **SW-9** | 风扇 PWM 闭环(以载板 LM75BD 为反馈)+ TACH 停转告警 |
|
| **SW-9** | 风扇 PWM 闭环(以载板 LM75BD 为反馈)+ TACH 停转告警 |
|
||||||
| **SW-10** | 整板功率预算:读输入侧 INA226,超适配器能力时按优先级降载 |
|
| **SW-10** | 整板功率预算:读输入侧 INA226,超适配器能力时按优先级降载 |
|
||||||
| **SW-11** | 上电默认全关:五路输出关、VBUS 断、按键不按、EDL 不触发 |
|
| **SW-11** | 上电默认全关:五路输出关、VBUS 断、按键不按、EDL 不触发 |
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户