欢迎光临
我们一直在努力

STM32+LVGL从零搭建GUI界面,CubeMX配置+移植+第一个界面

手把手教你把LVGL移植到STM32上,从CubeMX配置LCD驱动到显示第一个GUI界面,含完整代码和踩坑记录。

做嵌入式的同学,或多或少都接触过 LCD 屏幕。但直接用底层驱动画点、画线,效率太低了。LVGL(Light and Versatile Graphics Library)是一个开源的嵌入式 GUI 库,轻量、免费、效果好,是目前嵌入式 GUI 的首选方案。

这篇文章带你从零开始,把 LVGL 移植到 STM32F103 上,最终跑起来一个带按钮和标签的界面。


一、LVGL 是什么

特性说明
开源协议 MIT(免费商用)
最低 RAM 32KB(推荐 64KB+)
最低 Flash 256KB(推荐 512KB+)
支持的屏幕 TFT、OLED、黑白屏都支持
开发语言 C 语言
模拟器 有 PC 模拟器,不用硬件也能开发

适合的 MCU:STM32F4/F7/H7 系列效果最好;F103 可以跑但 RAM 紧张(只有 20KB),需要精简配置。


二、硬件准备

组件型号
MCU STM32F103C8T6(Blue Pill)
LCD 1.8寸 TFT(ST7735 驱动,128×160)
接口 SPI
IDE Keil MDK v5 或 STM32CubeIDE

如果你用的是其他 LCD(如 ILI9341、ST7789),原理一样,改一下驱动初始化即可。


三、CubeMX 配置

3.1 基础时钟配置

和之前 FreeRTOS 教程一样:

RCC → HSE: Crystal/Ceramic Resonator
Clock Configuration → PLL x9 → SYSCLK 72MHz

3.2 配置 SPI(驱动 LCD)

Pinout & Configuration → Connectivity → SPI1
– Mode: Full-Duplex Master
– Prescaler: 4 → 18MHz (LCD SPI 速率)
– Data Size: 8 Bits
– CPOL: Low
– CPHA: 1 Edge

引脚分配:

功能引脚
SPI1_SCK PA5
SPI1_MOSI PA7
LCD_CS PA4(GPIO Output)
LCD_DC PA3(GPIO Output)
LCD_RST PA2(GPIO Output)
LCD_BL PA1(GPIO Output,背光)

3.3 配置 SysTick 和时基

SYS → Debug: Serial Wire
SYS → Timebase Source: TIM1 ← 重要!

3.4 生成代码

Project Manager → Project Name: lvgl_demo
GENERATE CODE


四、移植 LVGL

4.1 下载 LVGL 源码

从 GitHub 下载:https://github.com/lvgl/lvgl

需要的文件:
lvgl/
├── lvgl.h
├── src/ ← 整个 src 目录
└── lv_conf_template.h

4.2 添加到工程

在工程目录下创建结构:

Drivers/
└── LVGL/
├── lvgl/ ← LVGL 源码
│ ├── src/
│ └── lvgl.h
├── lv_conf.h ← 配置文件(从模板复制修改)
└── lv_port_disp.c ← 显示接口(需要自己写)
└── lv_port_disp.h

4.3 修改 lv_conf.h(关键配置)

从 lv_conf_template.h 复制一份,改名为 lv_conf.h,修改以下关键配置:

/* 使能 LVGL */
#if 1 /* 把 0 改成 1 */

/* 颜色配置 – 根据你的 LCD 选择 */
#define LV_COLOR_DEPTH 16 /* ST7735 是 16 位色 */

/* 分辨率 – 根据你的屏幕 */
#define LV_HOR_RES 128
#define LV_VER_RES 160

/* 内存配置 – F103 RAM 紧张,精简 */
#define LV_MEM_CUSTOM 0
#define LV_MEM_SIZE (10 * 1024) /* 10KB 给 LVGL 用 */

/* 刷新周期 */
#define LV_DISP_DEF_REFR_PERIOD 30 /* 30ms,约 33fps */

/* 启用基础控件 */
#define LV_USE_LABEL 1
#define LV_USE_BTN 1
#define LV_USE_SLIDER 1
#define LV_USE_SWITCH 1

/* 字体 – 只保留需要的 */
#define LV_FONT_MONTSERRAT_14 1
#define LV_FONT_MONTSERRAT_16 0 /* 不需要的关掉省内存 */

4.4 实现显示接口(lv_port_disp.c)

这是移植的核心——告诉 LVGL 如何往你的 LCD 上画点:

#include "lvgl.h"
#include "st7735.h" // 你的 LCD 驱动

static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf1[128 * 20]; // 行缓冲,节省 RAM

/* 底层画点函数 */
static void disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p)
{
int32_t x, y;
for (y = area->y1; y <= area->y2; y++) {
for (x = area->x1; x <= area->x2; x++) {
ST7735_DrawPixel(x, y, color_p->full); // 调用你的 LCD 驱动
color_p++;
}
}

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

/* 初始化显示接口 */
void lv_port_disp_init(void)
{
/* 初始化 LCD 硬件 */
ST7735_Init();

/* 初始化行缓冲 */
lv_disp_draw_buf_init(&draw_buf, buf1, NULL, 128 * 20);

/* 注册显示驱动 */
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.draw_buf = &draw_buf;
disp_drv.flush_cb = disp_flush;
disp_drv.hor_res = 128;
disp_drv.ver_res = 160;
lv_disp_drv_register(&disp_drv);
}

注意:buf1 的大小直接影响刷新速度和 RAM 占用。F103 只有 20KB RAM,用 128 * 20(约 5KB)是折中方案。如果你的 MCU RAM 更大,可以整帧缓冲(128 * 160),效果更流畅。


五、编写第一个界面

5.1 基础界面:Hello LVGL

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

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); // 居中显示

