提升嵌入式开发幸福感的小技巧 通过优化编译流程、改进部署方式和精简开发环境,我们可以将宝贵的时间更多地投入到业务逻辑和功能创新上,而不是浪费在繁琐的重复性操作中
0. 调整lvgl工程,使工程能支持切换平台 在开发嵌入式GUI应用(如LVGL)时,最高效的方式是实现“PC端模拟调试,开发板端实机运行”。这需要我们将工程调整为跨平台架构,让同一套源代码无需修改,即可在x86和ARM等不同平台上编译运行
开源示例工程 :本文所有配置的完整实现,可以参考此开源项目:
Github:Cross-LVGL-demo
Gitee:cross-LVGL-demo
核心原理 利用CMake构建系统的平台判断能力,结合C语言的预处理指令,为不同平台链接不同的底层驱动。
PC平台 :使用 SDL2 (Simple DirectMedia Layer) 库来模拟显示窗口和鼠标键盘输入。
ARM平台 :使用嵌入式Linux标准的 Framebuffer (缓冲区刷新显示) 和 evdev (输入) 驱动。
第一步:改造构建系统 CMake 1. 创建独立的ARM工具链文件 (arm.cmake
) 将交叉编译配置独立出来,使主配置文件更整洁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 set (CMAKE_SYSTEM_NAME Linux)set (CMAKE_SYSTEM_PROCESSOR arm)set (TOOLCHAIN_DIR "/usr/local/arm/5.4.0/usr/" )set (CMAKE_C_COMPILER "${TOOLCHAIN_DIR}/bin/arm-linux-gcc" )set (CMAKE_CXX_COMPILER "${TOOLCHAIN_DIR}/bin/arm-linux-g++" )set (CMAKE_C_FLAGS"-Wl -rpath=." )set (CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)set (CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)set (CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)set (CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
2. 修改主CMakeLists.txt
,增加平台判断逻辑 使用CMake内置变量CMAKE_CROSSCOMPILING
来自动识别当前编译环境。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 cmake_minimum_required (VERSION 3.12 )project (main C)set (CMAKE_C_STANDARD 99 )set (CMAKE_C_STANDARD_REQUIRED ON )set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR} /bin)if (CMAKE_CROSSCOMPILING) set (TARGET_ARCH "arm" ) set (PLATFORM_LIBS pthread freetype m) else () set (TARGET_ARCH "pc" ) find_package (SDL2 REQUIRED) find_package (Threads REQUIRED) set (PLATFORM_LIBS SDL2::SDL2 Threads::Threads freetype m) endif ()link_directories ("libs/freetype/lib/${TARGET_ARCH}" )execute_process (COMMAND bash configure.sh ${TARGET_ARCH} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} )include_directories ( . libs/freetype/include UI lvgl lv_drivers ) file (GLOB_RECURSE ALL_SOURCES "lvgl/src/*.c" "lv_drivers/*.c" "lvgl/demos/*.c" "lvgl/examples/*.c" "UI/*.c" ) add_executable (${PROJECT_NAME} main.c mouse_cursor_icon.c ${ALL_SOURCES} )target_link_libraries (${PROJECT_NAME} PRIVATE ${PLATFORM_LIBS} )
自动化修改lv_conf.h
和lv_drv_conf.h
中的宏定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #!/bin/bash if [ "$# " -ne 1 ]; then echo "用法: $0 <pc|arm>" exit 1 fi TARGET_ARCH=$1 LV_DRV_CONF_PATH="lv_drv_conf.h" LV_CONF_PATH="lv_conf.h" if [ ! -f "$LV_DRV_CONF_PATH " ]; then echo "错误:未找到 lv_drv_conf.h 文件: $LV_DRV_CONF_PATH " exit 1 fi if [ ! -f "$LV_CONF_PATH " ]; then echo "错误:未找到 lv_conf.h 文件: $LV_CONF_PATH " exit 1 fi echo "正在为 '$TARGET_ARCH ' 架构配置驱动和内核..." if [ "$TARGET_ARCH " = "pc" ]; then echo "配置 lv_drv_conf.h: 启用 SDL, 禁用 Framebuffer/evdev" sed -i 's/^\([[:space:]]*#\s*define\s*USE_SDL\s*\)[01]/#define USE_SDL 1/' "$LV_DRV_CONF_PATH " sed -i 's/^\([[:space:]]*#\s*define\s*USE_FBDEV\s*\)[01]/#define USE_FBDEV 0/' "$LV_DRV_CONF_PATH " sed -i 's/^\([[:space:]]*#\s*define\s*USE_EVDEV\s*\)[01]/#define USE_EVDEV 0/' "$LV_DRV_CONF_PATH " echo "配置 lv_conf.h: 禁用自定义 Tick (LV_TICK_CUSTOM=0),因为 SDL 会处理" sed -i 's/^\([[:space:]]*#\s*define\s*LV_TICK_CUSTOM\s*\)[01]/#define LV_TICK_CUSTOM 0/' "$LV_CONF_PATH " elif [ "$TARGET_ARCH " = "arm" ]; then echo "配置 lv_drv_conf.h: 禁用 SDL, 启用 Framebuffer/evdev" sed -i 's/^\([[:space:]]*#\s*define\s*USE_SDL\s*\)[01]/#define USE_SDL 0/' "$LV_DRV_CONF_PATH " sed -i 's/^\([[:space:]]*#\s*define\s*USE_FBDEV\s*\)[01]/#define USE_FBDEV 1/' "$LV_DRV_CONF_PATH " sed -i 's/^\([[:space:]]*#\s*define\s*USE_EVDEV\s*\)[01]/#define USE_EVDEV 1/' "$LV_DRV_CONF_PATH " echo "配置 lv_conf.h: 启用自定义 Tick (LV_TICK_CUSTOM=1) 以使用 custom_tick_get()" sed -i 's/^\([[:space:]]*#\s*define\s*LV_TICK_CUSTOM\s*\)[01]/#define LV_TICK_CUSTOM 1/' "$LV_CONF_PATH " else echo "无效参数: '$TARGET_ARCH '。请使用 'pc' 或 'arm'" exit 1 fi echo "配置完成"
第二步:改造应用代码 main.c
使用预处理指令#if USE_SDL
来包裹平台相关的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 #define _DEFAULT_SOURCE #include <stdlib.h> #include <unistd.h> #include "lv_drv_conf.h" #include "lvgl/lvgl.h" #include "obj/head.h" #include "UI/ui.h" #include "lvgl/examples/lv_examples.h" #if USE_SDL #define SDL_MAIN_HANDLED #include <SDL2/SDL.h> #include "lv_drivers/sdl/sdl.h" #else #include "lv_drivers/display/fbdev.h" #include "lv_drivers/indev/evdev.h" #include <sys/time.h> uint32_t custom_tick_get (void ) ; #endif static void hal_init (void ) ;int main (int argc, char **argv) { (void )argc; (void )argv; lv_init(); hal_init(); ui_init(); while (1 ) { lv_timer_handler(); usleep(5 * 1000 ); } return 0 ; } static void hal_init (void ) { #if USE_SDL sdl_init(); static lv_disp_draw_buf_t disp_buf1; static lv_color_t buf1_1[SDL_HOR_RES * 100 ]; lv_disp_draw_buf_init(&disp_buf1, buf1_1, NULL , SDL_HOR_RES * 100 ); static lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.draw_buf = &disp_buf1; disp_drv.flush_cb = sdl_display_flush; disp_drv.hor_res = SDL_HOR_RES; disp_drv.ver_res = SDL_VER_RES; lv_disp_t * disp = lv_disp_drv_register(&disp_drv); lv_theme_t * th = lv_theme_default_init(disp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), LV_THEME_DEFAULT_DARK, LV_FONT_DEFAULT); lv_disp_set_theme(disp, th); lv_group_t * g = lv_group_create(); lv_group_set_default(g); static lv_indev_drv_t indev_drv_1; lv_indev_drv_init(&indev_drv_1); indev_drv_1.type = LV_INDEV_TYPE_POINTER; indev_drv_1.read_cb = sdl_mouse_read; lv_indev_drv_register(&indev_drv_1); static lv_indev_drv_t indev_drv_2; lv_indev_drv_init(&indev_drv_2); indev_drv_2.type = LV_INDEV_TYPE_KEYPAD; indev_drv_2.read_cb = sdl_keyboard_read; lv_indev_t *kb_indev = lv_indev_drv_register(&indev_drv_2); lv_indev_set_group(kb_indev, g); static lv_indev_drv_t indev_drv_3; lv_indev_drv_init(&indev_drv_3); indev_drv_3.type = LV_INDEV_TYPE_ENCODER; indev_drv_3.read_cb = sdl_mousewheel_read; lv_indev_t * enc_indev = lv_indev_drv_register(&indev_drv_3); lv_indev_set_group(enc_indev, g); #else #define DISP_BUF_SIZE (128 * 1024) fbdev_init(); static lv_color_t buf[DISP_BUF_SIZE]; static lv_disp_draw_buf_t disp_buf; lv_disp_draw_buf_init(&disp_buf, buf, NULL , DISP_BUF_SIZE); static lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.draw_buf = &disp_buf; disp_drv.flush_cb = fbdev_flush; disp_drv.hor_res = 800 ; disp_drv.ver_res = 480 ; lv_disp_drv_register(&disp_drv); evdev_init(); static lv_indev_drv_t indev_drv_1; lv_indev_drv_init(&indev_drv_1); indev_drv_1.type = LV_INDEV_TYPE_POINTER; indev_drv_1.read_cb = evdev_read; lv_indev_t *mouse_indev = lv_indev_drv_register(&indev_drv_1); LV_IMG_DECLARE(mouse_cursor_icon) lv_obj_t * cursor_obj = lv_img_create(lv_scr_act()); lv_img_set_src(cursor_obj, &mouse_cursor_icon); lv_indev_set_cursor(mouse_indev, cursor_obj); #endif } #if !USE_SDL uint32_t custom_tick_get (void ) { static uint64_t start_ms = 0 ; if (start_ms == 0 ) { struct timeval tv_start ; gettimeofday(&tv_start, NULL ); start_ms = (tv_start.tv_sec * 1000000 + tv_start.tv_usec) / 1000 ; } struct timeval tv_now ; gettimeofday(&tv_now, NULL ); uint64_t now_ms; now_ms = (tv_now.tv_sec * 1000000 + tv_now.tv_usec) / 1000 ; uint32_t time_ms = now_ms - start_ms; return time_ms; } #endif
1. 使用telnet,避免频繁切换窗口和反复串口调试 Telnet 连接开发板分为两部分:开发板配置 和 主机配置 。
注意虚拟机必须配置静态ip,如果不知道怎么配置静态IP,看这个文章:
配置静态ip的步骤
开发板配置
确保开发板与电脑在同一网段,例如 192.168.11.x
将所需软件放到合适的位置。
在 /etc/profile
文件中加入 Telnet 服务的后台启动命令,例如:
1 2 3 if ! ps | grep -q "[t]elnetd" ; then /IOT/telnetd & fi
这样每次开机都会自动启动 Telnet 服务
主机配置
在 Ubuntu 主机上安装 Telnet 客户端:
使用以下命令连接开发板:
连接成功后即可在电脑终端直接操作开发板
2. 使用nfs,将ubuntu里的文件夹挂载到开发版 NFS(Network File System,网络文件系统)可以让开发板直接访问Ubuntu上的目录,省去拷贝文件的步骤。 确保开发板与电脑在同一网段,例如 192.168.11.x
注意虚拟机必须配置静态ip,如果不知道怎么配置静态IP,看这个文章:
配置静态ip的步骤
主机配置(Ubuntu)
安装 NFS 服务端:
1 sudo apt-get install nfs-kernel-server
编辑共享目录配置文件 /etc/exports
在文件末尾添加一行:
1 /media/skyforever/Data/share 192.168.11.??(rw,sync ,no_root_squash,no_subtree_check)
/media/skyforever/Data/share
为需要共享的目录
192.168.11.0/24
表示允许同一网段的设备访问
no_root_squash
允许开发板上的 root 用户保留权限
重启 NFS 服务使配置生效:
1 sudo systemctl restart nfs-kernel-server
查看NFS服务是否正常运行
1 sudo systemctl status nfs-kernel-server
开发板配置(GEC6818)
GEC6818 开发板自带 NFS 客户端,无需额外安装服务。
在 /etc/profile
中添加挂载命令,保证开机自动挂载:
1 2 3 if ! mount | grep -q "/mnt/nfs" ; then mount -t nfs -o nolock,vers=3 192.168.11.??:/media/skyforever/Data/share /mnt/nfs fi
192.168.11.85
是主机的 IP 地址。
/mnt/nfs
是开发板上的挂载点。
保存并重启开发板后,就可以在 /mnt/nfs
目录下直接访问主机上的共享文件夹
3. 使用vscode的配置文件选项,避免调用大量插件造成卡顿 现代IDE功能强大,但也可能因为安装了适用于不同语言(Web、Python等)的众多插件而变得臃肿,在进行嵌入式C/C++开发时尤其影响性能。VSCode的“配置文件(Profiles)”功能可以完美解决此问题。
目的 :为嵌入式开发创建一个专属、轻量、无干扰的IDE环境。
操作步骤
创建新配置文件
定制嵌入式专属环境 切换到新创建的“C/C++”配置文件后:
打开 扩展 侧边栏 (Ctrl+Shift+X
)。
将所有与C/C++嵌入式开发无关的插件禁用 。例如Python、Jupyter、Prettier、Live Server等。
仅保留核心插件,如:
无缝切换 之后,你可以通过左下角的齿轮图标,在不同的配置文件之间快速切换。
当你需要进行嵌入式开发时,切换到“C/C++”配置文件,VSCode会立刻关掉无关插件的负载,变得极其流畅和专注。