TFT屏幕:STM32硬件SPI+DMA+队列自动传输

看了网上的很多的SPI+DMA的代码,感觉都有一些缺陷,就是基本都是需要有手动等待DMA完成的这个操作,我感觉这种等待操作在很大程度上浪费了时间,那么我加入的"队列"就是一种将等待时间利用起来的方法。

原本的SPI+DMA的操作逻辑如下图,是比较简单的

下面是我加入队列逻辑的过程图,会变得比较庞大,占用的内容资源也多会比较多,需要斟酌使用

加入队列前的基本流程是控制"DC电平->写入数据->等待DMA传输完成->DC电平->写入数据->等待DMA传输完成"这种操作是很浪费时间资源的,那么加入队列之后的操作是"写入数据->写入数据->写入数据"大部分时间都在写入队列与中断中,其它部分都是DMA自己在传输数据,不需要一直等待

我会在最后放一个完整的代码(我使用的是ST7789,其中的执行代码是适配LVGL的)包括我自己使用芯片的驱动,有需要的可以直接拿走,基本上只需要修改一点点就可以用了。

首先是队列的创建,我这里只是简单写了一下,这个队列创建很占内存,内存分配也不是很灵活,其中忙标志也没有锁,小概率会出事,的如果有大佬的话可以修改一下,使其更完善一点

cpp 复制代码
#define SPI_BUFFER_SIZE 4096
#define SPI_QUEUE_SIZE 8

//数据结构体
typedef struct {
    uint8_t data[SPI_BUFFER_SIZE];
    uint16_t data_size;
    bool is_data; // 0:命令, 1:数据 用于控制DC
} spi_transaction_t;

// 全局传输队列
spi_transaction_t tx_queue[SPI_QUEUE_SIZE];

//队列索引
volatile uint16_t tx_read_index = 0;
volatile uint16_t tx_write_index = 0;
volatile uint16_t tx_count = 0;
//DMA忙标志
volatile uint8_t dma_busy = 0;

之后是队列数据的输入,需要输入数据地址,数据大小还有DC电平翻转方向(0命令/1数据),如果是ESP32的话好像可以实现DC的自动翻转,会方便很多,这里实现不了,就只能在DMA之前先把DC翻转给提前完成 if (tx->is_data)。还要注意队列的索引变化

cpp 复制代码
/******************************************************************************
      函数说明:添加SPI传输任务到队列
******************************************************************************/
static void LCD_Add_To_Queue(uint8_t *data, uint16_t size, uint8_t is_data)
{
    // 等待队列有空位(非阻塞式可添加超时机制)
    while (tx_count >= SPI_QUEUE_SIZE) {
        // 可在此处添加少量延迟或任务调度
    }
    
    // 复制数据到队列
    uint8_t index = tx_write_index;
    memcpy(tx_queue[index].data, data, size);
    tx_queue[index].data_size = size;
    tx_queue[index].is_data = is_data;
    
    // 更新队列索引
    tx_write_index = (tx_write_index + 1) % SPI_QUEUE_SIZE;
    tx_count++;
    
    // 如果DMA空闲,立即启动传输
    if (!dma_busy) {
        LCD_Start_Next_Transaction();
    }
}


/******************************************************************************
      函数说明:启动下一个DMA传输任务
******************************************************************************/
static void LCD_Start_Next_Transaction(void)
{
    if (tx_count == 0)
    {       
        //lv_disp_t *disp = lv_disp_get_default();
        //if (disp != NULL) {
        //    lv_disp_flush_ready(disp->driver); 
        //}这个是为了适配LVGL加的
        return;
    }
    else if(dma_busy)
    {
        return;
    }

    uint8_t index = tx_read_index;
    spi_transaction_t *tx = &tx_queue[index];
    
    // 设置DC引脚
    if (tx->is_data) {
        OLED_DC_Set(); // 数据模式
    } else {
        OLED_DC_Clr(); // 命令模式
    }
    OLED_CS_Clr();
    dma_busy = 1;
    HAL_SPI_Transmit_DMA(&hspi1, tx->data, tx->data_size);
}

