写在前面
移植 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.c的read_cb。 - 没有输入时,
lv_port_indev可以整个不参与编译。
二、移植中最容易踩的 5 个坑
LV_COLOR_16_SWAP=0导致颜色错乱
现象:红蓝互换、色块发紫。解决办法:置 1。disp_flush里误调lv_disp_flush_ready
现象:花屏、撕裂。正确做法:DMA 完成后再通知。- CS 拉高太早
现象:屏幕下半截是旧画面。正确做法:在HAL_SPI_TxCpltCallback里拉高。 lv_tick_inc没给或频率不对
现象:界面卡住、动画不更新。正确做法:1 ms 一次。- 地址窗没加 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 |