Files
STM32F4-Base/App/algo/wave_store.h
T
wandering1 d64bbdc57f feat(wave): 故障录波数据面、接线与单通道回放页(软件闭环交付)
数据面(App/algo,纯逻辑,双端编译)
- wave_ring:8 路 × 320 点列优先环(1 kHz,预触发 160 + 后触发 160),adcTask
  唯一写者;触发/冻结/封存/冷却四态,冻结持续到落盘回执才解冻,滞留 2 s 自弃
  兜底——下一次真故障无波可录比丢一段更贵
- wave_store:段头编解码 + 拉模式打包游标 + 自带无表位运算 CRC-16/MODBUS(带
  锚点自检)+ 段名与整段长度等式;格式不依赖平台类型宽度
- 段头只存原始码值与封存瞬间生效系数,工程量只当单位标签、不进任何算式
- 三套主机单测(环 113 / 编解码 170 / 触发状态机 110),全部无桩、生产源原样
  纳入;新文件补 stddef 是 ARMCC 与主机差异,主机侧照不出这条分叉
- test/host/wave_read.py 是按 §6.2 的独立第二实现(dump/fft/shape/plot),92 条
  自洽断言;三份合成段互读双校验全过、哈希钉死,格式面双向闭合
- 三套新单测不加专属钩子,由覆盖率总闸全量真跑(沿用既有裁定)

接线(任务层)
- adc_cal:新增生效系数的 32 位定点原值入口(与既有浮点入口共用生效视图),
  段头按最低有效位精确记账,不经浮点回算
- adcTask:每拍压 8 路语义原始码值并推进状态机,封存那一拍同拍交棒封存瞬间事实;
  台架定周期自抓走双层编译门,产品镜像里零存在;串口新增一条 wave: 周期行
- defaultTask:卷空闲后一次心跳整段搬完(try 锁 + 256 字节分片 + 一次完整开档到
  关档),成败都回执;失败先删半成品再计错,段号只在成功时推进;持锁超 50 ms
  补一条告警并记历史峰值(喂狗余量可核)
- Lead 复核补第四道并发陷阱:采集任务优先级高于落盘道,搬段中途它可能正好把滞留
  超时的本段自弃、甚至封出下一段——数据区校验和是对「实际读到的字节」算的,
  半新半旧的段照样自洽,故关档后复检环侧身份判废:删文件、不补账、段号不推进
- Lead 复核抓到触发挂号不是原子操作(旧实现分三个字节写,消费者可在窗口里读到
  上一段的触发源与元件号):已把载荷压进一个字节,越界当场拒且不记任何计数;
  段文件字节布局零改动,三份合成段哈希逐字节不变
- protectTask:保护启动上升沿同点请求录波,一拍一次、元件号取最低位;判据本体
  零改动

人机(160×160 单色屏)
- 菜单树节点 26「手动录波」绑页面 + 记录查询组末新增「录波回放」叶,重跑生成器
  得 43 节点 / 34 叶 / 30 已绑,页面映射 28 → 30
- 回放页一屏一路、触发点前后各一周波共 40 点、每点 3 像素阶梯线(只用水平与
  垂直两种线段——单色屏斜线会被阈值裁掉);零点虚线、触发光标、窗口峰值、
  单位徽标与形状受限徽标;空态与坏段各一句显式文案,绝不用半截波形冒充数据
- ui_data 新增录波注入通道(六件操作表、三态返回);执行侧胶水带段头缓存、
  手动请求后连缓存一起作废、逐点走全仓唯一的码值换算入口
- 模拟器接真实环与编解码合成两段(含坏段与空态旗标),6 张新黄金参考,既有
  60 张逐字节零漂移未用更新模式;字库 218 → 224 字 / 319 字形
- 两份图形配置里图表组件仍为关;零堆分配;页面池水位不变
- 回放读路径的 uiTask 阻塞上界按实码写清:一次渲染 = 段表刷新 + 未缓存段头最坏
  22 次小读;已验段只剩一次开窗读 + 40 点解码;无实测数不编,实测归台架、
  仪器用已在跑的单轮工作耗时探针,不新加埋点

台账与文档
- 内存台账:录波环与落盘/回放静态逐条入册,三档对照 81,536 → 88,296 → 89,936
  字节(62.2% → 68.6%),累计 +8,400;预估 5.9 KB 与实测 +6,760 的差额点名;
  散列文件头注释同步;任务栈表核对后无需抬栈;体积趋势行与同码凭证由权威构建
  重打(0 错误 0 警告,四项体积与上一轮相同——本轮改动只有注释与声明位置)
- 设计说明:§5 补第四条并发陷阱;§6.3 定字段口径(两份独立实现的共同依据);
  §7 回填实测;§9 周期行写实;§10 台架判据改两段式且不比绝对相位;§12 补读路径
  风险行;抬头状态改成「代码面已交付、未做完的只有板级那一轮」
- 卡片:软件闭环一栏列证据;台架三条按用户裁定保持未完成,不做虚假销项
- 口径同步:数据可信、UI 补齐、页面补齐规划、产品需求、采集短板评测、任务规划
  索引与两份 README;陈旧现状句清扫 16 处(带日期戳的历史取数行一律不动)
