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]>
这个提交包含在:
@@ -10,5 +10,6 @@ idf_component_register(
|
||||
"esp/hal_gpio_esp.c"
|
||||
"esp/hal_pwm_esp.c"
|
||||
"esp/hal_time_esp.c"
|
||||
"esp/hal_store_esp.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_driver_i2c esp_driver_gpio esp_driver_ledc esp_timer freertos)
|
||||
REQUIRES esp_driver_i2c esp_driver_gpio esp_driver_ledc esp_timer esp_partition freertos)
|
||||
|
||||
@@ -63,3 +63,16 @@ uint32_t hal_lock_timeouts(hal_lock_id_t id)
|
||||
{
|
||||
return (id >= 0 && id < HAL_LOCK_COUNT) ? s_timeouts[id] : 0;
|
||||
}
|
||||
|
||||
/* ---- 临界区(不是第三把锁,见 hal_lock.h) -------------------------------
|
||||
`portMUX_TYPE` + `portENTER_CRITICAL()`:单核上是关中断,多核上再加一个
|
||||
自旋锁。ESP32-S3 是双核,两个核上的任务同时入队是真会发生的,所以这里
|
||||
必须是 portMUX 而不是 `portDISABLE_INTERRUPTS()`。
|
||||
|
||||
⚠️ 关中断的时间要短到能一眼数完 —— evlog 的入队里只有一次 memcpy 和两个
|
||||
下标自增。往里面加任何一行「顺手」的活(取时间戳、算校验、写 flash)
|
||||
之前,先想清楚它会不会把 20 ms 的巡检节拍抖出去。 */
|
||||
static portMUX_TYPE s_crit = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
void hal_crit_enter(void) { portENTER_CRITICAL(&s_crit); }
|
||||
void hal_crit_exit(void) { portEXIT_CRITICAL(&s_crit); }
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/* 掉电不丢区的 ESP 实现:分区表里那块名叫 `evlog` 的 data 分区(见 partitions.csv)。
|
||||
|
||||
⚠️ **不用 NVS,也不用 SPIFFS。** §10.7 要的是一条环形的、按序号增量拉取的
|
||||
记录流,而 NVS 是键值表(4096 条记录 = 4096 个键,它的页式 GC 会把
|
||||
「写一条事件」变成一次不可预测的搬移),SPIFFS 是文件系统(掉电一致性
|
||||
自己就是一整章)。evlog 自己已经是环形缓冲,它要的只是「擦一扇区 / 写一段字节」。
|
||||
|
||||
分区找不到时返回 HAL_ERR_STATE 而不是 abort:这一条**不该**把板子挡在启动之外。
|
||||
§10.7 的日志是排查手段,不是安全链的一环;丢了日志要告警,但五路照供电。
|
||||
(对比 `rail_selftest()`:那张表错了会把命令发到另一路上去,所以那里是 abort。) */
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "esp_partition.h"
|
||||
#include "hal_store.h"
|
||||
|
||||
static const esp_partition_t *s_part;
|
||||
|
||||
hal_err_t hal_store_init(void)
|
||||
{
|
||||
if (s_part != NULL) {
|
||||
return HAL_OK;
|
||||
}
|
||||
s_part = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
|
||||
ESP_PARTITION_SUBTYPE_ANY, "evlog");
|
||||
return (s_part != NULL) ? HAL_OK : HAL_ERR_STATE;
|
||||
}
|
||||
|
||||
void hal_store_geometry(hal_store_geom_t *g)
|
||||
{
|
||||
if (g == NULL) {
|
||||
return;
|
||||
}
|
||||
if (s_part == NULL) {
|
||||
g->sector_bytes = 0;
|
||||
g->sectors = 0;
|
||||
return;
|
||||
}
|
||||
/* ⚠️ 用分区自己报的 `erase_size`,不要写死 4096:esp_partition_erase_range()
|
||||
的对齐要求就是拿这个字段说的,两处不一致时报的是 ESP_ERR_INVALID_ARG。 */
|
||||
g->sector_bytes = s_part->erase_size;
|
||||
g->sectors = s_part->size / s_part->erase_size;
|
||||
}
|
||||
|
||||
static bool range_ok(uint32_t off, uint32_t len)
|
||||
{
|
||||
return s_part != NULL && len <= s_part->size && off <= s_part->size - len;
|
||||
}
|
||||
|
||||
hal_err_t hal_store_read(uint32_t off, void *buf, uint32_t len)
|
||||
{
|
||||
if (s_part == NULL) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (buf == NULL || !range_ok(off, len)) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
return (esp_partition_read(s_part, off, buf, len) == ESP_OK) ? HAL_OK : HAL_ERR_BUS;
|
||||
}
|
||||
|
||||
hal_err_t hal_store_write(uint32_t off, const void *buf, uint32_t len)
|
||||
{
|
||||
if (s_part == NULL) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (buf == NULL || !range_ok(off, len) || (off % 4u) != 0u || (len % 4u) != 0u) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
return (esp_partition_write(s_part, off, buf, len) == ESP_OK) ? HAL_OK : HAL_ERR_BUS;
|
||||
}
|
||||
|
||||
hal_err_t hal_store_erase(uint32_t sector)
|
||||
{
|
||||
if (s_part == NULL) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
const uint32_t off = sector * s_part->erase_size;
|
||||
if (off >= s_part->size) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
return (esp_partition_erase_range(s_part, off, s_part->erase_size) == ESP_OK)
|
||||
? HAL_OK
|
||||
: HAL_ERR_BUS;
|
||||
}
|
||||
@@ -54,6 +54,27 @@ bool hal_lock_held(hal_lock_id_t id);
|
||||
/* 累计等锁失败次数(超时)。遥测里报出去,用来看总线是不是排队了。 */
|
||||
uint32_t hal_lock_timeouts(hal_lock_id_t id);
|
||||
|
||||
/* ---- 临界区(A11)——**这不是第三把锁** ----------------------------------
|
||||
|
||||
事件环形缓冲的入队要在几条指令里改完两个下标,而入队方与出队方是
|
||||
优先级两端的两个任务:`t_safety`(最高,而且是在**断电之后**调的)
|
||||
与 `t_log`(最低)。这里不能用互斥锁,理由有两条,都不是风格问题:
|
||||
|
||||
· **不许阻塞。** t_safety.h 写着「接进来的东西不许阻塞」——
|
||||
入队要是可能停在一把 t_log 持着的锁上,巡检的节拍就被最低优先级的任务
|
||||
决定了。优先级继承能救回来,但那是「让阻塞变短」,不是「不阻塞」。
|
||||
· **§10.3 的共享资源只有两个**(I²C 与 595 链),这条架构结论不该被
|
||||
一个环形缓冲的下标改写。
|
||||
|
||||
所以给的是关中断的临界区:不阻塞、没有持有者、不参与「不许嵌套两把锁」
|
||||
那条规则,也不出现在 `hal_lock_id_t` 里。
|
||||
|
||||
⚠️ **里面只许改内存。** 关着中断去等锁、睡、跑 I²C 事务,都是错的
|
||||
—— sim 里 `sim_crit_blocking()` 会计数,`test_evlog.c` 拿它当断言。
|
||||
可以嵌套(按层数计),但没有理由这么写。 */
|
||||
void hal_crit_enter(void);
|
||||
void hal_crit_exit(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/* 掉电不丢的一块字节区(需求 §10.7 的「落盘」)—— HAL 的第六样东西。
|
||||
|
||||
计划 §4.1 写着「HAL 只抽象四样」,§10.3 的两把锁是第五样。这里的第六样同样
|
||||
要给个理由:§10.7 要求事件**在上位机不在线时也记得下来**,而「记得下来」在
|
||||
ESP32-S3 上就是往 NOR flash 的一个分区里写。事件日志本身(环形、批量、
|
||||
按序号增量拉取)是与平台无关的逻辑,宿主机上必须跑得到;能跑不到的只有
|
||||
「怎么擦一个扇区、怎么写一段字节」这三行。抽的就是这三行。
|
||||
|
||||
---- 抽象的是 NOR flash,不是文件 ----------------------------------------
|
||||
接口上带着 NOR 的两条物理事实,因为**上层绕不开它们**:
|
||||
1. **写只能把 1 变成 0。** 同一个位置写两遍不是覆盖,是按位与。
|
||||
所以 sim 的实现也是按位与 —— 真板上「以为覆盖成功了」的那种错,
|
||||
在一个 memcpy 式的假实现里一次也复现不出来。
|
||||
2. **擦除的粒度是扇区**(4 KB),擦完全是 0xFF。
|
||||
还有一条没写进接口但同样要记住的:擦一次就少一次寿命(十万量级)。
|
||||
§10.7 的量级是「每天几十条」,17 个扇区轮着用,一辈子擦不了几轮。
|
||||
|
||||
⚠️ **不做 wear leveling、不做坏块管理、不做文件系统。** 这块区只有一个用户
|
||||
(evlog),它自己就是个环形缓冲,天然轮流擦全部扇区。套一层 SPIFFS 反而
|
||||
多出一堆掉电一致性问题 —— 那正是这一节最不想要的东西。 */
|
||||
#ifndef HAL_STORE_H
|
||||
#define HAL_STORE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
uint32_t sector_bytes; /* 擦除粒度。ESP32-S3 的 NOR 是 4096 */
|
||||
uint32_t sectors; /* 这块区有几个扇区 */
|
||||
} hal_store_geom_t;
|
||||
|
||||
/* 幂等。ESP 侧是找到 `evlog` 分区;找不到返回 HAL_ERR_STATE
|
||||
(分区表里漏了这一项 —— 那是编译期就该发现的错,不是硬件故障)。 */
|
||||
hal_err_t hal_store_init(void);
|
||||
|
||||
/* 没 init 时两个字段都是 0。调用方拿它算容量,所以**不许猜**这两个数。 */
|
||||
void hal_store_geometry(hal_store_geom_t *g);
|
||||
|
||||
/* 读。off/len 以字节计,越界返回 HAL_ERR_ARG。 */
|
||||
hal_err_t hal_store_read(uint32_t off, void *buf, uint32_t len);
|
||||
|
||||
/* 写。⚠️ **按位与语义**(见上):往没擦过的地方写,得到的是两次内容的与,
|
||||
而且**不报错**。想覆盖就得先擦整个扇区。
|
||||
off 与 len 都要求 4 字节对齐(esp_flash 的要求,sim 里同样拦)。 */
|
||||
hal_err_t hal_store_write(uint32_t off, const void *buf, uint32_t len);
|
||||
|
||||
/* 擦一个扇区(整块变 0xFF)。sector 是索引,不是字节偏移。 */
|
||||
hal_err_t hal_store_erase(uint32_t sector);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HAL_STORE_H */
|
||||
@@ -23,6 +23,9 @@ static uint32_t s_violations[HAL_LOCK_COUNT];
|
||||
static bool s_require[HAL_LOCK_COUNT];
|
||||
static int s_task;
|
||||
static bool s_inited;
|
||||
static int s_crit_depth;
|
||||
static uint32_t s_crit_enters;
|
||||
static uint32_t s_crit_blocking;
|
||||
|
||||
void sim_lock_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
|
||||
@@ -37,6 +40,9 @@ void sim_lock_reset_internal(void)
|
||||
memset(s_require, 0, sizeof(s_require));
|
||||
s_task = 0;
|
||||
s_inited = false;
|
||||
s_crit_depth = 0;
|
||||
s_crit_enters = 0;
|
||||
s_crit_blocking = 0;
|
||||
}
|
||||
|
||||
static bool valid(hal_lock_id_t id) { return id >= 0 && id < HAL_LOCK_COUNT; }
|
||||
@@ -54,6 +60,7 @@ bool hal_lock_held(hal_lock_id_t id)
|
||||
|
||||
hal_err_t hal_lock_take(hal_lock_id_t id, uint32_t timeout_ms)
|
||||
{
|
||||
sim_crit_blocking_note(); /* 关着中断去等一把锁:真板上这是把系统钉死 */
|
||||
if (!valid(id) || !s_inited) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
@@ -105,3 +112,30 @@ void sim_lock_check(hal_lock_id_t id)
|
||||
s_violations[id]++;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 临界区(不是第三把锁,见 hal_lock.h) -------------------------------
|
||||
单线程 sim 里它没有任何互斥作用 —— 这里模拟的同样是**纪律**:
|
||||
进出配不配对、里面有没有干会阻塞的事。 */
|
||||
|
||||
void hal_crit_enter(void)
|
||||
{
|
||||
s_crit_depth++;
|
||||
s_crit_enters++;
|
||||
}
|
||||
|
||||
void hal_crit_exit(void)
|
||||
{
|
||||
assert(s_crit_depth > 0); /* 多放一次 = 真板上中断被提前打开,谁也发现不了 */
|
||||
s_crit_depth--;
|
||||
}
|
||||
|
||||
void sim_crit_blocking_note(void)
|
||||
{
|
||||
if (s_crit_depth > 0) {
|
||||
s_crit_blocking++;
|
||||
}
|
||||
}
|
||||
|
||||
int sim_crit_depth(void) { return s_crit_depth; }
|
||||
uint32_t sim_crit_enters(void) { return s_crit_enters; }
|
||||
uint32_t sim_crit_blocking(void) { return s_crit_blocking; }
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
/* 掉电不丢区的假实现 —— 一块 RAM,但**按 NOR flash 的规矩来**。
|
||||
|
||||
三条规矩,缺一条就会让 evlog 里最难查的那类错在宿主机上永远复现不出来:
|
||||
1. 写 = **按位与**(`*p &= v`),不是赋值。往没擦过的槽里再写一条记录,
|
||||
真板上得到的是两条记录的与(多半是一条校验不过的垃圾),而**不报错**;
|
||||
2. 擦 = 整个扇区变 0xFF,粒度就是扇区;
|
||||
3. **`sim_hal_reset()` 会把这块区擦干净(= 一块新板子),
|
||||
`sim_hal_reboot()` 不会(= 掉电重启)。** §10.7 的验收「掉电重启后能读回」
|
||||
全靠这两个的区别,别把 store 的清理塞进 reboot 里。
|
||||
|
||||
还多做一件真板会做而假实现容易漏的事:**掉电可以打断一次写**
|
||||
(`sim_store_tear_next_write()`),落进去的是前面若干字节,后面还是 0xFF。
|
||||
这正是 evlog 每条记录都要带校验字节的理由。 */
|
||||
#include <string.h>
|
||||
|
||||
#include "hal_store.h"
|
||||
#include "sim_hal.h"
|
||||
|
||||
static uint8_t s_mem[SIM_STORE_SECTORS * SIM_STORE_SECTOR_BYTES];
|
||||
static bool s_inited;
|
||||
static bool s_formatted; /* 这块「flash」出过厂没有(全 0xFF 才是出厂态) */
|
||||
static uint32_t s_writes;
|
||||
static uint32_t s_erases;
|
||||
static uint32_t s_bytes;
|
||||
static int s_fail_in; /* >0:第 n 次写整条失败;0 = 不注入 */
|
||||
static uint32_t s_tear_at; /* >0:下一次写只落这么多字节然后失败 */
|
||||
|
||||
void sim_store_reset_internal(void); /* 只给 sim_hal_reset() 用:连内容一起擦 */
|
||||
void sim_store_reboot_internal(void); /* 只给 sim_hal_reboot() 用:内容留着 */
|
||||
|
||||
static void wipe(void)
|
||||
{
|
||||
memset(s_mem, 0xFF, sizeof(s_mem));
|
||||
s_formatted = true;
|
||||
}
|
||||
|
||||
void sim_store_reset_internal(void)
|
||||
{
|
||||
wipe();
|
||||
s_inited = false;
|
||||
s_writes = 0;
|
||||
s_erases = 0;
|
||||
s_bytes = 0;
|
||||
s_fail_in = 0;
|
||||
s_tear_at = 0;
|
||||
}
|
||||
|
||||
void sim_store_reboot_internal(void)
|
||||
{
|
||||
/* 掉电重启:CPU 侧什么都不剩,flash 里的内容一个字节不变。 */
|
||||
if (!s_formatted) {
|
||||
wipe(); /* 从来没 reset 过的进程:至少给它一块出厂态的 flash */
|
||||
}
|
||||
s_inited = false;
|
||||
s_writes = 0;
|
||||
s_erases = 0;
|
||||
s_bytes = 0;
|
||||
s_fail_in = 0;
|
||||
s_tear_at = 0;
|
||||
}
|
||||
|
||||
hal_err_t hal_store_init(void)
|
||||
{
|
||||
if (!s_formatted) {
|
||||
wipe();
|
||||
}
|
||||
s_inited = true;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
void hal_store_geometry(hal_store_geom_t *g)
|
||||
{
|
||||
if (g == NULL) {
|
||||
return;
|
||||
}
|
||||
g->sector_bytes = s_inited ? SIM_STORE_SECTOR_BYTES : 0u;
|
||||
g->sectors = s_inited ? SIM_STORE_SECTORS : 0u;
|
||||
}
|
||||
|
||||
static bool range_ok(uint32_t off, uint32_t len)
|
||||
{
|
||||
if (len > sizeof(s_mem) || off > sizeof(s_mem) - len) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
hal_err_t hal_store_read(uint32_t off, void *buf, uint32_t len)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (buf == NULL || !range_ok(off, len)) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
memcpy(buf, &s_mem[off], len);
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t hal_store_write(uint32_t off, const void *buf, uint32_t len)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (buf == NULL || !range_ok(off, len) || (off % 4u) != 0u || (len % 4u) != 0u) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
|
||||
uint32_t n = len;
|
||||
bool fail = false;
|
||||
if (s_fail_in > 0 && --s_fail_in == 0) {
|
||||
return HAL_ERR_BUS; /* 一个字节都没落进去 */
|
||||
}
|
||||
if (s_tear_at > 0u) {
|
||||
n = (s_tear_at < len) ? s_tear_at : len;
|
||||
s_tear_at = 0u;
|
||||
fail = true; /* 掉电:前 n 个字节落进去了,然后就没有然后了 */
|
||||
}
|
||||
|
||||
const uint8_t *src = (const uint8_t *)buf;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
s_mem[off + i] &= src[i]; /* ⚠️ NOR:只能 1→0 */
|
||||
}
|
||||
s_writes++;
|
||||
s_bytes += n;
|
||||
return fail ? HAL_ERR_BUS : HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t hal_store_erase(uint32_t sector)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (sector >= SIM_STORE_SECTORS) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
memset(&s_mem[sector * SIM_STORE_SECTOR_BYTES], 0xFF, SIM_STORE_SECTOR_BYTES);
|
||||
s_erases++;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* ---- 假板子这一侧的控制面 ------------------------------------------------ */
|
||||
|
||||
void sim_store_fail_next_write(int nth) { s_fail_in = nth; }
|
||||
void sim_store_tear_next_write(uint32_t bytes) { s_tear_at = bytes; }
|
||||
uint32_t sim_store_writes(void) { return s_writes; }
|
||||
uint32_t sim_store_erases(void) { return s_erases; }
|
||||
uint32_t sim_store_bytes(void) { return s_bytes; }
|
||||
|
||||
uint8_t sim_store_byte(uint32_t off)
|
||||
{
|
||||
return (off < sizeof(s_mem)) ? s_mem[off] : 0xFFu;
|
||||
}
|
||||
@@ -25,6 +25,7 @@ uint32_t hal_time_elapsed_ms(uint32_t since_ms)
|
||||
|
||||
void hal_time_delay_ms(uint32_t ms)
|
||||
{
|
||||
sim_crit_blocking_note(); /* 关着中断去睡:见 hal_lock.h 的临界区那一段 */
|
||||
s_delay_calls++;
|
||||
s_now_us += (uint64_t)ms * 1000u;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
/* sim 的总闸:一次把四路都恢复出厂。每个测试的 setUp() 调它。
|
||||
各路的清理函数是内部符号,不进 sim_hal.h —— 测试没有理由单独重置某一路。 */
|
||||
/* sim 的总闸:一次把各路都恢复出厂。每个测试的 setUp() 调它。
|
||||
各路的清理函数是内部符号,不进 sim_hal.h —— 测试没有理由单独重置某一路。
|
||||
|
||||
⚠️ 这里有**两个**总闸:`sim_hal_reset()` 是「换了一块新板子」,
|
||||
`sim_hal_reboot()` 是「同一块板子掉电重启」。两者只差掉电不丢区那一块,
|
||||
而 §10.7 的验收正好卡在这个差上(见 hal_store_sim.c 开头)。 */
|
||||
#include "hal_i2c_acct.h"
|
||||
#include "sim_hal.h"
|
||||
|
||||
@@ -10,8 +14,11 @@ void sim_pca9555_reset_internal(void);
|
||||
void sim_pwm_reset_internal(void);
|
||||
void sim_time_reset_internal(void);
|
||||
void sim_lock_reset_internal(void);
|
||||
void sim_store_reset_internal(void);
|
||||
void sim_store_reboot_internal(void);
|
||||
|
||||
void sim_hal_reset(void)
|
||||
/* 除了掉电不丢区之外的一切。reset 与 reboot 的唯一区别就在那一块。 */
|
||||
static void reset_the_volatile_half(void)
|
||||
{
|
||||
sim_i2c_reset_internal();
|
||||
sim_gpio_reset_internal();
|
||||
@@ -22,3 +29,15 @@ void sim_hal_reset(void)
|
||||
sim_lock_reset_internal();
|
||||
hal_i2c_acct_reset();
|
||||
}
|
||||
|
||||
void sim_hal_reset(void)
|
||||
{
|
||||
reset_the_volatile_half();
|
||||
sim_store_reset_internal(); /* 新板子:flash 也是出厂态 */
|
||||
}
|
||||
|
||||
void sim_hal_reboot(void)
|
||||
{
|
||||
reset_the_volatile_half();
|
||||
sim_store_reboot_internal(); /* 掉电重启:flash 里的内容一个字节不动 */
|
||||
}
|
||||
|
||||
@@ -14,16 +14,23 @@
|
||||
#include "hal_i2c_acct.h"
|
||||
#include "hal_lock.h"
|
||||
#include "hal_pwm.h"
|
||||
#include "hal_store.h"
|
||||
#include "hal_time.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 四路 sim 全部恢复出厂:器件表清空、引脚未配置、PWM 未配置、时钟归零、
|
||||
init 标志清掉。每个测试的 setUp() 里调它。 */
|
||||
/* 各路 sim 全部恢复出厂:器件表清空、引脚未配置、PWM 未配置、时钟归零、
|
||||
init 标志清掉,**掉电不丢区整块擦成 0xFF**。每个测试的 setUp() 里调它。
|
||||
语义是「换了一块新板子」。 */
|
||||
void sim_hal_reset(void);
|
||||
|
||||
/* **掉电重启**:除了掉电不丢区里的内容,其余与 `sim_hal_reset()` 完全一样。
|
||||
§10.7 的验收「掉电重启后能读回」就是靠这两个的区别 ——
|
||||
拿 reset 去测重启,测的是「刚擦干净的 flash 读不出东西」,永远绿。 */
|
||||
void sim_hal_reboot(void);
|
||||
|
||||
/* ---------------- I²C ---------------- */
|
||||
|
||||
#define SIM_I2C_MAX_DEV 16
|
||||
@@ -172,6 +179,34 @@ void sim_lock_require(hal_lock_id_t id, bool on);
|
||||
uint32_t sim_lock_violations(hal_lock_id_t id);
|
||||
void sim_lock_check(hal_lock_id_t id); /* 给 sim 各路自己调,测试一般用不到 */
|
||||
|
||||
/* ---------------- 掉电不丢区(NOR flash 那块) ---------------- */
|
||||
|
||||
/* 假 flash 的几何。与 partitions.csv 里 `evlog` 分区的大小是同一个数,
|
||||
`test_evlog.c` 拿 `EVLOG_SECTORS` 把两边钉在一起。 */
|
||||
#define SIM_STORE_SECTOR_BYTES 4096
|
||||
#define SIM_STORE_SECTORS 17
|
||||
|
||||
/* 故障注入:第 nth 次写整条失败(一个字节都不落)。0 = 清除。 */
|
||||
void sim_store_fail_next_write(int nth);
|
||||
|
||||
/* **掉电打断一次写**:下一次写只落前 bytes 个字节,然后返回 HAL_ERR_BUS。
|
||||
这是每条记录都要带校验字节的理由 —— 半条记录在 flash 里长得和一条真记录一样。 */
|
||||
void sim_store_tear_next_write(uint32_t bytes);
|
||||
|
||||
uint32_t sim_store_writes(void); /* 成功或半成功的写事务次数 */
|
||||
uint32_t sim_store_erases(void); /* 擦过几个扇区 —— 寿命就花在这上面 */
|
||||
uint32_t sim_store_bytes(void); /* 真正落进去的字节数 */
|
||||
uint8_t sim_store_byte(uint32_t off); /* 越界返回 0xFF */
|
||||
|
||||
/* ---------------- 临界区(不是第三把锁,见 hal_lock.h) ---------------- */
|
||||
|
||||
int sim_crit_depth(void); /* 当前嵌套层数。测试结束时必须是 0 */
|
||||
uint32_t sim_crit_enters(void); /* 进过几次 —— 断言「入队真的是在临界区里做的」*/
|
||||
|
||||
/* 在临界区里干了会阻塞的事(等锁 / 睡)的次数。**必须恒为 0**。 */
|
||||
uint32_t sim_crit_blocking(void);
|
||||
void sim_crit_blocking_note(void); /* 给 sim 各路自己调 */
|
||||
|
||||
/* ---------------- 时钟 ---------------- */
|
||||
|
||||
void sim_time_set_us(uint64_t us);
|
||||
|
||||
在新工单中引用
屏蔽一个用户