/* 修改字体大小和颜色 */
static lv_style_t style;
lv_style_init(&style);
lv_style_set_text_font(&style, &lv_font_montserrat_14);
lv_style_set_text_color(&style, lv_color_hex(0x00FF00)); // 绿色
lv_obj_add_style(label, &style, 0);
}

int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_SPI1_Init();

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

/* 创建界面 */
ui_init();

/* 主循环 */
while (1) {
lv_timer_handler(); // LVGL 事件循环,必须在主循环中调用
HAL_Delay(5);
}
}

5.2 进阶界面:按钮 + 计数器

static lv_obj_t *count_label;
static int count = 0;

/* 按钮点击回调 */
static void btn_event_cb(lv_event_t *e)
{
count++;
lv_label_set_text_fmt(count_label, "Count: %d", count);
}

void ui_init(void)
{
/* 创建按钮 */
lv_obj_t *btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 80, 36);
lv_obj_align(btn, LV_ALIGN_CENTER, 0, –20);

/* 按钮上的文字 */
lv_obj_t *btn_label = lv_label_create(btn);
lv_label_set_text(btn_label, "Click Me");
lv_obj_center(btn_label);

/* 按钮点击事件 */
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL);

/* 计数显示标签 */
count_label = lv_label_create(lv_scr_act());
lv_label_set_text(count_label, "Count: 0");
lv_obj_align(count_label, LV_ALIGN_CENTER, 0, 20);
}

5.3 完整项目结构

lvgl_demo/
├── Core/
│ ├── Src/
│ │ ├── main.c ← 主循环,调用 lv_timer_handler
│ │ ├── st7735.c ← LCD 底层驱动
│ │ └── lv_port_disp.c ← LVGL 显示接口
│ └── Inc/
│ ├── st7735.h
│ └── lv_port_disp.h
├── Drivers/
│ └── LVGL/
│ ├── lvgl/ ← LVGL 源码
│ └── lv_conf.h ← 配置文件
└── MDK-ARM/ ← Keil 工程


六、常见问题排查

Q1:屏幕全白/全黑,没有显示

排查顺序:

  • 检查 SPI 有没有时钟输出(示波器看 SCK)
  • 检查 CS/DC/RST 引脚是否正确配置为 GPIO Output
  • 检查背光引脚是否拉高
  • 确认 LCD 驱动初始化序列是否正确(ST7735 和 ST7789 的初始化命令不同)
  • Q2:显示有画面但颜色不对

    原因:RGB565 的字节序问题。有些 LCD 期望大端,有些期望小端。

    解决:在 lv_conf.h 中切换:

    #define LV_COLOR_16_SWAP 1 /* 如果颜色不对,试试 0 和 1 切换 */

    Q3:画面闪烁

    原因:lv_timer_handler() 调用间隔太长,或者缓冲太小。

    解决:

    • 确保主循环中 HAL_Delay 不超过 5ms
    • 增大行缓冲:buf1[128 * 40]
    • 或者使用双缓冲:lv_disp_draw_buf_init(&draw_buf, buf1, buf2, 128 * 20);

    Q4:编译报内存不够

    原因:F103 的 20KB RAM 确实紧张。

    解决:

    • 减小 LV_MEM_SIZE 到 6KB
    • 减小行缓冲:buf1[128 * 10]
    • 关闭不需要的控件和字体
    • 如果还是不够,考虑换 STM32F103RCT6(48KB RAM)或 F4 系列

    七、优化技巧

    7.1 局部刷新代替全屏刷新

    LVGL 默认只刷新有变化的区域,这是它高效的关键。但如果用底层驱动的全屏刷新函数,会失去这个优势。

    确保 disp_flush 只刷新 area 指定的区域,不要全屏写入。

    7.2 使用 DMA 传输

    如果 LCD 驱动支持 DMA,可以把 disp_flush 改成 DMA 传输,CPU 不用等待:

    static void disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p)
    {
    uint32_t size = (area->x2 – area->x1 + 1) * (area->y2 – area->y1 + 2);

    ST7735_SetWindow(area->x1, area->y1, area->x2, area->y2);
    HAL_SPI_Transmit_DMA(&hspi1, (uint8_t *)color_p, size * 2);
    }

    /* DMA 传输完成回调 */
    void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
    {
    if (hspi->Instance == SPI1) {
    lv_disp_flush_ready(&disp_drv); // 通知 LVGL
    }
    }

    7.3 使用 FreeRTOS

    如果项目中有 FreeRTOS,把 LVGL 放在一个独立任务中:

    void lvgl_task(void *arg)
    {
    for (;;) {
    lv_timer_handler();
    vTaskDelay(pdMS_TO_TICKS(5));
    }
    }

    注意:LVGL 不是线程安全的。如果有多个任务操作 UI,需要用互斥锁保护。


    八、总结

    STM32 + LVGL 移植的核心步骤:

  • CubeMX 配好 SPI 和 GPIO — LCD 硬件接口
  • 下载 LVGL 源码,修改 lv_conf.h — 配置分辨率、颜色深度、内存
  • 实现 disp_flush — 把 LVGL 的画点操作映射到你的 LCD 驱动
  • 主循环调用 lv_timer_handler() — 驱动 UI 刷新和事件处理
  • 移植完成后,就可以用 LVGL 的可视化工具 SquareLine Studio 拖拽设计界面,导出 C 代码直接用,开发效率极高。


    如果本文对你有帮助,欢迎点赞、收藏、关注,我会持续分享嵌入式干货。

    赞(0)
    未经允许不得转载:171主机测评 » STM32+LVGL从零搭建GUI界面,CubeMX配置+移植+第一个界面
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址