固件 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]>
这个提交包含在:
+257
-236
@@ -1,236 +1,257 @@
|
||||
/* `t_safety` —— 20 ms 巡检任务的**一个周期**(需求 §10.3 第一行)。
|
||||
|
||||
---- R2 之后一个周期是什么样 ----------------------------------------------
|
||||
|
||||
R1:一把 I²C 锁包住整圈,读 6 片 INA226 + 1 片 PCA9555,放锁,再跑判据。
|
||||
R2:五路各在**自己的 mux 通道**上,所以一圈是**五次 `i2cbus_xact` + 一次主干**:
|
||||
|
||||
for 每一路: xact(本路通道) { 读 INA226 的 V/I → 读 PCA9555 两个输入端口
|
||||
→ 跑本路判据(必要时**就在这里**断 VBUS) }
|
||||
xact(主干) { 读输入侧 INA226 }
|
||||
放锁之后: PRSNT# 轮询 → 关 eFuse / abort 序列 / 记事件 → mux 失联判据
|
||||
|
||||
每路一次 `xact` 而不是一把大锁包住整圈,是为了给 `t_actuator` 留得进来的缝
|
||||
(R2 之后按键也走 I²C,计划 §5.6)。
|
||||
|
||||
分成「一个周期是个函数」而不是直接写成 `while(1)`,是为了让它在宿主机上跑得起来。
|
||||
|
||||
---- 四条不能改的规矩 ----------------------------------------------------
|
||||
1. **不许死等锁。** 拿不到就跳过这一路并计数(`skipped`)。死等的现象是
|
||||
巡检整个停摆,而它是优先级最高的那个任务 —— 停摆了没有任何人会发现。
|
||||
2. **一个器件读失败不能中断这一圈。** 某一路坏了,剩下四路的判据照常要出。
|
||||
失联 ≠ 断电(§10.2),失联更不等于「别的路也不看了」。
|
||||
3. **断 VBUS 必须在锁内、在本路的 `xact` 里做。** ⚠️ 这条是 R2 新的:
|
||||
`VBUS_EN` 从 GPIO 变成了刀片 PCA9555 的 P0.4(计划 §5.4),
|
||||
放锁之后再拿一次锁去断,中间隔着一次锁争用 —— 而且那时通道可能已经被
|
||||
别人切走了。**`EFUSE_EN` 相反**:它是 GPIO 直连,放锁之后关才对
|
||||
(拿着总线锁去写引脚只会把别人多挡一会儿)。
|
||||
4. **判据一条也不许额外发 I²C 事务。** 判据要的三个量(设定值、EN 的状态、
|
||||
正在跑的序列)全部有影子可查。多读一次就把预算顶穿,测试会当场变红。 */
|
||||
#ifndef T_SAFETY_H
|
||||
#define T_SAFETY_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
#include "ina226.h"
|
||||
#include "pca9555.h"
|
||||
#include "phonerail.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 每一路等 I²C 锁最多等这么久。等过头这一路就废了,而**废一路一圈**
|
||||
(20 ms 后重来)比**赖着不走**(把最高优先级任务钉死)好得多。 */
|
||||
#define T_SAFETY_LOCK_TIMEOUT_MS 5
|
||||
|
||||
/* ⭐ **整圈等锁的总预算**,R2 必须有的一条。
|
||||
|
||||
R1 一圈只拿一次锁,所以「单次超时 5 ms」就是全部的账。**R2 一圈要拿六次**
|
||||
(五路各一次 + 主干一次),单看每次 5 ms 没问题,六次加起来是 **30 ms —— 比
|
||||
20 ms 的周期还长**。总线长期繁忙时的现象不是「偶尔丢一拍」,而是**巡检成片
|
||||
地丢拍、周期整个失准**,而每一次 `xact` 都老老实实在 5 ms 就返回了,
|
||||
哪一层都不会报错。
|
||||
|
||||
所以除了单次超时,还要有一条整圈的闸:本圈在等锁上花掉的时间超过这个数,
|
||||
**剩下的路这一圈就不读了**(记 `skipped`)。取周期的一半,留另一半给判据、
|
||||
给 `t_actuator`、给别的任务。
|
||||
|
||||
⚠️ 已知的不公平:预算是按顺序花的,所以长期繁忙时**后面几路会比前面几路
|
||||
更常被跳过**。没有做轮转起点,因为 SR-5 判据数的是「连续两圈都越界」,
|
||||
某一路偶尔漏一圈只让它晚一圈跳闸;而轮转会让「第几次拿锁」这种可断言的
|
||||
不变量消失,代价比收益大。真正该看的是 `skipped` 有没有持续增长。 */
|
||||
#define T_SAFETY_LOCK_BUDGET_MS 10
|
||||
|
||||
/* ---- A10:判据阈值。**每一个都有出处,改了就是改需求** ------------------
|
||||
`test_thresholds_match_the_documents()` 会拦。 */
|
||||
|
||||
/* SR-5 / SW-2:「电压偏离设定 >±100 mV」。比的是 INA226 读回来的母线电压与
|
||||
**该路 DAC 的设定值**(`rail_setpoint_mv()`),不是 3900 这个常数 ——
|
||||
SW-4 允许运维单路微调,拿常数比会把一台调到 4.0 V 的机器当成过压。 */
|
||||
#define T_SAFETY_DEV_MV 100
|
||||
|
||||
/* SR-5 的**欠压**一侧单独放宽到 −250 mV(2026-09-28 用户拍板)。
|
||||
上板实测:小米8 插上屏幕后开机峰值 ~2.1 A,轨 1 读回 3785 mV(设定 3900,−115 mV,
|
||||
≈55 mΩ 压降),−100 mV 的门限把正在开机的手机直接关掉。SR-5 本来就只管记录告警,
|
||||
实时保护在硬件里;过压那一侧不动,仍是 +100 mV。 */
|
||||
#define T_SAFETY_UNDER_MV 250
|
||||
|
||||
/* SR-5 的「且持续 20 ms」——「持续」在这里数的是**圈**,不是墙钟毫秒:
|
||||
连着两圈新鲜样本都越界才算,而成立的那一刻就是断电的那一刻。
|
||||
|
||||
⚠️ **不要改回按墙钟算**(`now - 第一次看见 >= 20 ms`)。巡检的采样时刻带着
|
||||
亚毫秒残差:一圈自己要占 2.34 ms 总线,而「睡到下一拍」是按整毫秒算的,
|
||||
于是相邻两次判据的时刻实测是 **21 / 19 ms 交替**。拿 `>= 20 ms` 去判,
|
||||
有一半的机会 19 ms 那一次不算数、要等到第三圈 —— 也就是 40 ms 才关路,
|
||||
直接超了 §12.1 的验收,而且是**间歇性**的。数圈没有这个毛病。
|
||||
|
||||
⚠️ SR-5 说「持续 20 ms 才算」,§12.1 的验收说「20 ms 内关路」——
|
||||
字面上互相顶着,只有「从第一次看见算起、下一圈就断电」同时满足两条。 */
|
||||
#define T_SAFETY_DEV_HOLD_CYCLES 2
|
||||
|
||||
/* 上面那个圈数折算成毫秒 = (圈数 − 1) × 巡检周期。需求 SR-5 写的就是这个 20 ms,
|
||||
`test_thresholds_match_the_documents()` 拿它和任务表里的周期对账。 */
|
||||
#define T_SAFETY_DEV_HOLD_MS 20
|
||||
|
||||
/* SW-8 的反灌判据:设计文档 §15.2 对策 2 原话「if I < −50 mA → 立即拉低 VBUS_EN」。
|
||||
离噪声很远:INA226 电流 LSB 是 0.5 mA,分流失调 ±10 µV / 5 mΩ ≈ ±2 mA,
|
||||
−50 mA 是它的 25 倍。§12.1 的验收注入 −60 mA,落在阈值外侧。 */
|
||||
#define T_SAFETY_REV_UA (-50000)
|
||||
|
||||
/* 反灌**不做确认窗口**(一圈看见就断),这和 20 ms 的过压判据是有意不对称的:
|
||||
· 误断 VBUS 的代价是丢一次 USB 数据,手机还由直供电着,没有任何风险;
|
||||
· 漏判的代价是能量泵回 12V 母线,**连累另外四台**。
|
||||
一圈就断 ⇒ 最坏 40 ms + 一次 I²C 事务(≈0.16 ms),SW-8 给的预算是 200 ms。 */
|
||||
#define T_SAFETY_REV_BUDGET_MS 200
|
||||
|
||||
/* `EFUSE_EN` 拉高之后多久才开始判这一路的电压。tON 5.1 ms + 缓启动斜坡 11.5 ms
|
||||
≈ 17 ms(设计文档 §5 / §7.4),取 3 个巡检周期。
|
||||
没有这个窗口的现象是**每次上电都自己把自己关掉**。 */
|
||||
#define T_SAFETY_SETTLE_MS 60
|
||||
|
||||
/* ⭐ R2 新增:**连续**多少次选不中 mux 通道才认定它失联(计划 §5.8 与 §11)。
|
||||
⚠️ 这个数是「先按 3 写、上板实测总线误码率之后再定」的待定项,不是算出来的。
|
||||
太小(1):一次总线毛刺就停掉五台手机;太大(10):失联之后还有 200 ms
|
||||
在拿旧数据跑判据。判定成立之后**先脉冲一次 `MUX_RST#`**,再不行才全断。 */
|
||||
#define T_SAFETY_MUX_FAIL_N 3
|
||||
|
||||
/* ⭐ 0.1.4:**整片刀片不应答**(本路 INA226 与 PCA9555 **同时** NAK)连续多少圈就关这一路。
|
||||
与 §10.10 第一行「INA226 无应答 → 盲跑、保持供电」不冲突:那一行是**一颗**器件坏了,
|
||||
刀片上的另一颗还答应;两颗都不答应而 mux 是好的(选不中的话本路读函数根本不会被调,
|
||||
计不进来),只剩两种可能 ——
|
||||
· 刀片根本不在,是 `PRSNT#` 假在位(2026-09-28 板 B 第 2 槽:卡低之后空槽 eFuse 开了约 4 h);
|
||||
· 刀片在,但这一路完全失控:看不到 V/I、看不到 PG/FLT、**连 VBUS 都断不了**。
|
||||
两种都是「继续供电但看不见」,和 mux 失联全断同一个道理,只是范围是一路。
|
||||
10 圈 = 200 ms:本板实测单路 9 万圈 0 次 NAK,连续 10 次不是毛刺;空槽带 12 V 多 200 ms 无所谓。 */
|
||||
#define T_SAFETY_BLADE_MUTE_N 10
|
||||
|
||||
/* ---- A10:事件(§10.7 的「单条」形状) -----------------------------------
|
||||
A11 把这条记录塞进环形缓冲,这里只负责**产生**它。 */
|
||||
|
||||
typedef enum {
|
||||
SAFETY_EV_NONE = 0,
|
||||
SAFETY_EV_OVER_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_PUSHED_UP, /* SW-8:VBUS 期间电压被顶高 >+100 mV → 断这一路 VBUS */
|
||||
/* ⭐ R2 新增两条(计划 §5.8)。放在**末尾**:`evlog` 那边的数字是 flash 格式。 */
|
||||
SAFETY_EV_MUX_LOST, /* TCA9548A 连续选不中 ⇒ 五路全部断电(单点) */
|
||||
SAFETY_EV_SLOT_EMPTY, /* `PRSNT#` 为高(刀片不在位)⇒ 这一路不许带电 */
|
||||
/* 0.1.4,同样只许加在末尾。 */
|
||||
SAFETY_EV_BLADE_MUTE, /* 本路 INA226 + PCA9555 连续 T_SAFETY_BLADE_MUTE_N 圈都不应答 ⇒ 关这一路 */
|
||||
} safety_ev_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t t_ms; /* 上电起算的毫秒(§10.7:上位机连上后下发墙钟做偏移) */
|
||||
uint8_t rail; /* 1~5;**0 = 整板级**(mux 失联就是 0) */
|
||||
uint8_t type; /* safety_ev_t */
|
||||
int32_t bus_mv; /* 出事那一刻的实测值,不是设定值 */
|
||||
int32_t current_ua;
|
||||
} safety_event_t; /* 16 B —— §10.7「单条 16 B 量级」 */
|
||||
|
||||
/* 事件出口。A11 在这里接环形缓冲。
|
||||
⚠️ 回调是在**动作之后**才调的(先保命,再记账),而且此刻锁已经放掉了 ——
|
||||
接进来的东西不许阻塞,也不许回头调 t_safety_*。 */
|
||||
typedef void (*safety_ev_fn)(void *ctx, const safety_event_t *ev);
|
||||
void t_safety_set_event_sink(safety_ev_fn fn, void *ctx);
|
||||
|
||||
const char *t_safety_ev_str(safety_ev_t ev);
|
||||
|
||||
/* 每路的判据状态。全在快照里,`t_safety_init()` 一并清干净。 */
|
||||
typedef struct {
|
||||
bool en_on; /* 上一圈看到的 `EFUSE_EN` 影子,用来认它的上升沿 */
|
||||
uint32_t en_ms; /* 上升沿的时刻,缓启动窗口从这里算(这个是真按毫秒的) */
|
||||
uint8_t dev_n; /* 连着几圈越界了。回到范围内 / 这一路被关掉就清零 */
|
||||
uint8_t mute_n; /* 开着电、整片刀片连着几圈不应答。任一颗答应了 / 这一路关着就清零 */
|
||||
|
||||
/* 本圈锁内判据留给锁外的两件事(规矩 3:VBUS 锁内断,eFuse 锁外关)。 */
|
||||
uint8_t pend_ev; /* safety_ev_t,0 = 没有 */
|
||||
bool pend_efuse; /* 锁外要把 `EFUSE_EN` 拉低 */
|
||||
bool pend_abort; /* 锁外要打断这一路正在跑的序列 */
|
||||
} safety_rail_t;
|
||||
|
||||
typedef struct {
|
||||
/* 这一圈读回来的原始量。`ok` 为 false 时对应的值是**上一圈的**,不是 0 ——
|
||||
判据必须先看 ok 再看值,拿一个没更新的电压去判过压是最难查的那种错。 */
|
||||
ina226_vi_t rail[PHONERAIL_RAIL_COUNT];
|
||||
bool rail_ok[PHONERAIL_RAIL_COUNT];
|
||||
|
||||
/* ⭐ R2:**每路一份** PCA9555 输入(R1 是全板一片,所以只有一份)。 */
|
||||
pca9555_in_t io[PHONERAIL_RAIL_COUNT];
|
||||
bool io_ok[PHONERAIL_RAIL_COUNT];
|
||||
|
||||
/* ⭐ R2:`PRSNT#` 快照(纯 GPIO,不占总线)。 */
|
||||
bool present[PHONERAIL_RAIL_COUNT];
|
||||
|
||||
ina226_vi_t input; /* 输入侧 U13(0x43,主干),整板功率预算 */
|
||||
bool input_ok;
|
||||
|
||||
safety_rail_t st[PHONERAIL_RAIL_COUNT];
|
||||
safety_event_t last; /* 最近一条事件;`last.type == SAFETY_EV_NONE` = 还没出过 */
|
||||
uint32_t events;
|
||||
|
||||
uint32_t cycles; /* 跑过几圈(含被跳过的) */
|
||||
uint32_t skipped; /* 其中至少有一路因为拿不到锁被跳过的圈数 */
|
||||
uint32_t i2c_errors; /* 累计器件级失败次数(一圈里可能不止一次) */
|
||||
uint32_t mux_recovers; /* 脉冲过几次 `MUX_RST#` */
|
||||
uint32_t mux_trips; /* 因 mux 失联全断过几次 */
|
||||
/* mux 失联判据走到哪一步了:0 = 没事 / 1 = 这一轮复位过了 / 2 = 这一轮已经全断。
|
||||
⚠️ 少了这个「走到哪一步」,两段式就退化成**死循环**:每一圈都满足
|
||||
「连续失败够数」,于是每 20 ms 脉冲一次 `MUX_RST#`(永远断不掉),
|
||||
或者每 20 ms 全断一次并刷一条事件(把 evlog 那块 flash 写穿)。
|
||||
两种现象都不是「判据不灵」,而是「判据一直在响」。
|
||||
选中过一次通道(streak 归零)就回到 0,下次失联重新走两段。 */
|
||||
uint8_t mux_step;
|
||||
uint32_t bus_us; /* **上一圈**在 I²C 上真正花掉的时间 */
|
||||
uint32_t txns; /* **上一圈**真正发出去的事务数(拿它和预算对账) */
|
||||
uint64_t bus_cursor; /* 内部:位数游标,别在外面改 */
|
||||
uint32_t txn_cursor; /* 内部:事务数游标 */
|
||||
} safety_snap_t;
|
||||
|
||||
void t_safety_init(safety_snap_t *s);
|
||||
hal_err_t t_safety_step(safety_snap_t *s);
|
||||
|
||||
/* 按上面那一圈静态算出来的「一圈该占总线多久」,单位 µs。
|
||||
⚠️ R2 里它含 **6 次 mux 选通**(五路各一次 + 回主干一次)。
|
||||
它和 `s->bus_us` 应当**一模一样** —— 不一样只有两种可能:巡检里多读/少读了
|
||||
东西,或者有别的任务在同一圈里插了事务进来。测试拿这两个数对账。 */
|
||||
uint32_t t_safety_budget_us(void);
|
||||
|
||||
/* 一圈的名义事务数(R2 = 5×(选通+2+1) + (选通+2) = 23)。 */
|
||||
uint32_t t_safety_cycle_txns(void);
|
||||
|
||||
/* 占用率(千分之)。20 ms 周期、2340 µs → 117‰。R1 是 78‰(计划 §5.5)。 */
|
||||
uint32_t t_safety_load_permille(uint32_t bus_us, uint32_t period_ms);
|
||||
|
||||
/* 一行人话,给启动日志和遥测用。返回写进去的字符数(不含结尾 0)。 */
|
||||
int t_safety_format(const safety_snap_t *s, char *buf, size_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* T_SAFETY_H */
|
||||
/* `t_safety` —— 20 ms 巡检任务的**一个周期**(需求 §10.3 第一行)。
|
||||
|
||||
---- R2 之后一个周期是什么样 ----------------------------------------------
|
||||
|
||||
R1:一把 I²C 锁包住整圈,读 6 片 INA226 + 1 片 PCA9555,放锁,再跑判据。
|
||||
R2:五路各在**自己的 mux 通道**上,所以一圈是**五次 `i2cbus_xact` + 一次主干**:
|
||||
|
||||
for 每一路: xact(本路通道) { 读 INA226 的 V/I → 读 PCA9555 两个输入端口
|
||||
→ 跑本路判据(必要时**就在这里**断 VBUS) }
|
||||
xact(主干) { 读输入侧 INA226 }
|
||||
放锁之后: PRSNT# 轮询 → 关 eFuse / abort 序列 / 记事件 → mux 失联判据
|
||||
|
||||
每路一次 `xact` 而不是一把大锁包住整圈,是为了给 `t_actuator` 留得进来的缝
|
||||
(R2 之后按键也走 I²C,计划 §5.6)。
|
||||
|
||||
分成「一个周期是个函数」而不是直接写成 `while(1)`,是为了让它在宿主机上跑得起来。
|
||||
|
||||
---- 四条不能改的规矩 ----------------------------------------------------
|
||||
1. **不许死等锁。** 拿不到就跳过这一路并计数(`skipped`)。死等的现象是
|
||||
巡检整个停摆,而它是优先级最高的那个任务 —— 停摆了没有任何人会发现。
|
||||
2. **一个器件读失败不能中断这一圈。** 某一路坏了,剩下四路的判据照常要出。
|
||||
失联 ≠ 断电(§10.2),失联更不等于「别的路也不看了」。
|
||||
3. **断 VBUS 必须在锁内、在本路的 `xact` 里做。** ⚠️ 这条是 R2 新的:
|
||||
`VBUS_EN` 从 GPIO 变成了刀片 PCA9555 的 P0.4(计划 §5.4),
|
||||
放锁之后再拿一次锁去断,中间隔着一次锁争用 —— 而且那时通道可能已经被
|
||||
别人切走了。**`EFUSE_EN` 相反**:它是 GPIO 直连,放锁之后关才对
|
||||
(拿着总线锁去写引脚只会把别人多挡一会儿)。
|
||||
4. **判据一条也不许额外发 I²C 事务。** 判据要的三个量(设定值、EN 的状态、
|
||||
正在跑的序列)全部有影子可查。多读一次就把预算顶穿,测试会当场变红。 */
|
||||
#ifndef T_SAFETY_H
|
||||
#define T_SAFETY_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
#include "ina226.h"
|
||||
#include "pca9555.h"
|
||||
#include "phonerail.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 每一路等 I²C 锁最多等这么久。等过头这一路就废了,而**废一路一圈**
|
||||
(20 ms 后重来)比**赖着不走**(把最高优先级任务钉死)好得多。 */
|
||||
#define T_SAFETY_LOCK_TIMEOUT_MS 5
|
||||
|
||||
/* ⭐ **整圈等锁的总预算**,R2 必须有的一条。
|
||||
|
||||
R1 一圈只拿一次锁,所以「单次超时 5 ms」就是全部的账。**R2 一圈要拿六次**
|
||||
(五路各一次 + 主干一次),单看每次 5 ms 没问题,六次加起来是 **30 ms —— 比
|
||||
20 ms 的周期还长**。总线长期繁忙时的现象不是「偶尔丢一拍」,而是**巡检成片
|
||||
地丢拍、周期整个失准**,而每一次 `xact` 都老老实实在 5 ms 就返回了,
|
||||
哪一层都不会报错。
|
||||
|
||||
所以除了单次超时,还要有一条整圈的闸:本圈在等锁上花掉的时间超过这个数,
|
||||
**剩下的路这一圈就不读了**(记 `skipped`)。取周期的一半,留另一半给判据、
|
||||
给 `t_actuator`、给别的任务。
|
||||
|
||||
⚠️ 已知的不公平:预算是按顺序花的,所以长期繁忙时**后面几路会比前面几路
|
||||
更常被跳过**。没有做轮转起点,因为 SR-5 判据数的是「连续两圈都越界」,
|
||||
某一路偶尔漏一圈只让它晚一圈跳闸;而轮转会让「第几次拿锁」这种可断言的
|
||||
不变量消失,代价比收益大。真正该看的是 `skipped` 有没有持续增长。 */
|
||||
#define T_SAFETY_LOCK_BUDGET_MS 10
|
||||
|
||||
/* ---- A10:判据阈值。**每一个都有出处,改了就是改需求** ------------------
|
||||
`test_thresholds_match_the_documents()` 会拦。 */
|
||||
|
||||
/* SR-5 / SW-2:「电压偏离设定 >±100 mV」。比的是 INA226 读回来的母线电压与
|
||||
**该路 DAC 的设定值**(`rail_setpoint_mv()`),不是 3900 这个常数 ——
|
||||
SW-4 允许运维单路微调,拿常数比会把一台调到 4.0 V 的机器当成过压。 */
|
||||
#define T_SAFETY_DEV_MV 100
|
||||
|
||||
/* SR-5 的**欠压**一侧单独放宽到 −250 mV(2026-09-28 用户拍板)。
|
||||
上板实测:小米8 插上屏幕后开机峰值 ~2.1 A,轨 1 读回 3785 mV(设定 3900,−115 mV,
|
||||
≈55 mΩ 压降),−100 mV 的门限把正在开机的手机直接关掉。SR-5 本来就只管记录告警,
|
||||
实时保护在硬件里;过压那一侧不动,仍是 +100 mV。 */
|
||||
#define T_SAFETY_UNDER_MV 250
|
||||
|
||||
/* SR-5 的「且持续 20 ms」——「持续」在这里数的是**圈**,不是墙钟毫秒:
|
||||
连着两圈新鲜样本都越界才算,而成立的那一刻就是断电的那一刻。
|
||||
|
||||
⚠️ **不要改回按墙钟算**(`now - 第一次看见 >= 20 ms`)。巡检的采样时刻带着
|
||||
亚毫秒残差:一圈自己要占 2.34 ms 总线,而「睡到下一拍」是按整毫秒算的,
|
||||
于是相邻两次判据的时刻实测是 **21 / 19 ms 交替**。拿 `>= 20 ms` 去判,
|
||||
有一半的机会 19 ms 那一次不算数、要等到第三圈 —— 也就是 40 ms 才关路,
|
||||
直接超了 §12.1 的验收,而且是**间歇性**的。数圈没有这个毛病。
|
||||
|
||||
⚠️ SR-5 说「持续 20 ms 才算」,§12.1 的验收说「20 ms 内关路」——
|
||||
字面上互相顶着,只有「从第一次看见算起、下一圈就断电」同时满足两条。 */
|
||||
#define T_SAFETY_DEV_HOLD_CYCLES 2
|
||||
|
||||
/* 上面那个圈数折算成毫秒 = (圈数 − 1) × 巡检周期。需求 SR-5 写的就是这个 20 ms,
|
||||
`test_thresholds_match_the_documents()` 拿它和任务表里的周期对账。 */
|
||||
#define T_SAFETY_DEV_HOLD_MS 20
|
||||
|
||||
/* SW-8 的反灌判据:设计文档 §15.2 对策 2 原话「if I < −50 mA → 立即拉低 VBUS_EN」。
|
||||
离噪声很远:INA226 电流 LSB 是 0.5 mA,分流失调 ±10 µV / 5 mΩ ≈ ±2 mA,
|
||||
−50 mA 是它的 25 倍。§12.1 的验收注入 −60 mA,落在阈值外侧。
|
||||
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 的过压判据是有意不对称的:
|
||||
· 误断 VBUS 的代价是丢一次 USB 数据,手机还由直供电着,没有任何风险;
|
||||
· 漏判的代价是能量泵回 12V 母线,**连累另外四台**。
|
||||
一圈就断 ⇒ 最坏 40 ms + 一次 I²C 事务(≈0.16 ms),SW-8 给的预算是 200 ms。 */
|
||||
#define T_SAFETY_REV_BUDGET_MS 200
|
||||
|
||||
/* `EFUSE_EN` 拉高之后多久才开始判这一路的电压。tON 5.1 ms + 缓启动斜坡 11.5 ms
|
||||
≈ 17 ms(设计文档 §5 / §7.4),取 3 个巡检周期。
|
||||
没有这个窗口的现象是**每次上电都自己把自己关掉**。 */
|
||||
#define T_SAFETY_SETTLE_MS 60
|
||||
|
||||
/* ⭐ R2 新增:**连续**多少次选不中 mux 通道才认定它失联(计划 §5.8 与 §11)。
|
||||
⚠️ 这个数是「先按 3 写、上板实测总线误码率之后再定」的待定项,不是算出来的。
|
||||
太小(1):一次总线毛刺就停掉五台手机;太大(10):失联之后还有 200 ms
|
||||
在拿旧数据跑判据。判定成立之后**先脉冲一次 `MUX_RST#`**,再不行才全断。 */
|
||||
#define T_SAFETY_MUX_FAIL_N 3
|
||||
|
||||
/* ⭐ 0.1.4:**整片刀片不应答**(本路 INA226 与 PCA9555 **同时** NAK)连续多少圈就关这一路。
|
||||
与 §10.10 第一行「INA226 无应答 → 盲跑、保持供电」不冲突:那一行是**一颗**器件坏了,
|
||||
刀片上的另一颗还答应;两颗都不答应而 mux 是好的(选不中的话本路读函数根本不会被调,
|
||||
计不进来),只剩两种可能 ——
|
||||
· 刀片根本不在,是 `PRSNT#` 假在位(2026-09-28 板 B 第 2 槽:卡低之后空槽 eFuse 开了约 4 h);
|
||||
· 刀片在,但这一路完全失控:看不到 V/I、看不到 PG/FLT、**连 VBUS 都断不了**。
|
||||
两种都是「继续供电但看不见」,和 mux 失联全断同一个道理,只是范围是一路。
|
||||
10 圈 = 200 ms:本板实测单路 9 万圈 0 次 NAK,连续 10 次不是毛刺;空槽带 12 V 多 200 ms 无所谓。 */
|
||||
#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 的「单条」形状) -----------------------------------
|
||||
A11 把这条记录塞进环形缓冲,这里只负责**产生**它。 */
|
||||
|
||||
typedef enum {
|
||||
SAFETY_EV_NONE = 0,
|
||||
SAFETY_EV_OVER_VOLT, /* SR-5:高于设定 >100 mV 持续 20 ms → 关这一路 */
|
||||
SAFETY_EV_UNDER_VOLT, /* SR-5:低于设定 >100 mV 持续 20 ms → 关这一路 */
|
||||
SAFETY_EV_REVERSE, /* SW-8:电流转负(I < −300 mA)→ 断这一路 VBUS */
|
||||
SAFETY_EV_PUSHED_UP, /* SW-8:VBUS 期间电压被顶高 >+100 mV → 断这一路 VBUS */
|
||||
/* ⭐ R2 新增两条(计划 §5.8)。放在**末尾**:`evlog` 那边的数字是 flash 格式。 */
|
||||
SAFETY_EV_MUX_LOST, /* TCA9548A 连续选不中 ⇒ 五路全部断电(单点) */
|
||||
SAFETY_EV_SLOT_EMPTY, /* `PRSNT#` 为高(刀片不在位)⇒ 这一路不许带电 */
|
||||
/* 0.1.4,同样只许加在末尾。 */
|
||||
SAFETY_EV_BLADE_MUTE, /* 本路 INA226 + PCA9555 连续 T_SAFETY_BLADE_MUTE_N 圈都不应答 ⇒ 关这一路 */
|
||||
} safety_ev_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t t_ms; /* 上电起算的毫秒(§10.7:上位机连上后下发墙钟做偏移) */
|
||||
uint8_t rail; /* 1~5;**0 = 整板级**(mux 失联就是 0) */
|
||||
uint8_t type; /* safety_ev_t */
|
||||
int32_t bus_mv; /* 出事那一刻的实测值,不是设定值 */
|
||||
int32_t current_ua;
|
||||
} safety_event_t; /* 16 B —— §10.7「单条 16 B 量级」 */
|
||||
|
||||
/* 事件出口。A11 在这里接环形缓冲。
|
||||
⚠️ 回调是在**动作之后**才调的(先保命,再记账),而且此刻锁已经放掉了 ——
|
||||
接进来的东西不许阻塞,也不许回头调 t_safety_*。 */
|
||||
typedef void (*safety_ev_fn)(void *ctx, const safety_event_t *ev);
|
||||
void t_safety_set_event_sink(safety_ev_fn fn, void *ctx);
|
||||
|
||||
const char *t_safety_ev_str(safety_ev_t ev);
|
||||
|
||||
/* 每路的判据状态。全在快照里,`t_safety_init()` 一并清干净。 */
|
||||
typedef struct {
|
||||
bool en_on; /* 上一圈看到的 `EFUSE_EN` 影子,用来认它的上升沿 */
|
||||
uint32_t en_ms; /* 上升沿的时刻,缓启动窗口从这里算(这个是真按毫秒的) */
|
||||
uint8_t dev_n; /* 连着几圈越界了。回到范围内 / 这一路被关掉就清零 */
|
||||
uint8_t mute_n; /* 开着电、整片刀片连着几圈不应答。任一颗答应了 / 这一路关着就清零 */
|
||||
|
||||
/* 热插拔(T_SAFETY_INSERT_N 的理由)。 */
|
||||
bool was_present; /* 上一圈的 `PRSNT#`,认「插进来」那一下 */
|
||||
bool need_init; /* 插进来了、还没初始化成功 */
|
||||
uint8_t ins_n; /* 距上次尝试(或插入)过了几圈 */
|
||||
|
||||
/* 本圈锁内判据留给锁外的两件事(规矩 3:VBUS 锁内断,eFuse 锁外关)。 */
|
||||
uint8_t pend_ev; /* safety_ev_t,0 = 没有 */
|
||||
bool pend_efuse; /* 锁外要把 `EFUSE_EN` 拉低 */
|
||||
bool pend_abort; /* 锁外要打断这一路正在跑的序列 */
|
||||
} safety_rail_t;
|
||||
|
||||
typedef struct {
|
||||
/* 这一圈读回来的原始量。`ok` 为 false 时对应的值是**上一圈的**,不是 0 ——
|
||||
判据必须先看 ok 再看值,拿一个没更新的电压去判过压是最难查的那种错。 */
|
||||
ina226_vi_t rail[PHONERAIL_RAIL_COUNT];
|
||||
bool rail_ok[PHONERAIL_RAIL_COUNT];
|
||||
|
||||
/* ⭐ R2:**每路一份** PCA9555 输入(R1 是全板一片,所以只有一份)。 */
|
||||
pca9555_in_t io[PHONERAIL_RAIL_COUNT];
|
||||
bool io_ok[PHONERAIL_RAIL_COUNT];
|
||||
|
||||
/* ⭐ R2:`PRSNT#` 快照(纯 GPIO,不占总线)。 */
|
||||
bool present[PHONERAIL_RAIL_COUNT];
|
||||
|
||||
ina226_vi_t input; /* 输入侧 U13(0x43,主干),整板功率预算 */
|
||||
bool input_ok;
|
||||
|
||||
safety_rail_t st[PHONERAIL_RAIL_COUNT];
|
||||
safety_event_t last; /* 最近一条事件;`last.type == SAFETY_EV_NONE` = 还没出过 */
|
||||
uint32_t events;
|
||||
|
||||
uint32_t cycles; /* 跑过几圈(含被跳过的) */
|
||||
uint32_t skipped; /* 其中至少有一路因为拿不到锁被跳过的圈数 */
|
||||
uint32_t i2c_errors; /* 累计器件级失败次数(一圈里可能不止一次) */
|
||||
uint32_t blade_inits; /* 热插拔进来的刀片成功重新初始化过几次 */
|
||||
uint32_t mux_recovers; /* 脉冲过几次 `MUX_RST#` */
|
||||
uint32_t mux_trips; /* 因 mux 失联全断过几次 */
|
||||
/* mux 失联判据走到哪一步了:0 = 没事 / 1 = 这一轮复位过了 / 2 = 这一轮已经全断。
|
||||
⚠️ 少了这个「走到哪一步」,两段式就退化成**死循环**:每一圈都满足
|
||||
「连续失败够数」,于是每 20 ms 脉冲一次 `MUX_RST#`(永远断不掉),
|
||||
或者每 20 ms 全断一次并刷一条事件(把 evlog 那块 flash 写穿)。
|
||||
两种现象都不是「判据不灵」,而是「判据一直在响」。
|
||||
选中过一次通道(streak 归零)就回到 0,下次失联重新走两段。 */
|
||||
uint8_t mux_step;
|
||||
uint32_t bus_us; /* **上一圈**在 I²C 上真正花掉的时间 */
|
||||
uint32_t txns; /* **上一圈**真正发出去的事务数(拿它和预算对账) */
|
||||
uint64_t bus_cursor; /* 内部:位数游标,别在外面改 */
|
||||
uint32_t txn_cursor; /* 内部:事务数游标 */
|
||||
} safety_snap_t;
|
||||
|
||||
void t_safety_init(safety_snap_t *s);
|
||||
hal_err_t t_safety_step(safety_snap_t *s);
|
||||
|
||||
/* 按上面那一圈静态算出来的「一圈该占总线多久」,单位 µs。
|
||||
⚠️ R2 里它含 **6 次 mux 选通**(五路各一次 + 回主干一次)。
|
||||
它和 `s->bus_us` 应当**一模一样** —— 不一样只有两种可能:巡检里多读/少读了
|
||||
东西,或者有别的任务在同一圈里插了事务进来。测试拿这两个数对账。 */
|
||||
uint32_t t_safety_budget_us(void);
|
||||
|
||||
/* 一圈的名义事务数(R2 = 5×(选通+2+1) + (选通+2) = 23)。 */
|
||||
uint32_t t_safety_cycle_txns(void);
|
||||
|
||||
/* 占用率(千分之)。20 ms 周期、2340 µs → 117‰。R1 是 78‰(计划 §5.5)。 */
|
||||
uint32_t t_safety_load_permille(uint32_t bus_us, uint32_t period_ms);
|
||||
|
||||
/* 一行人话,给启动日志和遥测用。返回写进去的字符数(不含结尾 0)。 */
|
||||
int t_safety_format(const safety_snap_t *s, char *buf, size_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* T_SAFETY_H */
|
||||
|
||||
在新工单中引用
屏蔽一个用户