目录


硬件环境

项目 说明
板卡 TLT153-MiniEVM (NAND)
主控 全志 T153(4x Cortex-A7 1.608GHz + RISC-V E907)
内存 DDR3
存储 NAND
屏幕 10 寸 HDMI 触摸屏,物理分辨率 1024x600,触摸走 USB HID (event9)
桥接芯片 LT8912B(MIPI DSI → HDMI),已适配 720p 输出
实际渲染分辨率 1280x720 (720p),屏幕内部缩放到 1024x600
SDK aiot-t153-linux-v1.0
工具链 gcc-linaro-11.3.1-2022.06-x86_64_arm-linux-gnueabihf
LVGL 版本 v8.3.1 (SDK 自带)

背景

我的想法不是单纯学 LVGL API,而是把一个 UI 大屏项目当作载体,实践 Linux 嵌入式开发的完整流程:从 SDK 里的 demo 代码出发,搭建自己的多页面框架,解决交叉编译依赖、显示驱动适配、触摸输入配置、图片资源加载、性能优化等实际工程问题。

出发点是 SDK 自带的 lv_monitor 示例(系统监控面板),在此基础上做了以下事情:

  1. 搭建基于 lv_tileview 的多页面滑动框架
  2. 新增 Home、Clock、Sys Info、Gallery 四个页面
  3. 解决 PNG 背景图加载、内存不足、滑动卡顿等问题
  4. 实现顶部导航栏(页面标题 + 网络状态 + 时间 + Home 按钮)
  5. 脱离 Buildroot,直接用交叉编译链编译

整体架构

┌─────────────────────────────────────────────┐
│              顶部导航栏 (top_bar)             │
│  [Home]  [标题]          [Net] [HH:MM:SS]   │
├─────────────────────────────────────────────┤
│                                             │
│              lv_tileview (水平滑动)           │
│                                             │
│   tile 0      tile 1      tile 2    tile 3  │
│   Home        Clock     Sys Info   Gallery  │
│  (5个按钮)   (时钟+日历) (板卡信息)  (背景图) │
│                                             │
└─────────────────────────────────────────────┘
          ↑ 底层: scr 上的 lv_img 背景图 (共享)

页面顺序和滑动方向:

Home ←→ Clock ←→ Sys Info ←→ Gallery
 (0)      (1)       (2)        (3)

1. 工程基础搭建

1.1 脱离 Buildroot 独立编译

SDK 的 lv_monitor 本来通过 Buildroot 编译,依赖 TPlayer 等闭源库。为了快速迭代,改成直接用交叉编译链:

make CC=/path/to/arm-linux-gnueabihf-gcc

处理 TPlayer 依赖:在 Makefile 的 CFLAGS-DNO_TPLAYER,代码中用 #ifndef NO_TPLAYER 包裹 TPlayer 相关调用,Makefile 中排除 player_int.cmedia_file_play.crecorder_int.c

1.2 Makefile 改造

编译产物输出到 build/ 目录,源码目录保持干净:

BUILD_DIR ?= build
BIN = $(BUILD_DIR)/lv_monitor

# .o 文件映射到 build/ 下
AOBJS = $(addprefix $(BUILD_DIR)/,$(ASRCS:.S=$(OBJEXT)))
COBJS = $(addprefix $(BUILD_DIR)/,$(CSRCS:.c=$(OBJEXT)))
MAINOBJ = $(addprefix $(BUILD_DIR)/,$(MAINSRC:.c=$(OBJEXT)))

$(BUILD_DIR)/%.o: %.c
	@mkdir -p $(dir $@)
	@$(CC) $(CFLAGS) -c $< -o $@

clean:
	rm -rf $(BUILD_DIR)

1.3 触摸输入配置

lv_drv_conf.h 中修改 evdev 设备路径:

#define EVDEV_NAME   "/dev/input/event9"   /* USB 触摸屏 */

2. 页面管理器 page_manager

核心思路:用 lv_tileview 实现水平滑动切页,每个 tile 是一个独立页面。

2.1 page_manager.h 接口设计

/* 页面索引常量 */
#define PAGE_IDX_HOME     0
#define PAGE_IDX_CLOCK    1
#define PAGE_IDX_SYSINFO  2
#define PAGE_IDX_GALLERY  3

