小熊派 FreeRTOS+SPI+DMA 驱动 TFT-LCD
一、文章前言
入手了一块小熊派开发板,看到他板子上搭载了一块 TFT-LCD 编写编写驱动代码来使用 TFT ,该 TFT 通过 ST7789 驱动芯片进行驱动,本文通过 CubeMX 软件配置硬件 SPI + DMA 方式来驱动 ST7789,同时配置 FreeRTOS 方便控制 DMA, 文章 ST7789 的驱动代码参考 Mculover666 大神的博客:
【STM32Cube_17】使用硬件SPI驱动TFT-LCD(ST7789)
二、SPI+DMA 配置
先看一下小熊派 TFT 接口原理图:
建立一个适合小熊派 STM32L431RCT6 工程,设置时钟
开启 SPI2 ,修改时钟 SPI_CLK 到 PB13,选择只发模式,配置基本参数 8 位位宽,选择空闲时钟为高电平,第二个跳边沿传输数据:
然后进入到 DMA 设置,添加 DMA 通道,参数保持默认:
之后再配置相关中断优先级:
除此之外,TFT 的驱动还需要开启许多 GPIO 控制口:
- 控制背光的 LCD_PWR 原理图对应的 PB15
- 控制读写的 LCD_WR_RS 原理图对应 PC6
- 控制复位的 LCD_RST 原理图对应 PC7
GPIO 配置很简单不多说,具体步骤可以参考 Mculover666 大神的文章:
配置完成如下:
以上 SPI+DMA 驱动配置好了,下面配置 FreeRTOS
三、FreeRTOS 配置
开启 FreeRTOS 是为了更加方便 DMA 逻辑代码的实现,更重要的是因为后面我计划移植 LVGL 图形库,所以需要一个 RTOS 做支撑,FreeRTOS 的使用详细介绍可以看我的这篇文章:
下面简单介绍流程
点击中间件,选择 FreeRTOS,选择 CMSIS V2 版本接口:
创建一个任务用于刷新显存到 TFT:
创建一个二值信号量用于 DMA 回调的同步:
修改 HAL_Delay 的延时函数时基定时器为 TIM1 防止使用和 RTOS 的 Systick 冲突:
配置完成生成代码,具体步骤可以看参考文章
四、代码编写
生成代码后我们就可以编写逻辑代码了,但在编写前我们先大致了解一下驱动思路:
我们基于 FreeRTOS 操作系统编写驱动代码,建立一个写缓存任务,用于将显存区域的显示数据通过 SPI+DMA 的方式写入到 TFT 中,因为开启的是 DMA ,所以实际数据的写入过程是由 DMA 收发器完成的,因为更新显存的时候需要写大量数据,每次传输数据都要写几毫秒,所以将这个任务交给 DMA 去完成,我们只需要等待他 DMA 传输完成后再传输下一组数据就行,配合 FreeRTOS 使用信号量,DMA 没有传输完成时不会有信号释放,这时更新缓存任务就会挂起,等他发送完成才会执行,在 72M 主频的单片机移植 FreeRTOS 时,任务的调度一般 5us 作用,所以切换效率是非常高的,这样配合 FreeRTOS 使用 DMA 基本上写显示屏对主机资源的占用会压缩的非常非常小,下面是我根据 mculover666 教程代码修改的驱动代码
lcd.c 文件代码:
#include "lcd.h"
#include "gpio.h"
#include "spi.h"
#include "cmsis_os.h"
extern osSemaphoreId_t DMA_SemaphoreHandle;
//显存定义
//显存总大小 240*240*(16bit) = 240*240*2 个字节
#define LCD_TOTAL_BUF_SIZE (240*240*2)
//因为直接定义显存太大了,所以定义其 1/100 轮流刷新
#define LCD_Buf_Size 1152
static uint8_t lcd_buf[LCD_Buf_Size];
/**
*@brief LCD控制引脚和通信接口初始化
*@param none
*@retval none
*/
static void LCD_GPIO_Init(void)
{
/* 复位LCD */
LCD_PWR(0);
LCD_RST(0);
HAL_Delay(100);
LCD_RST(1);
}
/* USER CODE BEGIN 1 */
/**
* @brief SPI 发送字节函数
* @param TxData 要发送的数据
* @param size 发送数据的字节大小
* @return 0:写入成功,其他:写入失败
*/
uint8_t SPI_WriteByte(uint8_t *TxData,uint16_t size)
{
osStatus_t result;
//获取信号,如果上一个DMA传输完成
//信号就能获取到,没有传输完成任务就挂起
//等到传输完成再恢复
result = osSemaphoreAcquire(DMA_SemaphoreHandle,0xFFFF);
if(result == osOK)
{
//获取成功
return HAL_SPI_Transmit_DMA(&hspi2,TxData,size);
}else
{
//获取失败
return 1;
}
}
//DMA 传输完成后会调用 SPI传输完成回调函数
//在该函数中我们释放信号
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
if(hspi->Instance == hspi2.Instance)
osSemaphoreRelease(DMA_SemaphoreHandle);
}
/**
* @brief 写命令到LCD
* @param cmd —— 需要发送的命令
* @return none
*/
static void LCD_Write_Cmd(uint8_t cmd)
{
LCD_WR_RS(0);
SPI_WriteByte(&cmd, 1);
}
/**
* @brief 写数据到LCD
* @param dat —— 需要发送的数据
* @return none
*/
static void LCD_Write_Data(uint8_t dat)
{
LCD_WR_RS(1);
SPI_WriteByte(&dat, 1);
}
/**
* @breif 打开LCD显示背光
* @param none
* @return none
*/
void LCD_DisplayOn(void)
{
LCD_PWR(1);
}
/**
* @brief 关闭LCD显示背光
* @param none
* @return none
*/
void LCD_DisplayOff(void)
{
LCD_PWR(0);
}
/**
* @brief 设置数据写入LCD显存区域
* @param x1,y1 —— 起点坐标
* @param x2,y2 —— 终点坐标
* @return none
*/
void LCD_Address_Set(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
{
/* 指定X方向操作区域 */
LCD_Write_Cmd(0x2a);
LCD_Write_Data(x1 >> 8);
LCD_Write_Data(x1);
LCD_Write_Data(x2 >> 8);
LCD_Write_Data(x2);
/* 指定Y方向操作区域 */
LCD_Write_Cmd(0x2b);
LCD_Write_Data(y1 >> 8);
LCD_Write_Data(y1);
LCD_Write_Data(y2 >> 8);
LCD_Write_Data(y2);
/* 发送该命令,LCD开始等待接收显存数据 */
LCD_Write_Cmd(0x2C);
}
/**
* @brief 以一种颜色清空LCD屏
* @param color —— 清屏颜色(16bit)
* @return none
*/
void LCD_Clear(uint16_t color)
{
uint16_t j;
uint8_t data[2] = {0}; //color是16bit的,每个像素点需要两个字节的显存
/* 将16bit的color值分开为两个单独的字节 */
data[0] = color >> 8;
data[1] = color;
/* 显存的值需要逐字节写入显存 */
for(j = 0; j < LCD_Buf_Size / 2; j++)
{
lcd_buf[j * 2] = data[0];
lcd_buf[j * 2 + 1] = data[1];
}
/* 显存更新到 Flash */
LCD_ReFlash();
}
/**
* @brief 全屏更新显存到LCD
* @param none
* @return none
*/
void LCD_ReFlash()
{
uint16_t i;
/* 指定显存操作地址 */
LCD_Address_Set(0, 0, LCD_Width - 1, LCD_Height - 1);
/* 指定接下来的数据为数据 */
LCD_WR_RS(1);
/* 循环将显存缓冲区的数据循环写入到LCD */
for(i = 0; i < (LCD_TOTAL_BUF_SIZE / LCD_Buf_Size); i++)
{
SPI_WriteByte(lcd_buf, (uint16_t)LCD_Buf_Size);
}
}
/**
* @brief LCD画点
* @param none
* @return none
*/
void LCD_Draw_Point(uint16_t x1, uint16_t y1, uint16_t color)
{
uint8_t buf[2];
buf[0]=color>>8;
buf[1]=color;
LCD_Address_Set(x1,y1,x1+1,y1);
SPI_WriteByte(buf, 2);
}
/**
* @brief LCD初始化
* @param none
* @return none
*/
void LCD_Init(void)
{
/* 初始化和LCD通信的引脚 */
LCD_GPIO_Init();
osDelay(120);
/* 关闭睡眠模式 */
LCD_Write_Cmd(0x11);
osDelay(120);
/* 开始设置显存扫描模式,数据格式等 */
LCD_Write_Cmd(0x36);
LCD_Write_Data(0x00);
/* RGB 5-6-5-bit格式 */
LCD_Write_Cmd(0x3A);
LCD_Write_Data(0x65);
/* porch 设置 */
LCD_Write_Cmd(0xB2);
LCD_Write_Data(0x0C);
LCD_Write_Data(0x0C);
LCD_Write_Data(0x00);
LCD_Write_Data(0x33);
LCD_Write_Data(0x33);
/* VGH设置 */
LCD_Write_Cmd(0xB7);
LCD_Write_Data(0x72);
/* VCOM 设置 */
LCD_Write_Cmd(0xBB);
LCD_Write_Data(0x3D);
/* LCM 设置 */
LCD_Write_Cmd(0xC0);
LCD_Write_Data(0x2C);
/* VDV and VRH 设置 */
LCD_Write_Cmd(0xC2);
LCD_Write_Data(0x01);
/* VRH 设置 */
LCD_Write_Cmd(0xC3);
LCD_Write_Data(0x19);
/* VDV 设置 */
LCD_Write_Cmd(0xC4);
LCD_Write_Data(0x20);
/* 普通模式下显存速率设置 60Mhz */
LCD_Write_Cmd(0xC6);
LCD_Write_Data(0x0F);
/* 电源控制 */
LCD_Write_Cmd(0xD0);
LCD_Write_Data(0xA4);
LCD_Write_Data(0xA1);
/* 电压设置 */
LCD_Write_Cmd(0xE0);
LCD_Write_Data(0xD0);
LCD_Write_Data(0x04);
LCD_Write_Data(0x0D);
LCD_Write_Data(0x11);
LCD_Write_Data(0x13);
LCD_Write_Data(0x2B);
LCD_Write_Data(0x3F);
LCD_Write_Data(0x54);
LCD_Write_Data(0x4C);
LCD_Write_Data(0x18);
LCD_Write_Data(0x0D);
LCD_Write_Data(0x0B);
LCD_Write_Data(0x1F);
LCD_Write_Data(0x23);
/* 电压设置 */
LCD_Write_Cmd(0xE1);
LCD_Write_Data(0xD0);
LCD_Write_Data(0x04);
LCD_Write_Data(0x0C);
LCD_Write_Data(0x11);
LCD_Write_Data(0x13);
LCD_Write_Data(0x2C);
LCD_Write_Data(0x3F);
LCD_Write_Data(0x44);
LCD_Write_Data(0x51);
LCD_Write_Data(0x2F);
LCD_Write_Data(0x1F);
LCD_Write_Data(0x1F);
LCD_Write_Data(0x20);
LCD_Write_Data(0x23);
/* 显示开 */
LCD_Write_Cmd(0x21);
LCD_Write_Cmd(0x29);
/* 清屏为白色 */
LCD_Clear(WHITE);
/*打开显示*/
LCD_PWR(1);
}
lcd.h 文件代码:
#include "main.h"
#define LCD_PWR(n) (n?\
HAL_GPIO_WritePin(LCD_PWR_GPIO_Port,LCD_PWR_Pin,GPIO_PIN_SET):\
HAL_GPIO_WritePin(LCD_PWR_GPIO_Port,LCD_PWR_Pin,GPIO_PIN_RESET))
#define LCD_WR_RS(n) (n?\
HAL_GPIO_WritePin(LCD_WR_RS_GPIO_Port,LCD_WR_RS_Pin,GPIO_PIN_SET):\
HAL_GPIO_WritePin(LCD_WR_RS_GPIO_Port,LCD_WR_RS_Pin,GPIO_PIN_RESET))
#define LCD_RST(n) (n?\
HAL_GPIO_WritePin(LCD_RST_GPIO_Port,LCD_RST_Pin,GPIO_PIN_SET):\
HAL_GPIO_WritePin(LCD_RST_GPIO_Port,LCD_RST_Pin,GPIO_PIN_RESET))
//LCD屏幕分辨率定义
#define LCD_Width 240
#define LCD_Height 240
//颜色定义
#define WHITE 0xFFFF //白色
#define YELLOW 0xFFE0 //黄色
#define BRRED 0XFC07 //棕红色
#define PINK 0XF81F //粉色
#define RED 0xF800 //红色
#define BROWN 0XBC40 //棕色
#define GRAY 0X8430 //灰色
#define GBLUE 0X07FF //兰色
#define GREEN 0x07E0 //绿色
#define BLUE 0x001F //蓝色
#define BLACK 0x0000 //黑色
//初始化GPIO
static void LCD_GPIO_Init(void);
//SPI 写入接口
uint8_t SPI_WriteByte(uint8_t *TxData,uint16_t size);
//LCD 写命令
static void LCD_Write_Cmd(uint8_t cmd);
//LCD 写数据
static void LCD_Write_Data(uint8_t dat);
//LCD 开显示
void LCD_DisplayOn(void);
//LCD 关显示
void LCD_DisplayOff(void);
//LCD 设置写入范围
void LCD_Address_Set(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
//LCD 循环更新显存
void LCD_ReFlash(void);
//LCD 清屏
void LCD_Clear(uint16_t color);
//LCD 初始化
void LCD_Init(void);
//LCD 画点
void LCD_Draw_Point(uint16_t x1, uint16_t y1, uint16_t color);
freertos.c 文件代码
刷新任务的代码:
/* USER CODE END Header_TFT_ReFlash_Task */
__weak void TFT_ReFlash_Task(void *argument)
{
/* USER CODE BEGIN TFT_ReFlash_Task */
LCD_Init();
/* Infinite loop */
for(;;)
{
LCD_Clear(RED);
osDelay(1000);
LCD_Clear(BLUE);
osDelay(1000);
LCD_Clear(GREEN);
osDelay(1000);
LCD_Clear(PINK);
osDelay(1000);
}
/* USER CODE END TFT_ReFlash_Task */
}
五、实验现象
换色刷屏: