A10:安全巡检的判据(SR-5 / SW-8)

判据全在 t_safety_step() 里、**放锁之后**、直接写引脚(§10.3 原话:不经任何
队列或抽象层):

  SR-5   |实测 − 该路设定值| > 100 mV **连着两圈** → 先断 VBUS 再关 eFuse
  SW-8   I < −50 mA,或 VBUS 期间电压被顶高 > +100 mV → **一圈就断 VBUS**

七条实现约定(详见需求文档 §10.2):

· 「持续 20 ms」数的是圈,不是墙钟毫秒;
· 反灌不设确认窗口,与过压那 20 ms 的不对称是有意的 —— 误断 VBUS 只丢一次
  USB 数据,漏判会把能量泵回 12V 母线连累另外四台(设计文档 §15.2);
· 判据只在「该路该有电 + 这一圈的值新鲜 + 过了 60 ms 缓启动窗口」时武装
  (对应 §10.10 的「盲跑 → 保持供电,禁用该路判据」);
· 比的是该路 DAC 设定值的影子,不是 3900 这个常数(SW-4 要单路微调);
· SR-5 关路关干净(不留「只靠 USB 供着电」的窗口),SW-8 只断 VBUS 不碰 eFuse;
· 断电之前先 t_actuator_abort(),断 VBUS 时只打断 EDL(只有它写 VBUS_EN);
· 动作在前、记录在后,事件走回调出去,A11 在那儿接环形缓冲。

判据一条 I²C 事务都不发(设定值、两根 EN、正在跑的序列全部走影子),
§10.3 那 7.8 % 的预算不变。顺带新增两处影子:rail_efuse_is_on()/rail_vbus_is_on()
与 rail_setpoint_mv(),都**只在写成功时更新**,两个方向都倒向安全的那一侧。

29 个用例;25 条注错逐个跑过,24 条变红。第一版把「持续 20 ms」按墙钟算,
测试一半红一半绿 —— 巡检的采样时刻带亚毫秒残差(一圈占 1.56 ms 总线,
而睡到下一拍按整毫秒算),相邻两次判据的时刻是 21/19 ms 交替,19 ms 那一半
要拖到第三圈 = 40 ms,超验收且间歇性。改成数圈之后一次不飘,
并留了一条跑 20 轮、两种相位都踩得到的回归拦网。

Co-Authored-By: Claude Opus 5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-07 08:41:53 +08:00
共同撰写人 Claude Opus 5
父节点 b3206005ab
当前提交 273c53347e
共修改 11 个文件,包含 1354 行新增和 206 行删除
+42
查看文件
@@ -349,6 +349,48 @@ USB 通断」这些原本要人手做的动作全部电气化,交给板上 MCU
**推论**:失联期间固件必须能独立运行全部本地职责 —— 巡检、风扇闭环、事件记录、
按队列执行未完成的序列。上位机回来之后靠事件日志补齐时间线(10.7)。
> **A10 已落地**(2026-09-07,`firmware/main/t_safety.{h,c}`,判据全在
> `t_safety_step()` 里、放锁之后、直接写引脚)。本表第三行「本地判据触发 → 关对应
> 那一路」的两条判据是 SR-5 与 SW-8,实现时定下来的七条:
>
> 1. **「持续 20 ms」数的是圈,不是墙钟毫秒。** 巡检的采样时刻带亚毫秒残差
> (一圈自己占 1.56 ms 总线,而「睡到下一拍」按整毫秒算),实测相邻两次判据的
> 时刻是 **21 / 19 ms 交替**。按 `>= 20 ms` 判,19 ms 那半数情况要拖到第三圈
> ——**40 ms**,超了 §12.1 的验收,而且是间歇性的:同一个故障注两次,一次过
> 一次不过。改成「连着两圈都越界」,在 20 ms 的周期下就是 20 ms。
> (SR-5 说「持续 20 ms 才算」、§12.1 说「20 ms 内关路」,字面互相顶着,
> 只有「第一次看见的下一圈就断」同时满足两条。这 20 ms 是从**第一次看见**算的,
> 对外部世界的最坏延迟是 2 个周期 —— 不要紧,SR-5 原文就写着它
> 「仅承担记录与告警,不承担实时保护」。)
> 2. **反灌不设确认窗口,一圈看见就断**(设计文档 §15.2 对策 2 原话「立即」)。
> 与过压那 20 ms 的不对称是有意的:误断 VBUS 只丢一次 USB 数据,手机还由直供
> 电着;漏判则把能量泵回 12V 母线**连累另外四台**。最坏 2 圈 = 40 ms,
> SW-8 给的预算是 200 ms。
> 3. **判据只在「这一路该有电」且「这一圈的值是新鲜的」时武装**,另加一个
> `EFUSE_EN↑` 之后 **60 ms** 的缓启动窗口(tON 5.1 + 斜坡 11.5 ≈ 17 ms 的 3 倍)。
> 少了第一条,每圈都会「发现」关着的五路各偏低 3900 mV;少了第三条,
> **每次上电都会自己把自己关掉**;第二条就是 §10.10 那行「盲跑 → 保持供电,
> 禁用该路的 SR-5 / SW-8 判据」。
> 4. **比的是该路 DAC 的设定值**(`rail_setpoint_mv()` 的影子,不碰总线),
> 不是 3900 这个常数 —— SW-4 允许运维单路微调,拿常数比会把一台调到 4.0 V 的
> 机器当成过压关掉。
> 5. **SR-5 关路 = 先断 VBUS 再关 eFuse**(`rail_all_off()` 的同一条理由:中间不留
> 「手机只靠 USB 供着电」那个既不是开也不是关的窗口);**SW-8 只断 VBUS,
> 不碰 eFuse** —— 反灌是 USB 那一侧的事,把直供也关掉等于「因为充电器有问题
> 就把手机关机」。过压且 VBUS 通着时两条都成立,于是**先撤能量来源,
> 20 ms 后再决定要不要关这一路**。
> 6. **断电之前先 `t_actuator_abort()`。** 不 abort 的现象最阴:硬重启的 `HR_OFF`
> 会在 4 s 后自己把 `EFUSE_EN` 抬回去,「关掉的那一路」几秒后又带电了,
> 而日志里只有一条「已关路」。断 VBUS 时只打断 **EDL**(三个序列里只有它写
> `VBUS_EN`)—— 无条件打断会把一次正在进行的救砖重启也顺手停掉。
> 7. **动作在前,记录在后**,事件出口是一个回调(`t_safety_set_event_sink()`),
> A11 在那里接环形缓冲。记录用的 `safety_event_t` 已经按 §10.7 的「单条」
> 形状定好(时间戳 + 路号 + 类型 + 当时的 V/I,16 B)。
>
> 29 条 ctest 用例,25 条「一定会被写出来的错」逐个注进去跑过,**24 条变红**;
> 剩下那一条(关路之后把 `dev_n` 清零)在 sim 里造不出「写引脚失败」,抓不到 ——
> 所以那一行干脆没写,理由记在代码里。
### 10.3 任务划分(FreeRTOS / ESP-IDF)
| 任务 | 周期 | 优先级 | 职责 | 失败后果 |