然后最后一个是DMA传输完成的回调,进入回调后需要寻找队列中是否还有数据,如果还有数据,就把数据传给DMA

cpp 复制代码
/******************************************************************************
      函数说明:DMA传输完成回调
******************************************************************************/
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
    if (hspi == &hspi1) {
        OLED_CS_Set(); // 传输完成,拉高CS
        dma_busy = 0;
        
        // 更新队列
        tx_read_index = (tx_read_index + 1) % SPI_QUEUE_SIZE;
        tx_count--;
        // 立即启动下一个传输
        if (tx_count == 0)
        {       
            //lv_disp_t *disp = lv_disp_get_default();
            //if (disp != NULL) {
            //    lv_disp_flush_ready(disp->driver);
            //}这个是为了适配LVGL
        }
        else if (tx_count > 0) {
            LCD_Start_Next_Transaction();
        }
    }
}

最后是完整版本的代码

cpp 复制代码
#include "oled.h"
#include <stdbool.h>
#include <stdint.h>
//#include "lvgl.h"
#include "stm32f4xx_hal.h"
// 在oled.h中定义

#define SPI_BUFFER_SIZE 4096

#define SPI_QUEUE_SIZE 8
typedef struct {
    uint8_t data[SPI_BUFFER_SIZE];
    uint16_t data_size;
    bool is_data; // 0:命令, 1:数据
} spi_transaction_t;

// 全局传输队列
spi_transaction_t tx_queue[SPI_QUEUE_SIZE];

//队列索引
volatile uint16_t tx_read_index = 0;
volatile uint16_t tx_write_index = 0;
volatile uint16_t tx_count = 0;
//DMA忙标志
volatile uint8_t dma_busy = 0;

// 外部 SPI 句柄
extern SPI_HandleTypeDef hspi1;

/******************************************************************************
      函数说明:启动下一个DMA传输任务
******************************************************************************/
static void LCD_Start_Next_Transaction(void)
{
    if (tx_count == 0)
    {       
        //lv_disp_t *disp = lv_disp_get_default();
        //if (disp != NULL) {
        //    lv_disp_flush_ready(disp->driver);  // 正确!
        //}
        return;
    }
    else if(dma_busy)
    {
        return;
    }

    uint8_t index = tx_read_index;
    spi_transaction_t *tx = &tx_queue[index];
    
    // 设置DC引脚
    if (tx->is_data) {
        OLED_DC_Set(); // 数据模式
    } else {
        OLED_DC_Clr(); // 命令模式
    }
    OLED_CS_Clr();
    dma_busy = 1;
    HAL_SPI_Transmit_DMA(&hspi1, tx->data, tx->data_size);
}

/******************************************************************************
      函数说明:添加SPI传输任务到队列
******************************************************************************/
static void LCD_Add_To_Queue(uint8_t *data, uint16_t size, uint8_t is_data)
{
    // 等待队列有空位(非阻塞式可添加超时机制)
    while (tx_count >= SPI_QUEUE_SIZE) {
        // 可在此处添加少量延迟或任务调度
    }
    
    // 复制数据到队列
    uint8_t index = tx_write_index;
    memcpy(tx_queue[index].data, data, size);
    tx_queue[index].data_size = size;
    tx_queue[index].is_data = is_data;
    
    // 更新队列索引
    tx_write_index = (tx_write_index + 1) % SPI_QUEUE_SIZE;
    tx_count++;
    
    // 如果DMA空闲,立即启动传输
    if (!dma_busy) {
        LCD_Start_Next_Transaction();
    }
}

/******************************************************************************
      函数说明:DMA传输完成回调
******************************************************************************/
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
    if (hspi == &hspi1) {
        OLED_CS_Set(); // 传输完成,拉高CS
        dma_busy = 0;
        
        // 更新队列
        tx_read_index = (tx_read_index + 1) % SPI_QUEUE_SIZE;
        tx_count--;
        // 立即启动下一个传输
        if (tx_count == 0)
        {       
            //lv_disp_t *disp = lv_disp_get_default();
            //if (disp != NULL) {
            //    lv_disp_flush_ready(disp->driver);
            //}
        }
        else if (tx_count > 0) {
            LCD_Start_Next_Transaction();
        }
    }
}

