目录
1. 简介
SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线。同步,全双工。支持总线挂载多设备(一主多从)。
四根通信线:SCK(Serial Clock)串行时钟线;
MOSI(Master Output Slave Input)主机输出从机输入;
MISO(Master Input Slave Output)主机输入从机输出;
SS(Slave Select)从机选择(若是有多个从机,有几个从机就有几条SS线,可见硬件电路中的连接图)。
2. 硬件电路
所有SPI设备的SCK、MOSI、MISO分别连在一起;
主机另外引出多条SS控制线,分别接到各从机的SS引脚;
输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入。
移位示意图
工作原理,假如主机想要发送一个字节给从机,从机也想发送一个字节给主机,开始,当SCK处于上升沿移位寄存器最左边数据移出,例如SPI主机中移位寄存器最左边“1”,移出到MOSI引脚上,而SPI从机的移位寄存器的最左边的数据“0”,移出到MISO引脚上,当SCK处于下降沿,MOSI上的数据进入到SPI从机的移位寄存器最右边,MISO上的数据进入到SPI主机的移位寄存器最右边。往复八次经过时钟的上升沿和下降沿,即可完成相互发送一个字节数据。
当多个从机输出连在一起,如果同时开启输出,会造成冲突,解决方法是,当SS未被选中的状态,从机的MISO引脚必须关断输出,即配置为高阻态。
3. SPI时序基本单元
3.1 起始条件
SS从高电平切换到低电平
3.2 终止条件
SS从低电平切换到高电平
3.3 交换一个字节(模式0)
CPOL=0:空闲状态时,SCK为低电平
CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据
3.4 交换一个字节(模式1)
CPOL=0:空闲状态时,SCK为低电平
CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
3.5 交换一个字节(模式2)
CPOL=1:空闲状态时,SCK为高电平
CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据
3.6 交换一个字节(模式3)
CPOL=1:空闲状态时,SCK为高电平
CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
4. 代码编写
4.1 引脚初始化
void MySPI_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
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;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA4、PA5和PA7引脚初始化为推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA6引脚初始化为上拉输入
/*设置默认电平*/
MySPI_W_SS(1); //SS默认高电平
MySPI_W_SCK(0); //SCK默认低电平
}
其中,MySPI_W_SS(1); 和 MySPI_W_SCK(0);为封装函数,可以参照下一条。
4.2 引脚置高低电平封装
为了后续代码的编写方便,我们可以将,初始化的引脚进行封装。
4.2.1 SPI写SS引脚电平
此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平。
void MySPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue,设置SS引脚的电平
}
4.2.2 SPI写SCK引脚电平
此函数需要用户实现内容,当BitValue为0时,需要置SCK为低电平,当BitValue为1时,需要置SCK为高电平。
void MySPI_W_SCK(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue); //根据BitValue,设置SCK引脚的电平
}
4.2.3 SPI写MOSI引脚电平
此函数需要用户实现内容,当BitValue为0时,需要置MOSI为低电平,当BitValue非0时,需要置MOSI为高电平。
void MySPI_W_MOSI(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue); //根据BitValue,设置MOSI引脚的电平,BitValue要实现非0即1的特性
}
4.2.4 I2C读MISO引脚电平
此函数需要用户实现内容,当前MISO为低电平时,返回0,当前MISO为高电平时,返回1。
uint8_t MySPI_R_MISO(void)
{
return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6); //读取MISO电平并返回
}
也可以参考:
进行其他方法也能实现同样功能。
4.3 SPI起始
根据3.1我们可以看出SPI起始只需要将SS拉低就可以开始时序。
void MySPI_Start(void)
{
MySPI_W_SS(0); //拉低SS,开始时序
}
4.4 SPI终止
同理,根据3.2我们可以看出SPI起始只需要将SS拉高就可以结束时序。
void MySPI_Stop(void)
{
MySPI_W_SS(1); //拉高SS,终止时序
}
4.5 SPI交换传输一个字节
4.5.1 模式0
这里需要注意一下,在SPI中对于硬件SPI来说,由于使用了硬件的移位寄存器电路,所以下图中黄色部分几乎是同时发生的,但是对于软件SPI来说程序执行需要一条一条执行,有一个先后顺序,因此我们可以将这里看成一个先后执行的逻辑:
因此我们可以将其传送一位数据的流程如下,先SS下降,再移出数据,在SCK上升沿,在移入数据,在SCK下降沿,再移出数据。
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
uint8_t ByteReceive = 0x00; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
MySPI_W_MOSI(ByteSend & 0x80); //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线
MySPI_W_SCK(1); //拉高SCK,上升沿移出数据
if (MySPI_R_MISO() == 1){ByteReceive |= 0x80;} //读取MISO数据,并存储到Byte变量
//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0
MySPI_W_SCK(0); //拉低SCK,下降沿移入数据
MySPI_W_MOSI(ByteSend & 0x40); //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线
MySPI_W_SCK(1); //拉高SCK,上升沿移出数据
if (MySPI_R_MISO() == 1){ByteReceive |= 0x40;} //读取MISO数据,并存储到Byte变量
//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0
MySPI_W_SCK(0);
MySPI_W_MOSI(ByteSend & 0x20); //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线
MySPI_W_SCK(1); //拉高SCK,上升沿移出数据
if (MySPI_R_MISO() == 1){ByteReceive |= 0x20;} //读取MISO数据,并存储到Byte变量
//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0
MySPI_W_SCK(0);
MySPI_W_MOSI(ByteSend & 0x10); //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线
MySPI_W_SCK(1); //拉高SCK,上升沿移出数据
if (MySPI_R_MISO() == 1){ByteReceive |= 0x10;} //读取MISO数据,并存储到Byte变量
//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0
MySPI_W_SCK(0);
MySPI_W_MOSI(ByteSend & 0x08); //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线
MySPI_W_SCK(1); //拉高SCK,上升沿移出数据
if (MySPI_R_MISO() == 1){ByteReceive |= 0x08;} //读取MISO数据,并存储到Byte变量
//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0
MySPI_W_SCK(0);
//......一直移出到第八位
return ByteReceive; //返回接收到的一个字节数据
}
以上代码太过冗余,我们可以使用for循环来进行实现:
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
uint8_t i, ByteReceive = 0x00; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
for (i = 0; i < 8; i ++) //循环8次,依次交换每一位数据
{
MySPI_W_MOSI(ByteSend & (0x80 >> i)); //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线
MySPI_W_SCK(1); //拉高SCK,上升沿移出数据
if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);} //读取MISO数据,并存储到Byte变量
//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0
MySPI_W_SCK(0); //拉低SCK,下降沿移入数据
}
return ByteReceive; //返回接收到的一个字节数据
}
我们还可以根据“2.硬件电路”中的移位示意图中的数据进行操作,编写代码:
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
uint8_t i;
for (i = 0; i < 8; i ++) //循环8次,依次交换每一位数据
{
MySPI_W_MOSI(ByteSend & 0x80);
ByteSend <<=1;
MySPI_W_SCK(1); //拉高SCK,上升沿移出数据
if (MySPI_R_MISO() == 1){ByteReceive |= 0x01;}
MySPI_W_SCK(0); //拉低SCK,下降沿移入数据
}
return ByteReceive; //返回接收到的一个字节数据
}
这种方式相较于上一种代码效率更高,但是原始数据ByteSend会发生改变,因为这种方法是用移位数据本身进行操作的,效率跟高,但是原始数据ByteSend会在移位过程中发生改变,对于上一种方式编写的代码是还有掩码一次提取数据每一位,不会改变参数本身,两种方法皆可使用。
4.5.2 模式1
我们也可以将其传送一位数据的流程描述如下,先SS下降之后,在SCK上升沿,再移出数据,在SCK下降沿,在移入数据。
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
uint8_t i, ByteReceive = 0x00; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
for (i = 0; i < 8; i ++) //循环8次,依次交换每一位数据
{
MySPI_W_SCK(1);
MySPI_W_MOSI(ByteSend & (0x80 >> i)); //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线
MySPI_W_SCK(0);
if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);} //读取MISO数据,并存储到Byte变量
//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0
}
return ByteReceive; //返回接收到的一个字节数据
}
4.5.2 模式2
可以对比模式0,可以发现只是SCK极性相反,只需要将模式0的代码中出现SCK的地方“1”改为“0”,“0”改为“1”即可,将极性翻转一下。(注意初始化中的极性也要进行修改)
4.5.2 模式3
同理,可以对比模式1,可以发现只是SCK极性相反,只需要将模式0的代码中出现SCK的地方“1”改为“0”,“0”改为“1”即可,将极性翻转一下。(注意初始化中的极性也要进行修改)
标签:MISO,SCK,引脚,MySPI,SPI,GPIO,编写,STM32F1 From: https://blog.csdn.net/MANONGDKY/article/details/139161985