/* * 模块名称:录波段格式层实现(故障录波 · 段文件字节布局编解码 + 打包游标 + 段名轮转) * 模块功能:见 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 台基线实跑 @build 才暴露:本层 * wave_store.c 报 22 条 identifier "NULL" 未定义)。与 App/algo/sample_ring.c、App/algo/freq.c * 同一口径:谁用 NULL 谁自己 include stddef.h,不往公共头里塞(少波及别人的 TU)。 */ #include /* 打包游标一次最多从数据源取多少个点(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; }