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

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}>
)

模拟器工程还未完善,暂不上传

相关文章:

  • [实战]zynq7000设备树自动导出GPIO
  • 聊聊自动化用例的维护
  • Qt Creator中自定义应用程序的可执行文件图标
  • LM Studio模型下载慢怎么办
  • Java基础系列-HashMap源码解析2-AVL树
  • 从代码学习深度学习 - 自动并行 PyTorch 版
  • 57、Spring Boot 最佳实践
  • NLP高频面试题(五十三)——LLM中激活函数详解
  • 力扣hot100_链表(3)_python版本
  • 盈达科技:登顶GEO优化全球制高点,以AICC定义AI时代内容智能优化新标杆
  • TCP四大特性面试回答引导
  • 【无人机】无人机位置估计出现偏差的原因分析
  • ESP32-S3开发板麦克风录音到SD卡存储测试
  • 自主可控鸿道Intewell工业实时操作系统
  • Rust 语言使用场景分析
  • 【LangChain4j】AI 第一弹:LangChain4j 的理解
  • 图聚类中的亲和力传播
  • 数据库11(触发器)
  • 跨平台软件开发探讨
  • 三目云台转动性能稳定性
  • 中国体育报:中国乒协新周期新起点再出发
  • 上海小朋友喜欢读什么书?来看这份“少年儿童阅读报告”
  • 重庆市委原常委、政法委原书记陆克华严重违纪违法被开除党籍和公职
  • 河南濮阳南乐县官方回应“幼儿园强制订园服”:已责令整改
  • 叶辛秦文君进校园推广阅读
  • 中共中央、国务院印发《关于实施自由贸易试验区提升战略的意见》