1、SPI简介
1.1 什么是SPI
SPI,即Serial Peripheral Interface,串行外设接口。SPI是一种高速的、全双工、同步的串行通信总线;SPI采用主从方式工作,一般有一个主设备和一个或多个从设备;SPI需要至少4根线,分别是MISO(主设备输入,从设备输出)、MOSI(主设备输出,从设备输入)、SCK(时钟)、SS(从机选择)。
相较于IIC而言,SPI的传输速度更快,SPI协议并没有严格规定最大传输速度,这个最大传输速度一般取决于芯片厂商的设计需求;而IIC由于上拉电阻的原因,电平由低转高时耗时更多,一般速度最快在400KHz。
所有设备的SCK、MOSI、MISO分别连接在一起;主机另外引出多条SS控制线,分别连接各从机的SS引脚;输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入。
1.2 SPI外设
TM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担。
可配置8位/16位数据帧、高位先行/低位先行,一般都是8位数据帧、高位先行
时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)
支持多主机模型、主或从操作
可精简为半双工/单工通信
支持DMA
兼容I2S协议(音频传输协议)
STM32F103C8T6 硬件SPI资源:SPI1、SPI2
1.3 极性和相位
SPI总线有四种不同的工作模式,取决于极性(CPOL)和相位(CPHL)这两个因素。
CPOL表示CLK空闲时的状态:CPOL=0,空闲时SCK为低电平;反之则为高电平。
CPHL表示采样时刻:CPHL=0,每个周期的第一个时钟沿采样;CPHL=1,每个周期的第二个时钟沿采样。
2、SPI结构图
以下结构图来自STM32F103xxx
这里发送缓冲区就是TDR,接收缓冲区就是RDR,和串口那里一样,TDR和RDR占用同一个地址,统一叫做DR。如果我们需要连续发送一批数据,第一个数据写入到TDR,当移位寄存器没有数据移位时,TDR的数据会立刻转入移位寄存器,开始移位,这个转入时刻,会置状态寄存器的TXE为1,表示发送寄存器空,当我们检查TXE置1后,紧跟着,下一个数据,就可以提前写入到TDR里候着了,一旦上一个数据发完,下一个数据就可以立刻跟进,实现不间断的连续传输。然后移位寄存器这里,一旦有数据过来了,它就会自动产生时钟,将数据移出去,在数据移出的过程中,MISO的数据也会移入,一旦数据移出完成,数据移入也就完成了,这时移入的数据就会整体的从移位寄存器转入到接收缓冲区RDR,这个时刻会置状态寄存器的RXNE为1,表示接收寄存器非空,当我们检查RXNE置1后,就要尽快把数据从RDR读出来,在下一个数据到来之前,读出RDR,就可以实现连续接收。
基本结构
SPI移位示意图
从图中可以看出,SPI的数据收发,都是基于字节交换这个基本单元来进行的。当主机需要发送一个字节,并且同时需要接收一个字节时,就可以执行一下字节交换的时序,这要主机要发送的数据就跑到了从机,主机要从从机接收的数据,就跑到了主机,这就完成了发送同时接收的目的。
3、SPI时序
3.1 基本时序单元
起始条件:SS从高电平切换到低电平
终止条件:SS从低电平切换到高电平
SS低电平选中,高电平代表未选中,那么在低电平期间就代表正在通信,下降沿是通信的开始,上升沿是通信的结束。
交换一个字节(模式0):
CPOL=0:空闲状态时,SCK为低电平
CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据
交换一个字节(模式1):
CPOL=0:空闲状态时,SCK为低电平
CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
交换一个字节(模式2):
CPOL=1:空闲状态时,SCK为高电平
CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据
交换一个字节(模式3):
CPOL=1:空闲状态时,SCK为高电平
CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
3.2 SPI时序
发送指令:向SS指定的设备,发送指令(0x06)
指定地址写:向SS指定的设备,发送写指令(0x02),随后在指定地址(Address[23:0])下,写入指定数据(Data)
指定地址读:向SS指定的设备,发送读指令(0x03),随后在指定地址(Address[23:0])下,读取从机数据(Data)
4、示例代码
4.1 软件实现SPI
#include "stm32f10x.h" // Device header
//对SPI四个引脚的封装
//从机选择,写SS的引脚
void MySPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
void MySPI_W_SCK(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}
void MySPI_W_MOSI(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}
uint8_t MySPI_R_MISO(void)
{
return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}
void MySPI_Init(void)
{
//打开时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置端口
//先定义一个结构体变量
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure);
//引脚初始化之后的默认电平
MySPI_W_SS(1); //默认使用高电平,不选择从机
MySPI_W_SCK(0); //计划使用SPI模式0,所以默认是低电平
}
//写SPI的三个基本时序单元
//起始信号
void MySPI_Start(void)
{
MySPI_W_SS(0); //根据时序图将SS置低电平
}
//终止信号
void MySPI_Stop(void)
{
MySPI_W_SS(1); //根据时序图将SS置高电平
}
//交换一个字节
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
//定义一个字节,用于接收
uint8_t i, ByteReceive = 0x00;
for (i = 0; i < 8; i++)
{
//在SS下降沿之后开始交换字节,先有下降沿再有数据移出的动作
MySPI_W_MOSI(ByteSend & (0x80 >> i)); //这里相当于通过掩码的模式来获取想要的位
MySPI_W_SCK(1); //写SCK为1,产生上升沿,上升沿时,从机会自动把MOSI的数据读走,主机的任务就是把从机刚才放到MISO的数据位读进来
if (MySPI_R_MISO() == 1)
{
ByteReceive |= (0x80 >> i); //这样就把最高位存在ByteReceive中了
}
MySPI_W_SCK(0); //写SCK为0,产生下降沿,下降沿时,主机和从机移出下一位
//该for循环中的内容还可以进行优化,用掩码的方式提取,好处是不会改变ByteSend本身,后面有需要还可以使用
//用移位数据本身来操作,好处是提高了效率,但是ByteSend这个数据在移位过程中改变了
}
return ByteReceive;
}
4.2 硬件SPI读写
#include "stm32f10x.h" // Device header
//对SPI四个引脚的封装
//从机选择,写SS的引脚
//这里还是使用软件模拟
void MySPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
//以上代码替换成SPI外设的初始化
//第一步,开启时钟和GPIO的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
//第二步,初始化GPIO口,其中SCK和MOSI是由硬件外设控制的输出信号,所以配置为复用推挽输出,MISo是硬件外设的输入信号,设置为上拉拉输入,
//因为输入设备可以有多个,所以不存在复用输入这个东西,SS是软件控制的输出信号,所以配置为通用推挽输出
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure);
//第三步,配置SPI外设
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //选择SPI的模式,决定当前设备是SPI的主机还是从机
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //这里选择的是双线全双工
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //这里选择的是8位数据位
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //这里选择的是高位先行
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; //波特率预分频器,这里SCK的时钟频率就是72MHz / 128
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //时钟极性,这里选择的是模式0,空闲时低电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //时钟相位,这里选择第一个边沿采样,这个和SPI的四种模式有关
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //这里选择软件NSS
SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC校验的多项式,这里填多少都可以,反正我们不用
SPI_Init(SPI1, &SPI_InitStructure);
//第四步,开关控制,调用SPI_Cmd,给SPI使能即可
SPI_Cmd(SPI1, ENABLE);
//开启spi之后,还要调用一下MySPI_W_SS,默认给SS输出高电平,默认是不选中从机的
MySPI_W_SS(1);
}
//写SPI的三个基本时序单元
//起始信号
void MySPI_Start(void)
{
MySPI_W_SS(0); //根据时序图将SS置低电平
}
//终止信号
void MySPI_Stop(void)
{
MySPI_W_SS(1); //根据时序图将SS置高电平
}
//交换一个字节
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
//当调用这个交换字节的函数时,硬件的SPI外设就要自动控制SCK、MOSI、MISO这三个引脚来生成时序了
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET); //等待TXE的标志位为1
//写入数据至SPD的DR,就是TDR,要发送的数据
//ByteSend传入到TDR中,之后ByteSend会自动转入移位寄存器,一旦移位寄存器有数据了,就会自动产生时序波形
SPI_I2S_SendData(SPI1, ByteSend);
//发送和接收是同步的,到接收完成的时候也就代表发送移位完成了,接收移位完成时,会收到一个字节数据,这时会置标志位RXNE
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET); //等待RXNE的标志位为1,表示收到一个字节,同时也表示发送时序产生完成了
//读取DR,从RDR中,把交换接收的数据读出来
return SPI_I2S_ReceiveData(SPI1);
}
//注意事项1:这里的硬件SPI,必须是发送,同时接收,要想接收必须得先发送,因为只有你给TDR写数据,才会触发时序的生成
//如果不发送只接收,那时序是不会动的
//注意事项2:TXE和RXNE标志位,在写入SPI_DR时,TXE标志被清除,在读SPI数据寄存器可以清除RXNE标志位,所以按照上面程序的步骤就不用手动清除标志位了