12 KiB
12 KiB
LVGL PC 模拟器(simulator/)
在 PC 上编译运行与 MCU 同一份 UI 源码(
Drivers/BSP/UI/ui_menu.c+ui_font_cn_12.c), 实时预览 160×160 1bpp 单色菜单界面,无需烧录 MCU。部署细节与完整 FAQ 见
docs/UI 设计/LVGL模拟器部署与使用说明.md;本文聚焦本目录的内容与架构。
一、目录内容
simulator/
├── CMakeLists.txt # 构建脚本(CMake + MSVC,x64)
├── main.c # 入口:SDL 初始化 → LVGL → 菜单 → 主循环
├── sim_port.c / sim_port.h # 模拟器显示/输入端口(SDL 窗口 + 键盘),替代 MCU 端 ui_port.c
├── lv_conf.h # 模拟器专用 LVGL 配置(与 MCU 端 Lib/lv_conf.h 相互独立)
├── i1_to_png.py # 截图转 PNG:把 --screenshot 的 I1(1bpp) 原始位图转成 PNG(见 §3.5)
├── dbg_log.h # 调试日志 stub:DBG_* 映射到 printf(覆盖 MCU 版头文件)
├── SDL2/
│ └── SDL2-2.32.10/ # SDL2 预编译 SDK(随仓库提供,无需安装)
│ ├── include/ # SDL2 头文件
│ │ └── SDL2/ # ★ 头文件副本(LVGL SDL 驱动写死 #include "SDL2/SDL.h")
│ └── lib/
│ ├── x64/ # SDL2.lib / SDL2main.lib / SDL2.dll(构建使用)
│ └── x86/ # 32 位库(当前未使用)
└── build/ # CMake 生成物(可整体删除重建,勿提交)
└── Debug/lvgl_sim.exe # 可执行文件(SDL2.dll 由 POST_BUILD 自动复制到同目录)
文件职责
| 文件 | 说明 |
|---|---|
main.c |
程序入口。SDL_Init(TIMER|EVENTS) → lv_init() → sim_port_init(160,160) → ui_menu_init()(共享代码)→ 主循环(SDL 事件 + lv_timer_handler + lv_refr_now) |
sim_port.c |
创建 SDL 窗口显示设备(lv_sdl_window_create)、初始化 mono 主题(lv_theme_mono_init,1bpp 必需)、创建键盘输入设备并挂默认输入组。不创建鼠标(纯键盘导航,与 MCU 端 keypad 一致) |
lv_conf.h |
模拟器专用 LVGL 配置。与 MCU 端 Lib/lv_conf.h 的关键差异见 §3.3 |
dbg_log.h |
MCU 版 dbg_log.h 依赖 sys_clock.h(RTC 时间戳),PC 无法编译。此 stub 把 DBG_INFO/ERROR/DEBUG 映射到 printf;因 simulator/ 在 include 路径最前,优先生效覆盖 MCU 版 |
CMakeLists.txt |
收集 LVGL 源码(../Lib/lvgl)+ 共享 UI 源码(../Drivers/BSP/UI)+ 本目录端口代码,链接预编译 SDL2,产出 lvgl_sim.exe |
i1_to_png.py |
把 --screenshot 产出的 I1(1bpp) 原始位图转成 PNG,便于离线比对设计稿。详见 §3.5 |
共享代码不复制:
ui_menu.c/ui_font_cn_12.c直接引用Drivers/BSP/UI/下的原件, MCU 与 PC 两端永远同步。不要在本目录内复制 UI 源码。
二、架构框架
2.1 运行架构
main.c
│
├─ SDL_Init(TIMER | EVENTS) # 仅初始化计时/事件,视频由 LVGL SDL 驱动内部接管
├─ lv_init() # LVGL v9.3.0 内核
├─ sim_port_init(160, 160)
│ ├─ lv_group_create + set_default # 默认输入组
│ ├─ lv_sdl_window_create(160,160) # SDL 窗口 = 显示设备(I1 单色)
│ ├─ lv_theme_mono_init(disp, true, # mono 主题(白字黑底),1bpp 下不初始化 = 黑屏
│ │ LV_FONT_DEFAULT)
│ └─ lv_sdl_keyboard_create # 键盘 → 默认输入组(方向键/Enter/Esc)
├─ ui_menu_init() # ★ 与 MCU 共用(Drivers/BSP/UI/ui_menu.c)
│ # 基元控件页面导航,节点表驱动
└─ 主循环
├─ SDL_PollEvent # SDL_QUIT → 退出
├─ lv_timer_handler() # 驱动 LVGL 定时器/重绘
├─ lv_refr_now(NULL) # 强制整屏刷新(PARTIAL 模式下保证首帧输出)
└─ Sleep(lv_timer_handler 返回值) # 自适应休眠
2.2 渲染链路(1bpp 单色)
LVGL 场景(基元控件页面/label,LV_COLOR_FORMAT_I1)
→ SW 渲染器(仅启用 I1,LV_DRAW_SW_SUPPORT_I1=1)
→ SDL 窗口 blit(LV_SDL_RENDER_MODE_PARTIAL,1bpp 强制要求)
→ 屏幕(160×160 物理像素,Windows 高 DPI 可能放大显示,属正常)
对照 MCU 端链路(ui_port.c):
LVGL 场景(同一份 ui_menu.c)
→ SW 渲染器(I1)
→ flush 回调 memcpy 到内存帧缓冲 g_ui_fbuf(3200 B)
→ RemoDispBus 协议(TCP 7003)按需推给远端客户端显示
两端场景层完全相同,只有"渲染结果去向"不同:模拟器进 SDL 窗口,MCU 进内存帧缓冲再走网络。
2.3 MCU 端 vs 模拟器 对照
| 项 | MCU(Keil MDK) | 模拟器(CMake + MSVC) |
|---|---|---|
| LVGL 编译 | .uvprojx 工程添加 Lib/lvgl 源文件 |
Lib/lvgl/CMakeLists.txt(最小 CMake,仅模拟器用,不影响 Keil 编译) |
| lv_conf.h | Lib/lv_conf.h |
simulator/lv_conf.h(独立,互不影响) |
| 显示端口 | ui_port.c(离屏 framebuffer + flush 回调) |
sim_port.c(SDL 窗口) |
| 输入 | 远端 keypad(ui_port_queue_key,网络回传键码) |
SDL 键盘(lv_sdl_keyboard_create) |
| 主题 | 无主题(LV_USE_THEME_* = 0),控件用内置基础样式 |
mono 主题必须开启(否则 1bpp 黑底黑字) |
| 日志 | Drivers/BSP/dbg_log.h(USART1 + RTC 时间戳) |
simulator/dbg_log.h(printf stub) |
| 业务任务 | netTask / ftpTask / timeSyncTask 等 | 无,纯 UI |
| 时钟 | lv_tick_inc 由 ui_task 手动喂(10ms 周期) |
SDL 驱动内部 lv_tick_set_cb(SDL_GetTicks),不可再手动喂 |
2.4 关键设计决策
- SDL2:LVGL 官方 PC 端口同款显示方案;使用仓库内预编译 SDK(
SDL2/SDL2-2.32.10/),无需系统安装 - CMake + MSVC:
Visual Studio 16 2019生成器,-A x64;MSVC 下加/utf-8消除中文注释的 C4819 警告 - 共享 UI 源码:
ui_menu.c/ui_font_cn_12.c直接编译Drivers/BSP/UI/原件,改一处两端同时生效 - 纯键盘导航:不创建鼠标/触摸设备,与 MCU 端 keypad 操作语义一致(↑↓←→ / Enter / Esc)
- 仅 UI 预览:不含网络/存储/ADC 等硬件业务,无法验证与外设相关的逻辑
三、构建与运行
3.1 环境要求
| 依赖 | 版本 | 说明 |
|---|---|---|
| CMake | ≥ 3.15 | 本机验证 4.3.0-rc1 |
| MSVC | VS2019 BuildTools | Visual Studio 16 2019 生成器,Hostx64/x64 |
| SDL2 | 2.32.10 | 已内置于 SDL2/,无需安装 |
| 操作系统 | Windows | 已验证 win32 |
3.2 构建命令
# 首次(配置 + 构建)
cd simulator
cmake -B build -G "Visual Studio 16 2019" -A x64
cmake --build build --config Debug
# 日常(仅改 .c/.h 时增量编译)
cmake --build build --config Debug
# 运行
.\build\Debug\lvgl_sim.exe
# 或:cmake --build build --config Debug --target run
改
CMakeLists.txt后需重新cmake -B build;遇到奇怪问题先删build/全量重建。
3.3 lv_conf.h 与 MCU 端的关键差异
| 配置 | MCU 端(Lib/lv_conf.h) |
模拟器(本目录) | 原因 |
|---|---|---|---|
LV_USE_SDL |
0 | 1 | SDL 显示/键盘驱动 |
LV_SDL_RENDER_MODE |
— | PARTIAL |
1bpp + SDL 非 PARTIAL 会 #error |
LV_SDL_INCLUDE_PATH |
— | "SDL2/SDL.h" |
对应 include/SDL2/ 头文件副本 |
LV_USE_THEME_MONO |
0 | 1 | 1bpp 无主题 = 黑底黑字不可见 |
LV_DRAW_SW_SUPPORT_I1 |
1 | 1 | 仅启用 I1 渲染,裁掉其余色深 |
LV_MEM_SIZE |
40 KB | 40 KB | 保持一致(PC 内存充足,可放宽) |
LV_FONT_DEFAULT |
&ui_font_cn_12 |
同 | 共用同一字体 |
| 其余控件/驱动 | 裁剪 | 基本一致(另开 LV_USE_TEXTAREA/KEYBOARD/BUTTONMATRIX) |
模拟器略宽松,便于调试 |
3.4 键盘操作
| 按键 | 功能 |
|---|---|
| ↑ / ↓ | 菜单项上下移动 |
| → | 进入子页 |
| Enter | 确认 / 进入子页 |
| ← / Escape | 返回上级 |
3.5 截图比对设计稿(离线出图)
窗口模式只适合人工目检。要把渲染结果存成 PNG 与设计稿(docs/UI 设计/mockups/render_01.png)逐像素比对,用离屏截图 + 转 PNG:
cd simulator
# 1) 离屏渲染首帧,输出 I1(1bpp) 原始位图(已剥离 8 字节调色板,纯像素数据)
.\build\Debug\lvgl_sim.exe --screenshot shot.bin
# 2) 转 PNG(默认 160x160;--scale 放大便于查看)
python i1_to_png.py shot.bin # -> shot.png
python i1_to_png.py shot.bin -o 预览.png --scale 6
I1 位图格式(i1_to_png.py 据此解析):1 位/像素,每字节 MSB 在左(bit7 = 该行最左像素),bit=1 前景 / bit=0 背景,每行 宽/8 字节、行内无填充。
i1_to_png.py 常用参数:
| 参数 | 说明 |
|---|---|
-o, --output |
输出 PNG 路径(默认与输入同名 .png) |
-W, -H |
宽高(默认 160/160,非默认分辨率时必填) |
--scale N |
最近邻放大 N 倍,便于查看小图 |
--invert |
前/背景互换 |
--fg / --bg |
自定义前/背景色 R,G,B(默认黑白) |
--no-auto-palette |
关闭调色板自动识别(按纯像素解析) |
依赖 Pillow(
pip install pillow,本仓库已验证)。shot.bin/shot*.png均已列入.gitignore,可随时删除重生成。
四、已知坑(构建/运行)
cannot open include file "SDL2/SDL.h"LVGL SDL 驱动写死#include "SDL2/SDL.h",而原版 SDK 头文件平铺在include/下。 必须把头文件复制一份到include/SDL2/子目录(本仓库已做好):$inc = "simulator\SDL2\SDL2-2.32.10\include" New-Item -ItemType Directory -Path "$inc\SDL2" -Force Copy-Item "$inc\*.h" "$inc\SDL2\"cannot open include file "lv_conf.h"LV_CONF_DIR/SDL2_INCLUDE_DIR两个 CMake 变量必须在add_subdirectory(lvgl)之前定义(见CMakeLists.txt中的顺序注释),勿随意调整。- 运行后窗口全黑,按序排查:
LV_USE_THEME_MONO = 1且sim_port_init中调用了lv_theme_mono_init- 主循环调用了
lv_refr_now(NULL)(PARTIAL 模式保证首帧) - 没有重复设置 tick(SDL 驱动内部已
lv_tick_set_cb,不要再SDL_AddTimer+lv_tick_inc)
- 中文显示空白:字体
ui_font_cn_12是子集字体(当前 78 字),缺字渲染为空白。 补字流程见docs/UI 设计/UI_中文字体生成与配置.md(tools/font_gen/两步脚本)。 - 改了 UI 代码但模拟器没变化:确认改的是
Drivers/BSP/UI/ui_menu.c(共享原件)而非本目录副本(本目录无副本),然后重新cmake --build。 - 编码:UI 源码为 UTF-8(带 BOM),MSVC 已加
/utf-8。不要用 PowerShellSet-Content等重写含中文的文件(编码铁律,见根目录AGENTS.md)。 - 键盘输入无响应(方向键/Enter 无效):
main.c主循环不得调用SDL_PollEvent。 LVGL SDL 驱动内部有 5ms 定时器(lv_sdl_window.c的sdl_event_handler)统一轮询分发 SDL 事件; 主循环若先轮询会消费掉事件队列,键盘输入永远到不了 LVGL(现象:画面正常但按键全部无效)。 关窗退出由驱动LV_SDL_DIRECT_EXIT=1自动exit(0),主循环无需处理 SDL_QUIT。
五、边界说明
- 模拟器只跑 UI:不含 CH395F 网络、NAND 存储、ADC、RS-485 等硬件业务,
也不含
ui_net_sink(推流服务端)——远端推流链路只能在 MCU 上验证 build/为生成物,可随时删除重建(已在.gitignore中忽略)Lib/lvgl/CMakeLists.txt仅被本模拟器引用,不影响 Keil 的 MCU 编译