手把手教你把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:屏幕全白/全黑,没有显示
排查顺序:
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 移植的核心步骤:
移植完成后,就可以用 LVGL 的可视化工具 SquareLine Studio 拖拽设计界面,导出 C 代码直接用,开发效率极高。
如果本文对你有帮助,欢迎点赞、收藏、关注,我会持续分享嵌入式干货。


