1. LVGL移植概述与准备工作
在嵌入式开发中,图形用户界面(GUI)的实现一直是开发者面临的挑战之一。LVGL(Light and Versatile Graphics Library)作为一款开源的嵌入式图形库,因其轻量级、高性能和丰富的控件库而广受欢迎。本次我们将基于STM32F103系列开发板(野火指南者)进行LVGL v8版本的完整移植。
1.1 为什么选择LVGL?
LVGL相比其他嵌入式GUI具有以下优势:
- 内存占用小:最低仅需32KB Flash和8KB RAM即可运行
- 硬件要求低:支持单缓冲、双缓冲甚至直接绘制模式
- 控件丰富:内置按钮、图表、列表等30+控件
- 跨平台:支持STM32、ESP32等多种MCU
- 开源免费:采用MIT许可证
1.2 硬件准备清单
本次移植所需硬件:
- 野火指南者开发板(STM32F103VET6)
- 3.2寸TFT LCD(ILI9341驱动)
- 电阻触摸屏(XPT2046控制器)
- ST-Link调试器
- USB转串口模块(可选,用于调试输出)
提示:不同型号的开发板需要调整引脚配置,但整体移植流程相同
1.3 软件环境搭建
开发环境配置步骤:
- 安装Keil MDK-ARM(建议V5.25+)
- 安装STM32CubeMX(V6.5.0+)
- 下载LVGL源码:
git clone https://github.com/lvgl/lvgl.git - 准备野火/正点原子LCD例程(用于参考底层驱动)
2. LVGL工程目录结构解析
2.1 源码目录结构
LVGL源码主要包含以下关键目录:
code复制lvgl/
├── src/ # 核心源码
│ ├── core/ # 核心系统
│ ├── widgets/ # 控件实现
│ ├── draw/ # 绘制引擎
│ └── ... # 其他模块
├── examples/ # 示例代码
│ └── porting/ # 移植模板
└── lv_conf.h # 配置文件
2.2 必须关注的移植文件
移植过程中需要重点修改的文件:
lv_conf.h- 库功能配置lv_port_disp.c- 显示接口实现lv_port_indev.c- 输入设备接口lv_port_fs.c- 文件系统接口(可选)
3. 详细移植步骤
3.1 工程目录创建
在Keil工程中创建如下结构:
code复制Project/
├── Drivers/ # HAL库文件
├── Inc/ # 头文件
│ └── lvgl/ # LVGL头文件
├── Src/ # 源文件
│ └── lvgl/ # LVGL源文件
└── Middlewares/ # 中间件
└── LVGL/ # LVGL库文件
具体操作:
- 在工程目录创建
lvgl文件夹 - 在
lvgl下创建porting和lib子目录 - 将LVGL源码中的
src目录复制到lib下 - 将
examples/porting下的文件复制到porting目录
3.2 配置文件修改
关键配置项(lv_conf.h):
c复制#define LV_COLOR_DEPTH 16 // 颜色深度(16bit)
#define LV_HOR_RES_MAX 320 // 水平分辨率
#define LV_VER_RES_MAX 240 // 垂直分辨率
#define LV_USE_PERF_MONITOR 1 // 启用性能监控
#define LV_USE_MEM_MONITOR 1 // 启用内存监控
注意:配置文件中所有选项必须明确赋值,不能保留注释状态
3.3 显示接口实现
修改lv_port_disp.c中的关键函数:
c复制static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
/* 将颜色缓冲区内容复制到显存 */
LCD_Color_Fill(area->x1, area->y1, area->x2, area->y2, (uint16_t*)color_p);
/* 通知LVGL刷新完成 */
lv_disp_flush_ready(disp_drv);
}
3.4 输入设备配置
触摸屏接口实现(lv_port_indev.c):
c复制static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
static int16_t last_x = 0;
static int16_t last_y = 0;
/* 读取触摸坐标 */
uint8_t pressed = TP_Scan(0);
if(pressed) {
last_x = tp_dev.x[0];
last_y = tp_dev.y[0];
}
data->point.x = last_x;
data->point.y = last_y;
data->state = pressed ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;
}
3.5 系统时钟配置
在定时器中断中添加tick更新:
c复制void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM3) {
lv_tick_inc(1); // 每1ms调用一次
}
}
4. 驱动适配与优化
4.1 显示驱动适配
针对野火开发板的特殊适配:
c复制void LCD_Color_Fill(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t *color)
{
LCD_Set_Window(x1, y1, x2, y2);
LCD_WriteRAM_Prepare();
uint32_t size = (x2 - x1 + 1) * (y2 - y1 + 1);
while(size--) {
LCD->LCD_RAM = *color++;
}
}
4.2 内存优化策略
针对STM32F1的优化方案:
- 使用单缓冲模式:
c复制static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[LV_HOR_RES_MAX * 10]; // 行缓冲
lv_disp_draw_buf_init(&draw_buf, buf, NULL, LV_HOR_RES_MAX * 10);
- 启用LVGL的内存管理:
c复制#define LV_MEM_SIZE (8 * 1024) // 分配8KB内存池
5. GUI生成与集成
5.1 使用GUI Guider设计界面
操作流程:
- 下载安装GUI Guider(NXP官方工具)
- 创建新项目,选择LVGL v8
- 拖拽控件设计界面
- 导出工程文件(生成custom和generated目录)
5.2 工程文件集成
将生成的文件加入Keil工程:
- 复制custom和generated到工程目录
- 在Keil中添加文件到项目
- 包含头文件路径
- 在main.c中调用生成的初始化函数:
c复制void custom_init(lv_ui *ui)
{
setup_ui(ui);
lv_task_handler();
}
6. 常见问题与解决方案
6.1 显示异常问题排查
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 花屏 | 颜色格式不匹配 | 检查LV_COLOR_DEPTH与LCD驱动一致 |
| 局部刷新异常 | 显存地址错误 | 验证LCD_Color_Fill函数实现 |
| 屏幕偏移 | 分辨率设置错误 | 核对LV_HOR/VER_RES_MAX值 |
6.2 触摸屏校准问题
校准步骤:
- 实现触摸屏原始数据读取
- 在
touchpad_read中添加校准算法:
c复制// 校准系数(需通过校准程序获取)
#define CAL_X0 100
#define CAL_Y0 120
#define CAL_X1 3000
#define CAL_Y1 3200
data->point.x = (last_x - CAL_X0) * LV_HOR_RES_MAX / (CAL_X1 - CAL_X0);
data->point.y = (last_y - CAL_Y0) * LV_VER_RES_MAX / (CAL_Y1 - CAL_Y0);
6.3 性能优化技巧
- 启用双缓冲(当内存充足时):
c复制static lv_color_t buf1[LV_HOR_RES_MAX * LV_VER_RES_MAX / 10];
static lv_color_t buf2[LV_HOR_RES_MAX * LV_VER_RES_MAX / 10];
lv_disp_draw_buf_init(&draw_buf, buf1, buf2, sizeof(buf1)/sizeof(lv_color_t));
- 优化刷新区域:
c复制disp_drv->full_refresh = 0; // 禁用全屏刷新
- 使用硬件加速(如DMA2D):
c复制void disp_flush(...)
{
HAL_DMA2D_Start(&hdma2d, (uint32_t)color_p, (uint32_t)&LCD->RAM, area->x2 - area->x1 + 1, area->y2 - area->y1 + 1);
}
7. 实际应用示例
7.1 创建简单界面
不使用GUI Guider的手动创建示例:
c复制lv_obj_t * btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 100, 50);
lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0);
lv_obj_t * label = lv_label_create(btn);
lv_label_set_text(label, "Click Me!");
lv_obj_center(label);
lv_obj_add_event_cb(btn, btn_event_handler, LV_EVENT_ALL, NULL);
7.2 事件处理实现
按钮事件回调示例:
c复制static void btn_event_handler(lv_event_t * e)
{
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_CLICKED) {
static uint8_t cnt = 0;
cnt++;
lv_obj_t * btn = lv_event_get_target(e);
lv_obj_t * label = lv_obj_get_child(btn, 0);
lv_label_set_text_fmt(label, "Clicked: %d", cnt);
}
}
7.3 多页面管理
实现页面切换:
c复制lv_obj_t * create_page1(void)
{
lv_obj_t * page = lv_obj_create(NULL);
// 添加页面1内容
return page;
}
void switch_page(lv_ui *ui, uint8_t page_id)
{
lv_disp_t * disp = lv_disp_get_default();
switch(page_id) {
case 0: lv_disp_load_scr(ui->screen_main); break;
case 1: lv_disp_load_scr(create_page1()); break;
}
}
在STM32F1这类资源有限的MCU上成功移植LVGL需要特别注意内存管理和性能优化。通过合理配置缓冲区大小、使用高效的绘制策略以及充分利用硬件特性,完全可以实现流畅的GUI体验。实际项目中,建议先完成基础移植后,再逐步添加复杂功能。