提升嵌入式开发幸福感的小技巧

通过优化编译流程、改进部署方式和精简开发环境,我们可以将宝贵的时间更多地投入到业务逻辑和功能创新上,而不是浪费在繁琐的重复性操作中

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=.")

# 指定 find_library, find_path 等命令的搜索路径模式
# 从不搜索宿主系统路径,只在工具链路径中找程序
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# 只在工具链路径中找库
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
# 只在工具链路径中找头文件
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
# 只在工具链路径中找 CMake 包配置文件
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() # PC
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
# lv_demos
# lv_examples
)

# --- 源文件 ---
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})

3. 添加configure.sh脚本,自动修改LVGL配置文件

自动化修改lv_conf.hlv_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
# --- 配置 lv_drv_conf.h (驱动层) ---
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"

# --- 配置 lv_conf.h (内核层) ---
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
# --- 配置 lv_drv_conf.h (驱动层) ---
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"

# --- 配置 lv_conf.h (内核层) ---
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
/**
* @file main
* @brief 整合了 PC 和 ARM 平台的启动代码
*/

/*********************
* INCLUDES
*********************/
#define _DEFAULT_SOURCE /* needed for usleep() */
#include <stdlib.h>
#include <unistd.h>

// 驱动配置文件
#include "lv_drv_conf.h"

// LVGL 核心库
#include "lvgl/lvgl.h"

// UI 代码头文件
#include "obj/head.h"
#include "UI/ui.h"

// 添加对examples的引用
#include "lvgl/examples/lv_examples.h"

// 根据 lv_drv_conf.h 中的 USE_SDL 宏来包含不同的平台驱动头文件
#if USE_SDL
/* ========================= */
/* PC/SDL 平台头文件 */
/* ========================= */
#define SDL_MAIN_HANDLED
#include <SDL2/SDL.h>
#include "lv_drivers/sdl/sdl.h"
#else
/* ================================ */
/* ARM/Framebuffer 平台头文件 */
/* ================================ */
#include "lv_drivers/display/fbdev.h"
#include "lv_drivers/indev/evdev.h"
#include <sys/time.h>
uint32_t custom_tick_get(void); // ARM 平台 tick 函数声明
#endif


/**********************
* STATIC PROTOTYPES
**********************/
static void hal_init(void);


/**********************
* GLOBAL FUNCTIONS
**********************/
int main(int argc, char **argv)
{
(void)argc; /* Unused */
(void)argv; /* Unused */

/* 初始化 LVGL */
lv_init();

/* 初始化 HAL (显示, 输入设备, tick) */
hal_init();

/* === 调用 UI 代码 === */
// obj_pos1();
ui_init();


// obj_sjpg_1(); // 用图片数组显示
// obj_sjpg_2(); // 用指定路径显示
// obj_freetype_text();// 显示文字
// lv_flex_test(); // 布局测试
/* 主循环 */
while(1) {
lv_timer_handler();
usleep(5 * 1000);
}

return 0;
}


/**********************
* STATIC FUNCTIONS
**********************/

/**
* @brief 初始化硬件抽象层 (HAL)
* 根据 USE_SDL 的值选择初始化 PC/SDL 平台或 ARM/Framebuffer 平台
*/
static void hal_init(void)
{
#if USE_SDL
/* ================================== */
/* PC/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
/* ======================================= */
/* ARM 平台初始化 */
/* ======================================= */
#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
/**
* @brief ARM/fbdev 平台 tick 获取函数实现
*/
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的步骤

开发板配置

  1. 确保开发板与电脑在同一网段,例如 192.168.11.x

  2. 将所需软件放到合适的位置。

  3. /etc/profile 文件中加入 Telnet 服务的后台启动命令,例如:

    1
    2
    3
    if ! ps | grep -q "[t]elnetd"; then                         
    /IOT/telnetd &
    fi

    这样每次开机都会自动启动 Telnet 服务

主机配置

  1. 在 Ubuntu 主机上安装 Telnet 客户端:

    1
    sudo apt install telnet
  2. 使用以下命令连接开发板:

    1
    telnet 192.168.11.??

    连接成功后即可在电脑终端直接操作开发板


2. 使用nfs,将ubuntu里的文件夹挂载到开发版

NFS(Network File System,网络文件系统)可以让开发板直接访问Ubuntu上的目录,省去拷贝文件的步骤。
确保开发板与电脑在同一网段,例如 192.168.11.x
注意虚拟机必须配置静态ip,如果不知道怎么配置静态IP,看这个文章:

配置静态ip的步骤

主机配置(Ubuntu)

  1. 安装 NFS 服务端:

    1
    sudo apt-get install nfs-kernel-server
  2. 编辑共享目录配置文件 /etc/exports

    1
    sudo vi /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 用户保留权限
  3. 重启 NFS 服务使配置生效:

    1
    sudo systemctl restart nfs-kernel-server
  4. 查看NFS服务是否正常运行

    1
    sudo systemctl status nfs-kernel-server

开发板配置(GEC6818)

  1. GEC6818 开发板自带 NFS 客户端,无需额外安装服务。

  2. /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 是开发板上的挂载点。
  3. 保存并重启开发板后,就可以在 /mnt/nfs 目录下直接访问主机上的共享文件夹

3. 使用vscode的配置文件选项,避免调用大量插件造成卡顿

现代IDE功能强大,但也可能因为安装了适用于不同语言(Web、Python等)的众多插件而变得臃肿,在进行嵌入式C/C++开发时尤其影响性能。VSCode的“配置文件(Profiles)”功能可以完美解决此问题。

目的:为嵌入式开发创建一个专属、轻量、无干扰的IDE环境。

操作步骤

  1. 创建新配置文件

    • 点击VSCode左下角的 齿轮图标

    • 选择 配置文件 > 添加配置文件

      image-20250826103857012

    • 为新配置文件命名,例如“C/C++”。

      image-20250826104136844

    • 在弹出的选项中,可以选择从默认配置文件复制,这样无需重新安装插件,或者用官方推荐的插件列表

      image-20250826104239106

  2. 定制嵌入式专属环境
    切换到新创建的“C/C++”配置文件后:

    • 打开 扩展 侧边栏 (Ctrl+Shift+X)。
    • 将所有与C/C++嵌入式开发无关的插件禁用。例如Python、Jupyter、Prettier、Live Server等。
    • 仅保留核心插件,如:
      • C/C++
      • CMake Tools
  3. 无缝切换
    之后,你可以通过左下角的齿轮图标,在不同的配置文件之间快速切换。

    当你需要进行嵌入式开发时,切换到“C/C++”配置文件,VSCode会立刻关掉无关插件的负载,变得极其流畅和专注。