STM32单片机初学6-SPI通信驱动IPS彩屏

SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线。

和IIC一样,是单片机开发中最常用的通信方式之一。对比IIC,其特点表现在:

1.通信速率快,以STM32F103C8T6为例,其SPI通信速率可达18Mbps,即2.25MB/s,而IIC高速模式也才3.4Mbps;

2.全双工通信,SPI有两根数据线,一根主出从入,一根主入从出,在同一时刻既可以发送数据,也可以接受数据,而IIC是半双工,发送、接受数据不能同时进行;

主要应用场景有SD卡数据读写、TFT彩屏控制等。

本文将详细讲解利用STM32的硬件SPI接口来驱动IPS彩屏显示图片(显示文字就不详细介绍了,与OLED显示文字类似)。

先来看看SPI物理接口定义:  

MISO– Master Input Slave Output主设备数据输入,从设备数据输出;

MOSI– Master Output Slave Input,主设备数据输出,从设备数据输入;

SCLK – Serial Clock,时钟信号,由主设备产生;

CS – Chip Select,从设备使能信号,由主设备控制,每个从设备对应主设备的一个CS接口,若要与某个从设备进行通讯,则通过CS使能对应的从设备。在STM32中为SPI_NSS。

在我关于IIC软件模拟的文章中详细解析了IIC的软件模拟实现方法,同样的,SPI也能用软件模拟实现,只要按照其时序控制GPIO电平高低。这里暂不使用软件模拟实现,直接配置STM32内部的硬件SPI,再调用库函数。以STM32F103C8T6为例,该型号共有两个SPI。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_20,color_FFFFFF,t_70,g_se,x_16

这里我使用的屏幕是1.3寸的IPS彩屏,分辨率240*240,最高支持16位真彩(RGB565),驱动芯片ST7789。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_20,color_FFFFFF,t_70,g_se,x_16

 16位真彩即每个像素点有65535(2^16)种颜色。

三原色原理中说道:大多数的颜色可以通过红、绿、蓝三色按照不同的比例合成产生。同样,绝大多数单色光也可以分解成红、绿、蓝三种色光。

IPS屏幕上的每个像素点都由一组红绿蓝遮光片组成,白色的背光透过遮光片就呈现出红绿蓝三种颜色。改变对应遮光片的角度就能改变该颜色的强度,不同强度的红绿蓝三色混合在一起就组成了屏幕上显示的65535种颜色。

在这16位中,前面5位表示红色的强度,中间6位表示绿色的强度,后面5位表示蓝色的强度。也就是红绿蓝分别占5、6、5位,这就是RGB565得名原因。所以,红绿蓝三色的强度分别有32、64、32级。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_20,color_FFFFFF,t_70,g_se,x_16

一个像素点需要占用16个bit(即2Byte)的数据,整个屏幕就需要240x240x2=115200Byte(即115.2KB)数据。

——-针脚定义:

该屏幕有7个针脚,接口定义及与单片机连接方式如下:

GND:供电电源地

VCC:  供电电源正(3-5V)

SCL:SPI时钟线。接单片机SPI_SCK

SDA:SPI数据线。对该屏幕来说。只有数据输出而不需要输出,即主出从入,所以接单片机MOSI。单片机的MISO悬空不用。

RES:屏幕复位。单片机与屏幕进行通讯前,需将屏幕复位一次,即将该引脚置低短暂时间后再置高(习惯100ms)。

DC:数据/命令选择端口。与我之前介绍的OLED屏幕不同,OLED是通过向不同的寄存器里写数据来区分是命令还是显示SRAM数据,而该屏幕是通过该引脚电平状态来区分当前收到的数据是命令还是显示数据。DC端口为低时,写入的是命令;为高时,写入的是数据。

BLK:屏幕背光。与像素点自发光的OLED屏幕不同,IPS屏幕是需要有背光才能显示图像的。该屏幕默认是开启背光的,当BLK端口被置低,背光关闭。这里可以用一个GPIO来控制,或者直接悬空,或者接高电平。

具体与STM32的连接方式如下:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_20,color_FFFFFF,t_70,g_se,x_16

 因为这里只有TFT屏幕一个从站,所以SPI_NSS不接。

——————————————

接下来,一边看程序,一边讲解

1.头文件及GPIO置复位定义

将用到的外设头文件包含到.c文件中:GPIO、RCC、SPI

定义PA1、PA2、PA3的高低电平。这与51单片机不一样,51单片机可以使用sbit将变量映射到GPIO口,但是STM32没有sbit。

