跳转至

LVGL图形库快速入门:从零开始构建嵌入式GUI

学习目标

完成本教程后,你将能够:

  • 理解LVGL图形库的架构和工作原理
  • 在嵌入式项目中正确安装和配置LVGL
  • 实现LCD显示驱动并集成到LVGL
  • 配置触摸屏输入设备
  • 使用LVGL基本控件创建简单界面
  • 掌握LVGL的事件处理机制
  • 实现一个完整的GUI应用示例

前置要求

在开始本教程之前,你需要:

知识要求: - 熟悉C语言编程 - 了解嵌入式系统基础 - 掌握基本的显示器工作原理 - 了解SPI/I2C通信协议

技能要求: - 能够使用嵌入式开发IDE - 会配置GPIO和外设 - 具备基本的调试能力

准备工作

硬件准备

名称 数量 说明 参考型号
开发板 1 STM32F4系列或ESP32 STM32F407VGT6
LCD显示屏 1 2.4-3.5寸TFT屏 ILI9341/ST7789
触摸屏 1 电阻式或电容式 XPT2046/FT6236
杜邦线 若干 用于连接 -

软件准备

  • 开发环境:STM32CubeIDE / ESP-IDF / Arduino IDE
  • LVGL版本:v8.3+ (本教程使用v8.3)
  • 辅助工具
  • Git (用于下载LVGL源码)
  • 串口调试工具
  • LVGL模拟器 (可选,用于PC端测试)

环境配置

  1. 下载LVGL源码
# 克隆LVGL仓库
git clone https://github.com/lvgl/lvgl.git
cd lvgl
git checkout release/v8.3
  1. 项目目录结构
project/
├── lvgl/              # LVGL核心库
├── lv_drivers/        # 显示和输入驱动
├── lv_conf.h          # LVGL配置文件
├── main.c             # 主程序
└── drivers/           # 硬件驱动
    ├── lcd_driver.c   # LCD驱动
    └── touch_driver.c # 触摸驱动

步骤1:LVGL库集成

1.1 添加LVGL源码到项目

  1. 将下载的 lvgl 文件夹复制到项目目录
  2. 在IDE中添加LVGL源文件路径
  3. 复制 lvgl/lv_conf_template.hlv_conf.h 到项目根目录

1.2 配置lv_conf.h

打开 lv_conf.h 文件,进行基本配置:

/* 启用LVGL配置 */
#if 1  // 改为1启用配置

/* 颜色深度设置 */
#define LV_COLOR_DEPTH 16  // 16位色深(RGB565)

/* 显示分辨率 */
#define LV_HOR_RES_MAX 240  // 水平分辨率
#define LV_VER_RES_MAX 320  // 垂直分辨率

/* 内存配置 */
#define LV_MEM_CUSTOM 0     // 使用LVGL内置内存管理
#define LV_MEM_SIZE (32U * 1024U)  // 分配32KB内存

/* 刷新周期 */
#define LV_DISP_DEF_REFR_PERIOD 30  // 30ms刷新一次

/* 启用常用控件 */
#define LV_USE_BTN 1        // 按钮
#define LV_USE_LABEL 1      // 标签
#define LV_USE_SLIDER 1     // 滑块
#define LV_USE_SWITCH 1     // 开关
#define LV_USE_TEXTAREA 1   // 文本框

/* 字体配置 */
#define LV_FONT_MONTSERRAT_14 1  // 14号字体
#define LV_FONT_MONTSERRAT_16 1  // 16号字体

#endif

配置说明: - LV_COLOR_DEPTH: 根据LCD支持的色深设置(8/16/32) - LV_HOR_RES_MAX/LV_VER_RES_MAX: 设置为LCD的实际分辨率 - LV_MEM_SIZE: 根据可用RAM调整,建议至少16KB - 控件宏定义: 只启用需要的控件可减小代码体积

1.3 初始化LVGL

main.c 中添加LVGL初始化代码:

#include "lvgl.h"
#include "lv_port_disp.h"
#include "lv_port_indev.h"

/* 定义显示缓冲区 */
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf1[LV_HOR_RES_MAX * 10];  // 缓冲区1
static lv_color_t buf2[LV_HOR_RES_MAX * 10];  // 缓冲区2(可选)

int main(void)
{
    /* 系统初始化 */
    HAL_Init();
    SystemClock_Config();

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

    /* 初始化显示缓冲区 */
    lv_disp_draw_buf_init(&draw_buf, buf1, buf2, LV_HOR_RES_MAX * 10);

    /* 注册显示驱动 */
    lv_port_disp_init();

    /* 注册输入设备驱动 */
    lv_port_indev_init();

    /* 创建UI */
    ui_init();

    /* 主循环 */
    while(1)
    {
        lv_timer_handler();  // 处理LVGL任务
        HAL_Delay(5);        // 延时5ms
    }
}

代码说明: - lv_init(): 初始化LVGL库 - lv_disp_draw_buf_init(): 初始化显示缓冲区,使用双缓冲可提高性能 - lv_timer_handler(): LVGL任务处理函数,需要周期性调用(建议5-10ms) - 缓冲区大小: 屏幕宽度 × 行数,行数越多性能越好但占用内存越大

步骤2:实现显示驱动

2.1 创建显示驱动文件

创建 lv_port_disp.clv_port_disp.h 文件。

lv_port_disp.h:

#ifndef LV_PORT_DISP_H
#define LV_PORT_DISP_H

#include "lvgl.h"

void lv_port_disp_init(void);

#endif

2.2 实现显示驱动

lv_port_disp.c:

#include "lv_port_disp.h"
#include "lcd_driver.h"  // 你的LCD驱动头文件

/* 显示刷新回调函数 */
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    /* 获取刷新区域 */
    int32_t x1 = area->x1;
    int32_t y1 = area->y1;
    int32_t x2 = area->x2;
    int32_t y2 = area->y2;

    /* 设置LCD显示窗口 */
    LCD_SetWindow(x1, y1, x2, y2);

    /* 计算像素数量 */
    uint32_t size = (x2 - x1 + 1) * (y2 - y1 + 1);

    /* 发送像素数据到LCD */
    LCD_WriteData((uint16_t*)color_p, size);

    /* 通知LVGL刷新完成 */
    lv_disp_flush_ready(disp_drv);
}

void lv_port_disp_init(void)
{
    /* 初始化LCD硬件 */
    LCD_Init();

    /* 创建显示驱动 */
    static lv_disp_drv_t disp_drv;
    lv_disp_drv_init(&disp_drv);

    /* 设置分辨率 */
    disp_drv.hor_res = LV_HOR_RES_MAX;
    disp_drv.ver_res = LV_VER_RES_MAX;

    /* 注册刷新回调函数 */
    disp_drv.flush_cb = disp_flush;

    /* 设置显示缓冲区 */
    extern lv_disp_draw_buf_t draw_buf;
    disp_drv.draw_buf = &draw_buf;

    /* 注册显示驱动 */
    lv_disp_drv_register(&disp_drv);
}

2.3 LCD底层驱动示例

以ILI9341为例,实现基本的LCD驱动函数:

/* lcd_driver.c */
#include "lcd_driver.h"

/* LCD控制引脚定义 */
#define LCD_CS_PIN   GPIO_PIN_4
#define LCD_DC_PIN   GPIO_PIN_3
#define LCD_RST_PIN  GPIO_PIN_2

/* 写命令 */
void LCD_WriteCmd(uint8_t cmd)
{
    HAL_GPIO_WritePin(GPIOA, LCD_DC_PIN, GPIO_PIN_RESET);  // DC=0
    HAL_GPIO_WritePin(GPIOA, LCD_CS_PIN, GPIO_PIN_RESET);  // CS=0
    HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
    HAL_GPIO_WritePin(GPIOA, LCD_CS_PIN, GPIO_PIN_SET);    // CS=1
}

/* 写数据 */
void LCD_WriteData(uint16_t *data, uint32_t len)
{
    HAL_GPIO_WritePin(GPIOA, LCD_DC_PIN, GPIO_PIN_SET);    // DC=1
    HAL_GPIO_WritePin(GPIOA, LCD_CS_PIN, GPIO_PIN_RESET);  // CS=0
    HAL_SPI_Transmit(&hspi1, (uint8_t*)data, len * 2, HAL_MAX_DELAY);
    HAL_GPIO_WritePin(GPIOA, LCD_CS_PIN, GPIO_PIN_SET);    // CS=1
}

