MCU通信协议(一):SPI协议
SPI概述
SPI(Serial Peripheral Interface,串行外设接口)协议是一种全双工通信协议,SPI最主要的特点是:
一主多从:即一个主设备(主机)同时和多个串行设备(从机)通信;
同步全双工:具有时钟线作为同步信号,主机和从机需要共地,如果从机没有独立供电还需要主机提供供电口;能够同时发送和接收数据;
简单且高速:SPI的时序简单,因为线多端口多没有电平瓶颈(每新加一个从机主机就要增加一个SS端口进行片选,例如三个从机就要三个主机SS端口),主机输出配置成推挽输出,输入(MISO)配置成浮空或者上拉输入,电平驱动能力强;通信速率极高(几十Mbps);
四根通信线,接口开销大:SCK(Serial Clock)、MOSI、MISO、SS(Slave Select),其中MOSI、MISO其中的M代表主机Master,S代表从机Slave,分布代表主机发从机收,主机收从机发,MISO就接在从机的MISO上,MOSI就接在从机的MOSI上(一般命名上,从机作为没有主控的设备,MISO会命名成输出端,如DO、SDO等,MOSI会命名成DI、SDI等,片选则为CS);
SPI应用的外设主要有:TFT SPI屏幕、SD存储卡、2.4G无线通信芯片;
SPI通信的实现
SPI通信的核心实现是移位寄存器,移位寄存器按照SCK时钟进行移位,实现了主机和从机的数据交换;
从图中来看,现在主机通过某个SS端输出低电平选中了某个从机,现在双方要实现一个字节数据的交换:主机通过波特率发生器产生SCK时钟,在时钟的上升沿,主从机的移位寄存器向左移位,也即主机的1进入MOSI,从机的0进入MISO,在SCK时钟的下降沿,移位寄存器对MOSI、MISO进行电平采样,并且填充移位寄存器的最后一位,这样主机和从机的数据就实现了一个位的互换,如此循环八次,就实现了一个字节数据的交换。
而有时候我们希望只进行单工通信,只希望接收从机数据时,主机还是会在移动寄存器设置垃圾数据,例如0xFF或者0x00,交换从机的数据;而只希望发送主机数据时,那么主机端不要读取返回的数据即可,可见单工通信仍然采用双工的通信方式,某种程度上的确带来了资源的浪费。
另一个值得注意的是,SPI并没有规定SCK起始时必须是高电平还是低电平,也没有规定一定需要上升沿移位,下降沿采样,SPI给出了两个位进行配置:
CPOL(Clock Polarity,时钟极性):0代表初始化为低电平,1代表初始化为高电平;
CPHA(Clock Phase,时钟相位):0代表第一个边沿先采样第二个边沿移入数据,1代表第一个边沿先移入数据第二个边沿采样;这里的边沿指的是跳变沿。
因此结合起来一共有四个可能(CPOL|CPHA定义了Mode0-Mode3),不同的芯片采用的策略不同,具体要参考数据手册决定交换一个字节数据的写法。值得注意的是,如果CPHA=0,那么在SCK第一个边沿会移入数据,为了防止直接覆盖移位寄存器最后一位,移位寄存器首先会在SS的下降沿就移位一次(一般认为这使得移位时间提前了半个周期);
SPI通信时序
有了上面的认识,读写时序一笔带过即可。读写时序是基本一致的:
- 主机SSx输出低电平,选中某个从机;
- SCK产生时钟,按照模式0~模式3进行不同的移位、采用操作实现一个字节的数据交换,这个字节的指令定义了是读还是写设备,具体参照外设的数据手册;
- 如果单字节指令就完成数据传输,SS重新置回高电平,完成数据传输;
- 如果单字节指令后面还有指令(例如读写指令后面+地址+数据),SPI没有应答机制,也即SS不变化,继续按照SCK时钟传输指令,直到最后才将SS置回高电平;
为了防止冲突,当从机SS为高电平时,不会通过MISO向主机发数据,此时从机会输出高阻态。
SPI读写TFT触摸屏
纯手撕代码,从零写SPI协议,并且完成TFT触摸屏的stm32驱动模块;采用的是软件SPI的写法,因为多硬件控制、尤其是esp32这些少引脚设备,硬件SPI移植可能依赖库函数和引脚数量。
引脚配置
TFT SPI屏幕显示驱动ILI9341,触摸驱动Xpt2046。
stm32f103c8t6 | TFT SPI屏幕 |
---|---|
5V/3.3V | VCC |
GND | GND |
PB11 | CS |
PB12 | RESET |
PB10 | DC |
PB15 | SDI(MOSI) |
PB13 | SCK |
PB9 | LED |
PB14 | SDO(MISO) |
引脚初始化代码
相比于传统SPI接口,这里多出来了RESET、DC、LED三个接口。从ILI9341数据手册可以简单了解,RESET是低电平复位信号,复位在数字电路设计是很必要的信号,LED是背光信号,可以接3.3V常亮,也可以由引脚控制;DC是一个特殊信号,它是ILI9341区别命令和数据的一个信号,当它是高电平1时,代表读写的是数据参数,当它是低电平0时,代表读写的是命令参数,这在后面读写函数会用到,ILI9341的Command记录了LCD的设置命令。
因此这里除了MOSI配置成上拉输入,其他六个端口配置成推挽输出即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14void TFT_GPIOInit(void){
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB ,ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9| GPIO_Pin_10| GPIO_Pin_11 \
| GPIO_Pin_12|GPIO_Pin_13|GPIO_Pin_15;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
复位
复位是防止垃圾数据,统一时序电平,和FPGA类似,拉低RESET一会就好了,例程是100ms:
1
2
3
4
5
6
7
8
9
10
11
12void LCD_RESET(void)
{
LCD_RST_CLR;
delay_ms(100);
LCD_RST_SET;
delay_ms(50);
}
其中:
这里有必要了解一下寄存器知识,以下三个寄存器都能够对初始化的端口进行配置,使其输出低电平或者高电平;
- BSRR寄存器(端口位设置清除寄存器):32位寄存器,高16位写1为低电平,低16位写1是高电平;写0,无动作;只写
- BRR寄存器(端口位清除寄存器):32位寄存器,仅低16位有效,写1为低电平,写0无动作;只写
- ODR寄存器:32位寄存器,仅低16位有效,写1为高电平,写0为低电平;可读可写寄存器;
其中ODR必须一次设置16位,因此对单一端口配置经常使用BSRR和BRR寄存器,上面使用1左移12位,分别代表将引脚PB12置高电平、低电平。
- IDR寄存器:低十六位有效,读取引脚电平(后面用到)
SPI写数据
软件SPI,ILI9341采用的是模式0(兼容模式3),因此初始化时,先移位向MOSI放数据,再产生SCK时钟的上升沿,再拉回下降沿,如此循环8次就完成一个字节发送:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25uint8_t SwapData(uint8_t SendData){
uint8_t i;
for(i=0;i<8;i++){ //一个字节
if(SendData&0x80) //取最高位
LCD_MOSI_SET; //为1 MOSI也发送1
else
LCD_MOSI_CLR; //否则发0
SendData<<=1; //移位
LCD_SCK_SET; //创造SCK上升沿
if(LCD_MISO_ReadStatus) //读取MISO
SendData|=0x01; //放入主机的最后一位
LCD_SCK_CLR; //拉低SCK
}
return SendData; //最后主机的字节就交换成从机字节,虽然TFT显示没有使用,但是对称性~
}
//其中宏定义:
SPI向TFT发送字节数据
这里字节数据分成8bits的命令(寄存器地址)还是8bits的数据,区别在于DC是拉低还是拉高:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void LCD_SendCMD(uint8_t cmdbyte){
LCD_CS_CLR; //拉低SS片选
LCD_DC_CLR; //发命令:拉低DC
SwapData(cmdbyte);
LCD_CS_SET; //拉高片选
}
void LCD_SendData(uint8_t databyte){
LCD_CS_CLR; //拉低SS片选
LCD_DC_SET; //发数据:拉高DC
SwapData(databyte);
LCD_CS_SET; //拉高片选
}
//宏解析:1
2
3
4
5
6
7
8
9
10
11
12
13
14void LCD_SendData_16bits(uint16_t databyte_16bits){
LCD_CS_CLR; //拉低SS片选
LCD_DC_SET; //发数据:拉高DC
SwapData(databyte_16bits);
databyte_16bits>>=8;
SwapData(databyte_16bits);
LCD_CS_SET; //拉高片选
}
void LCD_SendData_ToAddr(uint8_t LCD_RegAddr, uint8_t LCD_RegValue)
{
LCD_SendCMD(LCD_RegAddr);
LCD_SendData(LCD_RegValue);
}(开抄,使其工作。