X-TRACK 开发指南:基于 Visual Studio 的 LVGL Windows PC 模拟器构建、运行与 HAL 仿真原理
X-TRACK 开发指南:基于 Visual Studio 的 LVGL Windows PC 模拟器构建、运行与 HAL 仿真原理
本文以
Software/X-Track/Simulator/目录下的官方 README 为骨架,结合 X-TRACK 仓库中的模拟器工程源码与配置,完整讲解这套基于 Visual Studio 的 LVGL PC 模拟器:如何克隆、构建、运行,以及 X-TRACK 码表应用如何在其上通过 HAL 仿真层实现 GPS 轨迹回放、时钟、电池、SD 卡等外设的“无硬件开发”体验。读完本文,你将掌握模拟器的完整使用流程,并理解其底层驱动与仿真机制,可直接用于 X-TRACK 固件的日常开发与调试。
X-TRACK 是一个支持离线地图与轨迹记录的 GPS 自行车码表项目,其 UI 基于 LVGL 构建。为了让开发者在不焊接硬件、不烧录固件的条件下快速迭代界面与业务逻辑,项目在 Software/X-Track/Simulator/ 目录下内置了一套预配置好的 Visual Studio 工程,把整套 LVGL 码表应用原封不动地搬到了 Windows 桌面窗口里运行。
一、模拟器是什么:一个零依赖的 Visual Studio 工程
按照官方 README 的说明(Software/X-Track/Simulator/README.md),这是一套为在 Windows PC 上体验 LVGL 而预配置的 Visual Studio 工程,其核心特点如下:
- 依赖极简:整个工程只依赖 Win32 API、C Runtime 与 C++ STL,不需要安装任何第三方 GUI 框架或额外的运行库,克隆后即可编译。
- 维护版本:工程目前基于 Visual Studio 2019 维护;理论上 Visual Studio 2017 也能编译,但官方不对此版本提供积极支持,因此遇到问题应先在 VS2019 下复现验证。
- 明确边界:该工程面向 Visual Studio 2019 的 MSVC 工具链,不适用于 Visual Studio Code;如果需要在 VS Code 或其他环境适配 LVGL Windows 应用,请参考 LVGL 官方的
lv_port_windows移植方案(本文不再赘述)。
版本沿革说明:该模拟器模板前身名为
lv_sim_visual_studio_sdl,后因不再依赖 SDL 而更名为lv_sim_visual_studio。X-TRACK 仓库将其作为子模块随项目一起维护,因此你在仓库内看到的目录结构即模板与 X-TRACK 定制内容的合集。
支持的功能特性
官方 README 列出了该模拟器模板支持的能力,结合仓库文件可逐条核实:
| 特性 | 说明 | 仓库佐证 |
|---|---|---|
| 仅依赖 Win32 API / C Runtime / C++ STL | 无第三方 GUI 依赖,可独立编译 | LVGL.Simulator.cpp 仅引入 <Windows.h>、lvgl、win32drv 等模块 |
| 原生支持 x86 / x64 / ARM / ARM64 | 四种架构 × Debug/Release 共 8 个目标 | BuildAllTargets.proj 中显式列出 8 组平台配置 |
| 支持 VC-LTL 工具链 | 可编译出接近 MinGW 体积的二进制 | Mile.Project.Cpp.VC-LTL.props |
| Per-monitor DPI Aware | 高分屏下窗口按显示器缩放 | 工程属性清单(LVGL.Simulator.manifest) |
| 支持 Windows 键盘与鼠标滚轮事件 | 在 HAL 层接入标准输入设备 | win32drv 驱动 + lv_win32_add_all_input_devices_to_group() 调用 |
二、获取源码:克隆与子模块管理
与许多依赖 LVGL 上游仓库的工程一样,模拟器工程通过 git submodule(子模块) 引用 lvgl、lv_drivers、lv_fs_if 等必要仓库。普通 git clone 不会自动拉取子模块,需要额外操作。官方 README 提供了三种方式:
方式一:一步到位克隆(含全部子模块)
git clone --recurse-submodules https://github.com/lvgl/lv_sim_visual_studio.git
方式二:先克隆主仓库,再补拉子模块
git clone https://github.com/lvgl/lv_sim_visual_studio.git
cd lv_sim_visual_studio
git submodule update --init --recursive
方式三:保持克隆与上游同步
在主仓库根目录依次执行两步——先拉主仓库更新(含子模块引用的变化),再让子模块代码跟进:
git pull
git submodule update --init --recursive
如果你 fork 了该仓库,从上游同步更新需要更复杂的流程(涉及上游 remote 与 rebase),本文不再展开。
就 X-TRACK 仓库而言,模拟器依赖的
lvgl、lv_drivers、lv_fs_if已以子目录形式随仓库一同分发(见 Software/X-Track/Simulator/LVGL.Simulator/ 下的lvgl/、lv_drivers/、lv_fs_if/),因此克隆 X-TRACK 仓库后即可直接打开工程。
三、构建与运行:从解决方案到启动窗口
3.1 图形界面方式(推荐开发日常使用)
官方 README 给出的标准流程如下:
- 用 Visual Studio 2019 打开解决方案文件 LVGL.Simulator.sln;
- 在“解决方案资源管理器”中把
LVGL.Simulator项目设为启动项目; - 点击工具栏上的 “Local Windows Debugger”(本地 Windows 调试器) 按钮;
- 工程会被自动编译并在一个 cmd 窗口中启动模拟器窗口。
窗口出现后即可用鼠标模拟触摸/点击、用键盘与滚轮模拟编码器输入,直接操作 X-TRACK 的整套码表 UI。
3.2 命令行方式:一键批量构建全部目标
仓库额外提供了命令行批量构建脚本 BuildAllTargets.cmd,其工作流程如下:
- 通过
vswhere自动定位 Visual Studio 安装目录; - 调用
vcvarsall.bat x86初始化 MSVC 构建环境; - 执行
MSBuild -m BuildAllTargets.proj,并行构建 BuildAllTargets.proj 中声明的全部 8 个目标:Debug/Release × x86/x64/ARM/ARM64。
该脚本适合 CI 或需要一次性产出多平台二进制(例如为 ARM64 平板准备模拟器版本)的场景。日常单平台调试仍建议直接用 3.1 节的 IDE 流程。
四、入口与主循环:模拟器里跑起来的码表应用
打开 LVGL.Simulator.cpp,可以看到模拟器的完整启动链路(main 函数,L44-L76):
#define SCREEN_HOR_RES 240
#define SCREEN_VER_RES 240
int main()
{
lv_init(); // 1. 初始化 LVGL 内核
lv_fs_if_init(); // 2. 注册文件系统接口(映射到 PC 目录)
lv_win32_init(..., SW_SHOW, SCREEN_HOR_RES, SCREEN_VER_RES, ...); // 3. 创建 240x240 模拟窗口
lv_win32_add_all_input_devices_to_group(NULL); // 4. 键盘/鼠标/滚轮接入 LVGL group
HAL::HAL_Init(); // 5. 初始化模拟 HAL(蜂鸣器、音频、GPS)
App_Init(); // 6. 启动 X-TRACK 码表应用
while (!lv_win32_quit_signal) // 7. 主循环
{
lv_timer_handler(); // LVGL 定时器/渲染
HAL::HAL_Update(); // 模拟外设数据更新(IMU/MAG/音频)
Sleep(1);
}
App_Uninit(); // 8. 退出清理
return 0;
}
几个值得注意的实现细节:
- 窗口分辨率与真机一致:
SCREEN_HOR_RES/SCREEN_VER_RES均为 240,与 X-TRACK 真机屏幕分辨率一致,保证 UI 布局、字体、触控热区在模拟器与真机间完全对齐。 - 模拟器侧不再运行官方 demo:README 中提到的
lv_demo_widgets等测试应用是上游模板的默认行为;在 X-TRACK 仓库中,main()已被替换为HAL::HAL_Init()+App_Init(),直接启动真实的码表应用(含 Dialplate 表盘、LiveMap 地图、Startup 开机动画、SystemInfos 系统信息等页面,见 Software/X-Track/USER/App/)。如果你想改回官方 demo 或加入自己的测试代码,可在此处替换App_Init()为相应 demo 函数。 - LVGL 配置对齐 PC 平台:模拟器使用独立的 lv_conf.h,其中
LV_TICK_CUSTOM在_WIN32下通过timeGetTime()提供毫秒级系统时钟(L102-L105),LV_DISP_DEF_REFR_PERIOD为 16ms、LV_INDEV_DEF_READ_PERIOD为 30ms,与真机保持一致以复现相同的刷新节奏。
五、HAL 仿真层:在 PC 上模拟整个码表外设
X-TRACK 的硬件抽象层(HAL)在模拟器中有独立实现,位于 LVGL.Simulator/HAL/,由 HAL.h 统一暴露与真机相同的接口。从 HAL.cpp 可看到初始化与周期更新各包含哪些模块:
void HAL::HAL_Init() { Buzz_init(); Audio_Init(); GPS_Init(); }
void HAL::HAL_Update() { IMU_Update(); MAG_Update(); Audio_Update(); }
各仿真模块的行为与真机的差异如下:
| 模块 | 模拟器行为 | 实现文件 |
|---|---|---|
| 时钟 Clock | 直接读取 PC 系统时间(time()/localtime()),Clock_SetInfo 仅打印设置日志 |
HAL_Clock.cpp |
| 电池 Power | 通过 Win32 GetSystemPowerStatus() 读取 PC 电源状态作为电量与充电状态,电压固定模拟为 3700mV |
HAL_Power.cpp |
| SD 卡 | 恒返回就绪,容量模拟为 32MB 的 SDHC 卡(32 * 1024 MB 即为 32GB 显示值),插入/拔出事件为空实现 |
HAL_SD_CARD.cpp |
| 编码器 Encoder | 空实现(返回 0/False),PC 上的交互由鼠标与键盘经 lv_win32 驱动接管 |
HAL_Encoder.cpp |
| GPS | 核心仿真:回放 GPX 轨迹文件,详见下文 | HAL_GPS.cpp |
模拟器上的 SD 卡文件读写并非真的空转:LVGL 文件系统层通过
lv_fs_if的 PC 驱动把/根路径映射到工程根目录(见 lv_fs_if.h 与 lv_fs_pc.c),路径拼接规则为sprintf(buf, LV_FS_PC_PATH "/%s", path),LV_FS_PC_PATH默认为../../../../(指向仓库根目录)。因此把地图瓦片、SystemSave.json、GPX 轨迹文件放到仓库根目录的对应位置,模拟器就能像读真机 SD 卡一样访问它们——这是无硬件预览离线地图功能的关键前提。
六、GPS 轨迹回放:最值得研究的模拟机制
模拟器最亮眼的仿真当属 GPS 模块。它不再“随机乱动”,而是解析一份 GPX 轨迹文件,按真实轨迹点顺序驱动码表的速度、航向与位置,让 LiveMap 页面能像真实骑行一样画出完整轨迹。
6.1 数据来源与刷新节奏
HAL_GPS.cpp 中的关键定义:
#define CONFIG_TRACK_VIRTUAL_GPX_FILE_PATH "/TRK_EXAMPLE.gpx"
GPS_Init()通过lv_fs_open打开该 GPX 文件,解析成功则置isVaild = true,并模拟出 10 颗可见卫星(satellites = 10);- 随后创建一个 LVGL 定时器,按
CONFIG_GPS_REFR_PERIOD周期调用GPS_Update()推进轨迹点; - 刷新周期定义在 Software/X-Track/USER/App/Config/Config.h:真机(ARDUINO)为 1000ms,PC 模拟器为 10ms——即模拟器以 100 倍频率快速“跑完”整条轨迹,方便快速预览全程效果。
6.2 逐点推进与速度、航向计算
GPS_Update()(HAL_GPS.cpp L166-L219)的核心逻辑:
- 调用
gpxParser.ReadNext(&point)读取下一个轨迹点; - 首次读到有效经纬度时直接作为当前定位(
isReset复位逻辑); - 之后每个点计算与上一位置的大圆距离
distanceBetween(),结合时间差diffTime换算速度并乘以 3.6 转为 km/h:gpsInfo.speed = (float)(distance / diffTime) * 3.6f; - 用
courseTo()依据两点经纬度计算航向角(北为 0°,顺时针),写入gpsInfo.course; - 同步更新经纬度与海拔(
longitude/latitude/altitude); - 读到文件末尾(
PARSER_FLAG_EOF)时把文件指针复位到开头、重置isReset,轨迹循环回放。
时间差有两个来源:若 GPX 点带时间戳,则用 Clock_GetDiffTime() 计算两点真实时间差(difftime/mktime,对应更新日志中“支持获取 GPX 点之间的时间差”);否则回退到固定刷新周期 CONFIG_GPS_REFR_PERIOD / 1000.0 秒。diffTime 过小(< 0.0001)时保留上次速度,避免除零产生 inf(对应更新日志 v1.7 中“修复 diffTime = 0 导致 speed = inf”)。
6.3 默认定位与可配置项
当 GPX 解析失败或尚未定位时,模拟器使用 Config.h 中的默认经纬度作为初始位置:
#define CONFIG_GPS_LONGITUDE_DEFAULT 116.391332f // 北京天安门附近
#define CONFIG_GPS_LATITUDE_DEFAULT 39.907415f
这两项与 DP_SysConfig 中系统配置的默认经纬度一致(见 DP_SysConfig.cpp),即开机后未定位前 LiveMap 默认显示的位置。
6.4 准备你自己的回放轨迹
想要用真实骑行数据测试,只需:
- 将任意 GPX 轨迹文件命名为
TRK_EXAMPLE.gpx; - 放置到
LV_FS_PC_PATH指向的仓库根目录(与TRK_EXAMPLE.gpx对应的实际路径由模拟器文件系统根目录决定,即仓库根目录下的Software/同级的根路径); - 重启模拟器,
GPS_Init()解析成功后码表即开始沿轨迹运动。
轨迹点带时间戳时回放会严格遵循真实时间间隔;不带时间戳时则以 10ms 固定节奏匀速推进。这正是 X-TRACK 更新日志中“HAL_GPS 支持读取 csv 格式文件在 PC 上模拟位置变化”“支持航向和速度模拟”的最终实现形态。
七、版本与同步注意事项
官方 README 的最后一节提醒了子模块版本维护策略,结合仓库现状可总结为以下三点:
- 子模块引用更新时机:模板仓库会在 lvgl 发布新的主版本后,为子模块引用打上匹配的版本标签并更新;日常的小版本更新也会被跟进(本仓库当前锁定 lvgl v8.1.1-dev,见 lv_conf.h 文件头注释)。
- 自行拉取子模块修复:如果上游子模块有你需要的新修复,需自行
git submodule update更新引用;若上游新增/删除了源文件,VS 工程(.vcxproj)通常需要同步调整文件列表,否则会编译失败——这一点从仓库历次“同步 lvgl 主程序 + 调整工程”的提交记录可以佐证。 - 对本仓库的实操意义:X-TRACK 开发者日常应保持
git pull后再git submodule update --init --recursive,确保lvgl等子模块与主仓库引用的 commit 一致,避免“主仓库已更新、子模块仍是旧代码”导致的链接或行为差异。
八、小结
这套基于 Visual Studio 的 LVGL PC 模拟器为 X-TRACK 提供了完整的无硬件开发闭环:
- 工程层面:一个零第三方依赖的 VS2019 解决方案,支持 x86/x64/ARM/ARM64 多平台构建,图形界面与 BuildAllTargets.cmd 命令行两种构建方式并行;
- 应用层面:
App_Init()直接启动真实码表应用,240×240 窗口与真机分辨率一致,鼠标/键盘/滚轮完整映射输入; - 外设层面:HAL 仿真层让时钟、电池、SD 卡、GPS 全部可用 PC 资源或文件模拟——尤其 GPS 通过回放 GPX 轨迹提供真实的速度、航向与位置变化,配合
LV_FS_PC_PATH的目录映射,离线地图与轨迹记录功能无需任何硬件即可完整预览与调试。
对希望参与 X-TRACK 开发的工程师而言,掌握本模拟器即可在 PC 上完成绝大多数 UI 与业务逻辑的开发验证;只有涉及真实传感器、编码器手感、背光与功耗等硬件耦合环节时,才需要回到 AT32F403A / AT32F435 真机平台(MDK-ARM_F403A、MDK-ARM_F435)验证。
