fix(ui/screenlink): 按键应答扣到按键后画面,反馈典型 250-450 ms 降至 70-135 ms

按键召唤答的是按键前的旧帧:uiTask 还没消化键答案就发走了,新画面
只能等下一次周期召唤(280 ms/轮)才带得出——真机反馈 250-450 ms 的
主构成(根因账本 docs/子系统设计/屏板链路设计说明.md §7.1)。

改动(用户批准范围 A+B+C;屏板固件未动,决策三不变):
- sb_link:按键召唤改为只入队不起发;uiTask 同拍重绘并组帧成功后交
  凭据(sb_link_key_rendered),TIM3 1 kHz 的 sb_link_ms_tick 以 ≤1 ms
  放行新帧。140 ms 截止(SB_KEY_RESP_MAX_MS,+59.6 ms 线上 <205 ms
  屏板放弃线)保底发旧帧,"一召一答"契约不破;线忙时同旧丢单、
  不留扣件。起发两中断源(USART1 IDLE/TIM3)共用 sb_tx_kick,
  "判忙→占位"在 PRIMASK 临界区原子完成。
- ui_task:排空注键挪到组帧之前,注键→即派即绘→组帧同拍闭环;
  组帧返回 BSP_OK 才交重绘凭据(BUSY 拍帧还没反映按键)。
- ui_port:注键单槽改 8 深环(连按不再互相覆盖,满丢最旧+计数);
  新增 ui_port_service_keys:同拍 lv_indev_read + lv_refr_now,
  省掉 indev ≤20 ms + 刷新 ≤20 ms 两段等待。
- ui_menu_view:菜单移动选中只失效新旧两行带(黄金参考零像素变化),
  滚动/异常回退整框失效。
- simulator:窗口循环 sleep 扣除本轮已耗时(原先无条件睡满
  lv_timer_handler 返回值,叠加 Windows Sleep 15.6 ms 粒度放大观感)。
- 串口 TX 上报新增 dfok/dfold(凭据放行新帧数/超时保底旧帧数):
  dfold 连涨 = uiTask 被饿 ≥140 ms 的直接现场证据。

验证:test/host/sb_link 164/164(新增 18 条:不立刻起发/凭据放行/
140 ms 截止保底/忙时不留扣件/陈旧凭据不放行等边界);菜单 8 状态 +
内容页 66 张黄金像素 PASS;Keil @build 0 错误 0 警告(两次);
BOM 99 文件无漂移、代码规范、doc-links 本次文件零问题、cppcheck PASS。
文档:屏板规划文档已被主干收编进设计说明,延迟账本落
docs/子系统设计/屏板链路设计说明.md §7.1,并同步 §4 口径、§8 接口、
§9.1 单测台账 146→164、§10 坑账本补条。

