当前位置: 首页 > article >正文

使用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慢太多了。


http://www.kler.cn/a/463147.html

相关文章:

  • Linux硬盘分区 --- 挂载分区mount、卸载分区umount、永久挂载
  • Alist-Sync-Web 网盘自动同步,网盘备份相互备份
  • 使用java语言,自定义redistemplate
  • Oracle exp和imp命令导出导入dmp文件
  • 【Vue】vue项目中命名规范(结合上一篇项目结构)
  • ubuntu常用快捷键和变量记录
  • 香港 GPU 服务器托管引领 AI 创新,助力 AI 发展
  • Ubuntu 上高效实现 Texlive 安装和管理
  • 关于flinkCDC监控mysql binlog时,datetime类型自动转换成时间戳类型问题
  • Kali 自动化换源脚本编写与使用
  • Mac M2 Pro安装MySQL 8.4.3
  • Django中创建自定义命令发送钉钉通知
  • ARM架构服务器安装部署KVM虚拟化环境
  • LLaMA 2开放基础和微调聊天模型
  • 自定义luacheck校验规则
  • spring boot通过文件配置yaml里面的属性
  • 从数据映射到文件生成:一个R语言实践案例
  • 自己电脑搭建个人知识库,一般电脑也能玩(支持通义千问、GPT等)。
  • VSCode 插件开发实战(十六):详解插件生命周期
  • selenium(三)
  • Midjourney技术浅析(三):文本编码
  • .NET | 详解通过Win32函数实现本地提权
  • 计算机网络—————考研复试
  • WOFOST作物模型(2.1):模型参数介绍
  • Python基于Django的web漏洞挖掘扫描技术的实现与研究(附源码,文档说明)
  • 数据库在大数据领域的探索与实践:动态存储与查询优化