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
+43 -4
View File
@@ -20,6 +20,9 @@
* s_depth 决定,键回调 user_data=depth 冗余);根框装饰
* (浅 V)+状态行改容器自绘(删 3 lv_line + 1 label 对象)
* 2026-09-09 王建锋 "浅 V"装饰几何提取至共享 ui_deco 模块(不再本地重复定义)
* 2026-10-30 王建锋 按键提速:view_apply_sel 选中移动且未滚动时
* 只失效新旧两行竖带(lv_obj_invalidate_area),绘制量
* 从整框降到 ~2/7;滚动/首建仍整框兜底
*/
/* 头文件包含区 */
@@ -320,14 +323,50 @@ static void view_scroll_to_sel(lv_obj_t *box, const menu_frame_t *f) {
* 限定条件:该层框体已建
* 函数说明:1. 行不是对象:选中只是数据,自绘回调绘制时定反色带/文字色
* 2. 选中行滚入可见区(手动计算,无行对象可供 scroll_to_view)
* 3. 失效框体:下一帧重绘
* 3. 失效范围按改动面收敛(2026-10-30 按键提速 C):
* · 选中变了且**没发生滚动** → 只有新旧两行外观变化,
* 只脏这两行组成的竖带(根框 107 px 高、行高 15 px,绘制量
* 从整框降到 ~2/7;自绘回调本就有 clip 剔除,带外行直接跳过)
* · 发生滚动 / 首次应用(新旧同号)→ 整框失效兜底
* 4. 失效区用绝对坐标,与自绘回调同一坐标框架
* (content_coords 起算、减滚动偏移)
*/
static void view_apply_sel(uint8_t depth, uint8_t sel) {
menu_frame_t *f = &s_frame[depth];
menu_frame_t *f = &s_frame[depth];
lv_obj_t *box = s_frame_box[depth];
uint8_t sel_old = f->sel;
int32_t sy_before = lv_obj_get_scroll_y(box);
f->sel = sel;
view_scroll_to_sel(s_frame_box[depth], f); /* 选中行必在可见区内 */
lv_obj_invalidate(s_frame_box[depth]); /* 重绘框体(反色带/文字色绘制时决定) */
view_scroll_to_sel(box, f); /* 选中行必在可见区内 */
if ((sel_old != sel) && (lv_obj_get_scroll_y(box) == sy_before)) {
lv_area_t ca;
lv_area_t band;
int32_t y_old;
int32_t y_new;
lv_obj_get_content_coords(box, &ca);
y_old = ca.y1 + (int32_t)sel_old * MENU_ROW_H - sy_before;
y_new = ca.y1 + (int32_t)sel * MENU_ROW_H - sy_before;
band.x1 = ca.x1;
band.x2 = ca.x2;
band.y1 = (y_old < y_new) ? y_old : y_new;
band.y2 = (y_old > y_new) ? y_old : y_new;
band.y2 += (MENU_ROW_H - 1);
if (band.y1 < ca.y1) {
band.y1 = ca.y1; /* 旧选中行可能被框顶裁掉(级联框贴屏上沿时) */
}
if (band.y2 > ca.y2) {
band.y2 = ca.y2; /* 同理框底 */
}
if (band.y1 <= band.y2) {
lv_obj_invalidate_area(box, &band);
return;
}
}
lv_obj_invalidate(box); /* 兜底:有滚动或首次建立,整框重绘 */
}
/*
+63 -11
View File
@@ -6,6 +6,9 @@
* 创建日期:2026-08-29
* 修改记录:
* 2026-08-29 王建锋 创建初始版本
* 2026-10-30 王建锋 按键提速:单槽键缓存改环形队列(连按不再
* 互相覆盖丢键);新增 ui_port_service_keys()——注键后
* 立即派发并重绘,不等下一个 20 ms 输入轮询/刷新定时器
*/
#include "ui_port.h"
@@ -22,8 +25,20 @@ static uint8_t g_ui_fbuf[UI_FB_SIZE];
* ui_port.h 中 UI_DRAW_BUF_PALETTE / UI_DRAW_BUF_SIZE 注释(含越界与整帧错位的历史成因)。 */
static uint8_t g_ui_draw_buf[UI_DRAW_BUF_SIZE];
/* 单键缓冲:菜单无需多键队列,仅保存最近一次待处理键 */
static volatile uint8_t g_pending_key = 0;
/* 待处理键环形队列(2026-10-30 按键提速 A):旧版是单槽 g_pending_key,
* 一拍的注入窗口里来两个键(屏板连按 + 7003 远端注键同拍到达)后者就静默
* 覆盖前者——用户侧的表象是"按了没反应/要按两下"。环形后深度 = LEN-1 = 7。
* 生产与消费都在 UI 主循环任务上下文(uiTask / 模拟器主线程),同上下文
* 单写者轮转,volatile 只防编译器重排,不需要临界区。满时**挤掉最旧**:
* 用户最新一次按键的意图必须保住(旧键没被看见已是损失,再丢新的等于连点不回)。 */
#define UI_KEY_RING_LEN 8u
static volatile uint8_t g_key_ring[UI_KEY_RING_LEN];
static volatile uint8_t g_key_head; /* 生产者(queue_key)推进 */
static volatile uint8_t g_key_tail; /* 消费者(keypad read)推进 */
static volatile uint8_t g_key_drop; /* 满挤旧计数(排障用,不上报) */
/* keypad 设备句柄(ui_port_service_keys 要直接驱动一次输入读取) */
static lv_indev_t *g_indev;
/* 远端键 -> LVGL 键 */
/*
@@ -52,15 +67,16 @@ static uint8_t ui_map_key(uint8_t remo_key) {
* data - 读取结果 lv_indev_data_t * 由本函数填充
* 返回值:无
* 限定条件:无
* 函数说明:1. 若有缓存键则上报 PRESSED 并清除缓存
* 2. 否则上报 RELEASED,等待下次按键
* 函数说明:1. 环形队列非空 → 弹出一个键上报 PRESSED(一次读一个键,
* 连按键由后续轮询逐个派发,与 LVGL keypad 的按次派发语义一致)
* 2. 队列空则上报 RELEASED,等待下次按键
*/
static void ui_keypad_read(lv_indev_t * indev, lv_indev_data_t * data) {
(void)indev;
if (g_pending_key != 0) {
if (g_key_head != g_key_tail) {
data->state = LV_INDEV_STATE_PRESSED;
data->key = g_pending_key;
g_pending_key = 0;
data->key = g_key_ring[g_key_tail];
g_key_tail = (uint8_t)((g_key_tail + 1u) % UI_KEY_RING_LEN);
} else {
data->state = LV_INDEV_STATE_RELEASED;
data->key = 0;
@@ -131,6 +147,7 @@ int ui_port_init(void) {
indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_KEYPAD);
lv_indev_set_read_cb(indev, ui_keypad_read);
g_indev = indev; /* ui_port_service_keys 直接驱动一次读取用 */
lv_group_t *g = lv_group_create();
if (g == NULL) {
@@ -157,14 +174,24 @@ uint8_t *ui_port_get_framebuffer(void) {
* 函数功能:向输入队列喂入一个远端按键值
* 入口参数:remo_key - 远端键码 uint8_t UI_KEY_* 取值之一
* 返回值:无
* 限定条件:无
* 函数说明:映射后缓存,由 keypad 回调在下次轮询上报
* 限定条件:与 keypad 读取同一任务上下文(uiTask / 模拟器主线程)
* 函数说明:映射后入环形队列;队列满挤掉**最旧**一个并计数——最新按键的
* 意图必须保住(旧版单槽直接覆盖丢键)。由 keypad 回调逐个上报。
*/
void ui_port_queue_key(uint8_t remo_key) {
uint8_t k = ui_map_key(remo_key);
if (k != 0) {
g_pending_key = k;
uint8_t next;
if (k == 0) {
return;
}
next = (uint8_t)((g_key_head + 1u) % UI_KEY_RING_LEN);
if (next == g_key_tail) {
g_key_tail = (uint8_t)((g_key_tail + 1u) % UI_KEY_RING_LEN);
g_key_drop++;
}
g_key_ring[g_key_head] = k;
g_key_head = next;
}
/*
@@ -178,3 +205,28 @@ void ui_port_step(uint32_t tick_ms) {
lv_tick_inc(tick_ms);
lv_timer_handler();
}
/*
* 函数功能:立即消费队列中的待处理按键并让改动落屏(按键提速,2026-10-30)
* 入口参数:无
* 返回值:无
* 限定条件:ui_port_init() 已成功调用;与 ui_port_step 同一任务上下文
* 函数说明:1. 队列空 = 直接返回(常态:绝大多数拍没有键,不触发输入读取)
* 2. lv_indev_read 把队头键以 PRESSED 派发(键事件直达 menu_key_cb
* 一类的处理器,处理器只 invalidate 脏区),lv_refr_now 立刻把
* 脏区绘进帧缓冲——"注键→派发→重绘"在同一拍完成
* 3. 省掉的两段等待:indev 轮询定时器 ≤20 ms + 刷新定时器 ≤20 ms;
* 真机上这两段原本压在 sb_link 延迟应答的截止预算里
* 4. 只服务队头一个键:LVGL keypad 的按次派发语义(PRESSED 一次一键),
* 连按剩余的键由本函数下一次调用或周期轮询继续消化
*/
void ui_port_service_keys(void) {
if (g_indev == NULL) {
return;
}
if (g_key_head == g_key_tail) {
return;
}
lv_indev_read(g_indev);
lv_refr_now(NULL);
}
+13
View File
@@ -91,6 +91,19 @@ void ui_port_queue_key(uint8_t key_val);
*/
void ui_port_step(uint32_t tick_ms);
/*
* 函数功能:立即消费队列中的待处理按键并让改动落屏(按键提速,2026-10-30)
* 入口参数:无
* 返回值:无
* 限定条件:ui_port_init() 已成功调用;与 ui_port_step 同一任务上下文
* 函数说明:1. 队列空 = 空操作(不触发输入读取)
* 2. 非空则立刻 lv_indev_read 派发键事件 + lv_refr_now 把脏区绘进
* 帧缓冲——调用方据此可在**同一拍**完成"注键→派发→重绘",
* 不等下一个 20 ms 输入轮询 / 刷新定时器(按键反馈提速 A 项)
* 3. 只消费队列头部一个键;连按时其余按键仍由周期轮询逐个派发
*/
void ui_port_service_keys(void);
#ifdef __cplusplus
}
#endif