RAM 台账(分支不跑 --write,并行分支提交约定):本笔盖戳行
RW_IRAM1 94,336 B (72.0%) / RW_CCM 57,376 B (87.9%),较上一台账行
+4,400 B——为距上次台账构建以来分支累计差(含批 3 状态灯等未盖戳
提交),非全部本笔引入;本次自身增量按对象计为 ui_port.o +12 B
(8 深键环+丢计数+indev 指针)与 sb_link/o 数十字节级。
gate: only mem-budget/mem-numbers pending lead ledger backfill
(合并落主干后 Lead 统一 --write 回填受管区)。
This commit is contained in:
2026-10-30 09:31:29 +08:00
parent 36f6948544
commit 7708f12c10
13 changed files with 592 additions and 59 deletions
@@ -13,7 +13,8 @@
串口重启被注释掉),只能断电重上或等 ≈83 min 自复位——多发是唯一风险源,busy 冻结在主机侧挡死。
- **一份显存、两个出口**:串口(本链路)为主,TCP 7003 网络为镜像;两出口读**同一份活显存**
(`ui_port_get_framebuffer()`),无第二份账可漂。
- **宿主 = `uiTask`(组帧 + 注键),唯一起发者 = USART1 空闲中断(µs 级)**。不新建任务、不占堆、
- **宿主 = `uiTask`(组帧 + 注键),起发全部在中断 = USART1 空闲中断(周期召唤直答)+ TIM3 1 kHz 节拍
(按键召唤的延迟应答放行,§7.1)**。不新建任务、不占堆、
不加看门狗位(产品 `required_mask` 维持 `0x1F7`,起发在中断、与任务心跳无关)。
```mermaid
@@ -24,14 +25,17 @@ flowchart TB
F -->|TX busy → BSP_ERR_BUSY| R
F -->|变了 → 组帧 + 自校验| S["s_frame 就绪"]
K["sb_link_key_take 出队"] --> M["ui_task_board_key_map 映射"] --> Q["ui_port_queue_key 注键"]
Q --> SV["ui_port_service_keys 同拍立刻重绘"] --> F
P["sb_link_poll 统计 + 离线判定"]
end
subgraph ISRCTX["USART1 空闲中断(屏板召唤到达)"]
A["sb_link_on_rx_event:校 6 B 帧 A"] -->|"key ≠ 0"| KQ["按键环形队列(入队)"]
A -->|帧有效 且 s_frame 就绪| D["sb_tx_kick 点 TX DMA"]
subgraph ISRCTX["中断上下文(两个起发源)"]
A["sb_link_on_rx_event:校 6 B 帧 A"] -->|"key ≠ 0"| KQ["按键入队 + 扣住本答(不起发)"]
A -->|周期召唤 且 s_frame 就绪| D["sb_tx_kick 点 TX DMA"]
T["sb_link_ms_tick(TIM3 1 kHz)"] -->|"凭据→放新帧 / 扣满 140 ms→发旧帧"| D
A -->|无条件| A2["重挂 ReceiveToIdle_IT"]
end
S -.-> D
S -.|"组帧 OK 且本拍注过键 → 交凭据"| T
KQ -.-> K
D --> TC["DMA2_Stream7 传完中断"] --> C["清 busy(攒了 dirty → 下一拍补组)"]
```
@@ -120,6 +124,13 @@ flowchart TB
对时 relisten 突发实测单轮 800 ms 以上缺席),屏板只给 205/280 ms,任务态起发救不了。
- **按键上行**:中断校验帧 A 后 `key ≠ 0` 入环形队列(可存 3 个,满了丢**最**新的一帧并计数——旧键已在路上),
`uiTask` 出队 → 映射 → 注键;**注键不在中断里**。
- **按键召唤延迟应答**(2026-10-30,`SB_KEY_RESP_MAX_MS = 140`):`key ≠ 0` 的帧 A 不再在 IDLE 中断里直接点
DMA 起发——召唤到达那一刻 `uiTask` 还没消化这个键,此刻发出必然是"按键前"的画面,白发一帧把反馈推给下一轮
召唤。现在中断只入队**并扣住本答**;`uiTask` 同拍排空注键 + 同步重绘(`ui_port_service_keys()`)+ 组帧返回
`BSP_OK` 后交 `sb_link_key_rendered()` 凭据;TIM3 1 kHz 的 `sb_link_ms_tick()` 见凭据即起发**带键的新帧**。
扣满 140 ms 保底发旧帧(140 + 59.6 线上 ≈ 199.6 ms < 205 ms 屏板放弃线,「一召一答」不破);线忙则同旧丢单、
**不留扣件**(扣一个没人应答过的召唤 = 之后凭空多发一帧)。起发从此**两个中断源**(IDLE 直答 + TIM3 放行),
`sb_tx_kick()` 的"判忙→占位"在 PRIMASK 临界区原子完成——交错起发 = 线上中段叠帧。完整延迟账本见 §7.1。
## 5. 键值映射(帧 A `key` → `UI_KEY_*`)
@@ -167,19 +178,57 @@ flowchart TB
| 组帧耗时 | **509 µs 典型 / 812 µs 峰值,预算 ≤1 ms**;没变只付 memcmp ≈20 µs | 台架 `us=` / `max=` 字段 |
| 耗时结构 | ≈480 µs 全在 CRC:每帧算满 4206 B,168 MHz 查表 ≈19~20 周期/字节,512 B 表驻 Flash(5 等待周期);清零+拷贝只 ≈20 µs。**占空比无约束**:一帧 280 ms ≈0.2 %,3.5 帧/s ≈0.17 % CPU | 压降两条路已评估**未实施**:表搬 SRAM(≈ −125 µs,+512 B SRAM)、998 B 零段 GF(2) 推进变换(≈ −89 µs,需长期维护矩阵代码)——这个占空比换不来收益;另循环变量用 `uint32_t` 防 ARMCC 每轮多一条 `UXTH`(`sb_crc16`) |
### 7.1 按键反馈延迟:根因账本与 2026-10-30 修复(延迟应答)
**背景**:用户报告真机 LCD 与模拟器按键反馈都慢(按下→看到画面变化)。模拟器插桩实测
(SDL 事件→键回调→REFR_READY,40 键)内部路径 **avg 0.68 ms / max 1.2 ms**——窗口自身的
代码路径不是量级来源;若现场 SDL 窗口仍感觉慢,剩余候选是旧构建 / vsync 呈现 /
远端查看器(7003 路径),另行取证。**真机的慢是结构性的**,账本如下。
**修复前(2026-10-29 基线)的账**:键 → 屏板扫描(外部,ms 级)→ 帧 A 立刻发出(屏板按键不等
周期轮)→ 主机 IDLE 中断入队并**立刻起发当前帧**——可此刻 `uiTask` 还没消化这个键(10 ms
节拍 + 渲染地板 61.3 ms 占住,见 §7 表)→ **这一答发出去的是按键之前的画面(白发)**;
新内容只能随下一次召唤带出。三个结构性主因:①键召唤答旧帧;②`uiTask` FULL 整屏渲染地板
61.3 ms;③召唤周期 ~275-280 ms(带键最早 205 ms)。放大器④:远端查看器在连时
`ui_net_sink_poll()` 可阻塞到秒级(登记簿「远端推流阻塞 uiTask」专项,本轮只裁案不做)。
典型 **~250-450 ms**、最坏 **~500 ms**;④ 发作时秒级。
**修复后(2026-10-30,用户裁定三处全做)**:
| 面 | 改动 | 省掉 |
|---|---|---|
| **延迟应答(核心)** | IDLE 中断收按键召唤 → 只入队、**扣住本答不发**;`uiTask` 本拍排空注键 + `ui_port_service_keys()`(立刻 `lv_indev_read`+`lv_refr_now`,各省一个 ≤20 ms 的 LVGL 定时轮)+ 组帧 OK 后交 `sb_link_key_rendered()` 凭据;TIM3 1 kHz 的 `sb_link_ms_tick()` 见凭据即起发**带键的新帧** | 整轮"答旧帧"≈140-280 ms |
| 闸门 | 起发从此**两个中断源**(IDLE 直答 + TIM3 放行)→ `sb_tx_kick()` 内 PRIMASK 临界区原子占位;交错起发 = 叠发毁帧,闸必须有 | — |
| 截止 | `SB_KEY_RESP_MAX_MS = 140`:uiTask 饿死(对时 reconcile 突发那类)扣满即发旧帧保底——140 + 59.6 线上 ≈ 199.6 ms < 205 ms 屏板放弃线,「一召一答」不破;线忙则同旧丢单不留扣件 | 协议安全边界不动 |
| 注键环 | `ui_port` 注键从单槽改 **8 深环**(满丢最旧+计数)——连按不覆盖 | 连按丢键 |
| 渲染收窄 | `view_apply_sel()` 选中移动且滚动未变时只 invalidate **两行带**(旧选中行+新选中行),整框 invalidate 退为滚动/首刷兜底 | 选中移动类的整屏重绘(61.3 ms 地板页里的大头) |
| 模拟器 | 主循环 sleep 扣掉本轮实际耗时(旧代码在 `lv_refr_now` 前算 sleep_ms,每轮多睡一段工作量) | 每轮一段过冲 |
**修复后的账**:召唤 → 下一拍 ≤10 ms → 注键+重绘(收窄后选中移动为几 ms 级;页面地板仍
61.3 ms)→ 组帧 ~0.5 ms → 放行 ≤1 ms → 线上 59.6 ms → **典型 ~70-135 ms**(修复前
250-450 ms);uiTask 饿死场景 **~200 ms 保底发旧帧**(修复前同场景秒级且白发一帧)。
统计口径:TX 上报行新增 `dfok=`(新帧放行次数)与 `dfold=`(扣满 140 ms 发旧帧次数——
**dfold 在涨 = uiTask 被饿 ≥140 ms 的直接证据**,与登记簿「reconcile 突发饿死 uiTask」对拍)。
**没动的**:屏板固件(外部工程 DTU-LCD,用户裁定不改)、召唤节奏(280 ms 是屏板的,主机
单方面把"下一次召唤"的期望值提前不了)、7003 解耦(只留方案在登记簿)。主机单测
`TC-SBLINK-030~033` 钉住扣件/放行/截止/四边界(套件 164/164)。
## 8. 对外接口(单一出处在各头文件,这里只列形状)
| 接口 | 上下文 | 作用 |
|---|---|---|
| `sb_link_init(&huart1)` | 启动,`app_main.c` 步骤 2b | 交叉核对波特率(对不上 `BSP_ERR_STATE`)+ 挂首轮接收 + 使能 DWT |
| `sb_link_receive_start()` | 中断/任务 | 重挂 IDLE 接收(一次性 API,漏一次永久静默) |
| `sb_link_on_rx_event(huart,size)` | 中断(rs485.c 转发) | 帧 A 校验、计数、按键入队、**点 TX DMA 应答**、重挂 |
| `sb_link_on_rx_event(huart,size)` | 中断(rs485.c 转发) | 帧 A 校验、计数、按键入队、**周期召唤点 TX DMA 直答;按键召唤扣件延迟应答(§7.1)**、重挂 |
| `sb_link_on_error(huart)` | 中断(rs485.c 转发) | 按 SR→DR 清错、按 `RxState` 决定重挂 RX、按 `gState` 清被打断 TX 的残留 busy |
| `sb_link_on_tx_cplt(huart)` | 中断(rs485.c 转发) | 清 busy、计数;**不在中断里组帧** |
| `sb_link_frame_update(bmp,mask)` | uiTask | 按需组帧(memcmp 判变化),busy 返 `BSP_ERR_BUSY`,自校验不过不发 |
| `sb_link_key_take()` | uiTask | 出队屏板原始键号(0 = 空);映射归 uiTask |
| `sb_link_key_rendered()` | uiTask | 交"键已画进画面、帧==活显存"凭据(组帧 `BSP_OK` 拍才准交);延迟应答的放行钥匙 |
| `sb_link_ms_tick()` | 中断(TIM3 1 kHz,`Src/main.c` USER CODE) | 扣件的毫秒时钟:见凭据放新帧(`dfok`)、扣满 140 ms 发旧帧保底(`dfold`) |
| `sb_link_poll(now_ms)` | uiTask | 离线判定(每拍)+ 5 s 变化才打的统计行(RX/TX 两行自分频) |
| `sb_link_get_stats(&st)` | 任意上下文 | 计数快照(rx/ok/crc/len/fmt/keys/tx/done/drop/fb/bad/us/off…) |
| `sb_link_get_stats(&st)` | 任意上下文 | 计数快照(rx/ok/crc/len/fmt/keys/tx/done/drop/fb/bad/us/off/dfok/dfold…) |
| `sb_crc16` / `sb_frame_a_parse` / `sb_frame_b_build_image` / `sb_frame_b_verify` | 纯逻辑 | 协议层四件套,主机单测可回归 |
| `sb_frame_b_build_logo` | 纯逻辑 | **待删除**(LOGO 出局,勿新增调用,§9.2) |
@@ -189,7 +238,7 @@ flowchart TB
| 层级 | 证据(计数名 = 台架/真机日志 `[SBLINK]` 行字段) |
|---|---|
| 主机单测 | `test/host/sb_proto/` **59/59**(屏板实测向量、CRC 表与屏板查表逐字节交叉验证);`test/host/sb_link/` **146/146**(ORE 的 SR→DR 顺序在桩里真执行、非阻塞错误不得重挂、busy 期间帧缓冲逐字节冻结、帧 B 逐字段、周期→µs 换算、离线 9.999 s/10 s 边界与恢复、按键 FIFO 与溢出丢新);`test/host/sb_led_map/` **17/17** |
| 主机单测 | `test/host/sb_proto/` **59/59**(屏板实测向量、CRC 表与屏板查表逐字节交叉验证);`test/host/sb_link/` **164/164**(ORE 的 SR→DR 顺序在桩里真执行、非阻塞错误不得重挂、busy 期间帧缓冲逐字节冻结、帧 B 逐字段、周期→µs 换算、离线 9.999 s/10 s 边界与恢复、按键 FIFO 与溢出丢新;2026-10-30 延迟应答 +18:不立刻起发/凭据放行新帧/140 ms 截止保底旧帧/忙时不留扣件/陈旧凭据不放行/首帧未备同旧丢单);`test/host/sb_led_map/` **17/17** |
| 台架(PC + USB 转 TTL 顶替屏板) | `rx == 发送帧数`、`ok == rx`、`crc=0 len=0 size=6`;错波特率灌 64 B 垃圾后**自动恢复**(`rearm=0`,一次都没真失败);杜邦线噪声形态 `bits=0x02`(NE)每帧 ~1.3 次但数据零错;激励停止 10 s 准点 OFFLINE |
| 真屏板冒烟(2026-10-29) | 5 s ≈18 帧 → 278 ms/帧,与屏板 280 ms 节拍吻合;`tx==done`、一召一答无双发;**屏板出图(用户目视确认)**——屏板收帧只有"CRC 过 → 刷屏 / 不过 → 静默丢弃"两种结局且无报错通道,**出图 = 屏板亲手端到端验收了帧长 4208、CRC 覆盖 `[0,4205]`、低字节在前、1 bpp 20 B/行 MSB 在前的布局**,这是任何主机侧计数都给不出的一条证据 |
| 真屏板 10 min 连续(2026-10-29) | `rx=4258 ok=4258 crc=0 len=0 fmt=0 err=0 bits=0x00 rearm=0`(**连错误中断都没进过**);`tx=4249 done=4248`(`drop=9` 全在开机头两秒帧未就绪期,窗口内零新增);`keys=53 kq=0`(逐键必到、零丢弃);`fb=902 bad=0`(每帧自校验通过);组帧峰值 `us=687 max=812` < 1 ms 预算 |
@@ -224,3 +273,6 @@ flowchart TB
8. **`info[0..3]` 双解释**:先判 `info[4..5]` 魔数再解释这 4 字节(普通图 = LED 掩码、LOGO = 宽高),
顺序反了会把 LED 掩码当宽高。
9. **纯注释改动也会让体积凭证报"不同码"**:先重建、再 `ram_baseline.py --update`。
10. **按键召唤"立刻应答"发出去的必然是按键前的帧**(2026-10-30 前的默认行为):屏板按键召唤来得比
`uiTask` 消化键快一到两个数量级,"有答"不等于"有新画面"——排查"按了键屏不变化"先对 `dfok=`
计数,别顺着协议层找;延迟应答机制与账本见 §4 末条与 §7.1。