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(
|
||||
"tasks.c"
|
||||
"t_safety.c"
|
||||
"t_actuator.c"
|
||||
"evlog.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES phonerail_hal drv)
|
||||
|
||||
+42
-6
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "board_pins.h"
|
||||
#include "esp_log.h"
|
||||
#include "evlog.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "hal_i2c.h"
|
||||
@@ -70,12 +71,14 @@ static bool act_pg_from_safety(void *ctx, int rail, bool *pg)
|
||||
return true;
|
||||
}
|
||||
|
||||
/* A10 的事件出口。**A11 会把这里换成环形缓冲的入队**(§10.7:`t_safety` 只入队、
|
||||
不落盘),在那之前先打到日志上 —— 一条也不留才是最坏的结果。
|
||||
⚠️ 这个回调是在断电之后、不持任何锁的情况下被调的,所以里面不许阻塞。 */
|
||||
/* A10 的事件出口,A11 把它接到了环形缓冲上(§10.7:`t_safety` 只入队、不落盘)。
|
||||
`evlog_put_safety()` 自己就是 `safety_ev_fn` 的形状,这里包一层只为了顺带
|
||||
打一行串口日志 —— 板子插着 USB 调试时,事件同时进日志流与 flash。
|
||||
⚠️ 这个回调是在断电之后、不持任何锁的情况下被调的:**入队在前,打印在后**。
|
||||
`ESP_LOGW` 会去抢串口,把它放在入队之前就是在事件最要紧的那一刻多等一会儿。 */
|
||||
static void log_safety_event(void *ctx, const safety_event_t *ev)
|
||||
{
|
||||
(void)ctx;
|
||||
evlog_put_safety(ctx, ev);
|
||||
ESP_LOGW(TAG, "安全事件 t=%u ms CH%u %s V=%d mV I=%d uA", (unsigned)ev->t_ms,
|
||||
(unsigned)ev->rail, t_safety_ev_str((safety_ev_t)ev->type),
|
||||
(int)ev->bus_mv, (int)ev->current_ua);
|
||||
@@ -145,12 +148,29 @@ static void task_link(void *arg)
|
||||
}
|
||||
}
|
||||
|
||||
/* A11:唯一碰 flash 的任务。§10.7「`t_log` 空闲时批量写」——
|
||||
醒得比攒批周期密(100 ms),但 `evlog_due()` 说不用写就立刻回去睡:
|
||||
代价是每秒 10 次几十条指令的问询,换来的是掉电丢失窗口的上界就是
|
||||
`EVLOG_MAX_AGE_MS`,而不是「上一次醒到下一次醒」。 */
|
||||
static void task_log(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
evlog_stats_t st;
|
||||
uint32_t reported = 0;
|
||||
for (;;) {
|
||||
/* A11:事件环形缓冲落盘。 */
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
if (evlog_due()) {
|
||||
(void)evlog_flush(EVLOG_FLUSH_MAX);
|
||||
}
|
||||
/* 丢过事件就报出来。§10.7 的日志是排查排线接触不良的唯一线索,
|
||||
而「日志自己丢过东西」这件事必须比日志本身更显眼。 */
|
||||
evlog_stats(&st);
|
||||
if (st.dropped != reported) {
|
||||
reported = st.dropped;
|
||||
ESP_LOGW(TAG, "事件缓冲丢过 %u 条(落盘 %u / 入队 %u,IO 错 %u)",
|
||||
(unsigned)st.dropped, (unsigned)st.stored, (unsigned)st.queued,
|
||||
(unsigned)st.io_errors);
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +223,22 @@ void app_main(void)
|
||||
而没 init 的执行器只会直接返回,那时序列还在跑,电关掉几秒又会被抬回来。 */
|
||||
t_actuator_init(act_pg_from_safety, NULL);
|
||||
|
||||
/* 事件日志(§10.7)。**要在接事件出口之前 init**,否则第一条事件进的是
|
||||
一个还没扫过 flash 的缓冲。
|
||||
⚠️ 失败**不 abort**:没有 `evlog` 分区时日志落不了盘,但五路照供电
|
||||
—— 日志是排查手段,不是安全链的一环(§10.2)。这与 `selftest_or_die()`
|
||||
的区别在于:那里错的是「命令会发到哪一路」,这里错的只是「记不下来」。 */
|
||||
err = evlog_init();
|
||||
if (err != HAL_OK) {
|
||||
ESP_LOGE(TAG, "事件日志落盘不可用:%s —— 事件只留在 RAM 里", hal_err_str(err));
|
||||
} else {
|
||||
evlog_stats_t st;
|
||||
evlog_stats(&st);
|
||||
ESP_LOGI(TAG, "事件日志:容量 %u 条,flash 里已有 %u~%u",
|
||||
(unsigned)evlog_capacity(), (unsigned)st.oldest_seq,
|
||||
(unsigned)st.next_seq);
|
||||
}
|
||||
|
||||
/* 事件出口。同样要在巡检起来之前接上,否则第一批事件没人接。 */
|
||||
t_safety_set_event_sink(log_safety_event, NULL);
|
||||
|
||||
|
||||
@@ -0,0 +1,570 @@
|
||||
/* 事件日志的实现。接口与「为什么这么分层」在 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_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)
|
||||
{
|
||||
/* 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, 0, 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, 0, 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, 0, 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_COUNT: break;
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/* 事件日志(需求 §10.7)——「RAM 环形缓冲 + 批量落盘 + 按序号增量拉取」。
|
||||
|
||||
§10.7 的第一句话是这一节全部设计的来源:**记录路径不能依赖上位机在线**。
|
||||
排线接触不良、手机把电压顶回来、eFuse 断了 —— 这些事发生时,
|
||||
最可能同时发生的就是「WiFi 也断了」。所以事件先进 RAM,再由最低优先级的
|
||||
`t_log` 攒够一批写进 flash;上位机什么时候连上来,什么时候按序号拉走。
|
||||
|
||||
---- 三层,各自只干一件事 ------------------------------------------------
|
||||
1. **入队**(`evlog_put*`):谁产生事件谁调。几条指令,临界区里做完,
|
||||
**绝不阻塞、绝不碰 flash**。t_safety 是在断电之后调它的(t_safety.h)。
|
||||
2. **落盘**(`evlog_flush`):只有 `t_log` 调。它是唯一碰 flash 的人。
|
||||
3. **读回**(`evlog_read`):按全局序号取,给 A12 的 `events 起始序号` 用。
|
||||
|
||||
---- 为什么序号不在入队时分配 --------------------------------------------
|
||||
序号是**记录在 flash 里的位置**算出来的(`epoch × 每扇区条数 + 槽号`),
|
||||
不是一个自增计数器。这样一来「序号」和「flash 里第几条」永远是同一件事,
|
||||
重启之后接着数也不用把计数器存起来。代价是**没落盘的事件还没有序号**:
|
||||
`evlog_read()` 只服务已经落盘的部分,要看最新的就先 `evlog_flush(0)`。
|
||||
反过来做(入队时发号)会在 RAM 缓冲溢出丢事件时留下一段永远补不上的空号,
|
||||
而上位机那边的现象是「拉日志卡在某个序号上不动」。
|
||||
|
||||
---- 丢事件是可以的,**悄悄丢**不行 --------------------------------------
|
||||
RAM 缓冲满了就覆盖最旧的(§10.7 的 4096 条同理:满了盖最旧,最新的一条
|
||||
永远进得去)。被盖掉的那些会在下一次落盘时留下一条 `LOG_EV_LOST`,
|
||||
`bus_mv` 字段记着丢了多少条。一份会**安静地**少掉几行的日志,
|
||||
比没有日志更糟 —— 排查的人会拿它当完整的时间线看。 */
|
||||
#ifndef EVLOG_H
|
||||
#define EVLOG_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
#include "t_safety.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- 事件类型 ------------------------------------------------------------
|
||||
⚠️⚠️ **这些数字是 flash 里的格式,不是一个内部枚举。** 板子上跑了半年的
|
||||
日志是按数字存的,改一个值就等于把历史记录全部重新解释了一遍,
|
||||
而且**读出来仍然是合法的记录**,没有任何一处会报错。
|
||||
只许在末尾追加,不许插入、不许重排、不许删除。
|
||||
`test_flash_format_is_frozen()` 把每一个数字都钉死了。 */
|
||||
typedef enum {
|
||||
LOG_EV_NONE = 0,
|
||||
LOG_EV_BOOT = 1, /* 每次上电的第一条。§10.7「上电从 0 计」⇒ 时间戳会跳回去,
|
||||
没有这一条的话,重启前后的记录混在一起没法读 */
|
||||
LOG_EV_LOST = 2, /* RAM 缓冲溢出,丢了 bus_mv 条(见文件头) */
|
||||
|
||||
/* A10 的四条判据事件。⚠️ 这里的数字与 `safety_ev_t` 的**不一样**,
|
||||
两边靠 `evlog_from_safety()` 里那个显式 switch 对应 —— 不许写成加个偏移。 */
|
||||
LOG_EV_OVER_VOLT = 3, /* SR-5:高于设定 >100 mV 持续 20 ms → 关这一路 */
|
||||
LOG_EV_UNDER_VOLT = 4, /* SR-5:低于设定 >100 mV 持续 20 ms → 关这一路 */
|
||||
LOG_EV_REVERSE = 5, /* SW-8:电流转负 → 断这一路 VBUS */
|
||||
LOG_EV_PUSHED_UP = 6, /* SW-8:VBUS 期间电压被顶高 → 断这一路 VBUS */
|
||||
|
||||
/* §10.7 必记事件里**还没人产生**的那几类。号先占住(见上面那条警告),
|
||||
产生它们的代码各归各的包:括号里是包号。 */
|
||||
LOG_EV_EFUSE_FAULT = 7, /* eFuse FLT# 拉低(A15) */
|
||||
LOG_EV_PG_CHANGE = 8, /* PGOOD 变化,arg = 新电平(A15) */
|
||||
LOG_EV_I2C_LOST = 9, /* I²C 器件失联,arg = 地址(A15) */
|
||||
LOG_EV_THERMAL_DERATE = 10,/* 过温降载,bus_mv = 温度 m°C(A16) */
|
||||
LOG_EV_FAN_STALL = 11, /* 风扇停转(A16) */
|
||||
LOG_EV_REBOOT = 12, /* 每次重启序列,arg = 软/硬 + 谁下的命令(A9/A12) */
|
||||
LOG_EV_CMD_REJECT = 13, /* 越界命令被拒,arg = 命令号(A12) */
|
||||
|
||||
LOG_EV_COUNT,
|
||||
} log_ev_t;
|
||||
|
||||
/* §10.7 的「单条」:时间戳 + 路号 + 事件类型 + 当时的 V/I,正好 16 B。
|
||||
⚠️ 这个结构体的**内存布局就是 flash 布局**,直接按字节写下去的。
|
||||
加字段 = 换格式(见上),改字段顺序同理。 */
|
||||
typedef struct {
|
||||
uint32_t t_ms; /* 本次上电起算的毫秒(§10.7:上位机连上后下发墙钟做偏移) */
|
||||
uint8_t rail; /* 1~5;0 = 整板级事件(BOOT / LOST / 风扇 …) */
|
||||
uint8_t type; /* log_ev_t */
|
||||
uint8_t arg; /* 事件相关的小参数,没有就 0 */
|
||||
uint8_t chk; /* 另外 15 个字节的 CRC-8。见 evlog.c「为什么要校验」 */
|
||||
int32_t bus_mv; /* 出事那一刻的实测值,不是设定值 */
|
||||
int32_t current_ua;
|
||||
} evlog_rec_t;
|
||||
|
||||
/* RAM 环形缓冲的条数。§10.7 写的是「≥256 条」。
|
||||
256 × 16 B = 4 KB —— 在 512 KB 的内部 RAM 里可以忽略,而它换来的是
|
||||
「t_log 被饿住 4 秒(按 §10.7 的量级远不止)也不丢事件」。 */
|
||||
#define EVLOG_RAM_CAP 256
|
||||
|
||||
/* 攒够这么多条就落盘一次。一次 flash 写事务的开销与条数几乎无关,
|
||||
16 条一批 ⇒ 每条事件的擦写摊薄到 1/16。 */
|
||||
#define EVLOG_BATCH 16
|
||||
|
||||
/* 但最老的那一条最多等这么久 —— 攒批不能把「掉电前记下来」这件事攒没了。
|
||||
1 s 是 §10.3 里 t_log「空闲时」的量级,也是掉电丢失窗口的上界。 */
|
||||
#define EVLOG_MAX_AGE_MS 1000
|
||||
|
||||
/* 一次 `evlog_flush()` 最多写这么多条。t_log 是最低优先级,长时间占着 flash
|
||||
本身没有危害,但一次写 4000 条会让**这个函数**变成一个几百毫秒的黑洞,
|
||||
而它是要被 A12 的命令处理器同步调用的(拉日志之前先 flush)。 */
|
||||
#define EVLOG_FLUSH_MAX 64
|
||||
|
||||
typedef struct {
|
||||
uint32_t queued; /* 累计入队几条 */
|
||||
uint32_t stored; /* 累计落盘几条(含补记的 LOST) */
|
||||
uint32_t dropped; /* 累计被覆盖掉几条(RAM 缓冲溢出) */
|
||||
uint32_t pending; /* 此刻还在 RAM 里没落盘的条数 */
|
||||
uint32_t io_errors;/* flash 写/擦失败次数 */
|
||||
uint32_t oldest_seq; /* flash 里还留着的最老一条的序号 */
|
||||
uint32_t next_seq; /* 下一条落盘的记录会拿到的序号 */
|
||||
} evlog_stats_t;
|
||||
|
||||
/* 扫一遍 flash 找到接着写的位置,并**入队一条 `LOG_EV_BOOT`**。
|
||||
幂等:重复调等于「又重启了一次」,会再记一条 BOOT。
|
||||
返回 HAL_ERR_STATE = 没有那块分区(ESP 上是分区表漏了 `evlog`)——
|
||||
⚠️ 调用方**不要因此 abort**:日志是排查手段,不是安全链的一环(§10.2)。
|
||||
这种情况下入队照常工作(事件留在 RAM 里),只是落不了盘。 */
|
||||
hal_err_t evlog_init(void);
|
||||
|
||||
/* 入队一条。**不阻塞、不碰 flash、不会失败** —— 缓冲满了就盖掉最旧的一条
|
||||
并记一次 `dropped`(下一次落盘时会补一条 LOST)。
|
||||
任何任务、任何优先级都可以调,包括 t_safety 断电之后的那一下。 */
|
||||
void evlog_put(uint8_t rail, log_ev_t type, uint8_t arg, int32_t bus_mv, int32_t ua);
|
||||
|
||||
/* A10 的事件出口。直接 `t_safety_set_event_sink(evlog_put_safety, NULL)`。
|
||||
⚠️ 时间戳用的是**事件里带的那个**(判据记的是断电那一刻),不是现在。 */
|
||||
void evlog_put_safety(void *ctx, const safety_event_t *ev);
|
||||
|
||||
/* `safety_ev_t` → `log_ev_t`。落盘格式与判据的枚举是两件事,见 log_ev_t 上的警告。
|
||||
认不出来的返回 LOG_EV_NONE(不会发生;发生了也不该把这条记录扔掉)。 */
|
||||
log_ev_t evlog_from_safety(uint8_t safety_ev);
|
||||
|
||||
/* 该落盘了吗:攒够 `EVLOG_BATCH` 条,或者最老的一条等了 `EVLOG_MAX_AGE_MS`。
|
||||
t_log 每一圈问一次,false 就接着睡。 */
|
||||
bool evlog_due(void);
|
||||
|
||||
/* 把 RAM 里的写进 flash。max = 0 表示「现在有多少写多少」(上界就是缓冲的
|
||||
256 条);t_log 每圈传的是 `EVLOG_FLUSH_MAX`。返回**真的写进 flash 几条**
|
||||
—— 含一条补记的 LOST,所以它可能比预算多 1(那条是上次欠下的账)。
|
||||
只有 t_log 与 A12 的拉日志命令调它 —— 它是**唯一**碰 flash 的地方。 */
|
||||
uint32_t evlog_flush(uint32_t max);
|
||||
|
||||
/* 从 from_seq 起,按序号读回**已落盘**的记录(没落盘的先 flush,见文件头)。
|
||||
返回真的读到几条。from_seq 比 `oldest_seq` 小时从 `oldest_seq` 开始
|
||||
—— 上位机据此就能发现「中间那一段已经被环形缓冲盖掉了」。 */
|
||||
uint32_t evlog_read(uint32_t from_seq, evlog_rec_t *out, uint32_t max);
|
||||
|
||||
void evlog_stats(evlog_stats_t *st);
|
||||
|
||||
/* flash 里最多能留几条(§10.7 要求 ≥4096)。没有分区时返回 0。 */
|
||||
uint32_t evlog_capacity(void);
|
||||
|
||||
/* 只用于日志与测试失败信息,返回静态字符串,不会是 NULL。 */
|
||||
const char *evlog_ev_str(log_ev_t ev);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* EVLOG_H */
|
||||
@@ -3,7 +3,7 @@
|
||||
#define PHONERAIL_H
|
||||
|
||||
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
||||
#define PHONERAIL_FW_VERSION "0.1.0-a10"
|
||||
#define PHONERAIL_FW_VERSION "0.1.0-a11"
|
||||
|
||||
/* 通道数。五路直供,见设计文档 §2。 */
|
||||
#define PHONERAIL_RAIL_COUNT 5
|
||||
|
||||
在新工单中引用
屏蔽一个用户