7708f12c10
按键召唤答的是按键前的旧帧: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 回填受管区)。
233 lines
10 KiB
C
233 lines
10 KiB
C
/*
|
||
* 模块名称: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);
|
||
}
|