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:
@@ -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 异常 |
|
||||
|
||||
Reference in New Issue
Block a user