LVGL显示代码移植(1)

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

210.3.9 LVGL显示代码移植(1)

/* lv_port_disp_template.h:本节把模板总开关由 0 改为 1。 */
#if 1                                                  /* 让显示端口头文件的正文参与条件编译。 */

/* lv_port_disp_template.c:同样启用源文件,并接入课程已有的 LCD 驱动。 */
#if 1                                                  /* 让显示端口源文件的正文参与条件编译。 */
#include "lv_port_disp_template.h"                    /* 引入本显示端口的声明以及 LVGL 类型。 */
#include <stdbool.h>                                   /* 提供 bool、true 和 false。 */
#include "lcd_init.h"                                 /* 提供 LCD_Init、LCD_W 和 LCD_H。 */
#include "lcd.h"                                      /* 提供本课程 LCD 绘图函数的声明。 */

#ifndef MY_DISP_HOR_RES                                /* 尚未从别处定义显示宽度时进入该分支。 */
#define MY_DISP_HOR_RES LCD_W                          /* 用 LCD 驱动的宽度宏统一配置 LVGL。 */
#endif                                                 /* 结束显示宽度的条件编译。 */

#ifndef MY_DISP_VER_RES                                /* 尚未从别处定义显示高度时进入该分支。 */
#define MY_DISP_VER_RES LCD_H                          /* 用 LCD 驱动的高度宏统一配置 LVGL。 */
#endif                                                 /* 结束显示高度的条件编译。 */

static void disp_init(void);                           /* 前置声明底层 LCD 初始化函数。 */
static void disp_flush(lv_disp_drv_t * disp_drv,       /* 前置声明显示刷新回调及驱动参数。 */
                       const lv_area_t * area,          /* 声明待刷新的矩形区域参数。 */
                       lv_color_t * color_p);           /* 声明连续像素数据指针参数。 */

void lv_port_disp_init(void)                           /* 定义 LVGL 显示端口的总初始化函数。 */
{                                                      /* 开始显示端口初始化。 */
    disp_init();                                       /* 先初始化实际 LCD 硬件。 */
    static lv_disp_draw_buf_t draw_buf_dsc_1;          /* 静态保存单绘制缓冲区的描述信息。 */
    static lv_color_t buf_1[MY_DISP_HOR_RES * 10];     /* 静态分配能容纳 10 行像素的单缓冲区。 */
    lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1,      /* 绑定第一个绘制缓冲区。 */
                          NULL,                        /* 传入 NULL,表示不启用第二个缓冲区。 */
                          MY_DISP_HOR_RES * 10);       /* 告诉 LVGL 缓冲容量是宽度乘 10 个像素。 */
    static lv_disp_drv_t disp_drv;                     /* 静态保存显示驱动描述符。 */
    lv_disp_drv_init(&disp_drv);                       /* 先用 LVGL 默认值初始化驱动描述符。 */
    disp_drv.hor_res = MY_DISP_HOR_RES;                /* 把水平分辨率设为 LCD_W。 */
    disp_drv.ver_res = MY_DISP_VER_RES;                /* 把垂直分辨率设为 LCD_H。 */
    disp_drv.flush_cb = disp_flush;                    /* 注册把绘制结果送往 LCD 的刷新回调。 */
    disp_drv.draw_buf = &draw_buf_dsc_1;               /* 把所选单缓冲区交给显示驱动。 */
    lv_disp_drv_register(&disp_drv);                   /* 向 LVGL 注册配置完成的显示驱动。 */
}                                                      /* 结束显示端口初始化。 */

static void disp_init(void)                            /* 定义 LVGL 显示端口的底层初始化函数。 */
{                                                      /* 开始函数体。 */
    LCD_Init();                                        /* 初始化课程使用的 LCD 控制器与接口。 */
}                                                      /* 结束函数体。 */