/* 设置显示窗口 */
void LCD_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
{
    /* 列地址设置 */
    LCD_WriteCmd(0x2A);
    LCD_WriteData16(x1);
    LCD_WriteData16(x2);

    /* 行地址设置 */
    LCD_WriteCmd(0x2B);
    LCD_WriteData16(y1);
    LCD_WriteData16(y2);

    /* 写入显存 */
    LCD_WriteCmd(0x2C);
}

/* LCD初始化 */
void LCD_Init(void)
{
    /* 硬件复位 */
    HAL_GPIO_WritePin(GPIOA, LCD_RST_PIN, GPIO_PIN_RESET);
    HAL_Delay(100);
    HAL_GPIO_WritePin(GPIOA, LCD_RST_PIN, GPIO_PIN_SET);
    HAL_Delay(100);

    /* ILI9341初始化序列 */
    LCD_WriteCmd(0x11);  // 退出睡眠模式
    HAL_Delay(120);

    LCD_WriteCmd(0x36);  // 内存访问控制
    LCD_WriteData8(0x48);

    LCD_WriteCmd(0x3A);  // 像素格式设置
    LCD_WriteData8(0x55);  // 16位色

    LCD_WriteCmd(0x29);  // 开启显示
    HAL_Delay(100);
}

驱动说明: - LCD_WriteCmd(): 发送命令到LCD控制器 - LCD_WriteData(): 发送像素数据 - LCD_SetWindow(): 设置要刷新的矩形区域 - 使用SPI接口通信,也可以使用并口(8080/6800)

步骤3:配置输入设备

3.1 创建输入设备驱动

创建 lv_port_indev.clv_port_indev.h 文件。

lv_port_indev.h:

#ifndef LV_PORT_INDEV_H
#define LV_PORT_INDEV_H

#include "lvgl.h"

void lv_port_indev_init(void);

#endif

3.2 实现触摸屏驱动

lv_port_indev.c:

#include "lv_port_indev.h"
#include "touch_driver.h"  // 触摸屏驱动

/* 触摸读取回调函数 */
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;

    /* 读取触摸状态 */
    if(Touch_Scan())
    {
        /* 触摸按下 */
        data->state = LV_INDEV_STATE_PRESSED;

        /* 读取触摸坐标 */
        data->point.x = Touch_GetX();
        data->point.y = Touch_GetY();

        /* 保存坐标 */
        last_x = data->point.x;
        last_y = data->point.y;
    }
    else
    {
        /* 触摸释放 */
        data->state = LV_INDEV_STATE_RELEASED;

        /* 使用上次坐标 */
        data->point.x = last_x;
        data->point.y = last_y;
    }
}

void lv_port_indev_init(void)
{
    /* 初始化触摸屏硬件 */
    Touch_Init();

    /* 创建输入设备驱动 */
    static lv_indev_drv_t indev_drv;
    lv_indev_drv_init(&indev_drv);

    /* 设置输入设备类型 */
    indev_drv.type = LV_INDEV_TYPE_POINTER;  // 指针设备(触摸屏)

    /* 注册读取回调函数 */
    indev_drv.read_cb = touchpad_read;

    /* 注册输入设备 */
    lv_indev_drv_register(&indev_drv);
}

3.3 触摸屏底层驱动示例

以XPT2046电阻式触摸屏为例:

/* touch_driver.c */
#include "touch_driver.h"

#define TOUCH_CS_PIN  GPIO_PIN_5

/* 触摸坐标 */
static uint16_t touch_x = 0;
static uint16_t touch_y = 0;

/* SPI读写 */
static uint16_t Touch_ReadAD(uint8_t cmd)
{
    uint16_t data = 0;

    HAL_GPIO_WritePin(GPIOA, TOUCH_CS_PIN, GPIO_PIN_RESET);
    HAL_SPI_Transmit(&hspi2, &cmd, 1, HAL_MAX_DELAY);
    HAL_SPI_Receive(&hspi2, (uint8_t*)&data, 2, HAL_MAX_DELAY);
    HAL_GPIO_WritePin(GPIOA, TOUCH_CS_PIN, GPIO_PIN_SET);

    return data >> 3;  // 12位数据
}

