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:
@@ -65,6 +65,22 @@ static volatile uint32_t s_led_mask;
|
||||
/* TX DMA 占用标志:中断侧置位、TxCplt/错误中断清零,任务侧读 */
|
||||
static volatile uint8_t s_tx_busy;
|
||||
|
||||
/*
|
||||
* 按键召唤延迟应答(2026-10-30 按键延迟整治)。三个标志跨三个上下文:
|
||||
* s_key_defer 1 = 按键召唤被扣住待发(IDLE 中断挂上,TIM3 节拍放行时清)
|
||||
* s_key_defer_ms 已扣毫秒数(TIM3 节拍独占读写,单写单读)
|
||||
* s_key_rendered 任务侧凭据(sb_link_key_rendered 置 1、TIM3 节拍放行时清):
|
||||
* ui_task 在本拍出过键、重绘过、且 frame_update 返回了 OK——
|
||||
* 此刻帧缓冲的内容就是"按键之后"的画面,值得为它扣着不发。
|
||||
* Cortex-M4 的字节访问原子,标志竞态的后果核过全部是良性的(最坏晚 1 ms 放行,
|
||||
* 或多扣到超时发旧帧),不存在撕裂帧或叠发的分支。起发本身另有原子闸(见
|
||||
* sb_tx_kick:扣件放行与 IDLE 直答从此是**两个中断源**,判 busy→占位必须连续)。
|
||||
* 窗口上限的账在 sb_link.h 的 SB_KEY_RESP_MAX_MS(140 + 60 < 205 屏板放弃线)。
|
||||
*/
|
||||
static volatile uint8_t s_key_defer;
|
||||
static volatile uint8_t s_key_defer_ms;
|
||||
static volatile uint8_t s_key_rendered;
|
||||
|
||||
/*
|
||||
* 按键交接:中断侧单写 s_key_head、任务侧单读 s_key_tail。
|
||||
* 两个下标都只由各自的写者推进,所以不需要临界区(单生产者单消费者环形队列)。
|
||||
@@ -83,6 +99,7 @@ static uint32_t s_report_err_irq;
|
||||
static uint32_t s_report_rearm_fail;
|
||||
static uint32_t s_report_tx_kick;
|
||||
static uint32_t s_report_frame_build;
|
||||
static uint32_t s_report_defer; /* dfok+dfold 合并基线:扣件放行也触发上报 */
|
||||
static uint8_t s_first_frame_logged;
|
||||
|
||||
/* 离线判定:上次看到"有效召唤帧"的时刻与当时的计数基线。
|
||||
@@ -152,23 +169,41 @@ static void sb_key_push(uint8_t key)
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:点一次 TX DMA 把当前帧发出去
|
||||
* 函数功能:点一次 TX DMA 把当前帧发出去(带原子闸,起发的唯一通道)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:**中断上下文**;调用前须已确认 s_frame_ready 且 !s_tx_busy
|
||||
* 返回值:1 = 已起发;0 = 上一帧还在发 / 还没有帧,什么都没做
|
||||
* 限定条件:**中断上下文**(IDLE 直答与 TIM3 放行两个来源都走这里)
|
||||
* 函数说明:起发失败只计数不清帧——下一轮召唤还会再点一次。busy 由 TxCplt 清,
|
||||
* 不由本函数清(清了就等于允许叠发,屏板会把两帧粘成一帧撕裂)。
|
||||
* 不由本函数清(清了就等于允许叠发,屏板会把两帧粘成一帧撕裂);
|
||||
* 唯一的例外是 HAL 明确拒绝的本次调用(根本没上线,占位必须退回)。
|
||||
* ⚠ "判空闲→占位"必须在关中断里连着做完:本函数从 2026-10-30 起有
|
||||
* 两个中断来源(USART1 的 IDLE 直答、TIM3 节拍的延迟应答放行),两者
|
||||
* 抢占关系不由本模块定;各让一步的交错就是"双双看到 !busy → 各自
|
||||
* Transmit_DMA"——第二次的失败路径会把第一次的占位误清成空闲,下一拍
|
||||
* 召唤即可在线上中段叠帧。临界区只圈两个标志位(亚 µs),HAL 不进。
|
||||
*/
|
||||
static void sb_tx_kick(void)
|
||||
static uint8_t sb_tx_kick(void)
|
||||
{
|
||||
uint32_t primask;
|
||||
|
||||
primask = __get_PRIMASK();
|
||||
__disable_irq();
|
||||
if ((s_tx_busy != 0U) || (s_frame_ready == 0U))
|
||||
{
|
||||
__set_PRIMASK(primask);
|
||||
return 0U;
|
||||
}
|
||||
s_tx_busy = 1U;
|
||||
__set_PRIMASK(primask);
|
||||
|
||||
if (HAL_UART_Transmit_DMA(s_huart, s_frame, (uint16_t)SB_LINK_FRAME_LEN) != HAL_OK)
|
||||
{
|
||||
s_tx_busy = 0U;
|
||||
s_stats.tx_kick_fail++;
|
||||
return;
|
||||
return 0U;
|
||||
}
|
||||
s_stats.tx_kick++;
|
||||
return 1U;
|
||||
}
|
||||
|
||||
/* ======================== 公开函数实现 ======================== */
|
||||
@@ -197,11 +232,15 @@ bsp_err_t sb_link_init(UART_HandleTypeDef *p_huart)
|
||||
s_report_rearm_fail = 0U;
|
||||
s_report_tx_kick = 0U;
|
||||
s_report_frame_build = 0U;
|
||||
s_report_defer = 0U;
|
||||
s_first_frame_logged = 0U;
|
||||
|
||||
s_frame_ready = 0U;
|
||||
s_led_mask = 0U;
|
||||
s_tx_busy = 0U;
|
||||
s_key_defer = 0U;
|
||||
s_key_defer_ms = 0U;
|
||||
s_key_rendered = 0U;
|
||||
s_key_head = 0U;
|
||||
s_key_tail = 0U;
|
||||
s_offline_base_ms = 0U;
|
||||
@@ -301,19 +340,31 @@ void sb_link_on_rx_event(UART_HandleTypeDef *p_huart, uint16_t size)
|
||||
|
||||
/* 起发就在这儿,不在任务里轮询。一召唤 → 一答 4208 B,绝不叠发。
|
||||
* 上一帧还没发完(屏板自己超时重问过)就丢这一轮:多发一帧会让屏板把
|
||||
* 两帧粘成一帧撕裂,而且它接收溢出后会永久收不到东西(见 §4.4 的警示)。 */
|
||||
if (s_frame_ready == 0U)
|
||||
* 两帧粘成一帧撕裂,而且它接收溢出后会永久收不到东西(见 §4.4 的警示)。
|
||||
* 2026-10-30 按键延迟整治:按键召唤不再立刻起发——此刻帧里还是按键**之前**的
|
||||
* 画面,发出去等于把这次按键的反馈推给下一次召唤(平均多等 ~140 ms)。
|
||||
* 改为扣住本答,等任务侧把键画进画面并组好帧(sb_link_key_rendered 交
|
||||
* 凭据)后由 TIM3 节拍放行;最迟 SB_KEY_RESP_MAX_MS 发旧帧保底,
|
||||
* "一召一答"不破(欠发对屏板是安全的,它只会溢出超发,重问即恢复)。 */
|
||||
if ((frame.cmd == SB_CMD_KEY) && (frame.key != 0U))
|
||||
{
|
||||
s_stats.tx_busy_drop++; /* 帧还没组出来(刚上电)——与"上一帧未发完"同类记 */
|
||||
if ((s_frame_ready == 0U) || (s_tx_busy != 0U))
|
||||
{
|
||||
/* 没帧可扣、线又被占:同旧丢单,屏板 ≥205 ms 后带键重问 */
|
||||
s_stats.tx_busy_drop++;
|
||||
}
|
||||
else
|
||||
{
|
||||
s_key_defer_ms = 0U;
|
||||
s_key_rendered = 0U; /* 上一拍的重绘凭据不算数,本次重新交 */
|
||||
s_key_defer = 1U;
|
||||
}
|
||||
}
|
||||
else if (s_tx_busy != 0U)
|
||||
else if (sb_tx_kick() == 0U)
|
||||
{
|
||||
/* 周期召唤(及 key==0 的空键召唤):照旧立刻答,闸门拒了就丢单 */
|
||||
s_stats.tx_busy_drop++;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb_tx_kick();
|
||||
}
|
||||
}
|
||||
else if (ret == BSP_ERR_CRC)
|
||||
{
|
||||
@@ -451,7 +502,8 @@ void sb_link_poll(uint32_t now_ms)
|
||||
(st.err_irq != s_report_err_irq) ||
|
||||
(st.rearm_fail != s_report_rearm_fail));
|
||||
tx_changed = (uint8_t)((st.tx_kick != s_report_tx_kick) ||
|
||||
(st.frame_build != s_report_frame_build));
|
||||
(st.frame_build != s_report_frame_build) ||
|
||||
((st.defer_fresh + st.defer_old) != s_report_defer));
|
||||
if ((rx_changed == 0U) && (tx_changed == 0U))
|
||||
{
|
||||
return;
|
||||
@@ -475,13 +527,15 @@ void sb_link_poll(uint32_t now_ms)
|
||||
{
|
||||
s_report_tx_kick = st.tx_kick;
|
||||
s_report_frame_build = st.frame_build;
|
||||
DBG_INFO("tx=%lu done=%lu drop=%lu kerr=%lu abor=%lu fb=%lu bad=%lu us=%lu max=%lu kq=%lu off=%u",
|
||||
s_report_defer = st.defer_fresh + st.defer_old;
|
||||
DBG_INFO("tx=%lu done=%lu drop=%lu kerr=%lu abor=%lu fb=%lu bad=%lu us=%lu max=%lu kq=%lu off=%u dfok=%lu dfold=%lu",
|
||||
(unsigned long)st.tx_kick, (unsigned long)st.tx_done,
|
||||
(unsigned long)st.tx_busy_drop, (unsigned long)st.tx_kick_fail,
|
||||
(unsigned long)st.tx_abort, (unsigned long)st.frame_build,
|
||||
(unsigned long)st.frame_bad, (unsigned long)st.frame_us,
|
||||
(unsigned long)st.frame_us_max, (unsigned long)st.key_drop,
|
||||
(unsigned)st.offline);
|
||||
(unsigned)st.offline,
|
||||
(unsigned long)st.defer_fresh, (unsigned long)st.defer_old);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,6 +647,77 @@ void sb_link_on_tx_cplt(UART_HandleTypeDef *p_huart)
|
||||
s_stats.tx_done++;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:任务侧向延迟应答交"键已画进画面、帧内容 == 活显存"的凭据
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:任务上下文(uiTask);仅当本拍 sb_link_frame_update() 返回过 BSP_OK
|
||||
* 之后才准调
|
||||
* 函数说明:这是延迟应答的放行钥匙。调用方须保证本拍已经走完"注键 → LVGL 重绘 →
|
||||
* 组帧",且组帧没有 BUSY——此刻帧缓冲里的就是按键之后的画面,
|
||||
* 凭这个放行不会发出"键没画上去"的旧帧。BUSY 的拍次不许交凭据
|
||||
* (画面没进帧),扣住的召唤自然等下一个 OK 拍或按上限发旧帧。
|
||||
* 凭据只对当前这次扣件有效:新的按键召唤挂起时会先清掉旧凭据重交
|
||||
* (见 sb_link_on_rx_event),连续按键不会用上一次的重绘冒充。
|
||||
*/
|
||||
void sb_link_key_rendered(void)
|
||||
{
|
||||
s_key_rendered = 1U;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按键召唤延迟应答的毫秒时钟(放行 / 保底 / 计时)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:中断上下文(TIM3 1 kHz 分支调用,见 Src/main.c 的 USER CODE Callback 1);
|
||||
* 不得调内核 API、不得打日志
|
||||
* 函数说明:有扣件时每拍按优先级判两件事——
|
||||
* 1. 有凭据 && 线空闲 → 起发新帧(dfok +1);若线反而忙(极小竞态:
|
||||
* IDLE 中断抢先把上一轮发了)则不算放行,落去第 2 步继续计时、
|
||||
* 下一拍重试——线忙窗口 ≤59.6 ms,重试必然追得上;
|
||||
* 2. 扣件时长到 SB_KEY_RESP_MAX_MS 上限 → 不论新旧把当前帧保底发出
|
||||
* (dfold +1;闸门再拒就按丢单记,屏板重问即恢复)。
|
||||
* dfold 在涨 = uiTask 被饿到 ≥140 ms 的直接证据(对时突发那类问题)。
|
||||
* 初始化之前调用安全:s_huart 为空直接返回——TIM3 比 sb_link_init 早
|
||||
* ≈100 ms 就在跑(内核启动前节拍,见 docs/架构设计/项目启动顺序管理.md)。
|
||||
*/
|
||||
void sb_link_ms_tick(void)
|
||||
{
|
||||
if ((s_huart == NULL) || (s_key_defer == 0U))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ((s_key_rendered != 0U) && (s_tx_busy == 0U))
|
||||
{
|
||||
if (sb_tx_kick() != 0U)
|
||||
{
|
||||
s_key_defer = 0U;
|
||||
s_key_rendered = 0U;
|
||||
s_stats.defer_fresh++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (s_key_defer_ms < 255U)
|
||||
{
|
||||
s_key_defer_ms++;
|
||||
}
|
||||
if (s_key_defer_ms >= SB_KEY_RESP_MAX_MS)
|
||||
{
|
||||
s_key_defer = 0U;
|
||||
s_key_rendered = 0U;
|
||||
if (sb_tx_kick() != 0U)
|
||||
{
|
||||
s_stats.defer_old++;
|
||||
}
|
||||
else
|
||||
{
|
||||
s_stats.tx_busy_drop++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取一份链路统计快照
|
||||
* 入口参数:p_out - 输出缓冲(非空)
|
||||
@@ -631,5 +756,7 @@ void sb_link_get_stats(sb_link_stats_t *p_out)
|
||||
p_out->frame_us = s_stats.frame_us;
|
||||
p_out->frame_us_max = s_stats.frame_us_max;
|
||||
p_out->key_drop = s_stats.key_drop;
|
||||
p_out->defer_fresh = s_stats.defer_fresh;
|
||||
p_out->defer_old = s_stats.defer_old;
|
||||
p_out->offline = s_stats.offline;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
* - 收:IDLE 空闲中断断帧、6 B 帧 A 校验、错误中断里清 ORE 并重挂接收;
|
||||
* - 发:任务侧组一帧 B(4208 B)进静态缓冲,中断侧收到召唤帧**直接点 TX DMA**,
|
||||
* TxCplt 里收尾(不排队、不攒批、不在任务里逐字节轮询);
|
||||
* 例外(2026-10-30 按键延迟整治):**按键召唤延迟应答**——key≠0 的
|
||||
* 召唤到达时当前帧必然还是"按键前"的内容(键还在中断队列里没被画),
|
||||
* 立刻发出去等于让屏板白等一轮;改为扣住这一答,等新帧组好(任务侧
|
||||
* 同拍完成)再由 1 kHz 节拍钩子在中断里放行,超时则发旧帧保底
|
||||
* - 按键上行:中断只入队,注键由任务侧取走(**注键不能在中断里做**);
|
||||
* - 链路监视:连续 >=10 s 无召唤帧才判离线;CRC 错帧静默丢弃并计数。
|
||||
* 协议字节级编解码(CRC / 帧 A 解析 / 帧 B 组帧)在 sb_proto,本层只做搬运与时序。
|
||||
@@ -13,6 +17,11 @@
|
||||
* 修改记录:
|
||||
* 2026-10-29 王建锋 创建初始版本(700 kbaud + 收中断)
|
||||
* 2026-10-29 王建锋 增加:帧 B 组帧入口、中断起发、TxCplt 收尾、按键队列、离线判定
|
||||
* 2026-10-30 王建锋 按键延迟整治:按键召唤**延迟应答**——收到
|
||||
* key≠0 的帧不再立刻把"按键前的旧帧"发走,而是扣住等
|
||||
* 任务侧把按键画进新帧(上限 SB_KEY_RESP_MAX_MS),
|
||||
* 一召唤一答与"起发必须在 IDLE 中断"两条约束都不变;
|
||||
* sb_link_ms_tick() 挂在 1 kHz 采样节拍上驱动扣件超时
|
||||
*
|
||||
* 为什么单独一层、不并进 sb_proto:
|
||||
* sb_proto 是纯逻辑(CRC / 组帧 / 解析),能在 test/host 里原样 include 跑单测;
|
||||
@@ -86,6 +95,23 @@ extern "C" {
|
||||
* 不必知道协议层的命名;两者恒等由下面一行保证。 */
|
||||
#define SB_LINK_FRAME_LEN SB_FRAME_B_LEN
|
||||
|
||||
/*
|
||||
* 按键召唤延迟应答的上限(ms,2026-10-30 按键延迟整治的核心参数)。
|
||||
*
|
||||
* 为什么要有这个窗:按键召唤到达的那一刻,键还躺在中断队列里没被画——立刻应答
|
||||
* 发出去的是**按键前**的旧帧,屏板白等一轮,新画面要等下一次召唤(平均 ~140 ms、
|
||||
* 最坏 280 ms)才带得出来。现在把按键召唤扣住:任务侧同拍完成"出键→即派即绘→
|
||||
* 组帧"后交回凭据(sb_link_key_rendered),1 kHz 节拍钩子 ≤1 ms 内放行新帧。
|
||||
* 实测路径预算:召唤落点→下一拍 ≤10 + 渲染地板 ≤62 + 组帧 ≤1 ≈ **73 ms**,
|
||||
* 留了将近一倍余量给"召唤恰好落在数据拍/重绘中段"这类不利相位。
|
||||
*
|
||||
* 为什么是 140 而不是更大:扣住的窗从召唤时刻起算,到点必须发(旧帧)保"一召唤
|
||||
* 一答";140 + 线上 59.6 ≈ 199.6 < **205**(屏板放弃计时——它带键重问的最早时刻),
|
||||
* 超时答案也赶在下一次召唤之前发完,不与任何重问对撞。再大就要撞上 205 线。
|
||||
* 出处:docs/子系统设计/屏板链路设计说明.md §7.1(205/280 两线)与本函数说明。
|
||||
*/
|
||||
#define SB_KEY_RESP_MAX_MS (140U)
|
||||
|
||||
/* ============================================================================
|
||||
* 二、链路统计
|
||||
* ==========================================================================*/
|
||||
@@ -126,6 +152,10 @@ typedef struct
|
||||
|
||||
/* ---- 按键上行与链路监视 ---- */
|
||||
uint32_t key_drop; /* 按键队列满导致的丢弃次数 */
|
||||
/* ---- 2026-10-30 按键延迟整治:延迟应答的两类放行 ---- */
|
||||
uint32_t defer_fresh; /* 等到新画面后放行(理想路径;正常按键应几乎全落这里) */
|
||||
uint32_t defer_old; /* 扣到 SB_KEY_RESP_MAX_MS 仍未见新画面 → 发旧帧保底
|
||||
* (uiTask 被饿住的直接证据;持续非 0 要先查任务卡点) */
|
||||
uint8_t offline; /* 1 = 连续 >= SB_LINK_OFFLINE_MS 无有效召唤帧 */
|
||||
} sb_link_stats_t;
|
||||
|
||||
@@ -179,9 +209,13 @@ bsp_err_t sb_link_receive_start(void);
|
||||
* size - 本次断出来的字节数
|
||||
* 返回值:无
|
||||
* 限定条件:**中断上下文**,只做 CRC 与计数(4 B 校验约 1 µs),不得打日志、不得阻塞。
|
||||
* 函数说明:长度不是 6 就直接记 rx_len_err 并丢弃;是 6 才解析。解析通过后**直接点 TX DMA**
|
||||
* 把已经组好的帧 B 发出去(起发必须在中断里,不能在任务里轮询——屏板按
|
||||
* "总线空闲 1 个字符"断帧,一次几十 µs 的任务抢占就能把帧切断,而屏板不报超时)。
|
||||
* 函数说明:长度不是 6 就直接记 rx_len_err 并丢弃;是 6 才解析。周期召唤解析通过后
|
||||
* **直接点 TX DMA** 把已经组好的帧 B 发出去(起发必须在中断里,
|
||||
* 不能在任务里轮询——屏板按"总线空闲 1 个字符"断帧,一次几十 µs 的任务
|
||||
* 抢占就能把帧切断,而屏板不报超时)。**按键召唤(cmd=KEY 且 key≠0)
|
||||
* 例外:只入队、扣住不应答**(延迟应答,见 SB_KEY_RESP_MAX_MS 的账),
|
||||
* 由 sb_link_ms_tick() 在新帧就绪或超时时放行——这一答必须走同一个
|
||||
* 中断起发通道,绝不因为"等画面"而退化成任务态发送。
|
||||
* 业务动作仍然不做:按键只入队,注键由任务侧 sb_link_key_take() 取走后做。
|
||||
* 末尾无条件重挂接收。
|
||||
*/
|
||||
@@ -245,6 +279,26 @@ bsp_err_t sb_link_frame_update(const uint8_t *p_live_bmp, uint32_t led_mask);
|
||||
*/
|
||||
uint8_t sb_link_key_take(void);
|
||||
|
||||
/*
|
||||
* 函数功能:交"按键已画进画面、帧与活显存一致"的放行凭据(延迟应答用)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:任务上下文(uiTask);本拍 sb_link_frame_update() 返回 BSP_OK 之后才准调
|
||||
* 函数说明:按键召唤不再立刻应答(见 sb_link_on_rx_event),TIM3 的 sb_link_ms_tick()
|
||||
* 看到本凭据才把"带新画面的那一答"放上线。BUSY 拍次不许交(画面还没进帧)。
|
||||
*/
|
||||
void sb_link_key_rendered(void);
|
||||
|
||||
/*
|
||||
* 函数功能:按键召唤延迟应答的毫秒时钟(1 kHz 调用)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:**中断上下文**(Src/main.c TIM3 分支),不得调内核 API / 打日志
|
||||
* 函数说明:有扣件时——凭据已到就起发新帧(dfok+1),扣满 SB_KEY_RESP_MAX_MS
|
||||
* 则发当前帧保底(dfold+1)。初始化前调用安全(直接返回)。
|
||||
*/
|
||||
void sb_link_ms_tick(void);
|
||||
|
||||
/*
|
||||
* 函数功能:TX 完成入口(由 HAL_UART_TxCpltCallback 转发进来)
|
||||
* 入口参数:p_huart - 触发回调的句柄(非本链路的会被直接忽略)
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user