A11:事件日志(环形缓冲 + 落盘)
事件从 evlog_put*() 进一个 256 条的 RAM 环(临界区里两条指令,**不阻塞、 不碰 flash**),由最低优先级的 t_log 攒够 16 条或最老那条等满 1 s 时批量写进 新的 `evlog` 分区(17 扇区 × 4 KB = **4335 条**)。A10 的事件出口原样接上, 判据那一层一个字没动。 八条实现约定(详见需求文档 §10.7): · 落盘不用 NVS 也不用 SPIFFS,是一块裸 data 分区 —— NVS 的页式 GC 会把 「写一条事件」变成一次不可预测的搬移。HAL 因此多出第六样 hal_store; · 17 个扇区不是 16:每扇区要拿一个 16 B 的槽当扇区头,4 KB 只装 255 条, 16 扇区 = 4080,差 16 条不到 §10.7 要求的 4096; · 序号 = flash 里的位置(epoch × 255 + 槽号),不是自增计数器 —— 重启接着数 不用存计数器;代价是没落盘的事件还没有序号,A12 读之前要先 flush; · 入队走关中断的临界区,**不是第三把锁**:入队方是最高优先级的 t_safety, 停在一把 t_log 持着的锁上等于让最低优先级的任务决定巡检节拍; · 丢事件可以,悄悄丢不行 —— 缓冲满了盖最旧的,但补一条 LOG_EV_LOST 记着丢了几条; · 每条记录带 CRC-8(见下),写失败就放弃该扇区剩余部分,被放弃的那段在序号 空间里是个洞,读回时给一条空记录占位(跳过的话上位机游标会卡住); · 每次上电记一条 LOG_EV_BOOT:时间戳上电从 0 计,没有它两次上电的记录会穿插。 事件类型的数字是 flash 格式的一部分,只许往末尾加;safety_ev_t → log_ev_t 是显式 switch 不是加偏移。§10.7 里还没人产生的六类号已占住(A12/A15/A16 去填)。 27 个用例;23 条注错逐个跑过,21 条变红。第一版校验和用异或,漏掉的正是最常见 的那条:payload 全 0 的记录被掉电切在第 8 个字节上时,后 8 个字节留在 0xFF, 而 8 个 0xFF 的异或恰好是 0 —— 半条的校验和与整条一模一样,扫描把半条当成 一条真事件。换成 CRC-8 之后拦住。另外两个只在这里才踩得到的坑:「掉电重启」 不能用 sim_hal_reset()(那是换新板子,flash 一起擦,测试永远绿),为此加了 sim_hal_reboot();「读到洞就跳过」这个错被测试里 static 数组的零初始化掩盖过 一次,缓冲要先填成非零。 Co-Authored-By: Claude Opus 5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,650 @@
|
||||
/* A11:事件日志(需求 §10.7)。
|
||||
|
||||
验收是两句话:**「写满 4096 条不丢最新」**与**「掉电重启后能读回」**。
|
||||
这两条各有一个「测试写法不对就永远绿」的坑,先写在这里:
|
||||
|
||||
· 「不丢最新」不是「写 4096 条能读回 4096 条」。要写**超过**容量,
|
||||
然后断言读回来的是**最新的那些**。写不满的话,环形缓冲那一段代码
|
||||
一行都没被跑到。
|
||||
· 「掉电重启」不能用 `sim_hal_reset()` —— 那是「换了一块新板子」,
|
||||
flash 一起擦掉了,于是测的是「刚擦干净的 flash 读不出东西」,永远绿。
|
||||
重启用 `sim_hal_reboot()`:CPU 侧全清、flash 一个字节不动。
|
||||
|
||||
还有一类只有在这里才测得到的东西:**掉电可以把一次写切成两半**。
|
||||
`sim_store_tear_next_write()` 造的就是它,而它正是每条记录带一个校验字节的理由。 */
|
||||
#include <string.h>
|
||||
|
||||
#include "evlog.h"
|
||||
#include "hal_store.h"
|
||||
#include "hal_time.h"
|
||||
#include "sim_hal.h"
|
||||
#include "t_safety.h"
|
||||
#include "unity.h"
|
||||
|
||||
void setUp(void) { sim_hal_reset(); }
|
||||
|
||||
/* 全局不变量,每个用例结束都查一遍:临界区进出配对,而且里面没干过会阻塞的事。
|
||||
这两条错了不会让任何一个断言变红,只会让板子偶尔抖一下(hal_lock.h)。 */
|
||||
void tearDown(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, sim_crit_depth(), "临界区没配对");
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, sim_crit_blocking(), "在临界区里阻塞了");
|
||||
}
|
||||
|
||||
static void fill(uint32_t n)
|
||||
{
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
evlog_put(1, LOG_EV_PG_CHANGE, (uint8_t)i, (int32_t)i, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t next_seq(void)
|
||||
{
|
||||
evlog_stats_t st;
|
||||
evlog_stats(&st);
|
||||
return st.next_seq;
|
||||
}
|
||||
|
||||
static evlog_rec_t read_one(uint32_t seq)
|
||||
{
|
||||
evlog_rec_t r;
|
||||
memset(&r, 0, sizeof(r));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, evlog_read(seq, &r, 1));
|
||||
return r;
|
||||
}
|
||||
|
||||
/* ---- flash 格式:改了就是把历史记录重新解释了一遍 ------------------------ */
|
||||
|
||||
/* 事件类型的数字**存在 flash 里**。改一个值,板子上跑了半年的日志全部变了意思,
|
||||
而且读出来仍然是合法记录,没有一处会报错。所以这里把每一个数字都钉死;
|
||||
要加新类型只能往末尾追加。 */
|
||||
static void test_flash_format_is_frozen(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_UINT32(16, (uint32_t)sizeof(evlog_rec_t)); /* §10.7「单条 16 B」*/
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, LOG_EV_NONE);
|
||||
TEST_ASSERT_EQUAL_INT(1, LOG_EV_BOOT);
|
||||
TEST_ASSERT_EQUAL_INT(2, LOG_EV_LOST);
|
||||
TEST_ASSERT_EQUAL_INT(3, LOG_EV_OVER_VOLT);
|
||||
TEST_ASSERT_EQUAL_INT(4, LOG_EV_UNDER_VOLT);
|
||||
TEST_ASSERT_EQUAL_INT(5, LOG_EV_REVERSE);
|
||||
TEST_ASSERT_EQUAL_INT(6, LOG_EV_PUSHED_UP);
|
||||
TEST_ASSERT_EQUAL_INT(7, LOG_EV_EFUSE_FAULT);
|
||||
TEST_ASSERT_EQUAL_INT(8, LOG_EV_PG_CHANGE);
|
||||
TEST_ASSERT_EQUAL_INT(9, LOG_EV_I2C_LOST);
|
||||
TEST_ASSERT_EQUAL_INT(10, LOG_EV_THERMAL_DERATE);
|
||||
TEST_ASSERT_EQUAL_INT(11, LOG_EV_FAN_STALL);
|
||||
TEST_ASSERT_EQUAL_INT(12, LOG_EV_REBOOT);
|
||||
TEST_ASSERT_EQUAL_INT(13, LOG_EV_CMD_REJECT);
|
||||
TEST_ASSERT_EQUAL_INT(14, LOG_EV_COUNT);
|
||||
}
|
||||
|
||||
/* §10.7 的「必记事件」有八类,A10 只产生其中两类。剩下的号先占住,
|
||||
免得 A12/A15/A16 各自往中间插一个,把 flash 里的旧记录挪了位。 */
|
||||
static void test_every_event_type_has_a_name(void)
|
||||
{
|
||||
for (int i = 0; i < LOG_EV_COUNT; i++) {
|
||||
const char *s = evlog_ev_str((log_ev_t)i);
|
||||
TEST_ASSERT_NOT_NULL(s);
|
||||
TEST_ASSERT_TRUE_MESSAGE(s[0] != '?', "有事件类型没给名字");
|
||||
}
|
||||
TEST_ASSERT_EQUAL_STRING("?", evlog_ev_str((log_ev_t)LOG_EV_COUNT));
|
||||
}
|
||||
|
||||
/* `safety_ev_t` 与 `log_ev_t` 是两套编号,中间是一个**显式的 switch**。
|
||||
写成「加个偏移」的话,t_safety.h 里插一个新事件类型就会把 flash 里
|
||||
所有旧记录的含义整体挪位 —— 而且不会有任何一处报错。 */
|
||||
static void test_the_safety_mapping_is_explicit(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_OVER_VOLT, evlog_from_safety(SAFETY_EV_OVER_VOLT));
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_UNDER_VOLT, evlog_from_safety(SAFETY_EV_UNDER_VOLT));
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_REVERSE, evlog_from_safety(SAFETY_EV_REVERSE));
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_PUSHED_UP, evlog_from_safety(SAFETY_EV_PUSHED_UP));
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_NONE, evlog_from_safety(SAFETY_EV_NONE));
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_NONE, evlog_from_safety(200)); /* 认不出的不许瞎猜 */
|
||||
|
||||
/* 两套编号**不重合**:重合的话「忘了转换」这个错测不出来。 */
|
||||
TEST_ASSERT_NOT_EQUAL((int)SAFETY_EV_OVER_VOLT, (int)LOG_EV_OVER_VOLT);
|
||||
TEST_ASSERT_NOT_EQUAL((int)SAFETY_EV_PUSHED_UP, (int)LOG_EV_PUSHED_UP);
|
||||
}
|
||||
|
||||
/* A10 的事件出口是 `safety_ev_fn`。A11 要能**原样**接上去(HANDOFF 里
|
||||
「只接 sink,不动判据」那条),所以这里就按那个函数指针类型调一次。 */
|
||||
static void test_the_sink_plugs_into_the_safety_layer_unchanged(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
const safety_ev_fn sink = evlog_put_safety;
|
||||
t_safety_set_event_sink(sink, NULL);
|
||||
|
||||
const safety_event_t ev = {
|
||||
.t_ms = 1234, .rail = 3, .type = SAFETY_EV_REVERSE,
|
||||
.bus_mv = 3902, .current_ua = -61000,
|
||||
};
|
||||
sim_time_advance_ms(9999); /* 记账发生在断电之后 */
|
||||
sink(NULL, &ev);
|
||||
TEST_ASSERT_EQUAL_UINT32(2, evlog_flush(0));
|
||||
|
||||
const evlog_rec_t r = read_one(1);
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_REVERSE, r.type);
|
||||
TEST_ASSERT_EQUAL_UINT8(3, r.rail);
|
||||
TEST_ASSERT_EQUAL_INT32(3902, r.bus_mv);
|
||||
TEST_ASSERT_EQUAL_INT32(-61000, r.current_ua);
|
||||
/* ⚠️ 时间戳是**断电那一刻**的,不是入队那一刻的。 */
|
||||
TEST_ASSERT_EQUAL_UINT32(1234, r.t_ms);
|
||||
}
|
||||
|
||||
/* ---- 容量:§10.7 的 4096 条 ---------------------------------------------- */
|
||||
|
||||
/* 每个扇区要拿一个 16 B 的槽当扇区头,于是 4 KB 的扇区只装得下 255 条。
|
||||
16 个扇区 = 4080,**差 16 条不到 4096** —— 分区因此是 17 个扇区。
|
||||
这条断言同时把 partitions.csv 与 sim 的几何钉在一起。 */
|
||||
static void test_capacity_meets_the_requirement(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
const uint32_t per_sector = SIM_STORE_SECTOR_BYTES / sizeof(evlog_rec_t) - 1u;
|
||||
TEST_ASSERT_EQUAL_UINT32(255, per_sector);
|
||||
TEST_ASSERT_EQUAL_UINT32(SIM_STORE_SECTORS * per_sector, evlog_capacity());
|
||||
TEST_ASSERT_TRUE_MESSAGE(evlog_capacity() >= 4096u, "装不下 §10.7 要求的 4096 条");
|
||||
}
|
||||
|
||||
/* ---- 入队:不阻塞、不碰 flash ------------------------------------------- */
|
||||
|
||||
/* t_safety 是在**断电之后**调入队的(t_safety.h),所以它一条 flash 事务都不许发。
|
||||
200 条事件进来,写次数必须一次都不涨。 */
|
||||
static void test_putting_events_never_touches_flash(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
TEST_ASSERT_EQUAL_UINT32(1, evlog_flush(0));
|
||||
|
||||
const uint32_t w0 = sim_store_writes();
|
||||
const uint32_t e0 = sim_store_erases();
|
||||
const uint32_t c0 = sim_crit_enters();
|
||||
fill(200);
|
||||
TEST_ASSERT_EQUAL_UINT32(w0, sim_store_writes());
|
||||
TEST_ASSERT_EQUAL_UINT32(e0, sim_store_erases());
|
||||
/* 而且每一条都是在临界区里进的环 —— 两个下标不这样改是会撕的。 */
|
||||
TEST_ASSERT_TRUE(sim_crit_enters() - c0 >= 200u);
|
||||
|
||||
evlog_stats_t st;
|
||||
evlog_stats(&st);
|
||||
TEST_ASSERT_EQUAL_UINT32(200, st.pending);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, st.dropped);
|
||||
}
|
||||
|
||||
/* 缓冲满了盖掉的是**最旧**的一条。反过来(拒收新的)在真板上的现象是:
|
||||
出事的那一串事件里,最有价值的最后几条恰好一条都没有。 */
|
||||
static void test_the_ring_overwrites_the_oldest_not_the_newest(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init()); /* 这一条 BOOT 也占一格 */
|
||||
fill(EVLOG_RAM_CAP + 10u); /* 一共 267 条进 256 格 */
|
||||
|
||||
evlog_stats_t st;
|
||||
evlog_stats(&st);
|
||||
TEST_ASSERT_EQUAL_UINT32(11, st.dropped);
|
||||
TEST_ASSERT_EQUAL_UINT32(EVLOG_RAM_CAP, st.pending);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(EVLOG_RAM_CAP + 1u, evlog_flush(0)); /* +1 = 那条 LOST */
|
||||
const evlog_rec_t last = read_one(next_seq() - 1u);
|
||||
TEST_ASSERT_EQUAL_INT32((int32_t)(EVLOG_RAM_CAP + 9u), last.bus_mv); /* 最后一条在 */
|
||||
}
|
||||
|
||||
/* 丢事件可以,**悄悄丢**不行:一份会安静少掉几行的日志,比没有日志更糟 ——
|
||||
排查的人会拿它当完整的时间线看。丢掉的那些在日志里留下一条 LOST。 */
|
||||
static void test_dropped_events_leave_a_mark_in_the_log(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
fill(EVLOG_RAM_CAP + 10u);
|
||||
(void)evlog_flush(0);
|
||||
|
||||
const evlog_rec_t first = read_one(0);
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_LOST, first.type);
|
||||
TEST_ASSERT_EQUAL_INT32(11, first.bus_mv); /* 丢了几条 */
|
||||
}
|
||||
|
||||
/* ---- 攒批:§10.7「t_log 空闲时批量写」 ---------------------------------- */
|
||||
|
||||
/* 一条一次的话,flash 的擦写寿命按事件条数算;攒 16 条一批,摊薄到 1/16。 */
|
||||
static void test_flush_is_batched_not_one_write_per_event(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
(void)evlog_flush(0);
|
||||
|
||||
const uint32_t w0 = sim_store_writes();
|
||||
fill(EVLOG_BATCH);
|
||||
TEST_ASSERT_EQUAL_UINT32(EVLOG_BATCH, evlog_flush(0));
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, sim_store_writes() - w0, "没攒批,一条写一次");
|
||||
}
|
||||
|
||||
/* 攒批不能把「掉电前记下来」攒没了:最老的一条最多等 EVLOG_MAX_AGE_MS。 */
|
||||
static void test_the_oldest_event_never_waits_longer_than_the_window(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
(void)evlog_flush(0);
|
||||
|
||||
fill(1);
|
||||
TEST_ASSERT_FALSE(evlog_due()); /* 一条,刚进来 */
|
||||
sim_time_advance_ms(EVLOG_MAX_AGE_MS - 1u);
|
||||
TEST_ASSERT_FALSE(evlog_due());
|
||||
sim_time_advance_ms(1);
|
||||
TEST_ASSERT_TRUE(evlog_due()); /* 等够了:一条也要写 */
|
||||
|
||||
(void)evlog_flush(0);
|
||||
fill(EVLOG_BATCH - 1u);
|
||||
TEST_ASSERT_FALSE(evlog_due()); /* 差一条不够一批,而且没等够 */
|
||||
fill(1);
|
||||
TEST_ASSERT_TRUE(evlog_due()); /* 攒够一批:不等了 */
|
||||
|
||||
/* ⚠️ 计时的起点是**最老**那一条,不是最新那一条。写成后者的话,
|
||||
事件慢慢地一条一条来(真板上就是这个样子)时窗口每次都被重置,
|
||||
于是永远不 due —— 现象是「日志一直不落盘」,而没有任何一条断言会红。 */
|
||||
(void)evlog_flush(0);
|
||||
fill(1);
|
||||
sim_time_advance_ms(600);
|
||||
fill(1);
|
||||
sim_time_advance_ms(600); /* 最老那条已经等了 1200 ms */
|
||||
TEST_ASSERT_TRUE_MESSAGE(evlog_due(), "攒批的计时起点取成了最新那一条");
|
||||
}
|
||||
|
||||
/* `evlog_flush(max)` 要守住预算 —— A12 的拉日志命令是同步调它的,
|
||||
一次写几千条会把命令处理器变成一个几百毫秒的黑洞。 */
|
||||
static void test_flush_respects_its_budget(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
(void)evlog_flush(0);
|
||||
|
||||
fill(50);
|
||||
TEST_ASSERT_EQUAL_UINT32(10, evlog_flush(10));
|
||||
evlog_stats_t st;
|
||||
evlog_stats(&st);
|
||||
TEST_ASSERT_EQUAL_UINT32(40, st.pending);
|
||||
TEST_ASSERT_EQUAL_UINT32(40, evlog_flush(0));
|
||||
}
|
||||
|
||||
/* ---- 验收一:写满 4096 条不丢最新 --------------------------------------- */
|
||||
|
||||
static void test_writing_past_the_capacity_keeps_the_newest(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
|
||||
const uint32_t total = 5000; /* 比 4335 条的容量多出一截,环一定绕过 */
|
||||
for (uint32_t i = 0; i < total; i++) {
|
||||
evlog_put(1, LOG_EV_PG_CHANGE, 0, (int32_t)i, 0);
|
||||
if (evlog_due()) {
|
||||
(void)evlog_flush(EVLOG_FLUSH_MAX);
|
||||
}
|
||||
}
|
||||
(void)evlog_flush(0);
|
||||
|
||||
evlog_stats_t st;
|
||||
evlog_stats(&st);
|
||||
/* ① 一条都没在 RAM 里丢(t_log 跟得上)。 */
|
||||
TEST_ASSERT_EQUAL_UINT32(0, st.dropped);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, st.io_errors);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, st.pending);
|
||||
/* ② 序号一直往前数,flash 里留着的是最后那一段。 */
|
||||
TEST_ASSERT_EQUAL_UINT32(total + 1u, st.next_seq); /* +1 = 开机那条 BOOT */
|
||||
TEST_ASSERT_TRUE_MESSAGE(st.oldest_seq > 0, "没绕过,这条用例没测到环形");
|
||||
TEST_ASSERT_TRUE(st.next_seq - st.oldest_seq <= evlog_capacity());
|
||||
|
||||
/* ③ **最新的五条一条不少**,而且就是最后写进去的那五条。 */
|
||||
evlog_rec_t buf[5];
|
||||
TEST_ASSERT_EQUAL_UINT32(5, evlog_read(st.next_seq - 5u, buf, 5));
|
||||
for (uint32_t i = 0; i < 5; i++) {
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_PG_CHANGE, buf[i].type);
|
||||
TEST_ASSERT_EQUAL_INT32((int32_t)(total - 5u + i), buf[i].bus_mv);
|
||||
}
|
||||
}
|
||||
|
||||
/* 擦除次数要按「条数 / 每扇区 255」走,不是按条数走 —— 后者的现象是
|
||||
flash 一年就写穿了,而在测试里只是「跑得慢一点」。 */
|
||||
static void test_erase_count_stays_proportional_to_the_log(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
const uint32_t after_format = sim_store_erases(); /* 出厂格式化擦了整块 */
|
||||
TEST_ASSERT_EQUAL_UINT32(SIM_STORE_SECTORS, after_format);
|
||||
|
||||
for (uint32_t i = 0; i < 5000; i++) {
|
||||
evlog_put(1, LOG_EV_PG_CHANGE, 0, (int32_t)i, 0);
|
||||
if (evlog_due()) {
|
||||
(void)evlog_flush(EVLOG_FLUSH_MAX);
|
||||
}
|
||||
}
|
||||
(void)evlog_flush(0);
|
||||
const uint32_t rolled = sim_store_erases() - after_format;
|
||||
TEST_ASSERT_TRUE_MESSAGE(rolled <= 5001u / 255u + 2u, "擦得太勤,是不是没按扇区攒");
|
||||
TEST_ASSERT_TRUE(rolled >= 5001u / 255u);
|
||||
}
|
||||
|
||||
/* 上位机拿一个早就被盖掉的序号来拉日志时,从**还留着的最老一条**给起 ——
|
||||
它对着 `oldest_seq` 一比就知道中间断了一段(而不是以为自己拉全了)。 */
|
||||
static void test_reading_before_the_oldest_starts_at_the_oldest(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
for (uint32_t i = 0; i < 5000; i++) {
|
||||
evlog_put(1, LOG_EV_PG_CHANGE, 0, (int32_t)i, 0);
|
||||
if (evlog_due()) {
|
||||
(void)evlog_flush(EVLOG_FLUSH_MAX);
|
||||
}
|
||||
}
|
||||
(void)evlog_flush(0);
|
||||
|
||||
evlog_stats_t st;
|
||||
evlog_stats(&st);
|
||||
evlog_rec_t a, b;
|
||||
TEST_ASSERT_EQUAL_UINT32(1, evlog_read(0, &a, 1));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, evlog_read(st.oldest_seq, &b, 1));
|
||||
TEST_ASSERT_EQUAL_INT32(b.bus_mv, a.bus_mv);
|
||||
TEST_ASSERT_EQUAL_UINT32(b.t_ms, a.t_ms);
|
||||
}
|
||||
|
||||
static void test_reading_past_the_end_returns_nothing(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
fill(3);
|
||||
(void)evlog_flush(0);
|
||||
|
||||
evlog_rec_t buf[8];
|
||||
TEST_ASSERT_EQUAL_UINT32(0, evlog_read(next_seq(), buf, 8));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, evlog_read(next_seq() + 100u, buf, 8));
|
||||
TEST_ASSERT_EQUAL_UINT32(4, evlog_read(0, buf, 8)); /* BOOT + 3 条 */
|
||||
}
|
||||
|
||||
/* ---- 验收二:掉电重启后能读回 ------------------------------------------- */
|
||||
|
||||
static void test_a_new_board_formats_itself(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
evlog_stats_t st;
|
||||
evlog_stats(&st);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, st.next_seq);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, st.oldest_seq);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, st.pending); /* 开机那条 BOOT 已经在队里 */
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(1, evlog_flush(0));
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_BOOT, read_one(0).type);
|
||||
}
|
||||
|
||||
static void test_a_reboot_reads_back_what_was_written(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
sim_time_advance_ms(4321);
|
||||
fill(20);
|
||||
TEST_ASSERT_EQUAL_UINT32(21, evlog_flush(0));
|
||||
const uint32_t before = next_seq();
|
||||
|
||||
sim_hal_reboot(); /* ⚠️ 不是 sim_hal_reset():flash 里的内容要留着 */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
|
||||
/* 没有重新格式化 —— 扫描认错了扇区的现象就是「日志每次重启都空了」。 */
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, sim_store_erases(), "重启把日志擦了");
|
||||
TEST_ASSERT_EQUAL_UINT32(before, next_seq());
|
||||
|
||||
evlog_rec_t buf[21];
|
||||
TEST_ASSERT_EQUAL_UINT32(21, evlog_read(0, buf, 21));
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_BOOT, buf[0].type);
|
||||
for (uint32_t i = 0; i < 20; i++) {
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_PG_CHANGE, buf[i + 1u].type);
|
||||
TEST_ASSERT_EQUAL_INT32((int32_t)i, buf[i + 1u].bus_mv);
|
||||
TEST_ASSERT_EQUAL_UINT32(4321, buf[i + 1u].t_ms);
|
||||
}
|
||||
|
||||
/* 重启之后接着写的记录排在旧的后面,序号不倒退。 */
|
||||
TEST_ASSERT_EQUAL_UINT32(1, evlog_flush(0));
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_BOOT, read_one(before).type);
|
||||
}
|
||||
|
||||
/* §10.7「上电从 0 计」:重启之后时间戳会跳回去。没有 BOOT 这条记录的话,
|
||||
两次上电的记录在时间轴上互相穿插,而日志里看不出中间重启过。 */
|
||||
static void test_the_log_survives_many_reboots_and_shows_each_one(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
fill(5);
|
||||
(void)evlog_flush(0);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
sim_hal_reboot();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
sim_time_advance_ms(100);
|
||||
fill(5);
|
||||
(void)evlog_flush(0);
|
||||
}
|
||||
|
||||
static evlog_rec_t buf[64];
|
||||
const uint32_t n = evlog_read(0, buf, 64);
|
||||
TEST_ASSERT_EQUAL_UINT32(24, n); /* 4 次上电 × (1 条 BOOT + 5 条) */
|
||||
uint32_t boots = 0;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
if (buf[i].type == LOG_EV_BOOT) {
|
||||
boots++;
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(4, boots, "分不出这是第几次上电");
|
||||
}
|
||||
|
||||
/* 绕过扇区边界之后再重启:扫描要认出 **epoch 最大**的那个扇区。
|
||||
取最小的(或者取扇区号最小的)在只写过一个扇区时看不出任何区别 ——
|
||||
这条用例存在的唯一理由就是「写超过 255 条再重启」。 */
|
||||
static void test_a_reboot_after_wrapping_finds_the_newest_sector(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
for (uint32_t i = 0; i < 700; i++) { /* 700 > 255×2:跨过两次扇区边界 */
|
||||
evlog_put(1, LOG_EV_PG_CHANGE, 0, (int32_t)i, 0);
|
||||
if (evlog_due()) {
|
||||
(void)evlog_flush(EVLOG_FLUSH_MAX);
|
||||
}
|
||||
}
|
||||
(void)evlog_flush(0);
|
||||
const uint32_t before = next_seq();
|
||||
TEST_ASSERT_EQUAL_UINT32(701, before); /* 700 + 那条 BOOT */
|
||||
|
||||
sim_hal_reboot();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(before, next_seq(), "重启后接错了扇区");
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_store_erases());
|
||||
TEST_ASSERT_EQUAL_INT32(699, read_one(before - 1u).bus_mv); /* 最后一条还在 */
|
||||
TEST_ASSERT_EQUAL_INT32(0, read_one(1).bus_mv); /* 第一条也还在 */
|
||||
|
||||
/* 接着写的落在原来那个扇区的后面,不是另起一个。 */
|
||||
TEST_ASSERT_EQUAL_UINT32(1, evlog_flush(0));
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_BOOT, read_one(before).type);
|
||||
}
|
||||
|
||||
/* 半个扇区的时候重启,要接着那半个扇区往下写,而不是换一个新扇区 ——
|
||||
后者的现象是「每重启一次就浪费 255 个槽」,日志容量按重启次数缩水。 */
|
||||
static void test_a_reboot_resumes_in_the_same_sector(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
fill(5);
|
||||
(void)evlog_flush(0);
|
||||
|
||||
sim_hal_reboot();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
TEST_ASSERT_EQUAL_UINT32(6, next_seq()); /* 6,不是 255 */
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_store_erases());
|
||||
}
|
||||
|
||||
/* ---- 掉电打断一次写 ------------------------------------------------------ */
|
||||
|
||||
/* 半条记录在字节层面跟一条真记录长得一模一样。校验字节的用处就是让
|
||||
重启扫描停在它前面,而不是把它当成有效记录继续往后写。 */
|
||||
static void test_a_torn_write_does_not_become_a_record(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
TEST_ASSERT_EQUAL_UINT32(1, evlog_flush(0)); /* BOOT 落在槽 0 */
|
||||
|
||||
evlog_put(2, LOG_EV_EFUSE_FAULT, 0, 3900, 1200);
|
||||
sim_store_tear_next_write(8); /* 掉电切在第 8 个字节上 */
|
||||
TEST_ASSERT_EQUAL_UINT32(0, evlog_flush(0));
|
||||
|
||||
sim_hal_reboot();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, next_seq(), "半条记录被当成了一条真记录");
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_BOOT, read_one(0).type);
|
||||
}
|
||||
|
||||
/* ⚠️ 这条是第一版校验(异或和)唯一漏掉的情况,也是它被换成 CRC-8 的理由:
|
||||
一条 `bus_mv` 与 `current_ua` 都是 0 的记录被切在第 8 个字节上时,
|
||||
后 8 个字节留在 0xFF,而 **8 个 0xFF 的异或恰好是 0** ——
|
||||
半条记录的异或和与整条一模一样,校验照过,扫描把它认成一条真事件。 */
|
||||
static void test_a_torn_write_with_zero_payload_is_still_rejected(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
TEST_ASSERT_EQUAL_UINT32(1, evlog_flush(0));
|
||||
|
||||
evlog_put(0, LOG_EV_FAN_STALL, 0, 0, 0); /* 后 8 个字节全是 0 */
|
||||
sim_store_tear_next_write(8);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, evlog_flush(0));
|
||||
|
||||
sim_hal_reboot();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
TEST_ASSERT_EQUAL_UINT32(1, next_seq());
|
||||
}
|
||||
|
||||
/* 写失败之后这个扇区的**剩下部分整个放弃**。接着往同一个扇区写的话,
|
||||
重启扫描会停在那半条上,把后面写进去的全部当成不存在 ——
|
||||
代价是最多浪费 254 个槽,换来的是「扫描停在哪 = 日志到哪为止」永远成立。 */
|
||||
static void test_a_torn_write_abandons_the_rest_of_the_sector(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
TEST_ASSERT_EQUAL_UINT32(1, evlog_flush(0));
|
||||
|
||||
evlog_put(2, LOG_EV_EFUSE_FAULT, 0, 3900, 0);
|
||||
sim_store_tear_next_write(8);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, evlog_flush(0));
|
||||
|
||||
evlog_stats_t st;
|
||||
evlog_stats(&st);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, st.io_errors);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, st.dropped); /* 出了环却没写进去 = 真丢了 */
|
||||
|
||||
/* 下一条走的是新扇区:序号跳到 255 的整数倍(LOST 补记占第一条)。 */
|
||||
evlog_put(2, LOG_EV_EFUSE_FAULT, 0, 3901, 0);
|
||||
TEST_ASSERT_EQUAL_UINT32(2, evlog_flush(0));
|
||||
evlog_stats(&st);
|
||||
TEST_ASSERT_EQUAL_UINT32(255u + 2u, st.next_seq);
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_LOST, read_one(255).type);
|
||||
TEST_ASSERT_EQUAL_INT32(3901, read_one(256).bus_mv);
|
||||
}
|
||||
|
||||
/* 被放弃的那一段槽在序号空间里是个洞。读到洞时给一条空记录**占住位置** ——
|
||||
跳过它的话,上位机的游标 `from_seq + n` 会对不上,拉日志会卡在洞上不动。 */
|
||||
static void test_a_hole_does_not_stall_the_cursor(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
(void)evlog_flush(0);
|
||||
evlog_put(2, LOG_EV_EFUSE_FAULT, 0, 3900, 0);
|
||||
sim_store_tear_next_write(8);
|
||||
(void)evlog_flush(0);
|
||||
evlog_put(2, LOG_EV_EFUSE_FAULT, 0, 3901, 0);
|
||||
(void)evlog_flush(0);
|
||||
|
||||
/* ⚠️ 先把缓冲填成非零:`static` 数组是零初始化的,而「读到洞就跳过」
|
||||
留下的正是零 —— 不填的话那个错看起来和「填了一条空记录」一模一样。 */
|
||||
static evlog_rec_t buf[260];
|
||||
memset(buf, 0xAA, sizeof(buf));
|
||||
const uint32_t n = evlog_read(0, buf, 260);
|
||||
TEST_ASSERT_EQUAL_UINT32(257, n); /* 序号 0~256 一个不少 */
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_BOOT, buf[0].type);
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_NONE, buf[1].type); /* 半条那一槽 */
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_NONE, buf[254].type); /* 被放弃的那一段 */
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_LOST, buf[255].type);
|
||||
TEST_ASSERT_EQUAL_INT32(3901, buf[256].bus_mv);
|
||||
}
|
||||
|
||||
/* 一次写整个失败(flash 真坏了)时,那一批事件是真丢了 —— 但同样要留下痕迹。 */
|
||||
static void test_a_failed_write_is_counted_and_marked_lost(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
(void)evlog_flush(0);
|
||||
|
||||
fill(3);
|
||||
sim_store_fail_next_write(1);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, evlog_flush(0));
|
||||
|
||||
evlog_stats_t st;
|
||||
evlog_stats(&st);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, st.io_errors);
|
||||
TEST_ASSERT_EQUAL_UINT32(3, st.dropped);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(1, evlog_flush(0)); /* 补记那条 LOST */
|
||||
const evlog_rec_t r = read_one(next_seq() - 1u);
|
||||
TEST_ASSERT_EQUAL_INT(LOG_EV_LOST, r.type);
|
||||
TEST_ASSERT_EQUAL_INT32(3, r.bus_mv);
|
||||
|
||||
/* 补完就清账。不清的话每一次落盘都再补一条,日志会被 LOST 刷屏 ——
|
||||
而「日志里全是 LOST」这件事,光看丢失计数是看不出来的。 */
|
||||
const uint32_t seq_after = next_seq();
|
||||
TEST_ASSERT_EQUAL_UINT32(0, evlog_flush(0));
|
||||
TEST_ASSERT_EQUAL_UINT32(seq_after, next_seq());
|
||||
}
|
||||
|
||||
/* ---- 假 flash 自己:它必须像 NOR,不是像 memcpy ------------------------- */
|
||||
|
||||
/* 写只能把 1 变成 0。假实现要是写成赋值,「忘了先擦」这个错在宿主机上
|
||||
一次也复现不出来,而真板上的现象是「记录读出来是两条的与」。 */
|
||||
static void test_the_store_is_nor_flash_not_memcpy(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_store_init());
|
||||
|
||||
uint32_t v = 0xF0F0F0F0u;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_store_write(0, &v, sizeof(v)));
|
||||
v = 0x0F0F0F0Fu;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_store_write(0, &v, sizeof(v)));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_store_read(0, &v, sizeof(v)));
|
||||
TEST_ASSERT_EQUAL_HEX32(0x00000000u, v); /* 按位与,不是覆盖 */
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_store_erase(0));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_store_read(0, &v, sizeof(v)));
|
||||
TEST_ASSERT_EQUAL_HEX32(0xFFFFFFFFu, v);
|
||||
}
|
||||
|
||||
static void test_the_store_refuses_what_the_flash_driver_refuses(void)
|
||||
{
|
||||
uint32_t v = 0;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, hal_store_read(0, &v, sizeof(v))); /* 没 init */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_store_init());
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hal_store_write(1, &v, sizeof(v))); /* 偏移没对齐 */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hal_store_write(0, &v, 2)); /* 长度没对齐 */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hal_store_erase(SIM_STORE_SECTORS));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG,
|
||||
hal_store_read(SIM_STORE_SECTORS * SIM_STORE_SECTOR_BYTES, &v, 4));
|
||||
|
||||
hal_store_geom_t g;
|
||||
hal_store_geometry(&g);
|
||||
TEST_ASSERT_EQUAL_UINT32(SIM_STORE_SECTOR_BYTES, g.sector_bytes);
|
||||
TEST_ASSERT_EQUAL_UINT32(SIM_STORE_SECTORS, g.sectors);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_flash_format_is_frozen);
|
||||
RUN_TEST(test_every_event_type_has_a_name);
|
||||
RUN_TEST(test_the_safety_mapping_is_explicit);
|
||||
RUN_TEST(test_the_sink_plugs_into_the_safety_layer_unchanged);
|
||||
RUN_TEST(test_capacity_meets_the_requirement);
|
||||
RUN_TEST(test_putting_events_never_touches_flash);
|
||||
RUN_TEST(test_the_ring_overwrites_the_oldest_not_the_newest);
|
||||
RUN_TEST(test_dropped_events_leave_a_mark_in_the_log);
|
||||
RUN_TEST(test_flush_is_batched_not_one_write_per_event);
|
||||
RUN_TEST(test_the_oldest_event_never_waits_longer_than_the_window);
|
||||
RUN_TEST(test_flush_respects_its_budget);
|
||||
RUN_TEST(test_writing_past_the_capacity_keeps_the_newest);
|
||||
RUN_TEST(test_erase_count_stays_proportional_to_the_log);
|
||||
RUN_TEST(test_reading_before_the_oldest_starts_at_the_oldest);
|
||||
RUN_TEST(test_reading_past_the_end_returns_nothing);
|
||||
RUN_TEST(test_a_new_board_formats_itself);
|
||||
RUN_TEST(test_a_reboot_reads_back_what_was_written);
|
||||
RUN_TEST(test_the_log_survives_many_reboots_and_shows_each_one);
|
||||
RUN_TEST(test_a_reboot_resumes_in_the_same_sector);
|
||||
RUN_TEST(test_a_reboot_after_wrapping_finds_the_newest_sector);
|
||||
RUN_TEST(test_a_torn_write_does_not_become_a_record);
|
||||
RUN_TEST(test_a_torn_write_with_zero_payload_is_still_rejected);
|
||||
RUN_TEST(test_a_torn_write_abandons_the_rest_of_the_sector);
|
||||
RUN_TEST(test_a_hole_does_not_stall_the_cursor);
|
||||
RUN_TEST(test_a_failed_write_is_counted_and_marked_lost);
|
||||
RUN_TEST(test_the_store_is_nor_flash_not_memcpy);
|
||||
RUN_TEST(test_the_store_refuses_what_the_flash_driver_refuses);
|
||||
return UNITY_END();
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户