/* 扫描触摸 */
bool Touch_Scan(void)
{
    /* 读取触摸状态引脚 */
    if(HAL_GPIO_ReadPin(GPIOA, TOUCH_IRQ_PIN) == GPIO_PIN_RESET)
    {
        /* 读取X坐标 */
        uint16_t x1 = Touch_ReadAD(0x90);
        uint16_t x2 = Touch_ReadAD(0x90);

        /* 读取Y坐标 */
        uint16_t y1 = Touch_ReadAD(0xD0);
        uint16_t y2 = Touch_ReadAD(0xD0);

        /* 滤波处理 */
        if(abs(x1 - x2) < 50 && abs(y1 - y2) < 50)
        {
            touch_x = (x1 + x2) / 2;
            touch_y = (y1 + y2) / 2;

            /* 坐标转换和校准 */
            touch_x = (touch_x - X_MIN) * LV_HOR_RES_MAX / (X_MAX - X_MIN);
            touch_y = (touch_y - Y_MIN) * LV_VER_RES_MAX / (Y_MAX - Y_MIN);

            return true;
        }
    }

    return false;
}

/* 获取坐标 */
uint16_t Touch_GetX(void) { return touch_x; }
uint16_t Touch_GetY(void) { return touch_y; }

/* 触摸初始化 */
void Touch_Init(void)
{
    /* 配置SPI和GPIO */
    // ...
}

触摸驱动说明: - 电阻式触摸屏通过SPI读取AD值 - 需要进行坐标校准,将AD值映射到屏幕坐标 - 使用滤波算法减少抖动 - 电容式触摸屏(如FT6236)使用I2C接口,驱动方式类似

步骤4:创建基本界面

4.1 创建第一个标签

void ui_init(void)
{
    /* 创建标签对象 */
    lv_obj_t * label = lv_label_create(lv_scr_act());

    /* 设置标签文本 */
    lv_label_set_text(label, "Hello LVGL!");

    /* 设置标签位置(居中) */
    lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);

    /* 设置字体 */
    lv_obj_set_style_text_font(label, &lv_font_montserrat_16, 0);
}

运行结果: 屏幕中央显示 "Hello LVGL!" 文字。

4.2 创建按钮

/* 按钮点击事件回调 */
static void btn_event_cb(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);

    if(code == LV_EVENT_CLICKED)
    {
        /* 按钮被点击 */
        printf("Button clicked!\n");

        /* 获取按钮对象 */
        lv_obj_t * btn = lv_event_get_target(e);

        /* 获取按钮上的标签 */
        lv_obj_t * label = lv_obj_get_child(btn, 0);

        /* 修改标签文本 */
        lv_label_set_text(label, "Clicked!");
    }
}

void create_button(void)
{
    /* 创建按钮 */
    lv_obj_t * btn = lv_btn_create(lv_scr_act());

    /* 设置按钮大小 */
    lv_obj_set_size(btn, 120, 50);

    /* 设置按钮位置 */
    lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0);

    /* 添加事件回调 */
    lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL);

    /* 在按钮上创建标签 */
    lv_obj_t * label = lv_label_create(btn);
    lv_label_set_text(label, "Click Me");
    lv_obj_center(label);
}

代码说明: - lv_btn_create(): 创建按钮对象 - lv_obj_set_size(): 设置对象大小 - lv_obj_align(): 设置对象对齐方式 - lv_obj_add_event_cb(): 添加事件回调函数 - 事件类型包括: CLICKED, PRESSED, RELEASED, VALUE_CHANGED等

4.3 创建滑块控件

/* 滑块值改变事件 */
static void slider_event_cb(lv_event_t * e)
{
    lv_obj_t * slider = lv_event_get_target(e);

    /* 获取滑块当前值 */
    int32_t value = lv_slider_get_value(slider);

    /* 更新标签显示 */
    lv_obj_t * label = lv_event_get_user_data(e);
    lv_label_set_text_fmt(label, "Value: %d", value);
}

void create_slider(void)
{
    /* 创建滑块 */
    lv_obj_t * slider = lv_slider_create(lv_scr_act());

    /* 设置滑块大小和位置 */
    lv_obj_set_width(slider, 200);
    lv_obj_align(slider, LV_ALIGN_CENTER, 0, -30);

    /* 设置滑块范围 */
    lv_slider_set_range(slider, 0, 100);

    /* 设置初始值 */
    lv_slider_set_value(slider, 50, LV_ANIM_OFF);

    /* 创建显示值的标签 */
    lv_obj_t * label = lv_label_create(lv_scr_act());
    lv_label_set_text(label, "Value: 50");
    lv_obj_align(label, LV_ALIGN_CENTER, 0, 20);

    /* 添加值改变事件 */
    lv_obj_add_event_cb(slider, slider_event_cb, LV_EVENT_VALUE_CHANGED, label);
}

