/* 事件日志(需求 §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 #include #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) */ /* A15 新增(需求 §10.9 第 6 步「等各路 PG,超时则记事件并保持该路关闭」)。 与 `PG_CHANGE`(8)不是一回事:那是「PG 变了」,这是「开了电、PG 一直没来、已经关回去」。 arg:bit0 = 超时那一刻 FLT# 在报;bit1 = 这一片 PCA9555 读不到,改按 INA226 电压判的。 */ LOG_EV_PG_TIMEOUT = 17, /* C2 新增:OTA 的每一步。arg = `ota_ev_t`(ota.h);失败那一条 bus_mv = `ota_err_t`, current_ua = 镜像字节数。整板级(rail = 0)。 */ LOG_EV_OTA = 18, /* 0.1.2 新增:硬重启按电源键之后的开机校验(t_actuator.h `ACT_HR_BOOT_TRIES`)。 只在「重按过」或「最终没开起来」时记,一次就开的不记(每次硬重启都记一条太吵)。 arg:bit0~3 = 一共按了几次,bit7 = 1 最终没开起来(手机电流始终 < 100 mA)。 bus_mv = 判定时这一路电压,current_ua = 判定时电流。 频繁出现 = 这一台的电源键弹片和刀片铜盘接触不良(2026-09-28 板 B 第 1 路)。 */ LOG_EV_BOOT_CHECK = 19, /* 0.1.4 新增:整片刀片不应答(本路 INA226 + PCA9555 连续 10 圈都 NAK,eFuse 开着)⇒ 关这一路。 `t_safety.h` 的 `T_SAFETY_BLADE_MUTE_N`。bus_mv / current_ua 是上一次读到的旧值,仅供参考。 出现 = 这一槽 `PRSNT#` 假在位,或刀片的 I²C 整个断了。 */ LOG_EV_BLADE_MUTE = 20, /* 0.1.5 新增:刷机模式(chgmode.h)开 / 关 / 到期。arg = 分钟数(1~120,开), 0 = 命令关(CHG_EV_OFF),255 = 到期(CHG_EV_EXPIRED)。bus_mv / current_ua 是当时这一路的读数。 */ LOG_EV_CHG_MODE = 21, /* 0.1.14 新增:Hub 按需启停(hubctl.h)。整板级(rail = 0)。 arg:0 = 关(`HUB_RST#` 拉低)、1 = 开(放开),0x10|模式 = 改模式(0 auto / 1 on)。 bus_mv = `hub_why_t`(开的理由;关那一条是 0)。 */ LOG_EV_HUB = 22, 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); /* 同上,只是 `LOG_EV_BOOT` 那一条的 arg 由调用方给(A15:ESP 上填 `esp_reset_reason()`, §10.10「看门狗复位 … 记一条复位事件」就是这一条 —— 不另开事件号, 因为「这次上电是怎么来的」本来就是 BOOT 的属性)。`evlog_init()` = `evlog_init_ex(0)`。 */ hal_err_t evlog_init_ex(uint8_t boot_arg); /* 入队一条。**不阻塞、不碰 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 */