/* * 模块名称:录波段格式层(故障录波 · 段文件字节布局编解码 + 打包游标 + 段名轮转) * 模块功能:把「一段录波」在内存字节缓冲与结构化元数据之间逐字节翻译,并给落盘道 * 提供分次搬运的打包游标。口径唯一属主 = 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 #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 */