一.简介:
SPI(Serial Peripheral Interface),即串行外围设备接口,是一种全双工同步通信总线。
1.物理层
上图为SPI通讯常用的连线方式,
SPI通讯使用四条线:3条总线SCK、MOSI、MISO;1条片选线NSS(低电平有效)
-
SCK(Serial Clock): 时钟信号线;
-
MOSI(Master Output,Slave Input) : 主设备输出,从设备输入,数据的方向为主机到从机;
-
MISO(Master Input, Slave Output) : 主设备输入,从设备输出,数据的方向为从机到主机;
-
NSS:从设备选择信号线,即片选信号线(CS)。有几个从设备,主机相应增加几条片选信号线。
3条总线SCK、MOSI、MISO并联
2.协议层
- 时序图:
上图为主机的通讯时序。NSS、SCK、MOSI 信号都由主机控制产生,而 MISO 的信号由从机产生,主机通过该信号线读取从机的数据。MOSI 与 MISO 的信号只在 NSS 为低电平的时候才有效,在 SCK 的每个时钟周期 MOSI 和 MISO 传输一位数据。
- 起始、停止信号
NSS 信号线由高变低,是 SPI 通讯的起始信号;
NSS 信号由低变高,是 SPI 通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。
- CPOL/CPHA
SPI一共四种通讯模式
- CPOL:时钟极性,即SPI通讯之前,NSS线为高电平时SCK的状态.
CPOL=0,空闲时SCK时钟为低电平;CPOL=1,空闲时SCK时钟为高电平。 - CPHA:时钟相位,即数据的采样时刻。
CPHA=0,MOSI 或 MISO 数据线上的信号将会在SCK 时钟线的"奇数边沿"被采样;
CPHA=1,"偶数边沿"被采样。
二.I2C_InitTypeDef 结构体
bash
typedef struct
{
uint16_t SPI_Direction; /* 设置 SPI 的单双向模式 */
uint16_t SPI_Mode; /* 设置 SPI 的主/从机端模式 */
uint16_t SPI_DataSize; /* 设置 SPI 的数据帧长度,可选 8/16 位 */
uint16_t SPI_CPOL; /* 设置时钟极性 CPOL,可选高/低电平 */
uint16_t SPI_CPHA; /* 设置时钟相位,可选奇/偶数边沿采样 */
uint16_t SPI_NSS; /* 设置 NSS 引脚由 SPI 硬件控制还是软件控制*/
uint16_t SPI_BaudRatePrescaler; /* 设置时钟分频因子,fpclk/分频数 =fSCK */
uint16_t SPI_FirstBit; /* 设置 MSB/LSB 先行 */
uint16_t SPI_CRCPolynomial; /* 设置 CRC 校验的表达式 */
}SPI_InitTypeDef;
通过标准外设库中SPI_InitTypeDef结构体 和初始化函数 配置SPI外设。
1)SPI_Direction:设置SPI通讯方向,双线全双工(SPI_Direction_2Lines_FullDuplex ),双线只接收 (SPI_Direction_2Lines_RxOnly),单线只接收 (SPI_Direction_1Line_Rx)、单线只发送模式(SPI_Direction_1Line_Tx)。
2)SPI_Mode:一般设置为主机模式(SPI_Mode_Master )。
3)SPI_DataSize:数据帧大小是为 8 位 (SPI_DataSize_8b ) 还是 16位(SPI_DataSize_16b)。
4)SPI_CPOL:时钟极性,高电平(SPI_CPOL_High )、低电平(SPI_CPOL_Low)。
5)SPI_CPHA:时钟相位,为 SPI_CPHA_1Edge (在 SCK 的 奇 数 边 沿 采 集 数 据) 或SPI_CPHA_2Edge (在 SCK 的偶数边沿采集数据)。
6)SPI_NSS:硬件模式(SPI_NSS_Hard)、软件模式(SPI_NSS_Soft ),软件模式则需编写程序控制 GPIO 端口拉高或置低产生非片选和片选信号,应用较多。
7)SPI_BaudRatePrescaler:可设置为2、4(SPI_BaudRatePrescaler_4) 、6、8、16、32、64、128、256分频。
8)SPI_FirstBit:MSB 先行 (高位数据在前) 还是 LSB 先行 (低位数据在前)
9)SPI_CRCPolynomial:CRCPolynomial(CRC多项式)的设置是用来配置CRC(循环冗余校验)的计算方式
使用方法(例):
bash
void INIT_SPI_CONFIG()
{
SPI_InitTypeDef SPI_InitStruct;
//开启SPI时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
//结构体赋值
SPI_InitStruct.SPI_CPHA = SPI_CPHA_2Edge;//奇/偶数边沿采样
SPI_InitStruct.SPI_CPOL = SPI_CPOL_High;//空闲时,CS高低电平
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;//数据帧长度:8位
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//双向全双工
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;//通过软件控制cs
SPI_InitStruct.SPI_CRCPolynomial = 7;//CRCPolynomial(CRC多项式)的设置是用来配置CRC(循环冗余校验)的计算方式
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;//波特率分频因子
//初始化函数配置SPI外设
SPI_Init(SPI1,&SPI_InitStruct);
//使能SPI
SPI_Cmd(SPI1,ENABLE);
}
三.收发数据原理
1.发送单字节数据
bash
//发送字节
uint8_t SPI_FLASH_SendByte(uint8_t byte)
{
//等待发送缓冲区为空
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET)
{
}
SPI_I2S_SendData(SPI1,byte);
//等待接收缓冲区非空
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) == RESET)
{}
return SPI_I2S_ReceiveData(SPI1);
}
- 发送数据过程:
SPI_I2S_SendData()发送数据-->SPI数据寄存器DR-->发送缓冲区->SPI外设发出数据
2.读取单字节数据
bash
//接收字节 必须先发送一个任意数字后,才可使用SPI_I2S_ReceiveData()读取数据
uint8_t SPI_FLASH_ReadByte()
{
return SPI_FLASH_SendByte(Dummy_Byte);
}
- 接收数据过程:
SPI外设接收新数据-->接收缓冲区->SPI数据寄存器DR->SPI_I2C_ReceiveData()接收新数据;
复杂的数据通讯都是由单个字节数据收发组成的。
3.注意事项:
在SPI中,读操作和写操作是同步的。即使主设备只需要进行写操作,它也会收到从设备返回的数据;如果主设备只需要读取数据,它也必须发送一个空字节来触发从设备的数据传输。
- 写操作:主设备向从设备发送数据时,从设备会在相同的时钟周期中返回数据。主设备将发送的数据通过MOSI线传送到从设备,同时从设备通过MISO线将数据发送回主设备。如果主设备只进行写操作,它将忽略从设备返回的数据。
- 读操作:主设备要读取从设备的数据时,主设备必须向从设备发送一个数据字节(这可以是任意的空字节),从设备在接收到这个数据字节后,开始向主设备发送数据。
四.读写串行FLASH
1.简介