4.4 创建开关控件

/* 开关状态改变事件 */
static void switch_event_cb(lv_event_t * e)
{
    lv_obj_t * sw = lv_event_get_target(e);

    if(lv_obj_has_state(sw, LV_STATE_CHECKED))
    {
        printf("Switch ON\n");
        /* 执行开启操作 */
    }
    else
    {
        printf("Switch OFF\n");
        /* 执行关闭操作 */
    }
}

void create_switch(void)
{
    /* 创建开关 */
    lv_obj_t * sw = lv_switch_create(lv_scr_act());

    /* 设置位置 */
    lv_obj_align(sw, LV_ALIGN_CENTER, 0, 60);

    /* 添加事件 */
    lv_obj_add_event_cb(sw, switch_event_cb, LV_EVENT_VALUE_CHANGED, NULL);

    /* 创建标签 */
    lv_obj_t * label = lv_label_create(lv_scr_act());
    lv_label_set_text(label, "LED Control");
    lv_obj_align_to(label, sw, LV_ALIGN_OUT_LEFT_MID, -10, 0);
}

步骤5:完整示例 - 温度监控界面

5.1 界面设计

创建一个简单的温度监控界面,包含: - 温度显示标签 - 温度曲线图 - 风扇控制开关 - 温度阈值滑块

5.2 完整代码实现

/* 全局对象 */
static lv_obj_t * temp_label;
static lv_obj_t * chart;
static lv_chart_series_t * ser;
static lv_obj_t * fan_switch;
static lv_obj_t * threshold_slider;

/* 模拟温度读取 */
float read_temperature(void)
{
    /* 这里应该读取实际的温度传感器 */
    static float temp = 25.0;
    temp += (rand() % 20 - 10) / 10.0;  // 模拟温度变化
    if(temp < 20.0) temp = 20.0;
    if(temp > 40.0) temp = 40.0;
    return temp;
}

/* 风扇控制事件 */
static void fan_event_cb(lv_event_t * e)
{
    lv_obj_t * sw = lv_event_get_target(e);

    if(lv_obj_has_state(sw, LV_STATE_CHECKED))
    {
        /* 开启风扇 */
        printf("Fan ON\n");
        // HAL_GPIO_WritePin(FAN_GPIO, FAN_PIN, GPIO_PIN_SET);
    }
    else
    {
        /* 关闭风扇 */
        printf("Fan OFF\n");
        // HAL_GPIO_WritePin(FAN_GPIO, FAN_PIN, GPIO_PIN_RESET);
    }
}

/* 阈值滑块事件 */
static void threshold_event_cb(lv_event_t * e)
{
    lv_obj_t * slider = lv_event_get_target(e);
    int32_t value = lv_slider_get_value(slider);

    lv_obj_t * label = lv_event_get_user_data(e);
    lv_label_set_text_fmt(label, "Threshold: %d°C", value);
}

/* 创建温度监控界面 */
void create_temp_monitor_ui(void)
{
    /* 创建标题 */
    lv_obj_t * title = lv_label_create(lv_scr_act());
    lv_label_set_text(title, "Temperature Monitor");
    lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);
    lv_obj_set_style_text_font(title, &lv_font_montserrat_16, 0);

    /* 创建温度显示 */
    temp_label = lv_label_create(lv_scr_act());
    lv_label_set_text(temp_label, "25.0°C");
    lv_obj_align(temp_label, LV_ALIGN_TOP_MID, 0, 40);
    lv_obj_set_style_text_font(temp_label, &lv_font_montserrat_24, 0);

    /* 创建温度曲线图 */
    chart = lv_chart_create(lv_scr_act());
    lv_obj_set_size(chart, 200, 100);
    lv_obj_align(chart, LV_ALIGN_CENTER, 0, -20);
    lv_chart_set_type(chart, LV_CHART_TYPE_LINE);
    lv_chart_set_range(chart, LV_CHART_AXIS_PRIMARY_Y, 0, 50);
    lv_chart_set_point_count(chart, 20);

    /* 添加数据系列 */
    ser = lv_chart_add_series(chart, lv_palette_main(LV_PALETTE_RED), 
                               LV_CHART_AXIS_PRIMARY_Y);

    /* 创建风扇控制 */
    lv_obj_t * fan_label = lv_label_create(lv_scr_act());
    lv_label_set_text(fan_label, "Fan:");
    lv_obj_align(fan_label, LV_ALIGN_BOTTOM_LEFT, 20, -60);

    fan_switch = lv_switch_create(lv_scr_act());
    lv_obj_align(fan_switch, LV_ALIGN_BOTTOM_LEFT, 70, -65);
    lv_obj_add_event_cb(fan_switch, fan_event_cb, LV_EVENT_VALUE_CHANGED, NULL);

    /* 创建阈值设置 */
    lv_obj_t * threshold_label = lv_label_create(lv_scr_act());
    lv_label_set_text(threshold_label, "Threshold: 30°C");
    lv_obj_align(threshold_label, LV_ALIGN_BOTTOM_LEFT, 20, -30);

    threshold_slider = lv_slider_create(lv_scr_act());
    lv_obj_set_width(threshold_slider, 180);
    lv_obj_align(threshold_slider, LV_ALIGN_BOTTOM_MID, 0, -10);
    lv_slider_set_range(threshold_slider, 20, 40);
    lv_slider_set_value(threshold_slider, 30, LV_ANIM_OFF);
    lv_obj_add_event_cb(threshold_slider, threshold_event_cb, 
                        LV_EVENT_VALUE_CHANGED, threshold_label);
}