/******************************************************************************
      函数说明:DMA传输错误回调
******************************************************************************/
void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi)
{
    if (hspi == &hspi1) {
        //OLED_CS_Set(); // 错误时也要拉高CS
        dma_busy = 0;
        // 可添加错误处理逻辑
    }
}

/******************************************************************************
      函数说明:LCD写命令
******************************************************************************/
void LCD_WR_REG(u8 dat)
{
    LCD_Add_To_Queue(&dat, 1, 0); // 0表示命令
}

/******************************************************************************
      函数说明:LCD写8位数据
******************************************************************************/
void LCD_WR_DATA8(u8 dat)
{
    LCD_Add_To_Queue(&dat, 1, 1); // 1表示数据
}

/******************************************************************************
      函数说明:LCD写16位数据
******************************************************************************/
void LCD_WR_DATA(u16 dat)
{
    uint8_t temp[2] = {dat >> 8, dat & 0xFF};
    LCD_Add_To_Queue(temp, 2, 1); // 1表示数据
}

/******************************************************************************
      函数说明:批量写入数据(优化性能)
******************************************************************************/
void LCD_Write_Bulk(uint8_t *data, uint16_t size)
{
    LCD_Add_To_Queue(data, size, 1);
}

/******************************************************************************
      函数说明:设置显示区域起始坐标和结束坐标
      参数说明:x1,x2 起始和结束的列地址
                y1,y2 起始和结束的行地址
      返回值:  无
******************************************************************************/
void LCD_Address_Set(u16 x1, u16 y1, u16 x2, u16 y2)
{
    if (USE_HORIZONTAL == 0)
    {
        LCD_WR_REG(0x2A); // Column Address Set
        LCD_WR_DATA(x1);
        LCD_WR_DATA(x2);
        LCD_WR_REG(0x2B); // Page Address Set
        LCD_WR_DATA(y1);
        LCD_WR_DATA(y2);
        LCD_WR_REG(0x2C); // Memory Write
    }
    else if (USE_HORIZONTAL == 1)
    {
        LCD_WR_REG(0x2A);
        LCD_WR_DATA(y1);          // 注意:x 和 y 互换
        LCD_WR_DATA(y2);
        LCD_WR_REG(0x2B);
        LCD_WR_DATA(239 - x2);    // 坐标翻转
        LCD_WR_DATA(239 - x1);
        LCD_WR_REG(0x2C);
    }
    else if (USE_HORIZONTAL == 2)
    {
        LCD_WR_REG(0x2A);
        LCD_WR_DATA(239 - x2);
        LCD_WR_DATA(239 - x1);
        LCD_WR_REG(0x2B);
        LCD_WR_DATA(319 - y2);
        LCD_WR_DATA(319 - y1);
        LCD_WR_REG(0x2C);
    }
    else if (USE_HORIZONTAL == 3)
    {
        LCD_WR_REG(0x2A);
        LCD_WR_DATA(319 - y2);
        LCD_WR_DATA(319 - y1);
        LCD_WR_REG(0x2B);
        LCD_WR_DATA(x1);
        LCD_WR_DATA(x2);
        LCD_WR_REG(0x2C);
    }
}

