本文目录
SPI(Serial Peripheral Interface)是一种串行通信协议,通常用于连接微控制器和外部设备,如传感器、存储器、显示屏等。它是一种全双工的通信协议,意味着数据可以在同时的时候在两个方向上传输。
一、IIC、UART、SPI的比较
通信协议 | UART | IIC | SPI |
---|---|---|---|
通信特征 | 异步串行全双工 | 同步串行半双工 | 同步串行全双工 |
接口 | TX、RX | SCL、SDA | MOSI、MISO、SCL、CS/NSS |
速度 | 多种波特率 | 100Khz、400Khz、3.4Mhz | 由时钟频率与实际功能决定 |
数据帧格式 | 起始位+数据位+校验位+停止位 | 起始条件+位传输+应答+停止条件 | 四种模式:MODE0~MODE3 |
主从设备通信 | 没有主从 | 有主从 | 有主从 |
总线结构 | 一对一 | 一对多 | 一对多 |
二、SPI通讯特点
- SPI是一种高速,全双工,同步串行总线。
- SPI有主从俩种模式,通常由一个主设备和一个或者多个从设备组从。SPI不支持多主机。
- SPI通们至少需委四根线,分别是 MISO(主设备数据输入,从设备输出),MOSI(王设备数据输出从设备输入),SCLK(时钟信号),CS/SS(片选信号)。
在通信过程中,主机只能选择一个从机进行通信。将进行通信的从机A的CS/NSS(片选)引脚拉低(低电平),其他从机的CS/NSS(片选)引脚全部拉高(高电平),即可进行与从机A进行通信。
三、通信四根线的作用
- SCK (Serial Clock): 这是时钟信号,它由主设备产生,用于同步数据传输速度。通常SPI设备在上升沿或下降沿读取数据。
- MISO (Master In Slave Out): 这是从设备向主设备传输数据的线路。
- MOSI (Master Out Slave In): 这是主设备向从设备传输数据的线路。
- CS/SS (Chip Select/Slave Select):从设备选择信号线,常称为片选信号线,也称为 NSS。每个从设备都有独立的这一条 NSS 信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。I2C 协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而 SPI 协议中没有设备地址,它使用 NSS 信号线来寻址,当主机要选择从设备时,把该从设备的 NSS 信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行 SPI 通讯。所以SPI 通讯以 NSS 线置低电平为开始信号,以 NSS 线被拉高作为结束信号。
- 主机SPI引脚模式配置:
把SCK/MOSI/MISO引脚初始化成复用推挽模式。 而CS(NSS)引脚由于使用软件控制,我们把它配置为普通的推挽输出模式。
四、SPI硬件接线
1. 常规模式接线
2. 菊花链模式
五、SPI通讯原理
所有通信(读/写)均有主设备通过控制时钟线产生跳变沿发起。
- 由主机拉低对应的CS,找到从机(选中从机通信)。
- 主机控制时钟线产生跳变沿(上升沿、下降沿),进行数据读/写。
- 主机拉高片选(完成通信,断开从机)。
六、SPI工作原理
(1)主机和从机都有一个串行移位寄存器,主机通过向它的SPI串行寄存器写入一个字节来发起一次传输。
(2)串行移位寄存器通过MOSI信号线将字节传送给从机,从机也将自己的串行移位寄存器中的内容通过MISO信号线返回给主机。这样,两个移位寄存器中的内容就被交换。
(3)外设的写操作和读操作是同步完成的。
主机通过 MOSI 线发送 1bit 效据,从机通过该线读取这 1bt 数据;从机通过 MIS0 线发送 1bit 数据,主机通过该线读取这 1bt 数据。当寄存器中的内容全部移出时,相当于完成了两个寄存器内容的交换。
如果主设备要给从设备传输数据,主设备只需要忽略掉从从设备按收到的效据即可。如果主设备要从从设备接收数据,主设备向从设备随机发送数据,从设备忽略掉从主设备接收的数据即可。
(4)SPI总线在进行数据传输时,默认先传输高位,后传输低位。 数据线为高电平表示逻辑1,数据线为低电平表示逻辑0,一个字节传输完成以后无需应答信号即可开启下一个字节的传输。
SPI总线采用同步方式,在时钟线的第一个或者第二个跳变沿采集数据(主机侧读数据),然后在紧接着的下一个跳变沿发数据。8个时钟周期即可完成一个字节的效据传输。
七、SPI的极性和相位–决定工作模式
四种模式:MODE0~MODE3。
SPI可以工作在不同的模式,主要取决于时钟信号的相位(CPHA)和极性(CPOL)。这两个参数决定了数据的采样时机。实际中采用较多的是“模式 0”与“模式 3”。
CPHA(Clock Phase,时钟相位):表示SCK在第几个时钟边缘采样数据。当CPHA=0,在SCK周期的第一个边沿采样数据,当CPHA=1,在SCK周期的第二个边沿采样数据。
CPOL(Clock Polarity,时钟极性):表示SCK在空闲时为高电平还是低电平。当CPOL=0,SCK空闲时为低电平,当CPOL=1,SCK空闲时为高电平。
模式 | CPOL(SCK) | CPHA | SCK空闲 | 采样点 |
---|---|---|---|---|
0 | 0 | 0 | 低电平 | 数据在周期的第一个电平转换沿处采样(上升沿采样) |
1 | 0 | 1 | 低电平 | 数据在周期的第二个电平转换沿处采样(下降沿采样) |
2 | 1 | 0 | 高电平 | 数据在周期的第一个电平转换沿处采样(下降沿采样) |
3 | 1 | 1 | 高电平 | 数据在周期的第二个电平转换沿处采样(上升沿采样) |
●CPHA=0时SPI通讯模式:
●CPHA=1时SPI通讯模式:
八、SPI通讯时序图
(1)起始信号: 在上图①处,NSS 信号线由高变低,是 SPI 通讯的起始信号。
NSS 是每个从机各自独占的信号线,当从机在自己的 NSS 线检测到起始信号后,就知道自己被主机选中了,开始准备与主机通讯。
(2)数据的有效性: 图中的②③④⑤标号处,MOSI 及 MISO 的数据在 SCK 的上升沿期间变化输出,在 SCK 的下降沿时被采样。即在 SCK 的下降沿时刻,MOSI 及 MISO 的数据有效,高电平时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效,MOSI 及 MISO为下一次表示数据做准备。
(3)停止信号: 在上图⑥处,NSS 信号由低变高,是 SPI 通讯的停止信号。表示本次通讯结束,从机的选中状态被取消。
九、实验一:IO口模拟SPI驱动W25Q64
使用IO口来模拟出SPI时序信号,即通过拉高拉低IO口电平来实现。
1. 传输一个字节的数据(发送和接收数据同时进行。sByte:发送数据,rByte:接收数据)
u8 SPI_TransferByte (u8 sByte)
{
u8 i, rByte;
for(i =0; i<8;i++)
{
SPI_SCK_L; //下降沿-准备发送数据
if(sByte &(0x80 >> i)) //高位发起
SPI_MOSI_H;
else
SPI_MOSI_L;
rByte <<= 1; //空出最低位,准备接收数据
SPI_SCK_H; //上升沿-采集数据
if(SPI_MISO_R) //读取 MISO的电平
rByte |= 1;
}
return rByte;
}
2. W25Q64指令时序分析
●统一解答:下文中为什么读数据时,使用
SPI_TransferByte(0xFF);
发送0xFF。
答:因为SPI读取数据和接收数据是同步进行的,所以我们想要接收(读取)数据时,必须先发送一个任意数据,这里我们使用0xFF(自定)。SPI_TransferByte()函数的返回值就是我们要读取的数据。
(1) 读状态寄存器1:指令0x05
u8 w25Q64_Readstatus1(void)
{
u8 status;
SPI_CS_L;//拉低片选
SPI_TransferByte(0x05);//发送指令0x05
status = SPI_TransferByte(0xFF); //接收状态寄存器的值,发送任意数据都行。
SPI_CS_H;//拉高片选
return status;
}
(2) 写使能:指令0x06
void w25Q64writeEnable(void)
{
SPI_CS_L;//拉低片选
SPI_TransferByte (0x06);//发送写使能指令0x06
SPI_CS_H;//拉高片选
}
(2)扇区擦除:指令0x20
void w25Q64_sectorErase (u32 addr) //扇区擦除
{
u8 *pAddr = (u8*) &addr; //扇区首地址
u8 status;
w25Q64writeEnable(); //写使能
SPI_CS_L;//拉低片选
SPI_TransferByte (0x20);//发送扇区擦除指令0x20
//发送24bit内部地址
SPI_TransferByte (pAddr[2]); // bit 23-16
SPI_TransferByte (pAddr[1]); //bit 15-8
SPI_TransferByte (pAddr[0]); //bit 7-0
SPI_CS_H; //拉高片选
//等待擦除完成 BUSY ==0
do{
status=w25Q64_Readstatus1(); //状态寄存器值为0时,擦除完成
delay_ms(1);
} while(status &(0x1));
}
(3)页写:指令0x20
例如:addr: 0x123456
bit 23-16: 0x12(addr >> 16)& OxFF
bit 15-8: Ox34(addr >> 8)& OxFF
u8 *pAddr = &addr;
*pAddr==pAddr[0]== 0x56
*(pAddr+1)==pAddr[1]= 0x34
*(pAddr+2) ==DAddr[2]= 0x12
void w25Q64_Page_Write_Data(u32 addr, u32 num, u8 *sBuf) //首地址、写几个、写什么
{
u8 *pAddr = (u8 *) &addr;
u8 status;
w25Q64_writeEnable(); //写使能
SPI_CS_L;//拉低片选
SPI_TransferByte (0x02);//发送指令0x03
//发送24bit内部地址
SPI_TransferByte(pAddr[2]); // bit 23-16
SPI_TransferByte(pAddr[1]); //bit 15-8
SPI_TransferByte(pAddr[0]);//bit 7-0
while (num--)
{
SPI_TransferByte(*sBuf++);
}
SPI_CS_H;//拉高片选
//等待写入完成 BUSY ==0
do{
status=w25Q64_Readstatus1(); //状态寄存器值为0时,写入完成
delay_ms(1);
} while(status &(0x1));
}
●补充:顺序写
页写不可以跨页写,但是顺序写可以。
void w25Q64_Orderwrite(u32 addr, u32 num, u8 *sBuf)
{
u32 remain = 0;
remain = 256 - addr % 256; //从本页首地址算出能写下的数量
if (num <= remain)//判断本页能否写下
{
w25Q64_Page_Write_Data(addr, num, sBuf);//直接写完所有数据
}
else //本页写不完
{
w25Q64_Page_Write_Data(addr, remain, sBuf);//将本页写完
}
addr +=remain; //地址偏移
sBuf +=remain; //数据地址偏移
remain = num - remain;//剩下的需要写入的数据项
//判断剩下的数据一页能否写完
while (remain >=256) //不能写完
{
w25Q64_Page_Write_Data(addr, 256, sBuf); //写满一页
addr +=256; //地址偏移
sBuf += 256; //数据地址偏移
remain = remain - 256;//剩下的需要写入的数据项
)
if (remain > 0) // 0 <num <256剩下的不够写满一页的
{
w25Q64_Page_Write_Data(addr,remain,sBuf);//剩下的写完
}
}
(4)读ID:指令0x90
ul6 w25Q64_ReadID(void)
{
ul6 ID;
w25Q64writeEnable(); //写使能
SPI_CS_L; //拉低片选
SPI_TransferByte (0x90);//发送读ID指令0x90
SPI_TransferByte (0x00);//发送0x000000
SPI_TransferByte (0x00);
SPI_TransferByte (0x00);
ID = SPITransferByte (0xFF);// Manufacturer ID
ID <<= 8;
ID |=SPI_TransferByte (0xFF); // Device ID
SPI_CS_H;//拉高片选
return ID;
}
(5)读数据:指令0x30
void w25Q64_ReadData(u32 addr, u32 num, u8 *rBuf) //首地址、读几个、读到哪
{
u8 *pAddr = (u8 *) &addr;
w25Q64_writeEnable(); //写使能
SPI_CS_L;//拉低片选
SPI_TransferByte (0x03);//发送指令0x03
//发送24bit内部地址
SPI_TransferByte(pAddr[2]); // bit 23-16
SPI_TransferByte(pAddr[1]); //bit 15-8
SPI_TransferByte(pAddr[0]);//bit 7-0
while (num--)
{
*rBuf++ = SPI_TransferByte(OxFF);//接收数据
}
SPI_CS_H;//拉高片选
}
十、实验二:SPI控制器驱动W25Q64
使用SPI控制器来驱动W25Q64就不需要IO口来模拟拉高拉低数据线,而是将IO口复用为SPI控制器功能,直接把要传输的内容写入SPI控制器的DR寄存器中,SPI控制器就会自动模拟出SPI时序信号去发送或接收数据。
1. 框图
2. 软件设计步骤
代码部分只需要在实验一的基础上修改下面的内容即可。页写、顺序写等代码不需要改动。
(1)修改SPI初始化函数
- 修改MOSI、MISO、SCK三个引脚配置为复用推挽功能,SS配置为普通推挽功能。
- 映射到SPI1
- 配置CR1、CR2
- 使能SPI1
(2)修改SPI传输字节函数
- 等待发送缓冲区为空。 写 DR。
- 等待接收缓冲区非空。 读DR。
3. 代码设计
●修改SPI初始化函数
void SPI_Init(void)
{
//打开GPIOA/B时钟
RCC->AHB1ENRl=(Ox3 <<0);
//CS -PB14 推挽输出
GPIOB->MODER&=~(0x3 <<28);
GPIOB->MODER|=(0x1 <<28);//输出
GPIOB->OTYPER&= ~(0x1<<14);//推挽
GPIOB->OSPEEDR|=(0x3 <<28);//100Mhz
//SCK -PA5复用功能
GPIOA->MODER&= ~(0x3<<10);
GPIOA->MODER|= (0x2<<10);//复用
GPIOA->OTYPER&= ~(0x1<<5);//推挽
GPIOA->OSPEEDR =(0x3 <<10);//100Mhz
GPIOA->AFR[O]&= ~(0xE<<20);//映射
GPIOA->AFR[0]|= (0x5<<20) ;//映射- AF5
//MOSI -PA7复用功能
GPIOA->MODER&= ~(0x3 <<14);
GPIOA->MODER |=(0x2 <<14);//复用
GPIOA->OTYPER&= ~(0x1<<7);//推挽
GPIOA->OSPEEDR |=(0x3<<14); //100Mhz
GPIOA->AFR[0]&= ~(0xEu<<28);//映射
GPIOA->AFR[0]|=(0x5u <<28) ;//映射- AF5
//MISO -PA6 复用功能
GPIOA->MODER&= ~(0x3<< 12);
GPIOA->MODER |=(0x2<< 12); //复用
GPIOA->PUPDR&= ~(0x3 <<12);//浮空
GPIOA->AFR[0]&= ~(0xEu<<24);//映射
GPIOA->AFR[0]|=(0x5u<<24);//映射-AF5
//初始化SPI控制器
RCC->APB2ENR |= (0x1<<12);//打开SPI1的时钟
//配置CR1
SPI1->CR1 =0;
/*
*选择双线单向通信
*禁止CRC
*8bit数据帧★全双工
*MSB在前
*scK波特率=fPCLR/2=84Mhz/ 2=42Mhz
*/
SPI1->CR1 |=(0x1<< 9);//使能软件从器件管理
SPI1->CR1 |=(0x1 <<8);//SSI =1
SPI1->CR1 |=(0x1 << 2);//SPI1配置为主模式
SPI1->CR1 |=(0x3 << 0) ;//SPI模式为MODE3
//配置CR2
SPI1->CR2 &=~(0x1<<4);//SPI Motorola模式
//使能SPI1
SPI1->CR1 |=(0x1<<6);
}
●修改SPI传输字节函数
u8 SPI_TransferByte (u8 sByte)
{
u8 rByte;
while( ( SPI1->SR&(0x1<<1) )==0 );//等待发送缓冲区为空
SPI1->DR = sByte; //写DR
while( ( SP11->SR &(Ox1 << 0) )==0 );//等待接收缓冲区非空
rByte = SPI1->DR; //读DR
return rByte;
}
补充:库函数
void SPI1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
// 启用时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIOA 时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // GPIOB 时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); // SPI1 时钟使能
// 初始化 MISO, MOSI 和 SCK 引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; // SCK, MISO, MOSI
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化 GPIOA
// 初始化 CS 引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14; // CS 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 普通推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); // 初始化 GPIOB
// 设置 CS 引脚为高电平
GPIO_SetBits(GPIOB, GPIO_Pin_14);
GPIO_SetBits(GPIOA, GPIO_Pin_5 | GPIO_Pin_7);
// SPI 配置
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 双线双向全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 设置为主设备模式
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 数据帧大小 8 位
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 空闲状态时钟为低电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 数据采样在时钟的第一个跳变沿
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // NSS 管脚由软件控制
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 波特率预分频器值为 4
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // MSB 位先发送
SPI_InitStructure.SPI_CRCPolynomial = 10; // CRC 校验多项式
// 初始化 SPI1 外设
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE); // 启用 SPI 外设
// 启动传输
SPI1_ReadWriteByte(0xFF);
}
u8 SPI1_ReadWriteByte(u8 TxData)
{
// 等待发送缓存为空
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
// 发送数据
SPI_I2S_SendData(SPI1, TxData);
// 等待接收缓存非空
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
// 读取接收到的数据
u8 RxData = SPI_I2S_ReceiveData(SPI1);
// 返回接收到的数据
return RxData;
}