5.3 定时更新温度数据

/* 定时器回调函数 */
static void temp_update_timer_cb(lv_timer_t * timer)
{
    /* 读取温度 */
    float temp = read_temperature();

    /* 更新温度显示 */
    lv_label_set_text_fmt(temp_label, "%.1f°C", temp);

    /* 更新曲线图 */
    lv_chart_set_next_value(chart, ser, (int32_t)temp);

    /* 检查温度阈值 */
    int32_t threshold = lv_slider_get_value(threshold_slider);
    if(temp > threshold)
    {
        /* 温度超过阈值,自动开启风扇 */
        if(!lv_obj_has_state(fan_switch, LV_STATE_CHECKED))
        {
            lv_obj_add_state(fan_switch, LV_STATE_CHECKED);
            printf("Auto fan ON: temp=%.1f, threshold=%d\n", temp, threshold);
        }
    }
}

/* 在ui_init中创建定时器 */
void ui_init(void)
{
    /* 创建界面 */
    create_temp_monitor_ui();

    /* 创建定时器,每1秒更新一次 */
    lv_timer_create(temp_update_timer_cb, 1000, NULL);
}

定时器说明: - lv_timer_create(): 创建LVGL定时器 - 第一个参数: 回调函数 - 第二个参数: 周期(毫秒) - 第三个参数: 用户数据 - LVGL定时器在 lv_timer_handler() 中自动调用

步骤6:样式和主题

6.1 自定义样式

void apply_custom_style(void)
{
    /* 创建样式对象 */
    static lv_style_t style_btn;
    lv_style_init(&style_btn);

    /* 设置背景颜色 */
    lv_style_set_bg_color(&style_btn, lv_palette_main(LV_PALETTE_BLUE));

    /* 设置圆角 */
    lv_style_set_radius(&style_btn, 10);

    /* 设置阴影 */
    lv_style_set_shadow_width(&style_btn, 10);
    lv_style_set_shadow_color(&style_btn, lv_palette_main(LV_PALETTE_BLUE));

    /* 创建按钮并应用样式 */
    lv_obj_t * btn = lv_btn_create(lv_scr_act());
    lv_obj_add_style(btn, &style_btn, 0);
    lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0);

    /* 添加标签 */
    lv_obj_t * label = lv_label_create(btn);
    lv_label_set_text(label, "Styled Button");
    lv_obj_center(label);
}

6.2 使用内置主题

void set_theme(void)
{
    /* 设置深色主题 */
    lv_theme_t * theme = lv_theme_default_init(
        lv_disp_get_default(),           // 显示设备
        lv_palette_main(LV_PALETTE_BLUE), // 主色调
        lv_palette_main(LV_PALETTE_RED),  // 次色调
        true,                             // 深色模式
        LV_FONT_DEFAULT                   // 字体
    );

    lv_disp_set_theme(lv_disp_get_default(), theme);
}

6.3 动画效果

