LVGL图形库移植STM32

写在前面

移植 LVGL 之前,必须保证裸机底层已经能单独驱动屏幕:

  • SPI 初始化 :SPI1 时钟、波特率分频正确(推荐 10.5~21 MHz,F4 跑 84 MHz APB2 时 SPI_BAUDRATEPRESCALER_4 = 21 MHz)。

  • DMA 初始化 :SPI1 TX 对应 DMA2 Stream3(Channel 3),在 CubeMX 里打开 DMA,模式 Normal,内存到外设。

  • GPIO :CS / DC / RST / BL(可选)已配置并封装好拉高/拉低宏。

  • 屏幕初始化命令序列:RST 时序、ST7735S 寄存器配置、地址窗、MadCtl 方向等已验证。

先让 BSP_TFT_Flush_DMA(0,0,127,127, buf, ...) 能单独刷屏,再接入 LVGL,能省 90% 的调试时间。

一、LVGL移植详细步骤

1.1整体图形化流程:(以下参考正点原子)


1.2 配置 lv_conf.h

这是移植的大脑。关键项:

作用 本项目设置
LV_COLOR_DEPTH 颜色深度 16(ST7735 只支持 RGB565)
LV_COLOR_16_SWAP 16 位色字节交换 必须设为 1,匹配 8 位 SPI 大端发送
LV_USE_PERF_MONITOR 性能监视器 调试时可开
LV_USE_MEM_MONITOR 内存监视器 调试时可开
LV_MEM_CUSTOM 是否用自定义内存分配器 小项目默认关即可

重点解释 LV_COLOR_16_SWAP

  • LVGL 内部默认小端(Little Endian),像素内存是 [低字节, 高字节]
  • ST7735 的 8 位 SPI 写 GRAM 时,期望先收到高字节。
  • LV_COLOR_16_SWAP=0,DMA 发出去的字节序反了,会红蓝颠倒或颜色错乱。

1.3 实现显示接口:disp_flush

这是 LVGL 与屏幕之间的唯一数据出口。核心思路:

c 复制代码
static void disp_flush(lv_disp_drv_t * disp_drv,
                       const lv_area_t * area,
                       lv_color_t * color_p)
{
    uint32_t w = area->x2 - area->x1 + 1;
    uint32_t h = area->y2 - area->y1 + 1;
    uint32_t byte_len = w * h * 2;  // RGB565 = 2 字节/像素
// 启动非阻塞 DMA 刷屏,函数立即返回
BSP_TFT_Flush_DMA_Async(area->x1, area->y1,
                        area->x2, area->y2,
                        (const uint8_t *)color_p, byte_len);
}

注意

  • disp_flush不要直接调用 lv_disp_flush_ready(disp_drv),否则 LVGL 会以为刷完了,提前发下一帧。
  • 必须等 DMA/SPI 真正发完后,在 HAL_SPI_TxCpltCallback 中拉高 CS,再调 lv_disp_flush_ready(disp_drv)

1.4 DMA 完成回调:通知 LVGL 的时机

c 复制代码
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
    if (hspi->Instance == SPI1) {
        SPI_CS_1;                              // 一帧发完再拉高片选
        g_tft_dma_done = 1;
        if (g_tft_dma_done_cb) g_tft_dma_done_cb(); // -> lv_disp_flush_ready()
    }
}

为什么 CS 要在 DMA 完成后再拉高?

  • 拉高 CS 会结束本次 GRAM 写入会话。若提前拉高,屏幕上会残留下半帧。
  • DMA 完成回调是硬件保证数据全部进 SPI 发送寄存器/移位寄存器的最可靠时机。

1.5 注册显示驱动:lv_port_disp_init

c 复制代码
static lv_disp_draw_buf_t draw_buf_dsc;
static lv_color_t buf_1[128 * 10];  // 10 行缓冲
void lv_port_disp_init(void)
{
BSP_TFT_Init();                             // 屏幕初始化
BSP_TFT_Register_DmaDone_Callback(tft_dma_done_cb);
lv_disp_draw_buf_init(&draw_buf_dsc, buf_1, NULL, 128 * 10);
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = 128;
disp_drv.ver_res = 128;
disp_drv.flush_cb = disp_flush;
disp_drv.draw_buf = &draw_buf_dsc;
lv_disp_drv_register(&disp_drv);
}

1.6 缓冲策略选择

策略 内存占用 复杂度 适用场景
单缓冲(10~20 行) 小屏、静态界面
双缓冲(10~20 行 ×2) 需要 DMA 与渲染并行
全屏双缓冲(128×128×2) 动画多、追求高帧率

小屏推荐单缓冲 :128×10 的 draw buffer 已足够 LVGL 工作。LVGL 会把屏幕切成多个水平条带,一条条调用 disp_flush


1.7 心跳:lv_tick_inc

LVGL 需要 1 ms 时基,用于动画、定时器、输入消抖等。

c 复制代码
// 放在 1ms 定时器中断里
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM11) {
        lv_tick_inc(1);
    }
}
  • 可以用 SysTick,但建议用独立定时器,避免被 HAL_Delay 卡死。
  • 必须有,否则所有动画和刷新都停止。

1.8 主循环:lv_timer_handler

c 复制代码
while (1) {
    lv_timer_handler();  // 处理 LVGL 任务、刷新、动画
    HAL_Delay(5);        // 通常 5~10 ms 调用一次
}
  • 不要在中断里调用 lv_timer_handler()
  • 返回本次执行距离下一次建议调用的时间(可以动态调整 HAL_Delay)。