/******************************************************************************
      函数说明:LCD初始化
      参数说明:无
      返回值:  无
******************************************************************************/
void Lcd_Init(void)
{
    OLED_RES_Clr();
    HAL_Delay(200);
    OLED_RES_Set();
    HAL_Delay(100);

    //************* Start Initial Sequence **********//
    LCD_WR_REG(0x36);
    if (USE_HORIZONTAL == 0) LCD_WR_DATA8(0x00);
    else if (USE_HORIZONTAL == 1) LCD_WR_DATA8(0xC0);
    else if (USE_HORIZONTAL == 2) LCD_WR_DATA8(0x70);
    else LCD_WR_DATA8(0xA0);

    LCD_WR_REG(0x3A);
    LCD_WR_DATA8(0x05);

    LCD_WR_REG(0xB2);
    LCD_WR_DATA8(0x0C);
    LCD_WR_DATA8(0x0C);
    LCD_WR_DATA8(0x00);
    LCD_WR_DATA8(0x33);
    LCD_WR_DATA8(0x33);

    LCD_WR_REG(0xB7);
    LCD_WR_DATA8(0x35);

    LCD_WR_REG(0xBB);
    LCD_WR_DATA8(0x19);

    LCD_WR_REG(0xC0);
    LCD_WR_DATA8(0x2C);

    LCD_WR_REG(0xC2);
    LCD_WR_DATA8(0x01);

    LCD_WR_REG(0xC3);
    LCD_WR_DATA8(0x12);

    LCD_WR_REG(0xC4);
    LCD_WR_DATA8(0x20);

    LCD_WR_REG(0xC6);
    LCD_WR_DATA8(0x0F);

    LCD_WR_REG(0xD0);
    LCD_WR_DATA8(0xA4);
    LCD_WR_DATA8(0xA1);

    LCD_WR_REG(0xE0);
    LCD_WR_DATA8(0xD0);
    LCD_WR_DATA8(0x04);
    LCD_WR_DATA8(0x0D);
    LCD_WR_DATA8(0x11);
    LCD_WR_DATA8(0x13);
    LCD_WR_DATA8(0x2B);
    LCD_WR_DATA8(0x3F);
    LCD_WR_DATA8(0x54);
    LCD_WR_DATA8(0x4C);
    LCD_WR_DATA8(0x18);
    LCD_WR_DATA8(0x0D);
    LCD_WR_DATA8(0x0B);
    LCD_WR_DATA8(0x1F);
    LCD_WR_DATA8(0x23);

    LCD_WR_REG(0xE1);
    LCD_WR_DATA8(0xD0);
    LCD_WR_DATA8(0x04);
    LCD_WR_DATA8(0x0C);
    LCD_WR_DATA8(0x11);
    LCD_WR_DATA8(0x13);
    LCD_WR_DATA8(0x2C);
    LCD_WR_DATA8(0x3F);
    LCD_WR_DATA8(0x44);
    LCD_WR_DATA8(0x51);
    LCD_WR_DATA8(0x2F);
    LCD_WR_DATA8(0x1F);
    LCD_WR_DATA8(0x1F);
    LCD_WR_DATA8(0x20);
    LCD_WR_DATA8(0x23);

    LCD_WR_REG(0x21); // 逆显示

    LCD_WR_REG(0x11); // Sleep Out
    HAL_Delay(120);

    LCD_WR_REG(0x29); // Display On
}


/******************************************************************************
      函数说明:填充指定区域
      参数说明:xsta,ysta   起始坐标
                xend,yend   结束坐标
      返回值:  无
******************************************************************************/
void LCD_Fill(u16 xsta, u16 ysta, u16 xend, u16 yend, u16 *color) {
    LCD_Address_Set(xsta, ysta, xend, yend);
    uint32_t total_pixels = (xend - xsta + 1) * (yend - ysta + 1);
    uint8_t *color_byte = (uint8_t*)color; // 转换为字节指针
    uint32_t transferred_pixels = 0;

    while (transferred_pixels < total_pixels) {
        // 每次传输最大块(SPI_BUFFER_SIZE字节 = 缓冲区容量)
        uint32_t chunk_bytes = SPI_BUFFER_SIZE;
        // 剩余像素对应的字节数 = (总像素 - 已传像素) * 2
        uint32_t remaining_bytes = (total_pixels - transferred_pixels) * 2;
        if (chunk_bytes > remaining_bytes) {
            chunk_bytes = remaining_bytes;
        }
        // 入队当前块
        LCD_Write_Bulk(color_byte + (transferred_pixels * 2), chunk_bytes);
        transferred_pixels += chunk_bytes / 2; // 字节数转像素数
    }
}



// 临时缓冲区:10 行
static u16 fill_buf[LCD_H * 10];