void page_manager_init(void);
void page_manager_destroy(void);
lv_obj_t * page_manager_get_tile(uint32_t index);
void page_manager_goto(uint32_t index);   /* 跳转到指定页面 */

2.2 tile 创建与滑动方向

/* tile 0: 主界面, 只能右滑 */
tiles[0] = lv_tileview_add_tile(tileview, 0, 0, LV_DIR_RIGHT);

/* tile 1: 时钟, 左右都能滑 */
tiles[1] = lv_tileview_add_tile(tileview, 1, 0, LV_DIR_LEFT | LV_DIR_RIGHT);

/* tile 2: 系统信息, 左右都能滑 */
tiles[2] = lv_tileview_add_tile(tileview, 2, 0, LV_DIR_LEFT | LV_DIR_RIGHT);

/* tile 3: 图片展示, 只能左滑返回 */
tiles[3] = lv_tileview_add_tile(tileview, 3, 0, LV_DIR_LEFT);

2.3 页面跳转

通过 page_manager_goto() 实现按钮点击跳转(不仅是滑动):

void page_manager_goto(uint32_t index)
{
    if (index < PAGE_COUNT && tileview) {
        lv_obj_set_tile(tileview, tiles[index], LV_ANIM_ON);
        lv_label_set_text(title_label, page_names[index]);
    }
}

3. 顶部导航栏

50px 高的半透明黑色条,覆盖在 tileview 之上,包含四个元素:

位置 元素 说明
左侧 页面标题 随 tileview 滑动自动切换
右侧 Home 按钮 点击跳转回主界面
右侧 网络状态 读取 /sys/class/net/eth0/operstate,绿色 up / 红色 down
最右 当前时间 HH:MM:SS,1 秒刷新

关键实现:

/* 导航栏置于 tileview 之上 */
lv_obj_move_foreground(top_bar);

/* 滑动时自动更新标题 */
static void tileview_changed_cb(lv_event_t *e)
{
    lv_obj_t *act_tile = lv_tileview_get_tile_act(lv_event_get_target(e));
    for (int i = 0; i < PAGE_COUNT; i++) {
        if (act_tile == tiles[i]) {
            lv_label_set_text(title_label, page_names[i]);
            break;
        }
    }
}

4. 背景图加载与性能优化

这部分踩了不少坑,是实际工程中最典型的问题。

4.1 PNG 图片加载

LVGL 通过 POSIX 文件系统驱动加载 PNG:

/* lv_conf.h 配置 */
#define LV_USE_FS_POSIX   1
#define LV_FS_POSIX_LETTER 'A'      /* 通过 "A:/path" 访问 */
#define LV_USE_PNG         1         /* 启用 lodepng */
#define LV_MEM_CUSTOM      1         /* 使用系统 malloc */

背景图路径:A:/root/res/bg_720p.png(1280x720 PNG)

4.2 内存不足导致 “no data”

首次加载 PNG 时屏幕纯白,左上角显示 “no data”。解码器能识别图片 header(1280x720, cf=5),但无法分配解码缓冲区。

原因:LVGL 默认内存池 LV_MEM_SIZE = 128KB,而 1280×720×4 字节 = 3.6MB。

解法:改 LV_MEM_CUSTOM = 1,让 LVGL 用系统 malloc/free,不受固定内存池限制。

4.3 滑动卡顿优化

第一版在每个 tile 里各放一张 PNG 背景图,滑动时需要同时解码渲染两张全屏 PNG,非常卡。

优化方案:

  1. 单张背景图共享:只在 scr 底层放一张 lv_img,所有 tile 设为透明
  2. 开启图片缓存LV_IMG_CACHE_DEF_SIZE = 4,解码一次后缓存
/* scr 底层唯一背景图 */
bg_img = lv_img_create(scr);
lv_img_set_src(bg_img, BG_IMAGE_PATH);

/* tileview 和所有 tile 透明 */
lv_obj_set_style_bg_opa(tileview, LV_OPA_TRANSP, 0);
lv_obj_set_style_bg_opa(tiles[i], LV_OPA_TRANSP, 0);

优化后滑动流畅,触摸响应正常。

[待插入图片]** 背景图加载成功的效果


5. Home 主界面

仿车机主界面风格,5 个圆角彩色按钮水平等距排列。

5.1 按钮配色

参考车机 UI 的渐变色块:

