Files
STM32F4-Base/App/algo/wave_store.c
T
wandering1 4a4ad6bc5f chore(全仓): @build/@test 旧文档别名清退为技能调用名
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名,
不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体
run_boot_test.py 的真实调用。

- AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改
  「编译(/keil-build)」「串口回归(/serial-boot-test)」
- keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree
  方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围)
- 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入
  等 41 行历史证据保留旧名
- gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑
- 顺手补并行方案同行漏网计数 51→52 钩子

验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all
rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52
钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
2026-10-30 13:03:12 +08:00

982 lines
38 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:录波段格式层实现(故障录波 · 段文件字节布局编解码 + 打包游标 + 段名轮转)
* 模块功能:见 App/algo/wave_store.h 的文件头注释块——口径属主、职责边界、跨任务安全、
* 二期不返工的约束全部写在那里,本文件不重复一遍。
* 本文件是纯逻辑:只 include App/algo/wave_store.h 与 C 标准库的 stdint/stddef,
* 不 include HAL / CMSIS / RTOS 头、不调 FatFs、不碰文件系统、不打日志。
* 适用平台:STM32F407ZGTx(Cortex-M4,小端);主机侧单测 test/host/wave_store/
* 作者:DTU 项目组
* 创建日期:2026-10-29(故障录波 task-1 · 数据面纯逻辑层)
* 修改记录:
* v1.0 2026-10-29 DTU 项目组 创建
*/
#include "wave_store.h"
/* ARMCC 的 stdint.h 不提供 NULL,而主机侧 gcc / MSVC 会传递性把它拉进来 ——
* 这个差别主机单测照不出来(2026-10-29 台基线实跑 /keil-build 才暴露:本层
* wave_store.c 报 22 条 identifier "NULL" 未定义)。与 App/algo/sample_ring.c、App/algo/freq.c
* 同一口径:谁用 NULL 谁自己 include stddef.h,不往公共头里塞(少波及别人的 TU)。 */
#include <stddef.h>
/* 打包游标一次最多从数据源取多少个点(128 字节栈上临时区,落盘道在 defaultTask 上跑,
* 不在 1 kHz 采集链上;取批是为了把「每点一次回调」压成「每 64 点一次回调」) */
#define WAVE_ST_BATCH (64u)
/* 数据区点下标非法时的内部哨兵(对外一律走 wave_store_data_off 的 0xFFFFFFFFu) */
#define WAVE_ST_BAD_OFF (0xFFFFFFFFu)
/* ===== 内部:小端读写(显式移位拼装,不依赖宿主字节序,板端与 PC 侧同一判据) ===== */
/*
* 函数功能:写 16 位小端
* 入口参数:p 目标位置(至少 2 字节);v 值
* 返 回 值:无 void
* 限定条件:调用方保证 p 可写 2 字节
*/
static void wave_st_put16(uint8_t *p, uint16_t v) {
p[0] = (uint8_t)(v & 0xFFu);
p[1] = (uint8_t)((v >> 8) & 0xFFu);
}
/*
* 函数功能:写 32 位小端
* 入口参数:p 目标位置(至少 4 字节);v 值
* 返 回 值:无 void
* 限定条件:调用方保证 p 可写 4 字节
*/
static void wave_st_put32(uint8_t *p, uint32_t v) {
p[0] = (uint8_t)(v & 0xFFu);
p[1] = (uint8_t)((v >> 8) & 0xFFu);
p[2] = (uint8_t)((v >> 16) & 0xFFu);
p[3] = (uint8_t)((v >> 24) & 0xFFu);
}
/*
* 函数功能:读 16 位小端
* 入口参数:p 源位置(至少 2 字节)
* 返 回 值:值 uint16_t
* 限定条件:调用方保证 p 可读 2 字节
*/
static uint16_t wave_st_get16(const uint8_t *p) {
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
/*
* 函数功能:读 32 位小端
* 入口参数:p 源位置(至少 4 字节)
* 返 回 值:值 uint32_t
* 限定条件:调用方保证 p 可读 4 字节
*/
static uint32_t wave_st_get32(const uint8_t *p) {
return ((uint32_t)p[0]) | (((uint32_t)p[1]) << 8) |
(((uint32_t)p[2]) << 16) | (((uint32_t)p[3]) << 24);
}
/*
* 函数功能:把 int16 写成小端两字节
* 入口参数:p 目标位置;v 码值
* 返 回 值:无 void
* 限定条件:调用方保证 p 可写 2 字节
* 函数说明:先转 uint16 再移位——负数右移是实现定义行为,转成无符号后行为确定。
*/
static void wave_st_put_code(uint8_t *p, int16_t v) {
wave_st_put16(p, (uint16_t)v);
}
/* ===== 内部:段头字段一致性自检(编码前与解码后各跑一次,同一把尺子) ===== */
/*
* 函数功能:校验段头元数据的**几何与表序**是否自相矛盾
* 入口参数:p_meta 段头元数据(非空)
* 返 回 值:WAVE_ST_OK = 合法;WAVE_ST_E_SIZE 长度类不成立(零通道 / 零点数 / 预触发超段长);
* WAVE_ST_E_FORMAT 档位 / 触发源 / 表序非法 int
* 限定条件:p_meta 由调用方保证非空
* 函数说明:为什么编码侧也要跑:写一个「看起来像样的坏段文件」比拒写糟得多——落盘道会
* 把 CRC 与长度都算自洽的垃圾写进 NAND,等到回放页或 PC 脚本才发现,那时
* 现场已经没了(§8.1「画成一条直线等于伪造证据」是同一条忌讳的另一半)。
*/
static int wave_st_meta_check(const wave_seg_meta_t *p_meta) {
uint8_t i;
/* 零通道 / 零点数:这一段连长度都不成立(§6.2 末行「长度为 0 判废」) */
if ((p_meta->ch_count == 0u) || (p_meta->ch_count > (uint8_t)WAVE_ST_CH_MAX)) {
return WAVE_ST_E_SIZE;
}
if (p_meta->pts_per_ch == 0u) {
return WAVE_ST_E_SIZE;
}
/* 预触发点数不能大于一段的点数;后触发 = pts_per_ch - pre_pts,允许为 0(手动触发
* 撞上落盘窗口等极端情形),但不允许负数——那是把段长算错 */
if (p_meta->pre_pts > p_meta->pts_per_ch) {
return WAVE_ST_E_SIZE;
}
/* 采样率与每周波点数是数据,但为 0 的数据没法解释(回放与 PC 都要拿它做除数) */
if ((p_meta->fs_hz == 0u) || (p_meta->pts_per_cycle == 0u)) {
return WAVE_ST_E_SIZE;
}
/* 与 PC 侧 reader 的拒绝清单对齐(谁都不能产出一段「自己写得出、对面读不了」的文件):
* 每周波不足 2 点连一个周期都描不出来;段长不足一个整周期则基波算法无从取窗。 */
if (p_meta->pts_per_cycle < 2u) {
return WAVE_ST_E_SIZE;
}
if (p_meta->pts_per_ch < (uint32_t)p_meta->pts_per_cycle) {
return WAVE_ST_E_SIZE;
}
if (p_meta->eng > (uint8_t)WAVE_ENG_PRI) {
return WAVE_ST_E_FORMAT;
}
if (p_meta->trig_src > (uint8_t)WAVE_TRIG_BENCH) {
return WAVE_ST_E_FORMAT;
}
/* 表序:第 i 项的语义号必须等于下标(§6.3「通道表序与列序」行)——
* 数据区第 i 列归第 i 项解释,错一位就是拿 IB 的波形配 IC 的系数 */
for (i = 0u; i < p_meta->ch_count; i++) {
if (p_meta->ch[i].sem != i) {
return WAVE_ST_E_FORMAT;
}
if (p_meta->ch[i].conv > (uint8_t)1u) {
return WAVE_ST_E_FORMAT;
}
/* 位宽留个合理域:本波只有 16(TPAFE)与 12(片内 ADC),但格式层不写死这两个值,
* 只挡明显不成立的(0 位、超过 32 位)——二期换转换器不必改这里 */
if ((p_meta->ch[i].bits == 0u) || (p_meta->ch[i].bits > 32u)) {
return WAVE_ST_E_FORMAT;
}
}
return WAVE_ST_OK;
}
/* ===== CRC-16/MODBUS ===== */
/*
* 函数功能:CRC-16/MODBUS 增量计算
* 入口参数:a_crc 上一次的中间值(首次传 WAVE_ST_CRC_INIT)
* p_data 数据(NULL 视为空操作)
* a_len 字节数
* 返 回 值:新的中间值 uint16_t
* 限定条件:任意上下文;纯计算
* 函数说明:反射形态 = 右移、异或 0xA001、初值 0xFFFF、异或输出 0;
* 写进文件时低字节在前(§6.3)。无表实现的理由见 wave_store.h 头注。
*/
uint16_t wave_store_crc_update(uint16_t a_crc, const uint8_t *p_data, uint32_t a_len) {
uint16_t crc = a_crc;
uint32_t i;
if (p_data == NULL) {
return crc;
}
for (i = 0u; i < a_len; i++) {
uint8_t b = p_data[i]; /* 身兼两职:本路字节 + 下面内层 8 次的计数器 */
crc = (uint16_t)(crc ^ (uint16_t)b);
for (b = 0u; b < 8u; b++) {
if ((crc & 0x0001u) != 0u) {
crc = (uint16_t)((crc >> 1) ^ WAVE_ST_CRC_POLY);
} else {
crc = (uint16_t)(crc >> 1);
}
}
}
/* 异或输出恒 0:写成宏引用,换异或输出时只改一处 */
return (uint16_t)(crc ^ WAVE_ST_CRC_XOROUT);
}
/*
* 函数功能:CRC-16/MODBUS 一次算完
* 入口参数:p_data 数据;a_len 字节数
* 返 回 值:CRC;空输入返回初值定义 uint16_t
* 限定条件:任意上下文。零长度的判废由调用方做(见 wave_store.h 同函数注释)
*/
uint16_t wave_store_crc(const uint8_t *p_data, uint32_t a_len) {
return wave_store_crc_update(WAVE_ST_CRC_INIT, p_data, a_len);
}
/*
* 函数功能:CRC 实现自检(对规范锚点,不是自算自验)
* 入口参数:无
* 返 回 值:WAVE_ST_OK 相符;WAVE_ST_E_FORMAT 不符 int
* 限定条件:任意上下文;开机或首次落盘前一次,不在每拍路径上
* 函数说明:锚点串「123456789」的 CRC-16/MODBUS = 0x4B37(§6.3)。字面量必须纯 ASCII,
* 所以这里用显式字节数组而不是把中文塞进字符串。
*/
int wave_store_crc_selftest(void) {
static const uint8_t s_anchor[9] = {
(uint8_t)0x31u, (uint8_t)0x32u, (uint8_t)0x33u,
(uint8_t)0x34u, (uint8_t)0x35u, (uint8_t)0x36u,
(uint8_t)0x37u, (uint8_t)0x38u, (uint8_t)0x39u
};
uint16_t got;
got = wave_store_crc(s_anchor, (uint32_t)sizeof(s_anchor));
if (got != WAVE_ST_CRC_CHECK) {
return WAVE_ST_E_FORMAT;
}
return WAVE_ST_OK;
}
/* ===== 段头编解码 ===== */
/* 语义通道的转换器归属与位宽(0 = TPAFE5160,1 = 片内 ADC1;口径属主是
* docs/硬件平台/ADC通道映射说明.md,本表是其抄本,改硬件接线时两处一起改)。
* 第 9 项 TEMP 本波不录入段文件(ch_count 恒 8),仍按实际归属填,
* 免得将来放开 TEMP 时这里是个 0。 */
static const uint8_t s_sem_conv[(uint8_t)WAVE_ST_CH_MAX] = {
(uint8_t)0u, (uint8_t)1u, (uint8_t)1u,
(uint8_t)0u, (uint8_t)0u, (uint8_t)1u, (uint8_t)1u, (uint8_t)1u, (uint8_t)1u
};
static const uint8_t s_sem_bits[(uint8_t)WAVE_ST_CH_MAX] = {
16u, 12u, 12u,
16u, 16u, 12u, 12u, 12u, 12u
};
/*
* 函数功能:把段头元数据填成本波默认值(只填格式侧静态事实)
* 入口参数:p_meta 输出
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int
* 限定条件:任意上下文
* 函数说明:填三样格式侧的静态事实:语义号 = 下标(§6.3 表序口径,也是 PC 侧判据)、
* conv / bits 按两片转换器的归属(下面那张表的口径属主是
* docs/硬件平台/ADC通道映射说明.md 的抄本,本层刻意不带 adc_input.h,
* 这样格式层在主机侧不需要任何假件就能编译;单测里 0xE6 的算术交叉验证
* 会同时钉住这套顺序,抄错就会红)。
* 不填 avail / k_q16 / b_codes:那些是封存瞬间的事实,必须由调用方现取现填
* (adc_cal_k_q16 / adc_cal_flags / sys_clock),本层不链它们;
* 调用方忘填 k_q16 与 avail 不会让编码失败(0 是合法整数),
* 但会让 PC 侧把波形解释成一条 0 线——所以单测里有一条专门检查
* 「填了系数的往返」与「没填系数时段的形状仍然正确、只是幅值归零」。
*/
int wave_store_meta_init(wave_seg_meta_t *p_meta) {
uint8_t i;
if (p_meta == NULL) {
return WAVE_ST_E_PARAM;
}
p_meta->fs_hz = (uint16_t)WAVE_ST_FS_DEF;
p_meta->pts_per_cycle = (uint8_t)WAVE_ST_PPC_DEF;
p_meta->ch_count = (uint8_t)WAVE_ST_CH_NUM;
p_meta->pts_per_ch = (uint16_t)WAVE_ST_PTS_DEF;
p_meta->pre_pts = (uint16_t)WAVE_ST_PRE_DEF;
p_meta->trig_src = (uint8_t)WAVE_TRIG_NONE;
p_meta->trig_elm = 0u;
p_meta->t0_ms = 0u;
p_meta->wall = 0u;
p_meta->eng = (uint8_t)WAVE_ENG_PIN;
p_meta->flags_seq = 0u;
p_meta->clip_mask = 0u;
p_meta->frame_seq = 0u;
for (i = 0u; i < (uint8_t)WAVE_ST_CH_MAX; i++) {
p_meta->ch[i].sem = i;
p_meta->ch[i].conv = s_sem_conv[i];
p_meta->ch[i].bits = s_sem_bits[i];
p_meta->ch[i].avail = 0u;
p_meta->ch[i].k_q16 = 0;
p_meta->ch[i].b_codes = 0;
}
return WAVE_ST_OK;
}
/*
* 函数功能:内部——把固定头 + 通道表写进缓冲(不含头 CRC)
* 入口参数:p_meta 段头;p_out 缓冲(容量须 >= 头 CRC 覆盖区)
* 返 回 值:写入字节数(= 头 CRC 覆盖区长度) uint32_t
* 限定条件:p_meta 已过 wave_st_meta_check、p_out 容量已核过
*/
static uint32_t wave_st_head_raw(const wave_seg_meta_t *p_meta, uint8_t *p_out) {
uint8_t i;
const uint32_t tbl_n = (uint32_t)p_meta->ch_count * WAVE_ST_CH_BYTES;
p_out[WAVE_ST_OFF_MAGIC] = (uint8_t)WAVE_ST_MAGIC0;
p_out[WAVE_ST_OFF_MAGIC + 1u] = (uint8_t)WAVE_ST_MAGIC1;
/* 魔数第三字节 = 版本号:与 ver 字段同源同值(§6.2 第 0 行 + §6.3 判废口径) */
p_out[WAVE_ST_OFF_MAGIC + 2u] = (uint8_t)WAVE_ST_VER;
p_out[WAVE_ST_OFF_VER] = (uint8_t)WAVE_ST_VER;
wave_st_put16(&p_out[WAVE_ST_OFF_FS_HZ], p_meta->fs_hz);
p_out[WAVE_ST_OFF_PTS_CYC] = p_meta->pts_per_cycle;
p_out[WAVE_ST_OFF_CH_CNT] = p_meta->ch_count;
wave_st_put16(&p_out[WAVE_ST_OFF_PTS_CH], p_meta->pts_per_ch);
wave_st_put16(&p_out[WAVE_ST_OFF_PRE_PTS], p_meta->pre_pts);
p_out[WAVE_ST_OFF_TRIG_SRC] = p_meta->trig_src;
p_out[WAVE_ST_OFF_TRIG_ELM] = p_meta->trig_elm;
wave_st_put32(&p_out[WAVE_ST_OFF_T0_MS], p_meta->t0_ms);
wave_st_put32(&p_out[WAVE_ST_OFF_WALL], p_meta->wall);
p_out[WAVE_ST_OFF_ENG] = p_meta->eng;
p_out[WAVE_ST_OFF_FLAGS] = p_meta->flags_seq;
wave_st_put16(&p_out[WAVE_ST_OFF_CLIP], p_meta->clip_mask);
wave_st_put32(&p_out[WAVE_ST_OFF_SEQ], p_meta->frame_seq);
for (i = 0u; i < p_meta->ch_count; i++) {
uint32_t pos = (uint32_t)WAVE_ST_OFF_TBL + ((uint32_t)i * WAVE_ST_CH_BYTES);
p_out[pos + WAVE_ST_CH_OFF_SEM] = p_meta->ch[i].sem;
p_out[pos + WAVE_ST_CH_OFF_CONV] = p_meta->ch[i].conv;
p_out[pos + WAVE_ST_CH_OFF_BITS] = p_meta->ch[i].bits;
p_out[pos + WAVE_ST_CH_OFF_AVAIL] = p_meta->ch[i].avail;
/* int32 系数按小端原样落 4 字节:右移到 24 位前把符号扩展掉,
* 保证板端 ARMCC 与主机 CRT 写出同一串字节 */
wave_st_put32(&p_out[pos + WAVE_ST_CH_OFF_KQ16], (uint32_t)p_meta->ch[i].k_q16);
wave_st_put32(&p_out[pos + WAVE_ST_CH_OFF_BCODE], (uint32_t)p_meta->ch[i].b_codes);
}
return (uint32_t)WAVE_ST_OFF_TBL + tbl_n;
}
/*
* 函数功能:编码段头(固定头 + 通道表 + 头 CRC)
* 入口参数:p_meta 段头元数据;p_out 输出缓冲;a_cap 容量
* 返 回 值:>= 0 写入字节数;负 = WAVE_ST_E_* int
* 限定条件:任意上下文;无静态缓冲、可重入
* 函数说明:头 CRC 覆盖 [0, 30+12×ch_count)、自身紧随其后、低字节在前(§6.3)。
*/
int wave_store_head_build(const wave_seg_meta_t *p_meta, uint8_t *p_out, uint32_t a_cap) {
uint32_t crc_pos;
uint16_t crc;
int rc;
if ((p_meta == NULL) || (p_out == NULL)) {
return WAVE_ST_E_PARAM;
}
rc = wave_st_meta_check(p_meta);
if (rc != WAVE_ST_OK) {
return rc;
}
crc_pos = WAVE_ST_HEAD_CRC_POS(p_meta->ch_count);
if (a_cap < (crc_pos + 2u)) {
return WAVE_ST_E_PARAM;
}
(void)wave_st_head_raw(p_meta, p_out);
crc = wave_store_crc(p_out, crc_pos);
/* 低字节在前(§6.3 首行):put16 本来就是小端,这里不额外翻转 */
wave_st_put16(&p_out[crc_pos], crc);
return (int)(crc_pos + 2u);
}
/*
* 函数功能:解码 + 全套判废(魔数 / 版本 / 缓冲长度 / 几何 / 表序 / 头 CRC)
* 入口参数:p_buf 头字节流;a_len 可读字节数;p_meta 输出
* 返 回 值:WAVE_ST_OK 或 WAVE_ST_E_*(见 wave_store.h) int
* 限定条件:任意上下文
* 函数说明:顺序刻意是「先验魔数与版本,再算整头长度,再验 CRC,最后验语义」——
* 前两步决定我们**有没有资格**按这份布局去数偏移;反过来先按 ch_count 算长度,
* 一个被写坏的 ch_count 就能让我们读到缓冲外面去。
*/
int wave_store_head_parse(const uint8_t *p_buf, uint32_t a_len, wave_seg_meta_t *p_meta) {
uint8_t ch_count;
uint32_t crc_pos;
uint32_t head_sz;
uint16_t want;
uint16_t got;
int rc;
wave_seg_meta_t tmp;
if ((p_buf == NULL) || (p_meta == NULL)) {
return WAVE_ST_E_PARAM;
}
/* 固定头是 30 + 1 字节通道数才能算出整头长度,至少得读到偏移 7 */
if (a_len < ((uint32_t)WAVE_ST_OFF_CH_CNT + 1u)) {
return WAVE_ST_E_PARAM;
}
if ((p_buf[WAVE_ST_OFF_MAGIC] != (uint8_t)WAVE_ST_MAGIC0) ||
(p_buf[WAVE_ST_OFF_MAGIC + 1u] != (uint8_t)WAVE_ST_MAGIC1)) {
return WAVE_ST_E_FORMAT;
}
/* ver 必须等于魔数第三字节、且等于本层认识的版本(§6.2 末行) */
if ((p_buf[WAVE_ST_OFF_VER] != p_buf[WAVE_ST_OFF_MAGIC + 2u]) ||
(p_buf[WAVE_ST_OFF_VER] != (uint8_t)WAVE_ST_VER)) {
return WAVE_ST_E_FORMAT;
}
ch_count = p_buf[WAVE_ST_OFF_CH_CNT];
head_sz = wave_store_head_bytes(ch_count);
if (head_sz == 0u) {
return WAVE_ST_E_SIZE;
}
if (a_len < head_sz) {
return WAVE_ST_E_PARAM;
}
crc_pos = WAVE_ST_HEAD_CRC_POS(ch_count);
want = wave_st_get16(&p_buf[crc_pos]);
got = wave_store_crc(p_buf, crc_pos);
if (want != got) {
return WAVE_ST_E_CRC;
}
/* 头 CRC 过了才开始解释字段:下面填的是一份可交回调用方的完整元数据 */
tmp.fs_hz = wave_st_get16(&p_buf[WAVE_ST_OFF_FS_HZ]);
tmp.pts_per_cycle = p_buf[WAVE_ST_OFF_PTS_CYC];
tmp.ch_count = ch_count;
tmp.pts_per_ch = wave_st_get16(&p_buf[WAVE_ST_OFF_PTS_CH]);
tmp.pre_pts = wave_st_get16(&p_buf[WAVE_ST_OFF_PRE_PTS]);
tmp.trig_src = p_buf[WAVE_ST_OFF_TRIG_SRC];
tmp.trig_elm = p_buf[WAVE_ST_OFF_TRIG_ELM];
tmp.t0_ms = wave_st_get32(&p_buf[WAVE_ST_OFF_T0_MS]);
tmp.wall = wave_st_get32(&p_buf[WAVE_ST_OFF_WALL]);
tmp.eng = p_buf[WAVE_ST_OFF_ENG];
tmp.flags_seq = p_buf[WAVE_ST_OFF_FLAGS];
tmp.clip_mask = wave_st_get16(&p_buf[WAVE_ST_OFF_CLIP]);
tmp.frame_seq = wave_st_get32(&p_buf[WAVE_ST_OFF_SEQ]);
for (ch_count = 0u; ch_count < (uint8_t)WAVE_ST_CH_MAX; ch_count++) {
tmp.ch[ch_count].sem = 0u;
tmp.ch[ch_count].conv = 0u;
tmp.ch[ch_count].bits = 0u;
tmp.ch[ch_count].avail = 0u;
tmp.ch[ch_count].k_q16 = 0;
tmp.ch[ch_count].b_codes = 0;
}
for (ch_count = 0u; ch_count < p_buf[WAVE_ST_OFF_CH_CNT]; ch_count++) {
uint32_t pos = (uint32_t)WAVE_ST_OFF_TBL + ((uint32_t)ch_count * WAVE_ST_CH_BYTES);
tmp.ch[ch_count].sem = p_buf[pos + WAVE_ST_CH_OFF_SEM];
tmp.ch[ch_count].conv = p_buf[pos + WAVE_ST_CH_OFF_CONV];
tmp.ch[ch_count].bits = p_buf[pos + WAVE_ST_CH_OFF_BITS];
tmp.ch[ch_count].avail = p_buf[pos + WAVE_ST_CH_OFF_AVAIL];
tmp.ch[ch_count].k_q16 = (int32_t)wave_st_get32(&p_buf[pos + WAVE_ST_CH_OFF_KQ16]);
tmp.ch[ch_count].b_codes = (int32_t)wave_st_get32(&p_buf[pos + WAVE_ST_CH_OFF_BCODE]);
}
rc = wave_st_meta_check(&tmp);
if (rc != WAVE_ST_OK) {
return rc;
}
*p_meta = tmp;
return WAVE_ST_OK;
}
/* ===== 长度 / 偏移换算 ===== */
/*
* 函数功能:整头字节数
* 入口参数:a_ch_count 通道数
* 返 回 值:字节数;通道数非法返回 0 uint32_t
* 限定条件:任意上下文
*/
uint32_t wave_store_head_bytes(uint8_t a_ch_count) {
if ((a_ch_count == 0u) || (a_ch_count > (uint8_t)WAVE_ST_CH_MAX)) {
return 0u;
}
return WAVE_ST_HEAD_BYTES(a_ch_count);
}
/*
* 函数功能:按头部字段算整段应有字节数
* 入口参数:p_meta 段头
* 返 回 值:期望字节数;字段非法返回 0 uint32_t
* 限定条件:任意上下文
* 函数说明:等式 = 32 + 12×ch_count + pts_per_ch×ch_count×2 + 2(§6.3;
* 8 路 320 点 = 5250)。落盘道拿它比对 f_write 写回字节数与 f_size。
*/
uint32_t wave_store_seg_size(const wave_seg_meta_t *p_meta) {
if (p_meta == NULL) {
return 0u;
}
if ((p_meta->ch_count == 0u) || (p_meta->ch_count > (uint8_t)WAVE_ST_CH_MAX)) {
return 0u;
}
if (p_meta->pts_per_ch == 0u) {
return 0u;
}
return WAVE_ST_SEG_TOTAL(p_meta->ch_count, p_meta->pts_per_ch);
}
/*
* 函数功能:数据区某点的文件绝对偏移
* 入口参数:a_ch_count 通道数;a_pts_ch 每通道点数;a_ch 列号;a_pt 段内点下标
* 返 回 值:偏移;非法返回 0xFFFFFFFFu 哨兵 uint32_t
* 限定条件:任意上下文
* 函数说明:按通道分列 → 偏移 = 整头长 + (列 × 点数 + 点下标) × 2。单通道整列是连续的,
* 这正是 §6.2「只读那一路」能一次连续读的原因。
*/
uint32_t wave_store_data_off(uint8_t a_ch_count, uint16_t a_pts_ch,
uint8_t a_ch, uint32_t a_pt) {
uint32_t head_sz = wave_store_head_bytes(a_ch_count);
if ((head_sz == 0u) || (a_pts_ch == 0u) || (a_ch >= a_ch_count)) {
return WAVE_ST_BAD_OFF;
}
if (a_pt >= (uint32_t)a_pts_ch) {
return WAVE_ST_BAD_OFF;
}
return head_sz + (((uint32_t)a_ch * (uint32_t)a_pts_ch) + a_pt) * 2u;
}
/*
* 函数功能:从某点起当前列还能连续读多少字节
* 入口参数:a_pts_ch 每通道点数;a_pt 起始点下标;a_want 想读点数
* 返 回 值:可连续读字节数(参数非法返回 0) uint32_t
* 限定条件:任意上下文
*/
uint32_t wave_store_data_bytes_avail(uint16_t a_pts_ch, uint32_t a_pt, uint32_t a_want) {
uint32_t left;
uint32_t n;
if ((a_pts_ch == 0u) || (a_want == 0u)) {
return 0u;
}
if (a_pt >= (uint32_t)a_pts_ch) {
return 0u;
}
left = (uint32_t)a_pts_ch - a_pt;
n = (a_want < left) ? a_want : left;
return n * 2u;
}
/*
* 函数功能:小端字节 → int16 点序列
* 入口参数:p_raw 原始字节;a_len 字节数;p_out 输出;a_max_pts 容量;p_pts 实际点数(可空)
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 / 奇数字节 / 容量不足 int
* 限定条件:任意上下文
* 函数说明:奇数字节按参数错处理而不是「丢掉尾巴」——半截样本被静默吞掉,
* 回放页画出来的波形会比真的少最后一个点,而这种误差正好最难查。
*/
int wave_store_le_decode(const uint8_t *p_raw, uint32_t a_len,
int16_t *p_out, uint32_t a_max_pts, uint32_t *p_pts) {
uint32_t n;
uint32_t i;
if ((p_raw == NULL) || (p_out == NULL)) {
return WAVE_ST_E_PARAM;
}
if (((a_len & 0x00000001u) != 0u) || (a_len > (a_max_pts * 2u))) {
return WAVE_ST_E_PARAM;
}
n = a_len / 2u;
for (i = 0u; i < n; i++) {
/* 先攒成 uint16 再转 int16:补码回绕在 C 里是实现定义但两大编译器一致,
* 而这里需要的正是「按补码位型解释」——TPAFE 的 16 位补码就是这么存的 */
p_out[i] = (int16_t)wave_st_get16(&p_raw[i * 2u]);
}
if (p_pts != NULL) {
*p_pts = n;
}
return WAVE_ST_OK;
}
/*
* 函数功能:码值 → 定点毫单位(mV)
* 入口参数:p_desc 通道表项(必须来自段文件头);a_code 码值;p_milli 输出 mV
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int
* 限定条件:任意上下文(只在回放页与单测用,不在 1 kHz 采集链上;用 int64 中间量)
* 函数说明:模型 eng = (code + b) × k × 1e-6(§6.3,与 adc_cal_apply() 同式),
* k = k_q16 / 65536 µV/码 → mV = (code + b) × k_q16 / 65536000,四舍五入。
* 中间量最大约 (32767 + 8192) × 5.28e7 ≈ 2.2e12,int64 宽裕。
*/
int wave_store_code_to_milli(const wave_ch_desc_t *p_desc, int16_t a_code,
int32_t *p_milli) {
int64_t num;
int64_t den = 65536000;
int64_t v;
if ((p_desc == NULL) || (p_milli == NULL)) {
return WAVE_ST_E_PARAM;
}
num = ((int64_t)a_code + (int64_t)p_desc->b_codes) * (int64_t)p_desc->k_q16;
if (num >= 0) {
v = (num + (den / 2)) / den;
} else {
v = -(((-num) + (den / 2)) / den);
}
*p_milli = (int32_t)v;
return WAVE_ST_OK;
}
/* ===== 整段校验 ===== */
/*
* 函数功能:整段镜像自检(头 + 数据区 + 数据 CRC)
* 入口参数:p_img 镜像;a_len 长度;p_meta 输出段头(可空)
* 返 回 值:WAVE_ST_OK 或 WAVE_ST_E_*(见 wave_store.h) int
* 限定条件:任意上下文;整段驻留时用(本波 5250 字节)
* 函数说明:三条判废一条都不能省:长度 0 或不等、头 CRC、数据 CRC。
* 期望总长用**头里的字段**算,不用编译期常量——文件是自己说它多长,
* 与镜像实际长度不一致就是坏段(§6.2 末行)。
*/
int wave_store_seg_check(const uint8_t *p_img, uint32_t a_len, wave_seg_meta_t *p_meta) {
wave_seg_meta_t meta;
uint32_t want_total;
uint32_t data_off;
uint32_t data_len;
uint16_t want;
uint16_t got;
int rc;
if (p_img == NULL) {
return WAVE_ST_E_PARAM;
}
if (a_len == 0u) {
return WAVE_ST_E_SIZE;
}
rc = wave_store_head_parse(p_img, a_len, &meta);
if (rc != WAVE_ST_OK) {
return rc;
}
want_total = wave_store_seg_size(&meta);
if (want_total == 0u) {
return WAVE_ST_E_SIZE;
}
if (a_len != want_total) {
return WAVE_ST_E_SIZE;
}
data_off = wave_store_head_bytes(meta.ch_count); /* 数据区起点 = 整头字节数 */
data_len = a_len - 2u - data_off;
if (data_len == 0u) {
return WAVE_ST_E_SIZE;
}
want = wave_st_get16(&p_img[a_len - 2u]);
got = wave_store_crc(&p_img[data_off], data_len);
if (want != got) {
return WAVE_ST_E_CRC;
}
if (p_meta != NULL) {
*p_meta = meta;
}
return WAVE_ST_OK;
}
/* ===== 打包游标 ===== */
/*
* 函数功能:初始化打包游标
* 入口参数:p_ctx 游标;p_meta 段头;p_reader 数据源(可空 = 只产头);p_user 上下文
* 返 回 值:WAVE_ST_OK 或 WAVE_ST_E_* int
* 限定条件:任意上下文(纯计算);一段一个游标
* 函数说明:段头整份拷进游标——搬运窗口几十毫秒,期间调用方完全可以去改它自己那份
* (比如 adcTask 又跑了几拍),本段的字节不能受影响。
*/
int wave_store_pack_start(wave_pack_ctx_t *p_ctx, const wave_seg_meta_t *p_meta,
wave_reader_fn p_reader, void *p_user) {
int rc;
if ((p_ctx == NULL) || (p_meta == NULL)) {
return WAVE_ST_E_PARAM;
}
rc = wave_st_meta_check(p_meta);
if (rc != WAVE_ST_OK) {
p_ctx->phase = (uint8_t)WAVE_PACK_IDLE;
return rc;
}
p_ctx->total = wave_store_seg_size(p_meta);
if (p_ctx->total == 0u) {
p_ctx->phase = (uint8_t)WAVE_PACK_IDLE;
return WAVE_ST_E_SIZE;
}
p_ctx->phase = (uint8_t)WAVE_PACK_HEAD;
p_ctx->ch = 0u;
p_ctx->head_done = 0u;
p_ctx->ch_done = 0u;
p_ctx->tail_done = 0u;
p_ctx->rsv = 0u;
p_ctx->crc_data = WAVE_ST_CRC_INIT;
p_ctx->sent = 0u;
p_ctx->p_reader = p_reader;
p_ctx->p_user = p_user;
p_ctx->meta = *p_meta;
return WAVE_ST_OK;
}
/*
* 函数功能:取整段字节流的下一片
* 入口参数:p_ctx 游标;p_out 缓冲;a_cap 容量;p_len 实际产出字节数
* 返 回 值:WAVE_ST_OK 还有后续;WAVE_ST_DONE 整段产完;负 = WAVE_ST_E_* int
* 限定条件:任意上下文,同一游标不得并发使用
* 函数说明:三段推进——头(一次给完,要求容量 >= 整头)、数据区(按列连续、整样本切,
* 绝不切半个 int16,也不跨列,便于回调按列读)、尾 CRC 两字节。
* 数据 CRC 边产边算(增量中间值),所以全程不需要整段驻留 RAM。
*/
int wave_store_pack_next(wave_pack_ctx_t *p_ctx, uint8_t *p_out,
uint32_t a_cap, uint32_t *p_len) {
uint32_t n = 0u;
if ((p_ctx == NULL) || (p_out == NULL) || (p_len == NULL)) {
return WAVE_ST_E_PARAM;
}
*p_len = 0u;
if ((p_ctx->phase == (uint8_t)WAVE_PACK_IDLE) || (a_cap == 0u)) {
return WAVE_ST_E_STATE;
}
if (p_ctx->phase == (uint8_t)WAVE_PACK_DONE) {
return WAVE_ST_DONE;
}
/* --- 头:整头一次产完(接线索按 §7 给 256 字节缓冲,足够 128 字节整头) --- */
if (p_ctx->phase == (uint8_t)WAVE_PACK_HEAD) {
uint32_t head_sz;
int rc;
head_sz = wave_store_head_bytes(p_ctx->meta.ch_count);
if (head_sz == 0u) {
return WAVE_ST_E_SIZE;
}
if (a_cap < head_sz) {
/* 不拆着头给:半截头意味着游标要存 128 字节中间态,纯浪费 */
return WAVE_ST_E_PARAM;
}
rc = wave_store_head_build(&p_ctx->meta, p_out, a_cap);
if (rc < 0) {
return rc;
}
n = (uint32_t)rc;
p_ctx->head_done = (uint16_t)n;
p_ctx->sent += n;
*p_len = n;
p_ctx->phase = (uint8_t)WAVE_PACK_DATA;
return WAVE_ST_OK;
}
/* --- 数据区:按列推进,一次最多一批 --- */
if (p_ctx->phase == (uint8_t)WAVE_PACK_DATA) {
int16_t batch[WAVE_ST_BATCH];
if (p_ctx->p_reader == NULL) {
/* 没有数据源却进了数据区:宁可报错也不产空字节。
* 产空字节会写出「长度对、内容是 0」的段文件,回放页会画出一条直线——
* 那正是 §8.1 明令禁止的伪造证据形态。 */
return WAVE_ST_E_STATE;
}
while ((n < a_cap) && (p_ctx->ch < p_ctx->meta.ch_count)) {
uint32_t room_pts = (a_cap - n) / 2u;
uint32_t col_left = (uint32_t)p_ctx->meta.pts_per_ch - (uint32_t)p_ctx->ch_done;
uint32_t take;
int32_t got;
uint32_t i;
if (col_left == 0u) {
p_ctx->ch++;
p_ctx->ch_done = 0u;
continue;
}
if (room_pts == 0u) {
break;
}
take = (room_pts < col_left) ? room_pts : col_left;
if (take > (uint32_t)WAVE_ST_BATCH) {
take = (uint32_t)WAVE_ST_BATCH;
}
got = p_ctx->p_reader(p_ctx->p_user, p_ctx->ch, (uint32_t)p_ctx->ch_done,
take, batch);
if (got < 0) {
return (int)got;
}
if ((uint32_t)got != take) {
/* 数据源给不满就是数据源的问题(越界 / 未封存),不拿半批凑长度 */
return WAVE_ST_E_STATE;
}
uint32_t base_n = n;
for (i = 0u; i < take; i++) {
wave_st_put_code(&p_out[n], batch[i]);
n += 2u;
}
p_ctx->crc_data = wave_store_crc_update(p_ctx->crc_data, &p_out[base_n], take * 2u);
p_ctx->ch_done = (uint16_t)((uint32_t)p_ctx->ch_done + take);
}
if (p_ctx->ch >= p_ctx->meta.ch_count) {
/* 最后一列的最后一点产出后,「列走完了」要到下一次进循环才发现,
* 所以这一拍可能一个字节都没产。这时候不许当参数错报出去:整段就差
* 最后两片 CRC,报 E_PARAM 会让落盘道把一次真故障判成落盘失败(白丢)。
* 让它落到下面的尾段分支,在同一次调用里把尾产完。 */
p_ctx->phase = (uint8_t)WAVE_PACK_TAIL;
}
p_ctx->sent += n;
*p_len = n;
if (n > 0u) {
return WAVE_ST_OK;
}
if (p_ctx->phase != (uint8_t)WAVE_PACK_TAIL) {
return WAVE_ST_E_PARAM; /* 容量连一个整样本都装不下 */
}
}
/* --- 尾:数据区 CRC,低字节在前 --- */
if (p_ctx->phase == (uint8_t)WAVE_PACK_TAIL) {
if (p_ctx->tail_done == 0u) {
p_out[n] = (uint8_t)(p_ctx->crc_data & 0xFFu);
n++;
p_ctx->tail_done = 1u;
}
if ((p_ctx->tail_done == 1u) && (n < a_cap)) {
p_out[n] = (uint8_t)((p_ctx->crc_data >> 8) & 0xFFu);
n++;
p_ctx->tail_done = 2u;
p_ctx->phase = (uint8_t)WAVE_PACK_DONE;
}
p_ctx->sent += n;
*p_len = n;
return (p_ctx->phase == (uint8_t)WAVE_PACK_DONE) ? WAVE_ST_DONE : WAVE_ST_OK;
}
return WAVE_ST_E_STATE;
}
/*
* 函数功能:游标已产出字节数
* 入口参数:p_ctx 游标
* 返 回 值:字节数;空指针返回 0 uint32_t
* 限定条件:任意上下文(只读)
*/
uint32_t wave_store_pack_offset(const wave_pack_ctx_t *p_ctx) {
return (p_ctx == NULL) ? 0u : p_ctx->sent;
}
/*
* 函数功能:游标对应整段的期望总长
* 入口参数:p_ctx 游标
* 返 回 值:字节数;空指针返回 0 uint32_t
* 限定条件:任意上下文(只读)
*/
uint32_t wave_store_pack_total(const wave_pack_ctx_t *p_ctx) {
return (p_ctx == NULL) ? 0u : p_ctx->total;
}
/* ===== 段文件名轮转(只算名字,不碰文件系统) ===== */
/*
* 函数功能:内部——把两位十进制写进缓冲
* 入口参数:p 目标(至少 2 字节);a_idx 0..99
* 返 回 值:无 void
* 限定条件:a_idx 已由调用方限制在 0..99
*/
static void wave_st_put_idx2(char *p, uint8_t a_idx) {
p[0] = (char)('0' + (a_idx / 10u));
p[1] = (char)('0' + (a_idx % 10u));
}
/*
* 函数功能:按段号生成段文件路径
* 入口参数:p_out 缓冲;a_cap 容量;a_idx 段号 0..99
* 返 回 值:>= 0 写入字符数(不含结束符);WAVE_ST_E_PARAM / WAVE_ST_E_RANGE int
* 限定条件:任意上下文;纯字符拼装(不用格式化函数),板端与 PC 侧逐字节一致
* 函数说明:形态 = 目录 / 前缀 + 两位十进制 + 扩展名,严格 8.3(§5.2 第三条)。
*/
int wave_store_seg_path(char *p_out, uint32_t a_cap, uint8_t a_idx) {
static const char s_dir[] = WAVE_ST_DIR;
static const char s_pfx[] = WAVE_ST_PFX;
static const char s_ext[] = WAVE_ST_EXT;
uint32_t pos = 0u;
uint32_t i;
char buf[WAVE_ST_PATH_MAX];
if (p_out == NULL) {
return WAVE_ST_E_PARAM;
}
if (a_idx >= (uint8_t)WAVE_ST_SEG_MAX) {
return WAVE_ST_E_RANGE;
}
for (i = 0u; i < ((uint32_t)sizeof(s_dir) - 1u); i++) {
buf[pos++] = s_dir[i];
}
buf[pos++] = '/';
for (i = 0u; i < ((uint32_t)sizeof(s_pfx) - 1u); i++) {
buf[pos++] = s_pfx[i];
}
wave_st_put_idx2(&buf[pos], a_idx);
pos += 2u;
for (i = 0u; i < ((uint32_t)sizeof(s_ext) - 1u); i++) {
buf[pos++] = s_ext[i];
}
if (pos >= (uint32_t)sizeof(buf)) {
/* 宏与拼装长度漂移时的兜底:宁可给不出名字,也绝不把结束符写到别人缓冲外 */
return WAVE_ST_E_SIZE;
}
buf[pos] = '\0';
/* 容量必须容得下结束符:写成 a_cap < pos 会放过「刚好差一个字节」的调用方,
* 于是把 p_out[pos] 写到别人缓冲外面(越界写在板子上是静默的) */
if (a_cap <= pos) {
return WAVE_ST_E_PARAM;
}
for (i = 0u; i <= pos; i++) {
p_out[i] = buf[i];
}
return (int)pos;
}
/*
* 函数功能:下一个轮转段号
* 入口参数:a_idx 当前段号(越界按 0 处理)
* 返 回 值:下一个段号 uint8_t
* 限定条件:任意上下文
*/
uint8_t wave_store_next_idx(uint8_t a_idx) {
if (a_idx >= (uint8_t)WAVE_ST_SEG_MAX) {
return 0u;
}
if (a_idx >= ((uint8_t)WAVE_ST_SEG_MAX - 1u)) {
return 0u;
}
return (uint8_t)(a_idx + 1u);
}
/*
* 函数功能:内部——大小写无关地比对一个字符
* 入口参数:a 待测字符(视作 ASCII);b 期望字符(取自段名宏)
* 返 回 值:1 相同;0 不同 uint8_t
* 限定条件:两侧都是 ASCII;非字母字符原样比
* 函数说明:**两侧都**归一化成小写再比。只归一化一侧是个真踩过一次的坑——
* 段名宏 WAVE_ST_PFX / WAVE_ST_EXT 是大写形态(FatFs 8.3 短名就是大写),
* 只把待测串转小写等于「小写 vs 大写」永不相等,反解会拒绝掉全部合法段名
* (单测 用例:首轮 100% 拒绝就是这个)。盘上大小写与宏写法
* 谁都不该去猜,故两侧各归一一次。
*/
static uint8_t wave_st_ci_eq(char a, char b) {
char x = a;
char y = b;
if ((x >= 'A') && (x <= 'Z')) {
x = (char)(x + ('a' - 'A'));
}
if ((y >= 'A') && (y <= 'Z')) {
y = (char)(y + ('a' - 'A'));
}
return (x == y) ? 1u : 0u;
}
/*
* 函数功能:从文件名或路径反解段号
* 入口参数:p_path 路径或纯文件名;p_idx 输出
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_NAME 不是本层命名形态 int
* 限定条件:任意上下文
* 函数说明:只认 basename 上的「wave + 两位数字 + .bin」(大小写都收)。
* 不匹配就返回错误,**绝不**把陌生名字当录波段——扫目录重建段账时误认一个
* 别的文件,等于把别人的文件当波形解释(名字/参数类错误码严禁升级成卷操作,
* 存储架构说明 §10.1 同一条教训)。
*/
int wave_store_idx_from_name(const char *p_path, uint8_t *p_idx) {
static const char s_pfx[] = WAVE_ST_PFX;
static const char s_ext[] = WAVE_ST_EXT;
const char *base = p_path;
const char *q = p_path;
uint32_t i;
uint32_t len = 0u;
uint8_t hi;
uint8_t lo;
if ((p_path == NULL) || (p_idx == NULL)) {
return WAVE_ST_E_PARAM;
}
/* 找 basename:支持反斜杠形态(PC 侧脚本拼出来的路径可能被原样传进来) */
while (*q != '\0') {
if ((*q == '/') || (*q == '\\')) {
base = q + 1;
}
q++;
}
q = base;
while ((*q != '\0') && (len < (uint32_t)WAVE_ST_PATH_MAX)) {
q++;
len++;
}
if ((*q != '\0') || (len != ((uint32_t)sizeof(s_pfx) - 1u) + 2u +
((uint32_t)sizeof(s_ext) - 1u))) {
return WAVE_ST_E_NAME;
}
for (i = 0u; i < ((uint32_t)sizeof(s_pfx) - 1u); i++) {
if (wave_st_ci_eq(base[i], s_pfx[i]) == 0u) {
return WAVE_ST_E_NAME;
}
}
hi = (uint8_t)base[i];
lo = (uint8_t)base[i + 1u];
if ((hi < '0') || (hi > '9') || (lo < '0') || (lo > '9')) {
return WAVE_ST_E_NAME;
}
for (i = (i + 2u); i < len; i++) {
if (wave_st_ci_eq(base[i], s_ext[i - ((uint32_t)sizeof(s_pfx) - 1u) - 2u]) == 0u) {
return WAVE_ST_E_NAME;
}
}
*p_idx = (uint8_t)(((hi - '0') * 10u) + (lo - '0'));
return WAVE_ST_OK;
}