上图FLASH 芯片 (型号:W25Q64) 是一种使用 SPI 通讯协议的存储器,它的 CS/CLK/DIO/DO 引脚分别连接到了 STM32 对应的 SPI 引脚 NSS/SCK/MOSI/MISO 上。它与 EEPROM 都是掉电后数据不丢失的存储器,但 FLASH 存储器容量普遍大于 EEPROM。FLASH 芯片只能一大片一大片地擦写,而EEPROM可以单个字节擦写。
1)指令表
通过指令,实现FLASH读写操作。
上述指令表中,带括号的字节参数,方向为FLASH向主机传输;不带括号的字节参数,方向为主机向FLASH传输。
2)获取DeviceID
Device ID:标记不同的设备:
bash
//获取Device ID
uint8_t SPI_FLASH_ReadDeviceID(void)
{
//发送指令:0xAB dummy dummy dummy;接收:DeviceID
uint32_t DeviceID = 0;
uint32_t temp = 0;
//低电平 启动通讯
SPI_FLASH_CS_LOW();
temp = SPI_FLASH_SendByte(W25X_DeviceID_COMMAND);
printf("temp1-1:%x0X\r\n",temp);
temp = SPI_FLASH_SendByte(Dummy_Byte);
printf("temp1-2:%x0X\r\n",temp);
temp = SPI_FLASH_SendByte(Dummy_Byte);
printf("temp1-3:%x0X\r\n",temp);
temp = SPI_FLASH_SendByte(Dummy_Byte);
printf("temp1-4:%x0X\r\n",temp);
DeviceID = SPI_FLASH_ReadByte();
//高电平 关闭通讯
SPI_FLASH_CS_HIGH();
return DeviceID;
}
3)获取Flash ID
Flash ID:专用于存储芯片的标识编码(如SPI NOR Flash),遵循JEDEC标准,包含厂商ID、容量和版本信息
c
//获取Flash ID
uint32_t SPI_FLASH_ReadFlashID(void)
{
uint32_t FlashID,Temp1,Temp2,Temp3;
SPI_FLASH_CS_LOW();
//发送指令:0x9F;接收:生产厂商、存储类型、容量
SPI_FLASH_SendByte(W25X_JEDEC_COMMAND);
Temp1 = SPI_FLASH_ReadByte();
Temp2 = SPI_FLASH_ReadByte();
Temp3 = SPI_FLASH_ReadByte();
SPI_FLASH_CS_HIGH();
FlashID = (Temp1<<16)| (Temp2<<8) | Temp3;
return FlashID;
}
主机端通过读取Flash ID 来测试硬件是否连接正常,或用于识别设备。
2.写操作
1)擦除扇区
c
//擦除扇区数据(重要) 一个扇区为4KB
void SPI_FLASH_SectorErase()
{
//写使能
SPI_FLASH_WriteEnableCommand();
//等待FLASH写数据完成
SPI_FLASH_WaitForEnd();
SPI_FLASH_CS_LOW();
//发送扇区擦除指令 0x20
SPI_FLASH_SendByte(W25X_SectorErase_COMMAND);
//发送扇区擦除地址 高位、中位、低位
SPI_FLASH_SendByte((FLASH_SectorEraseAddress & 0xFF0000)>>16);
SPI_FLASH_SendByte((FLASH_SectorEraseAddress & 0x00FF00)>>8);
SPI_FLASH_SendByte(FLASH_SectorEraseAddress & 0x0000FF);
SPI_FLASH_CS_HIGH();
//等待擦除数据完成
SPI_FLASH_WaitForEnd();
}
//等待FLASH写数据完成
void SPI_FLASH_WaitForEnd()
{
uint8_t Flag;
SPI_FLASH_CS_LOW();
SPI_FLASH_SendByte(W25X_ReadStatusReg_COMMAND);//发送 读取状态寄存器 指令
do{
Flag = SPI_FLASH_SendByte(Dummy_Byte);
}while(Flag & WIP_STATUS == 0x01);
SPI_FLASH_CS_HIGH();
}
//发送FLASH 写使能 操作指令
void SPI_FLASH_WriteEnableCommand()
{
SPI_FLASH_CS_LOW();
SPI_FLASH_SendByte(W25X_WriteEnable_COMMAND);
SPI_FLASH_CS_HIGH();
}
注意事项:
- 擦除数据之间,要先发送写数据指令到FLASH;
- FLASH 存储器的特性决定了它只能把原来为"1"的数据位改写成"0",而原来为"0"的
数据位不能直接改写为"1"。所以这里涉及到数据"擦除"的概念,在写入前,必须要对目标存储矩阵进行擦除操作,把矩阵中的数据位擦除为"1"。
2)写入数据
c
//向Flash写入一页数据256Byte(DataSize < SPI_FLASH_PageSize)
void SPI_FLASH_WritePageData(uint8_t *SendDataAddress,uint32_t FlashAddress,uint8_t DataSize)
{
//写使能
SPI_FLASH_WriteEnableCommand();
SPI_FLASH_CS_LOW();
//写入命令
SPI_FLASH_SendByte(W25X_WritePage_COMMAND);
//写入地址 高位、中位、低位
SPI_FLASH_SendByte((FlashAddress & 0xFF0000)>>16);
SPI_FLASH_SendByte((FlashAddress & 0x00FF00)>>8);
SPI_FLASH_SendByte(FlashAddress & 0x0000FF);
//写入数据 一次写一个字节
while(DataSize--)
{
SPI_FLASH_SendByte(*SendDataAddress);
//指向下一个字节数据
SendDataAddress++;
}
SPI_FLASH_CS_HIGH();
//等待写入完成
SPI_FLASH_WaitForEnd();
}
//向Flash写入全部数据 DataSize:字节数
void SPI_FLASH_WriteData(uint8_t *SendDataAddress,uint32_t FlashAddress,uint8_t DataSize)
{
uint8_t PageNum = DataSize/SPI_FLASH_PageSize;//整数页数
uint8_t modNum = DataSize % SPI_FLASH_PageSize;//剩余字节数
if(PageNum == 0)
{
//写入一页 (DataSize<SPI_FLASH_PageSize)
SPI_FLASH_WritePageData(SendDataAddress,FlashAddress,DataSize);
}
else
{
//写入多页 (DataSize>SPI_FLASH_PageSize)
while(PageNum--)
{
SPI_FLASH_WritePageData(SendDataAddress,FlashAddress,SPI_FLASH_PageSize);
SendDataAddress += SPI_FLASH_PageSize;
FlashAddress += SPI_FLASH_PageSize;
}
SPI_FLASH_WritePageData(SendDataAddress,FlashAddress,modNum);
}
}
- 原理:
1)使能写操作;
2)读取FLASH芯片状态寄存器的内容(确定其是否空闲);
3)扇区擦除 (第一个字节为指令编码,紧跟着3个字节--要擦除的存储矩阵的地址);
4)页写入 (发送写指令,发送紧跟3个字节-写地址)。
3.读操作
c
//从Flash读取数据
void SPI_FLASH_ReadData(uint8_t *RecvDataAddress,uint32_t FlashAddress,uint8_t DataSize)
{
SPI_FLASH_CS_LOW();
//发送命令
SPI_FLASH_SendByte(W25X_ReadData_COMMAND);
//发送地址 高位、中位、低位
SPI_FLASH_SendByte((FlashAddress & 0xFF0000)>>16);
SPI_FLASH_SendByte((FlashAddress & 0x00FF00)>>8);
SPI_FLASH_SendByte(FlashAddress & 0x0000FF);
//读取数据 一次读一个字节
while(DataSize--)
{
*RecvDataAddress = SPI_FLASH_ReadByte();
RecvDataAddress++;
}
SPI_FLASH_CS_HIGH();
}
- 原理:
1)发送读指令(0x03);
2)发送读地址(三个字节,地址高位、地址中位、地址地位);
3)SPI_I2C_ReceiveData()读取数据。
4.发送和接收数据对比
c
//对比发送和接收的数据
STATUS CompareData(uint8_t *SendData,uint8_t *RecvData,uint8_t DataSize)
{
while(DataSize--)
{
if(*SendData != *RecvData)
return FAILED;
SendData++;
RecvData++;
}
return PASSED;
}
4.工程源代码
完整工程免费下载链接:STM32-SPI-FLASH 数据读写
6.运行结果
使用串口调试助手sscom33显示打印的数据: