/* `t_safety` —— 20 ms 巡检任务的**一个周期**(需求 §10.3 第一行)。 本包(A8)只做骨架:拿 I²C 锁 → 读 6 片 INA226 的 V+I → 读 PCA9555 两个输入端口 → 放锁 → 记账。**判据(SW-2 / SW-8 / 过压 / 反灌)是 A10 的事**,这里一条都没有。 分成「一个周期是个函数」而不是直接写成 `while(1)`,是为了让它在宿主机上跑得起来: 宿主机没有 FreeRTOS,任务体是死循环就一行也测不了。ESP 侧的 `while(1)` 在 app_main.c 里,只有三行(拿节拍 / 调这个 / 睡到下一拍)。 ---- 三条不能改的规矩 ---------------------------------------------------- 1. **不许死等锁。** 拿不到就跳过这一周期并计数(`skipped`)。死等的现象是 巡检整个停摆,而它是优先级最高的那个任务 —— 停摆了没有任何人会发现。 2. **一个器件读失败不能中断这一圈。** 六片里坏一片,剩下五路的判据照常要出。 失联 ≠ 断电(§10.2),失联更不等于「别的路也不看了」。 3. **越界要断电时直接写 `EFUSE_EN` / `VBUS_EN` 引脚**(§10.3 原话:不经任何队列 或抽象层)。所以 A10 的判据要写在这个函数里,不是发个消息给 t_actuator。 */ #ifndef T_SAFETY_H #define T_SAFETY_H #include #include #include #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 typedef struct { /* 这一圈读回来的原始量。`ok` 为 false 时对应的值是**上一圈的**,不是 0 —— 判据(A10)必须先看 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; 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 */