/* 按钮点击动画 */
static void btn_anim_event_cb(lv_event_t * e)
{
    lv_obj_t * btn = lv_event_get_target(e);

    /* 创建动画 */
    lv_anim_t a;
    lv_anim_init(&a);
    lv_anim_set_var(&a, btn);
    lv_anim_set_time(&a, 300);
    lv_anim_set_values(&a, 100, 120);  // 从100%放大到120%
    lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out);

    /* 设置宽度动画 */
    lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_width);
    lv_anim_start(&a);

    /* 设置高度动画 */
    lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_height);
    lv_anim_start(&a);
}

步骤7:编译和测试

7.1 编译项目

  1. 确保所有文件已添加到项目
  2. 检查头文件路径配置
  3. 点击编译按钮

可能的编译错误

错误1: undefined reference to 'lv_xxx'
解决: 检查LVGL源文件是否全部添加到项目

错误2: lv_conf.h: No such file or directory
解决: 确保lv_conf.h在项目根目录,并添加到包含路径

错误3: #error "LV_CONF_INCLUDE_SIMPLE is not defined"
解决: 在lv_conf.h开头添加 #if 1

7.2 下载和运行

  1. 连接开发板
  2. 下载程序
  3. 观察LCD显示

预期结果: - LCD正常显示界面 - 触摸操作响应正常 - 控件交互正常

7.3 性能测试

/* 测试帧率 */
void test_fps(void)
{
    static uint32_t last_time = 0;
    static uint32_t frame_count = 0;

    frame_count++;

    uint32_t current_time = HAL_GetTick();
    if(current_time - last_time >= 1000)
    {
        printf("FPS: %d\n", frame_count);
        frame_count = 0;
        last_time = current_time;
    }
}

/* 在主循环中调用 */
while(1)
{
    lv_timer_handler();
    test_fps();
    HAL_Delay(5);
}

性能优化建议: - 增大显示缓冲区可提高刷新速度 - 使用DMA传输像素数据 - 减少不必要的重绘 - 优化显示驱动的SPI速度

故障排除

问题1:LCD无显示

可能原因: - 硬件连接错误 - LCD初始化失败 - 显示驱动配置错误 - 背光未开启

解决方法: 1. 检查LCD电源和信号线连接 2. 用万用表测试SPI信号 3. 检查LCD初始化序列是否正确 4. 确认背光引脚已配置并拉高 5. 在 disp_flush() 中添加调试信息

static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    printf("Flush: x1=%d, y1=%d, x2=%d, y2=%d\n", 
           area->x1, area->y1, area->x2, area->y2);
    // ...
}

问题2:触摸不响应

可能原因: - 触摸屏未正确连接 - 触摸驱动配置错误 - 坐标校准不准确 - 触摸中断未配置

解决方法: 1. 检查触摸屏连接 2. 测试触摸芯片通信 3. 重新校准触摸坐标 4. 在 touchpad_read() 中添加调试信息

static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
    if(Touch_Scan())
    {
        printf("Touch: x=%d, y=%d\n", Touch_GetX(), Touch_GetY());
        // ...
    }
}

问题3:显示闪烁或撕裂

可能原因: - 缓冲区太小 - 刷新频率不合适 - SPI速度太慢 - 未使用双缓冲

解决方法: 1. 增大显示缓冲区大小 2. 调整 LV_DISP_DEF_REFR_PERIOD 3. 提高SPI时钟频率 4. 启用双缓冲

/* 增大缓冲区 */
static lv_color_t buf1[LV_HOR_RES_MAX * 20];  // 20行
static lv_color_t buf2[LV_HOR_RES_MAX * 20];  // 双缓冲

问题4:内存不足

可能原因: - LV_MEM_SIZE 设置过大 - 创建了太多对象 - 缓冲区占用过多内存

解决方法: 1. 减小 LV_MEM_SIZE 2. 及时删除不用的对象 3. 减小显示缓冲区 4. 使用外部RAM

/* 监控内存使用 */
lv_mem_monitor_t mon;
lv_mem_monitor(&mon);
printf("Memory: used=%d, free=%d, frag=%d%%\n", 
       mon.used_cnt, mon.free_cnt, mon.frag_pct);

问题5:程序运行缓慢

可能原因: - lv_timer_handler() 调用频率不够 - 显示刷新太慢 - CPU主频太低 - 启用了太多动画效果

解决方法: 1. 确保 lv_timer_handler() 每5-10ms调用一次 2. 优化显示驱动 3. 提高CPU主频 4. 减少动画和特效

