Files
STM32F4-Base/simulator/README.md
T
2026-09-09 13:18:06 +08:00

253 lines
14 KiB
Markdown
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.
# 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)
├── tests/ # 无头回归脚本 + 黄金参考(见 §3.6,流程文档 docs/UI 设计/UI_自动化测试流程.md)
│ ├── check_menu_rows.py # 菜单 8 状态像素回归 + 池基线门禁
│ ├── check_content_pages.py # 内容页黄金参考回归 + 开页-关页往返池门禁(--update 刷参考)
│ └── ref/ # 黄金参考截图 shot_{info,ac,dc}.bin(I1 原始位图,随仓库提交)
├── 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 |
| `tests/` | 无头回归脚本(菜单 8 状态像素回归、内容页黄金参考回归)+ 入库黄金参考截图。详见 §3.6 |
> **共享代码不复制**:`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 关键设计决策
1. **SDL2**:LVGL 官方 PC 端口同款显示方案;使用仓库内预编译 SDK(`SDL2/SDL2-2.32.10/`),无需系统安装
2. **CMake + MSVC**:`Visual Studio 16 2019` 生成器,`-A x64`;MSVC 下加 `/utf-8` 消除中文注释的 C4819 警告
3. **共享 UI 源码**:`ui_menu.c` / `ui_font_cn_12.c` 直接编译 `Drivers/BSP/UI/` 原件,改一处两端同时生效
4. **纯键盘导航**:不创建鼠标/触摸设备,与 MCU 端 keypad 操作语义一致(↑↓←→ / Enter / Esc)
5. **仅 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 构建命令
```powershell
# 首次(配置 + 构建)
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:
```powershell
cd simulator
# 1) 离屏渲染首帧,输出 I1(1bpp) 原始位图(已剥离 8 字节调色板,纯像素数据)
.\build\Debug\lvgl_sim.exe --screenshot shot.bin
# 2) 转 PNG(默认 160x160;--scale 放大便于查看;python 统一用仓库 .venv,见 AGENTS.md「Python 环境」)
..\.venv\Scripts\python.exe i1_to_png.py shot.bin # -> shot.png
..\.venv\Scripts\python.exe 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(已含于仓库 `.venv`,见 `requirements.txt`)。`shot.bin`/`shot*.png` 均已列入 `.gitignore`,可随时删除重生成。
### 3.6 无头回归测试(tests/)
UI 改动的自动化门禁脚本(完整流程见 `docs/UI 设计/UI_自动化测试流程.md`)。脚本纯标准库、任意 python3 均可,为环境统一一律用仓库 `.venv`:
```powershell
# 仓库根目录执行(前置:本目录已构建出 build/Debug/lvgl_sim.exe)
.venv\Scripts\python.exe simulator/tests/check_menu_rows.py # 菜单 8 状态像素回归 + 池基线门禁
.venv\Scripts\python.exe simulator/tests/check_content_pages.py # 内容页黄金参考回归 + 开页-关页往返池门禁
```
| 脚本 | 原理 | 失败输出 |
|------|------|----------|
| `check_menu_rows.py` | 键序注入(u/d/e/s)跑 8 个菜单状态,断言选中行白带、非选行黑底白字、框体边框、往返状态逐像素一致;S0 池水位须等于脚本内 `MENU_POOL_BASELINE` | 未过断言清单 + 保留临时截图 |
| `check_content_pages.py` | 三内容页(info=`e` / ac=`dee` / dc=`dede`)无头截图与 `tests/ref/shot_*.bin` **逐字节**比对;另校验开页-关页往返池水位 = `POOL_AFTER_ROUNDTRIP` | 差异像素数 + 双方 ink runs 对照 |
- **黄金参考更新**(有意改视觉后):先肉眼确认新效果正确,再
`.venv\Scripts\python.exe simulator/tests/check_content_pages.py --update` 刷新 `ref/`,随代码提交。
- **池水位常量漂移**:菜单/页面结构变更导致门禁失败时,确认新值合理后再更新脚本内常量并登记流程文档变更记录;无法解释的漂移 = 内存回归 bug。
- 退出码均为 0=PASS / 1=断言失败 / 2=环境错误(exe 缺失等)。
---
## 四、已知坑(构建/运行)
1. **`cannot open include file "SDL2/SDL.h"`**
LVGL SDL 驱动写死 `#include "SDL2/SDL.h"`,而原版 SDK 头文件平铺在 `include/` 下。
必须把头文件复制一份到 `include/SDL2/` 子目录(本仓库已做好):
```powershell
$inc = "simulator\SDL2\SDL2-2.32.10\include"
New-Item -ItemType Directory -Path "$inc\SDL2" -Force
Copy-Item "$inc\*.h" "$inc\SDL2\"
```
2. **`cannot open include file "lv_conf.h"`**
`LV_CONF_DIR` / `SDL2_INCLUDE_DIR` 两个 CMake 变量必须在 `add_subdirectory(lvgl)` **之前**定义(见 `CMakeLists.txt` 中的顺序注释),勿随意调整。
3. **运行后窗口全黑**,按序排查:
- `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`)
4. **中文显示空白**:字体 `ui_font_cn_12` 是子集字体(当前 97 汉字 + ASCII,共 193 字形),缺字渲染为空白。
补字流程见 `docs/UI 设计/UI_中文字体生成与配置.md`(`tools/font_gen/` 两步脚本)。
5. **改了 UI 代码但模拟器没变化**:确认改的是 `Drivers/BSP/UI/ui_menu.c`(共享原件)而非本目录副本(本目录无副本),然后重新 `cmake --build`。
6. **编码**:UI 源码为 UTF-8(带 BOM),MSVC 已加 `/utf-8`。**不要**用 PowerShell `Set-Content` 等重写含中文的文件(编码铁律,见根目录 `AGENTS.md`)。
7. **键盘输入无响应(方向键/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 编译