static void disp_flush(lv_disp_drv_t * disp_drv,       /* disp_drv 是当前发起刷新的显示驱动。 */
                       const lv_area_t * area,          /* area 给出本次待刷新的矩形坐标。 */
                       lv_color_t * color_p)            /* color_p 指向该矩形的连续像素数据。 */
{                                                      /* 开始刷新回调。 */
    LCD_Fill(area->x1, area->y1,                       /* 本课暂把 LVGL 区域起点传给 LCD_Fill。 */
             area->x2, area->y2, color_p);             /* 本课暂把区域终点和像素指针传入;此方案后续会纠正。 */
    lv_disp_flush_ready(disp_drv);                     /* 同步发送结束后,通知 LVGL 可再次使用缓冲区。 */
}                                                      /* 结束刷新回调。 */

/* lcd_init.h:课程选择方向 2,使逻辑分辨率变成 480×320。 */
#define USE_HORIZONTAL 2                               /* 0/1 为竖屏,2/3 为横屏,本课选 2。 */
#if USE_HORIZONTAL == 0 || USE_HORIZONTAL == 1         /* 判断是否采用竖屏方向。 */
#define LCD_W 320                                      /* 竖屏逻辑宽度为 320 像素。 */
#define LCD_H 480                                      /* 竖屏逻辑高度为 480 像素。 */
#else                                                  /* 方向 2 或 3 时进入横屏分支。 */
#define LCD_W 480                                      /* 横屏逻辑宽度为 480 像素。 */
#define LCD_H 320                                      /* 横屏逻辑高度为 320 像素。 */
#endif                                                 /* 结束方向对应分辨率的条件编译。 */

/* lcd.h:本课为了接收 color_p,加入 LVGL 类型并修改 LCD_Fill 的声明。 */
#include "lvgl.h"                                     /* 提供 lv_color_t 的类型定义。 */
void LCD_Fill(u16 xsta, u16 ysta,                      /* 声明矩形的起始坐标。 */
              u16 xend, u16 yend,                      /* 声明矩形的结束坐标。 */
              lv_color_t * color_p);                   /* 本课把原来的单个 u16 颜色改成颜色指针。 */

/* lcd.c:以下是本课把原纯色填充函数改成指针参数后的阶段性实现。 */
void LCD_Fill(u16 xsta, u16 ysta,                      /* 接收矩形起点。 */
              u16 xend, u16 yend,                      /* 接收矩形终点。 */
              lv_color_t * color_p)                    /* 接收颜色数据首地址。 */
{                                                      /* 开始函数体。 */
    u16 t = 1;                                         /* 控制分批发送循环是否继续。 */
    u32 num, num1;                                     /* num 是剩余像素数,num1 是本批像素数。 */
    num = (xend - xsta) * (yend - ysta);               /* 按旧接口的右开区间规则计算像素数。 */
    LCD_Address_Set(xsta, ysta, xend - 1, yend - 1);   /* 把旧接口的结束坐标转换成 LCD 的闭区间终点。 */
    LCD_CS_Clr();                                      /* 拉低片选,开始一次 LCD 传输。 */
    while(t)                                           /* 当仍有像素未发送时继续循环。 */
    {                                                  /* 开始一次分批处理。 */
        if(num > 65534)                                /* 单批超过 65534 个像素时先拆出一批。 */
        {                                              /* 开始大数据量分支。 */
            num -= 65534;                              /* 从剩余数量中扣除本批像素。 */
            num1 = 65534;                              /* 本批固定发送 65534 个像素。 */
        }                                              /* 结束大数据量分支。 */
        else                                           /* 剩余数量已不超过单批上限。 */
        {                                              /* 开始最后一批分支。 */
            t = 0;                                     /* 标记本批结束后退出 while。 */
            num1 = num;                                /* 最后一批发送全部剩余像素。 */
        }                                              /* 结束最后一批分支。 */
        for(uint32_t i = 0; i < num1; i++)             /* 按本批像素数循环发送。 */
            HAL_SPI_Transmit(&hspi1,                   /* 使用课程已有的 SPI1 句柄。 */
                             (uint8_t *)color_p,        /* 本课始终从同一个地址取 2 字节。 */
                             2, 100);                   /* 每个像素发送 2 字节,超时值为 100。 */
    }                                                  /* 结束分批循环。 */
    LCD_CS_Set();                                      /* 拉高片选,结束 LCD 传输。 */
}                                                      /* 结束函数体。 */

