C++20 module下的LVGL模拟器
ARM GCC:14.2
Toolchain:MSVC
前篇:使用SDL2搭建简易LVGL模拟器_lvgl sdl-CSDN博客
故事
从前
我所用的单片机工程本身是为了电赛设计的,由于电赛的特性,需要使用同一个实验平台通过不同外设的“排列组合”来实现不同实验项目的功能,也就是说外设基本只要开发一次即可复用(BSP层),唯一不同的是“不同的项目需要调用不同的外设、编写不同的逻辑来实现不同的功能”。遇到新的项目,也只是编写新的外设的控制代码,基本框架并不需要改变
在这个基础上,设计了这种的项目组织结构,同一个工程包含了不同的项目代码,BSP层、HAL层等都是共用的,包括main.cpp
不同项目的逻辑是编写在app.cpp里的(放在一个个独立的文件夹里),main.cpp通过extern来调用app.cpp里的接口,通过CMakeLists来选择编译哪个项目的app.cpp。如果需要实现更加复杂的功能,比如GUI、AI等,那么在项目文件夹里添加更多目录或者文件,通过CMakeLists选择性编译不同项目文件夹的所有源文件即可。
后来
起初为了兼容性,单片机的BSP(那些外设驱动)都是使用纯C编写的。可是随着开发的BSP驱动变多,要写的.h/c文件也变多,代码也越编写越杂乱,各种宏、全局变量等漫天飞。同时为了保证不同项目的一致性,比如有些项目需要使用GUI和RTOS,有些不需要。那么需要让main.cpp、ISR.cpp(中断处理)等共用的文件做出一些适配,那么又是一堆宏和子cmake里的一堆变量(虽然现在也没解决)。
后面想到工程本身就是为了电赛设计的,为何不依此一路走到黑?于是把搁置了许久的命名空间和C++20 module特性又重新启用了,抛弃了.h/c文件,改用.ixx文件。好处还不少:
- 首先就是编写驱动代码在一个文件里即可,不需要在.h/c文件里到处折腾,来回修改函数签名,也不需要建立include和source目录
- 代码在一个文件里,编译器能看到所有的代码,可以进行更好的优化。之前在.h/c中为了内联,不惜在头文件里要么直接定义static inline函数,要么使用#define和\配合。结果就是,想要内联就需要在头文件里写东西,而在头文件里写太多东西,就会让编译线性增长,而且里面的东西暴露的到处都是,代码补全提示里全是乱七八糟的东西。(使用LTO既慢,又容易出现未定义的问题)
- module特性可以很方便导出想要暴露的接口,其余的默认封闭。而命名空间让代码的组织清晰了不少,不同层分配在不同的命名空间。此外,模板、类、重载、引用等特性也都可以使用
- ……
改变
如下,lcd.ixx里由于使用了lvgl,只需要暴露初始化和涂块函数即可,其余函数和宏等仅在内部暴露
也可以使用模板类进行封装
既如此,那么在lvgl的封装中也使用该特性(参考LVGL使用过程中的一点启发),那么模拟器的画风就变成了这样(虽然代码风格上没怎么变)
命名空间的关系如下顶层命名空间是gui,里面分成三个部分,compose是用来存放组件的命名空间,里面包含了各种组件类的定义。Render是个类,负责对lvgl的初始化和运行的封装,widgets是用来存放组件定义的命名空间
在ui文件里,需要完成这些功能
代码层面,ui文件的编写风格也就是变成了这样(由python脚本自动生成)
module; #include <lvgl.h> export module gui:ui; export import :render; // 导入其他资源 /*!USER_DECLARE_BEGIN!*/ import ui_data;/*!USER_DECLARE_END!*/// ---------------- 导出并加载资源 ---------------- extern "C" {// 字体资源LV_FONT_DECLARE(lv_customer_font_SourceHanSerifSC_Regular_13)/*!USER_DECLARE_BEGIN!*//*!USER_DECLARE_END!*/// 图片资源LV_IMG_DECLARE(_dianzisheji_RGB565A8_61x42)/*!USER_DECLARE_BEGIN!*//*!USER_DECLARE_END!*/// 其他/*!USER_DECLARE_BEGIN!*//*!USER_DECLARE_END!*/ }// ---------------- 导出并定义组件 ---------------- export namespace gui::widgets::main {using namespace gui::compose;// 命名方式看个人习惯inline Image img_screen_img_1;inline Chart chart_screen_1;inline Button btn_screen_1;inline Label label_screen_1;inline Button btn_screen_2;inline Label label_screen_2;inline Button btn_screen_3;inline Label label_screen_3;inline Button btn_screen_4;inline Label label_screen_4;inline Button btn_screen_5;/*为了演示,删除了一些组件定义代码……*//*!USER_DECLARE_BEGIN!*/inline Timer updata_timer;/*!USER_DECLARE_END!*/ }// ======================= 用户空间 ======================= /*!USER_DECLARE_BEGIN!*/ // 全局变量定义 inline uint8_t length = 200; inline uint8_t wave_start_index = 0; inline uint16_t array_length = 400; inline size_t current_index = 0; // 当前读取位置 inline uint8_t count = 0;/*!USER_DECLARE_END!*/ // ======================= 用户空间 =======================// ---------------- 导出用户接口 ---------------- export namespace gui::ui {using namespace gui::widgets::main; // 使用组件命名空间/*!USER_DECLARE_BEGIN!*/// 类声明class Osc{public:static auto initChartComponent() -> void;// 生成随机数据static inline auto generate_data() -> void;static inline auto toggle_generation() -> void;// 新增定时器回调函数static void timer_cb(lv_timer_t *timer){if (is_generating){generate_data();}}static inline auto set_cursor_on_press() -> void{chart_screen_chart_1.set_cursor_pos_on_pressed(cursor);if (had_generated){char buf[10];lv_snprintf(buf, sizeof(buf), "%d %d", chart_screen_chart_1.get_pressed_point(),chart_screen_chart_1.get_cursor_point_y(series)); //格式化点数值成字符串label_screen_label_1.text(buf);}}static inline bool had_generated = false;private:// 添加生成状态标志static inline bool is_generating = false;static inline ChartSeries_t series{}; //数据 系列1static inline ChartCursor_t cursor{}; //光标 系列1static inline lv_point_t cursor_point{}; //光标 系列1};/*!USER_DECLARE_END!*/ }// ---------------- 初始化UI和事件 ---------------- export namespace gui {void Render::screenInit(){using namespace gui::widgets::main; // 使用组件命名空间scr.bg_color(lv_color_hex(0xffffff)).bg_grad_dir(LV_GRAD_DIR_NONE);img_screen_img_1.init().pos(405, 5).size(61, 42).add_flag(LV_OBJ_FLAG_CLICKABLE).src(&_dianzisheji_RGB565A8_61x42).pivot(50, 50).image_recolor_opa(0);chart_screen_1.init().pos(18, 8).size(375, 227).scrollbar_mode(LV_SCROLLBAR_MODE_OFF).div_count(11, 15).point_count(5).range(LV_CHART_AXIS_PRIMARY_Y).range(LV_CHART_AXIS_SECONDARY_Y).bg_color(lv_color_hex(0xffffff)).bg_grad_dir(LV_GRAD_DIR_NONE).border_width(1).border_opa(255).border_color(lv_color_hex(0xe8e8e8)).border_side(LV_BORDER_SIDE_FULL).radius(0).line_width(2).line_color(lv_color_hex(0xe8e8e8));label_screen_1_label.init(btn_screen_1).text("BB").center().width(LV_PCT(100));/*为了演示,删除了一些初始化代码……*//*!USER_DECLARE_BEGIN!*/ui::Osc::initChartComponent();/*!USER_DECLARE_END!*/}void Render::eventInit(){using namespace gui::widgets::main; // 使用组件命名空间/*!USER_DECLARE_BEGIN!*/// 绑定 随机生成数据事件btn_screen_1.OnClicked<ui::Osc::toggle_generation>();updata_timer.create(ui::Osc::timer_cb, 20);chart_screen_1.OnPressed<ui::Osc::set_cursor_on_press>();/*!USER_DECLARE_END!*/} }// ---------------- 模块内部实现 ---------------- namespace gui::ui {// 新增切换生成状态的方法auto Osc::toggle_generation() -> void{is_generating = !is_generating;had_generated = true;// 更新按钮文本label_screen_1.text(is_generating ? "BB" : "LL");if (is_generating){updata_timer.resume();}else{updata_timer.pause();}}auto Osc::generate_data() -> void{// 批量设置128个点for (int i = 0; i < 128; ++i){// 循环访问数组size_t idx = (current_index + i) % RAND_POOL_SIZE;chart_screen_1.next_value(series, rand_pool[idx]);}}auto Osc::initChartComponent() -> void{series = chart_screen_1.update_mode(LV_CHART_UPDATE_MODE_SHIFT) // 改为SHIFT模式.line_color(lv_color_hex(0x34e6ff)).point_count(128).remove_dot().add_series(lv_color_hex(0x34e6ff));cursor = chart_screen_1.add_cursor(lv_color_hex(0xfffb00), LV_DIR_ALL);scale_screen_1.border_opa(0);} }
结果
不过这些好处仅限arm交叉编译工具链gcc 14.2,在MinGW64中的gcc 14.2里可谓是一塌糊涂。理论上gcc版本相同对特性的支持就应该相同或者大差不差,结果在同一份代码的实际测试中,后者对module特性的支持竟然比前者要差一些。原本inline 变量这个特性在C++17中就已经实现了,但是后者对下面这样的代码竟然能报出重定义的错误,敢情是C++20的module特性与C++17的inline特性冲突了
export namespace gui::widgets::main {using namespace gui::compose;// 使用命名空间inline Component scr;// 主屏幕 }
转机
既然MinGW GCC无法胜任,那么只好转为使用对module特性支持更好的MSVC工具链(需要下载Visual Studio)
与此同时,需要下载SDL2的VC版,打开链接Releases · libsdl-org/SDL,找到VC版
下载解压后,里面的内容与左边的框框一致
为了在CMakeLists里与GCC的SDL2库路径保持一致,在右边的框里添加了一个SDL2目录
# 自动识别工具链类型 !!切换工具链时需要把cmake产物删除干净!! if(MSVC)# Visual Studio 工具链set(SDL2_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/SDL2/VC")set(SDL2_LIB_DIR "${SDL2_ROOT}/lib/x64")message(STATUS "[Config] Using MSVC toolchain") elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU")# 区分MinGW架构message(WARNING "[Config] MinGW64 GCC 14.2 对module和inline特性的支持不完善,会出问题")if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64" OR CMAKE_SIZEOF_VOID_P EQUAL 8)set(SDL2_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/SDL2/x86_64-w64-mingw32")message(STATUS "[Config] Using 64-bit MinGW")else()set(SDL2_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/SDL2/i686-w64-mingw32")message(STATUS "[Config] Using 32-bit MinGW")endif()set(SDL2_LIB_DIR "${SDL2_ROOT}/lib") else()message(FATAL_ERROR "[Config] Unsupported toolchain: ${CMAKE_CXX_COMPILER_ID}") endif()# …………target_include_directories(${PROJECT_NAME} PRIVATE ${SDL2_ROOT}/include)
如此一来,便可编译成功
代码
CMakeLists:
cmake_minimum_required(VERSION 3.29) project(Simulator LANGUAGES C CXX) include(common_functions.cmake)# ------------------------ 配置选项 ------------------------ set(PROJECT_DIR ../../../Projects/driversDevelop) set(PROJECT_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/..") set(THIRD_PARTY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../Middleware/Third_Party")# 自动识别工具链类型 !!切换工具链时需要把cmake产物删除干净!! if(MSVC)# Visual Studio 工具链set(SDL2_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/SDL2/VC")set(SDL2_LIB_DIR "${SDL2_ROOT}/lib/x64")message(STATUS "[Config] Using MSVC toolchain") elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU")# 区分MinGW架构message(WARNING "[Config] MinGW64 GCC 14.2 对module和inline特性的支持不完善,会出问题")if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64" OR CMAKE_SIZEOF_VOID_P EQUAL 8)set(SDL2_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/SDL2/x86_64-w64-mingw32")message(STATUS "[Config] Using 64-bit MinGW")else()set(SDL2_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/SDL2/i686-w64-mingw32")message(STATUS "[Config] Using 32-bit MinGW")endif()set(SDL2_LIB_DIR "${SDL2_ROOT}/lib") else()message(FATAL_ERROR "[Config] Unsupported toolchain: ${CMAKE_CXX_COMPILER_ID}") endif()set(LVGL_DIR "${THIRD_PARTY_DIR}/LVGL/lvgl") set(UI_DIR "${PROJECT_DIR}/ui") set(GUI_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../Compose")# 输出目录 set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")# ------------------------ 编译选项 ------------------------ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON)# MSVC 专用选项 if (MSVC)# C++20 模块和概念支持string(APPEND CMAKE_CXX_FLAGS " /std:c++latest")# 根据构建类型设置优化选项if (CMAKE_BUILD_TYPE STREQUAL "Release")add_compile_options(/O2)string(APPEND CMAKE_CXX_FLAGS " /RTC0") # Release 禁用运行时检查else()add_compile_options(/Od) # Debug 禁用优化endif()# 调试符号生成add_compile_options(/Zi) else()# GCC/Clang 选项add_compile_options(-O2 -g -fmodules-ts) endif()# ------------------------ 组件库定义 ------------------------ file(GLOB_RECURSE LVGL_SRCS "${LVGL_DIR}/src/*.c" "${LVGL_DIR}/examples/porting/*.c") file(GLOB_RECURSE DRIVERS_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/Drivers/*.c" "${CMAKE_CURRENT_SOURCE_DIR}/Drivers/*.cpp") file(GLOB_RECURSE RENDER_SRCS "${PROJECT_ROOT_DIR}/Render/*.c" "${PROJECT_ROOT_DIR}/Render/*.cpp") file(GLOB_RECURSE UI_SRCS "test.cpp" "${UI_DIR}/*.c" "${UI_DIR}/*.cpp") file(GLOB cxx_modules "${GUI_DIR}/*.ixx" "${UI_DIR}/*.ixx")# ------------------------ 可执行目标 ------------------------ add_executable(${PROJECT_NAME} "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp")# 添加模块文件 target_sources(${PROJECT_NAME} PUBLICFILE_SET CXX_MODULESBASE_DIRS ${GUI_DIR} ${UI_DIR}FILES ${cxx_modules} )# 添加普通源文件 target_sources(${PROJECT_NAME} PRIVATE${LVGL_SRCS}${DRIVERS_SRCS}${RENDER_SRCS}${UI_SRCS} )# 包含目录 target_include_directories(${PROJECT_NAME} PRIVATE${GUI_DIR}${UI_DIR}${LVGL_DIR}${LVGL_DIR}/examples/porting${CMAKE_CURRENT_SOURCE_DIR}/Drivers${SDL2_ROOT}/include${PROJECT_ROOT_DIR}/Render )# ------------------------ 依赖配置 ------------------------ find_library(SDL2_LIB SDL2 HINTS "${SDL2_LIB_DIR}" REQUIRED)# 查找库 target_link_libraries(${PROJECT_NAME} PRIVATE ${SDL2_LIB})# 链接库# 复制 SDL2.dll add_custom_command(TARGET ${PROJECT_NAME} POST_BUILDCOMMAND ${CMAKE_COMMAND} -E copy"${SDL2_LIB_DIR}/SDL2.dll"$<TARGET_FILE_DIR:${PROJECT_NAME}> )
模拟器工程还未完善,暂不上传