#include<stm32f10x.h>
#include<stm32f10x_gpio.h>
#include<stm32f10x_rcc.h>
#include<stm32f10x_spi.h>

#define SPI_DC_H() GPIO_SetBits(GPIOA,GPIO_Pin_2)			//数据/命令选择
#define SPI_DC_L() GPIO_ResetBits(GPIOA,GPIO_Pin_2)			

#define LCD_RES_H() GPIO_SetBits(GPIOA,GPIO_Pin_3)				//LCD复位(PA3)
#define LCD_RES_L() GPIO_ResetBits(GPIOA,GPIO_Pin_3)

#define LCD_BLK_H()	GPIO_SetBits(GPIOA,GPIO_Pin_1)				//LCD背光(PA1)
#define LCD_BLK_L() GPIO_ResetBits(GPIOA,GPIO_Pin_1)

2.将图片转成十六进制数据

要在OLED显示字符,是通过让屏幕像素点按照字模点亮或者灭来实现。显示图片也是同理,对于彩屏,只是每个像素点不止0或者1两种状态,它有65535种状态,只要让每个像素点按照我想要的方式处于各自的方式即可得到一张图片。我们可以把它放在一个数组里,里面的元素就是每个像素点的状态值。然后通过SPI按顺序将数组的元素传输至TFT屏幕的显示缓存寄存器,就能在屏幕显示想要的图片了。

这里需要用到的小工具是Img2Lcd。这是一个可以将图片转成数组的工具。可在网上下载该工具。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_20,color_FFFFFF,t_70,g_se,x_16

 现在来演示如何将下面这张图片转成C程序数组。由于我这里使用的240×240的正方形屏幕,所以为了能填满屏屏幕,特意将该图片裁切成了正方形。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_18,color_FFFFFF,t_70,g_se,x_16

 需要注意,Img2Lcd只能转换jpg格式的图片,如果是其他格式的图片,需要先转换成jpg格式。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_14,color_FFFFFF,t_70,g_se,x_16

 然后在小工具中打开该文件

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_20,color_FFFFFF,t_70,g_se,x_16

 打开之后还需要设置相关参数。输出灰度选择16位真彩色。

最大宽度与与高度设置需要注意,我这里使用的STM32F103C8T6,其Flash只有64KB,所以该数组的大小绝不能超过64KB,否则会因为存储空间不足而通不过编译。这里我留一定的Flash余量给其他变量及程序占用,给数组分配60KB的空间。而该屏幕一个像素点需要2B的空间,也就是一共能存储60K÷2=30000个像素点的状态。再对30000开根号,约等于173。所以这里将最大宽度与高度设置成173×173。(如果使用的单片机是128K或者以上的flash,则可以全屏显示,设置成240×240)

取消勾选“包含图像头数据”,勾选“高位在前”(这是因为该屏幕是先发送高位再发送低位,如果不勾选,则会出现反色效果)。最后,还可以稍微将对比度拉高一点(因为IPS屏色彩不够鲜艳,最后的画面会有点暗淡)。

设置完成之后,点击“最大宽度和高度”右边的小按钮,即可生成预览图如下。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_20,color_FFFFFF,t_70,g_se,x_16

 然后点击“保存”,选择路径后确认即可得到一个.c的文件,这里我们直接用记事本打开。可以发现里面是一个数组,元素个数为59859,正好等于173x173x2。

这里每个元素是一个2位十六进制的数,每两个元素代表一个像素点。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_20,color_FFFFFF,t_70,g_se,x_16

 然后,我们将该数组复制粘贴到程序中(如下)。数组很长,可将其折叠。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_20,color_FFFFFF,t_70,g_se,x_16

 这样,这步就算完成了。

3.SPI初始化

因为使用的是硬件SPI1,所以需要对SPI1进行初始化。

首先是GPIO设置,SPI1占用PA4-PA7共四个IO,这四个IO设置成复用推挽输出。另外还使用了PA1,PA2,PA3三个IO口分别做为背光、数据/命令选择、复位的控制端口。所以这里也需要给这三个IO初始化,设为推挽输出即可。然后开启对应的时钟。
然后是SPI的配置,与GPIO一样,在SPI的外设库中也有一个结构体,里面的成员是SPI的各项参数。在外设库中可以查看该结构体具体内容。8f7c84e0e5104c33a422f4e6ccc2fa00.png

下面的代码可供参考

