文件
phonerail/firmware/components/phonerail_hal/esp/hal_lock_esp.c
T
zhou yang和Claude Opus 5 18d3d69856 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]>
2026-09-07 09:20:02 +08:00

79 行
3.0 KiB
C
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/* 两把互斥锁的 FreeRTOS 实现。静态分配(`xSemaphoreCreateMutexStatic`)——
两把锁的数量是编译期定死的,没有理由去堆上要内存,也就没有「创建失败」这条路径。
⚠️ 用的是 mutex 不是 binary semaphore:mutex 有**优先级继承**。
t_safety 是最高优先级且每 20 ms 就要总线,低优先级的 t_link 拿着 I²C 锁去读遥测时
如果没有继承,t_safety 会被一个中优先级任务无限期挡在门外(经典优先级反转)。 */
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "hal_lock.h"
static StaticSemaphore_t s_buf[HAL_LOCK_COUNT];
static SemaphoreHandle_t s_mtx[HAL_LOCK_COUNT];
static uint32_t s_timeouts[HAL_LOCK_COUNT];
hal_err_t hal_lock_init(void)
{
for (int i = 0; i < HAL_LOCK_COUNT; i++) {
if (s_mtx[i] == NULL) {
s_mtx[i] = xSemaphoreCreateMutexStatic(&s_buf[i]);
s_timeouts[i] = 0;
}
}
return HAL_OK;
}
bool hal_lock_held(hal_lock_id_t id)
{
if (id < 0 || id >= HAL_LOCK_COUNT || s_mtx[id] == NULL) {
return false;
}
return xSemaphoreGetMutexHolder(s_mtx[id]) == xTaskGetCurrentTaskHandle();
}
hal_err_t hal_lock_take(hal_lock_id_t id, uint32_t timeout_ms)
{
if (id < 0 || id >= HAL_LOCK_COUNT || s_mtx[id] == NULL) {
return HAL_ERR_STATE;
}
/* 已经持有 = 重复加锁;持有另一把 = 嵌套。两者都是写错了,见 hal_lock.h。 */
for (int i = 0; i < HAL_LOCK_COUNT; i++) {
if (hal_lock_held((hal_lock_id_t)i)) {
return HAL_ERR_STATE;
}
}
const TickType_t ticks =
(timeout_ms == HAL_LOCK_FOREVER) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms);
if (xSemaphoreTake(s_mtx[id], ticks) != pdTRUE) {
s_timeouts[id]++;
return HAL_ERR_TIMEOUT;
}
return HAL_OK;
}
void hal_lock_give(hal_lock_id_t id)
{
configASSERT(id >= 0 && id < HAL_LOCK_COUNT && s_mtx[id] != NULL);
configASSERT(hal_lock_held(id)); /* 不是自己拿的就别放 */
(void)xSemaphoreGive(s_mtx[id]);
}
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); }