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]>
这个提交包含在:
@@ -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;
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户