/**
 * @brief 测试:逐块填充整个屏幕为蓝色
 */
void test_fill_entire_screen_blue(void)
{
    uint16_t y;

    HAL_Delay(500);

    // 填充当前块为蓝色
    for (int i = 0; i < LCD_H * 10; i++) {
        fill_buf[i] = YELLOW;
    }

    // 从 Y=0 开始,每次填充 10 行,直到填满 240 行
    for (y = 0; y < LCD_H; y += 10) {
        uint16_t y_end = (y + 9) < LCD_H ? (y + 9) : (LCD_H - 1);
        LCD_Fill(0, y, LCD_W, y_end, fill_buf);
    }
}

还有对应的.h文件

cpp 复制代码
#ifndef __OLED_H
#define __OLED_H	   						  

#include "main.h"
#include "stdlib.h"	  
#include "string.h"

#define USE_HORIZONTAL 0


#if USE_HORIZONTAL == 0 || USE_HORIZONTAL == 2
    #define LCD_W  240
    #define LCD_H  320
#else
    #define LCD_W  320
    #define LCD_H  240
#endif

#define	u8 unsigned char
#define	u16 unsigned int
#define	u32 unsigned long


#define OLED_CS_Clr() HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_RESET)
#define OLED_CS_Set() HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_SET)

#define OLED_DC_Clr() HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_RESET)
#define OLED_DC_Set() HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_SET)

#define OLED_RES_Clr() HAL_GPIO_WritePin(LCD_RST_GPIO_Port, LCD_RST_Pin, GPIO_PIN_RESET)
#define OLED_RES_Set() HAL_GPIO_WritePin(LCD_RST_GPIO_Port, LCD_RST_Pin, GPIO_PIN_SET)


#define OLED_CMD  0	
#define OLED_DATA 1	


void Lcd_Init(void); 
void test_fill_entire_screen_blue(void);



#define WHITE         	 0xFFFF
#define BLACK         	 0x0000	  
#define BLUE           	 0x001F  
#define BRED             0XF81F
#define GRED 			 0x07E0
#define GBLUE			 0X07FF
#define RED           	 0xF800
#define MAGENTA       	 0xF81F
#define GREEN         	 0x07E0
#define CYAN          	 0x7FFF
#define YELLOW        	 0xFFE0
#define BROWN 			 0XBC40
#define BRRED 			 0XFC07
#define GRAY  			 0X8430 

#define DARKBLUE      	 0X01CF	
#define LIGHTBLUE      	 0X7D7C	
#define GRAYBLUE       	 0X5458 

 
#define LIGHTGREEN     	 0X841F 
#define LGRAY 			 0XC618 

#define LGRAYBLUE        0XA651 
#define LBBLUE           0X2B12 
					  		 
#endif   		     
相关推荐
小莞尔7 小时前
【51单片机】【protues仿真】基于51单片机音乐盒(8首歌曲)系统
c语言·开发语言·单片机·嵌入式硬件·51单片机
智者知已应修善业7 小时前
【51单片机三路抢答器定时器1工作1外部中断1】2022-11-24
c语言·经验分享·笔记·嵌入式硬件·51单片机
霜绛7 小时前
Unity:XML笔记(一)——Xml文件格式、读取Xml文件、存储修改Xml文件
xml·笔记·学习·unity·游戏引擎
上等猿7 小时前
RPC个人笔记(包含动态代理)
笔记·rpc
虚行8 小时前
机器视觉软件--VisionPro、Visual Master,Halcon 和 OpenCV 的学习路线
学习
劲镝丶8 小时前
51单片机的电子音乐盒 (详细教程)
c语言·c++·单片机·嵌入式硬件·51单片机
强盛小灵通专卖员8 小时前
边缘计算设备 RK3576芯片
人工智能·深度学习·物联网·边缘计算·sci·rk3576·小论文
jianqiang.xue9 小时前
Proteus8 仿真教学全指南:从入门到实战的电子开发利器
stm32·单片机·51单片机·proteus·仿真
玛丽莲茼蒿9 小时前
K8s学习笔记(一)——
笔记·学习·kubernetes