判据全在 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]>
159 行
8.9 KiB
C
159 行
8.9 KiB
C
/* `t_safety` —— 20 ms 巡检任务的**一个周期**(需求 §10.3 第一行)。
|
||
|
||
一个周期干四件事:拿 I²C 锁 → 读 6 片 INA226 的 V+I 与 PCA9555 两个输入端口
|
||
→ 放锁 → **跑判据**(A10:SR-5 / SW-8,见本文件下半段)。
|
||
|
||
分成「一个周期是个函数」而不是直接写成 `while(1)`,是为了让它在宿主机上跑得起来:
|
||
宿主机没有 FreeRTOS,任务体是死循环就一行也测不了。ESP 侧的 `while(1)`
|
||
在 app_main.c 里,只有三行(拿节拍 / 调这个 / 睡到下一拍)。
|
||
|
||
---- 三条不能改的规矩 ----------------------------------------------------
|
||
1. **不许死等锁。** 拿不到就跳过这一周期并计数(`skipped`)。死等的现象是
|
||
巡检整个停摆,而它是优先级最高的那个任务 —— 停摆了没有任何人会发现。
|
||
2. **一个器件读失败不能中断这一圈。** 六片里坏一片,剩下五路的判据照常要出。
|
||
失联 ≠ 断电(§10.2),失联更不等于「别的路也不看了」。
|
||
3. **越界要断电时直接写 `EFUSE_EN` / `VBUS_EN` 引脚**(§10.3 原话:不经任何队列
|
||
或抽象层)。所以判据就写在这个函数里,不是发个消息给 t_actuator。 */
|
||
#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 锁最多等这么久。取周期的 1/4:等过头这一周期就废了,
|
||
而**废一个周期**(20 ms 后重来)比**赖着不走**(把最高优先级任务钉死)好得多。 */
|
||
#define T_SAFETY_LOCK_TIMEOUT_MS 5
|
||
|
||
/* ---- 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 的「且持续 20 ms」——「持续」在这里数的是**圈**,不是墙钟毫秒:
|
||
连着两圈新鲜样本都越界才算,而成立的那一刻就是断电的那一刻。
|
||
|
||
⚠️ **不要改回按墙钟算**(`now - 第一次看见 >= 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 内关路」——
|
||
字面上互相顶着,只有「从第一次看见算起、下一圈就断电」同时满足两条。
|
||
注意这是从**第一次看见**算的:物理事件最早可能发生在上一圈之后,
|
||
所以对着外部世界的最坏延迟是 2 个周期。这不要紧 —— SR-5 原文就写着
|
||
「仅承担记录与告警,不承担实时保护」,真正的过压保护是 eFuse 里的硬件 OVP。 */
|
||
#define T_SAFETY_DEV_HOLD_CYCLES 2
|
||
|
||
/* 上面那个圈数折算成毫秒 = (圈数 − 1) × 巡检周期。需求 SR-5 写的就是这个 20 ms,
|
||
`test_thresholds_match_the_documents()` 拿它和任务表里的周期对账 ——
|
||
哪天巡检周期改了,这两个数会当场对不上,而不是悄悄把判据变成 40 ms。 */
|
||
#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 母线,**连累另外四台**(设计文档 §15.2 末尾)。
|
||
一圈就断 ⇒ 最坏 40 ms(事件发生 + 一圈),SW-8 给的预算是 200 ms,5 倍余量。 */
|
||
#define T_SAFETY_REV_BUDGET_MS 200
|
||
|
||
/* `EFUSE_EN` 拉高之后多久才开始判这一路的电压。tON 5.1 ms + 缓启动斜坡 11.5 ms
|
||
≈ 17 ms(设计文档 §5 / §7.4),取 3 个巡检周期。
|
||
没有这个窗口的现象是**每次上电都自己把自己关掉**:斜坡走到一半时
|
||
实测电压离设定值差着好几百 mV,判据不认识「正在爬」和「掉下去了」的区别。 */
|
||
#define T_SAFETY_SETTLE_MS 60
|
||
|
||
/* ---- A10:事件(§10.7 的「单条」形状:时间戳 + 路号 + 类型 + 当时的 V/I)----
|
||
A11 会把这条记录塞进环形缓冲,这里只负责**产生**它。故意先把记录类型定下来,
|
||
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 */
|
||
} safety_ev_t;
|
||
|
||
typedef struct {
|
||
uint32_t t_ms; /* 上电起算的毫秒(§10.7:上位机连上后下发墙钟做偏移) */
|
||
uint8_t rail; /* 1~5 */
|
||
uint8_t type; /* safety_ev_t */
|
||
int32_t bus_mv; /* 出事那一刻的实测值,不是设定值 */
|
||
int32_t current_ua;
|
||
} safety_event_t; /* 16 B —— §10.7「单条 16 B 量级」 */
|
||
|
||
/* 事件出口。A11 在这里接环形缓冲;不接就只有快照里的 `last` 与计数。
|
||
⚠️ 回调是在**断电之后**才调的(先保命,再记账),而且此刻两把锁都没拿着 ——
|
||
接进来的东西不许阻塞,也不许回头调 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; /* 连着几圈越界了。回到范围内 / 这一路被关掉就清零 */
|
||
} safety_rail_t;
|
||
|
||
typedef struct {
|
||
/* 这一圈读回来的原始量。`ok` 为 false 时对应的值是**上一圈的**,不是 0 ——
|
||
判据必须先看 ok 再看值,拿一个没更新的电压去判过压是最难查的那种错。 */
|
||
ina226_vi_t rail[PHONERAIL_RAIL_COUNT];
|
||
bool rail_ok[PHONERAIL_RAIL_COUNT];
|
||
ina226_vi_t input; /* 输入侧 U13(0x43),整板功率预算 */
|
||
bool input_ok;
|
||
pca9555_in_t io; /* 五路 FLT# / PGOOD + Buck PG 或 + INA ALERT 或 */
|
||
bool io_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; /* 其中因为拿不到 I²C 锁而整圈跳过的 */
|
||
uint32_t i2c_errors; /* 累计器件级失败次数(一圈里可能不止一次) */
|
||
uint32_t bus_us; /* **上一圈**在 I²C 上真正花掉的时间 */
|
||
uint64_t bus_cursor; /* 内部:位数游标,别在外面改 */
|
||
} safety_snap_t;
|
||
|
||
void t_safety_init(safety_snap_t *s);
|
||
hal_err_t t_safety_step(safety_snap_t *s);
|
||
|
||
/* 按 §10.3 的算式静态算出来的「一圈该占总线多久」,单位 µs。
|
||
它和 `s->bus_us` 应当**一模一样** —— 不一样只有两种可能:巡检里多读/少读了东西,
|
||
或者有别的任务在同一圈里插了事务进来。测试拿这两个数对账。 */
|
||
uint32_t t_safety_budget_us(void);
|
||
|
||
/* 占用率(千分之)。20 ms 周期、1560 µs → 78‰。§10.3 手算的是「约 8 %」。 */
|
||
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 */
|