1.9 GRAM 偏移:解决毛边

1.44 寸 ST7735 的显存通常是 132×132,可视区 128×128 相对原点有偏移。如果全屏刷新出现 1 行或 1 列毛边:

c 复制代码
#define TFT_X_OFFSET  1  // 常见值:0/1/2
#define TFT_Y_OFFSET  1  // 常见值:0/1/2/3

所有设置地址窗的地方都加上偏移:

c 复制代码
uint16_t ax1 = X1 + TFT_X_OFFSET;
uint16_t ay1 = Y1 + TFT_Y_OFFSET;
uint16_t ax2 = X2 + TFT_X_OFFSET;
uint16_t ay2 = Y2 + TFT_Y_OFFSET;

1.10 无输入设备完全没关系

LVGL 的核心是显示,输入设备是可选的:

  • 不接触摸/按键 → 只能显示,不能交互。
  • 后续想加按键,再实现 lv_port_indev_template.cread_cb
  • 没有输入时,lv_port_indev 可以整个不参与编译。

二、移植中最容易踩的 5 个坑

  1. LV_COLOR_16_SWAP=0 导致颜色错乱
    现象:红蓝互换、色块发紫。解决办法:置 1。
  2. disp_flush 里误调 lv_disp_flush_ready
    现象:花屏、撕裂。正确做法:DMA 完成后再通知。
  3. CS 拉高太早
    现象:屏幕下半截是旧画面。正确做法:在 HAL_SPI_TxCpltCallback 里拉高。
  4. lv_tick_inc 没给或频率不对
    现象:界面卡住、动画不更新。正确做法:1 ms 一次。
  5. 地址窗没加 GRAM 偏移
    现象:边缘有 1×128 毛边。正确做法:所有 0x2A/0x2B 都加 TFT_X/Y_OFFSET

三、LVGL 常用 API 用途速查

3.1 系统与时基

API 用途
lv_init() 初始化 LVGL 内核,必须先调
lv_tick_inc(ms) 告诉 LVGL 过去了多少毫秒
lv_timer_handler() 主循环中调用,驱动刷新/动画/任务

3.2 屏幕与对象

API 用途
lv_scr_act() 获取当前活动屏幕
lv_obj_create(parent) 创建一个基础容器
lv_obj_clean(obj) 删除某对象的所有子对象
lv_obj_del(obj) 删除对象自身
lv_obj_set_size(obj, w, h) 设置对象宽高
lv_obj_set_pos(obj, x, y) 设置对象坐标
lv_obj_align(obj, align, x_ofs, y_ofs) 相对父对象对齐
lv_obj_center(obj) 居中(LV_ALIGN_CENTER 简写)
lv_obj_set_style_bg_color(obj, color, part) 设置背景色
lv_obj_set_style_text_color(obj, color, part) 设置文字颜色

3.3 标签 Label

c 复制代码
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);
API 用途
lv_label_create(parent) 创建标签
lv_label_set_text(label, "text") 设置文本
lv_label_set_text_fmt(label, "%d", n) 格式化设置文本
lv_label_set_long_mode(label, mode) 长文本模式(滚动、裁剪等)

3.4 按钮 Button

c 复制代码
lv_obj_t * btn = lv_btn_create(lv_scr_act());
lv_obj_set_size(btn, 80, 40);
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");
API 用途
lv_btn_create(parent) 创建按钮
lv_obj_add_event_cb(obj, cb, event, user_data) 注册事件回调
LV_EVENT_CLICKED / PRESSED / RELEASED 常用事件

3.5 进度条/滑块/圆弧

API 用途
lv_bar_create(parent) 创建进度条
lv_bar_set_value(bar, value, anim) 设置进度
lv_slider_create(parent) 创建滑块
lv_slider_set_value(slider, value, anim) 设置滑块值
lv_arc_create(par
相关推荐
Hello_Damon_Nikola1 小时前
伺服电机堵转检测算法详解
单片机·嵌入式硬件·算法
XTIOT6662 小时前
零部件 DPM 码扫码枪生产厂家:工业 DPM 设备多工位组网与 MES/ERP 数据集成方案
网络·单片机·嵌入式硬件·物联网·计算机视觉
振南的单片机世界2 小时前
PSC分频、ARR定周期、OPM停一次:基本定时器三要素
stm32·单片机·嵌入式硬件
JERRY. LIU3 小时前
VS CODE进行NRF CONNECT项目开发出现变量无法链接跳转问题及解决方法
数据库·嵌入式硬件·物联网·iot·嵌入式实时数据库
xxwxx__4 小时前
51单片机智能家居系统:从零实现温湿度监测、蓝牙遥控与PWM风扇调速
嵌入式硬件·51单片机·智能家居
集芯微电科技有限公司5 小时前
替代LM4890音频功率放大器低电磁干扰辐射
人工智能·单片机·嵌入式硬件·生成对抗网络·计算机外设
ACP广源盛139246256736 小时前
国产算力互联IX8024@ACP#Kimi K3 开源后的端侧部署硬件架构分析
大数据·人工智能·分布式·单片机·嵌入式硬件
智信软件6 小时前
MCU中的ADC应用
单片机·嵌入式硬件
醉逍遥_祥7 小时前
Linux进程与NuttX任务(Linux Processes vs NuttX Tasks)
linux·单片机·嵌入式软件