Files
STM32F4-Base/docs/UI设计/UI_设计架构.md
T
wandering1 dc214e1426 docs(UI): 内容页黄金参考当前态改报「60 张」,三数按双源实测同步
三数实测真值(每项两个独立来源互证):
- 张数 60:simulator/tests/ref/ 入库 shot_*.bin 实数 60 =
  check_content_pages.py 的 PAGES 条目数 60(id 逐一对齐)。
- 已绑 28 叶 / 未绑 5 叶:ui_page_registry.h 的 UI_PAGE_MAP_COUNT 28 =
  ui_menu_tree.txt 剔掉文件头注释示例行后的 #page= 实数(不剔是 29)。
- 字体 218 唯一字 / 313 字形:ui_cn_chars.txt 唯一码点 218(ASCII 区 0
  个,ASCII 0x20~0x7E 由生成器子集参数另计、不算进字数);
  ui_font_cn_12.c 的 .bitmap_index 条目 314 条含 notdef,剔掉 notdef →
  313 = 218 + ASCII 95。

口径更正(数字一律不动):当前态计数单位由「页」改报「张」——张数 =
页面个数 + 分页页多拍的第 2 屏(runreport_p2 / sparam_p2 两张),两个数
不许互相冒充。同日先前那行「53 → 59 页」是页面个数口径、与判据本体
不同源,原文冻结不回改,只在各文件变更记录末行追加当日更正。

历史一律冻结:变更记录里的 27 / 40 / 47 / 53→60 / 59 与字库 122/217、
179/274、185/280、183/278 都是当日快照,只追加、不回改。

夹带说明:.pre-commit-config.yaml 一并入本笔——pre-commit 要求配置本身
已 staged 才允许提交;该文件的改动是 ui-golden 显示名(60 页→60 张)与
一条钩子计数口径注释(同日 51 → 52 → 51 净零,属另一类,已在此点名)。
2026-10-28 15:33:42 +08:00

31 KiB
Raw Blame History

UI 设计架构

用途:说明 UI 系统如何实际工作(渲染/输入/菜单架构)与设计考量(为什么这样做)。 配套文档:LVGL模拟器部署与使用说明.md(模拟器部署/使用)、UI_中文字体生成与配置.md(字体生成)。


一、UI 系统速览

1.1 总体方案

MCU 无物理屏幕:LVGL 做 160×160 1bpp 单色离屏渲染,帧缓冲(3200 B)经 RemoDispBus 协议(TCP 端口 7003)按需推给远端客户端(DTU-RemoteLCD)显示;远端按键走同一通道回传。"屏幕"实际是 PC 端客户端。

1.2 文件地图

文件 端 职责
Drivers/BSP/UI/ui_port.c/h MCU LVGL 离屏显示端口(I1 + flush 到内存帧缓冲)+ 远端 keypad 输入
Drivers/BSP/UI/ui_menu.c/h 共用 菜单导航逻辑层:持有导航状态(路径/选中/深度/滚动,共 17 B)+ 键语义,单向驱动显示层;不持 LVGL 对象
Drivers/BSP/UI/ui_menu_view.c/h 共用 菜单显示层:横向级联框渲染(右贴靠/自适应宽/顶底对齐),持有全部 LVGL 对象与样式、无导航状态
Drivers/BSP/UI/ui_menu_tab.c/h 共用 菜单数据层:g_menu_tab[] 父子索引表 + 查询 API(生成物,勿手改)
Drivers/BSP/UI/ui_content_page.c/h 共用 内容页框架(机制 + 描述符表);装置信息/交流量/直流量/用户密钥四页由 ui_page_xxx 模块接入(独立屏按需建毁,自绘行,零常驻池占用);多页面"框架+页面插件"架构见 内容页架构.md
Drivers/BSP/UI/ui_net_sink.c/h 共用 RemoDispBus 服务端(收键 + 回送帧缓冲),MCU/PC 双端同一份;平台后端 net_socket + ui_net_sink_backend(见 RemoDispBus 协议文档)
Drivers/BSP/UI/ui_font_cn_12.c 共用 中文 1bpp 子集字体(lv_font_conv 生成物)
App/task/ui_task.c/h MCU UI 任务:等 NET_READY → 初始化 → 10ms 刷新循环
Lib/lv_conf.h MCU LVGL 配置(1bpp / 16KB 内存池 / 默认字体);⚠ 必须与 simulator/lv_conf.h 同一特性子集,见 §1.4 第 10 条
simulator/(main.c / sim/ / lv_conf.h / CMakeLists.txt) PC SDL2 模拟器(替代 ui_port,共用全部 UI 源码);支持 --screenshot / --keys <seq> 无头截图验证
tools/font_gen/(extract_cn.py / make_font.py)+ ui_cn_chars.txt 工具 字符集提取 / 字体生成
tools/menu_gen/gen_menu.py + ui_menu_tree.txt 工具 菜单声明源 → 生成 ui_menu_tab.c/h

