Files
STM32F4-Base/Drivers/BSP/UI/ui_port.c
T
wandering1 7708f12c10 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 回填受管区)。
2026-10-30 09:50:31 +08:00

233 lines
10 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:UI Display Port (LVGL 离屏显示端口)
* 模块功能:提供 LVGL 1bpp 离屏渲染端口与远程 keypad 输入设备
* 适用平台:STM32F407ZGTx
* 作者:王建锋
* 创建日期:2026-08-29
* 修改记录:
* 2026-08-29 王建锋 创建初始版本
* 2026-10-30 王建锋 按键提速:单槽键缓存改环形队列(连按不再
* 互相覆盖丢键);新增 ui_port_service_keys()——注键后
* 立即派发并重绘,不等下一个 20 ms 输入轮询/刷新定时器
*/
#include "ui_port.h"
#include <string.h>
#include "lvgl.h"
#define DBG_TAG "[UI_PORT]"
#include "dbg_log.h"
/* 160x160 1bpp 帧 = 3200 字节 */
static uint8_t g_ui_fbuf[UI_FB_SIZE];
/* 绘制缓冲:I1 索引色布局 = [palette 8B][pixel data],尺寸与取像素偏移的完整说明见
* ui_port.h 中 UI_DRAW_BUF_PALETTE / UI_DRAW_BUF_SIZE 注释(含越界与整帧错位的历史成因)。 */
static uint8_t g_ui_draw_buf[UI_DRAW_BUF_SIZE];
/* 待处理键环形队列(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 键 */
/*
* 函数功能:将远端键码映射为 LVGL 按键枚举值
* 入口参数:remo_key - 远端键码 uint8_t UI_KEY_* 取值之一
* 返回值:LVGL 按键值 uint8_t LV_KEY_*;未匹配返回 0
* 限定条件:无
* 函数说明:仅映射方向/确认/返回等导航键,其余返回 0 表示忽略
*/
static uint8_t ui_map_key(uint8_t remo_key) {
switch (remo_key) {
case UI_KEY_UP: return (uint8_t)LV_KEY_UP;
case UI_KEY_DOWN: return (uint8_t)LV_KEY_DOWN;
case UI_KEY_LEFT: return (uint8_t)LV_KEY_LEFT;
case UI_KEY_RIGHT: return (uint8_t)LV_KEY_RIGHT;
case UI_KEY_ENTER: return (uint8_t)LV_KEY_ENTER;
case UI_KEY_ESC: return (uint8_t)LV_KEY_ESC;
default: return 0;
}
}
/* keypad 读取回调,上报待处理键 */
/*
* 函数功能:keypad 输入设备读取回调,上报待处理按键
* 入口参数:indev - 输入设备句柄 lv_indev_t * 未使用
* data - 读取结果 lv_indev_data_t * 由本函数填充
* 返回值:无
* 限定条件:无
* 函数说明:1. 环形队列非空 → 弹出一个键上报 PRESSED(一次读一个键,
* 连按键由后续轮询逐个派发,与 LVGL keypad 的按次派发语义一致)
* 2. 队列空则上报 RELEASED,等待下次按键
*/
static void ui_keypad_read(lv_indev_t * indev, lv_indev_data_t * data) {
(void)indev;
if (g_key_head != g_key_tail) {
data->state = LV_INDEV_STATE_PRESSED;
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;
}
}
/* flush 回调:把 LVGL 1bpp 渲染结果拷到内存帧缓冲 */
/*
* 函数功能:显示刷新回调,将 LVGL 渲染结果写入内存帧缓冲
* 入口参数:disp - 显示设备 lv_display_t * 未使用
* area - 刷新区域 const lv_area_t * 未使用
* color_p - 1bpp 像素数据首地址 uint8_t *
* 返回值:无
* 限定条件:ui_port_init() 已成功调用
* 函数说明:整帧拷贝到 g_ui_fbuf,供 RemoDispBus 推送至远端
*/
static void ui_flush_cb(lv_display_t * disp, const lv_area_t * area, uint8_t * color_p) {
(void)disp;
(void)area;
/* color_p = 绘制缓冲首地址(含 palette 头)→ 跳过 palette 取真实像素 */
memcpy(g_ui_fbuf, color_p + UI_DRAW_BUF_PALETTE, UI_FB_SIZE);
lv_display_flush_ready(disp);
}
/*
* 函数功能:初始化 LVGL 显示端口与按键输入设备
* 入口参数:无
* 返回值:0 - 成功,其他 - 失败(显示或输入设备创建失败)
* 限定条件:无
* 函数说明:1. 初始化 LVGL 并创建 1bpp 离屏显示设备与绘制缓冲
* 2. 创建 keypad 设备及默认输入组,绑定刷新回调
* 3. 后续须周期调用 ui_port_step 驱动刷新
*/
int ui_port_init(void) {
lv_init();
static lv_display_t *disp = NULL;
disp = lv_display_create(UI_SCREEN_W, UI_SCREEN_H);
if (disp == NULL) {
return -1;
}
#if LV_USE_THEME_MONO
/* mono 主题显式取 dark_bg=true(黑底白字),这是远端画面"纯白"的根因修复:
* UI 内容恒以 lv_color_white() 绘制(ui_standby / ui_menu_view / ui_deco /
* ui_content_page),若底色是 LVGL 类默认浅灰 F5F5F5,白色内容在白底上完全
* 不可见,整帧近乎全白(实测 99% 像素为亮)。两步缺一不可:
* 1) lv_theme_mono_init(disp, true) 重建单例主题样式(create 时已按 false 初始化);
* 2) lv_display_set_theme() 只重应用到 screens[0](底层),活动屏幕必须再显式
* lv_theme_apply(),否则活动屏幕仍保持类默认底色(实测 bg_before=F5F5F5)。 */
lv_theme_t *th = lv_theme_mono_init(disp, true, LV_FONT_DEFAULT);
lv_display_set_theme(disp, th);
lv_theme_apply(lv_display_get_screen_active(disp));
DBG_INFO("theme: mono dark_bg=true applied");
#else
DBG_ERROR("theme: LV_USE_THEME_MONO=0 (no mono theme)");
#endif
lv_display_set_color_format(disp, LV_COLOR_FORMAT_I1);
/* I1 绘制缓冲头 8 字节是调色板(两个 lv_color32_t,内存序 B,G,R,A)。LVGL 不会替
* 调用方初始化它,实测上电后为全 0(两项都是透明黑)。显式写成 [0]=黑 [1]=白:
* 远端推流按"位=1 即亮"取用(见 ui_net_sink / DTU-RemoteLCD),语义必须确定。 */
g_ui_draw_buf[0] = 0x00u; g_ui_draw_buf[1] = 0x00u; g_ui_draw_buf[2] = 0x00u; g_ui_draw_buf[3] = 0xFFu;
g_ui_draw_buf[4] = 0xFFu; g_ui_draw_buf[5] = 0xFFu; g_ui_draw_buf[6] = 0xFFu; g_ui_draw_buf[7] = 0xFFu;
lv_display_set_buffers(disp, g_ui_draw_buf, NULL, UI_DRAW_BUF_SIZE,
LV_DISPLAY_RENDER_MODE_FULL);
lv_display_set_flush_cb(disp, ui_flush_cb);
static lv_indev_t *indev = NULL;
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) {
return -1;
}
lv_group_set_default(g);
lv_indev_set_group(indev, g);
return 0;
}
/*
* 函数功能:获取内存帧缓冲首地址
* 入口参数:无
* 返回值:帧缓冲首地址 uint8_t * 固定长度 UI_FB_SIZE 字节
* 限定条件:ui_port_init() 已成功调用
* 函数说明:缓冲由 flush 回调更新,供网络推流读取
*/
uint8_t *ui_port_get_framebuffer(void) {
return g_ui_fbuf;
}
/*
* 函数功能:向输入队列喂入一个远端按键值
* 入口参数:remo_key - 远端键码 uint8_t UI_KEY_* 取值之一
* 返回值:无
* 限定条件:与 keypad 读取同一任务上下文(uiTask / 模拟器主线程)
* 函数说明:映射后入环形队列;队列满挤掉**最旧**一个并计数——最新按键的
* 意图必须保住(旧版单槽直接覆盖丢键)。由 keypad 回调逐个上报。
*/
void ui_port_queue_key(uint8_t remo_key) {
uint8_t k = ui_map_key(remo_key);
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;
}
/*
* 函数功能:UI 周期步进(推进节拍并刷新 LVGL)
* 入口参数:tick_ms - 距上次调用毫秒数 uint32_t 建议 10
* 返回值:无
* 限定条件:ui_port_init() 已成功调用
* 函数说明:依次调用 lv_tick_inc 与 lv_timer_handler,须由任务周期调用
*/
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);
}