feat(vd960DBN): 脱机事件日志系统 (W25Q32 环形) — 解决重复上线取证

背景: 设备挂平台测试出现重复上线(现场未断电), 无本地日志无法区分
设备真复位 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) + 快照流 + 复位原因板上验证
This commit is contained in:
wangfq
2026-08-04 15:04:12 +08:00
parent a5ac54a2bf
commit ac3fb512d5
8 changed files with 836 additions and 0 deletions
@@ -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 <stdint.h>
/*===========================================================================
* 分区与容量
*===========================================================================*/
#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__ */
@@ -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);