- 分区重排(16 MB,nvs 0x9000 不动):ota_0 / ota_1 各 3 MB、otadata 0x610000、evlog 挪到 0x612000;
flash 大小改正为 16 MB;开 CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE
- ota.{h,c}(宿主机可测):URL / sha256 校验、判定(下载中 busy、待验证拒、同镜像不重下)、健康判据
- ota_esp.{h,c}:t_ota 任务经 HTTP 下载、边下边算 sha256、核 project_name 后切启动分区;
新固件 120 s 内连不上 broker 就回滚(t_link 判 + esp_timer 兜底);重启前等回应发出、事件落盘
- 新命令 ota = 13(两条链路都收,reboot 默认 false:复位 = 五台手机断电一次);status 加 ota 一块;
新事件 LOG_EV_OTA = 18;PROJECT_VER 取 PHONERAIL_FW_VERSION
- test_ota.c 13 例,全工程 442 例全绿;版本 0.1.0-c2
- 服务器:nginx 7765 只放行 /fw/<16 hex>.bin(不过门户,路由器转发已加);tools/ota_push.py 推送;
rack protocol.py 加事件 18 / 命令 13(已部署)
- 板 B:推送到自证健康 19 s;错哈希不切分区;删掉 broker 账号后 120 s 回滚,bad 报出被回滚的版本
Co-Authored-By: Claude Opus 5.5 <[email protected]>
580 行
21 KiB
C
580 行
21 KiB
C
/* 事件日志的实现。接口与「为什么这么分层」在 evlog.h,这里只写 flash 上的样子
|
||
与几处不写下来就一定会被改坏的地方。
|
||
|
||
---- flash 上的样子(17 个扇区 × 4 KB) ----------------------------------
|
||
|
||
扇区 n: [ 16 B 扇区头 ][ 记录 0 ][ 记录 1 ] … [ 记录 254 ]
|
||
magic/ver/ 每条 16 B,255 条
|
||
recs/epoch/chk
|
||
|
||
· **扇区头里的 `epoch` 是全局递增的**(第几个被写的扇区),
|
||
所以 `扇区号 = epoch % 17`,重启时不用记住任何东西:读 17 个头,
|
||
epoch 最大的那个就是正在写的扇区。
|
||
· **记录的序号 = `epoch × 255 + 槽号`**。序号因此就是「flash 里第几条」,
|
||
不需要额外存一个计数器,也不会在掉电后倒退。
|
||
· 环形:写满一个扇区就**擦掉下一个**(也就是最老的那个)再写它的头。
|
||
擦除是 4 KB 粒度的,所以「盖掉最旧的」一次盖 255 条 —— 这正是 §10.7
|
||
的容量算法里那 4096 条的来源。
|
||
|
||
---- 为什么每条记录要带一个校验字节 --------------------------------------
|
||
NOR flash 的写可以被掉电打断在任意一个字节上,落进去的是**半条记录**,
|
||
而半条记录在字节层面看跟一条真记录长得一模一样(时间戳合法、路号合法、
|
||
类型合法)。没有校验的话,重启后的扫描会把它当成有效记录继续往后写,
|
||
而上位机拉到的是一条时间戳来自过去、电压是 0xFFFF 的「事件」。
|
||
校验字节让扫描停在它前面 —— `test_a_torn_write_does_not_become_a_record()`。
|
||
|
||
⚠️ 校验的是**完整性**,不是正确性:它认得出「写了一半」,认不出「写错了地方」。
|
||
后者由扇区头里的 epoch 与 recs 兜着。
|
||
|
||
---- 字节序 --------------------------------------------------------------
|
||
记录是把结构体原样写下去的,所以 flash 里是**本机小端**。这不成问题:
|
||
读它的只有同一颗 MCU 上的同一份固件。上位机拿到的记录是经链路层
|
||
(A13/A14)序列化过去的,不是直接读 flash 的镜像。 */
|
||
#include <stddef.h>
|
||
#include <string.h>
|
||
|
||
#include "evlog.h"
|
||
#include "hal_lock.h"
|
||
#include "hal_store.h"
|
||
#include "hal_time.h"
|
||
|
||
#define EVLOG_MAGIC 0x474F4C45u /* "ELOG" */
|
||
#define EVLOG_VER 1
|
||
|
||
/* 一次 `hal_store_write()` 最多带几条。256 B 的栈上缓冲,够摊薄写事务的开销了;
|
||
再大也只是让 `evlog_flush()` 的最坏耗时变长。 */
|
||
#define EVLOG_CHUNK 16
|
||
|
||
typedef struct {
|
||
uint32_t magic;
|
||
uint16_t ver;
|
||
uint16_t recs; /* 本扇区能装几条。几何变了(换了 flash / 换了分区大小)时,
|
||
旧扇区认不出来 —— 这比按新几何去解释旧内容好得多 */
|
||
uint32_t epoch; /* 全局递增:第几个被写的扇区 */
|
||
uint32_t chk;
|
||
} evlog_shdr_t;
|
||
|
||
/* 两个「结构体就是 flash 布局」的地方,编译期钉死。 */
|
||
typedef char evlog_rec_is_16b[(sizeof(evlog_rec_t) == 16) ? 1 : -1];
|
||
typedef char evlog_hdr_is_16b[(sizeof(evlog_shdr_t) == 16) ? 1 : -1];
|
||
|
||
/* ---- RAM 环形缓冲 --------------------------------------------------------
|
||
入队方可能是任何一个任务(t_safety 优先级最高),出队方只有 t_log。
|
||
两个下标的改动在临界区里做完(hal_lock.h:那不是第三把锁)。 */
|
||
static evlog_rec_t s_ring[EVLOG_RAM_CAP];
|
||
static uint32_t s_head; /* 下一条写进去的位置 */
|
||
static uint32_t s_tail; /* 下一条要落盘的位置 */
|
||
static uint32_t s_count; /* 环里现有几条 */
|
||
static uint32_t s_oldest_ms; /* 环里最老那条的入队时刻(攒批的计时起点) */
|
||
static uint32_t s_lost; /* 欠着一条 LOST 记录,值是丢了多少条 */
|
||
|
||
/* ---- flash 侧 ------------------------------------------------------------ */
|
||
static bool s_have_store;
|
||
static uint32_t s_sector_bytes;
|
||
static uint32_t s_sectors;
|
||
static uint32_t s_recs; /* 每扇区能装几条记录(= sector_bytes/16 − 1) */
|
||
static uint32_t s_epoch; /* 正在写的扇区的 epoch */
|
||
static uint32_t s_slot; /* 该扇区里下一个空槽;== s_recs 表示这个扇区写满了 */
|
||
static uint32_t s_next_seq;
|
||
static uint32_t s_oldest_seq;
|
||
|
||
/* ---- 计数器 -------------------------------------------------------------- */
|
||
static uint32_t s_queued;
|
||
static uint32_t s_stored;
|
||
static uint32_t s_dropped;
|
||
static uint32_t s_io_errors;
|
||
|
||
/* ---- 校验 ---------------------------------------------------------------- */
|
||
|
||
/* CRC-8(多项式 0x07,初值 0xA5)。
|
||
|
||
⚠️ **不要「优化」成异或和。** 第一版就是异或和,它认不出下面这条:
|
||
一条 `bus_mv` 与 `current_ua` 都是 0 的记录(BOOT 就长这样),被掉电切在
|
||
第 8 个字节上,后 8 个字节留在 0xFF —— 而 8 个 0xFF 的异或恰好是 0,
|
||
于是半条记录的异或和与整条**一模一样**,校验照过。
|
||
`test_a_torn_write_with_zero_payload_is_still_rejected()` 就是这条。
|
||
CRC 对「一段字节整体变成 0xFF」敏感,初值也不是 0(认得出全 0 的一片)。 */
|
||
static uint8_t rec_chk(const evlog_rec_t *r)
|
||
{
|
||
const uint8_t *b = (const uint8_t *)r;
|
||
uint8_t c = 0xA5u;
|
||
for (size_t i = 0; i < sizeof(*r); i++) {
|
||
if (i == offsetof(evlog_rec_t, chk)) {
|
||
continue;
|
||
}
|
||
c ^= b[i];
|
||
for (int k = 0; k < 8; k++) {
|
||
c = (uint8_t)((c & 0x80u) ? ((uint8_t)(c << 1) ^ 0x07u) : (uint8_t)(c << 1));
|
||
}
|
||
}
|
||
return c;
|
||
}
|
||
|
||
/* 擦干净的槽(16 个 0xFF)在这里必须是 false —— 扫描就靠它找「上次写到哪了」,
|
||
而这是整份日志唯一的位置信息。所以不靠 CRC 去撞那 1/256,直接认死:
|
||
**全 0xFF = 没写过**。真记录里 `t_ms` 是 0xFFFFFFFF 且五个字段全 0xFF 的
|
||
那一条不存在(那是 49.7 天整、−1 mV、−1 µA、路号 255 同时成立)。 */
|
||
static bool rec_all_ff(const evlog_rec_t *r)
|
||
{
|
||
const uint8_t *b = (const uint8_t *)r;
|
||
for (size_t i = 0; i < sizeof(*r); i++) {
|
||
if (b[i] != 0xFFu) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
static bool rec_valid(const evlog_rec_t *r)
|
||
{
|
||
return !rec_all_ff(r) && r->chk == rec_chk(r);
|
||
}
|
||
|
||
static uint32_t hdr_chk(const evlog_shdr_t *h)
|
||
{
|
||
return h->magic ^ ((uint32_t)h->ver | ((uint32_t)h->recs << 16)) ^ h->epoch ^ 0x5A5A5A5Au;
|
||
}
|
||
|
||
static bool hdr_valid(const evlog_shdr_t *h, uint32_t sector)
|
||
{
|
||
return h->magic == EVLOG_MAGIC && h->ver == EVLOG_VER && h->recs == (uint16_t)s_recs &&
|
||
h->chk == hdr_chk(h) && (h->epoch % s_sectors) == sector;
|
||
}
|
||
|
||
static uint32_t rec_off(uint32_t sector, uint32_t slot)
|
||
{
|
||
return sector * s_sector_bytes + (slot + 1u) * (uint32_t)sizeof(evlog_rec_t);
|
||
}
|
||
|
||
/* ---- 入队(谁产生事件谁调;不阻塞、不碰 flash) -------------------------- */
|
||
|
||
static void push(const evlog_rec_t *r)
|
||
{
|
||
/* ⚠️ 临界区里**只改内存**:时间戳、校验都在外面算完了。
|
||
往里面加一行「顺手」的活,抖的是 t_safety 那 20 ms 的节拍。 */
|
||
hal_crit_enter();
|
||
if (s_count == EVLOG_RAM_CAP) {
|
||
/* 满了盖最旧的一条 —— §10.7 的原则是「最新的一条永远进得来」。
|
||
丢掉的这条会在下一次落盘时以一条 LOST 记录留下痕迹。 */
|
||
s_tail = (s_tail + 1u) % EVLOG_RAM_CAP;
|
||
s_count--;
|
||
s_dropped++;
|
||
s_lost++;
|
||
}
|
||
if (s_count == 0u) {
|
||
s_oldest_ms = r->t_ms; /* 攒批的计时从环里第一条算起 */
|
||
}
|
||
s_ring[s_head] = *r;
|
||
s_head = (s_head + 1u) % EVLOG_RAM_CAP;
|
||
s_count++;
|
||
s_queued++;
|
||
hal_crit_exit();
|
||
}
|
||
|
||
void evlog_put(uint8_t rail, log_ev_t type, uint8_t arg, int32_t bus_mv, int32_t ua)
|
||
{
|
||
evlog_rec_t r;
|
||
memset(&r, 0, sizeof(r)); /* 记录会被原样写进 flash:不留没初始化的字节 */
|
||
r.t_ms = hal_time_now_ms();
|
||
r.rail = rail;
|
||
r.type = (uint8_t)type;
|
||
r.arg = arg;
|
||
r.bus_mv = bus_mv;
|
||
r.current_ua = ua;
|
||
r.chk = rec_chk(&r);
|
||
push(&r);
|
||
}
|
||
|
||
void evlog_put_safety(void *ctx, const safety_event_t *ev)
|
||
{
|
||
(void)ctx;
|
||
if (ev == NULL) {
|
||
return;
|
||
}
|
||
evlog_rec_t r;
|
||
memset(&r, 0, sizeof(r));
|
||
/* ⚠️ 时间戳用事件里带的那个:判据记的是**断电那一刻**,
|
||
而这个回调是断完电才被调的。差的那几十微秒不重要,重要的是别把
|
||
「事件发生的时刻」悄悄换成「记账的时刻」—— 排查时序问题时会差出一整圈。 */
|
||
r.t_ms = ev->t_ms;
|
||
r.rail = ev->rail;
|
||
r.type = (uint8_t)evlog_from_safety(ev->type);
|
||
r.bus_mv = ev->bus_mv;
|
||
r.current_ua = ev->current_ua;
|
||
r.chk = rec_chk(&r);
|
||
push(&r);
|
||
}
|
||
|
||
/* 显式的一一对应。**不许写成 `LOG_EV_OVER_VOLT + (ev − 1)`** ——
|
||
那样 t_safety.h 里插进一个新事件类型,flash 里所有旧记录的含义就整体挪位了,
|
||
而且不会有任何一处报错。`test_the_safety_mapping_is_explicit()` 拦这件事。 */
|
||
log_ev_t evlog_from_safety(uint8_t safety_ev)
|
||
{
|
||
switch ((safety_ev_t)safety_ev) {
|
||
case SAFETY_EV_OVER_VOLT: return LOG_EV_OVER_VOLT;
|
||
case SAFETY_EV_UNDER_VOLT: return LOG_EV_UNDER_VOLT;
|
||
case SAFETY_EV_REVERSE: return LOG_EV_REVERSE;
|
||
case SAFETY_EV_PUSHED_UP: return LOG_EV_PUSHED_UP;
|
||
case SAFETY_EV_MUX_LOST: return LOG_EV_MUX_LOST;
|
||
case SAFETY_EV_SLOT_EMPTY: return LOG_EV_SLOT_EMPTY;
|
||
case SAFETY_EV_NONE: break;
|
||
}
|
||
return LOG_EV_NONE;
|
||
}
|
||
|
||
/* ---- 落盘(只有 t_log 与 A12 的拉日志命令调) ---------------------------- */
|
||
|
||
static hal_err_t write_header(uint32_t sector, uint32_t epoch)
|
||
{
|
||
evlog_shdr_t h;
|
||
memset(&h, 0, sizeof(h));
|
||
h.magic = EVLOG_MAGIC;
|
||
h.ver = EVLOG_VER;
|
||
h.recs = (uint16_t)s_recs;
|
||
h.epoch = epoch;
|
||
h.chk = hdr_chk(&h);
|
||
return hal_store_write(sector * s_sector_bytes, &h, sizeof(h));
|
||
}
|
||
|
||
/* 换到下一个扇区:先擦(这一下盖掉的就是最老的 255 条),再写头。 */
|
||
static hal_err_t advance_sector(void)
|
||
{
|
||
const uint32_t epoch = s_epoch + 1u;
|
||
const uint32_t sector = epoch % s_sectors;
|
||
hal_err_t err = hal_store_erase(sector);
|
||
if (err != HAL_OK) {
|
||
return err;
|
||
}
|
||
err = write_header(sector, epoch);
|
||
if (err != HAL_OK) {
|
||
return err;
|
||
}
|
||
s_epoch = epoch;
|
||
s_slot = 0;
|
||
s_next_seq = epoch * s_recs;
|
||
if (epoch >= s_sectors) {
|
||
s_oldest_seq = (epoch - s_sectors + 1u) * s_recs;
|
||
}
|
||
return HAL_OK;
|
||
}
|
||
|
||
/* 把 n 条连续记录写进当前扇区(跨扇区就自己换)。返回真的写进去几条。 */
|
||
static uint32_t store_run(const evlog_rec_t *r, uint32_t n)
|
||
{
|
||
uint32_t done = 0;
|
||
while (done < n) {
|
||
if (s_slot >= s_recs) {
|
||
if (advance_sector() != HAL_OK) {
|
||
s_io_errors++;
|
||
break;
|
||
}
|
||
}
|
||
uint32_t k = n - done;
|
||
if (k > s_recs - s_slot) {
|
||
k = s_recs - s_slot;
|
||
}
|
||
const uint32_t off = rec_off(s_epoch % s_sectors, s_slot);
|
||
if (hal_store_write(off, &r[done], k * (uint32_t)sizeof(evlog_rec_t)) != HAL_OK) {
|
||
s_io_errors++;
|
||
/* 写失败时**这一槽可能只落进去了半条**(掉电、或者 flash 真坏了)。
|
||
半条记录后面再接着写的话,重启扫描会停在半条那里,把后面写的
|
||
全部当成不存在。所以整个扇区的剩余部分就此放弃,下一条换扇区去。
|
||
代价是最多浪费 254 个槽,换来的是「扫描停在哪 = 日志到哪为止」
|
||
这条不变量永远成立。 */
|
||
s_slot = s_recs;
|
||
break;
|
||
}
|
||
s_slot += k;
|
||
s_next_seq += k;
|
||
s_stored += k;
|
||
done += k;
|
||
}
|
||
return done;
|
||
}
|
||
|
||
/* 补一条「这里丢了 n 条」。它自己也占一个序号 —— 日志里看得见的那种丢失。
|
||
返回真的写进去几条(0 或 1):它和普通记录一样算进 `evlog_flush()` 的返回值,
|
||
否则会出现「flush 说写了 0 条,序号却往前走了一格」这种对不上的账。 */
|
||
static uint32_t store_lost_marker(void)
|
||
{
|
||
uint32_t n;
|
||
hal_crit_enter();
|
||
n = s_lost;
|
||
s_lost = 0;
|
||
hal_crit_exit();
|
||
if (n == 0u) {
|
||
return 0u;
|
||
}
|
||
evlog_rec_t r;
|
||
memset(&r, 0, sizeof(r));
|
||
r.t_ms = hal_time_now_ms();
|
||
r.type = (uint8_t)LOG_EV_LOST;
|
||
r.bus_mv = (int32_t)n; /* 丢了几条 */
|
||
r.chk = rec_chk(&r);
|
||
if (store_run(&r, 1u) != 1u) {
|
||
hal_crit_enter();
|
||
s_lost += n; /* 连这条都没写进去,欠着,下次再补 */
|
||
hal_crit_exit();
|
||
return 0u;
|
||
}
|
||
return 1u;
|
||
}
|
||
|
||
static uint32_t pop(evlog_rec_t *dst, uint32_t want)
|
||
{
|
||
hal_crit_enter();
|
||
uint32_t n = (want < s_count) ? want : s_count;
|
||
for (uint32_t i = 0; i < n; i++) {
|
||
dst[i] = s_ring[(s_tail + i) % EVLOG_RAM_CAP];
|
||
}
|
||
s_tail = (s_tail + n) % EVLOG_RAM_CAP;
|
||
s_count -= n;
|
||
if (s_count > 0u) {
|
||
s_oldest_ms = s_ring[s_tail].t_ms;
|
||
}
|
||
hal_crit_exit();
|
||
return n;
|
||
}
|
||
|
||
bool evlog_due(void)
|
||
{
|
||
bool due;
|
||
hal_crit_enter();
|
||
due = (s_lost > 0u) || (s_count >= EVLOG_BATCH) ||
|
||
(s_count > 0u && hal_time_elapsed_ms(s_oldest_ms) >= EVLOG_MAX_AGE_MS);
|
||
hal_crit_exit();
|
||
return due;
|
||
}
|
||
|
||
uint32_t evlog_flush(uint32_t max)
|
||
{
|
||
if (!s_have_store) {
|
||
return 0u; /* 没有那块分区:事件留在 RAM 里,别把它们弄丢 */
|
||
}
|
||
const uint32_t budget = (max == 0u) ? EVLOG_RAM_CAP : max;
|
||
/* ⚠️ 补记的 LOST 不占预算:它是上一次落盘欠下的账,拖到下一次只会让
|
||
「日志里丢了一段」这件事更晚才被看见。最多超出预算 1 条。 */
|
||
uint32_t total = store_lost_marker();
|
||
|
||
uint32_t from_ring = 0;
|
||
evlog_rec_t buf[EVLOG_CHUNK];
|
||
while (from_ring < budget) {
|
||
uint32_t want = budget - from_ring;
|
||
if (want > EVLOG_CHUNK) {
|
||
want = EVLOG_CHUNK;
|
||
}
|
||
const uint32_t n = pop(buf, want);
|
||
if (n == 0u) {
|
||
break;
|
||
}
|
||
const uint32_t w = store_run(buf, n);
|
||
total += w;
|
||
from_ring += w;
|
||
if (w < n) {
|
||
/* 已经从环里取出来了却没写进去 —— 这些是真丢了,
|
||
记进 LOST,下一次落盘时它会以一条记录的形式出现在日志里。 */
|
||
hal_crit_enter();
|
||
s_dropped += (n - w);
|
||
s_lost += (n - w);
|
||
hal_crit_exit();
|
||
break;
|
||
}
|
||
}
|
||
return total;
|
||
}
|
||
|
||
/* ---- 读回 ---------------------------------------------------------------- */
|
||
|
||
uint32_t evlog_read(uint32_t from_seq, evlog_rec_t *out, uint32_t max)
|
||
{
|
||
if (!s_have_store || out == NULL || max == 0u) {
|
||
return 0u;
|
||
}
|
||
uint32_t seq = (from_seq < s_oldest_seq) ? s_oldest_seq : from_seq;
|
||
if (seq >= s_next_seq) {
|
||
return 0u;
|
||
}
|
||
uint32_t n = s_next_seq - seq;
|
||
if (n > max) {
|
||
n = max;
|
||
}
|
||
for (uint32_t i = 0; i < n; i++) {
|
||
const uint32_t s = seq + i;
|
||
const uint32_t epoch = s / s_recs;
|
||
const uint32_t slot = s % s_recs;
|
||
evlog_rec_t r;
|
||
memset(&r, 0, sizeof(r));
|
||
if (hal_store_read(rec_off(epoch % s_sectors, slot), &r, sizeof(r)) != HAL_OK ||
|
||
!rec_valid(&r)) {
|
||
/* 序号空间里的一个洞(写失败之后被放弃的槽)。**照样占一个位置**,
|
||
否则调用方的游标 `from_seq + n` 会对不上,拉日志会卡在洞上。 */
|
||
memset(&r, 0, sizeof(r));
|
||
r.type = (uint8_t)LOG_EV_NONE;
|
||
}
|
||
out[i] = r;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
/* ---- 启动:扫一遍 flash 找到接着写的位置 --------------------------------- */
|
||
|
||
static hal_err_t format_all(void)
|
||
{
|
||
for (uint32_t i = 0; i < s_sectors; i++) {
|
||
const hal_err_t err = hal_store_erase(i);
|
||
if (err != HAL_OK) {
|
||
return err;
|
||
}
|
||
}
|
||
const hal_err_t err = write_header(0u, 0u);
|
||
if (err != HAL_OK) {
|
||
return err;
|
||
}
|
||
s_epoch = 0;
|
||
s_slot = 0;
|
||
s_next_seq = 0;
|
||
s_oldest_seq = 0;
|
||
return HAL_OK;
|
||
}
|
||
|
||
hal_err_t evlog_init(void) { return evlog_init_ex(0); }
|
||
|
||
hal_err_t evlog_init_ex(uint8_t boot_arg)
|
||
{
|
||
/* RAM 侧无条件清干净:真板上这里刚上电,宿主机上这条让 `sim_hal_reboot()`
|
||
之后的行为和真重启一致(statics 在进程里是活着的)。 */
|
||
hal_crit_enter();
|
||
s_head = 0;
|
||
s_tail = 0;
|
||
s_count = 0;
|
||
s_oldest_ms = 0;
|
||
s_lost = 0;
|
||
hal_crit_exit();
|
||
s_queued = 0;
|
||
s_stored = 0;
|
||
s_dropped = 0;
|
||
s_io_errors = 0;
|
||
s_have_store = false;
|
||
s_sector_bytes = 0;
|
||
s_sectors = 0;
|
||
s_recs = 0;
|
||
s_epoch = 0;
|
||
s_slot = 0;
|
||
s_next_seq = 0;
|
||
s_oldest_seq = 0;
|
||
|
||
hal_err_t err = hal_store_init();
|
||
if (err != HAL_OK) {
|
||
/* 没有分区。**不 abort、不影响入队**:日志是排查手段,不是安全链
|
||
(evlog.h 上面那段)。事件照进 RAM,只是没人能把它们写下去。 */
|
||
evlog_put(0, LOG_EV_BOOT, boot_arg, 0, 0);
|
||
return err;
|
||
}
|
||
|
||
hal_store_geom_t g;
|
||
hal_store_geometry(&g);
|
||
if (g.sector_bytes < 2u * sizeof(evlog_rec_t) ||
|
||
(g.sector_bytes % sizeof(evlog_rec_t)) != 0u || g.sectors < 2u) {
|
||
s_have_store = false;
|
||
evlog_put(0, LOG_EV_BOOT, boot_arg, 0, 0);
|
||
return HAL_ERR_STATE;
|
||
}
|
||
s_sector_bytes = g.sector_bytes;
|
||
s_sectors = g.sectors;
|
||
s_recs = g.sector_bytes / (uint32_t)sizeof(evlog_rec_t) - 1u; /* 头占一个槽 */
|
||
s_have_store = true;
|
||
|
||
/* 17 个扇区头里 epoch 最大的那个 = 上次正在写的扇区。
|
||
`hdr_valid()` 顺带查了 `epoch % 扇区数 == 扇区号` —— 几何变了、
|
||
或者这块区被别人写过,都会在这里落空,然后整块重来。 */
|
||
bool found = false;
|
||
uint32_t best = 0;
|
||
for (uint32_t i = 0; i < s_sectors; i++) {
|
||
evlog_shdr_t h;
|
||
memset(&h, 0, sizeof(h));
|
||
if (hal_store_read(i * s_sector_bytes, &h, sizeof(h)) != HAL_OK) {
|
||
continue;
|
||
}
|
||
if (hdr_valid(&h, i) && (!found || h.epoch > best)) {
|
||
found = true;
|
||
best = h.epoch;
|
||
}
|
||
}
|
||
|
||
if (!found) {
|
||
err = format_all();
|
||
if (err != HAL_OK) {
|
||
s_io_errors++;
|
||
s_have_store = false;
|
||
}
|
||
} else {
|
||
s_epoch = best;
|
||
/* 扫到第一个校验不过的槽为止 —— 那就是上次写到的地方。
|
||
「校验不过」包含两种:擦干净的(0xFF)和被掉电切断的半条。 */
|
||
s_slot = 0;
|
||
for (uint32_t r = 0; r < s_recs; r++) {
|
||
evlog_rec_t rec;
|
||
memset(&rec, 0, sizeof(rec));
|
||
if (hal_store_read(rec_off(best % s_sectors, r), &rec, sizeof(rec)) != HAL_OK ||
|
||
!rec_valid(&rec)) {
|
||
break;
|
||
}
|
||
s_slot = r + 1u;
|
||
}
|
||
s_next_seq = best * s_recs + s_slot;
|
||
s_oldest_seq = (best >= s_sectors) ? (best - s_sectors + 1u) * s_recs : 0u;
|
||
}
|
||
|
||
/* §10.7「上电从 0 计」⇒ 时间戳会跳回去。没有这一条的话,重启前后的记录
|
||
在时间轴上互相穿插,而日志里看不出重启发生过。 */
|
||
evlog_put(0, LOG_EV_BOOT, boot_arg, 0, 0);
|
||
return s_have_store ? HAL_OK : HAL_ERR_STATE;
|
||
}
|
||
|
||
/* ---- 杂项 ---------------------------------------------------------------- */
|
||
|
||
void evlog_stats(evlog_stats_t *st)
|
||
{
|
||
if (st == NULL) {
|
||
return;
|
||
}
|
||
hal_crit_enter();
|
||
st->queued = s_queued;
|
||
st->stored = s_stored;
|
||
st->dropped = s_dropped;
|
||
st->pending = s_count;
|
||
hal_crit_exit();
|
||
st->io_errors = s_io_errors;
|
||
st->oldest_seq = s_oldest_seq;
|
||
st->next_seq = s_next_seq;
|
||
}
|
||
|
||
uint32_t evlog_capacity(void) { return s_have_store ? s_sectors * s_recs : 0u; }
|
||
|
||
const char *evlog_ev_str(log_ev_t ev)
|
||
{
|
||
switch (ev) {
|
||
case LOG_EV_NONE: return "(空)";
|
||
case LOG_EV_BOOT: return "上电";
|
||
case LOG_EV_LOST: return "事件丢失";
|
||
case LOG_EV_OVER_VOLT: return "过压关路";
|
||
case LOG_EV_UNDER_VOLT: return "欠压关路";
|
||
case LOG_EV_REVERSE: return "反灌断VBUS";
|
||
case LOG_EV_PUSHED_UP: return "顶高断VBUS";
|
||
case LOG_EV_EFUSE_FAULT: return "eFuse故障";
|
||
case LOG_EV_PG_CHANGE: return "PG变化";
|
||
case LOG_EV_I2C_LOST: return "I2C失联";
|
||
case LOG_EV_THERMAL_DERATE: return "过温降载";
|
||
case LOG_EV_FAN_STALL: return "风扇停转";
|
||
case LOG_EV_REBOOT: return "重启序列";
|
||
case LOG_EV_CMD_REJECT: return "命令被拒";
|
||
case LOG_EV_MUX_LOST: return "mux失联全断";
|
||
case LOG_EV_SLOT_EMPTY: return "刀片不在位";
|
||
case LOG_EV_PD_LIMIT: return "PD限功率";
|
||
case LOG_EV_PG_TIMEOUT: return "上电等不到PG";
|
||
case LOG_EV_OTA: return "固件升级";
|
||
case LOG_EV_COUNT: break;
|
||
}
|
||
return "?";
|
||
}
|