void SPI_UserInit(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7;    //PA4为SPI_NSS,PA5为SPI_SCK,PA6为SPI_MISO,PA7为SPI_MOSI,对SPI这4个端口进行初始化
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;			
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;				//复用推挽输出
  GPIO_Init(GPIOA, &GPIO_InitStructure);

  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;		//PA1为BLK背光,PA2位DC选择,PA3为屏幕复位
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;			
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;			//推挽输出
  GPIO_Init(GPIOA, &GPIO_InitStructure);
 
  SPI_InitTypeDef SPI_InitStructure;
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);		//使能时钟

  SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//设置为双向双线全双工
  SPI_InitStructure.SPI_Mode = SPI_Mode_Master;			//设置为SPI主站。控制屏幕,单片机需做主站
  SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;	//设置数据大小为8位,即每次发送8位数据
  SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;		//设置串行时钟的稳态为时钟高
  SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;		//设置位捕捉的时钟活动沿为第一个上升沿
  SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;			//设置NSS为软件控制
  SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;//设置波特率分频(该值越大,波特率越慢)
  SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;	//设置先发送高位
  SPI_InitStructure.SPI_CRCPolynomial = 7;			//设置CRC校验												
  SPI_Init(SPI1, &SPI_InitStructure);				//初始化
 
  SPI_Cmd(SPI1, ENABLE);		//使能SPI1
}

4.SPI写数据与写命令

与OLED屏幕一样,需要分别声明写数据和写命令的子函数。

前面讲过,该屏幕是通过DC端口来区分命令与数据,所以写数据与写命令的区别就是DC端口分别为高电平与低电平。由于写数据还分写8位数据与写16位数据,所以还得分两个写数据的函数。

需要注意,SPI本身是支持16数据帧的(通过 SPI_InitStructure.SPI_DataSize 设置),但是为了方便发送8位的数据帧,所以在SPI初始化中把DataSize设置成了8。而发送16位数据是通过先发送低8位,然后数据右移8位,再发送一次8位数据实现的。

void SPI_WriteCmd(u8 Data)	//写命令
{
	SPI_DC_L();
	SPI_I2S_SendData(SPI1,Data);
}

void LCD_WriteData8(u8 Data)	//写8位数据
{
	SPI_DC_H();
	SPI_I2S_SendData(SPI1,Data);
}

void LCD_WriteData16(u16 Data)	//写16位数据
{
	SPI_DC_H();
	SPI_I2S_SendData(SPI1,(Data>>8) ) ;	//Date右移8位
	SPI_I2S_SendData(SPI1,Data);		
}

5.屏幕初始化

与OLED屏幕一样,该IPS上电之后需要设置参数进行初始化。

具体需要设置的参数可查阅ST7789使用书册。可参考我以下参数。

void LCD_Init(void)
{
	u16 x;
	LCD_RES_L();
	Delay_ms(50);
	LCD_RES_H();
	Delay_ms(50);                //复位屏幕
	
	SPI_WriteCmd(0x3A);						//设置颜色格式为16位真彩
	LCD_WriteData8(0x05);				//03 4K;05 65K;06 262K
	
	SPI_WriteCmd(0x36);			//设置屏幕方向为从上到下,从左到右
	LCD_WriteData8(0x00);

//----------- ST7789S Frame rate setting ---------//
	SPI_WriteCmd(0xB2);				
	LCD_WriteData8(0x0C);
	LCD_WriteData8(0x0C);
	LCD_WriteData8(0x00);
	LCD_WriteData8(0x33);
	LCD_WriteData8(0x33);
	
	SPI_WriteCmd(0xB7); 
	LCD_WriteData8(0x35);
	  
//----------- ST7789S Power setting ---------//

	SPI_WriteCmd(0xBB);
	LCD_WriteData8(0x19);
	
	SPI_WriteCmd(0xC0);
	LCD_WriteData8(0x2C);
	
	SPI_WriteCmd(0xC2);
	LCD_WriteData8(0x01);
	
	SPI_WriteCmd(0xC3);
	LCD_WriteData8(0x12);   
	
	SPI_WriteCmd(0xC4);
	LCD_WriteData8(0x20);  
	
	SPI_WriteCmd(0xC6); 
	LCD_WriteData8(0x0F);    
	
	SPI_WriteCmd(0xD0); 
	LCD_WriteData8(0xA4);
	LCD_WriteData8(0xA1);

//----------- Posistive Voltage Gamma Control ---------//
	SPI_WriteCmd(0xE0);
	LCD_WriteData8(0xD0);
	LCD_WriteData8(0x04);
	LCD_WriteData8(0x0D);
	LCD_WriteData8(0x11);
	LCD_WriteData8(0x13);
	LCD_WriteData8(0x2B);
	LCD_WriteData8(0x3F);
	LCD_WriteData8(0x54);
	LCD_WriteData8(0x4C);
	LCD_WriteData8(0x18);
	LCD_WriteData8(0x0D);
	LCD_WriteData8(0x0B);
	LCD_WriteData8(0x1F);
	LCD_WriteData8(0x23);

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

//----------- Setting ---------//
	SPI_WriteCmd(0x21); 
	SPI_WriteCmd(0x11)
	SPI_WriteCmd(0x29);

	LCD_SetRegion(0,0,239,239);
	SPI_DC_H();
	for(x=0;x<57600;x++)            //清屏
		{
			LCD_WriteData16(0xffff);
		}
	LCD_BLK_H();
}