1.3 关键参数

项 值 位置
屏幕 160×160,1bpp(LV_COLOR_FORMAT_I1) ui_port.h
帧缓冲 3200 B(g_ui_fbuf,flush 回调整帧 memcpy) ui_port.c
绘制缓冲 3200 + palette 8 B(+32 B 余量):I1 索引色缓冲布局为 [palette 8B][pixel data],见 1.4 第 8 条 ui_port.c
LVGL 内存池 16 KB(LV_MEM_SIZE,2026-10-15 由 40 KB 收缩;实测峰值 6,568 B = 池的 40%) Lib/lv_conf.h
刷新周期 10 ms(ui_port_step(10) + osDelay(10)) ui_task.c
推流端口 7003(TCP,与 DTU-RemoteLCD 客户端对齐) ui_net_sink.h
默认字体 ui_font_cn_12(12px 1bpp,218 汉字/符号 + ASCII 0x20–0x7E,共 313 字形) Lib/lv_conf.h
uiTask 栈 16384 words(BelowNormal) Src/freertos.c

1.4 硬约束(改 UI 必须遵守)

  1. RAM 紧张:新增页面/控件/缓冲前必须核算 LVGL 池(16KB,实测峰值 6,568 B)与任务栈余量(见 docs/内存管理/RAM预算台账.md)
  2. 新增汉字必须重新生成字体(tools/font_gen/ 两步),否则显示空白
  3. 文本一律用静态字符串(lv_label_set_text_static),不 strdup
  4. UI 相关文件为 UTF-8 带 BOM(Drivers/BSP/UI/*、ui_task.c、Lib/lv_conf.h),其余源码无 BOM;禁止 PowerShell 整文件重写(编码铁律,见 AGENTS.md)
  5. 模拟器与 MCU 共用 UI 源码(ui_menu*.c / ui_font_cn_12.c),禁止在 simulator/ 下复制副本;新增文件须同时注册进 Keil .uvprojx 与模拟器 CMakeLists(双端注册)
  6. 验证顺序:模拟器预览 → @build(0 错 0 警)→ @test(Required 加 UI port init: OK;UI menu init: OK)
  7. LV_DRAW_SW_COMPLEX 必须为 1(两端均显式声明):主界面顶部"开口浅 V"装饰含斜线,lv_line 的斜段仅在复杂渲染器下绘制(draw_line_skew),关闭则只画水平线、两腿消失。且 LV_DRAW_SW_CIRCLE_CACHE_SIZE 须非零(默认 4),否则零长数组编译报错(C2466)
  8. I1 绘制缓冲必须含 palette 头(2026-09-21 实证,别再踩):LVGL 9.3 索引色格式的 draw buffer 布局是 [palette 8B][pixel data stride×h]—— lv_draw_buf_create() 按 stride×h + LV_COLOR_INDEXED_PALETTE_SIZE(cf)×4 分配,lv_draw.c:497 → lv_draw_buf_goto_xy() 取绘制指针时先 += palette,lv_refr.c get_max_row() 也从 data_size 里扣掉 palette。 但 lv_display_set_buffers() / lv_draw_buf_init() 的容量校验只看 stride×h(不含 palette) → 只给 3200 B 会被静默接受,后果有两条: ① 末 8 个像素字节写到缓冲之外(曾把紧邻 .bss 的 s_frame_box[0] 指针打脏 → 野指针 HardFault;金丝雀实测越界 n=8); ② flush 回调收到的首地址含 palette,照原样整帧拷贝会让推流画面整体错位 8 字节。 故 ui_port.c 按 stride×h + palette 供给缓冲,上报尺寸含 palette,flush 时 color_p + UI_DRAW_BUF_PALETTE 取像素。
  9. MCU 端必须显式应用 dark mono 主题 + 初始化 I1 调色板(2026-09-21 实证,远端"纯白屏"根因): lv_display_create() 内部会按 lv_theme_mono_init(disp, **false**)(白底黑字)初始化单例主题,而 lv_display_set_theme() 只在屏幕为空时把主题重应用到 screens[0](底层)——活动屏幕保持 LVGL 类默认浅灰 F5F5F5; UI 内容恒用 lv_color_white() 绘制(ui_standby/ui_menu_view/ui_deco/ui_content_page)→ 白底白字完全不可见, 远端整帧 99% 为亮(客户端显示"纯白")。修法(ui_port.c):lv_theme_mono_init(disp, true, ...) + lv_display_set_theme() + 对活动屏幕再 lv_theme_apply()(实测 bg_before=F5F5F5 → bg_after=000000)。 另外 I1 缓冲头 8 B 调色板 LVGL 不替调用方初始化(上电全 0 = 两项透明黑),须显式写 [0]=黑 [1]=白; 渲染器本身按亮度阈值置位(lv_draw_sw_blend_to_i1.c:亮 → 位=1),与客户端"位=1 即亮"的约定一致。
  10. 两份 lv_conf.h 必须是同一特性子集(2026-10-11 定;裁剪 LVGL 死代码时暴露的漂移):Lib/lv_conf.h(MCU)与 simulator/lv_conf.h(PC)是两份独立文件,此前已漂移——模拟器早已 I1-only,MCU 端却让 11 个 LV_DRAW_SW_SUPPORT_*、5 个未用控件类、LV_USE_THEME_SIMPLE、LV_USE_FLEX/GRID 全靠 lv_conf_internal 默认值隐式生效,把用不到的代码整份链进镜像 = 实测 64,576 B 死代码(旧镜像的 23.5%)。现两端对齐为同一子集:
    • 绘制核只留 LV_DRAW_SW_SUPPORT_I1(显示是 1bpp;字形 A8 位图是作为 mask 传入、混合按目的格式分派,故不需要 A8/L8/AL88);
    • 控件全关(label/button/buttonmatrix/bar/textarea/image/keyboard)——全仓(Drivers/BSP/UI、App、simulator)零控件构造函数,只用 lv_obj_create + lv_theme_mono_init;
    • LV_USE_THEME_SIMPLE 0:它是把上述控件类拉进镜像的锚点(lv_display_create() 会强链 lv_theme_simple_init);⚠ 9.3 没有 LV_USE_THEME_BASIC,旧配置里那条是无效项,已删;
    • LV_USE_FLEX 0 / LV_USE_GRID 0:全仓无 lv_obj_set_flex_* / lv_obj_set_grid_* 调用。 ⚠ 裁剪后 ARMCC 报 #177-D(上游 9.3.0 在 lv_draw_sw_blend_to_i1.c 漏了 lv_color16_from_u16 的定义处守卫)→ 本仓已按使用点补 #if LV_DRAW_SW_SUPPORT_RGB565_SWAPPED(原型 + 定义两处),升级 LVGL 时若上游已修可去掉。 ⚠ 将来引入控件型页面(原型库)时两端同步逐项打开并复跑 ui-verify。量化与验证记录见 ../内存管理/RAM使用分析与优化方案.md §2.6.4。 ✅ 已入门禁(2026-10-11):scripts/check_lv_conf_sync.py = pre-commit 钩子 lv-conf-sync。比对的是生效值(文件未定义则取 lv_conf_internal.h 兜底默认),并抓"写了 LVGL 不认的宏名"与同文件重复定义。首次跑即揪出两端共 26 个 9.3 不存在的选项(LVGL 8 / 9.4 模板残留,与 LV_USE_THEME_BASIC 同类)、3 处同文件重复定义、10 项 MCU 端靠默认值隐式生效的开关(含默认 =1 的 LV_USE_OBSERVER / LV_USE_DRAW_PXP / LV_USE_IMAGEBUTTON)——已全部清理/显式固化。 ✅ 门禁触发范围(2026-10-11):lv-conf-sync 的 files: 同时匹配 Lib/lv_conf.h、simulator/lv_conf.h 与脚本自身 → 两端任一改动都会跑。此前顶层 exclude 整目录挡掉 simulator/,模拟器侧改动拦不到;已删该条并把 simulator/ 下自研代码整体纳入 pre-commit(cppcheck 当天即揪出 sim_syslog.c 一处真实空指针解引用),详见 ../代码规范/pre-commit使用说明.md §5.5。

二、渲染链路(画面怎么到"屏幕")

2.1 MCU 端(产品实际路径)

LVGL 场景(160×160,LV_COLOR_FORMAT_I1 单色)
  → SW 软件渲染器(仅启用 I1,裁掉其余色深)
  → flush 回调:整帧 memcpy 到内存帧缓冲 g_ui_fbuf(3200 B)
  → RemoDispBus 协议(TCP 7003):远端客户端发 LCDMEM 请求才回送整帧
  → DTU-RemoteLCD 客户端(PC)解码显示
  • MCU 无物理屏幕:"屏幕"是 PC 端客户端,帧缓冲只是网络传输的载体
  • 按需拉取:不主动推帧,客户端要才给(菜单是低频交互,够用且省带宽)
  • LVGL 视角:ui_port.c 就是一个普通 display(I1 格式 + flush 回调),不知道帧最终走网络

2.2 模拟器端(PC 开发路径)

同一份 LVGL 场景(共用 ui_menu.c / ui_font_cn_12.c)
  → SW 渲染器(I1)
  → SDL 窗口 blit(LV_SDL_RENDER_MODE_PARTIAL,1bpp 强制要求)
  → 屏幕(160×160,高 DPI 下可能放大显示,属正常)
  • 两端场景层完全相同(同一份 UI 源码),只有"渲染结果去向"不同:MCU 进帧缓冲走网络,模拟器进 SDL 窗口
  • 模拟器必须初始化 mono 主题(lv_theme_mono_init):1bpp 下无主题 = 黑底黑字,全黑屏

三、输入链路(按键怎么到菜单)

3.1 统一模型(两端相同)

按键 → keypad 输入设备(LV_INDEV_TYPE_KEYPAD)
  → 默认输入组(lv_group)
  → 组内焦点对象 = 当前可见页面容器
   → 容器的 LV_EVENT_KEY 回调(menu_key_cb,user_data = 该层深度)
  • keypad 输入设备经 lv_group_send_data 把键码发给焦点对象(模拟器 --keys 无头验证走同一路径)
  • LVGL v9 的输入组不自动注册默认键回调(方向键移动焦点需焦点对象自己实现) → 4 个深度容器都在组内,但同一时刻仅最右框容器获焦,按键一律由逻辑层统一处理,无焦点争抢
  • 按下时输入设备捕获焦点对象(indev_obj_act),释放时向其回发 RELEASED/CLICKED → 焦点对象(容器)必须存活,这是"容器持久不删"的原因之一(见 §5.2)

3.2 MCU 端

远端客户端按键 → TCP 7003 → ui_net_sink 解析
  → ui_port_queue_key(UI_KEY_* → LV_KEY_* 映射)
  → keypad 输入设备 read_cb 上报 → 默认输入组

3.3 模拟器端

SDL 键盘 → LVGL SDL 驱动内部 5ms 定时器(sdl_event_handler)
  → 键码映射(↑↓←→/Enter/Esc → LV_KEY_*)
  → keypad 输入设备 → 默认输入组
  • ⚠️ 主循环不得再调 SDL_PollEvent:会先消费事件队列,按键全部失效(2026-09-02 实际踩坑,见 §5.7)

3.4 键位语义

键 行为
↑ / ↓ 移动选中(首尾环绕);选中行反色高亮 + 自动滚动到可见区
→ / Enter 选中条目右侧弹出子框(叶子节点建空白终点框)
← / Esc 关最右框(其行销毁,焦点回父框,选中态不变)

四、菜单架构(核心)

显示模型细节(右贴靠/自适应宽/顶底对齐的像素规则与实测值)见 菜单页面设计.md §四⑧;本节只讲代码架构。

4.1 三层拆分(高内聚低耦合)

层 文件 职责 持 LVGL 对象?
数据层 ui_menu_tab.c/h(生成物) g_menu_tab[] 父子索引表 + 查询 API(children/has_children/name/valid) 否
显示层 ui_menu_view.c/h 级联框渲染:样式 / 持久容器 / 框体(行与根框装饰、状态行均自绘、无对象);几何现算按层缓存 是(全部)
逻辑层 ui_menu.c/h 导航状态(s_path/s_sel/s_depth/s_scroll,共 17 B)+ 键语义;单向驱动显示层建/毁/刷新 否
  • 依赖方向:逻辑 → 显示,两者都用数据查询 API;显示层无导航状态、逻辑层无 LVGL 对象
  • 改菜单只编辑 ui_menu_tree.txt 后重跑 python tools/menu_gen/gen_menu.py(再生成 ui_menu_tab.c/h)

4.2 数据:父子索引表

  • g_menu_tab[]:42 节点,每项 = 名称 + parent 下标(MENU_ROOT=0xFF 为根);树序即表序
  • "页" = 对表的一次过滤:节点 N 的页条目 = 全部 parent == N 的行(按表序)
  • 菜单 vs 叶子由"是否有子项"自动推导:has_children(N) 非空 → 菜单行(带 ▶),为空 → 叶子(无箭头,ENTER 进空白终点框)

4.3 对象树

screen
└─ menu(铺满屏幕,flat)
   └─ cont 全屏持久容器(唯一焦点/键入口;各层框体均建其内,永不删)
       ├─ 根框体(持久)+ 根框装饰"浅 V" + 状态行(容器自绘,无对象)
       └─ 子框体进入时建、返回时毁
  • 持久容器永不删(唯一焦点/键入口,见 §5.2);子对象全部绝对定位(级联框须同屏叠加,层间不用 flex 堆叠)
  • 框体 = lv_obj(透明底 + 1px 白边框 + 自动滚动条),行在框体内自绘(无子对象)

4.4 条目行与选中态

  • 行 = 数据(节点下标 s_row_node[d][i],无对象)+ 框体 LV_EVENT_DRAW_MAIN 自绘:选中行整行白带 + 每行 lv_draw_label 文本(左留白 2px)/箭头(紧跟文本、间距 2px;叶子无箭头)
  • 行无对象、不入输入组:纯展示,按键一律由容器 → 逻辑层处理
  • 选中行 = 整行反色(白带、黑字),箭头随反色变黑(参考图 02/04 实测:选中行保留 ▶);非选中行黑底白字

4.5 框生命周期(按需建毁)

ENTER/→ 进子框:
  现算几何(x = 父框右缘 − 1 共享边框列;y = 父"选中行"行顶,超屏底则上移使本框底对齐该行行底)
  → cont 建框体 + 注册行数据(框深度由逻辑层 s_depth 决定,焦点恒在持久容器)
←/Esc 返回:
  销毁最右框(框体连自绘回调整体删除,容器保留)→ 焦点恒在持久容器;父层选中/滚动位原样保留
  • 容器持久、框动态:行自绘无对象,池内存 = 屏上框体(实测根框 5,368 B、最深四级级联 6,040 B、最重页面峰值 6,568 B,远低于 16KB 池)
  • 同节点再次进入:还原该框上次选中与滚动位(s_sel/s_scroll);换节点则清零
  • 叶子节点(无子项):ENTER 建空白终点框(w=6、h=2 占位),仅 ESC 可返回
  • 深度保护:depth+1 ≥ 4 不再下钻

4.6 焦点与按键处理

  • 焦点对象 = 最右(最深)框的容器;进/退时 lv_group_focus_obj 显式转移
  • 每层容器挂 menu_key_cb(user_data = depth),统一分发到逻辑层 menu_key_handle:↑↓ 移选(首尾环绕)+ 跟随滚动、→/ENTER 进、←/ESC 退
  • 选中变化只改选中数据 + 跟随滚动 + 重绘框体,不重建;滚动位回读持久化到 s_scroll[d]

4.7 内容页(ui_content_page)

菜单叶子节点进入后的独立全屏页面(装置信息/交流量/直流量/用户密钥密码页),与级联框同屏互斥。设计细节见 菜单页面设计.md §九、内容页架构.md。

  • 对象树:screen(新) → cont 全屏容器(flat + CLICK_FOCUSABLE,键入口);行全部自绘(无子对象),与级联框同一"行=数据"策略
  • 生命周期 = 按需建毁:ENTER 叶子(node 1/3/4)→ lv_obj_create(NULL) 新建独立屏 + 容器 → 入默认输入组获焦 → lv_screen_load 切屏;ESC → 焦点先回菜单持久容器、切回菜单屏,再删容器+屏。菜单屏对象保留不销毁(返回即原样还原)
  • 导航状态不压层:菜单 s_depth/s_sel/s_scroll 原样保留,内容页只记自身选中行 s_sel(每次进入从首行开始)
  • 池占用实测(模拟器 x64,16KB 池):开页峰值 +776 B(屏+容器+事件列表+局部样式),关页后回落;一次性残留 +80 B(screens 数组高水位 +8、lv_screen_load 给菜单屏设 X/Y 局部样式 +72,均为 LVGL 核心行为、非本模块泄漏)
  • flat 样式用常量样式(LV_STYLE_CONST_INIT):属性存静态数组、零池分配;动态 lv_style_t 首次 set_prop 会从池分配 values_and_props 且永不释放(x64 实测 88 B/个,MCU 同量级),常驻样式一律用常量样式
  • 多页面架构(已定稿并拆分,2026-09-09):待实现 30 个叶子页;框架(本模块)+ 每页一文件(ui_page_xxx,描述符为 extern const 对象)+ 4 个扩展钩子(refresh/count_cb/draw_cb/key_cb),见 内容页架构.md;架构第一步(共享"浅 V"装饰 ui_deco)与拆分本身均于 2026-09-09 落地,纯重构回归全绿(像素一致、池 4784/4864 B 不变);首个表单/操作页(用户密钥密码页:draw_cb+key_cb + 描述符 title_y/hide_deco/hide_footer/key_intercept)同日落地

五、设计决策与考量(为什么这样做)

5.1 弃用 lv_menu 控件

  • lv_menu 已被 LVGL 官方废弃;官方建议用基元控件构建菜单: lv_obj 构建页面 + 返回按钮切换可见页面(参考官方示例 lv_example_menu_navigation)
  • 基元控件方案的好处:无废弃风险、样式完全自主可控、与 mono 主题/1bpp 单色兼容性好
  • 配套动作:LV_USE_MENU 两端配置置 0(MCU Lib/lv_conf.h + 模拟器 simulator/lv_conf.h),省代码空间

5.2 页面容器持久不删

  • keypad 输入设备在按下时捕获焦点对象,释放时向其回发 RELEASED/CLICKED
  • 若在按键处理器内删除焦点对象(如"返回时销毁当前页"),释放事件将落在已释放内存上 → use-after-free
  • 容器永不删除,回发事件始终落在存活对象上,天然安全;要释放的只是容器内的框体(行自绘、无对象)

5.3 框按需建/销

  • LVGL 池仅 16KB(LV_MEM_SIZE,2026-10-15 由 40KB 收缩),全量预建 42 个节点的所有页面必然爆内存
  • 只建"当前路径"上的框:行自绘无对象,池内存 = 屏上框体(实测根框 used=5,368 B、最深四级级联 used=6,040 B、网口设置保存态峰值 used=6,568 B = 池的 40%)
  • 返回即 lv_obj_delete 释放,内存随时回落到根框水位

5.4 持久容器是唯一焦点对象

  • v9 输入组不自动处理方向键(无默认键回调),框深度无需由焦点对象携带
  • 持久容器为组内唯一焦点对象(初始化时入输入组并获焦,永不删、焦点永不迁移);容器集中处理按键:逻辑单点、行为可预测、无多对象焦点争抢
  • 框深度由逻辑层 s_depth 决定(4 容器时代的 user_data=depth 已冗余,去掉)
  • 框体(含自绘行)不入组:纯展示,不参与焦点/点击,避免与容器抢键

5.5 条目行自绘而非对象化

  • 每行对象有"机制税":lv_obj_t 结构体 + 样式向量 + 子 label(文本/箭头)状态,约 2 KB/行;屏上 2~3 框 × 多行会逼近 16 KB 池 → 行降级为数据(节点下标 s_row_node[4][16],1 B/行)
  • 框体 LV_EVENT_DRAW_MAIN 自绘:选中行整行白带(lv_draw_rect),每行文本/箭头经 lv_draw_label(与 label 同字体光栅化路径,像素级一致)
  • 行不需要对象语义(焦点/点击/按压反馈):输入由容器统一处理,行是纯展示
  • 自绘同时根除了旧对象模型的两类问题:样式继承"白底白字"坑(2026-09-02 实际踩坑)、行对象池内存膨胀

5.6 flat 样式剥离主题 card

  • mono 主题给所有基元对象附加 card 样式(背景/边框/圆角/内边距/文字色)
  • 160×160 小屏 1bpp 菜单不需要卡片观感(边框圆角浪费像素)
  • flat 样式(透明背景/无边框/无圆角/零内边距)后加优先,逐项覆盖 card
  • 常驻 flat 样式必须用常量样式(LV_STYLE_CONST_INIT + lv_style_const_prop_t[]):动态 lv_style_t 的 values_and_props 是池分配且永不释放(x64 实测 88 B/个),RAM 紧张下每个常驻动态样式都是永久泄漏;常量样式属性存 Flash、零池占用

5.7 SDL 事件只由 LVGL 驱动轮询

  • LVGL SDL 驱动内部有 5ms 定时器(lv_sdl_window.c 的 sdl_event_handler)统一 SDL_PollEvent 并分发
  • 主循环若再 SDL_PollEvent,会先消费事件队列 → 驱动定时器轮询时队列已空 → 键盘全部失效
  • 关窗退出由驱动 LV_SDL_DIRECT_EXIT=1 自动 exit(0),主循环无需处理 SDL_QUIT

5.8 推流往返的心跳边界(F3,2026-10-25 大窗判读)

  • 现象:netTask 监听重建风暴期,ui_net_sink_poll() 内「单个可等待往返都回来了、 但一轮里串了好几个」,把 uiTask 两次心跳之间的间隔顶过 800ms——看门狗按位连击 判据(docs/子系统设计/看门狗设计说明.md §5.3)抓到 bit4 单任务 8 连击窗 (2026-10-25 台架实测,uisn=154ms 仍在段内样本,:7003 churn 与探针重建并发叠加)。
  • 处置(本票 = 心跳面):ui_net_sink_set_beat_cb() 回调注入——sink 在每次 accept / recv_nb / send / close / 重建三件套的返回处调用钩子,uiTask 注册 task_wd_beat(TASK_WD_UI)。效果 = 心跳间隔从「整轮往返之和」收敛到「单个往返」; 只要每次往返都会回来,UI 就不该被记停摆。往返本身不改(有界化/请求序号 = docs/任务规划/README.md 的跨上下文收口条目,批 3;§11.6 否决过的 「给 net_* 裸加超时」不重演)。
  • 分层:Drivers/BSP 不得 include App 头(layer-deps),故心跳经回调注入而非直调; 钩子内只做置位类动作、不阻塞(契约写在 Drivers/BSP/UI/ui_net_sink.h)。
  • 零行为变化面:钩子未注册(模拟器 / 黄金回归 / start 之前)= 与 F3 之前逐字一致; 协议、帧格式、门控、兜底节拍全部不动。回归钉 = test/host/ui_sink/(17 断言: 未注册全协议路径 / 门控零 RPC 零心跳 / 每往返恰好一次心跳 / 重建与 EOF 的 close 护栏不变)。
  • 残留暴露(刻意不归本票):单个往返自身仍无上界(osMessageQueuePut(osWaitForever)
    • xTaskNotifyWait(portMAX_DELAY)),netTask 若被单消息拖 >800ms,UI 位连击照成立—— 那是「判据抓对了人」,处置在 net 侧,不在心跳侧。

六、变更记录(简表)

日期 变更
2026-08-30 LVGL v9.3.0 入库;初始 UI(ui_port / ui_menu / ui_net_sink / ui_task);开启 192K 全部 RAM
2026-08-31 推流/按键链路打通;移除 LVGL 未用 demos/tests;搭建 PC 模拟器(CMake + SDL2)
2026-09-01 代码规范钩子落地,UI 代码随全库规范修复
2026-09-02 菜单重构:弃用 lv_menu → 基元控件页面导航(§4/§5);字体 83→78 字(补「返/回」);修复模拟器主循环吞事件致键盘失效(§5.7);修复选中行白底白字(§5.5)
2026-09-03 关键约束 7:LV_DRAW_SW_COMPLEX 须=1(顶部"浅 V"斜线依赖);两端 lv_conf 显式固化,LV_DRAW_SW_CIRCLE_CACHE_SIZE=4 防零长数组
2026-09-04 菜单重构为"横向级联框"模型(见 菜单页面设计.md §四⑧):数据/显示/逻辑三层拆分(新增 ui_menu_view 显示层、ui_menu_tab 生成物,ui_menu 瘦身为纯状态机);字体重生成补缺字形(78 字,含菜单树新增的整/控/软/板等);模拟器加 --keys <seq> 无头注键截图验证
2026-09-08 菜单 RAM 收敛两连:行对象→框体自绘(行降为数据,§4.4/§5.5);4 层容器→单一持久容器(焦点恒在其上,框深度由逻辑层 s_depth 决定,§4.3/§5.4)+ 根框装饰/状态行容器自绘(无对象);LVGL 池实测 根框 4872 B / 最深四级级联 6128 B
2026-09-09 内容页模块 ui_content_page(§4.7):装置信息/交流量/直流量三叶子页,独立屏按需建毁 + 行自绘;flat 样式改常量样式(LV_STYLE_CONST_INIT,零池分配,§5.6);字体 78→97 汉字(193 字形)
2026-09-09 菜单显示层 flat 样式同法转常量样式(§5.6):LVGL 池基线 4872→4784 B、最深四级级联 6128→6040 B(x64 省 88 B,MCU 同量级);LV_STYLE_CONST_INIT 生成外部链接符号,内容页同名者改名 cp_style_flat 避免两文件冲突
2026-09-09 内容页多页面架构定稿(内容页架构.md):框架+页面插件模型(每页一文件 ui_page_xxx + 4 个扩展钩子)、30 个待实现叶子页清单、新增页面六步流程;同日"浅 V"装饰提取为共享 ui_deco 模块(架构第一步,菜单根框/内容页共用,像素回归验证一致)
2026-09-09 内容页拆分执行(内容页架构.md §九 变更记录):ui_content_page 提纯为纯框架(描述符表 + NULL 防护 + 描述符驱动绘制/按键、4 个扩展钩子);三页数据拆出 ui_page_device_info / ui_page_ac / ui_page_dc(extern const 描述符对象);双端注册;Keil 0 错 0 警 + 模拟器 0 警告 + ui-verify 五门禁全绿(像素一致、池 4784/4864 B 不变)
2026-09-09 用户密钥密码页落地(首个表单/操作页,§4.7 / 内容页架构.md §4.1):框架描述符新增 title_y/hide_deco/hide_footer/key_intercept + 新 API ui_content_page_invalidate;新模块 ui_page_password(3 数字框反显光标 + arm API);菜单受保护叶子路由(arm→开密码页,确认一致后回调先关页再续做进入动作);字体 97→102 汉字(198 字形);双端注册;回归扩至 7 页 + 往返水位,五门禁全绿
2026-09-10 网口设置内容页(§4.7 / 内容页架构.md):新增 Drivers/BSP/NETCFG(Flash 持久化 + CRC-16,工厂默认 192.168.1.100/24)+ 页面插件 ui_page_net_cfg(5 行动态值 + 可编辑:↑↓调字节/←→移光标/ENTER 确认,保存行 ENTER → net_cfg_save 下次上电生效);框架新增钩子 init_cb + API ui_content_page_selected_row;net_init 增 mac 参数、netTask 等 SYS_EVT_STORAGE_READY → net_cfg_load → net_init(ip,mask,gw,mac) → 回写实际 MAC 影子;字体 102→111 字符(208 字形,修复低码位符号丢失);双端注册;回归扩至 11 页,五门禁全绿
2026-09-14 运行记录/自检记录内容页 + 待机显示模块:新增 App/log/sys_log(定长 64B 文本记录引擎,MCU 落 NAND /log/run.log、/log/selftest.log,FTP 可直下)+ 页面插件 ui_page_runlog / ui_page_selftest(动态列表:count_cb + refresh,每条 2 行时间戳/描述)+ ui_standby(空闲超时自动切待机屏,仅确认键回菜单);密码页错误提示"密码输入错误"+清空重输;字体 111→122 字(217 字形);详见 docs/子系统设计/运行与自检记录设计说明.md
2026-10-25 F3 推流心跳边界(§5.8):ui_net_sink_set_beat_cb() 回调注入,accept/recv_nb/send/close/重建三件套返回处喂 UI 心跳(2026-10-25 大窗 bit4 8 连击实证:单轮多慢往返之和串成 800ms 缺席);往返语义不改(上界归 E-27/E-28);未注册 = 零行为变化(模拟器/黄金回归形态);新增 test/host/ui_sink/ 主机套件 17 断言
2026-10-25 字库扩充(页面扩充规划预置字 +60,含罗马数字 Ⅰ/Ⅱ/Ⅲ):默认字体升至 179 字符/274 字形,像素基线不变,ROM +1,528 B、RAM 0;修复生成器 make_font.py 的 cmap 插入顺序缺陷——LVGL get_glyph_dsc_id() 线性扫描首个包含即返回,补回字形(▶)单码位条目被下探后的宽稀疏 range 吞掉即全渲染方框,现按首命中语义插位/守卫/校验(详见 docs/UI设计/UI_中文字体生成与配置.md 文末事故记录)
2026-10-28 UI 补齐批接线(单写者):事故/操作/保护告警/保护启动/遥控/运行报告六叶绑定内容页(生成物 UI_PAGE_MAP_COUNT 22→28,节点 42 / 叶 33 不变)+ 6 个页面 .c 双端注册;默认字体 185 字符/280 字形 → 218 字符/313 字形(+33 字,§1 概况表"默认字体"行同步,旧数 183/278 系未回填);内容页像素黄金参考 53 → 59 页;ROM/RAM 增量待 @build 后按 .map 实测回填
2026-10-28 全仓计数复扫(三数双源互证 + 口径更正):① simulator/tests/ref/ 入库 shot_*.bin 实数 60 = check_content_pages.py 的 PAGES 条目数 60(两源一致;其中 2 张是分页页多拍的第 2 页,所以张数比页面个数多。上一行「53 → 59 页」按页面个数口径、同日 docs/门禁说明/ui-golden.md「53 → 60」按入库张数口径——两个数各自成立,但不许互相冒充;本仓判据本体 = 入库张数,当前态一律报 60 张。② 已绑内容页 28 叶(生成物 UI_PAGE_MAP_COUNT = ui_menu_tree.txt 剔掉文件头注释示例行后的 #page= 实数 28;不剔是 29)/ 未绑 5 叶 = 遥信类型 + 遥信防抖 + 双点遥信 + 手动录波 + SNTP 设置,与分类表逐项闭合。③ 中文字体 218 唯一字 / 313 字形:ui_cn_chars.txt 唯一码点 218(ASCII 区 0 个,ASCII 由生成器子集参数另打进去、不计入字数);ui_font_cn_12.c 的 .bitmap_index 条目 314 条含 notdef → 字形 = 314 − 1 = 313 = 218 + ASCII 95。④ 本次另笔补的漏改当前态:运行与自检记录设计说明 §2.1(122 字→218)、LVGL模拟器部署与使用说明 §6.2 与 Q4(83 / 186→218 / 313)、lvgl-simulator skill 正文(53 页→60 页);页面扩充详细方案 §1.1 的「已绑 6 叶」按立项快照冻结、只加现值指针