先存后刷: MQTT 分片下载 → W25Qxx OTA 暂存区 (0x010000 512KB) → 全镜像 CRC32 复核 → 本地 0x9F ISP 透传刷写 Loop MCU (AT32F421) - 新增 ota_srv.c/h: CRC32(ISO-HDLC 逐位零表)/OtaMeta 双备份(节流 4KB flush)/ 会话状态机(begin/data/end/abort/flash)/本地刷写状态机(非阻塞 tick: A5→A6→A7 停等 ACK 1s×3)/安全窗口(有车拒绝 force 跳过)/失败 event_report ota_error - iot_mqtt_srv.c: 6 个 ota_* 命令分发 + 会话静默(event_report 积压/offlog·快照 落盘暂停, 结束补发) + IOT_EVT_OTA_ERROR + iot_any_car - usart_biz.c: uart_srv OTA 分支 ACK 分发 (本地刷写→ota_flash_feed_ack, BLE 透传不变) - peripheral_main.c: ota_init + ota_poll 挂主循环 - offlog: OTA_START 0x60/OTA_RESULT 0x61 审计事件 - tests/test_ota_srv.c: gcc 隔离单测 9 组全过 (mock NOR/UART2/时钟, 嵌入真实实现) 抓到 2 个真 bug: ①A7 序号未递减(bootloader 等不到末包) ②重发时 retry 清零(超时永不失败) - 语法检查: ota_srv/iot_mqtt_srv/usart_biz/offlog 0 新增错误 (基线 interrupt 假阳性除外) - devlog 置顶条目 + 待板上验证清单
174 lines
10 KiB
C
174 lines
10 KiB
C
/**
|
||
******************************************************************************
|
||
* @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 <stdint.h>
|
||
|
||
/*===========================================================================
|
||
* 分区与容量
|
||
*===========================================================================*/
|
||
/* 固定分区 (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_OTA_START = 0x60, /* 固件升级开始 (V1.08); payload[0]=slot, payload[1]=target, payload[2..5]=size BE */
|
||
OFFLOG_EVT_OTA_RESULT = 0x61, /* 固件升级结果 (V1.08); payload[0..3]=result code BE (0=成功, 非0=OTA_ERR_*) */
|
||
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);
|
||
void offlog_ota_start(uint8_t slot, uint32_t size); /* 固件升级开始 (V1.08, 会话静默前写入) */
|
||
void offlog_ota_result(uint32_t code); /* 固件升级结果 (V1.08, 恢复后补记) */
|
||
|
||
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__ */
|