merge: v2-on-ui into task/screenlink-bench-serial (wave + screenlink)

This commit is contained in:
2026-10-30 01:08:37 +08:00
134 changed files with 10699 additions and 1190 deletions
+180
View File
@@ -106,6 +106,15 @@
* 不并进 cal: 行是因为 9 个码值会把该行推到 168 字符、
* 撞 DBG_CON_LINE_MAX(192) 的静默截断边界(见该行注释)。
* 无新增跨任务状态、无新增外设访问,栈 +36 B(局部码值数组)
* v3.4 2026-10-29 DTU 项目组 故障录波数据面接线(task-2):新增 2c——每拍把 8 路
* 语义原码压进 wave_ring 并 tick 推进;EVT_SEALED 那一拍现取
* 折算/有效性/墙钟事实组 wave_seal_cal_t 并 latch_meta 恰好
* 一次(握手次序照 App/algo/wave_ring.h 头注唯一合法次序);
* 心跳末尾加 §9 唯一周期行 wave:(字段逐字,取数
* wave_ring_get_stat);新增 2d——bench 定周期自抓,只在
* DTU_BENCH 且 WAVE_BENCH_AUTOCAP 非 0 的双层编译门内存在,
* 默认关闭 + 5 分钟时长上限 + 启动一次性告警(§10 红线);
* adc_task_init 内接 wave_ring_init(init 禁日志约束不破)
*/
#include "adc_task.h"
@@ -123,6 +132,8 @@
#include "console_log.h"
#include "sys_log.h" /* SYSLOG_SELFTEST:采样故障留痕(仅故障才写) */
#include "task_prof.h" /* 常开耗时探针:单拍耗时 */
#include "wave_ring.h" /* 故障录波环(task-2 数据面接线):push/tick/latch_meta/get_stat */
#include "sys_clock.h" /* 封存交棒的 wall 字段:sys_clock_get() epoch 秒(UINT32_MAX=未对时,写 0) */
#define DBG_TAG "[ADC_TASK]"
#include "dbg_log.h"
@@ -132,6 +143,17 @@
* 消费节拍改由 TIM3 ISR 置位 ADC_WAKE_FLAG 提供,与生产同源,不再自定周期) */
#define ADC_TASK_REPORT_MS 10000U
/* bench 定周期自抓(§10 红线「默认关闭 + 有时长上限 + 启动打一次告警」的落点):
* WAVE_BENCH_AUTOCAP 默认 0——产品档连外层 DTU_BENCH 都没有,双层编译门保证
* 产品镜像里 2d 段整段不存在;台架确实要定周期刷段时才本地改 1 重编译,
* 用完立刻回 0(每 2 s 一段 = 每 5 分钟一轮 NAND 写,长挂白烧写次数,dhara
* 坏块记账还是持久代价)。宏形态照 App/algo/adc_cal.c 的 adc_cal_prot_blocked():
* 联合条件放 #if,未定义宏恒为假,不把 cppcheck 引到旁路上。
* 周期/上限取自 §10 的算式:2 s/段、300 s 自动停。 */
#define WAVE_BENCH_AUTOCAP 0
#define WAVE_BENCH_PERIOD_MS 2000u
#define WAVE_BENCH_MAX_MS 300000u
/* 统计:已消费帧数 / 无新帧次数(首拍必为无新帧,之后应稳定为 0 增量)。
* 原始码值缓冲由 语义取数与有效性层 App/algo/adc_input.c 持有(消费侧按语义取数,不写死下标) */
static uint32_t s_frames_consumed;
@@ -157,6 +179,20 @@ static uint8_t s_rail_syslogged;
* 200 ms(与 v2.6 修"重复采样污染 DFT 窗"同族误差,勿"优化"成拍计数) */
static uint32_t s_meas_ms;
/* 故障录波环接线状态(task-2;状态机与计数全在 App/algo/wave_ring.c,这里只搬运):
* s_wave_codes - 本拍 8 路语义原码(文件级 static,不上单拍的栈——本任务静态
* 最坏链余量按 1024 B ISR 底线核)
* s_wave_cal - EVT_SEALED 拍的交棒结构(同理不放局部:整块 ~160 B) */
static int16_t s_wave_codes[WAVE_RING_CH];
static wave_seal_cal_t s_wave_cal;
#if defined(DTU_BENCH) && (WAVE_BENCH_AUTOCAP != 0)
/* bench 自抓编排:on = 已过写者就绪门开始跑;off = 到时自动停(init 重置) */
static uint8_t s_wave_bench_on;
static uint8_t s_wave_bench_off;
static uint32_t s_wave_bench_t0;
static uint32_t s_wave_bench_next;
#endif
/* 消费者就绪门:由 adcTask 自己在进入等待前置 1;单写者 = 本任务,ISR 只读。
* ⚠ 为什么必须有这道门:TIM3 在 app_main_init()(Src/main.c:140)里就
@@ -288,6 +324,13 @@ int adc_task_init(void) {
s_meas_ms = 0u;
adc_health_init(); /* 内含 采样通道自检 贴轨观察器复位(rail_init) */
(void)meas_init(); /* 遥测快照双区/序号复位(幂等;发布状态属本任务,init 无日志) */
wave_ring_init(); /* 录波环纯清 RAM(无内核依赖;本函数在开闸前跑,init 禁日志不破) */
#if defined(DTU_BENCH) && (WAVE_BENCH_AUTOCAP != 0)
s_wave_bench_on = 0u;
s_wave_bench_off = 0u;
s_wave_bench_t0 = 0u;
s_wave_bench_next = 0u;
#endif
return 0;
}
@@ -454,6 +497,103 @@ void adc_task_func(void *arg) {
adc_health_report(&h_st);
}
/* 2c) 故障录波环泵送(task-2 数据面接线;状态机与计数全在 App/algo/wave_ring.c,
* 本段只做搬运)。每拍取 8 路语义**原码**压环 + tick 推进;触发那拍的样本
* 落在段内点 WAVE_RING_PRE-1(点序 lead 钉死)。只存原码不存折算工程量——
* §6.3 裁定:样本域 = 原始码,折算系数进段头,回放/PC 侧统一换算。
* 某路取不到(开机头几拍)保持上值(与 adc: 心跳行同一兜底口径)。
* tick 返 EVT_SEALED 的那拍必须**同拍** latch meta(头注唯一合法次序),
* 漏了 = 2 s 后环自弃计 err,defaultTask 根本不会见到这一段。 */
{
uint8_t w;
int32_t wev;
for (w = 0u; w < WAVE_RING_CH; w++) {
int32_t wc = 0;
if (adc_input_raw((adc_semantic_t)w, &wc) == 0) {
s_wave_codes[w] = (int16_t)wc;
}
}
(void)wave_ring_push(s_wave_codes, WAVE_RING_CH);
wev = wave_ring_tick(now, s_frames_consumed);
if (wev == WAVE_RING_EVT_SEALED) {
uint8_t c;
/* 封存瞬间事实(交棒结构 s_wave_cal 为文件级 static,不上栈):
* eng / VALID 位图 / CLIPPED 位图 / 墙钟 / 逐路 conv+bits+avail+k+b。
* eng 口径 = adc_cal.h 注③ 的 8 路量纲标签(判据源
* App/app/ui_data_feed.c feed_eng(),与 prot_blocked 的三路口径**不是**
* 一件事):8 路电气量全 VALID 才是二次值;V0.1 无 CT 变比,一次值不可达。
* wall 未对时写 0(sys_clock_get 失败 = UINT32_MAX,不许拿上电毫秒冒充)。
* k 取 adc_cal_k_q16() int32 原值;b 的 float 回乘在本域无损
* (|b| 限幅 8192 << 2^24,float32 精确表示整数;禁用浮点只针对 k)。 */
s_wave_cal.flags_seq = 0u;
s_wave_cal.clip_mask = 0u;
for (c = 0u; c < WAVE_RING_CH; c++) {
const uint32_t f = adc_cal_flags((adc_semantic_t)c);
const adc_channel_desc_t *d = adc_input_desc((adc_semantic_t)c);
if ((f & ADC_CAL_F_VALID) != 0u) {
s_wave_cal.flags_seq |= (uint8_t)(1u << c);
}
if ((f & ADC_CAL_F_CLIPPED) != 0u) {
s_wave_cal.clip_mask |= (uint16_t)(1u << c);
}
s_wave_cal.ch[c].conv = (d != NULL) ? (uint8_t)d->source
: (uint8_t)ADC_SRC_STM32;
s_wave_cal.ch[c].bits = (s_wave_cal.ch[c].conv == (uint8_t)ADC_SRC_TPAFE)
? 16u : 12u;
s_wave_cal.ch[c].avail = (uint8_t)f;
s_wave_cal.ch[c].k_q16 = adc_cal_k_q16((adc_semantic_t)c);
s_wave_cal.ch[c].b_codes = (int32_t)adc_cal_b_codes((adc_semantic_t)c);
}
s_wave_cal.eng = (s_wave_cal.flags_seq == 0xFFu)
? (uint8_t)WAVE_ENG_SEC : (uint8_t)WAVE_ENG_PIN;
{
uint32_t wall = sys_clock_get();
s_wave_cal.wall = (wall == UINT32_MAX) ? 0u : wall;
}
/* 只在 EVT_SEALED 拍调一次;返错不补救——环 2 s 超时自弃兜底,
* 这里补第二行日志就违反 §9「不在已留痕处重复锚点」 */
(void)wave_ring_latch_meta(&s_wave_cal);
}
}
/* 2d) bench 定周期自抓(§10 红线落点:默认关闭 + 时长上限 + 启动一次性告警)。
* 编译门 = DTU_BENCH 且 WAVE_BENCH_AUTOCAP 非 0,产品镜像无本段。
* 节拍:每 WAVE_BENCH_PERIOD_MS 投一次 bench 请求(环侧冷却/去重自管,
* 被丢的计入 ovf——这是 bench 的预期观测面,不是故障)。
* 计时起点等写者就绪门(v2.1 理由:就绪前直发会与 defaultTask 存储
* 日志字节级交错);到时自动停 + 一次性 STOP 行,之后不再投任何请求。
* ⚠ 这两条 WARN/STOP 是一次性生命周期日志,不是周期行,§9 一条线约束不破 */
#if defined(DTU_BENCH) && (WAVE_BENCH_AUTOCAP != 0)
if (s_wave_bench_off == 0u) {
if (s_wave_bench_on == 0u) {
if (dbg_console_writer_ready() != 0u) {
s_wave_bench_on = 1u;
s_wave_bench_t0 = now;
s_wave_bench_next = now + WAVE_BENCH_PERIOD_MS;
/* [WARN] 进正文(本仓无 WARN 级;用 ERR 级会带 [ERR] 误触 @test 禁用标记) */
DBG_INFO("[WARN] wave bench autocap ON: 1 seg / %u ms, auto-stop %u s (BENCH BUILD ONLY)",
(unsigned int)WAVE_BENCH_PERIOD_MS,
(unsigned int)(WAVE_BENCH_MAX_MS / 1000u));
}
} else if ((uint32_t)(now - s_wave_bench_t0) >= WAVE_BENCH_MAX_MS) {
s_wave_bench_off = 1u;
DBG_INFO("wave bench autocap STOP after %u s",
(unsigned int)(WAVE_BENCH_MAX_MS / 1000u));
} else if ((uint32_t)(now - s_wave_bench_next) >= WAVE_BENCH_PERIOD_MS) {
s_wave_bench_next = now + WAVE_BENCH_PERIOD_MS;
(void)wave_ring_request((uint8_t)WAVE_TRIG_BENCH, 0u);
} else {
/* 本拍未到周期点:无事可做(比较用无符号差值,回绕安全) */
}
}
#endif
/* 3) 首帧码值上报:**写者就绪后**才落日志。写者未注册时 con_append 走
* 无锁直发路径,与 defaultTask 的存储初始化日志会字节级交错
* (2026-10-10 台架实测);此时挂起上报,就绪后补报最近一帧。
@@ -610,6 +750,46 @@ void adc_task_func(void *arg) {
(long)(ms.freq_hz * 100.0f),
(unsigned)ms.freq_valid);
}
/* 录波环周期行(docs/子系统设计/故障录波设计说明.md §9 逐字模板,
* 字段名与顺序**不许动**——PC/台架脚本按行名 grep)。全链路只加这一条
* 周期行(每条 DBG ≈90 B Flash + 216 B 栈,§9 明令不加第二条):
* seg = 累计成功 ACK 的段数(wave_ring_stat.seg_total,不是轮转号)
* armed= 1 表示此刻有段在飞(FREEZE/SEALED;IDLE/COOL 为 0——
* COOL 只挡新触发不录新段,画进 armed 会让台架误读)
* src = 触发源 M 手动 / P 保护 / T bench 定时,- = 本 boot 未触发过
* err = 落盘失败 + SEALED 超时自弃(环内 err_cnt 收口,落盘道不数
* 第二份账);ovf = ACK 前重复触发与冷却期丢弃数;
* drop = 冻结窗口丢弃的录波点数(单位=点)
* 判读:bench 自抓一轮后 seg_total 应增长;err 涨而 seg 不涨 =
* 落盘道在丢段,查 defaultTask 侧 [WAVE] 失败行 */
{
wave_ring_stat_t wst;
(void)wave_ring_get_stat(&wst);
{
const char *sc;
if (wst.trig_src == (uint8_t)WAVE_TRIG_MANUAL) {
sc = "M";
} else if (wst.trig_src == (uint8_t)WAVE_TRIG_PROTECT) {
sc = "P";
} else if (wst.trig_src == (uint8_t)WAVE_TRIG_BENCH) {
sc = "T";
} else {
sc = "-";
}
DBG_INFO("wave: seg=%lu armed=%u src=%s pts=%u err=%lu ovf=%lu drop=%lu",
(unsigned long)wst.seg_total,
(unsigned)((wst.state == (uint8_t)WAVE_RING_ST_FREEZE) ||
(wst.state == (uint8_t)WAVE_RING_ST_SEALED)) ? 1u : 0u,
sc,
(unsigned int)WAVE_RING_PTS,
(unsigned long)wst.err_cnt,
(unsigned long)wst.ovf_cnt,
(unsigned long)wst.drop_cnt);
}
}
}
report_at = now + ADC_TASK_REPORT_MS;
}
+269
View File
@@ -26,6 +26,18 @@
* v1.3 2026-10-25 DTU 项目组 写回缓存空闲 flush 接线:主循环 syslog 排空
* 之后调 vol_idle_sync_poll()(卷空闲周期 CTRL_SYNC 兜底,
* 模块 = App/fs/vol_idle_sync.[ch],道F 批 2)
* v1.4 2026-10-29 DTU 项目组 故障录波落盘道(task-2):vol_idle_sync_poll 之后
* 新增 wave_store_drain()——一次心跳整段搬完(lead 裁③:
* 一次 try 锁内 pack_next 256 B 分片 + 完整 open...close),
* 持锁耗时 >50 ms 补一条 WARN、峰值 ms 记本层静态;
* 轮转号自持(启动扫 /wave 重建,成功 ACK 才推进,lead 裁(b));
* 失败 = 先 unlink 半成品再 write_done(-1)(lead 裁①);
* 卷未就绪 / 环未封存静默跳过(§5.1 所有权 + §9 一条线约束)
* v1.5 2026-10-29 DTU 项目组 Lead 复核补第四道并发陷阱(本道与 adcTask 的抢占窗):
* 搬段中途 adcTask 可能正好走到 SEALED 滞留自弃(2 s)甚至
* 已封出下一段——CRC 是对"读到的字节"算的,半新半旧的段照样
* 自洽,只能靠写完关档后复检环侧身份(握手量 + 段首 t0/帧序)
* 判废:判废即 unlink、不 ACK、号不推进,失败按既有单条留痕
*/
#include "default_task.h"
@@ -53,6 +65,8 @@
#include "task_prof.h" /* 各任务最坏耗时上报 */
#include "vol_lock.h"
#include "vol_idle_sync.h" /* 写回缓存空闲 flush 兜底(App/fs/vol_idle_sync.h) */
#include "wave_ring.h" /* 录波环消费侧:sealed/sealed_meta/read_cb/write_done(task-2) */
#include "wave_store.h" /* 段打包游标 + 段号轮转纯函数(落盘道本体,task-2) */
#include "task_handles.h" /* 任务句柄与属性块的引用契约(定义处仍是 Src/freertos.c) */
/* Private defines -----------------------------------------------------------*/
@@ -72,6 +86,14 @@
* (自检已上报、卷保留、不重格式化),等下次上电重新 resume */
#define STORAGE_BRINGUP_RETRIES 3u
/* 录波段落盘持锁耗时告警线(lead 裁③i):一次心跳整段 5250 B(约 21 次 256 B
* f_write),设计 §4.2 预估落盘窗「几十 ms」;越过本线说明本段在占用 defaultTask
* 喂狗链前的最慢一段,按事件补一条 WARN(不是周期行),并把历史峰值留在本层
* 静态里供收工报告核喂狗余量(vol_lock.h 铁律「等锁即毁喂狗链」的前例在案) */
#define WAVE_DRAIN_HELD_WARN_MS 50u
/* 搬运缓冲(§7 的 256 B;wave_store_pack_next 头阶段要求 cap >= 128,lead 转告(d)) */
#define WAVE_DRAIN_CHUNK_B 256u
/* Private typedef -------------------------------------------------------------*/
typedef struct {
const char *p_name;
@@ -97,6 +119,7 @@ static void stack_probe_poll(void);
#if !defined(TEST_SUITE_STORAGE)
static void default_task_net_cfg_save_notify(void);
static FRESULT storage_bring_up(FATFS *fs);
static void wave_store_drain(void);
#endif
/*
@@ -301,6 +324,10 @@ void default_task_func(void *argument) {
* 一次 f_close 同型的同步段。放在 drain 之后:本拍若写过日志,close
* 已同步过,这里随即 clean 短路——漏网脏页的驻留窗由周期封顶 */
vol_idle_sync_poll();
/* 录波段落盘(task-2):卷空闲 + 环已封存才搬,就绪门/try 锁全在函数内;
* 一次心跳最多一段(lead 裁③)。放在 idle sync 之后:本拍若刚写过录波段,
* 下一拍空闲 flush 会兜住残留脏页,语义与 syslog 排空同位 */
wave_store_drain();
#endif
/* 栈水位观测:首次 5s 后报告,此后每 2s 采样、出现更深峰值才打印 */
task_wd_seg(TASK_WD_SEG_STACK);
@@ -565,6 +592,248 @@ static FRESULT storage_bring_up(FATFS *fs) {
}
#endif /* !TEST_SUITE_STORAGE */
/*
* 函数功能:故障录波落盘道——把已封存的 wave_ring 段整段搬进 /wave(task-2)
* 入口参数:无 void
* 返回值:无 void
* 限定条件:仅 defaultTask 主循环调用(vol_idle_sync_poll 之后);内部绝不阻塞——
* try 锁 + 有界段长(5250 B / 256 B ≈ 21 次 f_write,设计 §4.2 预估几十 ms),
* 绝不把喂狗点做成等待态(vol_lock.h 使用约定:defaultTask 一律 try)
* 函数说明:所有权按 docs/子系统设计/故障录波设计说明.md §5.1——环的生产者是
* adcTask,本道是唯一落盘消费者;跨任务握手只认 wave_ring_sealed()。
* 本道承担五条口径:
* ① 打开一律 FA_WRITE|FA_OPEN_APPEND:本移植 FA_OPEN_APPEND 不含 FA_WRITE
* (docs/架构设计/STM32F4-Base存储架构说明.md §10.1 陷阱一),少写位
* f_write 恒 FR_DENIED;
* ② 每次 f_write 比对写回字节数:本移植簇分配失败返 FR_OK+短写无错误码
* (同处陷阱二),短写一律判失败;
* ③ 轮转号由本道自持(lead 转告(b)):ring.seg_idx 只是本 boot 本地计数,
* 盘上权威下标 = 首次持锁扫 /wave 用 wave_store_idx_from_name() 重建
* (取已存在最大号 +1,空目录 = 0,陌生名字不入账),写成功才
* wave_store_next_idx() 推进;失败不推进、重试同号;
* ④ 成败都必须 wave_ring_write_done() ACK——ACK 前环一直冻结;err 账
* 由 ring 的 err_cnt 单点收口(§9),本道不数第二份(ACK 前环已
* 超时自弃的那次例外:skip 掉 ACK,不去补第二笔账);
* ⑤ 锁跨越一次完整 open...close(vol_lock.h 铁律),持锁耗时超
* WAVE_DRAIN_HELD_WARN_MS 按事件补一条 WARN(§9 唯一周期行不在本道),
* 历史峰值 s_wheld_ms_max 供核 defaultTask 喂狗余量(lead 裁③)。
* 失败处置(lead 裁①):开过文件才 unlink 半成品(未开成的失败不动盘,
* 绝不拿未初始化的路径误删旧段);unlink 失败照旧 ACK(-1)——最坏留
* 一个坏段文件,PC 侧被 CRC 判废,不会画成证据。
* 静默跳过(不刷屏):卷未就绪 / 环未封存 / try 锁没到(下轮再来,
* 环侧 2 s 超时自弃是兜底账,本道不为此打日志)。
* (上面五条口径就是本函数功能「把已封存段整段搬进 /wave」的逐条展开,
* 细则出处 docs/子系统设计/故障录波设计说明.md §5.1 与 §10)
*/
static void wave_store_drain(void) {
static FIL s_wfil; /* static:FF_FS_TINY=0 时 FIL ~528 B,
* 绝不上栈(FS_VOL_PROBE 同一理由) */
static uint8_t s_wbuf[WAVE_DRAIN_CHUNK_B];
static wave_pack_ctx_t s_wpack;
static wave_seg_meta_t s_wmeta;
static char s_wpath[WAVE_ST_PATH_MAX];
static DIR s_wdir;
static FILINFO s_wfno;
static uint8_t s_wnext_idx; /* 本道自持的下一个段号 0..99 */
static uint8_t s_wscan_done; /* 1 = 本 boot 已扫过 /wave */
static uint8_t s_wdir_ready; /* 1 = /wave 已建或已存在 */
static uint8_t s_wcrc_state; /* 0 未验 1 过 2 败(败=永久自弃,不烧垃圾段) */
static uint32_t s_wheld_ms_max; /* 持锁搬段历史峰值 ms(收工报告核余量用) */
uint32_t t0_ms;
uint32_t held_ms;
uint32_t written = 0u;
uint32_t len = 0u;
uint32_t iter;
uint8_t opened = 0u;
uint8_t skip_ack = 0u; /* 1 = 环已自行了断本段,本道不 ACK 不补账 */
wave_ring_stat_t wst2; /* 关档后复检环侧身份用(v1.5) */
const char *why = NULL;
FRESULT fr;
UINT bw;
int rc;
/* 就绪门(同 vol_idle_sync_poll 的四级门风格) */
if (sys_evtHandle == NULL) {
return;
}
if ((osEventFlagsGet(sys_evtHandle) & SYS_EVT_STORAGE_READY) == 0u) {
return;
}
/* 封存门:只搬已封存段(唯一跨任务握手量,§5.1) */
if (wave_ring_sealed() == 0u) {
return;
}
/* 空闲门:try 锁;卷被 syslog/配置/FTP 占住 → 本轮放弃,不排队不打日志 */
if (vol_lock_wait(0u) == 0u) {
return;
}
t0_ms = (uint32_t)xTaskGetTickCount();
/* CRC 锚点自检每 boot 一次(落盘前失败 = 整族失败,永久自弃免得白烧写次数) */
if (s_wcrc_state == 0u) {
s_wcrc_state = (wave_store_crc_selftest() == WAVE_ST_OK) ? 1u : 2u;
}
if (s_wcrc_state == 2u) {
why = "crc selftest FAIL";
}
if ((why == NULL) && (s_wdir_ready == 0u)) {
fr = f_mkdir(WAVE_ST_DIR);
if ((fr == FR_OK) || (fr == FR_EXIST)) {
s_wdir_ready = 1u;
} else {
why = "mkdir /wave";
}
}
if ((why == NULL) && (s_wscan_done == 0u)) {
FRESULT rr;
fr = f_opendir(&s_wdir, WAVE_ST_DIR);
if (fr == FR_OK) {
uint8_t found = 0u;
uint8_t mx = 0u;
for (;;) {
uint8_t ix;
rr = f_readdir(&s_wdir, &s_wfno);
if ((rr != FR_OK) || (s_wfno.fname[0] == 0)) {
break;
}
if (wave_store_idx_from_name(s_wfno.fname, &ix) == WAVE_ST_OK) {
if ((found == 0u) || (ix > mx)) {
mx = ix;
}
found = 1u;
}
}
(void)f_closedir(&s_wdir);
if (rr == FR_OK) {
/* max+1 即下一写号;回绕号复用由 PC 侧 t0/frame_seq 对账兜底,
* 目录里没有的事实不去猜更多(陌生名字一律不入账) */
s_wnext_idx = (found != 0u) ? wave_store_next_idx(mx) : 0u;
s_wscan_done = 1u;
} else {
why = "readdir /wave";
}
} else {
why = "opendir /wave";
}
}
if ((why == NULL) && (s_wscan_done != 0u)) {
if (wave_ring_sealed_meta(&s_wmeta) != WAVE_ST_OK) {
/* sealed 检查与此读之间环 2 s 超时自弃(err 已由环计过):
* 本道不 ACK、不补账、不打日志,放锁走人 */
skip_ack = 1u;
}
}
if ((why == NULL) && (skip_ack == 0u)) {
/* lead 转告(e):E_FORMAT/E_SIZE = 坏段头,一律当落盘失败丢段计 err,
* 不当参数错吞掉 */
rc = wave_store_pack_start(&s_wpack, &s_wmeta, wave_ring_read_cb, NULL);
if (rc != WAVE_ST_OK) {
why = "pack_start";
}
}
if ((why == NULL) && (skip_ack == 0u)) {
if (wave_store_seg_path(s_wpath, (uint32_t)sizeof(s_wpath),
s_wnext_idx) <= 0) {
why = "seg_path";
} else {
fr = f_open(&s_wfil, s_wpath, FA_WRITE | FA_OPEN_APPEND);
if (fr != FR_OK) {
why = "f_open";
} else {
opened = 1u;
}
}
}
iter = 0u;
while ((why == NULL) && (opened != 0u)) {
rc = wave_store_pack_next(&s_wpack, s_wbuf,
(uint32_t)sizeof(s_wbuf), &len);
if ((rc != WAVE_ST_OK) && (rc != WAVE_ST_DONE)) {
why = "pack_next";
break;
}
if (len != 0u) {
bw = 0u;
fr = f_write(&s_wfil, s_wbuf, (UINT)len, &bw);
if ((fr != FR_OK) || (bw != (UINT)len)) {
why = "f_write short"; /* ②:FR_OK+短写 = 本移植独有失败形态 */
break;
}
written += len;
}
if (rc == WAVE_ST_DONE) {
break;
}
if (++iter > 32u) { /* 5250/256 = 21 片;游标卡死时的有界保险 */
why = "pack stall";
break;
}
}
if (opened != 0u) {
fr = f_close(&s_wfil);
if ((why == NULL) && (skip_ack == 0u) && (fr != FR_OK)) {
why = "f_close";
}
}
if ((why == NULL) && (skip_ack == 0u) && (opened != 0u)) {
if (written != wave_store_pack_total(&s_wpack)) {
why = "len mismatch"; /* 长度等式(§6.3):半截文件骗不过这条 */
}
}
/* v1.5(lead 复核):adcTask 优先级高于本道,搬段中途它可能正好把滞留 2 s 的本段
* 自弃(甚至已经封出下一段)。数据区的 CRC 是对"本道读到的字节"算的,半新半旧的
* 段一样自洽 → 关档后必须复检:握手量没了、或段首身份不是本段,一律判废。
* 判废走既有失败路:unlink 本道开过的文件 + write_done(-1)(此时多半回 E_STATE,
* 环侧的 err 已由自弃计过,这里不重复补账)+ 段号不推进(同号重写覆盖)。 */
if ((why == NULL) && (skip_ack == 0u)) {
if ((wave_ring_sealed() == 0u) || (wave_ring_get_stat(&wst2) != WAVE_ST_OK)
|| (wst2.last_t0_ms != s_wmeta.t0_ms) || (wst2.last_seq != s_wmeta.frame_seq)) {
why = "ring abandoned";
}
}
if (skip_ack == 0u) {
/* 失败留痕边沿锁存(搬进本块,lead 裁方案 A):
* ① 它是 static,初值只在编译期发生一次、不是每次进块重置;
* ② 它是"上一拍是否处于失败态"的边沿锁存,所以必须跨调用存活;
* ③ 单独放这里而不在函数入口那 12 连排台账里,因为 variableScope
* 检查要求把局部量收进真正覆盖全部使用点的最小块(本仓刻意开了 style)。 */
static uint8_t s_wfail_latch; /* 一个 episode 只留一条痕 */
if (why == NULL) {
(void)wave_ring_write_done(0);
s_wnext_idx = wave_store_next_idx(s_wnext_idx); /* 成功才推进(转告(b)) */
s_wfail_latch = 0u;
} else {
if (opened != 0u) {
(void)f_unlink(s_wpath); /* 只清本道开过的文件;unlink 失败照旧 ACK */
}
(void)wave_ring_write_done(-1);
if (s_wfail_latch == 0u) {
s_wfail_latch = 1u; /* 持续失败不刷屏;成功一次重新武装 */
DBG_ERROR("[WAVE] seg drop: %s (idx=%u written=%lu)", why,
(unsigned)s_wnext_idx, (unsigned long)written);
}
}
}
/* 持锁耗时台账(lead 裁③i/ii):超线按事件补一条 WARN(原先这里静默占锁,
* 就是「等锁即毁喂狗链」前例的同族黑洞);峰值进静态供收工报告 */
held_ms = (uint32_t)xTaskGetTickCount() - t0_ms;
if (held_ms > s_wheld_ms_max) {
s_wheld_ms_max = held_ms;
}
if ((skip_ack == 0u) && (held_ms > (uint32_t)WAVE_DRAIN_HELD_WARN_MS)) {
/* 本仓 dbg_log 无 WARN 级(宏只有 ERROR/INFO/DEBUG,ERR 级自动带 [ERR] = @test
* 禁用标记):[WARN] 按 console_log.c 先例进正文,慢不是错(真失败另有 seg drop) */
DBG_INFO("[WAVE][WARN] seg held %lu ms (idx=%u)",
(unsigned long)held_ms, (unsigned)s_wnext_idx);
}
vol_lock_release(); /* 成败/跳过都放锁:锁泄漏比丢一段录波重一百倍 */
}
/*
* 函数功能:"保存网络配置"请求通知(注册给 net_cfg 的保存交接;
* 2026-09-25 自 Src/freertos.c 迁出,逻辑零改动)
+23
View File
@@ -45,6 +45,12 @@
行为不变:判定式、调用位置(采样恢复分支内、巡检/压板
之前)、边沿日志与 SOE 分型全部照旧;s_prev_cal_lock
仍只作边沿日志用,**不是**判据缓存
* v1.4 2026-10-29 DTU 项目组 故障录波触发接线(task-2,lead 裁②):
* s_log_record_edge() 的 picked 上升沿分支补一次
* wave_ring_request()——与 SYSLOG_START 同一事件点
* (设计 §4.1「同源,避免两处各自判定」),判据本体
* (prot_elm_run/判定逻辑)零改动;一拍只请求一次,
* elm = 本拍最低位上升元件
*/
#include "protect_task.h"
@@ -62,6 +68,7 @@
#include "prot_set.h"
#include "soe_store.h" /* B9:动作/复归边沿 → SOE 投递(零 FS,队列异步) */
#include "sys_log.h" /* task-10:事故 / 保护告警 / 保护启动 三类文本记录 */
#include "wave_ring.h" /* task-2:picked 上升沿 → 录波请求(与 SYSLOG_START 同源同点) */
#include "task_watchdog.h"
#include "tpafe5160.h" /* tpafe5160_frame_seq():帧序号(BUSY EXTI 写) */
@@ -151,8 +158,24 @@ static void s_log_active_edge(uint32_t active) {
static void s_log_record_edge(uint32_t active, uint32_t pick) {
uint32_t p_chg = pick ^ s_prev_pick;
uint32_t a_chg = active ^ s_prev_active;
const uint32_t p_rise = p_chg & pick; /* picked 0->1 的元件位集合 */
uint32_t i;
/* task-2 补(第 3 类边沿的用途):picked 上升沿同点请求录波。一拍只调一次——
* 多元件同拍启动录同一段波就够(环本就不重入,后续请求由 ring 计 ovf,不当
* 失败);元件号取最低位上升元件(位号最小 = 表中靠前,速断段优先于定时限段)。
* 返回值刻意忽略:本函数在 protectTask 的最坏栈链上(B9 写进 DoD 的硬线),
* 不为此加日志;err/ovf 两本账全在串口 wave: 周期行与环 stat 里
* (docs/子系统设计/故障录波设计说明.md §9)。 */
if (p_rise != 0u) {
uint32_t e = 0u;
while (((p_rise >> e) & 1u) == 0u) {
e++; /* p_rise != 0 → 必在 32 位内命中,无越界 */
}
(void)wave_ring_request((uint8_t)WAVE_TRIG_PROTECT, (uint8_t)e);
}
for (i = 0u; (i < (uint32_t)PE_COUNT) && (((p_chg | a_chg) != 0u)); i++) {
const uint32_t bit = 1u << i;
+473
View File
@@ -60,6 +60,16 @@
#include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */
#include "net_latency_test.h" /* M3 探针:ui_loop(生产循环实测周期) */
#include "task_watchdog.h"
/* 故障录波回放道(UI 道):录波注入表执行侧需要的四份头。
* wave_ring.h 给触发投递(request 任意上下文可调、非阻塞);
* wave_store.h 给段格式解析与定点 mV 换算(唯一口径,页面不复述算式);
* vol_lock.h + ff.h:/wave 只读遍历按 soe_store.c 读者同款纪律——
* try 锁非阻塞(卷忙本轮按「无段」处理,uiTask 绘制节拍绝不等待)、
* 锁跨完整文件操作序列、小窗口分次读、整段 5250B 绝不驻留 RAM */
#include "ff.h"
#include "vol_lock.h"
#include "wave_ring.h"
#include "wave_store.h"
#include "task_prof.h" /* P2 常开探针:循环周期 / 工作耗时 / 推流往返 */
#include "sb_link.h" /* 屏板链路:帧 B 组帧 / 按键出队 / 统计与离线判定(批 2) */
@@ -359,6 +369,463 @@ static int ui_task_soe_get(uint32_t idx, ui_w_soe_rec_t *p_out) {
static const ui_soe_reader_t s_soe_reader = { ui_task_soe_count, ui_task_soe_get };
/* ═══════════════ 故障录波注入表执行侧(UI 道,2026-10-29)═══════════════
* 纪律逐条对齐 soe_store.c 读者(vol_lock try / 锁跨完整文件序列 / 小窗口):
* - 卷忙(vol_lock_wait(0) 返 0)一律按「本轮无段」处理——uiTask 是
* LVGL + 推流节拍的所有者,等锁 = 停摆,宁可空一帧也不阻塞(lead 裁定);
* - 段完整性判定只信 wave_store 自己的判据(尺寸等式 / magic+ver / 双 CRC /
* sem==index),本胶水不读 avail/flags 做二次裁断(§6.3 与 2026-10-29
* 「VALID+CLIPPED 非矛盾」裁定同源:可用性不是完整性);
* - 整段 5250B 不驻留:256B 静态窗口分次读,头区一次、数据区流式过 CRC。
* uiTask 阻塞上界(IWDG 800ms 窗、uiTask 在 required_mask 内;写法对齐上方
* 维护通道那段上界,同文件先例):
* - 第零项是「本块不等待」:vol_lock_wait(0) 拿不到锁即按「本轮无段」
* 返回——不等锁 = 零阻塞,它本身就是上界的一项,不是只保心跳判读;
* - 一次渲染实际发生的 I/O(进回放页首绘、或按键后重绘那帧)三件叠加:
* ① 表重扫 = 1 次 f_opendir + 至多 WAVE_ST_SEG_MAX(100)次 f_readdir,
* 收尾是 100 元插入排序(纯内存);
* ② 段头装载 = 仅未缓存段才发生:1 次 f_open + 头区 1 次 128B 读 +
* head_parse 内的头 CRC + 数据区 20 次 256B 流式读 + 尾 2B 一次
* = 最坏 22 次小读;5120B 逐字节 CRC 与 sem 表校验是纯计算,
* 168MHz 下微秒到毫秒量级;缓存命中零卷操作;
* ③ 窗口装载 = 1 次 f_open + 1 次 f_lseek + 1 次 80B 读(40 点一窗),
* 解码 40 点 + 逐点 code_to_milli 峰值扫描 40 次(int64 乘除,
* 微秒级)——不是全段 8×320 扫描,回放页一屏只碰一个通道一窗;
* - 纯内存不碰卷的路径(评审据此看最坏的落点):manual_req 只投一个
* 请求字节;已缓存段的上下切道、左右切段的段号表环游;空态与坏态
* 直接返回。所以最坏只落在「第一次进回放页 / 第一次切到未缓存段」
* 这类一次性动作上,同段内连续按键不重复付 ②。
* 没有实测数,上面是与落盘道同一把尺的**推算**:落盘道的告警线是
* 50 ms / 21 次写,读路径无簇分配、不写 FAT 元数据,只会更便宜,故按
* 同量级不高于它估——一次渲染最坏一窗 ≈ 段表 + 一段全验 + 一窗,仍远
* 小于 800ms。实测归台架,仪器就用本文件已经在跑的 task_prof 单轮
* 工作耗时探针,不必新加埋点。
* 段序口径(V0.1 已知限制,终报点名):目录重建后按段号数值排序环游,
* 盘上写满 100 段翻转后「数值序 = 时间序」不再严格成立——每屏顶行都带
* 段号自证身份,查事故先看号再看序;翻转窗口排序升级留给台架结论。
*/
static uint8_t s_wave_idx[WAVE_ST_SEG_MAX]; /* 盘上现存段号升序表 */
static uint8_t s_wave_cnt; /* 表内条数 */
static uint8_t s_wave_scanned; /* 表是否有效(manual_req 后作废) */
static uint8_t s_wave_cached_idx; /* 段头缓存属于哪一段 */
static uint8_t s_wave_cache_ok; /* 该段已验完整(头+数据 CRC) */
static wave_seg_meta_t s_wave_meta; /* 已验段的头(172B,唯一驻留) */
static uint8_t s_wave_buf[256u]; /* 读窗:数据区流式 256B/次(lead 裁定
* 2026-10-29 第 1 条·第一条:128B 抬 256B,
* f_read 次数对半砍,RAM 只多 128B;
* 不选 100 位「已验」位图——位图的作废纪律
* 一旦漏做,「CRC 不符必须拒收」这条台架
* 判据对旧段号会静默失效,多 128B 没这个面)*/
/*
* 函数功能:重扫 /wave 目录,把现存段号收集成升序表
* 入口参数:无
* 返回值:0 - 扫描完成(条数见 s_wave_cnt);-1 - 卷忙本轮跳过 int
* 限定条件:uiTask;一次 f_opendir + 有界 f_readdir,全程锁内
* 函数说明:目录不存在(从未录过波)与目录空都归「0 段」,不是错误;
* 名字不合形态的文件(E_NAME)静默跳过——那是别的道写进来的
* 东西,回放页不替它背书也不报警
*/
static int ui_task_wave_scan(void) {
static DIR dir; /* static:同 FIL 理由,压栈深 */
FILINFO finfo;
uint8_t idx;
uint8_t i;
if (vol_lock_wait(0u) == 0u) {
return -1;
}
s_wave_cnt = 0u;
if (f_opendir(&dir, WAVE_ST_DIR) == FR_OK) {
for (;;) {
if (f_readdir(&dir, &finfo) != FR_OK || finfo.fname[0] == '\0') {
break;
}
if (finfo.fattrib & AM_DIR) {
continue;
}
if (wave_store_idx_from_name(finfo.fname, &idx) == 0) {
if (s_wave_cnt < WAVE_ST_SEG_MAX) {
s_wave_idx[s_wave_cnt++] = idx;
}
}
}
f_closedir(&dir);
}
vol_lock_release();
/* 插入排序:条数上限 100,不引 qsort 的 stdlib 依赖 */
for (i = 1u; i < s_wave_cnt; i++) {
uint8_t j;
uint8_t tmp;
tmp = s_wave_idx[i];
j = i;
while ((j > 0u) && (s_wave_idx[j - 1u] > tmp)) {
s_wave_idx[j] = s_wave_idx[j - 1u];
j--;
}
s_wave_idx[j] = tmp;
}
s_wave_scanned = 1u;
return 0;
}
/*
* 函数功能:确保段表可用(必要时重扫)
* 入口参数:无
* 返回值:0 - 表可用;-1 - 卷忙 int
* 限定条件:uiTask 绘制/按键链
*/
static int ui_task_wave_table(void) {
if (s_wave_scanned == 0u) {
return ui_task_wave_scan();
}
return 0;
}
/*
* 函数功能:表内查段号下标
* 入口参数:a_idx - 段号;a_pos - 出参下标
* 返回值:1 - 命中;0 - 不在表内 uint8_t
* 限定条件:表已可用(先过 ui_task_wave_table)
*/
static uint8_t ui_task_wave_find(uint8_t a_idx, uint8_t *a_pos) {
uint8_t i;
for (i = 0u; i < s_wave_cnt; i++) {
if (s_wave_idx[i] == a_idx) {
*a_pos = i;
return 1u;
}
}
return 0u;
}
/*
* 函数功能:读段并做完整性判定(头 CRC 由 head_parse 判,数据 CRC 流式判),
* 成功则把段头留在 s_wave_meta 供 seg_win 免重读
* 入口参数:a_idx - 段号
* 返回值:UI_WAVE_OK / UI_WAVE_NONE(卷忙/文件没了)/ UI_WAVE_BAD(判废) int
* 限定条件:uiTask;锁跨 open→close 完整序列;头 128B 一次、数据区 256B
* 分窗流式(共约 1 + 20 次 f_read),整段不驻留
* 耗时账(lead 裁定 2026-10-29 第 1 条·第三条并存):未缓存段的完整性
* 判定最坏约 21 次小读 + 5120B 逐位 CRC,全部在卷锁内、uiTask 上;
* IWDG 800ms 窗内余量几何**实测毫秒归台架**(终报点名待测项)。
* 缓存只留一段:左右切段 A→B→A 会把 B 的校验再来一遍——接受该
* 成本的代价换「CRC 不符必须拒收」永远现算,不做跨键存的位图
* 函数说明:缓存命中(同段且已验)直接返 OK——左右切段回到上一段、上下切
* 通道都靠它把重复的 5250B 全扫压成一次;文件读中途消失(被别的
* 写者轮转掉)按 NONE 处理,页面下轮自然重扫
*/
static int ui_task_wave_load_head(uint8_t a_idx) {
static FIL f; /* static:FIL = 528B,理由同 soe_store_read */
char path[WAVE_ST_PATH_MAX]; /* 容量 = WAVE_ST_PATH_MAX(17 字节口径,
* 自己声明路径缓冲不得写死 16) */
FRESULT fr;
UINT br = 0u;
int rc;
if ((s_wave_cache_ok != 0u) && (s_wave_cached_idx == a_idx)) {
return UI_WAVE_OK;
}
if (wave_store_seg_path(path, (uint32_t)sizeof(path), a_idx) != 0) {
return UI_WAVE_NONE;
}
if (vol_lock_wait(0u) == 0u) {
return UI_WAVE_NONE; /* 卷忙 = 本轮按无段处理(不阻塞 uiTask) */
}
rc = UI_WAVE_NONE;
if (f_open(&f, path, FA_READ) == FR_OK) {
uint32_t size;
uint32_t total;
uint32_t n;
size = (uint32_t)f_size(&f);
total = 0u;
rc = UI_WAVE_BAD;
/* 第一步:头区(首窗 128B = 当前形态整头);head_parse 自带判废 */
n = (size < (uint32_t)WAVE_ST_HEAD_SZ_CUR) ? size
: (uint32_t)WAVE_ST_HEAD_SZ_CUR;
fr = f_read(&f, s_wave_buf, n, &br);
if ((fr == FR_OK) && (br == n)) {
if (wave_store_head_parse(s_wave_buf, n, &s_wave_meta) == 0) {
total = wave_store_seg_size(&s_wave_meta);
}
}
/* 第二步:尺寸等式(零长/长度与头部字段矛盾 = 整段判废,§6.3) */
if ((total != 0u) && (size == total)) {
uint32_t off;
uint16_t crc;
uint8_t tail[2];
/* 第三步:数据区流式 CRC(128B 窗;CRC 覆盖不含尾两字节自身) */
crc = WAVE_ST_CRC_INIT;
off = wave_store_head_bytes(s_wave_meta.ch_count);
rc = UI_WAVE_BAD;
while ((off + 2u) < size) {
n = (size - 2u - off < (uint32_t)sizeof(s_wave_buf))
? (size - 2u - off) : (uint32_t)sizeof(s_wave_buf);
fr = f_read(&f, s_wave_buf, n, &br);
if ((fr != FR_OK) || (br != n)) {
rc = UI_WAVE_BAD;
break;
}
crc = wave_store_crc_update(crc, s_wave_buf, n);
off += n;
}
/* 第四步:读回存储值比对(低字节在前) */
if ((rc == UI_WAVE_BAD) && (f_lseek(&f, size - 2u) == FR_OK) &&
(f_read(&f, tail, 2u, &br) == FR_OK) && (br == 2u)) {
if (crc == (uint16_t)(tail[0] | ((uint16_t)tail[1] << 8))) {
rc = UI_WAVE_OK;
}
}
} else if (total == 0u) {
rc = UI_WAVE_BAD; /* seg_size 返 0 = 头字段自相矛盾,判废 */
} else {
rc = UI_WAVE_BAD; /* 长度与头部字段不符,判废 */
}
(void)f_close(&f);
}
vol_lock_release();
if (rc == UI_WAVE_OK) {
s_wave_cached_idx = a_idx;
s_wave_cache_ok = 1u;
} else {
s_wave_cache_ok = 0u;
}
return rc;
}
/*
* 函数功能:录波触发页唯一写出口——投一次手动触发请求
* 入口参数:无
* 返回值:0 - 已受理;非零 - 拒收(未 IDLE/冷却中/参数),页面显示 ERR int
* 限定条件:uiTask(request 本身任意上下文可调,这里只是转发不等待)
* 函数说明:受理 = ring 进冻结窗,成段最快也在约一秒后由默认任务落盘;
* 顺手作废段表缓存,页面下次取数会看到新段
*/
static int ui_task_wave_manual_req(void) {
int r = wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u);
if (r == 0) {
s_wave_scanned = 0u;
s_wave_cache_ok = 0u; /* lead 裁定 2026-10-29 第 3 条:段表与段头缓存
* 一起作废——同名文件会被轮转重写,旧头字段
* (pts/pre/k/b/逐路 avail)不作废就会顶着新段
* 的号用旧尺子。单槽缓存本身 = 已知限制:
* 只挡「本段验过不再重验」,多段来回切会重验 */
}
return r;
}
/*
* 函数功能:取盘上「数值最大段号」(回放页开屏落点)
* 入口参数:a_idx - 出参段号
* 返回值:UI_WAVE_OK / NONE(无段或卷忙本轮)/ BAD 本件不产(表只含现存文件)
* 函数说明:口径 = 数值最大,**不是时间最新**(lead 裁定 2026-10-29):
* 回绕后数值序不等于时间序,页顶显示的段号即自证;真做时间序要
* 逐段开文件读 frame_seq/t0,一百次开档不在回放页的账上,本波不做
*/
static int ui_task_wave_seg_first(uint8_t *a_idx) {
if (ui_task_wave_table() != 0) {
return UI_WAVE_NONE; /* 卷忙 = 本轮无段(同一条不阻塞纪律) */
}
if (s_wave_cnt == 0u) {
return UI_WAVE_NONE;
}
*a_idx = s_wave_idx[s_wave_cnt - 1u];
return UI_WAVE_OK;
}
/*
* 函数功能:环游取邻段(a_dir:-1 更早 / +1 更新;段号数值序环回)
* 入口参数:a_cur - 当前段号;a_out - 出参邻段段号
* 返回值:UI_WAVE_OK / NONE(表不可用或已无邻段可去) int
* 限定条件:uiTask;当前段已被轮转覆盖时先重扫一次再定位
*/
static int ui_task_wave_seg_around(uint8_t a_cur, int8_t a_dir, uint8_t *a_out) {
uint8_t pos = 0u;
if (ui_task_wave_table() != 0) {
return UI_WAVE_NONE;
}
if (ui_task_wave_find(a_cur, &pos) == 0u) {
if ((ui_task_wave_scan() != 0) || (s_wave_cnt == 0u) ||
(ui_task_wave_find(a_cur, &pos) == 0u)) {
/* 当前段没了(翻转覆盖):退回首段,画面由调用侧重载 */
if (s_wave_cnt == 0u) {
return UI_WAVE_NONE;
}
*a_out = s_wave_idx[0u];
return UI_WAVE_OK;
}
}
if (s_wave_cnt == 1u) {
*a_out = s_wave_idx[0u]; /* 单段:原地(用户看到的仍是这段) */
return UI_WAVE_OK;
}
if (a_dir > 0) {
*a_out = s_wave_idx[(uint8_t)((pos + 1u) % s_wave_cnt)];
} else {
*a_out = s_wave_idx[(uint8_t)((pos + s_wave_cnt - 1u) % s_wave_cnt)];
}
return UI_WAVE_OK;
}
/*
* 函数功能:切到段号表里的前一段(LR 左键的落点,数值升序表 → 「前」= 号更小)
* 入口参数:a_cur 当前段号;a_out 输出切换后的段号(越界/空表不写)
* 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 盘上一段都没有 int
* 限定条件:仅 uiTask 上下文(段号表是本任务私有缓存)
* 函数说明:只是 ui_task_wave_seg_around 的方向包装,存在的理由是 ops 表要两个
* 独立函数指针,不写第二份查找逻辑。
*/
static int ui_task_wave_seg_prev(uint8_t a_cur, uint8_t *a_out) {
return ui_task_wave_seg_around(a_cur, -1, a_out);
}
/*
* 函数功能:切到段号表里的后一段(LR 右键;数值升序表 → 「后」= 号更大)
* 入口参数:a_cur 当前段号;a_out 输出切换后的段号(越界/空表不写)
* 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 盘上一段都没有 int
* 限定条件:仅 uiTask 上下文(同上)
* 函数说明:与 seg_prev 共用 ui_task_wave_seg_around,切段即触发一次懒加载;
* 未缓存段的读盘 + 流式 CRC 也发生在这条调用链上(单轮上界见本
* 文件「故障录波注入表执行侧」块首注释的 uiTask 阻塞上界段)。
*/
static int ui_task_wave_seg_next(uint8_t a_cur, uint8_t *a_out) {
return ui_task_wave_seg_around(a_cur, 1, a_out);
}
/*
* 函数功能:读段头(含双 CRC 完整性判定——判定发生在 ui_task_wave_load_head)
* 入口参数:a_idx - 段号;a_eng/a_trig/a_clip - 段头三字段原值
* 返回值:UI_WAVE_OK / NONE / BAD int
* 限定条件:uiTask;命中缓存零卷操作
*/
static int ui_task_wave_seg_head(uint8_t a_idx, uint8_t *a_eng,
uint8_t *a_trig, uint8_t *a_clip) {
int rc = ui_task_wave_load_head(a_idx);
if (rc != UI_WAVE_OK) {
return rc;
}
*a_eng = s_wave_meta.eng;
*a_trig = s_wave_meta.trig_src;
*a_clip = (uint8_t)(s_wave_meta.clip_mask & 0xFFu);
return UI_WAVE_OK;
}
/*
* 函数功能:读固定显示窗(触发点前后各一周波)的码值与峰值
* 入口参数:a_idx - 段号;a_ch - 通道号;a_codes - UI_WAVE_WIN_PTS 个码值
* (已并入段头 b_codes);a_peak_milli - 窗内绝对值最大点的 mV
* 返回值:UI_WAVE_OK / NONE(段头未验过/卷忙)/ BAD(数据区读不齐) int
* 限定条件:uiTask;先段头后窗口(未过完整性判定的段不供数据,页面显示
* 「该段不可读」而不是半截波形——判废优先于展示)
* 函数说明:窗口起点用段头 pre_pts 现算(§8.2「数据不是编译常量」:
* 起点 = pre_pts - 窗长一半,与光标居中是同一个 20 的一半,
* 改窗宽只改 ui_data.h 的 UI_WAVE_WIN_PTS 一处);峰值的 mV
* 换算逐点走 wave_store_code_to_milli —— 全仓唯一口径,
* 这里不重抄公式(§6.3;PC 道同一条款)
*/
static int ui_task_wave_seg_win(uint8_t a_idx, uint8_t a_ch,
int16_t *a_codes, int32_t *a_peak_milli) {
static FIL f;
char path[WAVE_ST_PATH_MAX];
UINT br = 0u;
uint32_t off;
uint32_t want;
uint32_t start;
uint32_t got_pts = 0u;
int16_t raw[UI_WAVE_WIN_PTS];
int32_t milli;
int rc;
if (ui_task_wave_load_head(a_idx) != UI_WAVE_OK) {
return UI_WAVE_BAD; /* 段头坏了/读不到:窗口无从谈起 */
}
if (a_ch >= s_wave_meta.ch_count) {
return UI_WAVE_BAD;
}
if ((s_wave_meta.pts_per_ch < UI_WAVE_WIN_PTS) ||
(s_wave_meta.pre_pts < (UI_WAVE_WIN_PTS / 2u))) {
return UI_WAVE_BAD;
}
start = (uint32_t)s_wave_meta.pre_pts - (UI_WAVE_WIN_PTS / 2u);
off = wave_store_data_off(s_wave_meta.ch_count, s_wave_meta.pts_per_ch,
a_ch, start);
if (off == 0xFFFFFFFFu) {
return UI_WAVE_BAD;
}
want = wave_store_data_bytes_avail(s_wave_meta.pts_per_ch, (uint16_t)start,
UI_WAVE_WIN_PTS);
if (want != UI_WAVE_WIN_PTS * 2u) {
return UI_WAVE_BAD; /* 窗口贴不出完整 40 点:显式不可读 */
}
if (wave_store_seg_path(path, (uint32_t)sizeof(path), a_idx) != 0) {
return UI_WAVE_NONE;
}
if (vol_lock_wait(0u) == 0u) {
return UI_WAVE_NONE;
}
rc = UI_WAVE_BAD;
if (f_open(&f, path, FA_READ) == FR_OK) {
if ((f_lseek(&f, off) == FR_OK) &&
(f_read(&f, s_wave_buf, want, &br) == FR_OK) && (br == want)) {
if ((wave_store_le_decode(s_wave_buf, want, raw, UI_WAVE_WIN_PTS,
&got_pts) == 0) &&
(got_pts == UI_WAVE_WIN_PTS)) {
rc = UI_WAVE_OK;
}
}
(void)f_close(&f);
}
vol_lock_release();
if (rc == UI_WAVE_OK) {
uint8_t i;
int32_t peak = 0;
for (i = 0u; i < UI_WAVE_WIN_PTS; i++) {
int32_t v;
v = (int32_t)raw[i] + s_wave_meta.ch[a_ch].b_codes;
if (v > 32767L) {
v = 32767L;
} else if (v < (-32768L)) {
v = -32768L;
}
a_codes[i] = (int16_t)v;
/* 显示与度量同一把尺(「谁被显示谁被度量」,§6.3):
* a_codes 画的是 raw+b,而 wave_store_code_to_milli 的公式是
* (a_code + b_codes) × k_q16 / 65536000 取整(App/algo/
* wave_store.c:567 实测口径)——传 raw 进去,b 已由唯一口径
* 自己加过,PK 数与屏上波形天然同尺,这里不再加第二遍
* (加两遍才是错);clamp 只护 int16 画布,b 的 V0.1 恒 0 */
if (wave_store_code_to_milli(&s_wave_meta.ch[a_ch], raw[i],
&milli) == 0) {
if ((milli < 0 ? -milli : milli) >
(peak < 0 ? -peak : peak)) {
peak = milli;
}
}
}
*a_peak_milli = peak;
}
return rc;
}
/* 录波通道函数表(六件齐全;ui_data_wave_bind 拒绝残表,缺件就是装配错) */
static const ui_data_wave_ops_t s_wave_ops = {
ui_task_wave_manual_req,
ui_task_wave_seg_first,
ui_task_wave_seg_prev,
ui_task_wave_seg_next,
ui_task_wave_seg_head,
ui_task_wave_seg_win,
};
/*
* 函数功能:推流往返边界的心跳续命钩(F3,注册给 ui_net_sink)
* 入口参数:无
@@ -462,6 +929,12 @@ void ui_task_func(void *argument) {
if (ui_data_maint_bind(&s_maint_ops) != 0) {
DBG_ERROR("UI maint bind: FAIL");
}
/* 录波通道注入(故障录波道 UI 侧,2026-10-29):手动录波触发页与回放页
* 的唯一数据入口,同样先于 ui_menu_init。未注入窗口里六件按「无录波段」
* 空态渲染(不画线、不崩);残表 = 装配错,启动日志必须可见 */
if (ui_data_wave_bind(&s_wave_ops) != 0) {
DBG_ERROR("UI wave bind: FAIL");
}
/* 快照层复位:必须先于 ui_menu_init(首帧之前任何页面被打开都按"无数据"
* 渲染 "--",不会读到上电残留)——UI_数据供给设计说明.md §5.3 */
ui_data_init();