按钮 颜色 文字 功能
1 紫色 (0x8B2FC9) Clock 跳转时钟页
2 靛蓝 (0x1A3A8A) Sys Info 跳转系统信息页
3 绿色 (0x2E9B3E) 3 预留
4 深红 (0xA02040) 4 预留
5 蓝灰 (0x3A5A7A) 5 预留

5.2 按钮样式

每个按钮有垂直渐变、高光边框、阴影和按下反馈:

/* 渐变 */
lv_obj_set_style_bg_grad_color(btn,
    lv_color_darken(lv_color_hex(btn_colors[i]), 80), 0);
lv_obj_set_style_bg_grad_dir(btn, LV_GRAD_DIR_VER, 0);

/* 按下效果 */
lv_obj_set_style_bg_color(btn,
    lv_color_darken(lv_color_hex(btn_colors[i]), 40),
    LV_STATE_PRESSED);

5.3 点击跳转

static void btn_click_cb(lv_event_t *e)
{
    int idx = (int)(intptr_t)lv_event_get_user_data(e);
    switch (idx) {
    case 0: page_manager_goto(PAGE_IDX_CLOCK);   break;
    case 1: page_manager_goto(PAGE_IDX_SYSINFO); break;
    }
}

在这里插入图片描述
Home 主界面 5 个按钮的实际效果


6. Clock 时钟页面

左侧模拟时钟 + 数字时间/日期/星期,右侧日历控件。

6.1 模拟时钟