/* main.c:本节只用纯色值检查改过的 LCD_Fill 是否仍能工作。 */
uint16_t lcd_color;                                    /* 保存一个 16 位 RGB565 纯色值。 */
lcd_color = BLACK;                                     /* 课程先试 WHITE,随后改为 BLACK 再试。 */
LCD_Fill(0, 0, LCD_W, LCD_H,                           /* 请求填充整个 480×320 屏幕。 */
         (lv_color_t *)&lcd_color);                    /* 把纯色变量地址强制转换成课程所改的指针类型。 */

本节源码边界: 上面保留的是视频在第 210 课实际采用的阶段性代码。LCD_Fill() 中 color_p 没有递增,因此白屏、黑屏测试只能证明“反复发送同一个颜色”仍可工作,不能证明 LVGL 的多颜色区域刷新正确。第 211 课运行 demo 后出现颜色异常,第 212 课才明确纠正这个认识并另写逐像素发送函数;这里不提前抄入后续实现。

一、工程修改

上一课只完成了 LVGL 源文件、Keil 分组、头文件路径和 C99 配置。本课进入移植任务的第四部分:配置屏幕输出,也就是在 LVGL 的显示抽象层与已有 ST7796 LCD 驱动之间建立连接。

本课涉及的主要文件如下:

文件本节用途
midware/lvgl/examples/porting/lv_port_disp_template.c初始化显示、建立绘制缓冲、登记分辨率和刷新回调
midware/lvgl/examples/porting/lv_port_disp_template.h开启显示端口模板并声明初始化接口
BSP/LCD/lcd_init.h设置屏幕方向以及 LCD_W、LCD_H
BSP/LCD/lcd.h、lcd.c阶段性修改 LCD_Fill() 参数和发送代码
Core/Src/main.c暂时用白色、黑色执行纯色填充测试
BSP/TOUCH/touch.c临时注释因 LCD_Fill() 改签名而报错的校准调用

二、配置屏幕输出

2.1 创建新工程并打开

将上一课的第 15 号工程复制一份作为第 16 号工程。课程把工程目录名从 15LCD_Display_Touch_Lvgl 改成 16LCD_Display_Lvgl,即名称中去掉 Touch,表示当前先集中处理显示端口;触摸输入的适配还没有开始。

第 15 号工程本来就是上一课保存的中间检查点,并非“已经移植完成”的工程。复制的意义是保留可回退快照,随后应确认 Keil 打开的是第 16 号目录中的工程,避免把改动继续写回第 15 号工程。

2.2 配置屏幕输出第一步: 修改条件编译

课程没有按模板顶部英文提示另存为 lv_port_disp.c/.h,而是继续使用已经加入 Keil 的:

midware/lvgl/examples/porting/lv_port_disp_template.c
midware/lvgl/examples/porting/lv_port_disp_template.h

把两个文件顶部的 #if 0 都改为 #if 1。这不是普通运行时判断,而是预处理条件:值为 0 时,模板主体在编译前就被排除;改为 1 后,显示端口的类型、函数和代码才真正进入编译。

首次直接启用模板后出现编译错误,课程先停用没有选择的缓冲示例。尤其是模板的第 3 个示例还引用了当前工程未定义的旧宏 LV_VER_RES_MAX,并且其两个全屏缓冲本来也远超本机内部 SRAM 容量。处理模板时不能同时把三个互斥示例都当作有效配置。

2.3 配置屏幕输出第二步: 添加屏幕初始化代码

lv_port_disp_init() 首先调用模板内部的 disp_init(),所以把 LCD_Init() 放进 disp_init(),并在端口源文件中包含 lcd_init.h。这样调用关系变为:

