【快速上手STM32】SPI通信协议&&1.8寸TFT-LCD(ST7735S)
SPI简介
SPI,英文全称Serial Peripheral Interface,即串行外围设备接口,是一种高速、全双工、同步的串行通信总线。
我们之前说过I2C,那么我们就拿I2C和SPI做个对比。
SPI和I2C对比,优势在于SPI的传输速率比I2C快得多,劣势在于SPI需要用的通信线比较多。
I2C只需要两根线,而SPI至少需要4根:
- SCK(Serial Clock):串行时钟线,由主设备产生,用于同步数据传输。
- MOSI(Master Output Slave Input):主机输出从机输入线,主设备通过这条线发送数据给从设备。
- MISO(Master Input Slave Output):主机输入从机输出线,主设备通过这条线接收从设备发送的数据。
- SS(Slave Select):从机选择线(每个从机一根选择线),用于选择与主设备进行通信的从设备。通常情况下,SS线为低电平有效,即当SS线为低电平时,选中对应的从设备进行通信。
通常我们拿个使用SPI来通信的模块都是拿来当从机的,所以我们的STM32(MCU)就是主机了,因此对于我们STM32来说,MOSI就是输出线,MISO就是输入线,那么我们应该怎么去配置对应线连接的GPIO口呢,我们可以看看官方手册。
官方手册的建议(在第八章)是MOSI(主模式)设为推挽复用输出,MISO设为浮空输入或者是上拉输入。
跟I2C一样,我这边还是用的软件模拟SPI,优点就是GPIO口的选择非常自由,而且只要GPIO口够用,可连接的SPI设备是可以有很多的,因此在上面GPIO口的模式配置就配置为推挽输出而不是推挽复用输出,因为我们是软件模拟,不需要复用功能。当然如果各位小伙伴想用硬件SPI的话,配置GPIO口的模式就需要配置为复用。
SPI时序
开始与结束
我们先了解一下SPI是如何通信的,首先是SS线,这个是从机选择线,当它处于低电平的状态下,那么该从机属于被选中的状态,当它处于高电平的状态下,这个从机就属于不被选中。
因此我们可以认为当SS从高电平拉低到低电平,这个算是起始的一个时序。而SS从低电平拉高到高电平,就算是结束的时序。
和I2C相比,SPI的起始和结束的时序相对简单。并且SPI通信也不需要设备的地址,只需要一根SS选择线即可控制是否选择该从机,好处是简单快捷,而坏处就是当我们连接的从机过多时,主机光是连接从机选择线都得花掉不少GPIO口。
发送接收字节
发送接收字节看似是两个时序,但是在SPI中却是同一个时序,因为SPI的机制是我们发送一个字节,并且接收一个字节(那怕我们并不需要接收数据)。反过来看也可以是我们接收一个字节,并且发送一个字节(那怕这个字节是无用的数据)。
通过上图大家应该就可以明白了。我们所谓的发送接收字节实际上是交换字节,因此实际上SPI就仨时序,开始一个,结束一个,交换bit一个。
因此我们剩下就是要搞明白交换bit的时序是什么。
交换bit的时序有四种版本,分别是模式0,1,2,3,一般情况下模式0是通用的,因此我们了解一下模式0。
实际上就是在SCK上升沿的时候,移出MOSI的数据,在SCK下降沿的时候读取MISO的数据。
因此我们需要在SCK上升沿之前把需要发送的数据位放置在MOSI线上。
并且在SCK下降沿的时候马上读取MISO线上的数据位(实际上下降沿和读取应该是同时的,但是我们软件模拟没法同时,但是效果是一样的)
STM32软件模拟SPI
通过上面时序的介绍,我们就可以写出下面的软件模拟SPI的代码了,下面的代码可以直接用,只需要将对应通信线的GPIO口的宏定义改掉即可自己指定通信线使用的GPIO口。
#include "stm32f10x.h" // Device header
#define Z_SPI_SS_GPIO GPIO_Pin_0
#define Z_SPI_SCK_GPIO GPIO_Pin_1
#define Z_SPI_MOSI_GPIO GPIO_Pin_2
#define Z_SPI_MISO_GPIO GPIO_Pin_3
void Z_SPI_SetSS(uint8_t val){
if(val==0) GPIO_WriteBit(GPIOA,Z_SPI_SS_GPIO,Bit_RESET);
else GPIO_WriteBit(GPIOA,Z_SPI_SS_GPIO,Bit_SET);
}
void Z_SPI_SetSCK(uint8_t val){
if(val==0) GPIO_WriteBit(GPIOA,Z_SPI_SCK_GPIO,Bit_RESET);
else GPIO_WriteBit(GPIOA,Z_SPI_SCK_GPIO,Bit_SET);
}
void Z_SPI_SetMOSI(uint8_t val){
if(val==0) GPIO_WriteBit(GPIOA,Z_SPI_MOSI_GPIO,Bit_RESET);
else GPIO_WriteBit(GPIOA,Z_SPI_MOSI_GPIO,Bit_SET);
}
uint8_t Z_SPI_GetMISO(void){
return GPIO_ReadInputDataBit(GPIOA,Z_SPI_MISO_GPIO);
}
//SPI初始化GPIO口
void Z_Init_SPI(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//SS SCK MOSI 都是推挽输出
GPIO_InitTypeDef itd;
itd.GPIO_Mode=GPIO_Mode_Out_PP;
itd.GPIO_Pin=Z_SPI_SS_GPIO|Z_SPI_SCK_GPIO|Z_SPI_MOSI_GPIO;
itd.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&itd);
//MISO 是浮空输入或者是上拉输入
itd.GPIO_Mode=GPIO_Mode_IPU;
itd.GPIO_Pin=Z_SPI_MISO_GPIO;
GPIO_Init(GPIOA,&itd);
//SS默认是不选中,即高电平
Z_SPI_SetSS(1);
//SCK默认低电平,以便拼接后续交换字节的时序
Z_SPI_SetSCK(0);
//MOSI与MISO什么电平都无所谓.
}
//SPI开始时序
void Z_SPI_Start(void){
Z_SPI_SetSS(0);
}
//SPI结束时序
void Z_SPI_End(void){
Z_SPI_SetSS(1);
}
//交换字节时序
uint8_t Z_SPI_SwapByte(uint8_t data){
uint8_t receive=0x00;
for(uint16_t i=0;i>i));
Z_SPI_SetSCK(1);
if(Z_SPI_GetMISO()==1) receive|=(0x80>>i);
Z_SPI_SetSCK(0);
}
return receive;
}
TFT-LCD
TFT-LCD,全称Thin Film Transistor Liquid Crystal Display,即薄膜晶体管液晶显示器,是多数液晶显示器的一种,使用薄膜晶体管技术改善影象品质。这种显示器技术是一种微电子技术与液晶显示器技术巧妙结合的技术。
具体来说,TFT-LCD由一系列的像素组成,每个像素由液晶分子和薄膜晶体管构成。薄膜晶体管是一种电子开关,通过控制其通断状态来控制液晶分子的排列,从而实现像素的显示。每个像素都有一个对应的薄膜晶体管,它们分别由一个源极、栅极和漏极组成。当薄膜晶体管的栅极电压升高时,源极和漏极之间会形成一个导通通道,电流可以通过。反之,当栅极电压降低时,通道将关闭,电流无法通过。
液晶分子的排列状态会影响光的传播和偏振方向。液晶分子在电场的作用下可以呈现不同的排列方式,分别为平行排列和垂直排列。当液晶分子呈现平行排列时,光线经过液晶层时会发生偏转,无法通过偏振器,像素呈现出黑色。而当液晶分子呈现垂直排列时,光线能够通过液晶层和偏振器,像素呈现出亮色。
TFT-LCD液晶屏的优点包括显示效果良好,色彩还原度远超其他种类的显示屏,呈现给用户的画面色彩鲜艳,饱和度高,纯白、纯黑画面纯净。同时,其IPS系列可以达到178度全视角,用户无论从哪个角度观看效果都非常好。然而,由于其超薄的外形,无法实现超高亮度的需求,亮度上存在一定的限制。
TFT-LCD液晶屏的应用场景非常广泛,包括消费类电子产品(如智能手机、平板电脑、笔记本电脑、电视机等)、医疗设备领域(如超声诊断仪、心电图机等)、工业自动化领域(如数控机床、工业机器人等)以及汽车电子领域(如汽车仪表、导航设备等)。
另外,根据行业报告,TFT-LCD光罩市场规模在逐年增长,其在笔记本和平板电脑、液晶显示器、智能手机、液晶电视等领域的应用也在不断扩大。
总的来说,TFT-LCD技术因其独特的显示性能和广泛的应用场景,成为了当今显示技术领域的重要一环。如需更多关于TFT-LCD技术的信息,建议查阅相关的技术手册或行业研究报告。
上面一大段的介绍都来自文心一言,简单来说我们需要知道的就是TFT-LCD就是一显示屏。
问题就在于我们应该如何驱动。
上图为我上手的1.8寸屏幕的TFT-LCD,它用的驱动芯片是ST7735S。
其中CS线就是SS线,SDA线就是MOSI线,SCL线就是SCK线。并且我们发现它没有MISO线,而且还多了DC和RST和BLK。
首先没有MISO线是因为我们并不需要与这块屏幕进行双向通信,也就是说它不会给我们发送数据,只会接受我们给的数据,因此没有MISO线。
DC线是ST7735S用于区分接受的是数据还是命令的,当DC线处于低电平的情况下收到数据,那么这个数据就是命令。当DC线处于高电平的情况下收到数据,那么这个数据就是参数。
BLK线应该是背光控制,无需控制则接3.3V常亮即可。
RST线是给TFT复位用的。
下面机翻的有些不到位,我概括一下。
就是RST拉低,然后持续至少10us,接着把RST拉高,而后等待5~120ms即复位完成。
当我们需要初始化之前需要先复位。因此我们可以在上面复位的时序之后接初始化的代码。
一般来说厂家会提供初始化的代码,可以找TFT屏幕的卖家要,后面我会贴出来。
命令
从官方提供的手册可以看出可供我们操作的命令非常多,官方的手册是英文的,上图是我找的翻译网站去机翻出来的,这就有问题了,英文我看不懂,中文则和原版的意思有出入,英语不好的我留下了没文化的泪。
总之虽然命令多,但是我们常用的其实就那几个。
既然它是屏幕,那么最重要的就是显示,因此我们只需要懂得如何显示即可。
简单来说我们需要选中一块区域,然后往这块区域里面写入我们要显示的数据。
而我们选中一块区域需要知道这块区域的位置,而ST7725S的办法如下。
发送0x2A的命令,告诉屏幕我们现在要指定区域的列范围了,命令发送完我们需要再发送四个参数(四个字节)用来指定列的起始和开头,通过表格可以看出,第一个参数和第二个参数共同决定了开始的列,而第三个参数和第四个参数共同决定了结束的列。
但是问题在于这块屏幕的大小是128*160的,所以第一个参数和第三个参数应该为0,因为我们用不着那么高的位数。
发送0x2B命令则指定区域的行,具体的流程和指定列是一样的。
指定了行和列之后,我们再发送0x2C的命令,接着直接发送多个长度为16bit的参数去填充我们指定的区域即可。
而每个参数就是一个RGB三色像素点,那么我们应该如何通过两个字节去表示RGB的数值呢(这边需要各位小伙伴们对RGB表示颜色有些许了解)
我们来看看厂家提供的初始化代码。
看的出来指定像素的时候,初始化代码中是指定了16bit为一个像素点。
我们还得看看16个bit如何表示出RGB三种颜色的数值。
R分5bit,G分6bit,B分5bit,一共16bit。
我们知道一般RBG每个通道都需要8个bit也就是数值最多为255。而这边表示RGB的一共才16bit,因此在色彩的表现方面会稍差一些,但是也是够用的。
知道了怎么使用这块屏幕之后我们可以开始敲代码了。
首先先封装一下SPI的函数,因为ST7735S还是有着自己的使用规则。
主要需要封装的就是给ST7735S发送数据有两种不同含义,可能是命令也可能是参数,那么就要封装一下。并且因为一个像素点是16个bit,而我们上面SPI的驱动只有交换一个字节,也就是发送一个字节,因此再封装一个发送两个字节的表示RGB的16个bit的函数。
#define Z_ST7735S_RST_GPIO GPIO_Pin_2
#define Z_ST7735S_DC_GPIO GPIO_Pin_3
void Z_ST7735S_SetRST(uint8_t val){
if(val==0) GPIO_WriteBit(GPIOA,Z_ST7735S_RST_GPIO,Bit_RESET);
else GPIO_WriteBit(GPIOA,Z_ST7735S_RST_GPIO,Bit_SET);
}
void Z_ST7735S_SetDC(uint8_t val){
if(val==0) GPIO_WriteBit(GPIOA,Z_ST7735S_DC_GPIO,Bit_RESET);
else GPIO_WriteBit(GPIOA,Z_ST7735S_DC_GPIO,Bit_SET);
}
void Z_ST7735S_SendCommand(uint8_t command){
Z_SPI_Start();
Z_ST7735S_SetDC(0);
Z_SPI_SwapByte(command);
Z_SPI_End();
}
void Z_ST7735S_SendData(uint8_t data){
Z_SPI_Start();
Z_ST7735S_SetDC(1);
Z_SPI_SwapByte(data);
Z_SPI_End();
}
void Z_ST7735S_Send16bitsRGB(uint16_t rgb){
Z_ST7735S_SendData(rgb>>8);
Z_ST7735S_SendData(rgb);
}
接下来就可以开始控制我们的屏幕了。
首先需要初始化,初始化的代码厂家会提供,我们修改一下函数名为自己的就行,可以CTRL+F查找然后一键替换。
在初始化品ST7735S之前,我们还需要额外初始化一下DC和RST的GPIO口,这是SPI的初始化中没有的。而且需要先拉低RST,等待至少10us(代码中Delay了1ms),接着拉高RST,等待最多120ms,然后就可以开始发送命令和参数去初始化ST7735S了。
可以查看手册去看看初始化代码中的命令是什么意思,然后根据自己的需求去修改对应位置的参数即可。
void Z_ST7735S_Init(void){
Z_SPI_Init();
//除了上面SPI初始化的GPIO口,还需要额外初始化RST和DC
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef itd;
itd.GPIO_Mode=GPIO_Mode_Out_PP;
itd.GPIO_Pin=Z_ST7735S_DC_GPIO|Z_ST7735S_RST_GPIO;
itd.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&itd);
Z_ST7735S_SetRST(0);
Delay_ms(1);
Z_ST7735S_SetRST(1);
Delay_ms(120);
//厂家提供的固定的初始化代码
Z_ST7735S_SendCommand(0x11); //Sleep out
Delay_ms(120); //Delay 120ms
//------------------------------------ST7735S Frame Rate-----------------------------------------//
Z_ST7735S_SendCommand(0xB1);
Z_ST7735S_SendData(0x05);
Z_ST7735S_SendData(0x3C);
Z_ST7735S_SendData(0x3C);
Z_ST7735S_SendCommand(0xB2);
Z_ST7735S_SendData(0x05);
Z_ST7735S_SendData(0x3C);
Z_ST7735S_SendData(0x3C);
Z_ST7735S_SendCommand(0xB3);
Z_ST7735S_SendData(0x05);
Z_ST7735S_SendData(0x3C);
Z_ST7735S_SendData(0x3C);
Z_ST7735S_SendData(0x05);
Z_ST7735S_SendData(0x3C);
Z_ST7735S_SendData(0x3C);
//------------------------------------End ST7735S Frame Rate---------------------------------//
Z_ST7735S_SendCommand(0xB4); //Dot inversion
Z_ST7735S_SendData(0x03);
//------------------------------------ST7735S Power Sequence---------------------------------//
Z_ST7735S_SendCommand(0xC0);
Z_ST7735S_SendData(0x28);
Z_ST7735S_SendData(0x08);
Z_ST7735S_SendData(0x04);
Z_ST7735S_SendCommand(0xC1);
Z_ST7735S_SendData(0XC0);
Z_ST7735S_SendCommand(0xC2);
Z_ST7735S_SendData(0x0D);
Z_ST7735S_SendData(0x00);
Z_ST7735S_SendCommand(0xC3);
Z_ST7735S_SendData(0x8D);
Z_ST7735S_SendData(0x2A);
Z_ST7735S_SendCommand(0xC4);
Z_ST7735S_SendData(0x8D);
Z_ST7735S_SendData(0xEE);
//---------------------------------End ST7735S Power Sequence-------------------------------------//
Z_ST7735S_SendCommand(0xC5); //VCOM
Z_ST7735S_SendData(0x1A);
Z_ST7735S_SendCommand(0x36); //MX, MY, RGB mode
Z_ST7735S_SendData(0xC0);
//------------------------------------ST7735S Gamma Sequence---------------------------------//
Z_ST7735S_SendCommand(0xE0);
Z_ST7735S_SendData(0x04);
Z_ST7735S_SendData(0x22);
Z_ST7735S_SendData(0x07);
Z_ST7735S_SendData(0x0A);
Z_ST7735S_SendData(0x2E);
Z_ST7735S_SendData(0x30);
Z_ST7735S_SendData(0x25);
Z_ST7735S_SendData(0x2A);
Z_ST7735S_SendData(0x28);
Z_ST7735S_SendData(0x26);
Z_ST7735S_SendData(0x2E);
Z_ST7735S_SendData(0x3A);
Z_ST7735S_SendData(0x00);
Z_ST7735S_SendData(0x01);
Z_ST7735S_SendData(0x03);
Z_ST7735S_SendData(0x13);
Z_ST7735S_SendCommand(0xE1);
Z_ST7735S_SendData(0x04);
Z_ST7735S_SendData(0x16);
Z_ST7735S_SendData(0x06);
Z_ST7735S_SendData(0x0D);
Z_ST7735S_SendData(0x2D);
Z_ST7735S_SendData(0x26);
Z_ST7735S_SendData(0x23);
Z_ST7735S_SendData(0x27);
Z_ST7735S_SendData(0x27);
Z_ST7735S_SendData(0x25);
Z_ST7735S_SendData(0x2D);
Z_ST7735S_SendData(0x3B);
Z_ST7735S_SendData(0x00);
Z_ST7735S_SendData(0x01);
Z_ST7735S_SendData(0x04);
Z_ST7735S_SendData(0x13);
//------------------------------------End ST7735S Gamma Sequence-----------------------------//
Z_ST7735S_SendCommand(0x3A); //65k mode
Z_ST7735S_SendData(0x05);
Z_ST7735S_SendCommand(0x29); //Display on
}
接下来我们就可以让这块屏幕显示东西了。
一般情况下我们仅仅是初始化之后没有任何操作得到的结果是一块花屏。
发送了0x01的命令之后会得到一块白屏。
我这里不太确定,0x01是不是真的就是把屏幕复位成白屏,因为我英文不好,中文翻译的怪怪的,毕竟这个命令是软件复位,人家可没说软件复位是不是把屏幕复位成白屏。
而且文心一言也说软件复位这个命令可能使显示出现黑屏或是异常。
主要是我手上就一块屏幕,没法做比较,因此没法判断出白屏的原因,大家可以试一下自己的屏幕。
刷新整个屏幕
那么不用上面的命令,我们也不能顶着个花屏去显示东西吧,因此我们封装一个刷新整个屏幕的函数,也就是把整个屏幕都指定为我们的区域,然后去把这个区域(整个屏幕)塞入同一个像素值,这样就实现了刷新整个屏幕的效果。
//指定范围
void Z_ST7735S_SpecifyScope(uint8_t xs,uint8_t xe,uint8_t ys,uint8_t ye){
Z_ST7735S_SendCommand(0x2A); //指定列范围
Z_ST7735S_SendData(0x00);
Z_ST7735S_SendData(xs);
Z_ST7735S_SendData(0x00);
Z_ST7735S_SendData(xe);
Z_ST7735S_SendCommand(0x2B); //指定行范围
Z_ST7735S_SendData(0x00);
Z_ST7735S_SendData(ys);
Z_ST7735S_SendData(0x00);
Z_ST7735S_SendData(ye);
Z_ST7735S_SendCommand(0x2C); //开始内存写入
}
void Z_ST7735S_RefreshAll(uint16_t rgb){
Z_ST7735S_SpecifyScope(0,127,0,159);
for(uint16_t j=0;j
















