2026-09-16 完成、当时未提交,这里补提交(原样,不含 09-27 上板的修改)。 - 器件层 / 拓扑层整体从 R1 换到 R2:TCA9548A 分通道、五路同址 INA226 0x40 + 输入侧 0x43、 刀片 PCA9555(VBUS_EN 挪到 P0.4)、CH224A 只读驱动、风扇闭环 t_thermal - 新增 i2cbus(mux 原子事务)、slot(刀片 I/O 门面)、host/qtest.sh - 74HC595 链退役(代码与测试保留) - 判据阈值与时间常量未动;19 个 ctest 全绿,idf.py build 通过 Co-Authored-By: Claude Opus 5.5 <[email protected]>
167 行
9.8 KiB
C
167 行
9.8 KiB
C
/* 事件日志(需求 §10.7)——「RAM 环形缓冲 + 批量落盘 + 按序号增量拉取」。
|
||
|
||
§10.7 的第一句话是这一节全部设计的来源:**记录路径不能依赖上位机在线**。
|
||
排线接触不良、手机把电压顶回来、eFuse 断了 —— 这些事发生时,
|
||
最可能同时发生的就是「WiFi 也断了」。所以事件先进 RAM,再由最低优先级的
|
||
`t_log` 攒够一批写进 flash;上位机什么时候连上来,什么时候按序号拉走。
|
||
|
||
---- 三层,各自只干一件事 ------------------------------------------------
|
||
1. **入队**(`evlog_put*`):谁产生事件谁调。几条指令,临界区里做完,
|
||
**绝不阻塞、绝不碰 flash**。t_safety 是在断电之后调它的(t_safety.h)。
|
||
2. **落盘**(`evlog_flush`):只有 `t_log` 调。它是唯一碰 flash 的人。
|
||
3. **读回**(`evlog_read`):按全局序号取,给 A12 的 `events 起始序号` 用。
|
||
|
||
---- 为什么序号不在入队时分配 --------------------------------------------
|
||
序号是**记录在 flash 里的位置**算出来的(`epoch × 每扇区条数 + 槽号`),
|
||
不是一个自增计数器。这样一来「序号」和「flash 里第几条」永远是同一件事,
|
||
重启之后接着数也不用把计数器存起来。代价是**没落盘的事件还没有序号**:
|
||
`evlog_read()` 只服务已经落盘的部分,要看最新的就先 `evlog_flush(0)`。
|
||
反过来做(入队时发号)会在 RAM 缓冲溢出丢事件时留下一段永远补不上的空号,
|
||
而上位机那边的现象是「拉日志卡在某个序号上不动」。
|
||
|
||
---- 丢事件是可以的,**悄悄丢**不行 --------------------------------------
|
||
RAM 缓冲满了就覆盖最旧的(§10.7 的 4096 条同理:满了盖最旧,最新的一条
|
||
永远进得去)。被盖掉的那些会在下一次落盘时留下一条 `LOG_EV_LOST`,
|
||
`bus_mv` 字段记着丢了多少条。一份会**安静地**少掉几行的日志,
|
||
比没有日志更糟 —— 排查的人会拿它当完整的时间线看。 */
|
||
#ifndef EVLOG_H
|
||
#define EVLOG_H
|
||
|
||
#include <stdbool.h>
|
||
#include <stdint.h>
|
||
|
||
#include "hal_err.h"
|
||
#include "t_safety.h"
|
||
|
||
#ifdef __cplusplus
|
||
extern "C" {
|
||
#endif
|
||
|
||
/* ---- 事件类型 ------------------------------------------------------------
|
||
⚠️⚠️ **这些数字是 flash 里的格式,不是一个内部枚举。** 板子上跑了半年的
|
||
日志是按数字存的,改一个值就等于把历史记录全部重新解释了一遍,
|
||
而且**读出来仍然是合法的记录**,没有任何一处会报错。
|
||
只许在末尾追加,不许插入、不许重排、不许删除。
|
||
`test_flash_format_is_frozen()` 把每一个数字都钉死了。 */
|
||
typedef enum {
|
||
LOG_EV_NONE = 0,
|
||
LOG_EV_BOOT = 1, /* 每次上电的第一条。§10.7「上电从 0 计」⇒ 时间戳会跳回去,
|
||
没有这一条的话,重启前后的记录混在一起没法读 */
|
||
LOG_EV_LOST = 2, /* RAM 缓冲溢出,丢了 bus_mv 条(见文件头) */
|
||
|
||
/* A10 的四条判据事件。⚠️ 这里的数字与 `safety_ev_t` 的**不一样**,
|
||
两边靠 `evlog_from_safety()` 里那个显式 switch 对应 —— 不许写成加个偏移。 */
|
||
LOG_EV_OVER_VOLT = 3, /* SR-5:高于设定 >100 mV 持续 20 ms → 关这一路 */
|
||
LOG_EV_UNDER_VOLT = 4, /* SR-5:低于设定 >100 mV 持续 20 ms → 关这一路 */
|
||
LOG_EV_REVERSE = 5, /* SW-8:电流转负 → 断这一路 VBUS */
|
||
LOG_EV_PUSHED_UP = 6, /* SW-8:VBUS 期间电压被顶高 → 断这一路 VBUS */
|
||
|
||
/* §10.7 必记事件里**还没人产生**的那几类。号先占住(见上面那条警告),
|
||
产生它们的代码各归各的包:括号里是包号。 */
|
||
LOG_EV_EFUSE_FAULT = 7, /* eFuse FLT# 拉低(A15) */
|
||
LOG_EV_PG_CHANGE = 8, /* PGOOD 变化,arg = 新电平(A15) */
|
||
LOG_EV_I2C_LOST = 9, /* I²C 器件失联,arg = 地址(A15) */
|
||
LOG_EV_THERMAL_DERATE = 10,/* 过温降载,bus_mv = 温度 m°C(A16) */
|
||
LOG_EV_FAN_STALL = 11, /* 风扇停转(A16) */
|
||
LOG_EV_REBOOT = 12, /* 每次重启序列,arg = 软/硬 + 谁下的命令(A9/A12) */
|
||
LOG_EV_CMD_REJECT = 13, /* 越界命令被拒,arg = 命令号(A12) */
|
||
|
||
/* ⭐ R2 新增(计划 §5.8 / §5.9)。**加在末尾**,前面 13 个的数字一个都没动 ——
|
||
flash 里跑过的历史记录因此仍然读得对。 */
|
||
LOG_EV_MUX_LOST = 14, /* TCA9548A 连续选不中 ⇒ 五路全断(rail = 0,整板级) */
|
||
LOG_EV_SLOT_EMPTY = 15,/* `PRSNT#` 为高:刀片不在位,这一路不许带电 */
|
||
LOG_EV_PD_LIMIT = 16, /* PD 档位换了 / 触发限功率降载,bus_mv = 上限 mW/100(R-6) */
|
||
|
||
LOG_EV_COUNT,
|
||
} log_ev_t;
|
||
|
||
/* §10.7 的「单条」:时间戳 + 路号 + 事件类型 + 当时的 V/I,正好 16 B。
|
||
⚠️ 这个结构体的**内存布局就是 flash 布局**,直接按字节写下去的。
|
||
加字段 = 换格式(见上),改字段顺序同理。 */
|
||
typedef struct {
|
||
uint32_t t_ms; /* 本次上电起算的毫秒(§10.7:上位机连上后下发墙钟做偏移) */
|
||
uint8_t rail; /* 1~5;0 = 整板级事件(BOOT / LOST / 风扇 …) */
|
||
uint8_t type; /* log_ev_t */
|
||
uint8_t arg; /* 事件相关的小参数,没有就 0 */
|
||
uint8_t chk; /* 另外 15 个字节的 CRC-8。见 evlog.c「为什么要校验」 */
|
||
int32_t bus_mv; /* 出事那一刻的实测值,不是设定值 */
|
||
int32_t current_ua;
|
||
} evlog_rec_t;
|
||
|
||
/* RAM 环形缓冲的条数。§10.7 写的是「≥256 条」。
|
||
256 × 16 B = 4 KB —— 在 512 KB 的内部 RAM 里可以忽略,而它换来的是
|
||
「t_log 被饿住 4 秒(按 §10.7 的量级远不止)也不丢事件」。 */
|
||
#define EVLOG_RAM_CAP 256
|
||
|
||
/* 攒够这么多条就落盘一次。一次 flash 写事务的开销与条数几乎无关,
|
||
16 条一批 ⇒ 每条事件的擦写摊薄到 1/16。 */
|
||
#define EVLOG_BATCH 16
|
||
|
||
/* 但最老的那一条最多等这么久 —— 攒批不能把「掉电前记下来」这件事攒没了。
|
||
1 s 是 §10.3 里 t_log「空闲时」的量级,也是掉电丢失窗口的上界。 */
|
||
#define EVLOG_MAX_AGE_MS 1000
|
||
|
||
/* 一次 `evlog_flush()` 最多写这么多条。t_log 是最低优先级,长时间占着 flash
|
||
本身没有危害,但一次写 4000 条会让**这个函数**变成一个几百毫秒的黑洞,
|
||
而它是要被 A12 的命令处理器同步调用的(拉日志之前先 flush)。 */
|
||
#define EVLOG_FLUSH_MAX 64
|
||
|
||
typedef struct {
|
||
uint32_t queued; /* 累计入队几条 */
|
||
uint32_t stored; /* 累计落盘几条(含补记的 LOST) */
|
||
uint32_t dropped; /* 累计被覆盖掉几条(RAM 缓冲溢出) */
|
||
uint32_t pending; /* 此刻还在 RAM 里没落盘的条数 */
|
||
uint32_t io_errors;/* flash 写/擦失败次数 */
|
||
uint32_t oldest_seq; /* flash 里还留着的最老一条的序号 */
|
||
uint32_t next_seq; /* 下一条落盘的记录会拿到的序号 */
|
||
} evlog_stats_t;
|
||
|
||
/* 扫一遍 flash 找到接着写的位置,并**入队一条 `LOG_EV_BOOT`**。
|
||
幂等:重复调等于「又重启了一次」,会再记一条 BOOT。
|
||
返回 HAL_ERR_STATE = 没有那块分区(ESP 上是分区表漏了 `evlog`)——
|
||
⚠️ 调用方**不要因此 abort**:日志是排查手段,不是安全链的一环(§10.2)。
|
||
这种情况下入队照常工作(事件留在 RAM 里),只是落不了盘。 */
|
||
hal_err_t evlog_init(void);
|
||
|
||
/* 入队一条。**不阻塞、不碰 flash、不会失败** —— 缓冲满了就盖掉最旧的一条
|
||
并记一次 `dropped`(下一次落盘时会补一条 LOST)。
|
||
任何任务、任何优先级都可以调,包括 t_safety 断电之后的那一下。 */
|
||
void evlog_put(uint8_t rail, log_ev_t type, uint8_t arg, int32_t bus_mv, int32_t ua);
|
||
|
||
/* A10 的事件出口。直接 `t_safety_set_event_sink(evlog_put_safety, NULL)`。
|
||
⚠️ 时间戳用的是**事件里带的那个**(判据记的是断电那一刻),不是现在。 */
|
||
void evlog_put_safety(void *ctx, const safety_event_t *ev);
|
||
|
||
/* `safety_ev_t` → `log_ev_t`。落盘格式与判据的枚举是两件事,见 log_ev_t 上的警告。
|
||
认不出来的返回 LOG_EV_NONE(不会发生;发生了也不该把这条记录扔掉)。 */
|
||
log_ev_t evlog_from_safety(uint8_t safety_ev);
|
||
|
||
/* 该落盘了吗:攒够 `EVLOG_BATCH` 条,或者最老的一条等了 `EVLOG_MAX_AGE_MS`。
|
||
t_log 每一圈问一次,false 就接着睡。 */
|
||
bool evlog_due(void);
|
||
|
||
/* 把 RAM 里的写进 flash。max = 0 表示「现在有多少写多少」(上界就是缓冲的
|
||
256 条);t_log 每圈传的是 `EVLOG_FLUSH_MAX`。返回**真的写进 flash 几条**
|
||
—— 含一条补记的 LOST,所以它可能比预算多 1(那条是上次欠下的账)。
|
||
只有 t_log 与 A12 的拉日志命令调它 —— 它是**唯一**碰 flash 的地方。 */
|
||
uint32_t evlog_flush(uint32_t max);
|
||
|
||
/* 从 from_seq 起,按序号读回**已落盘**的记录(没落盘的先 flush,见文件头)。
|
||
返回真的读到几条。from_seq 比 `oldest_seq` 小时从 `oldest_seq` 开始
|
||
—— 上位机据此就能发现「中间那一段已经被环形缓冲盖掉了」。 */
|
||
uint32_t evlog_read(uint32_t from_seq, evlog_rec_t *out, uint32_t max);
|
||
|
||
void evlog_stats(evlog_stats_t *st);
|
||
|
||
/* flash 里最多能留几条(§10.7 要求 ≥4096)。没有分区时返回 0。 */
|
||
uint32_t evlog_capacity(void);
|
||
|
||
/* 只用于日志与测试失败信息,返回静态字符串,不会是 NULL。 */
|
||
const char *evlog_ev_str(log_ev_t ev);
|
||
|
||
#ifdef __cplusplus
|
||
}
|
||
#endif
|
||
|
||
#endif /* EVLOG_H */
|