lv_port_disp_init()
└─ disp_init()
   └─ LCD_Init()

课程随后把 main.c 原位置的 LCD_Init() 注释掉,目的是让 LCD 的硬件初始化归显示端口管理,避免两个位置重复初始化。要注意,本节尚未在 main() 中正式完成 lv_init()、lv_port_disp_init()、tick 与任务处理的整套调用;这些属于后续课程。因此这里完成的是端口代码接线,不等于 LVGL 已经运行。

2.4 配置屏幕输出第三步: 设置屏幕为横屏

在 BSP/LCD/lcd_init.h 中把:

#define USE_HORIZONTAL 0

改为:

#define USE_HORIZONTAL 2

该驱动约定 0、1 为竖屏,2、3 为横屏;课程选择 2。对应的条件编译结果为:

显示方向LCD_WLCD_H
竖屏(0/1)320480
横屏(2/3)480320

这里的方向值不只是交换两个数字。LCD_Init() 内还会根据它配置 LCD 控制器的扫描/映射方向;LCD_W、LCD_H 则让上层代码使用与该方向一致的逻辑坐标范围。

2.5 配置屏幕输出第四步: 选择图形缓冲模式

LVGL 先在 RAM 中的绘制缓冲区生成像素,再调用 flush_cb 把这批像素传给 LCD。它不是 LCD 控制器内部的显存,也不在 MCU 的 Flash 中。本工程把缓冲数组声明为 static lv_color_t[],实际占用的是 SRAM;原视频口述和原笔记把资源压力说成 Flash,精校后予以纠正。

模板给出三种示例,本课只保留第 1 种:

示例缓冲配置工作方式本课状态
1一个 10 行缓冲LVGL 画完一块,刷新完成后再复用同一缓冲启用
2两个 10 行缓冲配合 DMA 时,可一边发送一块、一边绘制下一块注释
3两个全屏缓冲,并启用 full_refresh传统全帧双缓冲,通常更适合能直接切换帧缓冲地址的显示控制器注释

课程配置 LV_COLOR_DEPTH == 16,因此每个 lv_color_t 像素为 2 字节。横屏 480×320 下的静态容量为:

[ \text{示例 1}=480\times10\times2=9600\text{ B}=9.375\text{ KiB} ]

[ \text{示例 2}=2\times480\times10\times2=19200\text{ B}=18.75\text{ KiB} ]

[ \text{示例 3}=2\times480\times320\times2=614400\text{ B}=600\text{ KiB} ]

第 1 种并不是完整帧缓冲:当待更新区域大于 10 行时,LVGL 会把它拆成多块,逐块绘制和刷新。一个缓冲时,调用 lv_disp_flush_ready() 前 LVGL 不能安全地重写这块内存;两个缓冲只有在底层采用 DMA 等异步传输时,才可能真正形成绘制与发送并行,不能只靠多声明一个数组就自动提速。

2.6 配置屏幕输出第五步: 配置LVGL屏幕分辨率

模板原值是固定的 320×240,本课改为使用 LCD 驱动的宏:

MY_DISP_HOR_RES → LCD_W → 480
MY_DISP_VER_RES → LCD_H → 320

然后 disp_drv.hor_res、disp_drv.ver_res 分别取这两个值。这样以后只需在 LCD 驱动中改变方向/尺寸,上层端口即可同步,不必在多处重复写死分辨率。

这些值有两种用途,必须保持一致:

  • buf_1[MY_DISP_HOR_RES * 10] 用水平分辨率算出 10 行需要多少像素;
  • disp_drv.hor_res/ver_res 告诉 LVGL 合法的屏幕坐标范围与布局尺寸。

若缓冲长度仍按旧宽度计算,而驱动却按新宽度绘制,就可能发生越界;若 LVGL 分辨率与 LCD 的方向不一致,界面会出现截断、旋转或坐标错位。

2.7 配置屏幕输出第六步: 配置显示函数

