/** ****************************************************************************** * @file offlog.h * @author wfq * @version V1.0 * @date 2026-08-04 * @brief DBN 脱机事件日志 (W25Qxx 外部 SPI NOR, 环形) * * 目的: 解决"平台重复上线/异常重启但现场无日志"的取证问题。 * 区分「设备真复位」vs「MQTT 断连重连」(两者都会导致平台重收 initialize): * - 真复位: 会有 OFFLOG_EVT_BOOT 事件, boot_seq 递增 * - 断连重连: boot_seq 不变, 只有 IOT_* 网络事件 * * 分区 (2026-08-12 ROADMAP 新规划): * | 参数区 0x000000 64KB (固定) * | OTA 区 0x010000 512KB (固定) * | 事件区 0x090000 动态: W25Q32=512KB / Q64=1MB / Q128=2MB / Q256=4MB * | 快照区 事件区后 剩余容量 (snapshot.c, 2026-08-12 已实现) * 上电读 JEDEC ID (0x9F) 决定事件区容量 (g_offlog_part 运行时分区表) * * 环形实现: * - 扇区 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 /*=========================================================================== * 分区与容量 *===========================================================================*/ /* 固定分区 (ROADMAP 2026-08-12): 参数区 64KB + OTA 512KB, 事件区紧随其后 */ #define OFFLOG_PARAM_SIZE 0x10000UL /* 参数区 64KB (固定) */ #define OFFLOG_OTA_SIZE 0x80000UL /* OTA 镜像暂存 512KB (固定) */ #define OFFLOG_AREA_BASE (OFFLOG_PARAM_SIZE + OFFLOG_OTA_SIZE) /* 0x090000 */ #define OFFLOG_HEAD_SECTOR 0 /* 区内扇区号: 头扇区 */ #define OFFLOG_EVT_PER_SECTOR 128 /* 4096 / 32 */ /* 芯片 JEDEC ID (0x9F 命令第 3 字节, EF 40 xx) */ #define OFFLOG_CHIP_W25Q32 0x16 #define OFFLOG_CHIP_W25Q64 0x17 #define OFFLOG_CHIP_W25Q128 0x18 #define OFFLOG_CHIP_W25Q256 0x19 /* 运行时分区表 (offlog_init 由 JEDEC ID 填充; 默认 W25Q32) */ typedef struct { uint8_t chip; /* OFFLOG_CHIP_xxx */ uint32_t area_size; /* 事件日志区大小 */ uint32_t data_sectors; /* 数据扇区数 (不含头扇区) */ uint32_t max_records; /* 最大记录数 = data_sectors * 128 */ } OfflogPart; extern OfflogPart g_offlog_part; /* 兼容宏: 展开为运行时值 (offlog.c 主体引用不变) */ #define OFFLOG_AREA_SIZE (g_offlog_part.area_size) #define OFFLOG_DATA_SECTOR_CNT (g_offlog_part.data_sectors) #define OFFLOG_DATA_SIZE ((g_offlog_part.data_sectors) * 4096UL) #define OFFLOG_MAX_RECORDS (g_offlog_part.max_records) #define OFFLOG_DATA_BASE (OFFLOG_AREA_BASE + 4096UL) /* 头扇区记录 (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..max_records) */ 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, /* 日志清除 (审计) */ }; /* 复位原因寄存器 (CH32V20x RCC 基址 0x40021000, RSTSCKR 偏移 0x24, STM32F1 兼容布局; 若实际硬件布局不同, 读出的原因位会异常, 待板上验证) */ #define OFFLOG_RCC_BASE 0x40021000UL #define OFFLOG_RCC_RSTSCKR (*(volatile uint32_t *)(OFFLOG_RCC_BASE + 0x24UL)) /* 复位原因位 (RCC_RSTSCKR, CH32V20x 布局 — 2026-08-12 对照 WCH ch32v20x.h 修正, 原定义错位: IWDG/SFT/POR 位置全错, 且 RMVF 清标志写错位导致标志从未清除) */ #define OFFLOG_RST_LPWR (1UL << 31) /* 低功耗复位 */ #define OFFLOG_RST_WWDG (1UL << 30) /* 窗口看门狗复位 */ #define OFFLOG_RST_IWDG (1UL << 29) /* 独立看门狗复位 (IWDG 超时 → 主循环卡死!) */ #define OFFLOG_RST_SFT (1UL << 28) /* 软件复位 NVIC_SystemReset */ #define OFFLOG_RST_POR (1UL << 27) /* 上电/掉电复位 (真断电!) */ #define OFFLOG_RST_PIN (1UL << 26) /* NRST 引脚复位 */ #define OFFLOG_RST_RMVF (1UL << 24) /* 清除复位标志 (写 1 清全部, WCH: RCC_RMVF) */ /*=========================================================================== * 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 审计) */ /*=========================================================================== * 协议导出 (MQTT V1.06 / TCP JSON V1.02) *===========================================================================*/ uint8_t offlog_enabled(void); /* 日志功能是否启用 (Flash 初始化成功) */ uint32_t offlog_seq_last(void); /* 最新一条记录全局序号 (0=空) */ const char *offlog_type_str(uint8_t type); /* 事件类型数字 → 协议字符串名 */ int offlog_evt_to_json(const OfflogEvt *e, char *buf, int buf_len); /* 记录 → JSON 对象 (旧方案, 保留) */ int offlog_evt_to_hex(const OfflogEvt *e, char *buf, int buf_len); /* 32B → 64 hex (TCP/MQTT log_query, V1.03/V1.07) */ #define OFFLOG_MAX_QUERY_RECORDS 4 /* log_query 分页上限 (MQTT ≤500B 发布限制) */ #endif /* _OFFLOG_H__ */