lv_meter 实现,灰色底 (#505050),三根指针:

指针 颜色 粗细
时针 绿色 (0x00ff00) 5px
分针 黄绿 (0xe1ff00) 3px
秒针 红色 (0xff4444) 2px

刻度:61 个小刻度(每 5 个一个大刻度),映射 12 小时制。

6.2 日历控件

lv_calendar,灰色底 (#505050),白色日期文字,头部高亮色 #BBFF00

6.3 1 秒定时器

clock_timer = lv_timer_create(clock_timer_cb, 1000, NULL);

回调中同时更新时钟指针、数字时间、日期、星期和日历。

[待插入图片]* 时钟页面完整效果(时钟 + 日历)


7. Sys Info 系统信息页面

灰色圆角面板 (#404040),白色文字,显示以下信息:

类别 数据来源 刷新
板卡型号 硬编码 静态
CPU 型号 硬编码 静态
内核版本 /proc/version 5 秒
运行时间 /proc/uptime 5 秒
内存使用 /proc/meminfo 5 秒
CPU 温度 /sys/class/thermal/thermal_zone0/temp 5 秒
系统负载 /proc/loadavg 5 秒

面板支持滚动查看(内容超出面板高度时)。

📷 **[待插入图片]**
系统信息页面完整效果


8. Gallery 图片展示页面

最简单的页面,只有底层共享背景图透出 + 一个 << swipe left 返回提示。用于验证背景图在纯透明 tile 上的显示效果,也作为后续图片浏览功能的预留位。


9. lv_conf.h 关键配置

从默认配置到可用,改了以下关键项:

配置项 默认值 修改后 原因
LV_MEM_CUSTOM 0 1 使用系统 malloc,PNG 解码需要 3.6MB
LV_USE_FS_POSIX 0 1 通过 POSIX 文件系统加载图片
LV_FS_POSIX_LETTER - ‘A’ 文件路径前缀
LV_USE_PNG 0 1 PNG 图片解码
LV_USE_SJPG 0 1 JPEG 图片解码
LV_IMG_CACHE_DEF_SIZE 0 4 图片缓存,避免重复解码
LV_USE_CALENDAR 0 1 日历控件
LV_USE_METER 0 1 仪表盘控件(模拟时钟)
LV_FONT_MONTSERRAT_16 0 1 导航栏网络状态字体
LV_FONT_MONTSERRAT_28 0 1 按钮文字字体
LV_FONT_MONTSERRAT_36 0 1 数字时间字体
LV_FONT_MONTSERRAT_48 0 1 大屏数字时间字体
LV_FONT_SIMSUN_16_CJK 0 1 中文字体(预留)

实际运行视频

实际运行效果

仓库开源地址

https://gitee.com/zkppp/t153_-lvgl_-desktop

踩坑记录

坑 1:PNG 解码 “no data”,纯白屏幕

  • 现象:lv_img_decoder_get_info() 返回成功 (1280x720, cf=5),但渲染出来是纯白 + 左上角 “no data”
  • 原因:LV_MEM_SIZE = 128KB,解码 1280×720 ARGB32 需要约 3.6MB
  • 解法:LV_MEM_CUSTOM = 1,让 LVGL 用系统 malloc
  • 附带问题:改完后 lv_deinit() 编译报错(该函数在 LV_MEM_CUSTOM=1 时不编译),直接去掉调用即可

坑 2:3 张 PNG 背景导致滑动卡顿

  • 现象:每个 tile 各放一张 1280×720 PNG,滑动非常卡顿
  • 原因:LV_IMG_CACHE_DEF_SIZE = 0(无缓存),滑动时同时解码 2 张全屏 PNG
  • 解法:只在 scr 底层放 1 张背景图,所有 tile 透明;同时开启图片缓存

坑 3:交叉编译后 Exec format error

  • 现象:推送到板子运行 ./lv_monitorcannot execute binary file: Exec format error
  • 原因:Makefile 里 CC ?= gcc 默认用了宿主机的 x86 gcc
  • 解法:编译时显式指定 make CC=/path/to/arm-linux-gnueabihf-gcc

坑 4:中文标题乱码

  • 现象:导航栏中文标题显示为方块
  • 原因:Montserrat 字体不含中文字形
  • 临时方案:标题改用英文;启用了 LV_FONT_SIMSUN_16_CJK(16px 中文字体),后续可用 LVGL 字体工具生成更大号中文字体

坑 5:lv_font_montserrat_xx 链接报错

  • 现象:undefined reference to 'lv_font_montserrat_28'
  • 原因:代码里用了某个字号但 lv_conf.h 里对应宏是 0
  • 解法:把用到的字号在 lv_conf.h 里全部改成 1,make clean 全量重编

验证结果

  • 四个页面正常显示,滑动切换流畅
  • 背景图加载成功,不再卡顿
  • Home 按钮跳转 Clock 和 Sys Info 正常
  • 导航栏标题随页面滑动自动切换
  • 导航栏 Home 按钮可从任意页面返回主界面
  • 网络状态实时显示 (eth0 up/down)
  • 时间实时刷新
  • 时钟指针和日历日期同步
  • 系统信息动态刷新(温度/内存/负载)
  • 触摸操作正常 (event9 USB 触摸屏)

涉及文件

文件 说明
main.c 程序入口,初始化驱动,创建 4 个页面
page_manager.c/h 页面管理器,tileview + 导航栏 + 跳转 API
page_home.c/h 主界面,5 个彩色按钮
page_clock.c/h 时钟页面,模拟时钟 + 日历
page_sysinfo.c/h 系统信息,灰色面板 + 读取 /proc /sys
lv_conf.h LVGL 配置(内存/字体/文件系统/图片解码)
lv_drv_conf.h 驱动配置(evdev 触摸屏路径)
Makefile 编译脚本(build/ 输出、排除 TPlayer)
res/bg_720p.png 1280x720 背景图片资源

总结

  1. lv_tileview 是做多页面滑动切换的最简单方案,每个 tile 独立管理自己的 UI 元素,页面间解耦干净
  2. LVGL 在嵌入式 Linux 上加载大图(>720p PNG)必须用系统 malloc(LV_MEM_CUSTOM=1),默认 128KB 内存池完全不够
  3. 多页面共享同一张背景图的正确做法是把 lv_img 放在 scr 最底层,上层 tileview 和 tile 全部透明,而不是每个 tile 各放一张
  4. 脱离 Buildroot 独立编译的核心是处理好闭源依赖(条件编译排除)和交叉编译器路径
  5. 导航栏放在 tileview 之上(lv_obj_move_foreground),但要注意不拦截触摸事件,否则会影响下方的滑动操作
  6. 定时器(lv_timer_create)是刷新动态数据的标准方式,时钟 1 秒刷新,系统信息 5 秒刷新

后续

  • 为 Home 按钮 3~5 添加实际功能(音视频播放、摄像头预览等)
  • 制作自定义大号中文字体(用 LVGL 字体转换工具从 TTF 生成)
  • 将背景图从 PNG 转为 LVGL bin 格式,进一步减少加载时间
  • 集成原有 lv_monitor 系统测试功能到 Sys Info 页面
  • 尝试开机自启动和全屏无边框运行
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