使用SDL2搭建简易LVGL模拟器
IDE: CLion
编译器:gcc 14.2
语言:C++20
引言:开发单片机GUI时,不可能每次更新界面时通过烧录来确定更新情况,这种做法既费时又费力。于是就需要引入模拟器,通过上位机可以快速自然地看到开发效果。虽然LVGL已经有了多种模拟器软件而且做得已经相当不错了,如GUI Guider、SquareLine Studio等,但对于一些情况仍有一定限制,比如lvgl版本(截止目前,GUI Guider最新版本只支持8.3.1)、屏幕尺寸限制(GUI Guider仅支持有限尺寸的几种屏幕)。基于此,自行搭建一个简易模拟器,前期会比较耗时且上手较难。但对于学习LVGL的API、LVGL驱动接口、上位机开发还是很有帮助的,且更加自由。总之,各有优缺点,可根据情况选择或尝试。
一、原理简介
LVGL是跨平台的,因此无论是单片机的arm32环境,还是PC机的x86_x64环境,都可以使用一份代码完成基本相同的界面效果,这为模拟器的搭建提供了非常有力的支持。
对于单片机使用LVGL,如果仅显示界面的情况下,我们是在lv_port_disp.c中给disp_flush这个函数提供了一个接口如LCD_Color_Fill函数,使其可以控制LCD的界面绘制。基于此,只要我们创建一个窗口,把绘制窗口的接口提供给disp_flush,那么即可实现同样的绘制效果。
更进一步的考虑,我们在单片机上提供LCD_Color_Fill供LVGL使用,其实是把LCD显存的控制权交给了LVGL,同样的,在上位机中我们只有把窗口“显存”的控制权交给LVGL即可。而窗口的创建有多种实现方法,这里我们使用SDL2库,这可以帮助我们快速完成窗口的搭建。
二、搭建SDL框架
1,前置工作
事先说明,前置工作准备中可能会有一些知识需要自行学习一下,此处不会做过多介绍
先在CLion中创建一个工程,工具链就使用自带的MinGW
使用SDL2创建窗口,需要先安装SDL2库,而安装第三方库,我们可以使用Vcpkg这个包管理器。这个需要浅浅地学习一下基本用法,快速学习一门新工具的使用是一项基本功能,有许多博客可以参考。
此外需要说明的是Vcpkg下载安装很慢,需要么换源要么开代理,后者可以使用如下软件,开源且转为git提供代理、加速功能的,DevSidecar下载链接。
不过可能会出现一个悖论:想要使用github下载就需要先安装DevSidecar,而要安装DevSidecar就需要先能使用github下载。我的建议是,凭运气,有些时候可以进入github下载,晚上效果或许会好一些。
使用该软件,如果未关闭下图中的三个服务就退出,会导致你无法打开浏览器界面,重新进入该软件,重新关闭三个服务再退出即可。
使用Vcpkg下载库时,有些时候会卡住不动,即使开了git的代理,那么可以换个时间段重新试试。如果Vcpkg使用顺利的话,安装SDL2仍会出现很多问题,总得来说是,C++的包管理功能和第三方库的生态还不够完善。SDL2的配置可以参考博客_使用CLion的Vcpkg安装SDL2,添加至CMakelists时报错,编译报错-CSDN博客
2,基本框架创建
在前置工作准备完毕之后,就可以正式编写代码了。
①窗口搭建
SDL的窗口搭建只有四个步骤,SDL初始化、窗口创建、渲染器创建、纹理创建。
先定义下面四个全局变量(记得包含头文件SDL.h)
SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *texture; static uint32_t tft_fb[480 * 320];// 窗口的“显存”,目前尺寸设为480*320
然后编写一个函数hal_init(名称随意)
void hal_init() { // 初始化 SDL if (SDL_Init(SDL_INIT_VIDEO) != 0) { std::cerr << "Unable to initialize SDL: " << SDL_GetError() << std::endl; keep_running = false; return; } // 创建窗口 window = SDL_CreateWindow("LVGL Simulator", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 480, 320, 0); // 创建渲染器(此处使用硬件加速,可以使用软件加速SDL_RENDERER_SOFTWARE) renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); // 创建纹理 texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 480, 320); if (!window || !renderer || !texture) { std::cerr << "Failed to create window/renderer/texture: " << SDL_GetError() << std::endl; SDL_Quit(); keep_running = false; return; } }
②事件处理
如果只是创建窗口,那么程序运行后就会发生“程序未响应”的错误,这是因为SDL2里有各种事件需要处理,如果不处理的话,自然就是程序未响应。
我们需要实现一个事件循环,这里为了方便退出,可以定义一个全局变量
volatile int keep_running = 1;// volatile防止编译器优化
把死循环的while(1)替换为下面即可,这my_SDL_event_Handler()就是定义的事件循环里
while (keep_running) { my_SDL_event_Handler(); // 短暂休眠 SDL_Delay(10); }
我们需要实现它,里面可以不进行任何处理,只是把事件从SDL的事件队列中取出来而已。
static void my_SDL_event_Handler() { static SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_QUIT: keep_running = false; break; case SDL_WINDOWEVENT: switch (event.window.event) { case SDL_WINDOWEVENT_TAKE_FOCUS: case SDL_WINDOWEVENT_EXPOSED: default: break; } default: break; } }
③添加“显存”
在步骤①中,我们定义了tft_fb这个“显存”。而SDL刷新界面是需要手动更新的,我们可以定义一个线程,每隔一段时间把“显存”中的数据更新到SDL中
SDL_CreateThread(updateSDLGram , "updateSDLGram",nullptr);
int updateSDLGram(void* data) { while (keep_running) { SDL_UpdateTexture(texture, nullptr, tft_fb, 480 * sizeof(uint32_t)); SDL_RenderClear(renderer); SDL_RenderCopy(renderer, texture, nullptr, nullptr); SDL_RenderPresent(renderer); SDL_Delay(10); } return 1; }
此时你可以通过改变tft_fb数组的内容,来实现窗口界面的变化。
④演示与完整代码
为了便于展示效果,我们可以定义一个随机数,让它在主循环中更新tft_fb。
// 更新像素数据 for (int i = 0; i < 480 * 320; i++) tft_fb[i] = std::rand() + 2 * i; // 你可以在这里生成更复杂的颜色值
最终效果如下(干涉条纹是吧):
完整代码:
#include <iostream> #include <SDL.h> volatile int keep_running = 1; static void my_SDL_event_Handler(); int updateSDLGram(void* data); SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *texture; static uint32_t tft_fb[480 * 320]; void hal_init(); int main(int argc, char *argv[]) { hal_init(); SDL_CreateThread(updateSDLGram , "updateSDLGram",nullptr); while (keep_running) { my_SDL_event_Handler(); // 更新像素数据 for (int i = 0; i < 480 * 320; i++) tft_fb[i] = std::rand() + 2 * i; // 你可以在这里生成更复杂的颜色值 // 短暂休眠 SDL_Delay(10); } // 清理资源 SDL_DestroyTexture(texture); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; } static void my_SDL_event_Handler() { static SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_QUIT: keep_running = false; break; case SDL_WINDOWEVENT: switch (event.window.event) { case SDL_WINDOWEVENT_TAKE_FOCUS: case SDL_WINDOWEVENT_EXPOSED: default: break; } default: break; } } } void hal_init() { // 初始化 SDL if (SDL_Init(SDL_INIT_VIDEO) != 0) { std::cerr << "Unable to initialize SDL: " << SDL_GetError() << std::endl; keep_running = false; return; } // 创建窗口 window = SDL_CreateWindow("LVGL Simulator", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 480, 320, 0); // 创建渲染器(此处使用硬件加速,可以使用软件加速SDL_RENDERER_SOFTWARE) renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); // 创建纹理 texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 480, 320); if (!window || !renderer || !texture) { std::cerr << "Failed to create window/renderer/texture: " << SDL_GetError() << std::endl; SDL_Quit(); keep_running = false; return; } } int updateSDLGram(void* data) { while (keep_running) { SDL_UpdateTexture(texture, nullptr, tft_fb, 480 * sizeof(uint32_t)); SDL_RenderClear(renderer); SDL_RenderCopy(renderer, texture, nullptr, nullptr); SDL_RenderPresent(renderer); SDL_Delay(10); } return 1; }
三、绑定LVGL
1,整理
后续要添加lvgl库,为了让代码更简洁规范一些,需要先整理一下。可以再创建一个simulator.hpp/cpp,用于把前面的SDL框架给封装起来
main.cpp
#include "simulator.hpp" int main(int argc, char *argv[]) { // 模拟器初始化 simulator_init(argc, argv); // 主循环 while (simulator_is_running()) { simulator_event_Handler(); } // 清理资源 simulator_quit(); return 0; }
simulator.hpp
// // Created by fairy on 2025/1/1 15:21. // #ifndef LVGL_SIMULATOR_SIMULATOR_HPP #define LVGL_SIMULATOR_SIMULATOR_HPP void simulator_init(int argc, char *argv[]); void simulator_quit(); void simulator_event_Handler(); const bool simulator_is_running(); #endif //LVGL_SIMULATOR_SIMULATOR_HPP
simulator.cpp
// // Created by fairy on 2025/1/1 15:21. // #include "simulator.hpp" #include <SDL.h> #include <iostream> volatile int keep_running = 1; SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *texture; static uint32_t tft_fb[480 * 320]; const bool simulator_is_running(){return keep_running;} /** * @brief 初始化 SDL * @note 初始化了 SDL 窗口、渲染器、纹理,并创建了一个更新纹理的线程 */ void simulator_init(int argc, char *argv[]) { // 忽略参数 (void)argc, (void)argv; // 初始化 SDL if (SDL_Init(SDL_INIT_VIDEO) != 0) { std::cerr << "Unable to initialize SDL: " << SDL_GetError() << std::endl; keep_running = false; return; } // 创建窗口 window = SDL_CreateWindow("LVGL Simulator", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 480, 320, 0); // 创建渲染器(此处使用硬件加速,可以使用软件加速SDL_RENDERER_SOFTWARE) renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); // 创建纹理 texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 480, 320); if (!window || !renderer || !texture) { std::cerr << "Failed to create window/renderer/texture: " << SDL_GetError() << std::endl; SDL_Quit(); keep_running = false; return; } // 创建更新纹理的线程 SDL_CreateThread([](void *) { while (keep_running) { SDL_UpdateTexture(texture, nullptr, tft_fb, 480 * sizeof(uint32_t)); SDL_RenderClear(renderer); SDL_RenderCopy(renderer, texture, nullptr, nullptr); SDL_RenderPresent(renderer); SDL_Delay(10); } return 1; }, "updateSDLGram", nullptr); } /** * @brief 退出 SDL */ void simulator_quit() { SDL_DestroyTexture(texture); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); } /** * @brief 处理 SDL 事件 */ void simulator_event_Handler() { static SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_QUIT: keep_running = false; break; case SDL_WINDOWEVENT: switch (event.window.event) { case SDL_WINDOWEVENT_TAKE_FOCUS: case SDL_WINDOWEVENT_EXPOSED: default: break; } default: break; } } // 短暂休眠 SDL_Delay(10); }
此时,还需要改一下CMakelists
cmake_minimum_required(VERSION 3.30) project(lvgl_simulator) set(CMAKE_CXX_STANDARD 20) # ---------------------定义资源文件----------------------- file(GLOB_RECURSE SOURCE "simulator.cpp" ) # main.cpp必须放在这里,而不是SOURCE,除非你没有把Vcpkg放在该工程目录里,否则会把所有main.cpp都包含进来 add_executable(lvgl_simulator main.cpp ${SOURCE}) # ---------------------链接第三方库----------------------- # 前面必须加一个CMAKE_PREFIX_PATH,设置SDL_PATH并没有任何用,我就一句话,艹 set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} Vcpkg/packages/sdl2_x64-mingw-dynamic) find_package(SDL2 CONFIG REQUIRED) target_link_libraries(lvgl_simulator PUBLIC $<TARGET_NAME_IF_EXISTS:SDL2::SDL2main> $<IF:$<TARGET_EXISTS:SDL2::SDL2>,SDL2::SDL2,SDL2::SDL2-static> ) # 确保 SDL2.dll 复制到输出目录 configure_file(Vcpkg/packages/sdl2_x64-mingw-dynamic/bin/SDL2.dll ${CMAKE_CURRENT_BINARY_DIR}/SDL2.dll COPYONLY) configure_file(Vcpkg/packages/sdl2_x64-mingw-dynamic/debug/bin/SDL2d.dll ${CMAKE_CURRENT_BINARY_DIR}/SDL2d.dll COPYONLY)
2,对接LVGL
从github上下载lvgl,可以选择最新的版本,在当前目录下把lvgl_x.x放进来,并重命名为lvgl
然后就是在CMakelists里,包含对应的一系列源文件、头文件、目录,这与单片机中移植LVGL没有任何区别,所以这里仅做简单的介绍
①整理
lv_conf_template.h 更名为lv_conf.h,并把里面的预编译指令改为1
删除无关目录和文件(也可以不删),仅保留examples、src目录和一些头文件
把要用到的资源文件添加进CMakelists中
cmake_minimum_required(VERSION 3.30) project(lvgl_simulator) set(CMAKE_CXX_STANDARD 20) # ---------------------定义资源文件----------------------- # -----lvgl库----- set(LVGL_DIR lvgl/../ lvgl/src lvgl/examples/porting ) file(GLOB_RECURSE LVGL_SRC "lvgl/src/*.c" "lvgl/examples/porting/*.c" ) # ---------------------包含资源文件----------------------- file(GLOB_RECURSE SOURCE "simulator.cpp" # LVGL库 ${LVGL_SRC} ) include_directories( ${LVGL_DIR} ) # main.cpp必须放在这里,而不是SOURCE,除非你没有把Vcpkg放在该工程目录里,否则会把所有main.cpp都包含进来 add_executable(lvgl_simulator main.cpp ${SOURCE}) # ---------------------链接第三方库----------------------- # 前面必须加一个CMAKE_PREFIX_PATH,设置SDL_PATH并没有任何用,我就一句话,艹 set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} Vcpkg/packages/sdl2_x64-mingw-dynamic) find_package(SDL2 CONFIG REQUIRED) target_link_libraries(lvgl_simulator PUBLIC $<TARGET_NAME_IF_EXISTS:SDL2::SDL2main> $<IF:$<TARGET_EXISTS:SDL2::SDL2>,SDL2::SDL2,SDL2::SDL2-static> ) # 确保 SDL2.dll 复制到输出目录 configure_file(Vcpkg/packages/sdl2_x64-mingw-dynamic/bin/SDL2.dll ${CMAKE_CURRENT_BINARY_DIR}/SDL2.dll COPYONLY) configure_file(Vcpkg/packages/sdl2_x64-mingw-dynamic/debug/bin/SDL2d.dll ${CMAKE_CURRENT_BINARY_DIR}/SDL2d.dll COPYONLY)
② 添加显示驱动
回到目录中,我们可以看到porting中的文件,改为lv_port_disp.h/c
然后预编译置为1,把长度和宽度改为自己需要的屏幕尺寸
在simulator.hpp中,我们添加一个模拟LCD_Color_Fill的颜色块填充函数(记得用extern "C"括起来)
void LCD_Set_Pixel(uint16_t x, uint16_t y, uint32_t color) { tft_fb[y * 480 + x] = color; } // 只使用全缓冲区 void LCD_Color_Fill(uint16_t xsta, uint16_t ysta, uint16_t xend, uint16_t yend, uint32_t *color) { for (int i = ysta; i <= yend; i++) { for (int j = xsta; j <= xend; j++) { LCD_Set_Pixel(j, i, color[j + i * xend]); } } }
不考虑低耦合什么的,直接引用simulator.hpp头文件,并调用该函数
修改lv_port_disp_init函数,单缓冲、双缓冲随意,注释删不删也随意
void lv_port_disp_init(void) { /*------------------------- * Initialize your display * -----------------------*/ disp_init(); /*------------------------------------ * Create a display and set a flush_cb * -----------------------------------*/ lv_display_t *disp = lv_display_create(MY_DISP_HOR_RES, MY_DISP_VER_RES); lv_display_set_flush_cb(disp, disp_flush); // /* Example 1 // * One buffer for partial rendering*/ // LV_ATTRIBUTE_MEM_ALIGN // static uint8_t buf_1_1[MY_DISP_HOR_RES * 10 * BYTE_PER_PIXEL]; /*A buffer for 10 rows*/ // lv_display_set_buffers(disp, buf_1_1, NULL, sizeof(buf_1_1), LV_DISPLAY_RENDER_MODE_PARTIAL); /* Example 2 * Two buffers for partial rendering * In flush_cb DMA or similar hardware should be used to update the display in the background.*/ LV_ATTRIBUTE_MEM_ALIGN static uint8_t buf_2_1[MY_DISP_HOR_RES * 20 * BYTE_PER_PIXEL]; LV_ATTRIBUTE_MEM_ALIGN static uint8_t buf_2_2[MY_DISP_HOR_RES * 20 * BYTE_PER_PIXEL]; lv_display_set_buffers(disp, buf_2_1, buf_2_2, sizeof(buf_2_1), LV_DISPLAY_RENDER_MODE_PARTIAL); // /* Example 3 // * Two buffers screen sized buffer for double buffering. // * Both LV_DISPLAY_RENDER_MODE_DIRECT and LV_DISPLAY_RENDER_MODE_FULL works, see their comments*/ // LV_ATTRIBUTE_MEM_ALIGN // static uint8_t buf_3_1[MY_DISP_HOR_RES * MY_DISP_VER_RES * BYTE_PER_PIXEL]; // // LV_ATTRIBUTE_MEM_ALIGN // static uint8_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES * BYTE_PER_PIXEL]; // lv_display_set_buffers(disp, buf_3_1, buf_3_2, sizeof(buf_3_1), LV_DISPLAY_RENDER_MODE_DIRECT); }
simulator.hpp
// // Created by fairy on 2025/1/1 15:52. // #ifndef LVGL_SIMULATOR_SIMULATOR_HPP #define LVGL_SIMULATOR_SIMULATOR_HPP #ifdef __cplusplus #include <cstdint> #endif void simulator_init(int argc, char *argv[]); void simulator_quit(); void simulator_event_Handler(); bool simulator_is_running(); /*****************/ #ifdef __cplusplus extern "C" { #endif void LCD_Set_Pixel(uint16_t x, uint16_t y, uint32_t color); void LCD_Color_Fill(uint16_t xsta, uint16_t ysta, uint16_t xend, uint16_t yend, uint32_t *color); #ifdef __cplusplus } #endif #endif //LVGL_SIMULATOR_SIMULATOR_HPP
simulator.cpp
// // Created by fairy on 2025/1/1 15:52. // #include "simulator.hpp" #include <SDL.h> #include <iostream> static volatile int keep_running = 1; static SDL_Window *window; static SDL_Renderer *renderer; static SDL_Texture *texture; static uint32_t tft_fb[480 * 320]; bool simulator_is_running(){return keep_running;} /** * @brief 初始化 SDL * @note 初始化了 SDL 窗口、渲染器、纹理,并创建了一个更新纹理的线程 */ void simulator_init(int argc, char *argv[]) { // 忽略参数 (void)argc, (void)argv; // 初始化 SDL if (SDL_Init(SDL_INIT_VIDEO) != 0) { std::cerr << "Unable to initialize SDL: " << SDL_GetError() << std::endl; keep_running = false; return; } // 创建窗口 window = SDL_CreateWindow("LVGL Simulator", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 480, 320, 0); // 创建渲染器(此处使用硬件加速,可以使用软件加速SDL_RENDERER_SOFTWARE) renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); // 创建纹理 texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 480, 320); if (!window || !renderer || !texture) { std::cerr << "Failed to create window/renderer/texture: " << SDL_GetError() << std::endl; SDL_Quit(); keep_running = false; return; } // 创建更新纹理的线程 SDL_CreateThread([](void *) { while (keep_running) { SDL_UpdateTexture(texture, nullptr, tft_fb, 480 * sizeof(uint32_t)); SDL_RenderClear(renderer); SDL_RenderCopy(renderer, texture, nullptr, nullptr); SDL_RenderPresent(renderer); SDL_Delay(10); } return 1; }, "updateSDLGram", nullptr); } /** * @brief 退出 SDL */ void simulator_quit() { SDL_DestroyTexture(texture); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); } /** * @brief 处理 SDL 事件 */ void simulator_event_Handler() { static SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_QUIT: keep_running = false; break; case SDL_WINDOWEVENT: switch (event.window.event) { case SDL_WINDOWEVENT_TAKE_FOCUS: case SDL_WINDOWEVENT_EXPOSED: default: break; } default: break; } } // for (int i = 0; i < 480 * 320; i++) // tft_fb[i] = std::rand() + 2 * i; // 你可以在这里生成更复杂的颜色值 // 短暂休眠 SDL_Delay(10); } /*****************************************************************************/ void LCD_Set_Pixel(uint16_t x, uint16_t y, uint32_t color) { tft_fb[y * 480 + x] = color; } // 只适用满缓冲区 void LCD_Color_Fill(uint16_t xsta, uint16_t ysta, uint16_t xend, uint16_t yend, uint32_t *color) { for (int i = ysta; i <= yend; i++) { for (int j = xsta; j <= xend; j++) { LCD_Set_Pixel(j, i, color[j + i * xend]); } } }
③引入lvgl
如单片机一样,这里也需要添加lv_init()和lv的时钟lv_tick_inc等
main.cpp
#include <iostream> #include <SDL.h> #include "simulator.hpp" #include "lvgl.h" #include "lv_port_disp.h" int lv_tick_thread(void *data); int main(int argc, char *argv[]) { // 模拟器初始化 simulator_init(argc, argv); // lvgl初始化 lv_init(); lv_port_disp_init(); // lv_tick线程 SDL_CreateThread(lv_tick_thread, "lv_tick", nullptr); // 主循环 while (simulator_is_running()) { // 模拟器事件处理 simulator_event_Handler(); // lvgl事件处理 lv_timer_handler(); } // 清理资源 simulator_quit(); return 0; } /* 初始化时间滴答 * 需要定期调用 'lv_tick_inc()' 以告知 LittlevGL 自上次调用以来经过了多少时间 * 创建一个名为 "tick" 的 SDL 线程,该线程将定期调用 'lv_tick_inc()' */ int lv_tick_thread(void *data) { (void) data; /* 忽略传递的参数,因为在这个函数中不需要使用它 */ while (simulator_is_running()) { SDL_Delay(5); /* 休眠 5 毫秒 */ lv_tick_inc(5); /* 告诉 LittlevGL 已经过去了 5 毫秒 */ } return 0; }
如果此时你运行,会发现窗口的打印会相当奇怪,其一是因为RGB编码不同,单片机一般使用RGB565,而上位机一般使用ARGB8888。其二是因为,前面的LCD涂色函数原本是针对全缓冲区写的,而此处为了尽可可能模拟单片机,使用的并非全缓冲区。
故此时需要对LCD函数进行一些改进。编码问题,可以新建函数适配RGB565编码或者直接把SDL的纹理编码换为RGB565
uint32_t rgb565_to_rgb8888(uint16_t &color) { uint8_t r = ((color >> 11) & 0x1F) * 8; uint8_t g = ((color >> 5) & 0x3F) * 4; uint8_t b = (color & 0x1F) * 8; return (r << 16) | (g << 8) | b; } void LCD_Set_Pixel(uint16_t x, uint16_t y, uint16_t color) { tft_fb[x + y * 480] = color; } void LCD_Color_Fill(uint16_t xsta, uint16_t ysta, uint16_t xend, uint16_t yend, const uint16_t *color) { uint16_t width = xend - xsta + 1; uint16_t height = yend - ysta + 1; uint32_t row_bytes = width * sizeof(uint16_t); auto *tft_ptr = (tft_fb+xsta+ysta * 480); auto *color_ptr = color; for (int y = 0; y < height; ++y) { memcpy(tft_ptr + y * width, color_ptr+y * width,row_bytes); } } void LCD_Color_Clean(uint16_t xsta, uint16_t ysta, uint16_t xend, uint16_t yend, uint16_t color) { uint16_t width = xend - xsta + 1; uint16_t height = yend - ysta + 1; uint32_t row_bytes = width * sizeof(uint16_t); auto *tft_ptr = (tft_fb+xsta+ysta * 480); for (int y = 0; y < height; ++y) { memset(tft_ptr + y * width, color, row_bytes); } } void LCD_Clear(uint16_t color) { LCD_Color_Clean(0, 0, 480 - 1, 320 - 1, color); }
不过还是切换纹理更加方便
texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STREAMING, 480, 320);
四、演示与完整代码
使用LVGL的API创建一个简单的控件
实验效果如下,这表明可以正常使用lvgl的API进行界面开发了。后续可如单片机开发一样,只调用lvgl的API即可,不需要调用SDL的API,因为底层已经搭建好了。更推荐的是,把该工程嵌入到单片机工程中,这样就可以确保模拟的与实际烧录到单片机的效果是一样的(除了性能),且由于共享同份界面代码,故不必搬移代码。
由于本工程用于测试模拟器搭建的可行性,故没有上传到github上。对于触摸等功能可以通过映射的方式来模拟,比如触摸可用鼠标点击事件来模拟。更多功能可自行参考GUI Guider生成的工程,比如monitor.c文件。
CMakelists:
cmake_minimum_required(VERSION 3.30) project(lvgl_simulator C CXX) set(CMAKE_CXX_STANDARD 20) add_compile_options(-O2 -g) # ---------------------定义资源文件----------------------- # -----lvgl库----- set(LVGL_DIR lvgl/../ lvgl/src lvgl/examples/porting ) file(GLOB_RECURSE LVGL_SRC "lvgl/src/*.c" "lvgl/examples/porting/*.c" ) # ---------------------包含资源文件----------------------- file(GLOB_RECURSE SOURCE "simulator.cpp" # LVGL库 ${LVGL_SRC} ) include_directories( ${LVGL_DIR} ) # main.cpp必须放在这里,而不是SOURCE,除非你没有把Vcpkg放在该工程目录里,否则会把所有main.cpp都包含进来 add_executable(lvgl_simulator main.cpp ${SOURCE}) # ---------------------链接第三方库----------------------- # 前面必须加一个CMAKE_PREFIX_PATH,设置SDL_PATH并没有任何用,我就一句话,艹 set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} Vcpkg/packages/sdl2_x64-mingw-dynamic) find_package(SDL2 CONFIG REQUIRED) target_link_libraries(lvgl_simulator PUBLIC $<TARGET_NAME_IF_EXISTS:SDL2::SDL2main> $<IF:$<TARGET_EXISTS:SDL2::SDL2>,SDL2::SDL2,SDL2::SDL2-static> ) # 确保 SDL2.dll 复制到输出目录 if (${CMAKE_BUILD_TYPE} EQUAL Debug) configure_file(Vcpkg/packages/sdl2_x64-mingw-dynamic/debug/bin/SDL2d.dll ${CMAKE_CURRENT_BINARY_DIR}/SDL2d.dll COPYONLY) elseif (${CMAKE_BUILD_TYPE} EQUAL Release) configure_file(Vcpkg/packages/sdl2_x64-mingw-dynamic/bin/SDL2.dll ${CMAKE_CURRENT_BINARY_DIR}/SDL2.dll COPYONLY) endif ()
main.cpp
#include <iostream> #include <SDL.h> #include "simulator.hpp" #include "lvgl.h" #include "lv_port_disp.h" int lv_tick_thread(void *data); int main(int argc, char *argv[]) { // 模拟器初始化 simulator_init(argc, argv); // lvgl初始化 lv_init(); lv_port_disp_init(); // lv_tick线程 SDL_CreateThread(lv_tick_thread, "lv_tick", nullptr); // 创建一个屏幕对象 lv_obj_t * scr = lv_obj_create(nullptr); lv_obj_set_size(scr, 480,320); lv_obj_set_style_bg_color(scr, lv_color_white(), LV_PART_MAIN); //设置屏幕背景色 lv_obj_t * label = lv_label_create(scr); lv_label_set_text(label, "Hello World!"); lv_obj_update_layout(scr); lv_scr_load(scr); // 主循环 while (simulator_is_running()) { // 模拟器事件处理 simulator_event_Handler(); // lvgl事件处理 lv_timer_handler(); } // 清理资源 simulator_quit(); return 0; } /* 初始化时间滴答 * 需要定期调用 'lv_tick_inc()' 以告知 LittlevGL 自上次调用以来经过了多少时间 * 创建一个名为 "tick" 的 SDL 线程,该线程将定期调用 'lv_tick_inc()' */ int lv_tick_thread(void *data) { (void) data; /* 忽略传递的参数,因为在这个函数中不需要使用它 */ while (simulator_is_running()) { SDL_Delay(5); /* 休眠 5 毫秒 */ lv_tick_inc(5); /* 告诉 LittlevGL 已经过去了 5 毫秒 */ } return 0; }
simulator.hpp
// // Created by fairy on 2025/1/1 15:52. // #ifndef LVGL_SIMULATOR_SIMULATOR_HPP #define LVGL_SIMULATOR_SIMULATOR_HPP #ifdef __cplusplus #include <cstdint> #endif void simulator_init(int argc, char *argv[]); void simulator_quit(); void simulator_event_Handler(); bool simulator_is_running(); /*****************/ #ifdef __cplusplus extern "C" { #endif void LCD_Set_Pixel(uint16_t x, uint16_t y, uint16_t color); void LCD_Color_Fill(uint16_t xsta, uint16_t ysta, uint16_t xend, uint16_t yend, const uint16_t *color); void LCD_Color_Clean(uint16_t xsta, uint16_t ysta, uint16_t xend, uint16_t yend, uint16_t color); void LCD_Clear(uint16_t color); #ifdef __cplusplus } #endif #endif //LVGL_SIMULATOR_SIMULATOR_HPP
simulator.cpp
// // Created by fairy on 2025/1/1 15:52. // #include "simulator.hpp" #include <SDL.h> #include <iostream> static volatile int keep_running = 1; static SDL_Window *window; static SDL_Renderer *renderer; static SDL_Texture *texture; static uint16_t tft_fb[480 * 320]; bool simulator_is_running() { return keep_running; } /** * @brief 初始化 SDL * @note 初始化了 SDL 窗口、渲染器、纹理,并创建了一个更新纹理的线程 */ void simulator_init(int argc, char *argv[]) { // 忽略参数 (void) argc, (void) argv; // 初始化 SDL if (SDL_Init(SDL_INIT_VIDEO) != 0) { std::cerr << "Unable to initialize SDL: " << SDL_GetError() << std::endl; keep_running = false; return; } // 创建窗口 window = SDL_CreateWindow("LVGL Simulator", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 480, 320, 0); // 创建渲染器(此处使用硬件加速,可以使用软件加速SDL_RENDERER_SOFTWARE) renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); // 创建纹理 texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STREAMING, 480, 320); if (!window || !renderer || !texture) { std::cerr << "Failed to create window/renderer/texture: " << SDL_GetError() << std::endl; SDL_Quit(); keep_running = false; return; } // 创建更新纹理的线程 SDL_CreateThread([](void *) { while (keep_running) { SDL_UpdateTexture(texture, nullptr, tft_fb, 480 * sizeof(uint16_t)); SDL_RenderClear(renderer); SDL_RenderCopy(renderer, texture, nullptr, nullptr); SDL_RenderPresent(renderer); SDL_Delay(10); } return 1; }, "updateSDLGram", nullptr); } /** * @brief 退出 SDL */ void simulator_quit() { SDL_DestroyTexture(texture); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); } /** * @brief 处理 SDL 事件 */ void simulator_event_Handler() { static SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_QUIT: keep_running = false; break; case SDL_WINDOWEVENT: switch (event.window.event) { case SDL_WINDOWEVENT_TAKE_FOCUS: case SDL_WINDOWEVENT_EXPOSED: default: break; } default: break; } } // 短暂休眠 SDL_Delay(10); } /*****************************************************************************/ uint32_t rgb565_to_rgb8888(uint16_t &color) { uint8_t r = ((color >> 11) & 0x1F) * 8; uint8_t g = ((color >> 5) & 0x3F) * 4; uint8_t b = (color & 0x1F) * 8; return (r << 16) | (g << 8) | b; } void LCD_Set_Pixel(uint16_t x, uint16_t y, uint16_t color) { tft_fb[x + y * 480] = color; } void LCD_Color_Fill(uint16_t xsta, uint16_t ysta, uint16_t xend, uint16_t yend, const uint16_t *color) { uint16_t width = xend - xsta + 1; uint16_t height = yend - ysta + 1; uint32_t row_bytes = width * sizeof(uint16_t); auto *tft_ptr = (tft_fb+xsta+ysta * 480); auto *color_ptr = color; for (int y = 0; y < height; ++y) { memcpy(tft_ptr + y * width, color_ptr+y * width,row_bytes); } } void LCD_Color_Clean(uint16_t xsta, uint16_t ysta, uint16_t xend, uint16_t yend, uint16_t color) { uint16_t width = xend - xsta + 1; uint16_t height = yend - ysta + 1; uint32_t row_bytes = width * sizeof(uint16_t); auto *tft_ptr = (tft_fb+xsta+ysta * 480); for (int y = 0; y < height; ++y) { memset(tft_ptr + y * width, color, row_bytes); } } void LCD_Clear(uint16_t color) { LCD_Color_Clean(0, 0, 480 - 1, 320 - 1, color); }
Tips:
最初通过GUI Guider工程使用CLion搭建lvgl模拟器是因为GUI Guider的代码编辑功能有问题。后来基于GUI Guider工程搭建的lvgl模拟器工程由于gcc版本为9.2,感觉编译有些慢,且不能使用更高版本的C++,于是就有了本篇,不过实测下来,并没有比原先gcc版本快多少。尤其是最后的链接过程,比MinGW32慢太多了。