- 门禁说明:nested-comment 记仓库根探测必须兼容 linked worktree 的 gitdir 文件
  形态(修前该钩子在任务树里恒报环境错、静默不可用);coding-standard 记
  「函数头注释超过 24 行就把函数功能挤出回看窗口」这条机制与本次现场
- 覆盖率基线登记两个新覆盖模块(套件 41 → 44 套);环单测重打后该文件有效行
  227 → 226、覆盖行 226 → 225,比率仍 99.6%,是分母变小不是覆盖率回退

为什么是单一提交:文档链接、双端注册、生成器幂等(菜单与字库)、内存数字与
体积事实源、编号门禁这五道整仓门禁要求代码、页面、菜单树、字库、文档与台账
同处一个提交才自洽,按数据面/接线/人机/台账拆成四笔必然红其中至少四道

编号门禁新增行 14 处命中已按语义改写:魔数常量按字符命名、段名样例改由格式串
生成、频谱表头改 f / 2x / 5x 并加一行图例;改写后自洽断言 92 条仍全过
2026-10-29 19:23:15 +08:00

492 lines
31 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.
/*
* 模块名称:录波段格式层(故障录波 · 段文件字节布局编解码 + 打包游标 + 段名轮转)
* 模块功能:把「一段录波」在内存字节缓冲与结构化元数据之间逐字节翻译,并给落盘道
* 提供分次搬运的打包游标。口径唯一属主 = docs/子系统设计/故障录波设计说明.md §6.2
* (字节布局)与 §6.3(九问裁定:板端编码器与 PC 解码器 test/host/wave_read.py
* 两份独立实现的共同依据)。本文件是它的机器可执行形态,不在此重新定义任何口径;
* 两处冲突以设计文档为准并回报 lead,不在代码里单方面「修正」。
* 纯逻辑层纪律:不 include HAL / CMSIS / RTOS 头、不调 FatFs、不碰文件系统、
* 不建任务、不打日志(文件 I/O 与任务钩子全在接线层 App/task/ 里做),
* 因此可被主机侧单测 test/host/wave_store/ 原样纳入做逐字节断言。
* 职责边界(为什么这样切):
* 1) 固定头 + 通道表编解码、头 CRC 与数据区 CRC 自检 —— 格式知识只此一处;
* 2) 打包游标(分次搬运):文件系统唯一写者只要「每次给我不超过 256 字节的下一片」,
* 整段 5250 字节不驻留 RAM(设计说明 §7:搬运缓冲 256 B × 2,不做整段驻留);
* 3) 段文件名轮转与反解:只算名字,不打开文件(FatFs FF_USE_LFN=0 只能严格
* 8.3 短名,见 docs/架构设计/STM32F4-Base存储架构说明.md §10.1);
* 4) 数据区偏移换算 + 小端解码:回放页只画一路(设计说明 §8.1),按通道分列
* 让「只读那一路」成为一次连续读。
* 不链 App/algo/adc_cal(零依赖 = 主机免桩可测):通道表里的 k_q16 / b_codes /
* 可用性由调用方在封存瞬间取 int32 原值填进 meta 后交给本层编码。系数必须走
* adc_cal_k_q16()(接线道补的 int32 直读入口),不得经 adc_cal_k_uv() 的 float
* 通道回算 —— float 在 5.28e7 量级掉尾数(设计说明 §6.3「k / b 宽度与角色」行)。
* CRC 一张表的原始说法不成立,lead 已在 §6.3 记录实测依据:全仓无可复用的
* CRC-16 表(App/util/README.md 记 crc.c 与 crc.h 已于 2026-10-02 删除;屏板链路
* Drivers/BSP/UI/ui_net_sink.c 的 remo_crc() 是单字节异或、不是 CRC-16)。
* 故本层自带无表位运算 CRC-16/MODBUS,并用规范锚点自校验。
* 适用平台:STM32F407ZGTx(Cortex-M4,小端);主机侧单测 test/host/wave_store/
* 作者:DTU 项目组
* 创建日期:2026-10-29(故障录波 task-1 · 数据面纯逻辑层)
* 修改记录:
* v1.0 2026-10-29 DTU 项目组 创建:段头/通道表编解码、CRC-16/MODBUS 无表实现、
* 打包游标、段名轮转与反解、数据区偏移换算与小端解码
*
* 二期不返工的结构约束(设计说明 §2.1,改本层前先读):
* 1) 采样率与每周波点数是写进文件头的数据,不是编译期假设 —— 回放端与 PC 脚本一律
* 从头里读,本层算长度也照 meta 里的值算,不在代码里写死 1000 / 20;
* 2) 版本号并进魔数第三字节,换格式即变;本层解到不认识的版本一律判废;
* 3) 「带 TEMP 就要连 flags_seq 与 clip_mask 一起升版本」同源(§6.2 那两行的括注),
* 所以本层的通道数上限留到 9,但本波实际只用 8。
*/
#ifndef WAVE_STORE_H
#define WAVE_STORE_H
/* 同 App/algo/wave_ring.h:本头的 NULL 只出现在注释里(字段与入参的约束说明),
* 代码只用 stdint 的类型,故不 include stddef.h;用 NULL 的 .c 自己带。 */
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ===== 一、段文件布局常量(逐字段照 docs/子系统设计/故障录波设计说明.md §6.2 与 §6.3) ===== */
/* 魔数前两字节固定 'W' 与 'R',第三字节 = 版本号(设计说明 §6.2 偏移 0 行) */
#define WAVE_ST_MAGIC0 (0x57u) /* 'W' */
#define WAVE_ST_MAGIC1 (0x52u) /* 'R' */
/* 版本:本波恒 1。ver 字段必须同时等于魔数第三字节与本宏,否则整段判废(§6.2 末行) */
#define WAVE_ST_VER (1u)
/* 固定头字段偏移(字节,小端) */
#define WAVE_ST_OFF_MAGIC (0u) /* 3 字节:'W' 'R' ver */
#define WAVE_ST_OFF_VER (3u) /* 1 字节 */
#define WAVE_ST_OFF_FS_HZ (4u) /* 2 字节 本波 1000 */
#define WAVE_ST_OFF_PTS_CYC (6u) /* 1 字节 本波 20 */
#define WAVE_ST_OFF_CH_CNT (7u) /* 1 字节 本波 8 */
#define WAVE_ST_OFF_PTS_CH (8u) /* 2 字节 本波 320 */
#define WAVE_ST_OFF_PRE_PTS (10u) /* 2 字节 本波 160 */
#define WAVE_ST_OFF_TRIG_SRC (12u) /* 1 字节 */
#define WAVE_ST_OFF_TRIG_ELM (13u) /* 1 字节 */
#define WAVE_ST_OFF_T0_MS (14u) /* 4 字节 u32 */
#define WAVE_ST_OFF_WALL (18u) /* 4 字节 u32 */
#define WAVE_ST_OFF_ENG (22u) /* 1 字节 */
#define WAVE_ST_OFF_FLAGS (23u) /* 1 字节 */
#define WAVE_ST_OFF_CLIP (24u) /* 2 字节 */
#define WAVE_ST_OFF_SEQ (26u) /* 4 字节 u32 */
#define WAVE_ST_OFF_TBL (30u) /* 通道表起点 */
/* 通道表一路 12 字节 = 语义号 1 + 来源转换器 1 + 位宽 1 + 可用性 1 + k_q16 4 + b_codes 4
* 设计说明 §6.3「LSB 从哪来」:不设独立 LSB 字段,k_q16 就是 LSB(单位 µV/码 × 65536) */
#define WAVE_ST_CH_OFF_SEM (0u) /* 1 字节 */
#define WAVE_ST_CH_OFF_CONV (1u) /* 1 字节 */
#define WAVE_ST_CH_OFF_BITS (2u) /* 1 字节 */
#define WAVE_ST_CH_OFF_AVAIL (3u) /* 1 字节 = adc_cal_flags(sem) 低 8 位原值,不自造位义 */
#define WAVE_ST_CH_OFF_KQ16 (4u) /* 4 字节 int32 */
#define WAVE_ST_CH_OFF_BCODE (8u) /* 4 字节 int32 */
#define WAVE_ST_CH_BYTES (12u)
/* 本波通道数固定 8(TEMP 不录:理由见 §6.3 末段与 docs/任务规划/数据可信.md §7) */
#define WAVE_ST_CH_NUM (8u)
/* 通道数上限:给二期「带 TEMP 且同时升版本」留位;真正的口径是 meta 里的 ch_count 字段 */
#define WAVE_ST_CH_MAX (9u)
/* 每通道点数默认(= 预触发 + 后触发 = 160 + 160;长度是文件里的数据,宏只作默认值) */
#define WAVE_ST_PTS_DEF (320u)
#define WAVE_ST_PRE_DEF (160u)
#define WAVE_ST_FS_DEF (1000u)
#define WAVE_ST_PPC_DEF (20u)
/* 尺寸一律由字段算出,不写死字面量(改通道数/点数时自动跟着走):
* 头 CRC 覆盖区 = 30 + 12 × ch_count;头 CRC 自身紧随其后 2 字节;数据区自 32 + 12 × ch 起
* ch_count = 8 时:覆盖区 126、整头 128、数据区 5120、整段 5250(§6.3「CRC 覆盖范围」行) */
#define WAVE_ST_HEAD_CRC_POS(ch) ((uint32_t)(WAVE_ST_OFF_TBL) + \
((uint32_t)(ch) * WAVE_ST_CH_BYTES))
#define WAVE_ST_HEAD_BYTES(ch) ((uint32_t)(WAVE_ST_HEAD_CRC_POS(ch)) + 2u)
#define WAVE_ST_DATA_BYTES(ch, pts) ((uint32_t)(pts) * (uint32_t)(ch) * 2u)
#define WAVE_ST_SEG_TOTAL(ch, pts) (WAVE_ST_HEAD_BYTES(ch) + \
WAVE_ST_DATA_BYTES(ch, pts) + 2u)
/* 本波实际值(只给注释与单测引用,代码里一律用上面的参数化形态) */
#define WAVE_ST_HEAD_SZ_CUR (128u)
#define WAVE_ST_SEG_SZ_CUR (5250u)
/* 段目录与轮转(设计说明 §5.2 第三条:只能严格 8.3 短名;目录与 /log 平级、FTP 根下可见)
* 名字形态 = 目录 + 前缀 + 两位十进制 + 扩展名,号 0..99 共 100 个轮转覆盖 */
#define WAVE_ST_DIR "/wave"
#define WAVE_ST_PFX "WAVE"
#define WAVE_ST_EXT ".BIN"
#define WAVE_ST_SEG_MAX (100u)
#define WAVE_ST_PATH_MAX (17u) /* /wave/WAVE00.BIN = 16 字符 + 结束符 = 17 字节 */
/* CRC-16/MODBUS:多项式 0x8005 反射形态 = 0xA001、初值 0xFFFF、异或输出 0、低字节在前 */
#define WAVE_ST_CRC_POLY (0xA001u)
#define WAVE_ST_CRC_INIT (0xFFFFu)
#define WAVE_ST_CRC_XOROUT (0x0000u)
/* 自检锚点:串 123456789 的 CRC(§6.3 首行钉的常量) */
#define WAVE_ST_CRC_CHECK (0x4B37u)
/* ===== 二、返回码(int 通道;调用方必须接住或显式 (void),不得裸丢) ===== */
#define WAVE_ST_OK (0) /* 成功 */
#define WAVE_ST_DONE (1) /* 打包游标:整段已产完(不是错误) */
#define WAVE_ST_E_PARAM (-1) /* 空指针 / 容量不足 / 字段非法 */
#define WAVE_ST_E_CRC (-2) /* CRC 不符(头或数据区):整段判废 */
#define WAVE_ST_E_FORMAT (-3) /* 魔数不符 / 版本不认识 / 表内语义号不等于下标 */
#define WAVE_ST_E_SIZE (-4) /* 长度为 0 或长度与头部字段矛盾:整段判废 */
#define WAVE_ST_E_RANGE (-5) /* 列号 / 点下标 / 段号越界 */
#define WAVE_ST_E_STATE (-6) /* 状态错:未封存就读、未 start 就步进、缺数据源 */
#define WAVE_ST_E_NAME (-7) /* 文件名不是本层命名形态(扫目录重建段账时用) */
/* ===== 三、类型 ===== */
/* 触发源(§6.2 偏移 12 的取值;0 表示「无触发」,只出现在段外的状态查询里) */
typedef enum {
WAVE_TRIG_NONE = 0, /* 无 */
WAVE_TRIG_MANUAL = 1, /* 手动(UI「手动录波」确认,task-2 接线) */
WAVE_TRIG_PROTECT = 2, /* 保护启动(App/algo/prot_elm.c 的 picked 上升沿) */
WAVE_TRIG_BENCH = 3 /* bench 定周期自抓(只在 DTU_BENCH 构建里有这条来路) */
} wave_trig_src_t;
/* 折算档位(§6.2 偏移 22;单位一律以头里的 eng 为准,PC 不许按当前固件口径猜) */
typedef enum {
WAVE_ENG_PIN = 0, /* 引脚域(未标定;V0.1 产品构建停在这档) */
WAVE_ENG_SEC = 1, /* 二次值 */
WAVE_ENG_PRI = 2 /* 一次值 */
} wave_eng_t;
/* 通道表一路的内存形态(字段序 = §6.2 通道表字节序) */
typedef struct {
uint8_t sem; /* 语义号(adc_semantic_t 值;必须等于表内下标,§6.3) */
uint8_t conv; /* 来源转换器(adc_source_t 值:0 = TPAFE,1 = STM32) */
uint8_t bits; /* 转换器输出位宽(TPAFE 16 / STM32 12) */
uint8_t avail; /* 可用性 = adc_cal_flags(sem) 低 8 位原值(不自造位义) */
int32_t k_q16; /* 封存瞬间生效系数:µV/码 × 65536,就是 LSB */
int32_t b_codes; /* 封存瞬间生效零点偏移:码值 */
} wave_ch_desc_t;
/* 段头元数据 = 固定头 + 通道表的内存形态。
* 这是「待编码的段头」,不是「已写盘的证据」:打包游标只读它、不改它。 */
typedef struct {
uint16_t fs_hz; /* 采样率 Hz(从文件读出来的数据,不是编译期常量) */
uint8_t pts_per_cycle; /* 每周波点数 */
uint8_t ch_count; /* 通道数(本波 8) */
uint16_t pts_per_ch; /* 每通道点数(本波 320) */
uint16_t pre_pts; /* 预触发点数(本波 160;回放画触发光标用) */
uint8_t trig_src; /* 触发源(wave_trig_src_t) */
uint8_t trig_elm; /* 触发元件序号(prot_elm 实例下标;手动与定时为 0) */
uint32_t t0_ms; /* 段首相对上电毫秒 */
uint32_t wall; /* 对时后的 epoch 秒(未对时 0) */
uint8_t eng; /* 折算档位(wave_eng_t) */
uint8_t flags_seq; /* bit s = 语义通道 s 的 ADC_CAL_F_VALID(§6.3) */
uint16_t clip_mask; /* bit s = 语义通道 s 的 ADC_CAL_F_CLIPPED(V0.1 恒 0xE6) */
uint32_t frame_seq; /* 段首帧序号(与串口周期行的 frames 对账) */
wave_ch_desc_t ch[WAVE_ST_CH_MAX]; /* 通道表(前 ch_count 项有效) */
} wave_seg_meta_t;
/* 打包游标阶段(对外只用于「已搬完」判定与观测日志;推进全在 wave_store_pack_next 内部) */
typedef enum {
WAVE_PACK_IDLE = 0, /* 未初始化 */
WAVE_PACK_HEAD = 1, /* 正在产头(固定头 + 通道表 + 头 CRC) */
WAVE_PACK_DATA = 2, /* 正在产数据区(按通道分列,一列一列产) */
WAVE_PACK_TAIL = 3, /* 只剩数据区 CRC 两字节 */
WAVE_PACK_DONE = 4 /* 整段已产完 */
} wave_pack_phase_t;
/* 数据源回调:把「第 a_ch 列、段内第 a_from 点起的 a_cnt 个点」取进 p_dst。
* 返 回 值:实际拷贝的点数(等于 a_cnt 表示全给到);负 = 错误码 int32_t
* 只允许读已封存区:本层不提供、也不允许任何跨任务读活缓冲的路径
* (设计说明 §5.2 第一条陷阱:App/algo/sample_ring.c 的 sample_ring_window() 无临界区,
* 本波不复用它,跨任务只传「已封存」标志)。
* 生产实现 = wave_ring_read_cb()(签名一致,可直接传给 wave_store_pack_start)。 */
typedef int32_t (*wave_reader_fn)(void *p_user, uint8_t a_ch, uint32_t a_from,
uint32_t a_cnt, int16_t *p_dst);
/* 打包游标:段文件字节流的「拉模式」状态机。
* 为什么拉模式 + 小缓冲:defaultTask 是文件系统唯一写者,落盘窗口几十毫秒量级,
* 一次心跳最多一段;整段驻留 RAM 既超预算也没必要(设计说明 §7)。
* 游标不校验写回字节数 —— 那是调用方对着 f_write 做的事(§5.2 第二条陷阱:本移植
* f_write 簇分配失败会返 FR_OK + 短写、没有错误码,必须靠比对写回字节数判失败)。 */
typedef struct {
uint8_t phase; /* wave_pack_phase_t */
uint8_t ch; /* 数据区当前列 */
uint16_t head_done; /* 头区已产出字节数(含头 CRC 两字节) */
uint16_t ch_done; /* 当前列已产出点数 */
uint8_t tail_done; /* 尾部数据 CRC 已产出字节数(0..2) */
uint8_t rsv; /* 补齐用,保持结构体偶数字节 */
uint16_t crc_data; /* 数据区运行 CRC(增量中间值,避免整段驻留) */
uint32_t total; /* 整段期望字节数 */
uint32_t sent; /* 已产出字节数(= 下一片的文件偏移) */
wave_seg_meta_t meta; /* 段头快照(start 时拷入;搬运期间调用方改自己那份不影响) */
wave_reader_fn p_reader; /* 数据源;NULL = 只产头 */
void *p_user; /* 数据源上下文 */
} wave_pack_ctx_t;
/* ===== 四、CRC-16/MODBUS(无表位运算) ===== */
/*
* 函数功能:CRC-16/MODBUS 增量计算(输入输出均低位在前)
* 入口参数:a_crc 上一次的中间值(首次传 WAVE_ST_CRC_INIT)
* p_data 数据指针(NULL 且 a_len = 0 视为空操作,原样返回 a_crc)
* a_len 字节数
* 返 回 值:新的中间值(写文件时低字节在前,见 §6.3 首行) uint16_t
* 限定条件:任意上下文(含 adcTask 之外的 defaultTask / uiTask);纯计算、无静态状态、
* 不碰外设、不取临界区
* 函数说明:为什么自带实现 —— 全仓没有可复用的 CRC-16 表(依据见本文件头注)。
* 无表 = 零新增表 RAM 与表 Flash;数据区 5120 字节每字节 8 次移位,在 168 MHz
* 上量级是几十微秒,且只在 defaultTask 的落盘链上出现,不在 1 kHz 采集链上。
*/
uint16_t wave_store_crc_update(uint16_t a_crc, const uint8_t *p_data, uint32_t a_len);
/*
* 函数功能:CRC-16/MODBUS 一次算完(从初值起步)
* 入口参数:p_data 数据指针
* a_len 字节数
* 返 回 值:CRC uint16_t
* 限定条件:任意上下文。p_data 为 NULL 或 a_len = 0 时返回 WAVE_ST_CRC_INIT —— 这是
* 空输入的定义值,不是「算好的段 CRC」:零长度一律判废,调用方必须先判长度
* (设计说明 §6.2 末行:长度为 0 → 整段作废)
*/
uint16_t wave_store_crc(const uint8_t *p_data, uint32_t a_len);
/*
* 函数功能:CRC 自检 —— 用规范锚点验算实现是否仍是 CRC-16/MODBUS
* 入口参数:无
* 返 回 值:WAVE_ST_OK 相符;WAVE_ST_E_FORMAT 不符(实现或编译配置被改坏) int
* 限定条件:任意上下文;开机或首次落盘前调一次即可,不在每拍路径上
* 函数说明:锚点 = 串 123456789 的 CRC = 0x4B37(§6.3)。这条是机器可执行的口径锚,
* 防的是「自算自验永远相符」:只有对已知常数才咬得住(换机、换编译器、
* 反射方向写反都在这里暴露)。落盘前失败就自弃本段并计入 err(§9 观测行)。
*/
int wave_store_crc_selftest(void);
/* ===== 五、段头编解码 ===== */
/*
* 函数功能:把段头元数据填成本波默认值(只填格式侧的静态事实)
* 入口参数:p_meta 输出(调用方持有的结构;本层不返回静态地址)
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int
* 限定条件:任意上下文;不碰文件系统
* 函数说明:填 fs_hz = 1000、pts_per_cycle = 20、ch_count = 8、pts_per_ch = 320、
* pre_pts = 160,以及通道表的 sem = 下标 + conv / bits 按两片转换器
* (UA / UN / IA 走 TPAFE 16 位;UB / UC / IB / IC / IN 走片内 ADC 12 位 ——
* 通道归属口径属主是 docs/硬件平台/ADC通道映射说明.md,本函数只是它的抄本,
* 单测里对 0xE6 的算术交叉验证会同时钉住这套顺序)。
* 不填 avail / k_q16 / b_codes / trig_* / t0_ms / wall / eng / flags_seq /
* clip_mask / frame_seq:那些是封存瞬间的事实,必须由调用方现取现填
* (adc_cal_k_q16 / adc_cal_flags / sys_clock / wave_ring),本层不链它们。
*/
int wave_store_meta_init(wave_seg_meta_t *p_meta);
/*
* 函数功能:把段头元数据编码成段文件头字节流(固定头 + 通道表 + 头 CRC,全小端)
* 入口参数:p_meta 段头元数据(只读)
* p_out 输出缓冲
* a_cap 缓冲容量(须不小于按 ch_count 算出的整头字节数)
* 返 回 值:不小于 0 = 实际写入字节数;负 = WAVE_ST_E_* int
* 限定条件:任意上下文;无静态缓冲、可重入;不碰文件系统
* 函数说明:编码前先自检字段一致性 —— ch_count 为 0 或越界、pts_per_ch = 0、
* pre_pts 大于 pts_per_ch、表内语义号不等于下标(§6.3「通道表序与列序」那条
* PC 侧判据在编码侧同样成立)一律拒绝:不把坏头写成看起来像样的文件。
*/
int wave_store_head_build(const wave_seg_meta_t *p_meta, uint8_t *p_out, uint32_t a_cap);
/*
* 函数功能:把段文件头字节流解回元数据,并做全套判废检查(魔数 / 版本 / 长度 / 表序 / 头 CRC)
* 入口参数:p_buf 头字节流
* a_len 可读字节数
* p_meta 输出元数据(判废时内容不保证有效,调用方不得消费)
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针或缓冲不足;WAVE_ST_E_FORMAT 魔数 / 版本 /
* 表序不符;WAVE_ST_E_SIZE 零长度或长度与字段矛盾;WAVE_ST_E_CRC 头 CRC 不符 int
* 限定条件:任意上下文(uiTask 与 defaultTask 都可调)。UI 与 PC 侧必须用同一套判废语义:
* 解不过就只能显示「该段不可读」,绝不允许退化画一条直线
* (设计说明 §8.1 空态行:画直线等于伪造证据)
* 函数说明:a_len 不足「按头里 ch_count 推出来的整头长度」时按缓冲不足返回,不猜没读到
* 的字节;ver 不等于魔数第三字节、或不等于 WAVE_ST_VER 一律判废。
*/
int wave_store_head_parse(const uint8_t *p_buf, uint32_t a_len, wave_seg_meta_t *p_meta);
/* ===== 六、整段校验与长度 / 偏移换算(读侧) ===== */
/*
* 函数功能:整段镜像自检 —— 头 + 数据区 + 数据 CRC 三道全过才算「这一段可读」
* 入口参数:p_img 整段文件镜像(调用方已读进内存)
* a_len 镜像长度
* p_meta 输出段头(可为 NULL)
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_SIZE 长度为 0 或与期望总长不符;
* WAVE_ST_E_CRC 任一 CRC 不符;WAVE_ST_E_FORMAT 头本身判废 int
* 限定条件:任意上下文;只在整段驻留(本波 5250 字节)时用。回放页不要走这条 ——
* 用 wave_store_head_parse + wave_store_data_off + wave_store_crc_update 分段查,
* 才符合 §7 的 256 字节读窗预算
* 函数说明:这是「段文件 CRC 不符时脚本必须拒绝而不是画出东西」那条判据在板侧的同一份
* 实现,也是本层与 PC 解码器逐字节同解的对照物(两套主机单测互验)。
*/
int wave_store_seg_check(const uint8_t *p_img, uint32_t a_len, wave_seg_meta_t *p_meta);
/*
* 函数功能:按头部字段算整段文件应有的字节数(写侧记账与读侧长度判废共用)
* 入口参数:p_meta 段头元数据
* 返 回 值:期望字节数(8 路 320 点 = 5250);字段非法(含零长度)返回 0 uint32_t
* 限定条件:任意上下文
* 函数说明:返回 0 就是「这一段连长度都不成立」,据此判废。不要拿 0 去开缓冲,
* 也不要拿 0 去比对 f_write 的写回字节数(那会把失败读成成功)。
*/
uint32_t wave_store_seg_size(const wave_seg_meta_t *p_meta);
/*
* 函数功能:整头字节数(固定头 + 通道表 + 头 CRC 两字节)
* 入口参数:a_ch_count 通道数
* 返 回 值:字节数;通道数为 0 或越界返回 0 uint32_t
* 限定条件:任意上下文
*/
uint32_t wave_store_head_bytes(uint8_t a_ch_count);
/*
* 函数功能:数据区某一点在段文件里的绝对偏移(按通道分列 → 单通道是一整段连续区)
* 入口参数:a_ch_count 通道数
* a_pts_ch 每通道点数
* a_ch 列号(= 通道表下标 = 语义号,0 到 a_ch_count-1)
* a_pt 段内点下标(0 到 a_pts_ch-1)
* 返 回 值:文件偏移;参数非法返回 0xFFFFFFFFu 哨兵(这个值不是任何合法偏移) uint32_t
* 限定条件:任意上下文
* 函数说明:回放页据此定位「只读那一路」,配合 wave_store_data_bytes_avail 一次连续读;
* PC 侧同一算法,两份实现互验。
*/
uint32_t wave_store_data_off(uint8_t a_ch_count, uint16_t a_pts_ch,
uint8_t a_ch, uint32_t a_pt);
/*
* 函数功能:从某点起当前列还能连续读多少字节(不跨列、不越段尾)
* 入口参数:a_pts_ch 每通道点数
* a_pt 起始点下标
* a_want 想读的点数
* 返 回 值:可连续读字节数 = 2 × 实际可给点数;参数非法返回 0 uint32_t
* 限定条件:任意上下文
*/
uint32_t wave_store_data_bytes_avail(uint16_t a_pts_ch, uint32_t a_pt, uint32_t a_want);
/*
* 函数功能:把段文件读出的小端字节解码成 int16 点序列(按时间序,旧到新)
* 入口参数:p_raw 原始字节(来自文件,2 × 点数)
* a_len 字节数(须为偶数,且不大于 a_max_pts × 2)
* p_out 输出 int16 数组
* a_max_pts 输出容量(点数)
* p_pts 输出实际点数(可为 NULL)
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 / 奇数字节 / 容量不足 int
* 限定条件:任意上下文
* 函数说明:显式按小端逐字节拼装,不依赖宿主字节序,所以板端与 PC 侧同一判据。
* 片内 ADC 那几路的值本就是 0..4095 的原样正数,不做中点偏置
* (§6.3「样本编码」行:加偏置等于伪造双极性)。
*/
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);
/*
* 函数功能:把一路的 int16 码值折成定点毫单位(回放页峰值标注用;纯整数,不用 float)
* 入口参数:p_desc 该路通道表项(k_q16 / b_codes 来自段文件头,不来自当前固件)
* a_code 原始码值
* p_milli 输出四舍五入后的毫单位(k_q16 是 µV/码 → 输出单位 mV)(不可为 NULL)
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int
* 限定条件:任意上下文(只在回放页与单测里用,不在 1 kHz 采集链上;内部用 int64 中间量,
* 落在这条路径上时可接受)
* 函数说明:算式 eng = (code + b) × k × 1e-6,与 App/algo/adc_cal.c 的 adc_cal_apply()
* 同一模型(§6.3)。输出单位是 mV;单位含义由段头 eng 字段决定(eng = 0 是
* 引脚域,别读成二次值)。必须用段文件里的 k / b,不许拿当前固件的系数解释
* 老文件(§12 风险表「标定落地后老文件被解释错」)。
*/
int wave_store_code_to_milli(const wave_ch_desc_t *p_desc, int16_t a_code,
int32_t *p_milli);
/* ===== 七、打包游标(分次搬运,给文件系统唯一写者) ===== */
/*
* 函数功能:初始化打包游标(段头拷进游标,随后逐次拉整段字节流)
* 入口参数:p_ctx 游标(调用方持有,栈上或静态都行;本层不记它的地址)
* p_meta 段头元数据(内部拷贝;搬运期间调用方改自己那份不影响本段)
* p_reader 数据源回调;NULL = 只产头(回放页只校验头 / 主机单测只测头)
* p_user 数据源上下文(生产传 NULL,wave_ring 是单例不需要)
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_FORMAT 段头字段非法;
* WAVE_ST_E_SIZE 算不出合法长度 int
* 限定条件:任意上下文(纯计算,无临界区);一段一个游标,搬完再 start 下一段
* 函数说明:p_reader 为 NULL 而 ch_count 大于 0 时,游标仍能产头,但进数据区那一步
* 返回 WAVE_ST_E_STATE —— 不静默跳过数据区,否则调用方会写出一个
* 「长度看着对、内容是空的」段文件(那正是 §8.1 要防的伪造证据形态)。
*/
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);
/*
* 函数功能:取整段字节流的下一片(分次搬运的唯一推进口)
* 入口参数:p_ctx 已 start 的游标
* p_out 输出缓冲
* a_cap 本片容量(建议 256 字节,见 §7;数据区不切半点,奇数容量会少产 1 字节)
* p_len 输出本片实际字节数(不可为 NULL)
* 返 回 值:WAVE_ST_OK 产出本片且还有后续;WAVE_ST_DONE 整段已产完(*p_len 可为 0);
* 负 = WAVE_ST_E_*(未 start / 容量为 0 / 数据源回调返错 / 游标状态错) int
* 限定条件:任意上下文,但同一游标不得并发使用(文件系统唯一写者单线程推进)
* 函数说明:头区字节流与 wave_store_head_build 完全一致(内部同一份写字段函数);
* **头那一步要求 a_cap 不小于整头字节数**(本波 8 路 = 128),否则返回
* WAVE_ST_E_PARAM 且不推进游标——不拆着头给,是为了让游标不必存 128 字节
* 中间态;§7 的 256 字节搬运缓冲天然满足。
* 数据区 CRC 走增量中间值,整段最后两片是数据 CRC 的低字节、高字节。
* 调用方每写完一片都必须比对 f_write 写回字节数,短写按失败处理(§5.2)。
*/
int wave_store_pack_next(wave_pack_ctx_t *p_ctx, uint8_t *p_out,
uint32_t a_cap, uint32_t *p_len);
/*
* 函数功能:读游标已产出字节数(= 当前文件偏移;供记账、观测与「写没写全」判定)
* 入口参数:p_ctx 游标
* 返 回 值:已产出字节数;p_ctx 为 NULL 返回 0 uint32_t
* 限定条件:任意上下文(只读)
*/
uint32_t wave_store_pack_offset(const wave_pack_ctx_t *p_ctx);
/*
* 函数功能:读游标对应整段文件的期望总长(写完与 f_size / 累计写回字节数比对)
* 入口参数:p_ctx 游标
* 返 回 值:期望总字节数;p_ctx 为 NULL 返回 0 uint32_t
* 限定条件:任意上下文(只读)
*/
uint32_t wave_store_pack_total(const wave_pack_ctx_t *p_ctx);
/* ===== 八、段文件名轮转(只算名字,不碰文件系统) ===== */
/*
* 函数功能:按段号生成段文件路径(严格 8.3 短名:目录 + 前缀 + 两位十进制 + 扩展名)
* 入口参数:p_out 输出缓冲(容量须不小于 WAVE_ST_PATH_MAX)
* a_cap 容量
* a_idx 段号 0 到 WAVE_ST_SEG_MAX-1
* 返 回 值:不小于 0 = 写入的字符数(不含结束符;当前布局恒为 16,故缓冲至少要 17 字节);
* WAVE_ST_E_PARAM 空指针或容量不足;
* WAVE_ST_E_RANGE 段号越界 int
* 限定条件:任意上下文;纯字符拼装(不用 sprintf,见 CODE-RULE-06 系),逐字节可预期
* 函数说明:号 0..99 轮转覆盖,共 100 个名字,写满回绕盖最老的(§5.2 第三条)。
* 本函数不检查文件是否存在、不建目录:那些都是接线道的职责。
*/
int wave_store_seg_path(char *p_out, uint32_t a_cap, uint8_t a_idx);
/*
* 函数功能:下一个轮转段号(到 99 后回 0)
* 入口参数:a_idx 当前段号(越界输入按 0 处理,不猜)
* 返 回 值:下一个段号 uint8_t
* 限定条件:任意上下文
*/
uint8_t wave_store_next_idx(uint8_t a_idx);
/*
* 函数功能:从文件名或路径反解段号(掉电后靠扫目录重建段账用)
* 入口参数:p_path 路径或纯文件名(只看最后一段,大小写都收)
* p_idx 输出段号(不可为 NULL)
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_NAME 不是本层命名形态 int
* 限定条件:任意上下文
* 函数说明:只认「前缀 + 两位数字 + 扩展名」,其余(目录名、长名、以点开头)一律判不匹配
* —— 扫目录重建账时不许把陌生名字当录波段;名字/参数类错误码严禁升级成卷操作
* (docs/架构设计/STM32F4-Base存储架构说明.md §10.1 的同一条教训)。
*/
int wave_store_idx_from_name(const char *p_path, uint8_t *p_idx);
#ifdef __cplusplus
}
#endif
#endif /* WAVE_STORE_H */