From ac3fb512d5634118bf3eec2c236a560ac032ea84 Mon Sep 17 00:00:00 2001 From: wangfq Date: Tue, 4 Aug 2026 15:04:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(vd960DBN):=20=E8=84=B1=E6=9C=BA=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E6=97=A5=E5=BF=97=E7=B3=BB=E7=BB=9F=20(W25Q32=20?= =?UTF-8?q?=E7=8E=AF=E5=BD=A2)=20=E2=80=94=20=E8=A7=A3=E5=86=B3=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E4=B8=8A=E7=BA=BF=E5=8F=96=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景: 设备挂平台测试出现重复上线(现场未断电), 无本地日志无法区分 设备真复位 vs MQTT 断连重连。iot_handle_suback 每次重连都发 initialize 是重复上线的直接证据, 日志需能区分二者。 实现 (V3.6, ROADMAP P1.2 事件流先行): - offlog.c/h: W25Q32 256KB 环形事件日志 (头扇区+63数据扇区, 8064条) - 32B 定长记录: magic/type/len/flags/seq/ts_ms/unix_ts/boot_seq/payload[12] - 8 类事件: BOOT(复位原因)/IOT_CONNECT/READY/DISCONN/RECONN/ EVT_RETRY/GIVEUP/COIL/TIME_ANCHOR/LOG_CLEAR - 掉电恢复: 头扇区写指针锚点 + 上电 seq 连续性扫描 - 编译期断言防 32B padding 回归 - 插桩 (全部主循环上下文, socket 中断内不写 SPI): - main(): offlog_init + RCC_RSTSCKR 复位原因采集/清除 - iot_mqtt_poll(): MQTT 状态沿检测 (CONNECT/READY/DISCONN) - 重连退避/TCP超时/CONNACK拒绝: RECONN/DISCONN(4)/(3) - iot_evt_process/enqueue: EVT_RETRY/GIVEUP/COIL - net_srv.c dev_time_sync: TIME_ANCHOR 时钟同步锚点 - tests/test_offlog.c: gcc 隔离单测 6 例 (mock W25Q32 NOR 语义) 关键坑: ①中断内写SPI阻塞 ②结构体36B padding致环形错乱 ③扇区级覆盖粒度count扣减语义 (均单测抓出) 待办: P1.3 导出命令(log_query/stat/clear) + 快照流 + 复位原因板上验证 --- .../APP/include/offlog.h | 131 +++++++ .../APP/include/storage.h | 1 + .../APP/iot_mqtt_srv.c | 21 + .../OnlyUpdateApp_Peripheral/APP/net_srv.c | 2 + .../BLE/OnlyUpdateApp_Peripheral/APP/offlog.c | 366 ++++++++++++++++++ .../APP/peripheral_main.c | 11 + vd960DBN/docs/devlog.md | 70 ++++ vd960DBN/tests/test_offlog.c | 234 +++++++++++ 8 files changed, 836 insertions(+) create mode 100644 vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h create mode 100644 vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/offlog.c create mode 100644 vd960DBN/tests/test_offlog.c diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h new file mode 100644 index 0000000..26e8de7 --- /dev/null +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h @@ -0,0 +1,131 @@ +/** + ****************************************************************************** + * @file offlog.h + * @author wfq + * @version V1.0 + * @date 2026-08-04 + * @brief DBN 脱机事件日志 (W25Q32 外部 SPI NOR, 环形) + * + * 目的: 解决"平台重复上线/异常重启但现场无日志"的取证问题。 + * 区分「设备真复位」vs「MQTT 断连重连」(两者都会导致平台重收 initialize): + * - 真复位: 会有 OFFLOG_EVT_BOOT 事件, boot_seq 递增 + * - 断连重连: boot_seq 不变, 只有 IOT_* 网络事件 + * + * 分区 (W25Q32, 4MB): + * | 参数区 0x000000 64KB (现有配置, 不动) + * | 事件区 0x010000 256KB = 扇区 16..79 (头扇区 + 63 数据扇区) + * | OTA 区 0x050000 512KB (预留) + * | 快照区 0x0D0000 ~3.25MB (预留, 后续) + * + * 环形实现: + * - 扇区 0 (区内) = 头扇区, 存元数据 (boot_seq/写位置/条数), 扇区切换时更新 + * - 扇区 1..63 = 数据扇区, 每扇区 128 条 x 32B 定长记录 + * - 顺序写, 写满擦下一扇区 (环形覆盖), 天然磨损均衡 + * - 上电从头部记录的写位置向后扫描, 找第一条无效记录 = 真实写位置 (掉电恢复) + * + * 记录格式 (32B 定长): + * magic(1) type(1) len(1) flags(1) seq(4) boot_seq(2) rsvd(2) + * ts_ms(4) unix_ts(4) payload(14) + ****************************************************************************** + */ + +#ifndef _OFFLOG_H__ +#define _OFFLOG_H__ + +#include + +/*=========================================================================== + * 分区与容量 + *===========================================================================*/ +#define OFFLOG_AREA_BASE 0x010000UL /* 事件日志区起始 (绝对地址) */ +#define OFFLOG_AREA_SIZE 0x040000UL /* 256KB */ +#define OFFLOG_HEAD_SECTOR 0 /* 区内扇区号: 头扇区 */ +#define OFFLOG_DATA_SECTOR_CNT 63 /* 数据扇区数 (1..63) */ +#define OFFLOG_EVT_PER_SECTOR 128 /* 4096 / 32 */ + +#define OFFLOG_DATA_BASE (OFFLOG_AREA_BASE + 4096) +#define OFFLOG_DATA_SIZE (OFFLOG_DATA_SECTOR_CNT * 4096) +#define OFFLOG_MAX_RECORDS (OFFLOG_DATA_SECTOR_CNT * OFFLOG_EVT_PER_SECTOR) /* 8064 */ + +/* 头扇区记录 (32B, 扇区切换时更新) */ +typedef struct { + uint8_t magic[4]; /* "OFFL" */ + uint32_t boot_seq; /* 上次上电序号 (本 boot 由 init 时 +1) */ + uint32_t wr_sector; /* 上次写扇区号 (区内 1..63) */ + uint32_t wr_off; /* 上次写偏移 (相对 OFFLOG_AREA_BASE) */ + uint32_t wr_seq; /* 下一条记录全局 seq (掉电恢复锚点) */ + uint32_t count; /* 有效记录数 (0..8064) */ + uint32_t rsvd[2]; +} OfflogHead; /* 28B, 按 32B 对齐使用 */ + +#define OFFLOG_HEAD_MAGIC0 'O' +#define OFFLOG_HEAD_MAGIC1 'F' +#define OFFLOG_HEAD_MAGIC2 'L' +#define OFFLOG_HEAD_MAGIC3 'L' + +/* 事件记录 (32B 定长, 字段重排保证自然对齐无 padding: + 4×uint8 → uint32×3 → uint16×2 → payload[12] = 4+12+4+12 = 32) */ +typedef struct { + uint8_t magic; /* 0xA5 */ + uint8_t type; /* OFFLOG_EVT_xxx */ + uint8_t len; /* payload 有效字节数 (0..12) */ + uint8_t flags; /* bit0: unix_ts 有效 */ + uint32_t seq; /* 全局序号 (跨 boot 递增) */ + uint32_t ts_ms; /* boot 内相对时间 mstick() */ + uint32_t unix_ts; /* 已同步 Unix 秒 (flags.bit0=1 时有效) */ + uint16_t boot_seq; /* 启动序号 */ + uint16_t rsvd; + uint8_t payload[12]; /* 事件参数 */ +} OfflogEvt; /* 32B, 无 padding */ + +#define OFFLOG_EVT_MAGIC 0xA5 +#define OFFLOG_EVT_SIZE sizeof(OfflogEvt) +#define OFFLOG_UNIX_VALID_FLAG 0x01 + +/*=========================================================================== + * 事件类型 + *===========================================================================*/ +enum { + OFFLOG_EVT_BOOT = 0x01, /* 上电/复位; payload[0..3]=复位原因寄存器 RCC_RSTSCKR */ + OFFLOG_EVT_IOT_CONNECT = 0x10, /* MQTT TCP 连接成功 (socket 中断) */ + OFFLOG_EVT_IOT_READY = 0x11, /* MQTT SUBACK → 发 initialize (重复上线直接原因!) */ + OFFLOG_EVT_IOT_DISCONN = 0x12, /* MQTT 断连; payload[0]=原因(1=断开 2=超时 3=CONNACK拒绝 4=连接超时) */ + OFFLOG_EVT_IOT_RECONN = 0x13, /* 重连退避; payload[0..3]=退避 ms */ + OFFLOG_EVT_EVT_RETRY = 0x30, /* event_report ACK 超时重发; payload[0..3]=msg_id, payload[4]=retry */ + OFFLOG_EVT_EVT_GIVEUP = 0x31, /* event_report 重试耗尽挂起; payload[0..3]=msg_id */ + OFFLOG_EVT_COIL = 0x40, /* 线圈事件; payload[0]=sub(1=进 2=出 3=断开 4=恢复), payload[1]=ch, payload[2..5]=value(50ms) */ + OFFLOG_EVT_TIME_ANCHOR = 0x50, /* 时钟同步锚点; unix_ts=同步值 */ + OFFLOG_EVT_LOG_CLEAR = 0x70, /* 日志清除 (审计) */ +}; + +/* 复位原因位 (RCC_RSTSCKR, STM32F1/CH32V20x 兼容布局, 待板上验证) */ +#define OFFLOG_RST_IWDG (1UL << 31) /* 独立看门狗复位 (IWDG 超时 → 主循环卡死!) */ +#define OFFLOG_RST_WWDG (1UL << 30) /* 窗口看门狗复位 */ +#define OFFLOG_RST_LPWR (1UL << 29) /* 低功耗复位 */ +#define OFFLOG_RST_PIN (1UL << 26) /* NRST 引脚复位 */ +#define OFFLOG_RST_POR (1UL << 25) /* 上电/掉电复位 (真断电!) */ +#define OFFLOG_RST_SFT (1UL << 24) /* 软件复位 NVIC_SystemReset */ + +/*=========================================================================== + * API + *===========================================================================*/ +void offlog_init(void); /* 上电初始化 (扫描恢复) + 内部准备 BOOT 事件 */ +void offlog_boot(uint32_t reset_reason); /* 记录 BOOT 事件 (带复位原因) */ + +void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len); + +void offlog_iot_connect(void); +void offlog_iot_ready(void); +void offlog_iot_disconnect(uint8_t reason); +void offlog_iot_reconn(uint32_t backoff_ms); +void offlog_evt_retry(uint32_t msg_id, uint8_t retry); +void offlog_evt_giveup(uint32_t msg_id); +void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value); +void offlog_time_anchor(uint32_t unix_ts); + +uint32_t offlog_boot_seq(void); /* 当前 boot 序号 */ +uint16_t offlog_count(void); /* 有效记录数 */ +int offlog_read_idx(uint16_t idx, OfflogEvt *out); /* 按逻辑序号读, 0=成功 -1=越界 */ +void offlog_clear(void); /* 清空事件区 (清空后写 LOG_CLEAR 审计) */ + +#endif /* _OFFLOG_H__ */ diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/storage.h b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/storage.h index e611ea8..d0d0bf7 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/storage.h +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/storage.h @@ -32,6 +32,7 @@ void load_cfg_from_flash(void); void SPI_Flash_Read(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t size); void SPI_Flash_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size); +void SPI_Flash_Write_NoCheck(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size); void storage_init(void); diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c index 1303886..faae5cf 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c @@ -23,6 +23,7 @@ #include "simple_json.h" #include "tcp_json_srv.h" #include "storage.h" +#include "offlog.h" #include "ch32v20x_iwdg.h" #include #include @@ -37,6 +38,7 @@ extern ReportConfig g_report_cfg; // from tcp_json_srv.c extern uint8_t SocketId_TCP; // from net_srv.c, MQTT socket created by WCHNET_CreateTcpMqttSocket uint8_t g_iot_socket = 0xFF; // MQTT TCP socket ID IotMqttState g_iot_state = IOT_STATE_DISCONNECTED; +static IotMqttState _prev_iot_state = IOT_STATE_DISCONNECTED; // 事件日志: 状态沿检测 uint8_t g_iot_msg_id = 0; // MQTT packet identifier static uint32_t _iot_last_heartbeat = 0; // 上次 PINGREQ 时刻 (ms), MQTT 保活 static uint32_t _iot_reconnect_deadline = 0; @@ -108,6 +110,8 @@ static void iot_evt_enqueue(uint8_t type, uint8_t ch, uint32_t value) { IotEvent *e = &_evt_queue[(_evt_head + _evt_count) % IOT_EVT_QUEUE_DEPTH]; e->type = type; e->ch = ch; e->value = value; _evt_count++; + /* 事件日志: 线圈事件 (主循环上下文) */ + offlog_coil((uint8_t)type + 1, ch, value); /* sub: 1=进 2=出 3=断开 4=恢复 */ } _evt_gaveup = 0; // 新事件到达 → 解除挂起, 触发合并上报 PRINT("EVT: enqueue type=%d ch=%d val=%lu (cnt=%d)\n", type, ch, value, _evt_count); @@ -242,11 +246,13 @@ static void iot_evt_process(void) { if (_evt_retry >= IOT_EVT_MAX_RETRY) { PRINT("EVT: retry exhausted msg_id=%lu, hold %d events\n", _evt_pend_id, _evt_count); + offlog_evt_giveup(_evt_pend_id); // 事件日志: 重试耗尽 _evt_pend_id = 0; _evt_gaveup = 1; // 挂起: 新事件或重连时再触发 return; } _evt_retry++; + offlog_evt_retry(_evt_pend_id, _evt_retry); // 事件日志: ACK 超时重发 iot_evt_send(_evt_pend_id, _evt_pend_ts, _evt_pend_n); // 同 id/ts 重发 _evt_sent_ms = now; return; @@ -613,6 +619,7 @@ static void iot_process_recv(void) { iot_mqtt_send_subscribe(); } else { PRINT("IOT: CONNACK rejected, rc=%d\n", connack_rc); + offlog_iot_disconnect(3); // 事件日志: broker 拒绝 g_iot_state = IOT_STATE_DISCONNECTED; } } @@ -912,6 +919,18 @@ void iot_watchdog_kick(void) { void iot_mqtt_poll(void) { iot_watchdog_kick(); // 喂硬件 IWDG + /* 事件日志: MQTT 状态沿检测 (主循环上下文, 不在 socket 中断里写 SPI) + 真复位 → 会有 BOOT 事件 + boot_seq 递增; 仅断连重连 → 只有以下网络事件 */ + if (g_iot_state != _prev_iot_state) { + switch (g_iot_state) { + case IOT_STATE_MQTT_CONNECTED: offlog_iot_connect(); break; // TCP+CONNACK 成功 + case IOT_STATE_READY: offlog_iot_ready(); break; // SUBACK → 将发 initialize + case IOT_STATE_DISCONNECTED: offlog_iot_disconnect(1); break; // 断连(细分原因在各分支单独记) + default: break; + } + _prev_iot_state = g_iot_state; + } + /* 断线重连 */ if (g_iot_state == IOT_STATE_DISCONNECTED && g_iot_socket == 0xFF) { if (_iot_reconnect_deadline == 0 || mstick() > _iot_reconnect_deadline) { @@ -926,6 +945,7 @@ void iot_mqtt_poll(void) { _iot_reconnect_backoff = IOT_MQTT_RECONNECT_MAX_MS; } _iot_reconnect_deadline = mstick() + _iot_reconnect_backoff; + offlog_iot_reconn(_iot_reconnect_backoff); // 事件日志: 重连退避 PRINT("IOT: reconnect in %lu ms\n", _iot_reconnect_backoff); } } @@ -936,6 +956,7 @@ void iot_mqtt_poll(void) { if (g_iot_state == IOT_STATE_TCP_CONNECTING) { if (mstick() - _iot_connect_start > 10000) { PRINT("IOT: TCP connect timeout\n"); + offlog_iot_disconnect(4); // 事件日志: 连接超时 WCHNET_SocketClose(g_iot_socket, TCP_CLOSE_NORMAL); g_iot_socket = 0xFF; g_iot_state = IOT_STATE_DISCONNECTED; diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/net_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/net_srv.c index 2361570..d1a7cd5 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/net_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/net_srv.c @@ -19,6 +19,7 @@ #include "simple_json.h" #include "tcp_json_srv.h" #include "iot_mqtt_srv.h" +#include "offlog.h" uint32_t slen; @@ -97,6 +98,7 @@ void dev_time_sync(uint32_t unix_ts) if (unix_ts < 1600000000UL) return; // 合法性门槛(>2020-09), 挡掉上电秒数/0/异常 _dev_time_base_unix = unix_ts; _dev_time_base_tick = mstick(); + offlog_time_anchor(unix_ts); // 事件日志: 时钟同步锚点 (boot_seq↔unix 回算) PRINT("TIME: synced unix=%lu\n", (unsigned long)unix_ts); } diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/offlog.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/offlog.c new file mode 100644 index 0000000..50cb774 --- /dev/null +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/offlog.c @@ -0,0 +1,366 @@ +/** + ****************************************************************************** + * @file offlog.c + * @author wfq + * @version V1.0 + * @date 2026-08-04 + * @brief DBN 脱机事件日志 — W25Q32 环形实现 + * + * 依赖: storage.c (SPI_Flash_*), cmcng.h (mstick), net_srv.c (dev_time_now) + * 所有写操作在主循环上下文调用 (不在中断), SPI 擦除 ~45ms 阻塞可接受。 + ****************************************************************************** + */ + +#include "offlog.h" +#include "storage.h" +#include "cmcng.h" +#include "net_srv.h" +#include + +/* 编译期断言: 记录必须是 32B (定长环形索引依赖, 踩过 36B padding 的坑) */ +typedef char offlog_evt_size_must_be_32[(sizeof(OfflogEvt) == 32) ? 1 : -1]; +typedef char offlog_head_size_must_be_32[(sizeof(OfflogHead) == 32) ? 1 : -1]; + +/*=========================================================================== + * 内部状态 + *===========================================================================*/ +static uint32_t _wr_off; /* 下一条记录写入的绝对地址 */ +static uint16_t _wr_sector; /* 当前写扇区号 (区内 1..63) */ +static uint32_t _wr_seq; /* 下一条记录的全局 seq */ +static uint16_t _boot_seq; /* 当前 boot 序号 */ +static uint32_t _count; /* 有效记录数 (0..8064) */ +static uint8_t _ready; /* init 完成标志 */ + +/* 复位原因寄存器 (CH32V20x RCC 基址 0x40021000, RSTSCKR 偏移 0x24, + STM32F1 兼容布局; 若实际硬件布局不同, 读出的原因位会异常, 待板上验证) */ +#define OFFLOG_RCC_BASE 0x40021000UL +#define OFFLOG_RCC_RSTSCKR (*(volatile uint32_t *)(OFFLOG_RCC_BASE + 0x24UL)) + +/*=========================================================================== + * 基础读写 + *===========================================================================*/ +static void offlog_head_write(const OfflogHead *h) +{ + uint8_t buf[32]; + memset(buf, 0xFF, sizeof(buf)); + memcpy(buf, h, sizeof(OfflogHead)); + SPI_Flash_Write(buf, OFFLOG_AREA_BASE, sizeof(buf)); +} + +static void offlog_head_read(OfflogHead *h) +{ + uint8_t buf[32]; + SPI_Flash_Read(buf, OFFLOG_AREA_BASE, sizeof(buf)); + memcpy(h, buf, sizeof(OfflogHead)); +} + +static int offlog_evt_read_at(uint32_t abs_off, OfflogEvt *out) +{ + SPI_Flash_Read((uint8_t *)out, abs_off, OFFLOG_EVT_SIZE); + return (out->magic == OFFLOG_EVT_MAGIC) ? 0 : -1; +} + +/*=========================================================================== + * 上电扫描恢复 + * 从头扇区记录的写位置向后扫, 校验 magic + seq 连续, 找真实写位置。 + * 正常情况 (无掉电) 第一条就无效, 立即恢复; 掉电则向前推进若干条。 + *===========================================================================*/ +static void offlog_scan_recover(const OfflogHead *h) +{ + uint32_t pos = h->wr_off; + uint32_t scanned = 0; + uint32_t expect_seq = h->wr_seq; + OfflogEvt e; + + /* 防御: wr_off 必须在数据区范围内且 32B 对齐 */ + if (pos < OFFLOG_DATA_BASE || pos >= OFFLOG_AREA_BASE + OFFLOG_AREA_SIZE) { + pos = OFFLOG_DATA_BASE; + } + if ((pos - OFFLOG_DATA_BASE) % OFFLOG_EVT_SIZE != 0) { + pos = OFFLOG_DATA_BASE; /* 未对齐 → 从头开始 */ + } + + while (scanned < OFFLOG_MAX_RECORDS) { + if (offlog_evt_read_at(pos, &e) != 0) { + break; /* 找到真实写位置 */ + } + if (expect_seq != 0 && e.seq != expect_seq) { + break; /* seq 不连续 (损坏/残留) → 视为写位置 */ + } + pos += OFFLOG_EVT_SIZE; + if (pos >= OFFLOG_DATA_BASE + OFFLOG_DATA_SIZE) { + pos = OFFLOG_DATA_BASE; /* 回绕数据区 */ + } + scanned++; + expect_seq++; + } + + _wr_off = pos; + _wr_sector = (uint16_t)((pos - OFFLOG_DATA_BASE) / 4096) + 1; /* 区内 1..63 */ + _wr_seq = expect_seq; /* 下一条 = 最后有效 seq + 1 */ + _count = h->count + scanned; + if (_count > OFFLOG_MAX_RECORDS) _count = OFFLOG_MAX_RECORDS; + _boot_seq = (uint16_t)(h->boot_seq + 1); + if (_boot_seq == 0) _boot_seq = 1; +} + +/*=========================================================================== + * 初始化 + *===========================================================================*/ +void offlog_init(void) +{ + OfflogHead h; + + memset(&h, 0xFF, sizeof(h)); + offlog_head_read(&h); + + if (!(h.magic[0] == OFFLOG_HEAD_MAGIC0 && h.magic[1] == OFFLOG_HEAD_MAGIC1 && + h.magic[2] == OFFLOG_HEAD_MAGIC2 && h.magic[3] == OFFLOG_HEAD_MAGIC3)) { + /* 全新区: 擦全部数据扇区, 写头 */ + uint32_t i; + for (i = 0; i < OFFLOG_DATA_SECTOR_CNT; i++) { + SPI_Flash_Erase_Sector(OFFLOG_DATA_BASE / 4096 + i); + } + memset(&h, 0xFF, sizeof(h)); + h.magic[0] = OFFLOG_HEAD_MAGIC0; + h.magic[1] = OFFLOG_HEAD_MAGIC1; + h.magic[2] = OFFLOG_HEAD_MAGIC2; + h.magic[3] = OFFLOG_HEAD_MAGIC3; + h.boot_seq = 0; + h.wr_sector = 1; + h.wr_off = OFFLOG_DATA_BASE; + h.wr_seq = 0; + h.count = 0; + offlog_head_write(&h); + + _wr_off = OFFLOG_DATA_BASE; + _wr_sector = 1; + _wr_seq = 1; + _count = 0; + _boot_seq = 1; + } else { + offlog_scan_recover(&h); + } + + /* 回写头: boot_seq 递增 */ + h.magic[0] = OFFLOG_HEAD_MAGIC0; + h.magic[1] = OFFLOG_HEAD_MAGIC1; + h.magic[2] = OFFLOG_HEAD_MAGIC2; + h.magic[3] = OFFLOG_HEAD_MAGIC3; + h.boot_seq = _boot_seq; + h.wr_sector = _wr_sector; + h.wr_off = _wr_off; + h.wr_seq = _wr_seq; + h.count = _count; + offlog_head_write(&h); + + _ready = 1; +} + +/*=========================================================================== + * 写一条记录 (主循环上下文) + *===========================================================================*/ +static void offlog_flush_head(void) +{ + OfflogHead h; + h.magic[0] = OFFLOG_HEAD_MAGIC0; + h.magic[1] = OFFLOG_HEAD_MAGIC1; + h.magic[2] = OFFLOG_HEAD_MAGIC2; + h.magic[3] = OFFLOG_HEAD_MAGIC3; + h.boot_seq = _boot_seq; + h.wr_sector = _wr_sector; + h.wr_off = _wr_off; + h.wr_seq = _wr_seq; + h.count = _count; + offlog_head_write(&h); +} + +/* 目标扇区是否已有数据 (环形覆盖判定): 读首条记录 magic。 + 扇区整擦后顺序写, 首条必先写, 故首条 0xA5 即可判定有数据 */ +static int offlog_sector_has_data(uint32_t abs_sec_off) +{ + OfflogEvt e; + SPI_Flash_Read((uint8_t *)&e, abs_sec_off, OFFLOG_EVT_SIZE); + return (e.magic == OFFLOG_EVT_MAGIC) ? 1 : 0; +} + +static void offlog_write_raw(const OfflogEvt *e) +{ + /* 环形回绕: 写指针越过数据区末尾 → 回到区首 */ + if (_wr_off >= OFFLOG_DATA_BASE + OFFLOG_DATA_SIZE) { + _wr_off = OFFLOG_DATA_BASE; + } + + /* 切扇区判定: 写指针所在扇区与 _wr_sector 不一致 → 擦除目标扇区。 + 覆盖两种情形: ① 正好落在扇区边界 (sec_pos==0, 上一轮写满) + ② 当前扇区放不下下一条 (sec_pos + 32 > 4096) */ + { + uint32_t sec_index = (_wr_off - OFFLOG_DATA_BASE) / 4096; /* 0..62 */ + if ((uint32_t)(_wr_sector - 1) != sec_index) { + uint32_t target_abs = (OFFLOG_DATA_BASE / 4096 + sec_index) * 4096; + /* 仅环形覆盖 (目标扇区有旧数据) 才扣减 count; + 顺序推进到空扇区擦除无记录损失 */ + if (offlog_sector_has_data(target_abs)) { + uint32_t erased = (_count > OFFLOG_EVT_PER_SECTOR) + ? OFFLOG_EVT_PER_SECTOR : _count; + _count -= erased; + } + SPI_Flash_Erase_Sector(target_abs / 4096); + _wr_sector = (uint16_t)(sec_index + 1); /* 区内 1..63 */ + offlog_flush_head(); /* 扇区切换 → 更新头 (掉电恢复锚点) */ + } + } + + SPI_Flash_Write_NoCheck((uint8_t *)e, _wr_off, OFFLOG_EVT_SIZE); + _wr_off += OFFLOG_EVT_SIZE; + _wr_seq++; + if (_count < OFFLOG_MAX_RECORDS) _count++; +} + +void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len) +{ + OfflogEvt e; + uint32_t now = mstick(); + uint32_t unix_ts = dev_time_now(); + + if (!_ready) return; + + memset(&e, 0, sizeof(e)); + e.magic = OFFLOG_EVT_MAGIC; + e.type = type; + e.len = (len > 12) ? 12 : len; + e.seq = _wr_seq; + e.boot_seq= _boot_seq; + e.ts_ms = now; + e.unix_ts = unix_ts; + if (unix_ts >= 1600000000UL) { /* 与 dev_time_sync 合法性门槛一致 */ + e.flags |= OFFLOG_UNIX_VALID_FLAG; + } + if (payload != NULL && e.len > 0) { + memcpy(e.payload, payload, e.len); + } + offlog_write_raw(&e); +} + +/*=========================================================================== + * 便捷事件包装 + *===========================================================================*/ +void offlog_boot(uint32_t reset_reason) +{ + uint8_t p[4]; + p[0] = (uint8_t)(reset_reason >> 24); + p[1] = (uint8_t)(reset_reason >> 16); + p[2] = (uint8_t)(reset_reason >> 8); + p[3] = (uint8_t)reset_reason; + offlog_evt(OFFLOG_EVT_BOOT, p, 4); +} + +void offlog_iot_connect(void) +{ + offlog_evt(OFFLOG_EVT_IOT_CONNECT, NULL, 0); +} + +void offlog_iot_ready(void) +{ + offlog_evt(OFFLOG_EVT_IOT_READY, NULL, 0); +} + +void offlog_iot_disconnect(uint8_t reason) +{ + offlog_evt(OFFLOG_EVT_IOT_DISCONN, &reason, 1); +} + +void offlog_iot_reconn(uint32_t backoff_ms) +{ + uint8_t p[4]; + p[0] = (uint8_t)(backoff_ms >> 24); + p[1] = (uint8_t)(backoff_ms >> 16); + p[2] = (uint8_t)(backoff_ms >> 8); + p[3] = (uint8_t)backoff_ms; + offlog_evt(OFFLOG_EVT_IOT_RECONN, p, 4); +} + +void offlog_evt_retry(uint32_t msg_id, uint8_t retry) +{ + uint8_t p[5]; + p[0] = (uint8_t)(msg_id >> 24); + p[1] = (uint8_t)(msg_id >> 16); + p[2] = (uint8_t)(msg_id >> 8); + p[3] = (uint8_t)msg_id; + p[4] = retry; + offlog_evt(OFFLOG_EVT_EVT_RETRY, p, 5); +} + +void offlog_evt_giveup(uint32_t msg_id) +{ + uint8_t p[4]; + p[0] = (uint8_t)(msg_id >> 24); + p[1] = (uint8_t)(msg_id >> 16); + p[2] = (uint8_t)(msg_id >> 8); + p[3] = (uint8_t)msg_id; + offlog_evt(OFFLOG_EVT_EVT_GIVEUP, p, 4); +} + +void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value) +{ + uint8_t p[6]; + p[0] = sub; + p[1] = ch; + p[2] = (uint8_t)(value >> 24); + p[3] = (uint8_t)(value >> 16); + p[4] = (uint8_t)(value >> 8); + p[5] = (uint8_t)value; + offlog_evt(OFFLOG_EVT_COIL, p, 6); +} + +void offlog_time_anchor(uint32_t unix_ts) +{ + (void)unix_ts; /* 值已过合法性门槛; offlog_evt 内经 dev_time_now() 重新取一致值 */ + offlog_evt(OFFLOG_EVT_TIME_ANCHOR, NULL, 0); +} + +/*=========================================================================== + * 查询 + *===========================================================================*/ +uint32_t offlog_boot_seq(void) +{ + return _boot_seq; +} + +uint16_t offlog_count(void) +{ + return (uint16_t)_count; +} + +int offlog_read_idx(uint16_t idx, OfflogEvt *out) +{ + uint32_t cnt = _count; + if (!_ready || idx >= cnt) return -1; + + /* 逻辑首 = 写位置向前 cnt 条 (环形) */ + uint32_t phys = (_wr_off - OFFLOG_DATA_BASE); /* 0..DATA_SIZE */ + uint32_t start = (phys + OFFLOG_DATA_SIZE - (uint32_t)cnt * OFFLOG_EVT_SIZE) % OFFLOG_DATA_SIZE; + uint32_t read_off = OFFLOG_DATA_BASE + (start + (uint32_t)idx * OFFLOG_EVT_SIZE) % OFFLOG_DATA_SIZE; + + return offlog_evt_read_at(read_off, out); +} + +/*=========================================================================== + * 清空 (审计: 清空后写一条 LOG_CLEAR) + *===========================================================================*/ +void offlog_clear(void) +{ + uint32_t i; + if (!_ready) return; + + for (i = 0; i < OFFLOG_DATA_SECTOR_CNT; i++) { + SPI_Flash_Erase_Sector(OFFLOG_DATA_BASE / 4096 + i); + } + _wr_off = OFFLOG_DATA_BASE; + _wr_sector = 1; + _count = 0; + /* _wr_seq / _boot_seq 不重置: 序号单调递增, 保证日志全局唯一 */ + + offlog_evt(OFFLOG_EVT_LOG_CLEAR, NULL, 0); + offlog_flush_head(); +} diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/peripheral_main.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/peripheral_main.c index 796f827..323eae1 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/peripheral_main.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/peripheral_main.c @@ -22,6 +22,7 @@ #include "eth_driver.h" #include "net_srv.h" #include "storage.h" +#include "offlog.h" #include "tcp_json_srv.h" #include "loop_uart_proto.h" #include "iot_mqtt_srv.h" @@ -302,6 +303,16 @@ int main(void) PRINT("SystemCoreClock:%d\n", SystemCoreClock); GetMacAddr(gMacAddr); storage_init(); + offlog_init(); + + /* 复位原因: 必须在初始化后立即读 (离复位时刻最近), 并清除标志, + 保证下次上电只反映本次复位原因 (RCC_RSTSCKR, STM32F1/CH32V20x 兼容) */ + { + uint32_t rcc_rst = OFFLOG_RCC_RSTSCKR; + offlog_boot(rcc_rst); + OFFLOG_RCC_RSTSCKR |= (1UL << 27); /* RMVF: 清复位标志 */ + } + load_cfg_from_flash(); output_cfg_from_flash(); diff --git a/vd960DBN/docs/devlog.md b/vd960DBN/docs/devlog.md index e092256..a74a5c4 100644 --- a/vd960DBN/docs/devlog.md +++ b/vd960DBN/docs/devlog.md @@ -634,9 +634,79 @@ TCP 超时要等 ~2 分钟才触发 `SINT_STAT_TIM_OUT`, **BSS 节省**: 1024 bytes **协议兼容**: JSON 输出格式不变 (`{"msg_id":N,"cmd":"loop_data","ts":T,"data":{"channels":[...]}}`) +## 2026-08-04 — 脱机事件日志系统 (W25Q32 环形) — 解决"重复上线"取证 + +### 背景 + +设备挂平台测试出现**重复上线**(现场未断电)。平台每次收到 `initialize` 都视为上线,但无本地日志无法区分两种可能: + +| 可能 | 特征 | 诊断 | +|------|------|------| +| 设备真复位 | 有 BOOT 事件 + boot_seq 递增 | 设备侧问题(电源/看门狗/软件) | +| MQTT 断连重连 | 无 BOOT 事件,只有 IOT_* 事件,boot_seq 不变 | 网络问题(链路/ broker) | + +> 注:`iot_handle_suback()` 每次 MQTT 连上 READY 都会发 `iot_send_initialize()`——断连重连就会触发平台"重复上线",这是最大嫌疑点。 + +### 方案 (P1.2 落地: 事件流, 快照流后置) + +**分区 (W25Q32 4MB)**:参数区 64KB(0x000000) | **事件日志区 256KB(0x010000)** | OTA 区 512KB(0x050000) | 快照区 ~3.25MB(0x0D0000, 预留) + +**环形实现**:头扇区(扇区0, 32B 元数据) + 63 数据扇区 × 128 条 × 32B = 8064 条;顺序写、满扇擦下一扇区(天然磨损均衡);头在扇区切换时刷新,上电从头部写位置向后扫描恢复(掉电不丢) + +**记录格式 (32B 定长)**:`magic type len flags | seq(4) ts_ms(4) unix_ts(4) boot_seq(2) rsvd(2) | payload[12]` + +**事件类型 (V1)**: + +| type | 事件 | 说明 | +|------|------|------| +| 0x01 | BOOT | 复位原因寄存器 RCC_RSTSCKR 全量入日志 (IWDG/POR/SFT 区分) | +| 0x10/0x11 | IOT_CONNECT / IOT_READY | READY 即发 initialize——**重复上线直接证据** | +| 0x12 | IOT_DISCONN | 细分原因: 1=断开 2=超时 3=CONNACK拒绝 4=连接超时 | +| 0x13 | IOT_RECONN | 退避时长 ms | +| 0x30/0x31 | EVT_RETRY / EVT_GIVEUP | event_report ACK 超时重发/耗尽 (网络质量) | +| 0x40 | COIL | 进/出/断/恢复 (sub+ch+value) | +| 0x50 | TIME_ANCHOR | 时钟同步锚点 (boot_seq↔unix 回算绝对时间) | +| 0x70 | LOG_CLEAR | 清日志审计 | + +**插桩点**(全部主循环上下文,**socket 中断内不写 SPI**): + +| 文件 | 位置 | 事件 | +|------|------|------| +| peripheral_main.c | main() storage_init 后 | offlog_init + 复位原因读+清标志 | +| iot_mqtt_srv.c | iot_mqtt_poll() 状态沿检测 | CONNECT/READY/DISCONN | +| iot_mqtt_srv.c | 重连退避 / TCP超时 / CONNACK拒绝 | RECONN / DISCONN(4) / DISCONN(3) | +| iot_mqtt_srv.c | iot_evt_process | EVT_RETRY / EVT_GIVEUP | +| iot_mqtt_srv.c | iot_evt_enqueue | COIL | +| net_srv.c | dev_time_sync 成功 | TIME_ANCHOR | + +### 三个关键坑 (单测抓出来的) + +1. **中断上下文不能写 SPI**:`iot_mqtt_handle_sock_int` 是 WCHNET 中断,SPI 擦除 ~45ms 阻塞会炸;MQTT 事件改在 `iot_mqtt_poll()` 状态沿检测(毫秒级滞后,可接受) +2. **`sizeof(OfflogEvt)=36` 而非 32**:payload[14] 后结构体对齐补 2B padding → 每扇区实际 113 条,8064 条撑爆 63 扇区提前回绕、写指针错乱。修复:字段重排(uint8×4→uint32×3→uint16×2→payload[12])+ **编译期断言** `typedef char size_must_be_32[...]` +3. **扇区级覆盖粒度 vs 记录级 count**:回绕擦整扇区会丢 128 条,但 count 仍封顶 8064 → read_idx 反推逻辑首错位。修复:擦扇区前检查目标扇区是否有数据(读首条 magic),有则 `count -= min(count,128)` + +### 单测 (gcc 隔离, tests/test_offlog.c) + +| 用例 | 覆盖 | +|------|------| +| test_fresh_init | 全新初始化 + BOOT 事件字段 | +| test_ring_wrap | 8069 条环形回绕: count=7941, 逻辑首 seq=129 | +| test_power_loss_recovery | 掉电重启: boot_seq 递增, seq 跨 boot 连续 | +| test_sector_switch | 128 条扇区切换 + 头 wr_off/wr_sector | +| test_clear | 清空 + LOG_CLEAR 审计 (seq 不重置) | +| test_power_loss_mid_sector | 跨扇区掉电恢复 | + +### 待办 + +- [ ] P1.3 导出: `log_query`/`log_stat`/`log_clear` 命令接入 MQTT/TCP/BLE +- [ ] 快照流 (0xC0 帧原样落盘) 后置 +- [ ] 复位原因寄存器布局**待板上验证** (RCC_RSTSCKR 按 STM32F1 兼容写) +- [ ] MRS 工程编译确认 offlog.c 被自动收集 + ## 修订记录 | 版本 | 时间 | 说明 | +| V3.6 | 2026-08-04 | 脱机事件日志系统: W25Q32 256KB 环形(8064条) + BOOT/网络/事件/线圈/时钟锚点 8 类事件 + 掉电恢复 + gcc 单测 6 例 | | V3.5 | 2026-07-23 | 保活精简(去heartbeat+IWDG) + SocketSend故障恢复(3次失败重连+重建socket) | | V3.4 | 2026-07-23 | MQTT 栈重构: 发送缓冲隔离/命令处理/msg_id统一/看门狗, 共10项修复 | | V3.3 | 2026-07-23 | loop_data: 消灭 data_json[1024] BSS 缓冲 + coil_count 硬限, 防 mqttBuf 重叠致 _raw 异常 | diff --git a/vd960DBN/tests/test_offlog.c b/vd960DBN/tests/test_offlog.c new file mode 100644 index 0000000..d282aa8 --- /dev/null +++ b/vd960DBN/tests/test_offlog.c @@ -0,0 +1,234 @@ +/** + * test_offlog.c — offlog 环形事件日志 gcc 隔离单测 + * + * 编译: gcc -I../BLE/OnlyUpdateApp_Peripheral/APP/include \ + * -o test_offlog test_offlog.c + * 运行: ./test_offlog + * + * Mock: W25Q32 NOR 行为 (写=AND, 擦=0xFF), mstick/dev_time_now 固定值 + * 覆盖: 全新初始化 / 环形回绕 / 掉电恢复 / 扇区切换 / 清空审计 + */ + +#include +#include +#include + +/* ============ mock W25Q32 (4MB, NOR 语义) ============ */ +static uint8_t flash[4 * 1024 * 1024]; + +void SPI_Flash_Erase_Sector(uint32_t sec) +{ + memset(flash + sec * 4096, 0xFF, 4096); +} + +void SPI_Flash_Read(uint8_t *buf, uint32_t addr, uint16_t size) +{ + memcpy(buf, flash + addr, size); +} + +void SPI_Flash_Write_NoCheck(uint8_t *buf, uint32_t addr, uint16_t size) +{ + uint16_t i; + for (i = 0; i < size; i++) flash[addr + i] &= buf[i]; /* NOR: 只能 1→0 */ +} + +void SPI_Flash_Write(uint8_t *buf, uint32_t addr, uint16_t size) +{ + /* 模拟 storage.c 行为: 目标区域非全 FF 则擦整扇区再写 */ + uint16_t i; + int need_erase = 0; + for (i = 0; i < size; i++) { + if (flash[addr + i] != 0xFF) { need_erase = 1; break; } + } + if (need_erase) SPI_Flash_Erase_Sector(addr / 4096); + SPI_Flash_Write_NoCheck(buf, addr, size); +} + +/* ============ mock 时间 (已同步: >16亿) ============ */ +static uint32_t mock_ms = 0; +uint32_t mstick(void) { return mock_ms; } +uint32_t dev_time_now(void) { return 1784768575UL; } + +/* ch32v20x 库类型 (单测环境无 SDK) */ +typedef unsigned char u8; + +/* ============ 被测模块 (直接包含实现) ============ */ +#include "../BLE/OnlyUpdateApp_Peripheral/APP/offlog.c" + +/* ============ 断言 ============ */ +static int failures = 0; +#define CHECK(cond) do { \ + if (!(cond)) { printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); failures++; } \ +} while (0) + +static void reset_flash(void) +{ + memset(flash, 0xFF, sizeof(flash)); + mock_ms = 0; +} + +/* 测试1: 全新初始化 → BOOT 事件可读回 */ +static void test_fresh_init(void) +{ + OfflogEvt e; + uint8_t payload[4] = {0x18, 0, 0, 0}; /* 假复位原因 (大端序写入 payload) */ + + reset_flash(); + offlog_init(); + CHECK(offlog_boot_seq() == 1); + CHECK(offlog_count() == 0); /* init 不写事件 */ + + offlog_boot(0x18000000); /* bit27+bit24 之类 */ + CHECK(offlog_count() == 1); + CHECK(offlog_read_idx(0, &e) == 0); + CHECK(e.magic == OFFLOG_EVT_MAGIC); + CHECK(e.type == OFFLOG_EVT_BOOT); + CHECK(e.seq == 1); + CHECK(e.boot_seq == 1); + CHECK(e.flags & OFFLOG_UNIX_VALID_FLAG); + CHECK(e.len == 4); + CHECK(memcmp(e.payload, payload, 4) == 0); + CHECK(offlog_read_idx(1, &e) == -1); /* 越界 */ + printf("PASS test_fresh_init\n"); +} + +/* 测试2: 环形回绕 — 写满 8064+5 条, count 封顶, 序号单调 */ +static void test_ring_wrap(void) +{ + OfflogEvt e; + uint32_t i, n = OFFLOG_MAX_RECORDS + 5; + + reset_flash(); + offlog_init(); + for (i = 0; i < n; i++) { + offlog_evt(OFFLOG_EVT_IOT_RECONN, NULL, 0); + } + CHECK(offlog_count() == OFFLOG_MAX_RECORDS - OFFLOG_EVT_PER_SECTOR + 5); /* 8064-128+5 */ + /* 最新一条是最后写入的, seq 应等于 n (从 1 开始) */ + CHECK(offlog_read_idx(offlog_count() - 1, &e) == 0); + CHECK(e.seq == n); + /* 逻辑首: 回绕时整扇区(128条)被擦 + 写回5条 → 最旧 = 129 */ + CHECK(offlog_read_idx(0, &e) == 0); + CHECK(e.seq == n - OFFLOG_MAX_RECORDS + OFFLOG_EVT_PER_SECTOR - 4); + printf("PASS test_ring_wrap (seq %lu..%lu, count %u)\n", + (unsigned long)(n - OFFLOG_MAX_RECORDS + OFFLOG_EVT_PER_SECTOR - 4), + (unsigned long)n, offlog_count()); +} + +/* 测试3: 掉电恢复 — 写 10 条后重新 init, boot_seq 递增, seq 连续 */ +static void test_power_loss_recovery(void) +{ + OfflogEvt e; + uint32_t i; + + reset_flash(); + offlog_init(); + for (i = 0; i < 10; i++) { + offlog_evt(OFFLOG_EVT_COIL, NULL, 0); + } + CHECK(offlog_count() == 10); + + /* 模拟掉电重启 (flash 保留, RAM 全失) */ + offlog_init(); + CHECK(offlog_boot_seq() == 2); + CHECK(offlog_count() == 10); /* 上电恢复出之前 10 条 */ + + offlog_evt(OFFLOG_EVT_IOT_CONNECT, NULL, 0); + CHECK(offlog_count() == 11); + CHECK(offlog_read_idx(10, &e) == 0); + CHECK(e.type == OFFLOG_EVT_IOT_CONNECT); + CHECK(e.seq == 11); /* seq 跨 boot 连续 */ + CHECK(e.boot_seq == 2); + printf("PASS test_power_loss_recovery\n"); +} + +/* 测试4: 扇区切换 — 写满 128 条后头扇区更新, 数据完整 */ +static void test_sector_switch(void) +{ + OfflogEvt e; + OfflogHead h; + uint32_t i; + + reset_flash(); + offlog_init(); + for (i = 0; i < 128; i++) { + offlog_evt(OFFLOG_EVT_BOOT, NULL, 0); + } + /* 第 129 条触发扇区切换 */ + offlog_evt(OFFLOG_EVT_TIME_ANCHOR, NULL, 0); + CHECK(offlog_count() == 129); + + SPI_Flash_Read((uint8_t *)&h, OFFLOG_AREA_BASE, sizeof(h)); + CHECK(h.magic[0] == OFFLOG_HEAD_MAGIC0); + CHECK(h.wr_sector == 2); + /* 头在扇区切换时刷新: wr_off = 新扇区起点 (尚未写入第 129 条) */ + CHECK(h.wr_off == OFFLOG_DATA_BASE + 4096); + + CHECK(offlog_read_idx(127, &e) == 0); + CHECK(e.type == OFFLOG_EVT_BOOT); + CHECK(e.seq == 128); + CHECK(offlog_read_idx(128, &e) == 0); + CHECK(e.type == OFFLOG_EVT_TIME_ANCHOR); + CHECK(e.seq == 129); + printf("PASS test_sector_switch\n"); +} + +/* 测试5: 清空 — 擦数据区 + LOG_CLEAR 审计 */ +static void test_clear(void) +{ + OfflogEvt e; + uint32_t i; + + reset_flash(); + offlog_init(); + for (i = 0; i < 5; i++) { + offlog_evt(OFFLOG_EVT_IOT_READY, NULL, 0); + } + offlog_clear(); + CHECK(offlog_count() == 1); /* 只剩审计 */ + CHECK(offlog_read_idx(0, &e) == 0); + CHECK(e.type == OFFLOG_EVT_LOG_CLEAR); + CHECK(e.seq == 6); /* seq 不重置 */ + printf("PASS test_clear\n"); +} + +/* 测试6: 掉电恢复跨扇区 — 写到扇区 2 中间掉电 */ +static void test_power_loss_mid_sector(void) +{ + OfflogEvt e; + uint32_t i; + + reset_flash(); + offlog_init(); + for (i = 0; i < 130; i++) { /* 已跨入扇区 2 */ + offlog_evt(OFFLOG_EVT_COIL, NULL, 0); + } + CHECK(offlog_count() == 130); + + offlog_init(); /* 掉电重启 */ + CHECK(offlog_boot_seq() == 2); + CHECK(offlog_count() == 130); + + offlog_evt(OFFLOG_EVT_IOT_CONNECT, NULL, 0); + CHECK(offlog_count() == 131); + CHECK(offlog_read_idx(130, &e) == 0); + CHECK(e.seq == 131); + printf("PASS test_power_loss_mid_sector\n"); +} + +int main(void) +{ + test_fresh_init(); + test_ring_wrap(); + test_power_loss_recovery(); + test_sector_switch(); + test_clear(); + test_power_loss_mid_sector(); + + if (failures) { + printf("\n%d FAILURE(S)\n", failures); + return 1; + } + printf("\nALL PASS\n"); + return 0; +}