spi的概念
由于i2C开漏外加上拉电阻的电路结构,使得通信线高电平的驱动能力比较弱。这就会号致,通信线由低电平变到高电平的时候,这个上升沿耗时比较长,这会限制i2C的最大通信速度。所以,i2C的标准模式,只有100KHz的时钟频率,i2c的快速模式也只有400kHZ的时钟频率,虽然之后iic通过改进电路的方式设计出了高速模式,可以达到3.4MHZ但是高速模式普及程度不是很高,所以一般默认认为iic的时钟频率是400kHZ ,所以一般来说iic的传输速率是比spi慢的。spi是一般没有指定你可以配多少多少传输速度,都是你要用的这个从机的厂商设计需求为基准,比如说,我们这个W25Q64存储器芯片,手册里写的SPI时钟频率,最大可达80MHZ。
四个引脚的作用
SCK:数据位的输出和输入,都是在SCK的上升沿或下降沿进行的。并且同步时序中途慢点快点或者暂停一会儿都是没问题的,总之有时钟就叫同步时序。
然后就是spi比较豪横的地方就是它是全双工的就是发送和接受各用一根线,两者互不影响。iic就是一根线兼任发送和接收是半双工,同理串口也是全双工。所以这里,MOSI和MISO,就是分别用于发送和接收的两条线路。
mosi:主机向从机发送数据的线路,mo主机发送,si从机接受。
miso:下面这条MISO,就是主机从从机接收数据的线路。mi主机接受,si从机发送。
SS:从机选择线,但是是每个从机一根线(所以spi的引脚可能有一堆ss,会占用很多硬件资源)但不用像iic一样还需要寻址,找到那根连从机的线给低电平就是寻址了
spi硬件电路的设计
需要注意的是:
1.spi主机一般有主控制器担任,如stm32f103c8t6
2.每一个从机主机都会有一条ss线来决定与哪个从机通信
3.spi所有通信线都是单端信号,它们的高低电平都是相对GND的电压差。所以,单端信号,所有的设备还需要共地。(所以主从机要共地)一般单端信号是区别于差分信号的。差分信号不需要都共地,而单端信号则是必须共地,具体原因见下面大佬的文章。若电路板的面积非常紧张,单端信号可以只有一根信号线,地线走地平面,而差分信号一定要走两根等长、等宽、紧密靠近、且在同一层面的线。这样的情况常常发生在芯片的管脚间距很小,以至于只能穿过一根走线的情况下。
https://blog.csdn.net/yangxueyangxue/article/details/106837172https://blog.csdn.net/yangxueyangxue/article/details/1068371724.从机对应的ss为低电平的时候从机就会响应,当传输速度完成就会将ss置为高电平。
5.由于主机的输出配置的是推挽输出所以能输出强的高低电平有很强的驱动能力,能使spi引脚的下降沿和上升沿都非常迅速,所以解决了iic的拉高慢(上升沿慢)的问题。这么一看之前有人在单从机iic上用推挽输出也可能是为了让iic引脚对高电平的输出能力强一点然后传输速度快一点。所以一般spi信号都能轻松达到MHZ的速度级别。
6.在某些从机的ss未被配置为低电平(未被选中的情况下),由于多个从机的输出连在了一起,所以同时开启输出可能会造成冲突,所以从机的MISO引脚必须关断输出,从机配置这个引脚为高阻态。
移位示意图
主机的数据高位先行通过主机的移位寄存器左移到移位寄存器的低位。
现在来介绍一下这个环节具体的实现步骤:在波特率发生器时钟的上升沿,所有移位寄存器向左移动一位,移出去的位放到引脚上。在波特率发生器时钟的下降沿,引脚上的位采样输入到移位寄存器的最低位。从这张图一看应该是这样就是在波特率发生器时钟的上升沿,主机的移位寄存器最最高位的1向左移动一位到了mosi线上,从机移位寄存器的最高位的0左移到了MISO线上。在时钟的下降沿这个1进了从机移位寄存器的最低位,这个0进了主机移位寄存器的最低位,然后如此循环八次后就实现了,主机和从机一个字节的数据交换,等于就是一个置换反应,spi的原理就是不管是接收还是发送都是两个移位寄存器做一个交换,如果主机只想接收,就给从机随便置换一个字节的数据就行,这个数据从机一般也不会去理他。如果主机只想发送,就不处理从机发送过来的数据就 行。一般在只接受的时候,我们会统一发送0xff或0x00去和从机发送数据。
spi时序基本单元
spi软件引脚的初始化
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默认低电平
}
这里主机的输出引脚选择推挽输出,就是ss,mosi,clk这三个引脚,目的是为了防止出现IIc开漏输出导致上升沿影响速度的问题。而输入miso则采用浮空或者上拉输入(一般是考虑上拉,AI说的:MISO引脚可以浮空输入,但这可能导致不稳定或噪声干扰。使用上拉电阻是为了确保在主设备没有主动驱动MISO时,信号保持在高电平,从而提高可靠性。根据具体应用和系统设计,选择合适的配置以确保稳定性是关键。)
/*设置默认电平*/
MySPI_W_SS(1); //SS默认高电平
MySPI_W_SCK(0); //SCK默认低电平
同时这里ss需要默认置高电平,不然就是spi的开始阶段了。然后CLK就是看模式几来选择,这里我们用的是模式零所以直接选择默认低电平。
spi的ss,clk,mosi引脚的高低电平输出封装
void MySPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue,设置SS引脚的电平
Delay_ms(10);//当从机设备手册需要延时,就需要在这四个函数里加延时
}
void MySPI_W_SCK(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue); //根据BitValue,设置SCK引脚的电平
Delay_ms(10);//当从机设备手册需要延时,就需要在这四个函数里加延时
}
void MySPI_W_MOSI(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue); //根据BitValue,设置MOSI引脚的电平,BitValue要实现非0即1的特性
Delay_ms(10);//当从机设备手册需要延时,就需要在这四个函数里加延时
}
spi的miso输入引脚的高低电平读取
uint8_t MySPI_R_MISO(void)
{
Delay_ms(10);//当从机设备手册需要延时,就需要在这个函数里加延时
return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6); //读取MISO电平并返回
}
spi的起始和终止
void MySPI_Start(void)
{
MySPI_W_SS(0); //拉低SS,开始时序
}
void MySPI_Stop(void)
{
MySPI_W_SS(1); //拉高SS,终止时序
}
由于ss是低电平有效的,所以这个起始条件可以理解为ss变为低电平选中了某个从机。同理变成高电平就是结束了从机的选中状态。所以在通信的整个时序中ss一直要保持为低电平,一旦变成高电平就意味着通信结束,可以通过这个去控制spi的启停。SS拉低,通知从机的DO线从高阻变成输出。
这个代码里面是因为一开始ss默认是高电平所以有下降沿就是开始,而终止是由于spi在运行中不需要去操作ss所以他一直是低电平所以这里拉高就直接相当于上升沿终止了。
模式零
这里的
CPOL:时钟极性
CPHA:时钟相位
这里的空闲状态是指ss为高电平的时候
模式零:需要注意的一点就是他的mosi和miso第一个变化,不是在sck的第一个边沿上的。而是在ss的下降沿就开始变化数据(即移出数据),然后等sck的第一个边沿(一般是上升沿)的时候就直接移入数据,所以模式零会提前半个周期的时间,这样一改就和iic差不多sck低电平的时候改变数据线的值,sck高电平的时候数据线上的值,就不改变,然后读取数据线上的值。所以模式零在实际应用中基本上是用的最多。
下面是模式零交换一个字节的代码
模式1
模式2
模式3
spi的发送指令
一般在SPI中,通常采用的是指令码加读写数据的模型。在spi起始后会先发送给从机一个指令码,在从机中对应的会有一个指令集,当我们需要发送什么指令时,就可以在起始后第一个字节,发送指令集里面的数据,这样就能指导从机完成相应的功能了。
这里面这段时序的功能就是主机的移位寄存器用0x06换来了从机移位寄存器的0xff,到时候可以直接根据这个判断数据有没有发送完成。