fix(wave): 回放页开页作废钉表重扫 + 判废串口逐次留痕(#93 F1/F2) #94

Merged
wandering1 merged 1 commits from fix-wave-findings-b8c190da2e56 into master 2026-11-07 22:02:11 +08:00
7 changed files with 138 additions and 22 deletions
+85 -8
View File
@@ -419,9 +419,12 @@ static const ui_soe_reader_t s_soe_reader = { ui_task_soe_count, ui_task_soe_get
* 解码 40 点 + 逐点 code_to_milli 峰值扫描 40 次(int64 乘除,
* 微秒级)——不是全段 8×320 扫描,回放页一屏只碰一个通道一窗;
* - 纯内存不碰卷的路径(评审据此看最坏的落点):manual_req 只投一个
* 请求字节;已缓存段的上下切道、左右切段的段号表环游;空态与坏态
* 直接返回。所以最坏只落在「第一次进回放页 / 第一次切到未缓存段」
* 这类一次性动作上,同段内连续按键不重复付 ②。
* 请求字节;wave_enter 只翻两个作废旗标(#93 F1,不取锁不开盘);
* 已缓存段的上下切道、左右切段的段号表环游;空态与坏态直接返回。
* 所以最坏落在「进回放页(#93 F1 起每次开页都先作废、首帧付一次 ①,
* 总额仍封死在首帧那一帧)/ 第一次切到未缓存段」这类一次性动作上,
* 同段内连续按键不重复付 ②;空态再探盘的按键付 ①(方案 B 自带的
* 语义——盘真 0 段时每次再探重扫正是该付的,成本有界在空态按键数)。
* 没有实测数,上面是与落盘道同一把尺的**推算**:落盘道的告警线是
* 50 ms / 21 次写,读路径无簇分配、不写 FAT 元数据,只会更便宜,故按
* 同量级不高于它估——一次渲染最坏一窗 ≈ 段表 + 一段全验 + 一窗,仍远
@@ -433,7 +436,8 @@ static const ui_soe_reader_t s_soe_reader = { ui_task_soe_count, ui_task_soe_get
*/
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_scanned; /* 表是否有效(manual_req /
* wave_enter 后作废,#93 F1) */
static uint8_t s_wave_cached_idx; /* 段头缓存属于哪一段 */
static uint8_t s_wave_cache_ok; /* 该段已验完整(头+数据 CRC) */
static wave_seg_meta_t s_wave_meta; /* 已验段的头(172B,唯一驻留) */
@@ -567,6 +571,18 @@ static uint8_t ui_task_wave_find(uint8_t a_idx, uint8_t *a_pos) {
return 0u;
}
/*
* 判废原因码(#93 F2:ui_task_wave_load_head 五类 BAD 出口收敛成一条
* 串口留痕,reject= 字段取值即本组宏)。值域是判读表与 PC 侧互证的
* 接口的一部分:只增不改义。1 收「拿不到一个解析得动的头」的一切
* (头窗读败 = 无从解析,与 head_parse 拒收、段长字段自相矛盾同族)。
*/
#define UI_WAVE_REJ_HEAD 1u /* 头窗读败 / head_parse 拒收 / seg_size 自相矛盾 */
#define UI_WAVE_REJ_LEN 2u /* 长度等式不符(文件尺寸 != 头算出的整段尺寸) */
#define UI_WAVE_REJ_WIN 3u /* 数据区流式读窗失败 */
#define UI_WAVE_REJ_TAIL 4u /* 尾 2B(存储 CRC)读失败 */
#define UI_WAVE_REJ_CRC 5u /* 数据 CRC 不符(留痕带 stored/calc,可与 PC 侧 wave_read REJECT 行对值互证) */
/*
* 函数功能:读段并做完整性判定(头 CRC 由 head_parse 判,数据 CRC 流式判),
* 成功则把段头留在 s_wave_meta 供 seg_win 免重读
@@ -581,7 +597,10 @@ static uint8_t ui_task_wave_find(uint8_t a_idx, uint8_t *a_pos) {
* 成本的代价换「CRC 不符必须拒收」永远现算,不做跨键存的位图
* 函数说明:缓存命中(同段且已验)直接返 OK——左右切段回到上一段、上下切
* 通道都靠它把重复的 5250B 全扫压成一次;文件读中途消失(被别的
* 写者轮转掉)按 NONE 处理,页面下轮自然重扫
* 写者轮转掉)按 NONE 处理,页面下轮自然重扫;五类判废(reject=
* 原因码见上方 UI_WAVE_REJ_* 组)在本函数收尾统一收敛成一条
* DBG_ERROR——只有串口在手的远程轮也要有判废可查面(#93 F2,
* 电平与不判重两条裁定记录在设计说明 §10 的 2026-11-07 节)
*/
static int ui_task_wave_load_head(uint8_t a_idx) {
static FIL f; /* static:FIL = 528B,理由同 soe_store_read */
@@ -591,6 +610,11 @@ static int ui_task_wave_load_head(uint8_t a_idx) {
FRESULT ofr; /* 开档 FR 码:开档失败留痕用 */
UINT br = 0u;
int rc;
uint8_t reject = 0u; /* 判废原因码 UI_WAVE_REJ_*(#93 F2 留痕;
* 0 = 没走到任何判废点。BAD 时必非 0) */
uint16_t crc_stored = 0u; /* 仅 reject=UI_WAVE_REJ_CRC 时有值:与 PC 侧
* wave_read REJECT 行互证的 stored/calc 对 */
uint16_t crc_calc = 0u;
if ((s_wave_cache_ok != 0u) && (s_wave_cached_idx == a_idx)) {
return UI_WAVE_OK;
@@ -641,6 +665,7 @@ static int ui_task_wave_load_head(uint8_t a_idx) {
fr = f_read(&f, s_wave_buf, n, &br);
if ((fr != FR_OK) || (br != n)) {
rc = UI_WAVE_BAD;
reject = UI_WAVE_REJ_WIN;
break;
}
crc = wave_store_crc_update(crc, s_wave_buf, n);
@@ -649,14 +674,27 @@ static int ui_task_wave_load_head(uint8_t a_idx) {
/* 第四步:读回存储值比对(低字节在前) */
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))) {
uint16_t stored = (uint16_t)(tail[0] | ((uint16_t)tail[1] << 8));
if (crc == stored) {
rc = UI_WAVE_OK;
} else if (reject == 0u) {
/* 窗读全程跑完还对不上才是真判废(stored/calc 对值得留
* 给 wave_read 互证);窗已判废(REJ_WIN)时 crc 是半截
* 值,既不覆原因也不留这对比值——免得互证对上一个假数 */
reject = UI_WAVE_REJ_CRC;
crc_calc = crc;
crc_stored = stored;
}
} else if (reject == 0u) {
reject = UI_WAVE_REJ_TAIL; /* 尾 2B 定位/读失败 */
}
} else if (total == 0u) {
rc = UI_WAVE_BAD; /* seg_size 返 0 = 头字段自相矛盾,判废 */
reject = UI_WAVE_REJ_HEAD;
} else {
rc = UI_WAVE_BAD; /* 长度与头部字段不符,判废 */
reject = UI_WAVE_REJ_LEN;
}
(void)f_close(&f);
}
@@ -671,6 +709,21 @@ static int ui_task_wave_load_head(uint8_t a_idx) {
* 没有任何视觉面——必须留痕(10-31 半轮栽就栽在这类静默面上) */
DBG_ERROR("ui wave head idx=%u open fail fr=%d", (unsigned)a_idx,
(int)ofr);
} else if (rc == UI_WAVE_BAD) {
/* #93 F2:五类判废出口(头/长度等式/窗/尾/CRC)收敛成这一条
* 留痕——有画面在手判废有画面自证,只有串口在手的远程无人轮
* 判废不留痕就是事后黑箱。BAD 不写 cache_ok = 每次进坏段都
* 付一次完整校验,留痕与按键、画面一一对应,刻意**不判重**
* ——判废是低频人工动作,逐次留痕正是取证要的东西(电平
* ERROR + 不判重两条裁定见设计说明 §10 的 2026-11-07 节)。
* reject= 原因码表与「reject!=5 时两个 CRC 字段恒 0x0000」
* 判读口径见手册 §3;reject=5 的 stored/calc 与 PC 侧
* wave_read 的 REJECT 行同名同值、可对值互证。[ERR] 会命中
* /serial-boot-test 的 Forbidden 清单 → 坏段轮里按「预期内」
* 点名放行(同手册),正常盘上本行一条都不会出现 */
DBG_ERROR("ui wave head idx=%u reject=%u stored=0x%04X calc=0x%04X",
(unsigned)a_idx, (unsigned)reject,
(unsigned)crc_stored, (unsigned)crc_calc);
}
}
return rc;
@@ -698,6 +751,29 @@ static int ui_task_wave_manual_req(void) {
return r;
}
/*
* 函数功能:回放页开页 / 空态再探盘的作废钩子——作废段表与段头缓存
* (#93 F1 方案 A/B;与 manual_req 受理支同一条作废纪律,字段
* 契约见 ui_data.h wave_enter 注)
* 入口参数:无
* 返回值:无
* 限定条件:uiTask(页面框架 init_cb / key_cb 链都跑在本任务上;作废的
* 是本任务私有缓存,无锁无竞态);纯内存零 I/O,开页链不破
* 函数说明:只翻旗标不重扫——重扫仍懒在 ui_task_wave_table() 首次取数。
* manual_req 的作废盖「本机自己新落段」,本件盖「别的写者
* (FTP STOR)在上次开页之后投放的新段」:11-02 把钉表防抖立
* 成纪律时把这两种情形一起钉死了,开页因此看不见新段(#93 F1
* 发现面)。页内普通按键**不调本件** = 防抖纪律原样保留;空
* 态左右键调(方案 B)只在盘真 0 段时逐次重探——「新段可能已
* 落」这条注释里写了一直没机制兑现的语义从此有兑现。残留纪律
* (手册 §4 / 设计说明 §10 留痕):**页开着时**投放的段要再开
* 一次页才可见,页开着追新段本波不做
*/
static void ui_task_wave_enter(void) {
s_wave_scanned = 0u;
s_wave_cache_ok = 0u;
}
/*
* 函数功能:取盘上「数值最大段号」(回放页开屏落点)
* 入口参数:a_idx - 出参段号
@@ -903,9 +979,10 @@ static int ui_task_wave_seg_win(uint8_t a_idx, uint8_t a_ch,
return rc;
}
/* 录波通道函数表(六件齐全;ui_data_wave_bind 拒绝残表,缺件就是装配错) */
/* 录波通道函数表(七件齐全;ui_data_wave_bind 拒绝残表,缺件就是装配错) */
static const ui_data_wave_ops_t s_wave_ops = {
ui_task_wave_manual_req,
ui_task_wave_enter,
ui_task_wave_seg_first,
ui_task_wave_seg_prev,
ui_task_wave_seg_next,
@@ -1014,7 +1091,7 @@ void ui_task_func(void *argument) {
DBG_ERROR("UI maint bind: FAIL");
}
/* 录波通道注入(故障录波道 UI 侧,2026-10-29):手动录波触发页与回放页
* 的唯一数据入口,同样先于 ui_menu_init。未注入窗口里六件按「无录波段」
* 的唯一数据入口,同样先于 ui_menu_init。未注入窗口里七件按「无录波段」
* 空态渲染(不画线、不崩);残表 = 装配错,启动日志必须可见 */
if (ui_data_wave_bind(&s_wave_ops) != 0) {
DBG_ERROR("UI wave bind: FAIL");
+11 -3
View File
@@ -1090,14 +1090,14 @@ uint8_t ui_data_comm_pending(void) {
/* ── 通道六:故障录波(wave)注入与包装 ─────────────────────────────────────
* 包装层零判据:未注入统一按「无录波段」态返(页面显式空态文案,绝不
* 画平线——设计 §8.1 空态行);六件的参数校验只到指针级,语义判定
* 画平线——设计 §8.1 空态行);七件的参数校验只到指针级,语义判定
* (卷忙/段损坏)全在执行侧,与通道四/五同一条分层纪律。
*/
static const ui_data_wave_ops_t *s_wave = NULL;
/*
* 函数功能:注入录波执行侧函数表(启动期一次)
* 入口参数:ops - 六件齐;NULL = 撤销注入
* 入口参数:ops - 七件齐;NULL = 撤销注入
* 返回值:0 - 已注入/已撤销;-1 - 残表(存量不动,理由逐字同 comm bind) int
* 限定条件:uiTask 启动期,先于 ui_menu_init()
*/
@@ -1106,7 +1106,8 @@ int ui_data_wave_bind(const ui_data_wave_ops_t *a_ops) {
s_wave = NULL;
return 0;
}
if ((a_ops->manual_req == NULL) || (a_ops->seg_first == NULL) ||
if ((a_ops->manual_req == NULL) || (a_ops->wave_enter == NULL) ||
(a_ops->seg_first == NULL) ||
(a_ops->seg_prev == NULL) || (a_ops->seg_next == NULL) ||
(a_ops->seg_head == NULL) || (a_ops->seg_win == NULL)) {
return -1;
@@ -1123,6 +1124,13 @@ int ui_data_wave_manual_req(void) {
return s_wave->manual_req();
}
/* 函数功能:开页 / 空态再探盘的作废钩子(未注入 = 什么都不做,零副作用) void */
void ui_data_wave_enter(void) {
if (s_wave != NULL) {
s_wave->wave_enter();
}
}
/* 函数功能:录波通道三态查询的统一未注入兜底(返 NONE 显空态,不崩) int */
static int wp_wave_none(void) {
return UI_WAVE_NONE;
+17 -4
View File
@@ -947,14 +947,20 @@ uint8_t ui_data_comm_pending(void);
#define UI_WAVE_BAD 2
/*
* 录波执行侧注入表(六件齐全;ui_data_wave_bind 拒绝残表)
* 返回值约定:除 manual_req 外,各件返回 UI_WAVE_OK / NONE / BAD 三态;
* seg_win 的出参仅在返 UI_WAVE_OK 时可读(通道五同一条出参契约)。
* 录波执行侧注入表(七件齐全;ui_data_wave_bind 拒绝残表)
* 返回值约定:除 manual_req 与 wave_enter(两件是动作位,不产三态)外,
* 各件返回 UI_WAVE_OK / NONE / BAD 三态;seg_win 的出参仅在返
* UI_WAVE_OK 时可读(通道五同一条出参契约)。
*/
typedef struct {
/* 投一次手动录波请求。返回值:0 - 已受理;非零 - 拒收(含未落段通道
* 不可用),页面据此走确认链的 ERR 支 */
int (*manual_req)(void);
/* 回放页开页 / 空态再探盘时作废段表与段头缓存(#93 F1 方案 A/B 的
* 作废钩子;纯内存零 I/O——重扫仍懒在首次取数,开页链零 I/O 不破;
* 页内按键不调它(空态再探除外),11-02 钉表防抖纪律原样保留)。
* 返回值:无 */
void (*wave_enter)(void);
/* 取盘上数值最大的段号(回放页开屏落点;左右键从此起环游)。
* 回绕后本序不保证时间最新,页顶段号即自证;时间序要靠段头
* frame_seq/t0,代价是 100 次开文件,本波不做(lead 裁定 2026-10-29) */
@@ -980,7 +986,7 @@ typedef struct {
/*
* 函数功能:注入录波执行侧函数表(启动期一次;形状对齐 ui_data_comm_bind)
* 入口参数:ops - 函数表(六件都不得为 NULL);NULL = 撤销注入(宿主测试用)
* 入口参数:ops - 函数表(七件都不得为 NULL);NULL = 撤销注入(宿主测试用)
* 返回值:0 - 已注入/已撤销;-1 - 残表(存量不动) int
* 限定条件:uiTask 启动期、先于 ui_menu_init()
*/
@@ -992,6 +998,13 @@ int ui_data_wave_bind(const ui_data_wave_ops_t *a_ops);
*/
int ui_data_wave_manual_req(void);
/*
* 函数功能:回放页开页 / 空态再探盘时作废段表与段头缓存(#93 F1 方案
* A/B 的作废钩子;纯内存零 I/O,重扫懒在首帧取数)
* 返回值:无(未注入 = 什么都不做,页面取数行为不变) void
*/
void ui_data_wave_enter(void);
/*
* 函数功能:取数值最大段号(排序口径见 ui_data.c 同名胶水注释:回绕后本序
* 不保证时间最新,页顶段号即自证;真时间序要逐段读 frame_seq/t0,
+15 -3
View File
@@ -12,7 +12,10 @@
* 空态显式文案「无录波段」/「该段不可读」——画成一条直线等于
* 伪造证据(§8.1 空态行,此判据优先于「页面上总得画点东西」)。
* 键位:上下切通道、左右切段(key_intercept=1 才能把 LEFT 从
* 「关页」手里要来);ESC 关页。
* 「关页」手里要来);ESC 关页。页的**缓存作废面**:本文件不持
* 任何缓存,开页(pg_init)与空态再探盘(pg_key IDLE 支)各调
* 一次注入表的 wave_enter 作废钩子(#93 F1 方案 A/B,纯内存
* 零 I/O)——开页后盘上新落盘 / 新投放的段即随重扫进表。
* 取数纪律:本文件零 App include——段号/段头/窗口码值/峰值 mV 全部经
* ui_data.h 录波注入表进来(波形算式与定点换算在执行侧,页面
* 连 b_codes 都不碰,收到的码值已并偏移);首帧绘制才懒取数
@@ -33,6 +36,10 @@
* 本页 key_cb 返回 true(接管键),框架不再替本页失效重绘,状态变了
* 屏上却纹丝不动——该病在枚举修复前整页恒空、从未有键可切,故随
* 2026-11-02 回放页修复首次上板才暴露(真机实证见 PR #67)。
* v1.2 2026-11-07 DTU 项目组 #93 F1:开页(pg_init)与空态左右键
* 再探(pg_key IDLE 支)各调注入表新增的 wave_enter 作废钩子——
* 开机后新落盘/新投放的段重开页即见、空态再探不再被钉表短路;
* 本页像素形态与键位语义不变(黄金参考不刷),重扫仍懒在首帧。
*/
/* 头文件包含区 */
@@ -320,14 +327,18 @@ static const char *wp_eng_word(uint8_t a_eng) {
}
/*
* 函数功能:开页复位(框架 init_cb)——通道回到 UA、数据回到未取态
* 函数功能:开页复位(框架 init_cb)——通道回到 UA、数据回到未取态,
* 并调执行侧作废钩子丢掉钉住的段表/段头缓存(#93 F1 方案 A:
* 开机后新落盘的段开合一次页即进表;作废是纯内存动作,首帧
* 懒取数链原样,开页链零 I/O 不破)
* 入口参数:无
* 返回值:无
* 限定条件:框架开页链内;只清状态零 I/O(懒取数在首帧绘制)
* 限定条件:框架开页链内;只清状态与翻作废旗标、零 I/O(懒取数在首帧绘制)
*/
static void pg_init(void) {
s_state = WP_ST_IDLE;
s_ch = 0u;
ui_data_wave_enter();
}
/*
@@ -406,6 +417,7 @@ static bool pg_key(lv_key_t a_key) {
case LV_KEY_LEFT:
case LV_KEY_RIGHT:
if (s_state == WP_ST_IDLE) {
ui_data_wave_enter(); /* #93 F1 方案 B:先作废钉表,再探才探得出新段 */
wp_ensure(); /* 空态下按左右 = 再探一次盘(新段可能已落) */
ui_content_page_invalidate();
return true;
@@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1`
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| ER_IROM1 | 285,716 B (279.02 KB) | 1,048,576 B (1024 KB) | 27.2% |
| ER_IROM1 | 285,940 B (279.24 KB) | 1,048,576 B (1024 KB) | 27.3% |
| RW_IRAM1 | 94,752 B (92.53 KB) | 131,072 B (128 KB) | 72.3% |
| RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% |
| **Flash (ROM 合计)** | **287,788 B (281.0 KB)** | 1,048,576 B (1024 KB) | **27.4%** |
| **Flash (ROM 合计)** | **288,012 B (281.3 KB)** | 1,048,576 B (1024 KB) | **27.5%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
+1 -1
View File
@@ -15,7 +15,7 @@
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |
|---|---|---|---|---|
| ER_IROM1 (Flash) | 285,716 B | 1,048,576 B | 27.2% | 参考 |
| ER_IROM1 (Flash) | 285,940 B | 1,048,576 B | 27.3% | 参考 |
| RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS |
| UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 |
| RW_IRAM1 | 94,752 B | 131,072 B | 72.3% | PASS |
+7 -1
View File
@@ -853,9 +853,15 @@ static int sim_wave_seg_win(uint8_t a_idx, uint8_t a_ch, int16_t *a_codes,
return UI_WAVE_OK;
}
/* 录波通道假表(与真机 s_wave_ops 同形状六件) */
/* 函数功能:开页 / 空态再探盘作废钩子假桩(#93 F1:模拟器假表没有段表 /
* 段头缓存可作废,空做 = 行为不变;补这件与真机 s_wave_ops 同形状七件) void */
static void sim_wave_enter(void) {
}
/* 录波通道假表(与真机 s_wave_ops 同形状七件) */
static const ui_data_wave_ops_t s_wave_ops = {
sim_wave_manual_req,
sim_wave_enter,
sim_wave_seg_first,
sim_wave_seg_prev,
sim_wave_seg_next,