总结

通过本教程,你学习了:

  • ✅ LVGL图形库的基本架构和工作原理
  • ✅ 如何在嵌入式项目中集成LVGL
  • ✅ 显示驱动的实现方法和关键函数
  • ✅ 触摸屏输入设备的配置和校准
  • ✅ LVGL基本控件的使用(标签、按钮、滑块、开关)
  • ✅ 事件处理机制和回调函数
  • ✅ 定时器的使用和界面更新
  • ✅ 样式定制和主题设置
  • ✅ 完整GUI应用的开发流程

关键要点

  1. 显示驱动: 核心是实现 disp_flush() 回调函数,将LVGL的像素数据传输到LCD
  2. 输入设备: 实现 touchpad_read() 回调函数,读取触摸坐标并返回给LVGL
  3. 主循环: 必须周期性调用 lv_timer_handler(),建议5-10ms调用一次
  4. 内存管理: 合理配置 LV_MEM_SIZE 和显示缓冲区大小
  5. 性能优化: 使用双缓冲、DMA传输、提高SPI速度

进阶挑战

尝试以下挑战来巩固学习:

  1. 挑战1:多页面切换
  2. 创建多个界面页面
  3. 实现页面切换动画
  4. 使用 lv_scr_load_anim() 函数

  5. 挑战2:自定义控件

  6. 创建一个自定义的仪表盘控件
  7. 实现指针旋转动画
  8. 添加刻度和数值显示

  9. 挑战3:图片显示

  10. 使用LVGL图片转换工具
  11. 显示PNG/JPG图片
  12. 实现图片缩放和旋转

  13. 挑战4:中文字体支持

  14. 使用LVGL字体转换工具
  15. 生成中文字体文件
  16. 在界面中显示中文

  17. 挑战5:实时数据可视化

  18. 创建实时曲线图
  19. 实现数据滚动显示
  20. 添加数据统计功能

完整代码

完整的项目代码可以在这里下载:

  • GitHub仓库: lvgl-quickstart-example
  • 代码结构:
    lvgl-quickstart/
    ├── Core/
    │   ├── Src/
    │   │   └── main.c
    │   └── Inc/
    ├── lvgl/                    # LVGL核心库
    ├── lv_conf.h               # LVGL配置
    ├── lv_port_disp.c/h        # 显示驱动
    ├── lv_port_indev.c/h       # 输入设备驱动
    ├── lcd_driver.c/h          # LCD底层驱动
    ├── touch_driver.c/h        # 触摸底层驱动
    └── README.md
    

下一步

建议继续学习:

推荐资源

  1. 官方文档
  2. LVGL官方文档
  3. LVGL示例代码
  4. LVGL论坛

  5. 工具

  6. LVGL模拟器 - PC端测试工具
  7. 图片转换工具 - 在线图片转换
  8. 字体转换工具 - 在线字体转换

  9. 视频教程

  10. LVGL官方YouTube频道
  11. B站LVGL教程合集

  12. 参考书籍

  13. 《嵌入式GUI开发实战》
  14. 《LVGL图形库应用开发》

参考资料

  1. LVGL官方文档
  2. LVGL Documentation
  3. Porting Guide
  4. Display Interface
  5. Input Device Interface

  6. 硬件参考

  7. ILI9341 LCD Controller Datasheet
  8. ST7789 LCD Controller Datasheet
  9. XPT2046 Touch Controller Datasheet
  10. FT6236 Touch Controller Datasheet

  11. 开发板文档

  12. STM32F407 Reference Manual
  13. ESP32 Technical Reference Manual
  14. HAL Library User Manual

  15. 社区资源

  16. LVGL GitHub
  17. LVGL Forum
  18. LVGL Blog

  19. 相关教程

  20. SPI通信协议详解
  21. I2C通信协议详解
  22. DMA数据传输

反馈与支持

如果你在学习过程中遇到问题,欢迎通过以下方式获取帮助:

  • 💬 在评论区留言
  • 📧 发送邮件至 support@embedded-platform.com
  • 🐛 在GitHub提交Issue
  • 💡 在论坛发起讨论

贡献

欢迎为本教程贡献代码示例、改进建议或错误修正!


版权声明:本教程采用 CC BY-SA 4.0 许可协议。

最后更新: 2024-01-15
教程版本: 1.0
LVGL版本: 8.3+