文件
phonerail/firmware/main/t_safety.h
T
zhou yang和Claude Opus 5 ec31891429 A8:任务骨架与调度(5 个任务、两把锁)
需求 §10.3 落地。四件事:

1. 五任务表(tasks.c):周期与优先级写在一张 const 表里,开机自检查的是
   相对顺序(safety 唯一最高、log 唯一最低、thermal 与 actuator 同级),
   外加「巡检周期必须长于 INA226 一帧转换 8.8 ms」。不过就 abort。

2. 节拍器(sched.c):记下一次该醒的绝对时刻,回绕安全(int32_t 差判先后)。
   过载时跳过错过的整周期、保住相位,不补跑。

3. 两把锁(hal_lock):I²C 与 74HC595,禁止嵌套持有 —— 这块板上两者没有交集,
   「拿着 A 去拿 B」永远是写错了。ESP 侧用 mutex 取优先级继承。

4. t_safety 的一个周期:拿锁 → 6 片 INA226 的 V+I + PCA9555 两端口 → 放锁。
   不死等锁(5 ms 超时就跳过这圈),一片读失败不中断整圈、保留上一圈的值。

I²C 占用由 HAL 按线上真实字节数记账(两个目标同一份代码),静态预算与实测
必须相等:一次寄存器读 48 位,一圈 13 次事务 = 1560 µs / 20 ms = 7.8 %,
与 §10.3 手算的「约 1.5 ms / 约 8 %」相符(本节的 46 位没数 STOP 和重复 START)。

sim 侧配套两样:假 I²C 事务真的把虚拟时钟花掉(否则时序测试是空话),
以及可强制的加锁纪律检查(配一条反向测试,防止检查本身失灵)。

11 条 ctest(161 例)全绿,idf.py build 通过;16 个注错实验全部变红。

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 07:35:39 +08:00

73 行
3.5 KiB
C

/* `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 <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
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 */