/* A11:事件日志(需求 §10.7)。 验收是两句话:**「写满 4096 条不丢最新」**与**「掉电重启后能读回」**。 这两条各有一个「测试写法不对就永远绿」的坑,先写在这里: · 「不丢最新」不是「写 4096 条能读回 4096 条」。要写**超过**容量, 然后断言读回来的是**最新的那些**。写不满的话,环形缓冲那一段代码 一行都没被跑到。 · 「掉电重启」不能用 `sim_hal_reset()` —— 那是「换了一块新板子」, flash 一起擦掉了,于是测的是「刚擦干净的 flash 读不出东西」,永远绿。 重启用 `sim_hal_reboot()`:CPU 侧全清、flash 一个字节不动。 还有一类只有在这里才测得到的东西:**掉电可以把一次写切成两半**。 `sim_store_tear_next_write()` 造的就是它,而它正是每条记录带一个校验字节的理由。 */ #include #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); /* ⭐ R2 新增的三条(计划 §5.8 / §5.9)。追加在末尾,前面 13 个一个都没动 —— 所以 R1 那块板子上跑了半年的 flash 拿到 R2 固件上仍然读得对。 ⚠️ 这三行不是「补全」,它们是钉子:谁要是图省事把 MUX_LOST 插到 OVER_VOLT 前面("安全事件排一起好看"),历史记录里的 3 就变成了 mux 失联, 而读日志的工具、板子、测试全都不会报一个字。 */ TEST_ASSERT_EQUAL_INT(14, LOG_EV_MUX_LOST); TEST_ASSERT_EQUAL_INT(15, LOG_EV_SLOT_EMPTY); TEST_ASSERT_EQUAL_INT(16, LOG_EV_PD_LIMIT); TEST_ASSERT_EQUAL_INT(17, LOG_EV_PG_TIMEOUT); /* A15 */ TEST_ASSERT_EQUAL_INT(18, LOG_EV_OTA); /* C2 */ TEST_ASSERT_EQUAL_INT(19, LOG_EV_BOOT_CHECK); /* 0.1.2 开机校验 */ TEST_ASSERT_EQUAL_INT(20, LOG_EV_BLADE_MUTE); /* 0.1.4 刀片不应答 */ TEST_ASSERT_EQUAL_INT(21, LOG_EV_COUNT); } /* §10.7 的「必记事件」有八类,A10 只产生其中两类。剩下的号先占住, 免得 A12/A15/A16 各自往中间插一个,把 flash 里的旧记录挪了位。 ⭐ 这条用例的上界是 `LOG_EV_COUNT`,所以 R2 追加的 14/15/16 **自动**被它盖住: 往枚举里加一条却忘了在 `evlog_ev_str()` 里加一行,这里立刻红。 (`evlog_ev_str()` 的 switch 没有 `default`,`-Wswitch` 本来也会拦一道; 但只要有人手贱补一个 `default: return "?"`,编译器那一道就没了 —— 这条还在。) */ static void test_every_event_type_has_a_name(void) { const char *names[LOG_EV_COUNT]; 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] != '?', "有事件类型没给名字"); names[i] = s; } TEST_ASSERT_EQUAL_STRING("?", evlog_ev_str((log_ev_t)LOG_EV_COUNT)); /* 名字还得**两两不同**。加新类型时最省事的写法是复制上一行的 `case` —— 复制完忘了改字符串,上面那个循环一个字都不会说。真板上的现象是 日志里两种完全不同的事件打出同一行字(比如「刀片不在位」显示成 「mux失联全断」),排查的人会照着那行字去查错的那条链。 */ for (int i = 0; i < LOG_EV_COUNT; i++) { for (int j = i + 1; j < LOG_EV_COUNT; j++) { TEST_ASSERT_FALSE_MESSAGE(strcmp(names[i], names[j]) == 0, "两个事件类型共用了同一个名字"); } } } /* `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_BLADE_MUTE, evlog_from_safety(SAFETY_EV_BLADE_MUTE)); 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); } /* ⭐ R2:把「显式 switch」这件事**用具体数字**钉死。 上面那条用例全用符号写(`LOG_EV_REVERSE`),它证明得了「对应关系没接错」, 证明不了「这个函数不是一个偏移」—— 因为把 switch 换成 `return (log_ev_t)(LOG_EV_OVER_VOLT + safety_ev - 1)` 之后, R1 那四条(1→3 2→4 3→5 4→6)的符号断言**全部照样通过**。 R2 追加的两条恰好是这个写法的照妖镜:`safety_ev_t` 里它们排在 5/6, 而 `log_ev_t` 里它们是 14/15(前面还隔着 A12/A15/A16 占的七个号)。 偏移写法会把它们算成 7 和 8 —— 也就是 `LOG_EV_EFUSE_FAULT` 与 `LOG_EV_PG_CHANGE`。 **这个错在功能测试里完全看不见**:日志照写、条数不少、CRC 也对, 只有事后读日志的人会看到「五路全断」那一刻记的是「eFuse故障」。 所以下面每一对都写成裸数字:两个枚举里任何一边动了号,这条都红。 */ static void test_the_safety_mapping_is_a_table_not_an_offset(void) { /* 先把两边的号本身钉住 —— 不然「偏移对上了」可能是因为有人挪了 safety 那边。 */ TEST_ASSERT_EQUAL_INT(0, SAFETY_EV_NONE); TEST_ASSERT_EQUAL_INT(1, SAFETY_EV_OVER_VOLT); TEST_ASSERT_EQUAL_INT(2, SAFETY_EV_UNDER_VOLT); TEST_ASSERT_EQUAL_INT(3, SAFETY_EV_REVERSE); TEST_ASSERT_EQUAL_INT(4, SAFETY_EV_PUSHED_UP); TEST_ASSERT_EQUAL_INT(5, SAFETY_EV_MUX_LOST); TEST_ASSERT_EQUAL_INT(6, SAFETY_EV_SLOT_EMPTY); TEST_ASSERT_EQUAL_INT(7, SAFETY_EV_BLADE_MUTE); /* 逐对:safety 号 → flash 号,两边都是裸数字。 */ TEST_ASSERT_EQUAL_INT(0, (int)evlog_from_safety(0)); /* NONE */ TEST_ASSERT_EQUAL_INT(3, (int)evlog_from_safety(1)); /* OVER_VOLT */ TEST_ASSERT_EQUAL_INT(4, (int)evlog_from_safety(2)); /* UNDER_VOLT */ TEST_ASSERT_EQUAL_INT(5, (int)evlog_from_safety(3)); /* REVERSE */ TEST_ASSERT_EQUAL_INT(6, (int)evlog_from_safety(4)); /* PUSHED_UP */ TEST_ASSERT_EQUAL_INT(14, (int)evlog_from_safety(5)); /* MUX_LOST */ TEST_ASSERT_EQUAL_INT(15, (int)evlog_from_safety(6)); /* SLOT_EMPTY */ TEST_ASSERT_EQUAL_INT(20, (int)evlog_from_safety(7)); /* BLADE_MUTE(0.1.4) */ /* 而且这张表**在数学上不可能**是一个偏移:前四对差 2,后两对差 9。 写死这一条,是为了让「改成偏移」这件事在用例名上就说得清楚, 而不是靠上面某一行恰好不满足。 */ TEST_ASSERT_EQUAL_INT(2, (int)evlog_from_safety(1) - (int)SAFETY_EV_OVER_VOLT); TEST_ASSERT_EQUAL_INT(9, (int)evlog_from_safety(5) - (int)SAFETY_EV_MUX_LOST); /* 越过表尾的号一律 NONE。偏移写法在这里会算出一个**越界但合法**的 `log_ev_t`(8 → 记成 PG 变化,200 → 记成 202 这种根本没有的号)。 */ TEST_ASSERT_EQUAL_INT(0, (int)evlog_from_safety(8)); TEST_ASSERT_EQUAL_INT(0, (int)evlog_from_safety(200)); TEST_ASSERT_EQUAL_INT(0, (int)evlog_from_safety(255)); } /* ⭐ R2 的两条新判据要真的落得了盘、读得回来。 这里刻意走**完整的一条链**:`safety_ev_fn` 回调 → RAM 环 → flash → 重启 → 读回。 只断言 `evlog_from_safety()` 的返回值是不够的 —— `evlog_put_safety()` 里 `r.rail = ev->rail` 这一行同样能出错,而 mux 失联是**整板级事件(rail = 0)**, 正好落在「路号是 1~5」这个到处都在假设的前提之外: 谁要是在入队或读回的路上顺手做一次 `rail - 1` 当下标、或者把 0 「修正」成 1, 日志里五路全断那一刻就记成了「CH1 出事」,而板子、条数、CRC 全都正常。 */ static void test_the_r2_events_round_trip_through_flash(void) { TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init()); const safety_ev_fn sink = evlog_put_safety; /* 按 A10 的出口类型接 */ t_safety_set_event_sink(sink, NULL); /* mux 失联:**整板级**,rail = 0,五路全断那一刻的输入侧读数。 */ const safety_event_t mux = { .t_ms = 50000, .rail = 0, .type = SAFETY_EV_MUX_LOST, .bus_mv = 12050, .current_ua = 4300000, }; /* 刀片不在位:**单路**,rail = 5(那一路正好是 PRSNT# 不顺排的 IO7)。 */ const safety_event_t empty = { .t_ms = 50007, .rail = 5, .type = SAFETY_EV_SLOT_EMPTY, .bus_mv = 0, .current_ua = 0, }; sim_time_advance_ms(88888); /* 记账在断电之后:时间戳不许换成「现在」 */ sink(NULL, &mux); sink(NULL, &empty); TEST_ASSERT_EQUAL_UINT32(3, evlog_flush(0)); /* BOOT + 这两条 */ /* 断电重启:这两条必须是从 **flash** 里读回来的,不是还留在 RAM 里。 */ sim_hal_reboot(); TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init()); TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, sim_store_erases(), "重启把日志擦了"); const evlog_rec_t a = read_one(1); TEST_ASSERT_EQUAL_INT_MESSAGE(14, a.type, "mux 失联没按 14 号落盘"); TEST_ASSERT_EQUAL_UINT8_MESSAGE(0, a.rail, "整板级事件被安到了某一路头上"); TEST_ASSERT_EQUAL_UINT32(50000, a.t_ms); TEST_ASSERT_EQUAL_INT32(12050, a.bus_mv); TEST_ASSERT_EQUAL_INT32(4300000, a.current_ua); const evlog_rec_t b = read_one(2); TEST_ASSERT_EQUAL_INT_MESSAGE(15, b.type, "刀片不在位没按 15 号落盘"); TEST_ASSERT_EQUAL_UINT8(5, b.rail); TEST_ASSERT_EQUAL_UINT32(50007, b.t_ms); /* 读日志的人看到的是名字,不是数字 —— 顺带确认这两条打得出人话来。 */ TEST_ASSERT_EQUAL_STRING("mux失联全断", evlog_ev_str((log_ev_t)a.type)); TEST_ASSERT_EQUAL_STRING("刀片不在位", evlog_ev_str((log_ev_t)b.type)); } /* 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_safety_mapping_is_a_table_not_an_offset); RUN_TEST(test_the_r2_events_round_trip_through_flash); 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(); }