STM32移植LVGL的过程记录——使用硬件SPI+DMA
本次移植采用cubmx配置工程,基于HAL库移植LVGL
《详细步骤》
一. 配置STM32工程(只描述SPI配置)
Parameter配置

DMA配置

NVIC配置

二. LCD代码修改
1. 修改数据发送为硬件SPI
硬件SPI发送函数
void LCD_Writ_Bus(uint8_t dat)
{
LCD_CS_Clr(); // 拉低片选
HAL_SPI_Transmit(&hspi1, &dat, 1, HAL_MAX_DELAY); // 发送1字节
LCD_CS_Set(); // 拉高片选
}
2.修改填充函数为SPI+DMA(非必要)
屏幕填充函数
#define FILL_BUF_SIZE 1024
#define PIC_DMA_CHUNK 65534 // STM32 DMA 一次最多65535字节
uint8_t fill_buf[FILL_BUF_SIZE * 2]; // 每像素2字节
volatile uint8_t dma_tx_complete = 1; // 声明dma传输完成标志,必须是全局或静态变量
// DMA发送完成回调
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
if(hspi->Instance == SPI1)
{
dma_tx_complete = 1;
}
}
// DMA屏幕填充
void LCD_Fill(uint16_t xsta, uint16_t ysta, uint16_t xend, uint16_t yend, uint16_t color)
{
uint32_t width = xend - xsta;
uint32_t height = yend - ysta;
uint32_t total_pixels = width * height;
if (total_pixels == 0) return;
// 设置显示区域
LCD_Address_Set(xsta, ysta, xend - 1, yend - 1);
// 颜色拆成高低字节(大小端转换,LCD通常需要高字节先发)
uint8_t high_byte = color >> 8;
uint8_t low_byte = color & 0xFF;
// 准备填充缓冲区,每个像素2字节
for (int i = 0; i < FILL_BUF_SIZE; i++) {
fill_buf[2*i] = high_byte;
fill_buf[2*i + 1] = low_byte;
}
LCD_CS_Clr();
while (total_pixels > 0)
{
uint32_t chunk = (total_pixels > FILL_BUF_SIZE) ? FILL_BUF_SIZE : total_pixels;
total_pixels -= chunk;
// 等待上次 DMA 完成(你需要用标志或回调)
while (!dma_tx_complete);
dma_tx_complete = 0;
// 传输字节数 = chunk 像素数 * 2 字节
HAL_SPI_Transmit_DMA(&hspi1, fill_buf, chunk * 2);
while (!dma_tx_complete); // 等待传输完成
}
LCD_CS_Set();
}
3.修改图片显示函数为SPI+DMA(非必要)
点击查看代码
void LCD_ShowPicture(uint16_t x, uint16_t y, uint16_t length, uint16_t width, const uint8_t pic[])
{
uint32_t total_bytes = length * width * 2; // 每像素2字节
if(total_bytes == 0 || pic == NULL) return;
LCD_Address_Set(x, y, x + length - 1, y + width - 1);
LCD_CS_Clr();
while(total_bytes > 0)
{
uint32_t chunk_size = (total_bytes > PIC_DMA_CHUNK) ? PIC_DMA_CHUNK : total_bytes;
while(!dma_tx_complete); // 等待上次DMA传输完成
dma_tx_complete = 0;
HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)pic, chunk_size); // 启动DMA传输
while(!dma_tx_complete); // 等待DMA完成
pic += chunk_size; // 移动指针
total_bytes -= chunk_size;
}
LCD_CS_Set();
}
三. 下载LVGL源码并移植,下载地址:https://github.com/lvgl/lvgl/tree/release/v8.3
1. 文件裁剪,只需复制出框选的文件夹

从examples文件夹中,只保留porting文件夹,最后的文件结构如下

修改文件名:lv_conf_template.h;lv_port_disp_template.c;lv_port_disp_template.h;lv_port_fs_template.c;lv_port_fs_template.h;lv_port_indev_template.c;lv_port_indev_template.h


2. 将文件夹复制到工程,并在工程管理中新建3个文件夹,方便管理工程

添加文件到工程文件夹,每个子文件夹都要添加(要慢一点,不要漏),还需要添加头文件到工程中


3. 启用文件
启用 lv_conf.h 这个头文件,因为默认该文件是不参与编译,必须将#if 0 修改为 #if 1,如下图。

启用 lv_port_disp.c,默认该文件是不参与编译,必须将#if 0 修改为 #if 1,同时可以添加驱动头文件和设置屏幕尺寸,详细如下图。

4. 修改刷新函数
LVGL刷新函数
extern uint8_t fill_buf[FILL_BUF_SIZE * 2]; // 每像素2字节
// 声明dma传输完成标志,必须是全局或静态变量
extern volatile uint8_t dma_tx_complete;
static void disp_flush(lv_disp_drv_t * disp_drv,
const lv_area_t * area,
lv_color_t * color_p)
{
int32_t w = area->x2 - area->x1 + 1;
int32_t h = area->y2 - area->y1 + 1;
int32_t total = w * h;
LCD_Address_Set(area->x1, area->y1, area->x2, area->y2);
LCD_DC_Set();
LCD_CS_Clr();
/* 先把 dma_tx_complete 置 1,确保第一次就能直接填充并启动 DMA */
dma_tx_complete = 1;
while (total > 0)
{
/* ① 等待上一段 DMA 结束 */
while (!dma_tx_complete); // 这里先等待!
/* ② 计算本段大小并填充缓冲区 */
uint32_t chunk = (total > FILL_BUF_SIZE) ? FILL_BUF_SIZE : total;
for (uint32_t i = 0; i < chunk; i++) {
fill_buf[2*i] = color_p[i].full >> 8;
fill_buf[2*i + 1] = color_p[i].full & 0xFF;
}
/* ③ 启动本段 DMA */
dma_tx_complete = 0;
HAL_SPI_Transmit_DMA(&hspi1, fill_buf, chunk * 2);
color_p += chunk;
total -= chunk;
}
/* 等待最后一次 DMA 完成 */
while (!dma_tx_complete);
LCD_CS_Set();
lv_disp_flush_ready(disp_drv);
}
5. 调用初始化函数
LVGL初始化函数
lv_init(); // LVGL 初始化
lv_port_disp_init(); // 注册LVGL的显示任务
6. 调用时间函数
时间函数
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
lv_tick_inc(1);
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
/* USER CODE BEGIN SysTick_IRQn 1 */
/* USER CODE END SysTick_IRQn 1 */
}
7. 在循环中调用任务函数
任务函数
lv_task_handler();
8. 添加一个按键测试显示
点击查看代码
lv_obj_t *myBtn = lv_btn_create(lv_scr_act()); // 创建按钮; 父对象:当前活动屏幕
lv_obj_set_pos(myBtn, 60, 60); // 设置坐标
lv_obj_set_size(myBtn, 120, 50); // 设置大小
// 按钮上的文本
lv_obj_t *label_btn = lv_label_create(myBtn); // 创建文本标签,父对象:上面的btn按钮
lv_obj_align(label_btn, LV_ALIGN_CENTER, 0, 0); // 对齐于:父对象
lv_label_set_text(label_btn, "Test"); // 设置标签的文本
9. 编译测试
如果编译内存不足,可以修改lv_conf.h中的LV_MEM_SIZE
#define LV_MEM_SIZE (20U * 1024U)

浙公网安备 33010602011771号