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:
@@ -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