在函数后面,我这里加入了一个循环,这是用来清屏的。因为屏幕内部显示SRAM的数据是掉电就会丢失的。每次上电后并不会显示上次断电前的图像,而是显示像下面所示的雪花屏。

清屏的原理就是重新给显示SRAM写入一次数据,用某个状态(可以理解为颜色)填充所有像素点。这里我填充的是0xffff,0xffff在RGB565中是纯白色,所以最终效果是白屏。如果填充0x0000 ,则填充的是黑色,最终效果是黑屏。当然也可以根据需要填充任何自己想要的颜色。

循环一次填充一个像素点,全屏共57600个像素点,所以循环57600次。

6.设置显示区域

显示区域是一个长方形区域,所以需要设置起点与终点。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_20,color_FFFFFF,t_70,g_se,x_16

0x2a:设置(x0,x1)

0x2b:设置(y0,y1)

void LCD_SetRegion(u16 x0, u16 y0, u16 x1, u16 y1)//设置显示区域(x0,y0,x1,y1)
{
	SPI_WriteCmd(0x2a);
	LCD_WriteData16(x0);
	LCD_WriteData16(x1);
	SPI_WriteCmd(0x2b);
	LCD_WriteData16(y0);
	LCD_WriteData16(y1);
	SPI_WriteCmd(0x2c);	        //下面发送的是数据
}

7.SPI传输数组

STM32中SPI传输数据的库函数为        SPI_I2S_SendData(SPIx,Deta);

图片数组里共有59859个元素,所以需要发送59859个数据。发送数据前,需要将DC端口置高。

void image(void)
	{
		u16 x;
		SPI_DC_H();        //DC置高,发送数据
		for(x=0;x<59860;x++)    //循环填充像素
		{
			SPI_I2S_SendData(SPI1,gImage_girl[x]);    //将图片数组的元素通过SPI发送给IPS屏
		}
	}

8.主函数

初始化GPIO、SPI、ISP屏幕,设置显示区域坐标,发送数据。

为了能让图片在屏幕正中间显示,我们需要把显示区域设置在正中间。
这里进行简单的计算,240-173=67,67/2≈33,所以可以把坐标(x0,y0)设为(33,33),
图片分辨率为173 x173,所以x1,y1的坐标应为(33+173-1,33+173-1),即205,205。这里之所以需要减1,类似于小学数学的“种树问题”。所以限定区域的函数应为(33,33,205,205)

32756d0db6574345821b7c93c083e5b8.png

int main(void)
{	
	SPI_UserInit();
	LCD_Init();
	LCD_SetRegion(33,33,205,205);
	image();
}

最后显示效果如下:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_20,color_FFFFFF,t_70,g_se,x_16

如果是Flash大于115KB的单片机,则可以全屏显示(相机拍摄原因,导致图片看起来有色差):

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_20,color_FFFFFF,t_70,g_se,x_16

如果Flash较小,通过分多次也能显示全屏。

通过以上步骤,就能在屏幕上显示各种图片以及播放视频了。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_20,color_FFFFFF,t_70,g_se,x_16

 《bad apple》演示

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAI2xpdWZlbmdlcyM=,size_20,color_FFFFFF,t_70,g_se,x_16

显示文字

         本文章只是讲解并演示如何利用SPI通信在IPS屏上显示图片。不管何种单片机其Flash都是很有限的,实际运用中并不会将图片直接存在内部Flash中。最常用的方法是将图片存在SD卡,单片机再读取SD卡的数据。关于SD卡的使用,后面的文章再做讲解。

对于其他进阶用法如制作动画效果、播放视频以后再做介绍。

物联沃分享整理
物联沃-IOTWORD物联网 » STM32单片机初学6-SPI通信驱动IPS彩屏

发表评论