STM32移植LVGL的过程记录——使用硬件SPI+DMA

本次移植采用cubmx配置工程,基于HAL库移植LVGL

《详细步骤》

一. 配置STM32工程(只描述SPI配置)

Parameter配置

image


DMA配置

image


NVIC配置

image


二. 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. 文件裁剪,只需复制出框选的文件夹

image


从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)

posted @ 2025-06-21 16:27  RefuelW  阅读(2558)  评论(0)    收藏  举报