feat(vd960DBN): 传感快照区落地 snapshot.c + BLE SNAP_STAT/QUERY/CLEAR
- snapshot.h/c: 64B 定长 SnapRec (头部16B + 4x12B 0xC0线圈数据原样) 环形分区独立于事件日志, 快照区 = 总容量 - 固定区576KB - 事件区 - 线程模型: USART2 ISR 只打包+RAM暂存(8深满丢新), 主循环 snap_flush 落盘 (iot_sensor_ingest 在中断上下文, SPI 45ms 擦除严禁进中断) - dbn_ble_srv: SNAP_STAT(0x28)/SNAP_QUERY(0x29)/SNAP_CLEAR(0x2A) QUERY 上限 2 条 (64x2+2=130B, ODR 教训防御) - iot_mqtt_srv: ingest 挂 snap_enqueue, publish_sensor 挂 snap_flush (放在 READY 检查前, 断网照常落盘) - peripheral_main: offlog_init 后加 snap_init - 清空审计: 写事件流 log_clear payload[0]=2 - 测试: test_snapshot.c 8 例全过, offlog/ble_offlog 回归全过 - 文档: DLD960_BLE协议 V1.02 + ROADMAP P1.2 状态 + devlog
This commit is contained in:
@@ -92,3 +92,8 @@ wchreference/
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
|
|
||||||
|
# gcc 单测编译产物 (tests/*.c 保留)
|
||||||
|
test_offlog
|
||||||
|
test_ble_offlog
|
||||||
|
test_snapshot
|
||||||
|
|||||||
+87
-5
@@ -1,8 +1,8 @@
|
|||||||
# DLD960 BLE 通信协议(脱机事件日志)
|
# DLD960 BLE 通信协议(脱机事件日志 + 传感快照)
|
||||||
|
|
||||||
> 版本: V1.01(2026-08-12,分包上限改为随协商 MTU 动态)
|
> 版本: V1.02(2026-08-12,新增传感快照 3 命令;分包上限随协商 MTU 动态)
|
||||||
> 适用: vd960DBN(CH32V208,WCH BLE 协议栈)
|
> 适用: vd960DBN(CH32V208,WCH BLE 协议栈)
|
||||||
> 用途: 小程序/APP 经蓝牙读取设备本地 W25Q32 环形事件日志(离线取证:区分设备真复位 vs MQTT 断连重连)
|
> 用途: 小程序/APP 经蓝牙读取设备本地 W25Qxx 环形日志(离线取证:事件流区分真复位 vs MQTT 断连重连;快照流回放 0xC0 传感波形)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -43,6 +43,9 @@ Magic | Header | Data | CheckByte
|
|||||||
| `0x25` | `OFFLOG_STAT` | APP→设备 | 查询脱机事件日志统计(分页定位) |
|
| `0x25` | `OFFLOG_STAT` | APP→设备 | 查询脱机事件日志统计(分页定位) |
|
||||||
| `0x26` | `OFFLOG_QUERY` | APP→设备 | 按全局序号分页拉取日志记录 |
|
| `0x26` | `OFFLOG_QUERY` | APP→设备 | 按全局序号分页拉取日志记录 |
|
||||||
| `0x27` | `OFFLOG_CLEAR` | APP→设备 | 清除日志(审计留痕) |
|
| `0x27` | `OFFLOG_CLEAR` | APP→设备 | 清除日志(审计留痕) |
|
||||||
|
| `0x28` | `SNAP_STAT` | APP→设备 | 查询传感快照统计(分页定位) |
|
||||||
|
| `0x29` | `SNAP_QUERY` | APP→设备 | 按全局序号分页拉取快照记录 |
|
||||||
|
| `0x2A` | `SNAP_CLEAR` | APP→设备 | 清除快照(审计写入事件流) |
|
||||||
|
|
||||||
> 与 MQTT V1.06 / TCP JSON V1.02 的 `log_stat` / `log_query` / `log_clear` 语义一致,通道不同。
|
> 与 MQTT V1.06 / TCP JSON V1.02 的 `log_stat` / `log_query` / `log_clear` 语义一致,通道不同。
|
||||||
|
|
||||||
@@ -120,7 +123,84 @@ Magic | Header | Data | CheckByte
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6 记录格式(OfflogEvt,32B 定长,小端)
|
## 6 传感快照日志(0x28/0x29/0x2A,V1.02 新增)
|
||||||
|
|
||||||
|
**用途**:0xC0 传感帧(4 线圈波形)按上报节奏落盘,与事件日志**分区独立**(快照区 = 总容量 − 固定区 576KB − 事件区)。断网期间波形照常记录,可离线回放。
|
||||||
|
|
||||||
|
**线程语义(设备侧)**:采集路径(USART2 中断)只做打包 + RAM 暂存(8 深,满丢新),主循环每轮 `snap_flush()` 落盘——QUERY 读到的是已落盘记录。
|
||||||
|
|
||||||
|
### 6.1 查询快照统计 `SNAP_STAT` (0x28)
|
||||||
|
|
||||||
|
**请求 data:** 无(`Len=1`)。
|
||||||
|
|
||||||
|
**响应 data(19B,全小端):** 与 §3 `OFFLOG_STAT` 布局相同:
|
||||||
|
|
||||||
|
| 偏移 | 长度 | 字段 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 0 | 1 | `status` | `0x00`=OK;`0x01`=快照未启用 |
|
||||||
|
| 1 | 2 | `boot_seq` | 当前启动序号 |
|
||||||
|
| 3 | 4 | `count` | 有效记录条数(0~capacity) |
|
||||||
|
| 7 | 4 | `capacity` | 容量上限(**随芯片动态**:W25Q32=48064 / W25Q64=105408 / W25Q128=220096 / W25Q256=449472) |
|
||||||
|
| 11 | 4 | `seq_first` | 最早一条全局序号(count=0 时=0) |
|
||||||
|
| 15 | 4 | `seq_last` | 最新一条全局序号 |
|
||||||
|
|
||||||
|
### 6.2 分页拉取快照 `SNAP_QUERY` (0x29)
|
||||||
|
|
||||||
|
**请求 data(5B):**
|
||||||
|
|
||||||
|
| 偏移 | 长度 | 字段 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 0 | 4 | `start_seq` | 起始全局序号(小端,含);越界返回 0 条 |
|
||||||
|
| 4 | 1 | `count` | 拉取条数,**上限 2**(64B×2+2=130B ≤ 单包缓冲),超限按 2;0 按 2 |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
|
||||||
|
| 偏移 | 长度 | 字段 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 0 | 1 | `status` | `0x00`=OK;`0x01`=快照未启用;`0x02`=请求帧过短 |
|
||||||
|
| 1 | 1 | `count` | 本次实际返回记录条数(0~2) |
|
||||||
|
| 2 | N×64 | 记录 | `count` 条 SnapRec 原始结构(小端,见 §6.4) |
|
||||||
|
|
||||||
|
记录顺序 = 逻辑序号升序。QUERY 响应最大 130B:MTU=96 时 2 包(87+43 dat);MTU≥103 时 2 包(94+36 dat)。
|
||||||
|
|
||||||
|
### 6.3 清除快照 `SNAP_CLEAR` (0x2A)
|
||||||
|
|
||||||
|
**请求 data:** 无。**响应 data(1B):** `status`:`0x00`=OK。
|
||||||
|
|
||||||
|
> ⚠ **高风险操作**:清除动作本身写入**事件流** `log_clear` 审计(payload[0]=2=快照流,不可清除)。
|
||||||
|
> ⚠ **阻塞时长随芯片**:W25Q32 快照 751 扇区 SPI 擦除 ≈ 34s,期间主循环阻塞。请勿高频调用。
|
||||||
|
|
||||||
|
成功后 `SNAP_STAT` 的 `count` 归 0,`seq_last` 继续递增(序号不复位)。
|
||||||
|
|
||||||
|
### 6.4 快照记录格式(SnapRec,64B 定长,小端)
|
||||||
|
|
||||||
|
| 偏移 | 长度 | 字段 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 0 | 1 | `magic` | `0xA6`(区别于事件 `0xA5`) |
|
||||||
|
| 1 | 1 | `len` | 线圈数据有效长度 = coil_count×12(0~48) |
|
||||||
|
| 2 | 1 | `flags` | bit0=分包帧(预留) |
|
||||||
|
| 3 | 1 | `rsvd` | 保留(0) |
|
||||||
|
| 4 | 4 | `seq` | 全局序号(跨 boot 递增) |
|
||||||
|
| 8 | 4 | `ts_ms` | boot 内相对时间(ms,采集时刻) |
|
||||||
|
| 12 | 2 | `boot_seq` | 所属启动段 |
|
||||||
|
| 14 | 2 | `rsvd2` | 保留(0) |
|
||||||
|
| 16 | 48 | `coils` | 4×12B 线圈数据,与 0xC0 线上格式一致 |
|
||||||
|
|
||||||
|
**线圈 12B 单元**(与 0xC0 帧内每路线圈完全一致):
|
||||||
|
|
||||||
|
| 偏移(单元内) | 长度 | 字段 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 0 | 1 | `cfg` | freq_level(2) + direction(1) + freq_type(1) + sensitivity(4) |
|
||||||
|
| 1 | 1 | `cond` | condition(4) + loop_state(1) + car_state(1) + misc_type(2) |
|
||||||
|
| 2 | 3 | `freq` | 频率,3B 小端无符号 |
|
||||||
|
| 5 | 3 | `variation` | 变化量,3B 小端有符号补码(= Origin − CAPVD) |
|
||||||
|
| 8 | 4 | `misc` | 杂项 4B 小端(misc_type=0 时=通过时间,50ms 单位) |
|
||||||
|
|
||||||
|
> 绝对时间回算:用事件流 `time_anchor`(boot_seq ↔ unix_ts 映射)+ 本记录 `boot_seq`/`ts_ms`。未同步段仅相对时间。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7 记录格式(OfflogEvt,32B 定长,小端)
|
||||||
|
|
||||||
| 偏移 | 长度 | 字段 | 说明 |
|
| 偏移 | 长度 | 字段 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
@@ -154,8 +234,10 @@ Magic | Header | Data | CheckByte
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7 版本历史
|
## 8 版本历史
|
||||||
|
|
||||||
| 版本 | 日期 | 说明 |
|
| 版本 | 日期 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| V1.00 | 2026-08-10 | 脱机事件日志 3 命令:OFFLOG_STAT / OFFLOG_QUERY / OFFLOG_CLEAR |
|
| V1.00 | 2026-08-10 | 脱机事件日志 3 命令:OFFLOG_STAT / OFFLOG_QUERY / OFFLOG_CLEAR |
|
||||||
|
| V1.01 | 2026-08-12 | 分包上限改为随协商 MTU 动态:`chunk = min(MTU-9, 94)`;修复 Too large noti 丢包 |
|
||||||
|
| V1.02 | 2026-08-12 | 新增传感快照 3 命令:SNAP_STAT / SNAP_QUERY / SNAP_CLEAR(0x28/0x29/0x2A) |
|
||||||
|
|||||||
+2
-2
@@ -91,8 +91,8 @@
|
|||||||
|
|
||||||
**两条日志流分开存**:
|
**两条日志流分开存**:
|
||||||
|
|
||||||
1. **事件流(必录,低速率)**:car_enter/car_leave(含时间量)、线圈断线/恢复、继电器动作、上电/复位(含复位原因)、MQTT/TCP 连接与断开、event_report ACK 超时/重发/放弃、配置变更(含来源:BLE/MQTT/TCP)、灵敏度换档、**时钟同步锚点**、固件升级开始/结果、日志清除操作(审计自记录)
|
1. **事件流(必录,低速率)**:car_enter/car_leave(含时间量)、线圈断线/恢复、继电器动作、上电/复位(含复位原因)、MQTT/TCP 连接与断开、event_report ACK 超时/重发/放弃、配置变更(含来源:BLE/MQTT/TCP)、灵敏度换档、**时钟同步锚点**、固件升级开始/结果、日志清除操作(审计自记录)——**已实现(offlog)**
|
||||||
2. **快照流(环形可覆盖)**:0xC0 帧二进制原样 + 头部(序号/boot_seq/mstick),~64B/条,**记录节奏与网络上报同频**(空闲按 interval、活动 300ms、car 沿立即)——直接挂在 `iot_sensor_ingest()` 同源出口,天然与上报一致
|
2. **快照流(环形可覆盖)**:0xC0 帧 4 线圈数据原样(12B/线圈)+ 头部(序号/boot_seq/mstick),64B/条,**记录节奏与上报同频**——直接挂在 `iot_sensor_ingest()` 同源出口。**已实现(snapshot,2026-08-12)**:中断只入 RAM 暂存(8 深满丢新),主循环 flush 落盘;BLE 新增 SNAP_STAT/QUERY/CLEAR(0x28/0x29/0x2A)
|
||||||
|
|
||||||
**容量预算**(**以 W25Q32 为基准**:事件日志 512KB ≈ 16384 条 / 快照区 3008KB ≈ **4.8 万条** @64B):
|
**容量预算**(**以 W25Q32 为基准**:事件日志 512KB ≈ 16384 条 / 快照区 3008KB ≈ **4.8 万条** @64B):
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include "net_srv.h"
|
#include "net_srv.h"
|
||||||
#include "offlog.h"
|
#include "offlog.h"
|
||||||
|
#include "snapshot.h"
|
||||||
extern uint16_t peripheral_get_mtu(void);
|
extern uint16_t peripheral_get_mtu(void);
|
||||||
|
|
||||||
uint8_t g_flag_notify_temp = 0; //临时通知notify flag, 0 disable, 1 enable
|
uint8_t g_flag_notify_temp = 0; //临时通知notify flag, 0 disable, 1 enable
|
||||||
@@ -1012,6 +1013,97 @@ void manage_dbn_ble_default(uint8_t *pkg, uint8_t len)
|
|||||||
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
||||||
PRINT("BLE: offlog_clear done\n");
|
PRINT("BLE: offlog_clear done\n");
|
||||||
} break;
|
} break;
|
||||||
|
case CMD_DBN_SNAP_STAT: {
|
||||||
|
/* snap stat: status(1) boot_seq(2) count(4) capacity(4) seq_first(4) seq_last(4), all LE */
|
||||||
|
uint8_t _i = 0;
|
||||||
|
uint32_t _total = snap_count();
|
||||||
|
uint32_t _seq_last = snap_seq_last();
|
||||||
|
uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0;
|
||||||
|
uint32_t _boot = snap_boot_seq();
|
||||||
|
uint32_t _cap = SNAP_MAX_RECORDS;
|
||||||
|
|
||||||
|
if (!snap_enabled()) {
|
||||||
|
tmp_ble_buf[_i++] = 0x01; /* disabled */
|
||||||
|
} else {
|
||||||
|
tmp_ble_buf[_i++] = 0x00; /* ok */
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)(_boot & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)((_boot >> 8) & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)(_total & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)((_total >> 8) & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)((_total >> 16) & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)((_total >> 24) & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)(_cap & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 8) & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 16) & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 24) & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)(_seq_first & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 8) & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 16) & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 24) & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)(_seq_last & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 8) & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 16) & 0xFF);
|
||||||
|
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 24) & 0xFF);
|
||||||
|
}
|
||||||
|
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
||||||
|
PRINT("BLE: snap_stat count=%lu seq_first=%lu seq_last=%lu\n",
|
||||||
|
(unsigned long)_total, (unsigned long)_seq_first, (unsigned long)_seq_last);
|
||||||
|
} break;
|
||||||
|
case CMD_DBN_SNAP_QUERY: {
|
||||||
|
/* paged fetch: req pkg[4..7]=start_seq(LE32) pkg[8]=count(1..2)
|
||||||
|
resp: status(1) count(1) + N x 64B SnapRec raw (LE) */
|
||||||
|
uint32_t _start_seq = 0;
|
||||||
|
uint32_t _req_count = 0;
|
||||||
|
uint32_t _total = snap_count();
|
||||||
|
uint32_t _seq_last = snap_seq_last();
|
||||||
|
uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0;
|
||||||
|
uint8_t _i = 0, _j = 0;
|
||||||
|
|
||||||
|
if (!snap_enabled()) {
|
||||||
|
tmp_ble_buf[_i++] = 0x01; /* disabled */
|
||||||
|
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (len < 11) { /* frame too short: magic+header+len+cmd+5data+2ckb */
|
||||||
|
tmp_ble_buf[_i++] = 0x02; /* bad request */
|
||||||
|
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_start_seq = (uint32_t)pkg[4] | ((uint32_t)pkg[5] << 8)
|
||||||
|
| ((uint32_t)pkg[6] << 16) | ((uint32_t)pkg[7] << 24);
|
||||||
|
_req_count = pkg[8];
|
||||||
|
if (_req_count > SNAP_MAX_QUERY_RECORDS) _req_count = SNAP_MAX_QUERY_RECORDS;
|
||||||
|
if (_req_count == 0) _req_count = SNAP_MAX_QUERY_RECORDS;
|
||||||
|
/* overflow guard: resp dat = 2 + N*64 must fit tmp buf (ODR lesson) */
|
||||||
|
{ uint16_t _max_rec = (MAX_BLE_TMP_BUF_LEN >= 2) ?
|
||||||
|
((MAX_BLE_TMP_BUF_LEN - 2) / (uint16_t)sizeof(SnapRec)) : 0;
|
||||||
|
if (_req_count > _max_rec) _req_count = _max_rec; }
|
||||||
|
|
||||||
|
tmp_ble_buf[_i++] = 0x00; /* status ok */
|
||||||
|
tmp_ble_buf[_i++] = 0; /* count placeholder */
|
||||||
|
if (_total > 0 && _start_seq >= _seq_first && _start_seq <= _seq_last) {
|
||||||
|
uint32_t _idx = _start_seq - _seq_first;
|
||||||
|
while (_j < _req_count && (_idx + _j) < _total) {
|
||||||
|
SnapRec _rec;
|
||||||
|
if (snap_read_idx(_idx + _j, &_rec) != 0) break;
|
||||||
|
memcpy(&tmp_ble_buf[_i], &_rec, sizeof(SnapRec));
|
||||||
|
_i += sizeof(SnapRec);
|
||||||
|
_j++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tmp_ble_buf[1] = _j;
|
||||||
|
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
||||||
|
PRINT("BLE: snap_query start_seq=%lu req=%lu fetched=%u\n",
|
||||||
|
(unsigned long)_start_seq, (unsigned long)_req_count, (unsigned)_j);
|
||||||
|
} break;
|
||||||
|
case CMD_DBN_SNAP_CLEAR: {
|
||||||
|
/* clear snapshot (audit trail). blocking (sectors erase), main-loop ctx ok */
|
||||||
|
uint8_t _i = 0;
|
||||||
|
snap_clear();
|
||||||
|
tmp_ble_buf[_i++] = 0x00; /* ok */
|
||||||
|
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
||||||
|
PRINT("BLE: snap_clear done\n");
|
||||||
|
} break;
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
} break;
|
} break;
|
||||||
|
|||||||
@@ -52,6 +52,9 @@
|
|||||||
#define CMD_DBN_OFFLOG_STAT 0x25 // query offlog stat
|
#define CMD_DBN_OFFLOG_STAT 0x25 // query offlog stat
|
||||||
#define CMD_DBN_OFFLOG_QUERY 0x26 // query offlog records by seq
|
#define CMD_DBN_OFFLOG_QUERY 0x26 // query offlog records by seq
|
||||||
#define CMD_DBN_OFFLOG_CLEAR 0x27 // clear offlog (audit trail)
|
#define CMD_DBN_OFFLOG_CLEAR 0x27 // clear offlog (audit trail)
|
||||||
|
#define CMD_DBN_SNAP_STAT 0x28 // query snapshot stat
|
||||||
|
#define CMD_DBN_SNAP_QUERY 0x29 // query snapshot records by seq
|
||||||
|
#define CMD_DBN_SNAP_CLEAR 0x2A // clear snapshot (audit trail)
|
||||||
|
|
||||||
|
|
||||||
#define CMD_DBN_RW_UART_BAUD 0x31
|
#define CMD_DBN_RW_UART_BAUD 0x31
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
* | 参数区 0x000000 64KB (固定)
|
* | 参数区 0x000000 64KB (固定)
|
||||||
* | OTA 区 0x010000 512KB (固定)
|
* | OTA 区 0x010000 512KB (固定)
|
||||||
* | 事件区 0x090000 动态: W25Q32=512KB / Q64=1MB / Q128=2MB / Q256=4MB
|
* | 事件区 0x090000 动态: W25Q32=512KB / Q64=1MB / Q128=2MB / Q256=4MB
|
||||||
* | 快照区 事件区后 剩余容量 (预留, 后续)
|
* | 快照区 事件区后 剩余容量 (snapshot.c, 2026-08-12 已实现)
|
||||||
* 上电读 JEDEC ID (0x9F) 决定事件区容量 (g_offlog_part 运行时分区表)
|
* 上电读 JEDEC ID (0x9F) 决定事件区容量 (g_offlog_part 运行时分区表)
|
||||||
*
|
*
|
||||||
* 环形实现:
|
* 环形实现:
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
******************************************************************************
|
||||||
|
* @file snapshot.h
|
||||||
|
* @author wfq
|
||||||
|
* @version V1.0
|
||||||
|
* @date 2026-08-12
|
||||||
|
* @brief DBN 传感快照日志 (W25Qxx 外部 SPI NOR, 环形, 与事件日志区分区独立)
|
||||||
|
*
|
||||||
|
* 用途: 0xC0 传感帧 (4 线圈) 按上报节奏落盘, 断网期间也有现场波形可查;
|
||||||
|
* 事件流管"发生了什么", 快照流管"波形长什么样"。
|
||||||
|
*
|
||||||
|
* 分区 (ROADMAP 2026-08-12 新规划):
|
||||||
|
* | 参数区 0x000000 64KB (固定)
|
||||||
|
* | OTA 区 0x010000 512KB (固定)
|
||||||
|
* | 事件区 0x090000 动态 (offlog 持有, g_offlog_part.area_size)
|
||||||
|
* | 快照区 事件区后 剩余容量 (总容量 - 固定区 - 事件区)
|
||||||
|
* 上电读 JEDEC ID (0x9F) 决定事件区/快照区容量 (g_snap_part 运行时分区表)
|
||||||
|
*
|
||||||
|
* 环形实现 (与 offlog 同模式):
|
||||||
|
* - 扇区 0 (区内) = 头扇区, 存元数据 (boot_seq/写位置/条数), 扇区切换时更新
|
||||||
|
* - 数据扇区每扇区 64 条 x 64B 定长记录 (4096/64)
|
||||||
|
* - 顺序写, 写满擦下一扇区 (环形覆盖), 天然磨损均衡
|
||||||
|
* - 上电从头部记录的写位置向后扫描, 找第一条无效记录 = 真实写位置 (掉电恢复)
|
||||||
|
*
|
||||||
|
* 线程模型 (关键):
|
||||||
|
* - snap_enqueue(): 中断上下文 (USART2 ISR → lup_process_frame → iot_sensor_ingest),
|
||||||
|
* 只做打包 + RAM 环形暂存 (memcpy, 无阻塞, 无 SPI 操作); 满时丢新不覆盖
|
||||||
|
* - snap_flush(): 主循环上下文 (iot_mqtt_publish_sensor), RAM 暂存 → SPI 落盘
|
||||||
|
* (扇区切换擦除 ~45ms 阻塞仅发生在主循环, 与 offlog 同底线)
|
||||||
|
* - 单生产者 (中断) 单消费者 (主循环) 无锁环形: 生产者只写 tail,
|
||||||
|
* 消费者只读 head, 留一格判满
|
||||||
|
*
|
||||||
|
* 记录格式 SnapRec (64B 定长, 无 padding):
|
||||||
|
* magic(1) len(1) flags(1) rsvd(1) | seq(4) ts_ms(4) boot_seq(2) rsvd2(2) | coils(48)
|
||||||
|
* coils = 4 x 12B, 与 0xC0 线上线圈单元完全一致:
|
||||||
|
* [0] 配置1 freq_level(2)|direction(1)|freq_type(1)|sensitivity(4)
|
||||||
|
* [1] 条件 condition(4)|loop_state(1)|car_state(1)|misc_type(2)
|
||||||
|
* [2..4] 频率 3B LE
|
||||||
|
* [5..7] 变化量 3B LE (有符号补码)
|
||||||
|
* [8..11] 杂项 4B LE
|
||||||
|
******************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _SNAPSHOT_H__
|
||||||
|
#define _SNAPSHOT_H__
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "offlog.h" /* OFFLOG_CHIP_xxx + g_offlog_part (事件区大小) */
|
||||||
|
#include "loop_uart_proto.h" /* LUP_SensorReport / LUP_CoilSensor */
|
||||||
|
|
||||||
|
/*===========================================================================
|
||||||
|
* 分区与容量
|
||||||
|
*===========================================================================*/
|
||||||
|
/* 固定分区 (与 offlog 一致): 参数区 64KB + OTA 512KB */
|
||||||
|
#define SNAP_PARAM_SIZE 0x10000UL /* 参数区 64KB (固定) */
|
||||||
|
#define SNAP_OTA_SIZE 0x80000UL /* OTA 镜像暂存 512KB (固定) */
|
||||||
|
#define SNAP_FIXED_SIZE (SNAP_PARAM_SIZE + SNAP_OTA_SIZE) /* 0x090000 */
|
||||||
|
|
||||||
|
#define SNAP_HEAD_SECTOR 0 /* 区内扇区号: 头扇区 */
|
||||||
|
#define SNAP_REC_PER_SECTOR 64 /* 4096 / 64 */
|
||||||
|
|
||||||
|
/* 运行时分区表 (snap_init 由 JEDEC ID + g_offlog_part 填充; 默认 W25Q32) */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t chip; /* OFFLOG_CHIP_xxx */
|
||||||
|
uint32_t area_base; /* 快照区起始 (事件区后) */
|
||||||
|
uint32_t area_size; /* 快照区大小 (剩余容量) */
|
||||||
|
uint32_t data_sectors; /* 数据扇区数 (不含头扇区) */
|
||||||
|
uint32_t max_records; /* 最大记录数 = data_sectors * 64 */
|
||||||
|
} SnapPart;
|
||||||
|
extern SnapPart g_snap_part;
|
||||||
|
|
||||||
|
/* 兼容宏: 展开为运行时值 */
|
||||||
|
#define SNAP_DATA_BASE (g_snap_part.area_base + 4096UL)
|
||||||
|
#define SNAP_AREA_SIZE (g_snap_part.area_size)
|
||||||
|
#define SNAP_DATA_SECTOR_CNT (g_snap_part.data_sectors)
|
||||||
|
#define SNAP_DATA_SIZE ((g_snap_part.data_sectors) * 4096UL)
|
||||||
|
#define SNAP_MAX_RECORDS (g_snap_part.max_records)
|
||||||
|
|
||||||
|
/* 头扇区记录 (32B, 扇区切换时更新) */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t magic[4]; /* "SNAP" */
|
||||||
|
uint32_t boot_seq; /* 上次上电序号 (本 boot 由 init 时 +1) */
|
||||||
|
uint32_t wr_sector; /* 上次写扇区号 (区内 1..n) */
|
||||||
|
uint32_t wr_off; /* 上次写偏移 (绝对地址) */
|
||||||
|
uint32_t wr_seq; /* 下一条记录全局 seq (掉电恢复锚点) */
|
||||||
|
uint32_t count; /* 有效记录数 (0..max_records) */
|
||||||
|
uint32_t rsvd[2];
|
||||||
|
} SnapHead; /* 28B, 按 32B 对齐使用 */
|
||||||
|
|
||||||
|
#define SNAP_HEAD_MAGIC0 'S'
|
||||||
|
#define SNAP_HEAD_MAGIC1 'N'
|
||||||
|
#define SNAP_HEAD_MAGIC2 'A'
|
||||||
|
#define SNAP_HEAD_MAGIC3 'P'
|
||||||
|
|
||||||
|
/* 快照记录 (64B 定长, 字段重排保证自然对齐无 padding:
|
||||||
|
4×uint8 → uint32×2 → uint16×2 → coils[48] = 16+48 = 64) */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t magic; /* 0xA6 (区别于事件 0xA5) */
|
||||||
|
uint8_t len; /* 线圈数据有效长度 = coil_count*12 (0..48) */
|
||||||
|
uint8_t flags; /* bit0: 分包帧 (预留) */
|
||||||
|
uint8_t rsvd;
|
||||||
|
uint32_t seq; /* 全局序号 (跨 boot 递增, flush 时分配) */
|
||||||
|
uint32_t ts_ms; /* boot 内相对时间 mstick() (采集时刻) */
|
||||||
|
uint16_t boot_seq; /* 启动序号 */
|
||||||
|
uint16_t rsvd2;
|
||||||
|
uint8_t coils[48]; /* 4 × 12B 线圈数据 (0xC0 线上格式原样) */
|
||||||
|
} SnapRec; /* 64B, 无 padding */
|
||||||
|
|
||||||
|
#define SNAP_MAGIC 0xA6
|
||||||
|
#define SNAP_REC_SIZE sizeof(SnapRec)
|
||||||
|
#define SNAP_COIL_BYTES 12
|
||||||
|
#define SNAP_COILS_MAX 4
|
||||||
|
|
||||||
|
/* RAM 暂存深度 (中断 enqueue → 主循环 flush 之间的缓冲)
|
||||||
|
Loop 帧最快 150ms, publish_sensor 每轮 flush, 8 深 ≈ 1.2s 余量;
|
||||||
|
CH32V208 SRAM 紧张: 64B × 8 = 512B, 远小于 SPI_FLASH_BUF 的 4KB */
|
||||||
|
#define SNAP_RAM_DEPTH 8
|
||||||
|
#define SNAP_MAX_QUERY_RECORDS 2 /* 快照 QUERY 分页上限: (132-2)/64 */
|
||||||
|
|
||||||
|
|
||||||
|
/*===========================================================================
|
||||||
|
* API
|
||||||
|
*===========================================================================*/
|
||||||
|
void snap_init(void); /* 上电初始化 (扫描恢复) */
|
||||||
|
uint8_t snap_enabled(void); /* Flash 初始化成功 */
|
||||||
|
|
||||||
|
void snap_enqueue(const LUP_SensorReport *sr); /* 中断安全: 打包→RAM 暂存 (满丢新) */
|
||||||
|
void snap_flush(void); /* 主循环: RAM 暂存→SPI 落盘 */
|
||||||
|
|
||||||
|
uint32_t snap_count(void); /* 有效记录数 */
|
||||||
|
uint32_t snap_seq_last(void); /* 最新一条记录全局序号 (0=空) */
|
||||||
|
uint32_t snap_boot_seq(void); /* 当前 boot 序号 */
|
||||||
|
int snap_read_idx(uint32_t idx, SnapRec *out); /* 按逻辑序号读, 0=成功 -1=越界 */
|
||||||
|
void snap_clear(void); /* 清空快照区 (审计写入事件流) */
|
||||||
|
|
||||||
|
#endif /* _SNAPSHOT_H__ */
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
#include "tcp_json_srv.h"
|
#include "tcp_json_srv.h"
|
||||||
#include "storage.h"
|
#include "storage.h"
|
||||||
#include "offlog.h"
|
#include "offlog.h"
|
||||||
|
#include "snapshot.h"
|
||||||
#include "ch32v20x_iwdg.h"
|
#include "ch32v20x_iwdg.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -164,6 +165,7 @@ static void iot_sensor_ingest(const LUP_SensorReport *sr) {
|
|||||||
iot_evt_feed(sr); // 事件沿检测
|
iot_evt_feed(sr); // 事件沿检测
|
||||||
memcpy(&_cached_sr, sr, sizeof(LUP_SensorReport)); // 刷新 loop_data 快照
|
memcpy(&_cached_sr, sr, sizeof(LUP_SensorReport)); // 刷新 loop_data 快照
|
||||||
_cached_sr_valid = 1;
|
_cached_sr_valid = 1;
|
||||||
|
snap_enqueue(sr); /* 快照入队: 中断安全, 主循环 snap_flush 落盘 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* lup 传感回调: uart_srv 消费路径的帧从这里喂入 (lup_process_frame 已过校验) */
|
/* lup 传感回调: uart_srv 消费路径的帧从这里喂入 (lup_process_frame 已过校验) */
|
||||||
@@ -774,6 +776,10 @@ void iot_mqtt_publish_sensor(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 快照落盘: 主循环上下文 (Step1 直读帧 + 回调帧都已在 ingest 入队);
|
||||||
|
放在 READY/enable 检查之前 → 断网/未使能期间照常落 (脱机取证语义) */
|
||||||
|
snap_flush();
|
||||||
|
|
||||||
/*--- 事件上报状态机: 发送/重发/重连补报 ---*/
|
/*--- 事件上报状态机: 发送/重发/重连补报 ---*/
|
||||||
iot_evt_process();
|
iot_evt_process();
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include "net_srv.h"
|
#include "net_srv.h"
|
||||||
#include "storage.h"
|
#include "storage.h"
|
||||||
#include "offlog.h"
|
#include "offlog.h"
|
||||||
|
#include "snapshot.h"
|
||||||
#include "tcp_json_srv.h"
|
#include "tcp_json_srv.h"
|
||||||
#include "loop_uart_proto.h"
|
#include "loop_uart_proto.h"
|
||||||
#include "iot_mqtt_srv.h"
|
#include "iot_mqtt_srv.h"
|
||||||
@@ -304,6 +305,7 @@ int main(void)
|
|||||||
GetMacAddr(gMacAddr);
|
GetMacAddr(gMacAddr);
|
||||||
storage_init();
|
storage_init();
|
||||||
offlog_init();
|
offlog_init();
|
||||||
|
snap_init();
|
||||||
|
|
||||||
/* 复位原因: 必须在初始化后立即读 (离复位时刻最近), 并清除标志,
|
/* 复位原因: 必须在初始化后立即读 (离复位时刻最近), 并清除标志,
|
||||||
保证下次上电只反映本次复位原因 (RCC_RSTSCKR, STM32F1/CH32V20x 兼容) */
|
保证下次上电只反映本次复位原因 (RCC_RSTSCKR, STM32F1/CH32V20x 兼容) */
|
||||||
|
|||||||
@@ -0,0 +1,408 @@
|
|||||||
|
/**
|
||||||
|
******************************************************************************
|
||||||
|
* @file snapshot.c
|
||||||
|
* @author wfq
|
||||||
|
* @version V1.0
|
||||||
|
* @date 2026-08-12
|
||||||
|
* @brief DBN 传感快照日志 — 环形实现 (与 offlog 同模式, 分区独立)
|
||||||
|
*
|
||||||
|
* 依赖: storage.c (SPI_Flash_*), cmcng.h (mstick), offlog.c (g_offlog_part + 审计)
|
||||||
|
* 线程模型: enqueue 在中断 (USART2 ISR), flush/write_raw/clear 在主循环。
|
||||||
|
* 所有 SPI 操作只在主循环 (扇区擦除 ~45ms 阻塞可接受, 与 offlog 同底线)。
|
||||||
|
******************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "snapshot.h"
|
||||||
|
#include "storage.h"
|
||||||
|
#include "cmcng.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* 编译期断言: 记录必须是 64B (定长环形索引依赖) */
|
||||||
|
typedef char snap_rec_size_must_be_64[(sizeof(SnapRec) == 64) ? 1 : -1];
|
||||||
|
typedef char snap_head_size_must_be_32[(sizeof(SnapHead) == 32) ? 1 : -1];
|
||||||
|
|
||||||
|
/*===========================================================================
|
||||||
|
* 内部状态 (仅主循环访问: init/flush/clear/查询)
|
||||||
|
*===========================================================================*/
|
||||||
|
static uint32_t _wr_off; /* 下一条记录写入的绝对地址 */
|
||||||
|
static uint16_t _wr_sector; /* 当前写扇区号 (区内 1..n) */
|
||||||
|
static uint32_t _wr_seq; /* 下一条记录的全局 seq */
|
||||||
|
static uint16_t _boot_seq; /* 当前 boot 序号 */
|
||||||
|
static uint32_t _count; /* 有效记录数 (0..max_records) */
|
||||||
|
static uint8_t _ready; /* init 完成标志 */
|
||||||
|
|
||||||
|
/* 中断安全 RAM 暂存 (单生产者=中断 enqueue / 单消费者=主循环 flush, 无锁)
|
||||||
|
生产者只写 g_snap_wr / g_snap_count, 消费者只读 g_snap_rd; count 满则丢新 */
|
||||||
|
static SnapRec g_snap_pending[SNAP_RAM_DEPTH];
|
||||||
|
static volatile uint8_t g_snap_rd; /* 消费者 (主循环) 读指针 */
|
||||||
|
static volatile uint8_t g_snap_wr; /* 生产者 (中断) 写指针 */
|
||||||
|
static volatile uint8_t g_snap_count; /* 暂存条数 (容量 = SNAP_RAM_DEPTH) */
|
||||||
|
static volatile uint32_t g_snap_drop; /* 满时丢弃计数 (调试用) */
|
||||||
|
|
||||||
|
/*===========================================================================
|
||||||
|
* 运行时分区表 (ROADMAP 2026-08-12: JEDEC ID + 事件区大小 → 快照区 = 剩余)
|
||||||
|
*===========================================================================*/
|
||||||
|
SnapPart g_snap_part = {
|
||||||
|
OFFLOG_CHIP_W25Q32, /* 默认 */
|
||||||
|
0x110000UL, /* W25Q32: 0x090000 + 512KB 事件区 */
|
||||||
|
0x2F0000UL, /* 3008KB = 4MB - 576KB - 512KB */
|
||||||
|
751, /* 3008KB/4096 - 1 头扇区 */
|
||||||
|
48064, /* 751 * 64 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 芯片总容量 (W25Q32 4MB → W25Q256 32MB) */
|
||||||
|
static uint32_t snap_chip_total(uint8_t chip)
|
||||||
|
{
|
||||||
|
switch (chip) {
|
||||||
|
case OFFLOG_CHIP_W25Q32: return 0x00400000UL;
|
||||||
|
case OFFLOG_CHIP_W25Q64: return 0x00800000UL;
|
||||||
|
case OFFLOG_CHIP_W25Q128: return 0x01000000UL;
|
||||||
|
case OFFLOG_CHIP_W25Q256: return 0x02000000UL;
|
||||||
|
default: return 0x00400000UL; /* 未知→默认 4MB */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 读 JEDEC ID → 填分区表。事件区大小与 offlog 同一事实来源 (g_offlog_part),
|
||||||
|
快照区 = 总容量 - 固定区 - 事件区。未知芯片按 W25Q32 兜底 */
|
||||||
|
static void snap_part_detect(void)
|
||||||
|
{
|
||||||
|
uint8_t dev = SPI_Flash_ReadJEDEC_ID();
|
||||||
|
uint8_t chip = OFFLOG_CHIP_W25Q32;
|
||||||
|
uint32_t evt_size;
|
||||||
|
uint32_t total;
|
||||||
|
|
||||||
|
switch (dev) {
|
||||||
|
case OFFLOG_CHIP_W25Q32: chip = dev; break;
|
||||||
|
case OFFLOG_CHIP_W25Q64: chip = dev; break;
|
||||||
|
case OFFLOG_CHIP_W25Q128: chip = dev; break;
|
||||||
|
case OFFLOG_CHIP_W25Q256: chip = dev; break;
|
||||||
|
default: chip = OFFLOG_CHIP_W25Q32; break;
|
||||||
|
}
|
||||||
|
evt_size = g_offlog_part.area_size; /* 与事件区严格一致 */
|
||||||
|
total = snap_chip_total(chip);
|
||||||
|
|
||||||
|
g_snap_part.chip = chip;
|
||||||
|
g_snap_part.area_base = SNAP_FIXED_SIZE + evt_size;
|
||||||
|
g_snap_part.area_size = total - SNAP_FIXED_SIZE - evt_size;
|
||||||
|
g_snap_part.data_sectors = (g_snap_part.area_size / 4096UL) - 1;
|
||||||
|
g_snap_part.max_records = g_snap_part.data_sectors * SNAP_REC_PER_SECTOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*===========================================================================
|
||||||
|
* 基础读写
|
||||||
|
*===========================================================================*/
|
||||||
|
static void snap_head_write(const SnapHead *h)
|
||||||
|
{
|
||||||
|
uint8_t buf[32];
|
||||||
|
memset(buf, 0xFF, sizeof(buf));
|
||||||
|
memcpy(buf, h, sizeof(SnapHead));
|
||||||
|
SPI_Flash_Write(buf, g_snap_part.area_base, sizeof(buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void snap_head_read(SnapHead *h)
|
||||||
|
{
|
||||||
|
uint8_t buf[32];
|
||||||
|
SPI_Flash_Read(buf, g_snap_part.area_base, sizeof(buf));
|
||||||
|
memcpy(h, buf, sizeof(SnapHead));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int snap_rec_read_at(uint32_t abs_off, SnapRec *out)
|
||||||
|
{
|
||||||
|
SPI_Flash_Read((uint8_t *)out, abs_off, SNAP_REC_SIZE);
|
||||||
|
return (out->magic == SNAP_MAGIC) ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*===========================================================================
|
||||||
|
* 上电扫描恢复 (与 offlog 同模式: 从头记录写位置向后扫, magic + seq 连续)
|
||||||
|
*===========================================================================*/
|
||||||
|
static void snap_scan_recover(const SnapHead *h)
|
||||||
|
{
|
||||||
|
uint32_t pos = h->wr_off;
|
||||||
|
uint32_t scanned = 0;
|
||||||
|
uint32_t expect_seq = h->wr_seq;
|
||||||
|
SnapRec r;
|
||||||
|
|
||||||
|
/* 防御: wr_off 必须在数据区范围内且 64B 对齐 */
|
||||||
|
if (pos < SNAP_DATA_BASE || pos >= g_snap_part.area_base + g_snap_part.area_size) {
|
||||||
|
pos = SNAP_DATA_BASE;
|
||||||
|
}
|
||||||
|
if ((pos - SNAP_DATA_BASE) % SNAP_REC_SIZE != 0) {
|
||||||
|
pos = SNAP_DATA_BASE;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (scanned < SNAP_MAX_RECORDS) {
|
||||||
|
if (snap_rec_read_at(pos, &r) != 0) {
|
||||||
|
break; /* 找到真实写位置 */
|
||||||
|
}
|
||||||
|
if (expect_seq != 0 && r.seq != expect_seq) {
|
||||||
|
break; /* seq 不连续 (损坏/残留) → 视为写位置 */
|
||||||
|
}
|
||||||
|
pos += SNAP_REC_SIZE;
|
||||||
|
if (pos >= SNAP_DATA_BASE + SNAP_DATA_SIZE) {
|
||||||
|
pos = SNAP_DATA_BASE; /* 回绕数据区 */
|
||||||
|
}
|
||||||
|
scanned++;
|
||||||
|
expect_seq++;
|
||||||
|
}
|
||||||
|
|
||||||
|
_wr_off = pos;
|
||||||
|
_wr_sector = (uint16_t)((pos - SNAP_DATA_BASE) / 4096) + 1; /* 区内 1..n */
|
||||||
|
_wr_seq = expect_seq;
|
||||||
|
_count = h->count + scanned;
|
||||||
|
if (_count > SNAP_MAX_RECORDS) _count = SNAP_MAX_RECORDS;
|
||||||
|
_boot_seq = (uint16_t)(h->boot_seq + 1);
|
||||||
|
if (_boot_seq == 0) _boot_seq = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*===========================================================================
|
||||||
|
* 初始化
|
||||||
|
*===========================================================================*/
|
||||||
|
void snap_init(void)
|
||||||
|
{
|
||||||
|
SnapHead h;
|
||||||
|
|
||||||
|
snap_part_detect(); /* JEDEC ID + 事件区大小 → 快照区容量 */
|
||||||
|
memset(&h, 0xFF, sizeof(h));
|
||||||
|
snap_head_read(&h);
|
||||||
|
|
||||||
|
if (!(h.magic[0] == SNAP_HEAD_MAGIC0 && h.magic[1] == SNAP_HEAD_MAGIC1 &&
|
||||||
|
h.magic[2] == SNAP_HEAD_MAGIC2 && h.magic[3] == SNAP_HEAD_MAGIC3)) {
|
||||||
|
/* 全新区: 擦全部数据扇区, 写头 */
|
||||||
|
uint32_t i;
|
||||||
|
for (i = 0; i < SNAP_DATA_SECTOR_CNT; i++) {
|
||||||
|
SPI_Flash_Erase_Sector(SNAP_DATA_BASE / 4096 + i);
|
||||||
|
}
|
||||||
|
memset(&h, 0xFF, sizeof(h));
|
||||||
|
h.magic[0] = SNAP_HEAD_MAGIC0;
|
||||||
|
h.magic[1] = SNAP_HEAD_MAGIC1;
|
||||||
|
h.magic[2] = SNAP_HEAD_MAGIC2;
|
||||||
|
h.magic[3] = SNAP_HEAD_MAGIC3;
|
||||||
|
h.boot_seq = 0;
|
||||||
|
h.wr_sector = 1;
|
||||||
|
h.wr_off = SNAP_DATA_BASE;
|
||||||
|
h.wr_seq = 0;
|
||||||
|
h.count = 0;
|
||||||
|
snap_head_write(&h);
|
||||||
|
|
||||||
|
_wr_off = SNAP_DATA_BASE;
|
||||||
|
_wr_sector = 1;
|
||||||
|
_wr_seq = 1;
|
||||||
|
_count = 0;
|
||||||
|
_boot_seq = 1;
|
||||||
|
} else {
|
||||||
|
snap_scan_recover(&h);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 回写头: boot_seq 递增 */
|
||||||
|
h.magic[0] = SNAP_HEAD_MAGIC0;
|
||||||
|
h.magic[1] = SNAP_HEAD_MAGIC1;
|
||||||
|
h.magic[2] = SNAP_HEAD_MAGIC2;
|
||||||
|
h.magic[3] = SNAP_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;
|
||||||
|
snap_head_write(&h);
|
||||||
|
|
||||||
|
g_snap_rd = 0;
|
||||||
|
g_snap_wr = 0;
|
||||||
|
g_snap_count = 0;
|
||||||
|
g_snap_drop = 0;
|
||||||
|
_ready = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*===========================================================================
|
||||||
|
* 落盘 (主循环上下文)
|
||||||
|
*===========================================================================*/
|
||||||
|
static void snap_flush_head(void)
|
||||||
|
{
|
||||||
|
SnapHead h;
|
||||||
|
h.magic[0] = SNAP_HEAD_MAGIC0;
|
||||||
|
h.magic[1] = SNAP_HEAD_MAGIC1;
|
||||||
|
h.magic[2] = SNAP_HEAD_MAGIC2;
|
||||||
|
h.magic[3] = SNAP_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;
|
||||||
|
snap_head_write(&h);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 目标扇区是否已有数据 (环形覆盖判定): 读首条记录 magic */
|
||||||
|
static int snap_sector_has_data(uint32_t abs_sec_off)
|
||||||
|
{
|
||||||
|
SnapRec r;
|
||||||
|
SPI_Flash_Read((uint8_t *)&r, abs_sec_off, SNAP_REC_SIZE);
|
||||||
|
return (r.magic == SNAP_MAGIC) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 写一条记录 (仅主循环; rec->seq 已由 snap_flush 分配) */
|
||||||
|
static void snap_write_raw(const SnapRec *rec)
|
||||||
|
{
|
||||||
|
/* 环形回绕: 写指针越过数据区末尾 → 回到区首 */
|
||||||
|
if (_wr_off >= SNAP_DATA_BASE + SNAP_DATA_SIZE) {
|
||||||
|
_wr_off = SNAP_DATA_BASE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 切扇区判定: 写指针所在扇区与 _wr_sector 不一致 → 擦除目标扇区 */
|
||||||
|
{
|
||||||
|
uint32_t sec_index = (_wr_off - SNAP_DATA_BASE) / 4096; /* 0..n-2 */
|
||||||
|
if ((uint32_t)(_wr_sector - 1) != sec_index) {
|
||||||
|
uint32_t target_abs = (SNAP_DATA_BASE / 4096 + sec_index) * 4096;
|
||||||
|
if (snap_sector_has_data(target_abs)) {
|
||||||
|
uint32_t erased = (_count > SNAP_REC_PER_SECTOR)
|
||||||
|
? SNAP_REC_PER_SECTOR : _count;
|
||||||
|
_count -= erased;
|
||||||
|
}
|
||||||
|
SPI_Flash_Erase_Sector(target_abs / 4096);
|
||||||
|
_wr_sector = (uint16_t)(sec_index + 1); /* 区内 1..n */
|
||||||
|
snap_flush_head(); /* 扇区切换 → 更新头 (掉电恢复锚点) */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SPI_Flash_Write_NoCheck((uint8_t *)rec, _wr_off, SNAP_REC_SIZE);
|
||||||
|
_wr_off += SNAP_REC_SIZE;
|
||||||
|
_wr_seq++;
|
||||||
|
if (_count < SNAP_MAX_RECORDS) _count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*===========================================================================
|
||||||
|
* 打包 (与 0xC0 线上线圈单元 12B 格式一致)
|
||||||
|
*===========================================================================*/
|
||||||
|
static void snap_pack_coil(const LUP_CoilSensor *cs, uint8_t *o)
|
||||||
|
{
|
||||||
|
o[0] = (uint8_t)((cs->freq_level << 6) | (cs->direction << 5)
|
||||||
|
| (cs->freq_type << 4) | (cs->sensitivity & 0x0F));
|
||||||
|
o[1] = (uint8_t)((cs->condition << 4) | (cs->loop_state << 3)
|
||||||
|
| (cs->car_state << 2) | (cs->misc_type & 0x03));
|
||||||
|
o[2] = (uint8_t)(cs->freq & 0xFF);
|
||||||
|
o[3] = (uint8_t)((cs->freq >> 8) & 0xFF);
|
||||||
|
o[4] = (uint8_t)((cs->freq >> 16) & 0xFF);
|
||||||
|
o[5] = (uint8_t)(cs->variation & 0xFF);
|
||||||
|
o[6] = (uint8_t)((cs->variation >> 8) & 0xFF);
|
||||||
|
o[7] = (uint8_t)((cs->variation >> 16) & 0xFF);
|
||||||
|
o[8] = (uint8_t)(cs->misc.passtime_ms & 0xFF);
|
||||||
|
o[9] = (uint8_t)((cs->misc.passtime_ms >> 8) & 0xFF);
|
||||||
|
o[10] = (uint8_t)((cs->misc.passtime_ms >> 16) & 0xFF);
|
||||||
|
o[11] = (uint8_t)((cs->misc.passtime_ms >> 24) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 打包传感帧 → SnapRec (seq 留待 flush 时分配; ts_ms/boot_seq 用采集时刻) */
|
||||||
|
static void snap_pack(const LUP_SensorReport *sr, SnapRec *rec)
|
||||||
|
{
|
||||||
|
uint8_t i, n;
|
||||||
|
|
||||||
|
memset(rec, 0, sizeof(*rec));
|
||||||
|
rec->magic = SNAP_MAGIC;
|
||||||
|
n = (sr->coil_count > SNAP_COILS_MAX) ? SNAP_COILS_MAX : sr->coil_count;
|
||||||
|
rec->len = n * SNAP_COIL_BYTES;
|
||||||
|
rec->ts_ms = mstick();
|
||||||
|
rec->boot_seq = _boot_seq;
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
snap_pack_coil(&sr->coils[i], rec->coils + i * SNAP_COIL_BYTES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*===========================================================================
|
||||||
|
* 中断安全入队 (USART2 ISR 上下文; 无 SPI, 无阻塞; 满丢新不覆盖)
|
||||||
|
* - 生产者只写 g_snap_wr / g_snap_pending / g_snap_count
|
||||||
|
* - 消费者只读 g_snap_rd; count 为 volatile 字节, 单核单指令读写天然原子
|
||||||
|
* - 满判定: count >= DEPTH (容量 = SNAP_RAM_DEPTH)
|
||||||
|
*===========================================================================*/
|
||||||
|
void snap_enqueue(const LUP_SensorReport *sr)
|
||||||
|
{
|
||||||
|
SnapRec rec;
|
||||||
|
|
||||||
|
if (!_ready || sr == NULL) return;
|
||||||
|
|
||||||
|
snap_pack(sr, &rec);
|
||||||
|
|
||||||
|
if (g_snap_count >= SNAP_RAM_DEPTH) {
|
||||||
|
g_snap_drop++; /* 暂存满 → 丢新帧 (快照环形, 可接受) */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_snap_pending[g_snap_wr] = rec;
|
||||||
|
g_snap_wr = (uint8_t)((g_snap_wr + 1) & (SNAP_RAM_DEPTH - 1));
|
||||||
|
g_snap_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*===========================================================================
|
||||||
|
* 主循环落盘 (iot_mqtt_publish_sensor 每轮调用; 断网/未使能照常落)
|
||||||
|
* - seq 在主循环分配 (中断不碰 _wr_seq, 避免共享状态)
|
||||||
|
*===========================================================================*/
|
||||||
|
void snap_flush(void)
|
||||||
|
{
|
||||||
|
if (!_ready) return;
|
||||||
|
|
||||||
|
while (g_snap_count > 0) {
|
||||||
|
SnapRec *rec = &g_snap_pending[g_snap_rd];
|
||||||
|
rec->seq = _wr_seq; /* 主循环独占分配 */
|
||||||
|
snap_write_raw(rec);
|
||||||
|
g_snap_rd = (uint8_t)((g_snap_rd + 1) & (SNAP_RAM_DEPTH - 1));
|
||||||
|
g_snap_count--;
|
||||||
|
}
|
||||||
|
if (g_snap_drop != 0) {
|
||||||
|
g_snap_drop = 0; /* 丢弃计数清零 (调试观察点) */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*===========================================================================
|
||||||
|
* 查询
|
||||||
|
*===========================================================================*/
|
||||||
|
uint8_t snap_enabled(void)
|
||||||
|
{
|
||||||
|
return _ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t snap_count(void)
|
||||||
|
{
|
||||||
|
return _count;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t snap_seq_last(void)
|
||||||
|
{
|
||||||
|
return (_wr_seq > 0) ? (_wr_seq - 1) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t snap_boot_seq(void)
|
||||||
|
{
|
||||||
|
return _boot_seq;
|
||||||
|
}
|
||||||
|
|
||||||
|
int snap_read_idx(uint32_t idx, SnapRec *out)
|
||||||
|
{
|
||||||
|
uint32_t cnt = _count;
|
||||||
|
if (!_ready || idx >= cnt) return -1;
|
||||||
|
|
||||||
|
/* 逻辑首 = 写位置向前 cnt 条 (环形) */
|
||||||
|
uint32_t phys = (_wr_off - SNAP_DATA_BASE); /* 0..DATA_SIZE */
|
||||||
|
uint32_t start = (phys + SNAP_DATA_SIZE - cnt * SNAP_REC_SIZE) % SNAP_DATA_SIZE;
|
||||||
|
uint32_t read_off = SNAP_DATA_BASE + (start + idx * SNAP_REC_SIZE) % SNAP_DATA_SIZE;
|
||||||
|
|
||||||
|
return snap_rec_read_at(read_off, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*===========================================================================
|
||||||
|
* 清空 (审计: 事件流记一条 LOG_CLEAR, payload[0]=2=快照流)
|
||||||
|
*===========================================================================*/
|
||||||
|
void snap_clear(void)
|
||||||
|
{
|
||||||
|
uint32_t i;
|
||||||
|
if (!_ready) return;
|
||||||
|
|
||||||
|
for (i = 0; i < SNAP_DATA_SECTOR_CNT; i++) {
|
||||||
|
SPI_Flash_Erase_Sector(SNAP_DATA_BASE / 4096 + i);
|
||||||
|
}
|
||||||
|
_wr_off = SNAP_DATA_BASE;
|
||||||
|
_wr_sector = 1;
|
||||||
|
_count = 0;
|
||||||
|
/* _wr_seq / _boot_seq 不重置: 序号单调递增, 保证快照全局唯一 */
|
||||||
|
|
||||||
|
{
|
||||||
|
uint8_t p[1] = {2}; /* 2 = 快照流 (审计区分) */
|
||||||
|
offlog_evt(OFFLOG_EVT_LOG_CLEAR, p, 1);
|
||||||
|
}
|
||||||
|
snap_flush_head();
|
||||||
|
}
|
||||||
@@ -6,6 +6,34 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-08-12 — 传感快照区落地(snapshot.c,BLE 新增 0x28/0x29/0x2A)
|
||||||
|
|
||||||
|
### 背景
|
||||||
|
|
||||||
|
ROADMAP P1.2 分区规划(参数区→OTA→事件日志区→传感快照区)中,事件区已落地(offlog),快照区此前为预留。本次实现快照流:0xC0 传感帧按上报节奏落盘,断网期间波形照常记录,可离线回放。
|
||||||
|
|
||||||
|
### 关键设计决策
|
||||||
|
|
||||||
|
1. **64B 定长记录 SnapRec**:头部 16B(magic=0xA6/len/flags/seq/ts_ms/boot_seq)+ 4×12B 线圈数据(与 0xC0 线上格式逐字节一致,可直接对照抓包)→ W25Q32 下 48064 条。
|
||||||
|
2. **线程安全(踩坑预警)**:`iot_sensor_ingest()` 在 **USART2 ISR 上下文**(`loop_uart_proto.h:209` 明确标注)被调用!SPI 擦除 ~45ms 绝不能在中断里做 → 中断只打包+入 RAM 暂存(8 深,满丢新),主循环 `iot_mqtt_publish_sensor()` 每轮 `snap_flush()` 落盘。单生产者(中断)单消费者(主循环)count 计数环形,volatile 字节天然原子。
|
||||||
|
3. **分区表同一事实来源**:快照区 = 总容量 − 固定区 576KB − 事件区(`g_offlog_part.area_size`),杜绝两模块对事件区大小理解不一致。
|
||||||
|
4. **审计**:快照清除写事件流 `log_clear`(payload[0]=2=快照流)。
|
||||||
|
5. **BLE 指令对齐 OFFLOG 模式**:SNAP_STAT(0x28)/SNAP_QUERY(0x29)/SNAP_CLEAR(0x2A);QUERY 上限 2 条(64×2+2=130B ≤ 单包缓冲,ODR 教训防御)。
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
`test_snapshot.c` 8 例全过(中断安全/打包格式/环形回绕/掉电恢复/扇区切换/暂存满丢新/清空审计/分区表);`test_offlog` 8 例、`test_ble_offlog` 7 例回归全过。
|
||||||
|
|
||||||
|
> ⚠ 测试踩坑:不要在同文件 include offlog.c + snapshot.c —— 两个 .c 的 static 变量(如 `_wr_off`)在同一翻译单元**符号冲突**,快照 clear 审计会污染事件写指针。测试改为 mock `g_offlog_part` + `offlog_evt`。
|
||||||
|
|
||||||
|
### 待板上验证
|
||||||
|
|
||||||
|
- 快照区 0x110000 起、W25Q32 48064 条 capacity 打印
|
||||||
|
- 中断风暴下暂存满丢新行为(观察点:`g_snap_drop`)
|
||||||
|
- 掉电恢复跨扇区扫描
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-08-12 — BLE 分包粒度动态化修复(Too large noti 丢包)
|
## 2026-08-12 — BLE 分包粒度动态化修复(Too large noti 丢包)
|
||||||
|
|
||||||
### 背景
|
### 背景
|
||||||
|
|||||||
@@ -0,0 +1,353 @@
|
|||||||
|
/**
|
||||||
|
* test_snapshot.c — 传感快照日志 gcc 隔离单测
|
||||||
|
*
|
||||||
|
* 编译: gcc -I../BLE/OnlyUpdateApp_Peripheral/APP/include \
|
||||||
|
* -o test_snapshot test_snapshot.c
|
||||||
|
* 运行: ./test_snapshot
|
||||||
|
*
|
||||||
|
* Mock: W25Q32 NOR 行为 (写=AND, 擦=0xFF), mstick 固定值,
|
||||||
|
* g_offlog_part (事件区 512KB), offlog_evt (审计记录 mock)
|
||||||
|
* 注意: 不 include offlog.c — 两个 .c 的 static 变量 (如 _wr_off) 在
|
||||||
|
* 同一翻译单元会符号冲突 (踩过: snap_clear 审计写乱了快照写指针)
|
||||||
|
* 覆盖: 全新初始化 / 中断安全(enqueue 不落盘) / flush 落盘 / 打包格式
|
||||||
|
* 环形回绕 / 掉电恢复 / 扇区切换 / 暂存满丢新 / 清空审计 / 分区表
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "../BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h"
|
||||||
|
|
||||||
|
/* ============ 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 */
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t SPI_Flash_ReadJEDEC_ID(void) { return 0x16; } /* mock W25Q32 */
|
||||||
|
|
||||||
|
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 时间 ============ */
|
||||||
|
static uint32_t mock_ms = 0;
|
||||||
|
uint32_t mstick(void) { return mock_ms; }
|
||||||
|
|
||||||
|
/* ============ mock offlog 依赖 (不 include offlog.c, 避免 static 冲突) ============ */
|
||||||
|
OfflogPart g_offlog_part = {
|
||||||
|
OFFLOG_CHIP_W25Q32, /* W25Q32 */
|
||||||
|
0x80000UL, /* 事件区 512KB */
|
||||||
|
127,
|
||||||
|
16256,
|
||||||
|
};
|
||||||
|
static uint8_t g_audit_type;
|
||||||
|
static uint8_t g_audit_payload[4];
|
||||||
|
static uint8_t g_audit_len;
|
||||||
|
void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len)
|
||||||
|
{
|
||||||
|
g_audit_type = type;
|
||||||
|
g_audit_len = len;
|
||||||
|
if (payload && len > 0 && len <= 4) memcpy(g_audit_payload, payload, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ch32v20x 库类型 (单测环境无 SDK) */
|
||||||
|
typedef unsigned char u8;
|
||||||
|
|
||||||
|
/* ============ 被测模块 (直接包含实现) ============ */
|
||||||
|
#include "../BLE/OnlyUpdateApp_Peripheral/APP/snapshot.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 构造一帧传感报告: 4 线圈, 各线圈填可辨识值 */
|
||||||
|
static void make_report(LUP_SensorReport *sr, uint32_t base_freq, int32_t base_var)
|
||||||
|
{
|
||||||
|
uint8_t i;
|
||||||
|
memset(sr, 0, sizeof(*sr));
|
||||||
|
sr->sens_type = 0x0C;
|
||||||
|
sr->coil_count = 4;
|
||||||
|
for (i = 0; i < 4; i++) {
|
||||||
|
sr->coils[i].freq_level = 2; /* 中低 66nF */
|
||||||
|
sr->coils[i].direction = 1;
|
||||||
|
sr->coils[i].freq_type = 0;
|
||||||
|
sr->coils[i].sensitivity = 9;
|
||||||
|
sr->coils[i].condition = 5;
|
||||||
|
sr->coils[i].loop_state = 1;
|
||||||
|
sr->coils[i].car_state = (i & 1); /* 0/1 交替 */
|
||||||
|
sr->coils[i].misc_type = 0;
|
||||||
|
sr->coils[i].freq = base_freq + i * 1000;
|
||||||
|
sr->coils[i].variation = base_var + i * 100;
|
||||||
|
sr->coils[i].misc.passtime_ms = 0x10000000 + i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 测试1: 全新初始化 → enqueue 不落盘(中断安全), flush 落盘可读回 */
|
||||||
|
static void test_fresh_init_and_flush(void)
|
||||||
|
{
|
||||||
|
LUP_SensorReport sr;
|
||||||
|
SnapRec r;
|
||||||
|
uint32_t i;
|
||||||
|
|
||||||
|
reset_flash();
|
||||||
|
snap_init();
|
||||||
|
CHECK(snap_enabled() == 1);
|
||||||
|
CHECK(snap_count() == 0);
|
||||||
|
|
||||||
|
make_report(&sr, 0x123456, 1000);
|
||||||
|
mock_ms = 12345;
|
||||||
|
|
||||||
|
/* 中断路径: 只 enqueue, 不 flush → flash 不得有任何快照数据 */
|
||||||
|
snap_enqueue(&sr);
|
||||||
|
CHECK(snap_count() == 0); /* 未落盘 */
|
||||||
|
for (i = 0; i < 64; i++) {
|
||||||
|
CHECK(flash[SNAP_DATA_BASE + i] == 0xFF); /* 数据区仍全 FF */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主循环: flush → 落盘 1 条 */
|
||||||
|
snap_flush();
|
||||||
|
CHECK(snap_count() == 1);
|
||||||
|
CHECK(snap_read_idx(0, &r) == 0);
|
||||||
|
CHECK(r.magic == SNAP_MAGIC);
|
||||||
|
CHECK(r.len == 48); /* 4 线圈 × 12B */
|
||||||
|
CHECK(r.seq == 1);
|
||||||
|
CHECK(r.boot_seq == 1);
|
||||||
|
CHECK(r.ts_ms == 12345);
|
||||||
|
CHECK(snap_seq_last() == 1);
|
||||||
|
|
||||||
|
printf("PASS test_fresh_init_and_flush\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 测试2: 打包格式与 0xC0 线上线圈单元 12B 一致 */
|
||||||
|
static void test_pack_format(void)
|
||||||
|
{
|
||||||
|
LUP_SensorReport sr;
|
||||||
|
SnapRec r;
|
||||||
|
|
||||||
|
reset_flash();
|
||||||
|
snap_init();
|
||||||
|
make_report(&sr, 0x123456, -12345); /* 负变化量 */
|
||||||
|
/* 覆盖值: freq_level=2 direction=1 freq_type=0 sens=9 → o[0]=0xA9
|
||||||
|
condition=5 loop=1 car=0 misc=3 → o[1]=0x5B
|
||||||
|
freq=0x123456 → 56 34 12
|
||||||
|
variation=-12345 → 0xFFFFCFC7 截 24bit → C7 CF FF
|
||||||
|
misc=0x10000000 → 00 00 00 10 */
|
||||||
|
sr.coils[0].freq = 0x123456;
|
||||||
|
sr.coils[0].variation = -12345;
|
||||||
|
sr.coils[0].misc_type = 3;
|
||||||
|
sr.coils[0].misc.relay_count = 0x10000000;
|
||||||
|
|
||||||
|
snap_enqueue(&sr);
|
||||||
|
snap_flush();
|
||||||
|
snap_read_idx(0, &r);
|
||||||
|
|
||||||
|
CHECK(r.coils[0] == 0xA9);
|
||||||
|
CHECK(r.coils[1] == 0x5B);
|
||||||
|
CHECK(r.coils[2] == 0x56 && r.coils[3] == 0x34 && r.coils[4] == 0x12);
|
||||||
|
CHECK(r.coils[5] == 0xC7 && r.coils[6] == 0xCF && r.coils[7] == 0xFF);
|
||||||
|
CHECK(r.coils[8] == 0x00 && r.coils[9] == 0x00 && r.coils[10] == 0x00 && r.coils[11] == 0x10);
|
||||||
|
|
||||||
|
/* 线圈 2: car_state=1 → o[1] bit2=1 → 0x5F (其他字段相同) */
|
||||||
|
CHECK((r.coils[12 + 1] & 0x04) == 0x04);
|
||||||
|
CHECK((r.coils[0 + 1] & 0x04) == 0x00);
|
||||||
|
|
||||||
|
printf("PASS test_pack_format\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 测试3: 环形回绕 — 写满 max+5 条, count 封顶, 序号单调 */
|
||||||
|
static void test_ring_wrap(void)
|
||||||
|
{
|
||||||
|
LUP_SensorReport sr;
|
||||||
|
SnapRec r;
|
||||||
|
uint32_t i, n = SNAP_MAX_RECORDS + 5;
|
||||||
|
|
||||||
|
reset_flash();
|
||||||
|
snap_init();
|
||||||
|
make_report(&sr, 0x100, 100);
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
snap_enqueue(&sr);
|
||||||
|
snap_flush();
|
||||||
|
}
|
||||||
|
CHECK(snap_count() == SNAP_MAX_RECORDS - SNAP_REC_PER_SECTOR + 5);
|
||||||
|
CHECK(snap_read_idx(snap_count() - 1, &r) == 0);
|
||||||
|
CHECK(r.seq == n);
|
||||||
|
CHECK(snap_seq_last() == n);
|
||||||
|
CHECK(snap_read_idx(snap_count(), &r) == -1); /* 越界 */
|
||||||
|
printf("PASS test_ring_wrap (count %lu, max %lu)\n",
|
||||||
|
(unsigned long)snap_count(), (unsigned long)SNAP_MAX_RECORDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 测试4: 掉电恢复 — 写 10 条后重新 init, boot_seq 递增, seq 连续 */
|
||||||
|
static void test_power_loss_recovery(void)
|
||||||
|
{
|
||||||
|
LUP_SensorReport sr;
|
||||||
|
SnapRec r;
|
||||||
|
uint32_t i;
|
||||||
|
|
||||||
|
reset_flash();
|
||||||
|
snap_init();
|
||||||
|
make_report(&sr, 0x200, 200);
|
||||||
|
for (i = 0; i < 10; i++) {
|
||||||
|
snap_enqueue(&sr);
|
||||||
|
snap_flush();
|
||||||
|
}
|
||||||
|
CHECK(snap_count() == 10);
|
||||||
|
|
||||||
|
snap_init(); /* 模拟掉电重启 */
|
||||||
|
CHECK(snap_boot_seq() == 2);
|
||||||
|
CHECK(snap_count() == 10);
|
||||||
|
|
||||||
|
snap_enqueue(&sr);
|
||||||
|
snap_flush();
|
||||||
|
CHECK(snap_count() == 11);
|
||||||
|
CHECK(snap_read_idx(10, &r) == 0);
|
||||||
|
CHECK(r.seq == 11);
|
||||||
|
CHECK(r.boot_seq == 2);
|
||||||
|
printf("PASS test_power_loss_recovery\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 测试5: 扇区切换 — 写满 64 条后头扇区更新, 数据完整 */
|
||||||
|
static void test_sector_switch(void)
|
||||||
|
{
|
||||||
|
LUP_SensorReport sr;
|
||||||
|
SnapRec r;
|
||||||
|
SnapHead h;
|
||||||
|
uint32_t i;
|
||||||
|
|
||||||
|
reset_flash();
|
||||||
|
snap_init();
|
||||||
|
make_report(&sr, 0x300, 300);
|
||||||
|
for (i = 0; i < 64; i++) {
|
||||||
|
snap_enqueue(&sr);
|
||||||
|
snap_flush();
|
||||||
|
}
|
||||||
|
snap_enqueue(&sr);
|
||||||
|
snap_flush(); /* 第 65 条触发扇区切换 */
|
||||||
|
|
||||||
|
CHECK(snap_count() == 65);
|
||||||
|
SPI_Flash_Read((uint8_t *)&h, g_snap_part.area_base, sizeof(h));
|
||||||
|
CHECK(h.magic[0] == SNAP_HEAD_MAGIC0);
|
||||||
|
CHECK(h.wr_sector == 2);
|
||||||
|
CHECK(h.wr_off == SNAP_DATA_BASE + 4096);
|
||||||
|
|
||||||
|
CHECK(snap_read_idx(63, &r) == 0);
|
||||||
|
CHECK(r.seq == 64);
|
||||||
|
CHECK(snap_read_idx(64, &r) == 0);
|
||||||
|
CHECK(r.seq == 65);
|
||||||
|
printf("PASS test_sector_switch\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 测试6: RAM 暂存满丢新 — 8 深, 第 9 条丢, flush 只落 8 条 */
|
||||||
|
static void test_ram_overflow_drop(void)
|
||||||
|
{
|
||||||
|
LUP_SensorReport sr;
|
||||||
|
SnapRec r;
|
||||||
|
uint32_t i;
|
||||||
|
|
||||||
|
reset_flash();
|
||||||
|
snap_init();
|
||||||
|
make_report(&sr, 0x400, 400);
|
||||||
|
|
||||||
|
for (i = 0; i < SNAP_RAM_DEPTH + 3; i++) { /* 11 条, 深 8 */
|
||||||
|
snap_enqueue(&sr); /* 不 flush, 模拟中断风暴 */
|
||||||
|
}
|
||||||
|
CHECK(snap_count() == 0); /* 全部还在 RAM */
|
||||||
|
snap_flush();
|
||||||
|
CHECK(snap_count() == 8); /* 只落 8 条, 丢 3 条 */
|
||||||
|
CHECK(snap_read_idx(0, &r) == 0);
|
||||||
|
CHECK(r.seq == 1); /* 丢的是最新(尾部) */
|
||||||
|
CHECK(snap_read_idx(7, &r) == 0);
|
||||||
|
CHECK(r.seq == 8);
|
||||||
|
printf("PASS test_ram_overflow_drop\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 测试7: 清空 — 擦数据区 + 事件流审计 (payload[0]=2=快照流) */
|
||||||
|
static void test_clear_audit(void)
|
||||||
|
{
|
||||||
|
LUP_SensorReport sr;
|
||||||
|
SnapRec r;
|
||||||
|
uint32_t i;
|
||||||
|
|
||||||
|
reset_flash();
|
||||||
|
g_audit_type = 0; g_audit_len = 0; memset(g_audit_payload, 0, sizeof(g_audit_payload));
|
||||||
|
snap_init();
|
||||||
|
make_report(&sr, 0x500, 500);
|
||||||
|
for (i = 0; i < 5; i++) {
|
||||||
|
snap_enqueue(&sr);
|
||||||
|
snap_flush();
|
||||||
|
}
|
||||||
|
CHECK(snap_count() == 5);
|
||||||
|
|
||||||
|
snap_clear();
|
||||||
|
CHECK(snap_count() == 0);
|
||||||
|
CHECK(snap_seq_last() == 5); /* seq 不重置 */
|
||||||
|
CHECK(snap_read_idx(0, &r) == -1); /* 数据区已空 */
|
||||||
|
|
||||||
|
/* 审计: offlog_evt 被调用, type=LOG_CLEAR, payload[0]=2 */
|
||||||
|
CHECK(g_audit_type == OFFLOG_EVT_LOG_CLEAR);
|
||||||
|
CHECK(g_audit_len == 1);
|
||||||
|
CHECK(g_audit_payload[0] == 2);
|
||||||
|
printf("PASS test_clear_audit\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 测试8: 运行时分区 — W25Q32 快照区 0x110000, 3008KB, 48064 条 */
|
||||||
|
static void test_part_table(void)
|
||||||
|
{
|
||||||
|
reset_flash();
|
||||||
|
snap_init();
|
||||||
|
CHECK(g_snap_part.area_base == 0x110000UL);
|
||||||
|
CHECK(g_snap_part.area_size == 0x2F0000UL); /* 3008KB */
|
||||||
|
CHECK(g_snap_part.data_sectors == 751);
|
||||||
|
CHECK(g_snap_part.max_records == 751 * 64);
|
||||||
|
CHECK(SNAP_DATA_BASE == 0x111000UL); /* base + 4KB 头扇区 */
|
||||||
|
printf("PASS test_part_table\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
test_fresh_init_and_flush();
|
||||||
|
test_pack_format();
|
||||||
|
test_ring_wrap();
|
||||||
|
test_power_loss_recovery();
|
||||||
|
test_sector_switch();
|
||||||
|
test_ram_overflow_drop();
|
||||||
|
test_clear_audit();
|
||||||
|
test_part_table();
|
||||||
|
|
||||||
|
if (failures) {
|
||||||
|
printf("\n%d FAILURE(S)\n", failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("\nALL PASS\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user