Files
STM32F4-Base/App/algo/wave_ring.c
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

536 lines
23 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_ring.h 的文件头注释块——所有权矩阵、封存握手、跨任务安全、
* 形态钉死与预算全部写在那里,本文件不重抄一遍,只在实现处补「为什么这么写」。
* 本文件是纯逻辑:只 include App/algo/wave_ring.h(它带 wave_store.h),
* 不 include HAL / CMSIS / RTOS 头、不调 FatFs、不建任何 RTOS 对象、不打日志。
* 临界区也不需要:写者只有 adcTask 一个,读者只碰 SEALED 期间冻结的已封存区,
* §5.2 第一条陷阱(无临界区的活缓冲窗口)在本层根本不成立,因为没有那种入口。
* 适用平台:STM32F407ZGTx(Cortex-M4,小端);主机侧单测 test/host/wave_ring/
* 与 test/host/wave_trig/
* 作者:DTU 项目组
* 创建日期:2026-10-29(故障录波 task-1 · 数据面纯逻辑层)
* 修改记录:
* v1.0 2026-10-29 DTU 项目组 创建
* v1.1 2026-10-29 DTU 项目组 触发挂号改单字节载荷编码(lead 评审:三段写在
* adcTask 抢占下会把段头的触发源 / 元件号录成上一段的旧值,溯源字段作废)
*/
#include "wave_ring.h"
/* ARMCC 的 stdint.h 不提供 NULL,而主机侧 gcc / MSVC 会传递性把它拉进来 ——
* 这个差别主机单测照不出来(2026-10-29 台基线实跑 @build 才暴露:本层
* wave_ring.c 报 22 条 identifier "NULL" 未定义)。与 App/algo/sample_ring.c、App/algo/freq.c
* 同一口径:谁用 NULL 谁自己 include stddef.h,不往公共头里塞(少波及别人的 TU)。 */
#include <stddef.h>
/* 触发挂号的载荷编码(v1.1):挂号量必须**一次单字节存写**才原子,所以把载荷本身编进
* 那一个字节 —— 0 = 无请求,非 0 = (触发源 << 位移) | 元件号。触发源取值 1 到 3 占高 3 位、
* 元件号取 prot_elm 的 picked 位号(uint32 位图 → 0 到 31)占低 5 位,合起来 8 位够用。
* 本层双端编译(板端 ARMCC 与模拟器),不能引 FreeRTOS / CMSIS 的临界区内在函数,
* 所以「把三个字节压成一个字节」是唯一既原子又不依赖平台的写法。 */
#define WAVE_RG_REQ_SHIFT (5u) /* 载荷里触发源的低位置 */
#define WAVE_RG_REQ_ELM_MSK (0x1Fu) /* 载荷里元件号的位掩码(低 5 位) */
#define WAVE_RG_REQ_ELM_MAX (32u) /* 元件号合法上界(不含),picked 位图只有 32 位 */
/* ===== 一、静态对象(全部文件作用域 static,禁堆分配;落位与字节数见头注第七节) ===== */
/* 数据区:按通道分列,8 列 × 320 点 × 2 字节 = 5120 字节(§7 预算的大头)。
* 不许改成 float、不许按帧交错(§12:形态错一次 RAM 翻倍 + 跨步取数)。
* 不放在 CCM:DMA 看不见它,且 .sct 里 RW_CCM 只收 heap_4.o,普通 .bss 天然落 SRAM1 */
static int16_t s_ring[WAVE_RING_CH][WAVE_RING_PTS];
/* 已封存段的段头:latch 之前只有环侧字段有效,latch 之后才是完整段头。
* 这是 defaultTask 与 uiTask 能看到的唯一「段头视图」——内部指针一律不外露 */
static wave_seg_meta_t s_sealed;
/* 观测计数(§9 周期行的 seg= / ovf= / drop= / err= 全从这里取,落盘道不数第二份账) */
static uint32_t s_seg_total; /* 成功 ACK 的段数 */
static uint32_t s_ovf_cnt; /* 被忽略的触发数 */
static uint32_t s_drop_cnt; /* SEALED 窗口丢弃的点数(单位 = 点,一次丢一拍 = 加 8) */
static uint32_t s_err_cnt; /* 落盘失败 + 滞留超时自弃 */
static uint32_t s_last_t0_ms; /* 最近一段段首毫秒 */
static uint32_t s_last_seq; /* 最近一段段首帧序号 */
/* 状态机与索引 */
static uint16_t s_head; /* 下一个写入点(滚动位置) */
static uint16_t s_base; /* 已封存 / 正在录的段首在环内的下标(段内点 0) */
static uint16_t s_post_left; /* 后触发还差多少点封段 */
static uint16_t s_cool_left; /* 冷却剩余拍数 */
static uint16_t s_seal_hold; /* SEALED 已滞留拍数 */
static uint8_t s_state; /* wave_ring_state_t */
/* 触发请求挂号:0 = 无请求;非 0 = (触发源 << WAVE_RG_REQ_SHIFT) | 元件号。
* 刻意只用这一个字节承载「是谁要录波」:旧实现分三个字节写(先写挂号位、后写载荷),
* adcTask 抢在两条指令之间就会把上一段的旧触发源 / 旧元件号抄进本段 —— 而这两个字段是
* 这段记录唯一的溯源信息(现场靠它与 /log/start.log 对表),错一次事后无从发现。
* 单字节存写在 Cortex-M4 上原子,写序不再有意义,所以不需要任何顺序假设或屏障。 */
static uint8_t s_req;
static uint8_t s_trig_src; /* 本段实际用的触发源(钉基址时从请求抄过来) */
static uint8_t s_trig_elm; /* 本段实际用的元件序号 */
static uint8_t s_meta_ready; /* 1 = 段头已 latch */
static uint8_t s_seg_idx; /* 本次上电的轮转下标(跨上电接号由落盘道自持,见头注) */
/* 语义号 → 列号(本波恒等,但把这条恒等式收在一处:二期带 TEMP 时只改这张表,
* 写死下标的调用点会静默错位,走本函数的不会) */
static const int8_t s_col_of_sem[(uint8_t)WAVE_ST_CH_MAX] = {
0, 1, 2, 3, 4, 5, 6, 7, -1
};
/* ===== 二、内部小工具 ===== */
/*
* 函数功能:环内下标 +n 后取模
* 入口参数:a_idx 当前下标;a_step 步长(小于环长)
* 返 回 值:取模后的下标 uint16_t
* 限定条件:仅本文件;a_idx 与 a_step 均在环长量级,无溢出风险
* 函数说明:环长 320 是 2 的幂吗?不是(320 = 64×5),所以老老实实取模,
* 不用位与——写错一次就是「看起来能跑、偶尔串段」的那种 bug。
*/
static uint16_t wave_rg_wrap(uint16_t a_idx, uint16_t a_step) {
uint16_t v = (uint16_t)(a_idx + a_step);
if (v >= (uint16_t)WAVE_RING_PTS) {
v = (uint16_t)(v - (uint16_t)WAVE_RING_PTS);
}
return v;
}
/*
* 函数功能:把已封存段的「环侧字段」初始化(封存瞬间调,标定字段一律清零待 latch)
* 入口参数:a_now_ms 段首毫秒;a_frame_seq 段首帧序号
* 返 回 值:无 void
* 限定条件:仅本文件;只在钉基址那一拍调一次
* 函数说明:先把 ch[] 全部清成「sem = 下标、其余 0」,是为了让**没有 latch** 的段
* 不可能带着上一段的系数被写进文件:sealed() 在未 latch 时本就是 0,
* 但清零让「万一有人绕过握手直接读 s_sealed」也读不出旧真值。
*/
static void wave_rg_seal_reset(uint32_t a_now_ms, uint32_t a_frame_seq) {
uint8_t i;
s_sealed.fs_hz = (uint16_t)WAVE_ST_FS_DEF;
s_sealed.pts_per_cycle = (uint8_t)WAVE_ST_PPC_DEF;
s_sealed.ch_count = (uint8_t)WAVE_RING_CH;
s_sealed.pts_per_ch = (uint16_t)WAVE_RING_PTS;
s_sealed.pre_pts = (uint16_t)WAVE_RING_PRE;
s_sealed.trig_src = s_trig_src;
s_sealed.trig_elm = s_trig_elm;
s_sealed.t0_ms = a_now_ms;
s_sealed.wall = 0u;
s_sealed.eng = (uint8_t)WAVE_ENG_PIN;
s_sealed.flags_seq = 0u;
s_sealed.clip_mask = 0u;
s_sealed.frame_seq = a_frame_seq;
for (i = 0u; i < (uint8_t)WAVE_ST_CH_MAX; i++) {
s_sealed.ch[i].sem = i;
s_sealed.ch[i].conv = 0u;
s_sealed.ch[i].bits = 0u;
s_sealed.ch[i].avail = 0u;
s_sealed.ch[i].k_q16 = 0;
s_sealed.ch[i].b_codes = 0;
}
}
/* ===== 三、初始化与压环 ===== */
/*
* 函数功能:复位环与全部状态
* 入口参数:无
* 返 回 值:无 void
* 限定条件:只能在 adcTask 调,且在开始压环之前(见头文件同函数注释)
* 函数说明:数据区显式清零而不是依赖 .bss 清零:本函数也可能在运行中被重新调
* (显式重配 / 单测复用),留旧数据就等于留下「上一段的尾巴混进下一段」的口子。
*/
void wave_ring_init(void) {
uint8_t c;
uint16_t i;
for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) {
for (i = 0u; i < (uint16_t)WAVE_RING_PTS; i++) {
s_ring[c][i] = 0;
}
}
s_head = 0u;
s_base = 0u;
s_post_left = 0u;
s_cool_left = 0u;
s_seal_hold = 0u;
s_state = (uint8_t)WAVE_RING_ST_IDLE;
s_req = 0u;
s_trig_src = (uint8_t)WAVE_TRIG_NONE;
s_trig_elm = 0u;
s_meta_ready = 0u;
s_seg_idx = 0u;
s_seg_total = 0u;
s_ovf_cnt = 0u;
s_drop_cnt = 0u;
s_err_cnt = 0u;
s_last_t0_ms = 0u;
s_last_seq = 0u;
wave_rg_seal_reset(0u, 0u);
}
/*
* 函数功能:压入一拍的 8 路原始码值
* 入口参数:p_codes 语义序数组(前 8 项);a_n 必须等于 WAVE_RING_CH
* 返 回 值:WAVE_ST_OK 已写入;WAVE_ST_DONE 本拍被丢弃(SEALED 窗口);WAVE_ST_E_PARAM 参数错 int32_t
* 限定条件:只能在 adcTask 调,每拍一次
* 函数说明:写的是「列优先」的 8 个分散地址(每列同一行)——这就是 §6.2 分列形态的代价,
* 换来的是落盘与回放都能按列连续搬,不用跨步长取数。
*/
int32_t wave_ring_push(const int16_t *p_codes, uint8_t a_n) {
uint8_t c;
if ((p_codes == NULL) || (a_n != (uint8_t)WAVE_RING_CH)) {
return WAVE_ST_E_PARAM;
}
if (s_state == (uint8_t)WAVE_RING_ST_SEALED) {
/* 冻结窗口丢样本(§4.2):计数单位是点,一次丢一整拍 = WAVE_RING_CH 点 */
s_drop_cnt += (uint32_t)a_n;
return WAVE_ST_DONE;
}
for (c = 0u; c < (uint8_t)WAVE_RING_CH; c++) {
s_ring[c][s_head] = p_codes[c];
}
s_head = wave_rg_wrap(s_head, 1u);
return WAVE_ST_OK;
}
/* ===== 四、触发、推进、交棒、ACK ===== */
/*
* 函数功能:投递一次录波请求
* 入口参数:a_src 触发源(1 手动 / 2 保护 / 3 bench);a_elm 元件序号(0 到 31)
* 返 回 值:WAVE_ST_OK 受理;WAVE_ST_DONE 被忽略(ovf 已加一);WAVE_ST_E_PARAM 触发源
* 非法或元件号越界 int32_t
* 限定条件:任意上下文(**只写一个字节**,理由见 s_req 声明处)
* 函数说明:这里判「非 IDLE 就拒」而不是「非 IDLE 就排队」——本波不做多段排队(已定裁定),
* 而且排队会把「最近一次故障」降级成「上上次故障」。
* s_req 已挂号时也拒:同一拍内多个元件同时 picked 只该录一段。
* 两条入参校验排在状态判断之前:非法调用是调用方的错,不该记进 ovf 账 ——
* ovf 的语义是「真来了第二次触发」。
* 元件号上界写死在这里:今天没有调用方会越界(picked 是 uint32 位图),这条是给
* 二期加元件的人留的牙 —— 载荷只有 5 位,越界必须当场拒,不许静默截断。
*/
int32_t wave_ring_request(uint8_t a_src, uint8_t a_elm) {
if ((a_src < (uint8_t)WAVE_TRIG_MANUAL) || (a_src > (uint8_t)WAVE_TRIG_BENCH)) {
return WAVE_ST_E_PARAM;
}
if (a_elm >= (uint8_t)WAVE_RG_REQ_ELM_MAX) {
return WAVE_ST_E_PARAM;
}
if ((s_state != (uint8_t)WAVE_RING_ST_IDLE) || (s_req != 0u)) {
s_ovf_cnt++;
return WAVE_ST_DONE;
}
/* 一次存写:触发源与元件号同字节落地。别的上下文要么看到「没请求」、要么看到
* 「完整的这次请求」,不存在读到半份载荷的中间态 */
s_req = (uint8_t)(((uint32_t)a_src << (uint32_t)WAVE_RG_REQ_SHIFT) | (uint32_t)a_elm);
return WAVE_ST_OK;
}
/*
* 函数功能:推进一拍状态机
* 入口参数:a_now_ms 当前相对上电毫秒;a_frame_seq 当前帧序号
* 返 回 值:wave_ring_evt_t(本拍刚封段返回 WAVE_RING_EVT_SEALED) int32_t
* 限定条件:只能在 adcTask 调,每拍在 wave_ring_push 之后一次
* 函数说明:五个分支各管一件事,互不重叠:
* IDLE —— 消费挂号、钉段首基址、转 FREEZE
* FREEZE —— 后触发计点,计满转 SEALED
* SEALED —— 只计滞留,超时自弃(绝不让环永久焊死)
* COOL —— 递减冷却,到 0 回 IDLE
* 段首基址 = 触发那一拍写入点的「往前 160 点」,于是段内点 0 是最老的预触发点、
* 点 160 正好是触发后第一点;后触发的 160 点落在环的另一半(本来就即将被覆盖的
* 最老那 160 槽),所以「冻结」在物理上是**停止滚动覆盖**,不是停止写入。
*/
int32_t wave_ring_tick(uint32_t a_now_ms, uint32_t a_frame_seq) {
int32_t evt = (int32_t)WAVE_RING_EVT_NONE;
switch (s_state) {
case (uint8_t)WAVE_RING_ST_IDLE:
s_cool_left = 0u;
s_seal_hold = 0u;
{
/* 只读一次快照再解码:分两次读就会把两次请求的字段拼成一段的身份 */
uint8_t req = s_req;
if (req != 0u) {
/* 钉基址:本拍的样本已写进 (head-1),段首 = head - 预触发点数 */
s_trig_src = (uint8_t)((uint32_t)req >> (uint32_t)WAVE_RG_REQ_SHIFT);
s_trig_elm = (uint8_t)((uint32_t)req & (uint32_t)WAVE_RG_REQ_ELM_MSK);
s_req = 0u;
s_base = wave_rg_wrap(s_head, (uint16_t)((uint32_t)WAVE_RING_PTS - (uint32_t)WAVE_RING_PRE));
s_post_left = (uint16_t)WAVE_RING_POST;
s_meta_ready = 0u;
s_last_t0_ms = a_now_ms;
s_last_seq = a_frame_seq;
wave_rg_seal_reset(a_now_ms, a_frame_seq);
s_state = (uint8_t)WAVE_RING_ST_FREEZE;
}
}
break;
case (uint8_t)WAVE_RING_ST_FREEZE:
if (s_post_left > 0u) {
s_post_left--;
}
if (s_post_left == 0u) {
/* 封段:此刻起 s_head 正好回到 s_base,再写就覆盖段内点 0 —— 所以 push 开始丢 */
s_state = (uint8_t)WAVE_RING_ST_SEALED;
s_seal_hold = 0u;
evt = (int32_t)WAVE_RING_EVT_SEALED;
}
break;
case (uint8_t)WAVE_RING_ST_SEALED:
/* 兜底:defaultTask 挂住 / 卷没挂载 / 落盘道被更高优先级的事挡住,都不能让环
* 永久冻结——下一次真故障无波可录是比丢一段更贵的代价(lead 裁定) */
s_seal_hold++;
if (s_seal_hold >= (uint16_t)WAVE_SEAL_TIMEOUT_MS) {
s_err_cnt++;
s_meta_ready = 0u;
s_seal_hold = 0u;
s_cool_left = (uint16_t)WAVE_RING_COOL_MS;
s_state = (uint8_t)WAVE_RING_ST_COOL;
}
break;
case (uint8_t)WAVE_RING_ST_COOL:
/* 冷却只在 ACK(或自弃)之后计时:它是二次抑制窗,不是冻结窗 */
if (s_cool_left > 0u) {
s_cool_left--;
}
if (s_cool_left == 0u) {
s_state = (uint8_t)WAVE_RING_ST_IDLE;
}
break;
default:
/* 状态字节被写坏(跑飞 / 越界写)时回到 IDLE 而不是卡死:录波宁可多录也不能停 */
s_state = (uint8_t)WAVE_RING_ST_IDLE;
break;
}
return evt;
}
/*
* 函数功能:封存交棒——把封存瞬间事实锁进段头
* 入口参数:p_cal 封存瞬间事实(不可为 NULL)
* 返 回 值:WAVE_ST_OK 已锁存;WAVE_ST_E_STATE 不在待交棒态;WAVE_ST_E_PARAM 参数错 int32_t
* 限定条件:只能在 adcTask 调,只在 EVT_SEALED 那一拍调一次
* 函数说明:只 copy 封存瞬间的事实,不动环侧字段(点数 / 采样率 / 预触发这些
* 在封段那刻已按 §6.2 定死)。重复调用直接拒,不覆盖已有锁存:
* 半路换系数会让落盘道写进去的头与它已经在搬的数据不同源。
*/
int32_t wave_ring_latch_meta(const wave_seal_cal_t *p_cal) {
uint8_t i;
if (p_cal == NULL) {
return WAVE_ST_E_PARAM;
}
if (p_cal->eng > (uint8_t)WAVE_ENG_PRI) {
return WAVE_ST_E_PARAM;
}
if ((s_state != (uint8_t)WAVE_RING_ST_SEALED) || (s_meta_ready != 0u)) {
return WAVE_ST_E_STATE;
}
s_sealed.eng = p_cal->eng;
s_sealed.flags_seq = p_cal->flags_seq;
s_sealed.clip_mask = p_cal->clip_mask;
s_sealed.wall = p_cal->wall;
for (i = 0u; i < s_sealed.ch_count; i++) {
s_sealed.ch[i].conv = p_cal->ch[i].conv;
s_sealed.ch[i].bits = p_cal->ch[i].bits;
s_sealed.ch[i].avail = p_cal->ch[i].avail;
s_sealed.ch[i].k_q16 = p_cal->ch[i].k_q16;
s_sealed.ch[i].b_codes = p_cal->ch[i].b_codes;
}
s_meta_ready = 1u;
return WAVE_ST_OK;
}
/*
* 函数功能:落盘 ACK
* 入口参数:a_result 0 = 成功;非 0 = 失败(短写 / 打开失败 / CRC 自弃)
* 返 回 值:WAVE_ST_OK 已解冻并起冷却;WAVE_ST_E_STATE 当前不在 SEALED int32_t
* 限定条件:只能由 defaultTask 调,一段一次
* 函数说明:无论成败都解冻 + 起冷却:错误只计数不冻结(环冻着的代价是下一次故障无波可录)。
* 成功才推进轮转号与段计数(失败段留同号,下次重写覆盖,不留半截文件)。
*/
int32_t wave_ring_write_done(int32_t a_result) {
if (s_state != (uint8_t)WAVE_RING_ST_SEALED) {
return WAVE_ST_E_STATE;
}
if (s_meta_ready == 0u) {
/* 没 latch 就没有「段」可 ACK:这时候段头从未就绪,落盘道根本拿不到
* wave_ring_sealed_meta(),绝不可能真写出文件。若在这里放过 ACK,
* 就会白推进轮转号(号 +1 而盘上没那个文件 = 凭空吃掉一个号)并把
* seg_total 报成一个没发生过的数(串口周期行的 seg=NN 从此对不上账)。
* 拒绝而不是记账,把「没交棒就 ACK」变成调用方看得见的错误。
* 本段的出口只有一个:滞留到 WAVE_SEAL_TIMEOUT_MS 自弃(那一路照实计 err)。 */
return WAVE_ST_E_STATE;
}
if (a_result == 0) {
s_seg_idx = wave_store_next_idx(s_seg_idx);
s_seg_total++;
} else {
s_err_cnt++;
}
s_meta_ready = 0u;
s_seal_hold = 0u;
s_cool_left = (uint16_t)WAVE_RING_COOL_MS;
s_state = (uint8_t)WAVE_RING_ST_COOL;
return WAVE_ST_OK;
}
/* ===== 五、状态查询与已封存区读口 ===== */
/*
* 函数功能:查当前状态
* 入口参数:无
* 返 回 值:wave_ring_state_t 取值 uint8_t
* 限定条件:任意上下文(只读单字节)
*/
uint8_t wave_ring_state(void) {
return s_state;
}
/*
* 函数功能:查「有已封存段可读」——跨任务唯一握手量
* 入口参数:无
* 返 回 值:1 = SEALED 且段头已 latch;0 = 不可用 uint8_t
* 限定条件:任意上下文(只读单字节)
*/
uint8_t wave_ring_sealed(void) {
if ((s_state == (uint8_t)WAVE_RING_ST_SEALED) && (s_meta_ready != 0u)) {
return 1u;
}
return 0u;
}
/*
* 函数功能:取已封存段的段头快照
* 入口参数:p_out 输出(整份拷出)
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针;WAVE_ST_E_STATE 无就绪段 int32_t
* 限定条件:任意上下文(defaultTask / uiTask 只读)
*/
int32_t wave_ring_sealed_meta(wave_seg_meta_t *p_out) {
uint16_t i;
const uint8_t *p_src;
uint8_t *p_dst;
if (p_out == NULL) {
return WAVE_ST_E_PARAM;
}
if (wave_ring_sealed() == 0u) {
return WAVE_ST_E_STATE;
}
/* 逐字节拷出而不是 *p_out = s_sealed:结构体赋值在 ARMCC 上会调 __aeabi_memcpy,
* 这里没必要为一个赋值多拉一层依赖,量也就 136 字节 */
p_src = (const uint8_t *)&s_sealed;
p_dst = (uint8_t *)p_out;
for (i = 0u; i < (uint16_t)sizeof(wave_seg_meta_t); i++) {
p_dst[i] = p_src[i];
}
return WAVE_ST_OK;
}
/*
* 函数功能:按通道读已封存区的连续点窗
* 入口参数:a_ch 列号;a_from 段内起点;a_cnt 期望点数(超出段尾自动夹);p_dst 输出
* 返 回 值:实际点数;WAVE_ST_E_PARAM / WAVE_ST_E_RANGE / WAVE_ST_E_STATE int32_t
* 限定条件:任意上下文,只读已封存区(本层没有任何读活缓冲的入口)
* 函数说明:按 s_base 取模展开,读者看到的是 320 点的线性数组。
*/
int32_t wave_ring_read_ch(uint8_t a_ch, uint16_t a_from, uint16_t a_cnt, int16_t *p_dst) {
uint16_t left;
uint16_t n;
uint16_t idx;
uint16_t i;
if ((p_dst == NULL) || (a_cnt == 0u)) {
return WAVE_ST_E_PARAM;
}
if ((a_ch >= (uint8_t)WAVE_RING_CH) || (a_from >= (uint16_t)WAVE_RING_PTS)) {
return WAVE_ST_E_RANGE;
}
if (wave_ring_sealed() == 0u) {
return WAVE_ST_E_STATE;
}
left = (uint16_t)((uint16_t)WAVE_RING_PTS - a_from);
n = (a_cnt < left) ? a_cnt : left;
idx = wave_rg_wrap(s_base, a_from);
for (i = 0u; i < n; i++) {
p_dst[i] = s_ring[a_ch][idx];
idx = wave_rg_wrap(idx, 1u);
}
return (int32_t)n;
}
/*
* 函数功能:读口的 wave_reader_fn 形态适配(落盘道直接把它交给打包游标)
* 入口参数:p_user 未用;其余同 wave_ring_read_ch(点数与起点是 uint32 形态)
* 返 回 值:实际点数;负 = WAVE_ST_E_* int32_t
* 限定条件:任意上下文,只读已封存区
* 函数说明:32 位入参先夹到 16 位形态再转调,越界一律报错不夹成 0 ——
* 夹成 0 会让落盘道以为「这一列读完了」而写出一个短段。
*/
int32_t wave_ring_read_cb(void *p_user, uint8_t a_ch, uint32_t a_from,
uint32_t a_cnt, int16_t *p_dst) {
(void)p_user;
if (a_cnt > (uint32_t)WAVE_RING_PTS) {
return WAVE_ST_E_PARAM;
}
if (a_from > (uint32_t)WAVE_RING_PTS) {
return WAVE_ST_E_RANGE;
}
return wave_ring_read_ch(a_ch, (uint16_t)a_from, (uint16_t)a_cnt, p_dst);
}
/*
* 函数功能:取观测计数快照
* 入口参数:p_out 输出(不可为 NULL)
* 返 回 值:WAVE_ST_OK;WAVE_ST_E_PARAM 空指针 int32_t
* 限定条件:任意上下文(只读)
*/
int32_t wave_ring_get_stat(wave_ring_stat_t *p_out) {
if (p_out == NULL) {
return WAVE_ST_E_PARAM;
}
p_out->state = s_state;
p_out->trig_src = s_trig_src;
p_out->trig_elm = s_trig_elm;
p_out->meta_ready = s_meta_ready;
p_out->seg_idx = s_seg_idx;
/* 对外字段是 8 位:本波后触发最多 160 点,不会溢出;真溢出说明环长被改过,
* 饱和在 255 让它在日志里显眼,而不是绕回 0 假装快封段了 */
p_out->post_left = (s_post_left > 255u) ? 255u : (uint8_t)s_post_left;
p_out->rsv1 = 0u;
p_out->rsv2 = 0u;
p_out->cool_left = s_cool_left;
p_out->seal_hold = s_seal_hold;
p_out->seg_total = s_seg_total;
p_out->ovf_cnt = s_ovf_cnt;
p_out->drop_cnt = s_drop_cnt;
p_out->err_cnt = s_err_cnt;
p_out->last_t0_ms = s_last_t0_ms;
p_out->last_seq = s_last_seq;
return WAVE_ST_OK;
}
/*
* 函数功能:语义通道号 → 本环列号
* 入口参数:a_sem 语义号
* 返 回 值:列号;WAVE_ST_E_RANGE = 该语义不录 int32_t
* 限定条件:任意上下文(纯计算)
*/
int32_t wave_ring_col(uint8_t a_sem) {
if (a_sem >= (uint8_t)WAVE_ST_CH_MAX) {
return WAVE_ST_E_RANGE;
}
if (s_col_of_sem[a_sem] < 0) {
return WAVE_ST_E_RANGE;
}
return (int32_t)s_col_of_sem[a_sem];
}