目录
STM32软件SPI通信读写W25Q64存储器模块可以看我的这篇博客:STM32通过SPI软件读写W25Q64-CSDN博客
STM32硬件SPI通信读写W25Q64存储器模块可以看我的这篇博客STM32通过SPI硬件读写W25Q64-CSDN博客
SPI通信详解
1. SPI通信介绍
SPI(Serial Peripheral Interface)和I2C一样,都是实现主控芯片和外挂芯片之间的数据交流。
I2C 与 SPI 的比较
I2C的缺点:
- 驱动能力差: I2C接口由于线路上拉电阻的电路结构,使得通信总线的驱动能力较弱。当通信线路电压较高时,电阻上的损耗就越大,限制了I2C的最大通信速度。在标准模式下,I2C的最高时钟频率仅为100kHz,相比于SPI要慢得多。
SPI的优点:
- 传输速度高: SPI在设计上更注重速度,其设计简单且灵活,能够支持更高的传输速率。
SPI是一种串行同步通信协议。它通过四根通信线(SCK、MOSI、MISO、SS)进行数据传输。
- 全双工: SPI支持全双工通信,可以同时进行数据发送和接收。
- 多设备支持: SPI支持多设备挂载(主多从),但不支持多主多从。
- 时钟线: SPI使用一根时钟线(SCK)来提供时钟信号。数据的输出和输入都在SCK的上升沿或下降沿进行,由主机提供时钟信号,数据接收由从机来完成。
SPI通信线
- SCK(Serial Clock): 提供时钟信号,由主机控制。数据传输依赖于SCK的时钟信号。
- MOSI(Master Out Slave In): 主机输出,从机输入。用于主设备向从设备发送数据。
- MISO(Master In Slave Out): 从机输出,主机输入。用于从设备向主设备发送数据。
- SS(Slave Select): 片选信号,用于选择从设备。每个从设备都有一个独立的SS线,通过拉低SS信号来选择相应的从设备进行通信。
SPI通信线别称
- SCK: SCLK, CLK, CK
- MOSI: MI, SIO, DO(Data Output)
- MISO: DI(Data Input)
- SS: NSS(Not Slave Select), CS(Chip Select)
时序和数据传输
- 无应答机制: SPI没有应答机制,这意味着主机发送的数据没有确认信号,数据传输的完整性和正确性需要其他手段保障。
- 时钟信号同步: SPI通信依赖时钟信号(SCK)进行数据同步,确保数据位在SCK的时钟沿(上升沿或下降沿)传输。
2. SPI和I2C的区别
I2C
- I2C的硬件电路和软件时序更加复杂
- I2C通信的性价比高:在消耗最低硬件资源的情况下,实现最多的功能
- I2C的弱上拉,会限制最大通信速度(标准100KHZ以下,快速400KHZ以下)
SPI
- SPI传输更快。这一般取决于芯片厂商设计时的需求(比如W25Q64存储器芯片最大速度为80MHZ)
- SPI设计的功能少,学习简单
- SPI的硬件开销大,通信线的个数比较多。并且通信时经常会出现资源浪费的情况
- SPI不会有I2C的应答机制
3. SPI的硬件电路规定
- 所有SPI设备的SCK、MOSI、MISO分别连在一起
- 主机另外引出多条SS控制线,分别接到各从机的SS引脚
- 所有设备的输出引脚配置为推挽输出 !但是,SPI协议规定,为了防止从机全都是推挽输出导致冲突。 !规定从机在未被点名时,应该将输出设置为高阻态(引脚断开)
- 所有设备的输入引脚配置为浮空或上拉输入
主机通过MOSI输出、从机通过MOSI输入
主机已通过MISO输入,从机通过MISO输出
主机(输出)想和谁通信,就把他的控制线输出为低电平。然后被对应的从机接收(输入)(同一时间只能选一个从机)
4. SPI的移位示意图
SPI在通信时有三种情况
- 边发送边接收
- 只接收,不发送
- 只发送,不接收
1.边接收边发送
SPI的主机和从机可以理解为这个框图。
-
波特率发生器
波特率发生器就是主机的时钟。波特率发生器会控制主机和从机的移位寄存器的移位操作(左移)
-
在进行边接收边发送时,比如我们写入了1 0 1 0 1 0 1 0这个数据希望发送到从机、而从机西方发送0 1 0 1 0 1 0 1这个数据,希望发送到主机。
主机和从机的移位寄存器就会根据时钟的上升沿和下降沿(上升沿发送,下降沿接收)一位一位的发送和接收数据。
在下降沿没来到之前,暂存在发送寄存器中。
在下降沿来到之后,接收器就会接收主机/从机发送来的数据,并保存在移位寄存器的最低位。
如此循环八次,就可以得到一个字节的数据。
如下图所示
2.只接收,不发送
在只接收时,接收方还是会发送的,只是发送由我们指定发送0x00或者0xFF。 并且接收方发来的数据,发送方是不会去读取的。
3.只发送,不接收
在发送时,接收方还是会发送的,只是接收由我们指定发送0x00或者0xFF。 并且接收方发来的数据,发送方是不会去读取的。
5. SPI软件设计(时序基本单元)
5.1 起始条件与终止条件的时序
- 起始条件:SS从高电平到低电平
- 终止条件:SS从低电平到高电平
5.2 主机与从机交换一个字节的时序
SPI有两个配置位:CPOL
和 CPHA
他们的不同值,对应了SPI交换一个字节的不同方式
- CPOL = 0时:空闲状态时,SCK为低电平
- CPOL = 1时:空闲状态时,SCK为高电平
- CPHA =0时:SCK第一个边沿移入数据,第二个边沿移出数据
- CPHA =1时:SCK第一个边沿移出数据,第二个边沿移入数据
- 其中CPHA表示的是时钟相位,决定第一个时钟采样移入还是第二个时钟采样移入
一、交换一个字节(模式1)CPOL = 0 CPHA = 1
- 点名:SS信号线被置为低电平,表示从机已被选中并准备接收或发送数据。
- SCK空闲状态:SCK(Serial Clock)信号线保持为低电平,这是CPOL=0模式下的空闲状态。
- 坐下:当数据交换完成后,SS信号线被置回高电平,从机不再被选中。
- 第一个边沿来临:SCK的第一个上升沿到来,主机和从机开始发送高位数据。
- 第二个边沿来临:SCK的第二个下降沿到来,主机和从机接收到数据到最低位。
- 不断循环:这个过程不断重复,直到完成一个字节的数据交换。 如果想接收多个数据,就让SS一直为0就OK
- 主机输出后的电平没有硬性要求:在数据交换结束后,SCK可以继续保持低电平也可以恢复到空闲状态。
- 但是从机恢复推挽输出:从机在未被点名前应是高阻态。
二、交换一个字节(模式0)CPOL = 0 CPHA = 0
在实际应用中。模式0的应用是最多的。
- 点名:SS信号线被置为低电平,表示从机已被选中并准备接收或发送数据。
- 在第一个边沿来临的时候就要读取数据了,所以在此之前要提前发出最高位。
- 主机和从机在第一个边沿到达时开始接收信号了, 此时开始捕获数据到自己的移位寄存器的最低位。
- 主机和从机在第二个边沿开始发送信号, 送出自己的次高位
- 如此循环……,在接收到一个字节之后。 还会有一个低电平回到SCK静态。此时如果主机不打算交流了(SS返回高电平) 此时MOSI,主机输出端 可以上拉也可以保持,然后停止点名。 而MISO,从机的输出端 要回到高阻态
- 如果想要继续发送字节,就保持SS为0,主机从机的端口也不变。再来一遍这个波形。
三、交换一个字节(模式2)CPOL = 1 CPHA = 0
只不过是把CLK的波形取反一下就可以了。其他的和模式0一样
四、交换一个字节(模式0)CPOL = 0 CPHA = 0
只不过是把CLK的波形取反一下就可以了。其他的和模式1一样
6. SPI通信举例(软硬件波形、控制W25Q64方法)
6.1. 举例:SPI通信控制W25Q64使能
SPI不像I2C那样,有效数据流第一个字节是寄存器地址、之后依次是读写的数据。 使用的是读写寄存器的模型
SPI使用的是指令码+读写数据的模型:在SPI起始之后,第一个交换给从机的数据就是指令码。在从机中,会有一个对应的指令集。指令集会对应从机的不同功能。不同的功能对应不同的字节个数,比如控制W25Q64 存储器 使能,就需要一个字节。而控制写数据时,就需要多个字节。
W25Q64存储器的使能的指令为 0x06
在这里可以参考W25Q64使能的波形图
这实际上就是主机和从机交换了0x06和0xFF的一个字节。
在从机经过对比之后。发现0x06为自己的指令集中的使能,那么他就会开启。
- 并且SPI是没有I2C的应答机制的
6.2. 举例:SPI通信控制W25Q64指定地址写
指定地址写同样的也需要写指令集。不过这次的就不同于使能只有一位字节就可以了。
- 需要先发送写指令(0x02)、
- 随后在指定地址(W25Q64有8M的存储空间,所以他的地址长度为24位 [23::0],也就是我们要发送三个字节长的地址)
- 最后再发送指定数据。(Data)
波形如下:
这也是一次次的交换数据
这段波形的意思是:我要在地址为0x12 34 56的寄存器下写入0x55
从机的输出一直为1 也就是主机一直跟从机交换0xFF
这里的和I2C的一样,每次读写都会使地址指针++、如果在一次发送之后不终止,后续发送或者读取的字节就会一次指到后边的字节中
6.3. 举例:SPI通信控制W25Q64指定地址读
指定地址读
- 向SS指定的设备
- 发送读指令(0x03)
- 随后在指定地址(Address[23:0])下,读取从机数据(Data)
这个波形的意思是:
从前4个字节一直都是0xFF.第五个字节开始,从机开始发送字节。此时主机的输出为0xFF。从机开始发送这个寄存器中的值,从机的输出就不再是0xFF。而主机交换给从机的值是0XFF
这里的和I2C的一样,每次读写都会使地址指针++、如果在一次发送之后不终止,后续发送或者读取的字节就会一次指到后边的字节中
-
并且还可以看出,由于从机回复 是硬件执行。所以从机回复的高低电平是紧贴着下降沿的。
-
而软件,在下降沿后 主机的输出并没有紧贴着,稍有延迟。 这就是软件模拟的一些延迟。不过因为有同步时钟,这些都问题不大
-
并且。在W25Q64发送数据时,他的数据是在上一个波形的下降沿提前发送(移出)的。 因为这是SPI的 模式0
7.SPI基本时序编写
7.1编写时需要注意的点
SPI的基本时序有三个
- 起始
- 结束
- 交换字节
起始和结束只需要控制SS片选信号的高低电平即可
交换字节分为两种方法
-
掩码依次提取法
掩码提取其实就是通过 & 操作来提取出一个字节中的某一位
-
移位模型法
移位模型法正是SPI通信时用到的方法。这个方法更加高效
7.2程序文件简要说明:
- MySPI:为了防止与stm32函数重名,所以添加了My前缀。 主要是为了完成SPI的三个基本时序
- MySPI.h:函数声明
MySPI.c
#include "stm32f10x.h" // Device header
/*所用引脚列表*/
#define RCCPeriph RCC_APB2Periph_GPIOA
#define SCK_Port GPIOA
#define SCK_Pin GPIO_Pin_6
#define SS_Port GPIOA
#define SS_Pin GPIO_Pin_5
#define MOSI_Port GPIOA
#define MOSI_Pin GPIO_Pin_4
#define MISO_Port GPIOA
#define MISO_Pin GPIO_Pin_3
/**
* 函 数:写片选信号SS
* 参 数:BitValue:输入1片选信号SS为高电平
* 返 回 值:无
* 注意事项:无
*/
void MySPI_W_SS (uint8_t BitValue)
{
GPIO_WriteBit(SS_Port,SS_Pin,(BitAction)BitValue);
}
/**
* 函 数:写时钟信号SCK
* 参 数:BitValue:输入1时钟信号SCK为高电平
* 返 回 值:无
* 注意事项:无
*/
void MySPI_W_SCK (uint8_t BitValue)
{
GPIO_WriteBit(SCK_Port,SCK_Pin,(BitAction)BitValue);
}
/**
* 函 数:写 主机输出,从机输入信号MOSI
* 参 数:BitValue:输入1时主机输出高电平
* 返 回 值:无
* 注意事项:无
*/
void MySPI_W_MOSI (uint8_t BitValue)
{
GPIO_WriteBit(MOSI_Port,MOSI_Pin,(BitAction)BitValue);
}
/**
* 函 数:读 主机输入,从机输出信号MISO
* 参 数:无
* 返 回 值:BitValue
* 注意事项:无
*/
uint8_t MySPI_R_MISO (void)
{
return GPIO_ReadInputDataBit(MISO_Port,MISO_Pin);
}
/**
* 函 数:MySPI初始化
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void MySPI_Init(void)
{
/*配置时钟与引脚*/
RCC_APB2PeriphClockCmd(RCCPeriph,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = SCK_Pin | SS_Pin | MOSI_Pin;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(MOSI_Port,&GPIO_InitStructure); //时钟、片选、MOSI都是推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = MISO_Pin;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(MISO_Port,&GPIO_InitStructure); //MISO 为上拉输入
MySPI_W_SS(1); //片选默认为高
MySPI_W_SCK(0); //时钟默认为低
}
/*******************/
/*SPI的三个时序单元*/
/*******************/
/**
* 函 数:起始信号
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void MySPI_Start(void)
{
MySPI_W_SS(0);
}
/**
* 函 数:终止条件
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void MySPI_Stop(void)
{
MySPI_W_SS(1);
}
/**
* 函 数:交换一个字节(模式0)(方法1)
* 参 数:SendByte 待发送的字节
* 返 回 值:ReceiveByte 接收到的字节
* 注意事项:这是使用掩码依次提取数据中的每一位保存或发送
好处是不会改变传入参数本身,但是效率不高
如果要改为模式1,则先上升沿再发送。先下降沿再接收(2、3则直接改时钟极性就ok了)
*/
//uint8_t MySPI_WarpByte(uint8_t SendByte)
//{
// uint8_t ReceiveByte = 0x00;
//
// uint8_t i = 0;
// for(i = 0; i < 8; i++)
// {
// MySPI_W_MOSI(SendByte & (0x80 >> i)); //发送第一个bit
// MySPI_W_SCK(1);//第上升沿来临
// if (MySPI_R_MISO() == 1)
// {
// ReceiveByte |= (0x80 >> i); //按照从高往低接收数据
// }
// MySPI_W_SCK(0); //下降沿来临
// }
// return ReceiveByte;
//}
/**
* 函 数:交换一个字节(模式0)(方法2)
* 参 数:SendByte 待发送的字节
* 返 回 值:ReceiveByte 接收到的字节
* 注意事项:这是使用了移位模型的方式。效率更快
如果要改为模式1,则先上升沿再发送。先下降沿再接收(2、3则直接改时钟极性就ok了)
*/
uint8_t MySPI_WarpByte(uint8_t SendByte)
{
uint8_t i = 0;
for(i = 0; i < 8; i++)
{
MySPI_W_MOSI(SendByte & 0x80); //发送第一个bit
SendByte <<= 1; //发送数据左移一位
MySPI_W_SCK(1); //第上升沿来临
if (MySPI_R_MISO() == 1)
{
SendByte |= 0x01; //保存收到的数据到发送寄存器的最低位
}
MySPI_W_SCK(0); //下降沿来临
}
return SendByte;
}
MySPI.h
#ifndef __MYSPI_H
#define __MYSPI_H
//初始化
void MySPI_Init(void);
//起始
void MySPI_Start(void);
//终止
void MySPI_Stop(void);
//交换
uint8_t MySPI_WarpByte(uint8_t SendByte);
#endif