disp_flush() 是 LVGL 与 LCD 驱动之间最关键的显示出口。LVGL 把本次无效区域重新绘制到缓冲区后,传入三个参数:

参数正确含义
disp_drv发起本次刷新的显示驱动对象
area待刷新的矩形,坐标为 x1、y1、x2、y2
color_p指向该矩形第一个像素的指针,后面连续存放矩形内所有像素

LVGL 的区域终点是包含在区域内的,所以正确的宽、高和像素数应为:

[ w=x2-x1+1,\qquad h=y2-y1+1 ]

[ N=w\times h=(x2-x1+1)(y2-y1+1) ]

像素顺序可理解为从左到右、从上到下连续排列;每发送一个像素后,读取指针必须移到下一个 lv_color_t。模板原示例中的 color_p++ 正是在表达这一点。

2.7.1 本课实际采用的阶段性方案

视频先把 color_p 理解为“指向一个颜色”,因而直接复用原来的纯色填充函数:

  1. 把 LCD_Fill() 的第 5 个参数从 u16 color 改成 lv_color_t *color_p;
  2. 把 SPI 发送地址从 &color 改成 color_p;
  3. 在 lcd.h 中包含 lvgl.h,使 lv_color_t 可见;
  4. 在 disp_flush() 中把 area 的四个坐标和 color_p 传给 LCD_Fill();
  5. 因函数签名变化,注释掉 touch.c 中传入 WHITE 的校准清屏调用;
  6. 在 main.c 中定义 uint16_t lcd_color,取其地址并强制转换为 lv_color_t *,先后测试白屏与黑屏。

修改后工程能够编译,烧录后也能显示纯白和纯黑。这个实验只能验证:地址所指的同一个 16 位值被反复发送时,旧的纯色清屏功能仍然有效。

2.7.2 必须掌握的原理校正

本课方案还存在两个实质问题:

  • HAL_SPI_Transmit() 循环中始终使用同一个 color_p,没有 color_p++,所以整个区域只能重复第一个像素,无法呈现 LVGL 缓冲区中的多种颜色;
  • 原 LCD_Fill(xsta, ysta, xend, yend, ...) 把 xend/yend 当作右开区间,使用 (xend-xsta)*(yend-ysta) 并设置到 xend-1/yend-1;LVGL 的 area->x2/y2 却是闭区间终点。直接传参会少算最后一列和最后一行。

例如 LVGL 请求刷新 (0,0)~(479,9),区域实际是 480×10,共 4800 个像素;按旧 LCD_Fill() 的算法只会得到:

[ (479-0)\times(9-0)=4311 ]

所以“编译通过”和“能刷纯色”都不等于刷新回调已经正确。本节应掌握接口对接的方向以及错误产生的原因;完整的逐像素修正留到课程后续内容。

无论底层采用阻塞 SPI 还是 DMA,刷新结束时都必须调用 lv_disp_flush_ready(disp_drv):

  • 当前代码用阻塞式 HAL_SPI_Transmit(),应在全部像素发送结束后调用;
  • 若以后改成异步 DMA,则不能刚启动 DMA 就调用,而应在 DMA 传输完成后通知 LVGL。

三、时基配置

本节没有配置 LVGL 时基。视频结尾只预告后续还要修改实际配置并启用 demo;原笔记把这些写成了本课已完成事项,精校后明确课程边界。

截至第 210 课结束,准确状态是:

第 16 号工程已建立
→ 显示端口模板已启用
→ LCD 初始化已接入 disp_init()
→ 屏幕方向与 LVGL 分辨率已设为 480×320
→ 已选单个 10 行绘制缓冲
→ 已做阶段性的 LCD_Fill 刷新接线和纯色测试
→ LVGL tick 尚未配置
→ LVGL 初始化、任务处理与 demo 尚未完整运行
→ 多颜色区域刷新仍存在错误,后续课程继续修正

下一课才使用 TIM6 每 1 ms 调用 lv_tick_inc(1),并继续完成 LVGL 初始化与 demo 测试;这些内容不能提前算作本课成果。