IIC主要用于通讯速率一般的场合,而SPI一般用于较高速的场合。
一、SPI协议简介
SPI 协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设 备接口,是一种高速全双工的通信总线。它被广泛地使用在 ADC、LCD 等设备与 MCU 间, 要求通讯速率较高的场合。
(一)物理层
?
SPI 通讯使用 3 条总线及片选线,3 条总线分别为 SCK、MOSI、MISO,片选线为SS,它们的作用介绍如下:
-
SS:从设备选择信号线,常称为片选信号线,也称为NSS、CS。每个从设备都有独立的一条SS信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。IIC协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而SPI协议中没有设备地址,它使用SS信号线来寻址,当主机选择从设备时,把该从设备的SS信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行SPI通讯。所以SPI通讯以SS线置低电平为开始信号,以SS线被拉高作为结束信号。 -
SCK:时钟信号线,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样。(同步通讯)STM32 的 SPI 时钟频率最大为 fpclk/2,两个设备之间通讯时,通讯速率受限于低速设备。 -
MOSI(Master Output,Slave Input):主设备输出/从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,即这条线上数据的方向为主机到从机。 -
MISO(Master Input,Slave Output):主设备输入/从设备输出的引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。
(二)协议层
SPI协议定义了通讯的起始和停止信号、数据有效性、时钟同步等环节。
(1)SPI基本通讯过程
-
通讯的起始和停止信号 SS信号线由高变低,是SPI通讯的起始信号。NSS是每个从机各自独占的信号线,当从机检测到NSS线检测的起始信号后,就知道被选中了,开始准备与主机通讯。 当信号由低变高,是SPI通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。 -
数据有效性 SPI使用MOSI及MISO信号线来传输数据,使用SCK信号线进行数据同步,MOSI及MISO数据线在SCK的每个时钟周期传输一位数据,且数据输入输出是同时进行的。(图示为下降沿采集数据) -
CPOL/CPHA及通讯模式 时钟极性CPOL是指SPI通讯设备处于空闲状态时,SCK信号线的电平信号(即SPI通讯开始前、NSS线为高电平时SCK的状态)。CPOL=0时,SCK在空闲状态时为低电平,CPOL=1时则相反。 时钟相位CPHA是指数据在采用的时刻,当CPHA=0时,MOSI或MISO数据线上的信号将会在SCK时钟线的“奇数边沿”被采样。当CPHA=1时,数据线在SCK的“偶数边沿”采样。(无关上升沿下降沿),下图为奇数边沿采样。
?由CPOL及CPHA的不同状态,SPI分成了四种模式,主机与从机需要工作在相同的模式下才可以正常通讯,实际中采用较多的是“模式0”与“模式3”。
?
二、STM32的SPI外设架构
?
STM32的SPI外设可用作通讯的主机及从机,支持最高的SCK时钟 频率为fpclk/2 (STM32F429型号的芯片默认fpclk1为90MHz,fpclk2为45MHz), 完全支持SPI协议的4种模式,数据帧长度可设置为8位或16位,可设置数据 MSB先行(高位先行,从左往右)或LSB先行(低位先行,从右往左)。它还支持双线全双工(前面小节说明的都是这种模式)、 双线单向以及单线模式。
1-通讯引脚,2-时钟控制逻辑,3-数据控制逻辑,4-整体控制逻辑。
(1)通讯引脚
其中SPI1\SPI4\SPI5\SPI6是APB2上的设备,最高通讯速率达到45Mbit/s,SPI2\SPI3是APB1上的设备,最高通信速率为22.5Mbit/s。其它功能上没有差异。SPI2\SPI3引脚上上均有I2S,可用来设置音频,但是IIS与SPI不可以共用。
(2)时钟控制逻辑
SCK线的时钟信号,由波特率发生器根据“控制寄存器CR1”中的BR[0:2]位控制,该位是对fpclk时钟的分频因子,对fpclk的分频结果就是SCK引脚的输出时钟频率。
?其中fpclk频率是指SPI所在的APB总线频率,APB1为fpclk1,APB2为fpclk2。为了协调通讯速度比较慢的设备。
(3)数据控制逻辑
SPI的MOSI及MISO都连接到数据移位寄存器上,数据移位寄存器的数据来源于接收缓冲区及发送缓冲区。
-
通过写SPI的数据寄存器DR把数据填充到发送缓冲区中。 -
通过读数据寄存器DR,可以获取接收缓冲区的内容。 -
其中数据帧长度可以通过控制寄存器DR的DFF位配置成8位及16位模式:配置LSBFIRST位可以选择MSB先行还是LSB先行。 -
SPI 的 MOSI 及 MISO 都连接到数据移位寄存器上,数据移位寄存器的内容来源于接收缓冲区及发送缓冲区以及 MISO、MOSI 线。当向外发送数据的时候,数据移位寄存器以 “发送缓冲区”为数据源,把数据一位一位地通过数据线发送出去;当从外部接收数据的 时候,数据移位寄存器把数据线采样到的数据一位一位地存储到“接收缓冲区”中。
?
(4)整体控制逻辑
整体控制逻辑复制协调整个SPI外设。控制逻辑的工作模式根据我们配置的“控制寄 存器(CR1/CR2)”的参数而改变,基本的控制参数包括前面提到的 SPI 模式、波特率、LSB 先行、主从模式、单双向模式等等。我们可以通过工作状态寄存器读取SPI的工作状态,“状态寄存器(SR)”。控制逻辑还可以根据要求,负责控制产生SPI中断信号、DMA请求及控制NSS信号线。在实际的应用中,我们一般不使用SPI外设的标准NSS信号线,而是更简单地使用普通GPIO,软件控制它地电平输出,从而产生通讯起始和停止信号。
(5)通讯过程
?TXE标志代表的是缓冲区是否为空,当TXE为0时,发送缓冲区为非空,若为1时,发送缓冲区为空。当其为空时,也就说明可以准备发送下一个数据。RXNE为接收缓冲区是否为空的标志,其中0代表接收缓冲区为空,1代表接收缓冲区非空。
-
控制NSS信号线,产生起始信号。 -
把要发送的数据写入到”数据寄存器DR“中,该数据会被存储到发送缓冲区。 -
通讯开始,SCK时钟开始运行。MOSI把发送缓冲区中的数据一位一位地传输出去;MISO则把数据一位一位地存储进接收缓冲区中; -
当发送完一帧数据的时候,”状态寄存器SR“中的"TXE标志位"会被置1,表示传输完一帧,发送缓冲区已空;类似的,当接收完一帧数据的时候,”RXNE标志位“会被置1,表示传输完一帧,接收缓冲区非空; -
等待到”TXE标志位“为1时,若还要继续发送数据,则再次往”数据寄存器DR“写入数据即可;等待到”RXENE标志位“为1时,通过读取”数据寄存器DR“可以获取接收缓冲区中的内容。
假如使能了TXE或RXNE中断,TXE或RXNE置1时会产生SPI中断信号,进入同一个中断服务函数,到SPI中断服务程序后,可通过检查寄存器位来了解是哪一个事件,再分别进行处理。也可以使用DMA方式来收发”数据寄存器DR“中的数据。
需要注意的是CR寄存器中的SSM位:
?当我们让这个寄存器置1时,我们可以通过软件来模拟SPI,这也是比较常用的方式。
三、SPI结构体
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_Direction:有双线全双工、双线只接收、单线只接收、单线只发送模式。 -
SPI_Mode:主机模式、从机模式。这两个模式的最大区别是在于时钟信号线SCK信号线的时序,SCK的时序由通讯中的主机产生。若被设置为从机模式,则要接受外来的SCK信号。 -
SPI_DataSize:可以选择SPI通讯的数据帧大小为8位或者16位。 -
SPI_CPOL和SPI_CPHA:这两个成员配置SPI的时钟极性CPOL和时钟相位CPHA,这两个配置影响到SPI的通讯模式。时钟极性CPOL成员,可以设置为高电平或者为低电平。时钟相位CPHA成员,可以设置为在SCK奇数边沿采集数据或者是偶数边沿。 -
SPI_NSS:可以选择硬件模式或软件模式。在硬件模式中的SPI片选信号由SPI硬件自动产生,而软件模式则需要亲自把相应的GPIO端口拉高或者置低产生非片选和片选信号。 -
SPI_BaudRatePrescaler:参数可以设定为2、4、6、8、16、32、64、128、256分频。 -
SPI_FirstBit:MSB先行(高数据在前)还是LSB先行(低位数据在前)。 -
SPI_CRCPolynomial:适用于比较复杂的环境,这是 SPI 的 CRC 校验中的多项式,若我们使用 CRC 校验时,就使用这个成员的参数 (多项式),来计算 CRC 的值。
四、实践——SPI读写串行FLASH
?上面是我们即将改写的FLASH芯片。容量为16M。NCS引脚也为NSS引脚,DIO为MOSI引脚,DO为MISO引脚。WP为写保护引脚,低电平有效。HOLD为暂停通讯或结束通讯,用的很少,接为高电平。以下为引脚的连接图。
?在FLASH中,它一共有0-255即256个块(Block),每个块是64KB,16M=64*255/1024。每个块右分为0-15个扇区(Sector),每个扇区4KB。写入数据之前,必须要擦除数据,再重新写入数据,擦除的最小单位为扇区。设备ID为4018H,设备ID可以用来判断设备是否连接正常,以及设备是否配套正确。擦除整个芯片的命令为:C7h/60h。擦除扇区的命令为20h。此芯片为MSB先行。以上为该芯片手册中得出。
?
了解这款FLASH后,我们开始进行读写。
(1)定义引脚以及时钟
#define FLASH_SPI SPI5
#define FLASH_SPI_CLK RCC_APB2Periph_SPI5
#define RCC_APB_CLOCK_FUN RCC_APB2PeriphClockCmd
#define FLASH_SPI_CS_GPIO_PORT GPIOF
#define FLASH_SPI_CS_GPIO_CLK RCC_AHB1Periph_GPIOF
#define FLASH_SPI_CS_PIN GPIO_Pin_6
#define FLASH_SPI_SCK_GPIO_PORT GPIOF
#define FLASH_SPI_SCK_GPIO_CLK RCC_AHB1Periph_GPIOF
#define FLASH_SPI_SCK_PIN GPIO_Pin_7
#define FLASH_SPI_SCK_AF GPIO_AF_SPI5
#define FLASH_SPI_SCK_SOURCE GPIO_PinSource7
#define FLASH_SPI_MISO_GPIO_PORT GPIOF
#define FLASH_SPI_MISO_GPIO_CLK RCC_AHB1Periph_GPIOF
#define FLASH_SPI_MISO_PIN GPIO_Pin_8
#define FLASH_SPI_MISO_AF GPIO_AF_SPI5
#define FLASH_SPI_MISO_SOURCE GPIO_PinSource8
#define FLASH_SPI_MOSI_GPIO_PORT GPIOF
#define FLASH_SPI_MOSI_GPIO_CLK RCC_AHB1Periph_GPIOF
#define FLASH_SPI_MOSI_PIN GPIO_Pin_9
#define FLASH_SPI_MOSI_AF GPIO_AF_SPI5
#define FLASH_SPI_MOSI_SOURCE GPIO_PinSource9
(2)引脚初始化(复用GPIO)
#define CS_HIGH_DISABLE() GPIO_SetBits(FLASH_SPI_CS_GPIO_PORT,FLASH_SPI_CS_PIN)
#define CS_LOW_ENABLE() GPIO_ResetBits(FLASH_SPI_CS_GPIO_PORT,FLASH_SPI_CS_PIN)
void FLASH_SPI_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
//1.初始化GPIO
RCC_AHB1PeriphClockCmd(FLASH_SPI_CS_GPIO_CLK|FLASH_SPI_SCK_GPIO_CLK|FLASH_SPI_MISO_ GPIO_CLK|FLASH_SPI_MOSI_GPIO_CLK,ENABLE);
/* 连接 引脚源*/
GPIO_PinAFConfig(FLASH_SPI_SCK_GPIO_PORT,FLASH_SPI_SCK_SOURCE,FLASH_SPI_SCK_AF);
/* 连接 */
GPIO_PinAFConfig(FLASH_SPI_MISO_GPIO_PORT,FLASH_SPI_MISO_SOURCE,FLASH_SPI_MISO_AF);
GPIO_PinAFConfig(FLASH_SPI_MOSI_GPIO_PORT,FLASH_SPI_MOSI_SOURCE,FLASH_SPI_MOSI_AF);
/* 使能 SPI 时钟 */
RCC_APB_CLOCK_FUN(FLASH_SPI_CLK, ENABLE);
/* GPIO初始化 */
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //复用引脚配置为输出模式也可以进行输入
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
/* 配置SCK引脚为复用功能 */
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN ;
GPIO_Init(FLASH_SPI_SCK_GPIO_PORT, &GPIO_InitStructure);
/* 配置MISO引脚为复用功能 */
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN;
GPIO_Init(FLASH_SPI_MISO_GPIO_PORT, &GPIO_InitStructure);
/* 配置MOSI引脚为复用功能 */
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;
GPIO_Init(FLASH_SPI_MOSI_GPIO_PORT, &GPIO_InitStructure);
/*CS引脚 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; //推挽输出,本身硬件就有一个上拉
/* 配置SCK引脚为复用功能 */
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_CS_PIN ;
GPIO_Init(FLASH_SPI_CS_GPIO_PORT, &GPIO_InitStructure);
//2.配置SPI工作模式
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; //最快的分频
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge ; //偶数边沿
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High ; //空闲时SCK时钟高电平
SPI_InitStructure.SPI_CRCPolynomial = 0 ; //不需要使用CRC校验
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //数据帧
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //双向
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//高位先行
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;//软件配置
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //主机
SPI_Init(FLASH_SPI,&SPI_InitStructure);
SPI_Cmd(FLASH_SPI,ENABLE);
CS_HIGH_DISABLE();
}
为了看初始化是否成功,我们可以获取设备ID来检验。获取ID的命令为:9Fh。
?
uint32_t Read_Device_ID(void)
{
uint8_t temp[3];
//拉低片选
CS_LOW_ENABLE();
Read_Write_Byte(JEDEC_ID);
temp[0] = Read_Write_Byte(DUMMY); //发送任意字节,产生时序,下面同理
temp[1] = Read_Write_Byte(DUMMY);
temp[2] = Read_Write_Byte(DUMMY);
//拉高片选
CS_HIGH_DISABLE();
//将数据进行组合
return temp[0]<<16|temp[1]<<8|temp[2];
}
?以上的命令可以使用宏定义来方便使用:
#define DUMMY 0xFF //任意值
#define JEDEC_ID 0x9F //ID
#define ERACE_SECTOR 0x20 //擦除扇区
#define READ_DATA 0x03 //读取数据
#define READ_STATUS 0x05 //空闲
#define WRITE_ENABLE 0x06 //写使能
#define PAGE_PROGRAM 0x02 //写入的地址
为了防止时钟频率错误响应,我们需要检验标志位,取决于我们什么时候读,什么时候写:
//先发送再接收才会产生时序,一定要注意!!否则STM32不会产生时序,产生一下后就停止,只接受到了地址的数据
uint8_t Read_Write_Byte(uint8_t data)
{
time_out = SPI_FLAG_TIMEOUT;
while(SPI_GetFlagStatus(FLASH_SPI,SPI_FLAG_TXE) == RESET)
{
if((time_out--)==0) return SPI_TIMEOUT_UserCallback(0);
}
//发送缓冲区为空
SPI_I2S_SendData(FLASH_SPI,data);
time_out = SPI_FLAG_TIMEOUT;
//接收缓冲区为空,死循环
while(SPI_GetFlagStatus(FLASH_SPI,SPI_FLAG_RXNE) == RESET)
{
if((time_out--)==0) return SPI_TIMEOUT_UserCallback(1);
}
return SPI_I2S_ReceiveData (FLASH_SPI);
}
?其中的变量以及报错函数定义:
/*等待超时时间*/
#define SPI_FLAG_TIMEOUT ((uint32_t)0x1000)
#define SPI_LONG_TIMEOUT ((uint32_t)(10 * SPI_FLAG_TIMEOUT))
/*信息输出*/
#define FLASH_DEBUG_ON 0
#define FLASH_INFO(fmt,arg...) printf("<<-FLASH-INFO->> "fmt"\n",##arg)
#define FLASH_ERROR(fmt,arg...) printf("<<-FLASH-ERROR->> "fmt"\n",##arg)
#define FLASH_DEBUG(fmt,arg...) do{\
if(FLASH_DEBUG_ON)\
printf("<<-FLASH-DEBUG->>[%s] [%d]"fmt"\n",__FILE__,__LINE__, ##arg);\
}while(0)
static uint8_t SPI_TIMEOUT_UserCallback(uint8_t errorCode)
{
FLASH_ERROR("SPI 等待超时!errorCode = %d",errorCode);
return 0xFF;
}
为了保持在运行的过程中复位不会因为掉电而乱发数据,我们需要控制Release_Power_Down,稍后会进行补充。
(3)编写FLASH读写过程
//擦除过后扇区内所有的数据都应为1
void erace_setor(uint32_t addr)
{
//在擦除之前必须写使能
Write_Enable();
Wait_for_Ready();
//拉低片选
CS_LOW_ENABLE();
Read_Write_Byte(ERACE_SECTOR);
//一次能发送24bit
Read_Write_Byte((addr>>16)&0xFF);
Read_Write_Byte((addr>>8)&0xFF);
Read_Write_Byte(addr&0xFF);
//拉高片选
CS_HIGH_DISABLE();
//等待内部时序(等待擦除完成)
}
//写使能函数
void Write_Enable(void)
{
//拉低片选
CS_LOW_ENABLE();
Read_Write_Byte(WRITE_ENABLE);
//拉高片选
CS_HIGH_DISABLE();
}
在读取的过程中,我们需要得知状态,看它是否空闲后再写入数据、擦除数据、读取数据,这个函数需要在拉低片选前使用:
void Wait_for_Ready(void)
{
uint8_t reg_status=0x01;
while(reg_status &0x01)
{
//拉低片选
CS_LOW_ENABLE();
//读状态寄存器
Read_Write_Byte(READ_STATUS);
reg_status = Read_Write_Byte(DUMMY);
//拉高片选
CS_HIGH_DISABLE();
}
}
读取数据的函数如下(整块数据而非单个):
void Read_buffer(uint8_t* pdata,uint32_t addr,uint32_t numByteTorRead)
{
Wait_for_Ready();
//拉低片选
CS_LOW_ENABLE();
Read_Write_Byte(READ_DATA);
Read_Write_Byte((addr>>16)&0xFF);
Read_Write_Byte((addr>>8)&0xFF);
Read_Write_Byte(addr&0xFF);
while(numByteTorRead--)
{
*pdata = Read_Write_Byte(DUMMY);
pdata++;
}
//拉高片选
CS_HIGH_DISABLE();
}
完成了读数据,接下来是写入数据,最多写入256个数据:
void Write_buffer(uint8_t* pdata,uint32_t addr,uint32_t numByteTorWrite)
{
Write_Enable();
Wait_for_Ready();
//拉低片选
CS_LOW_ENABLE();
Read_Write_Byte(PAGE_PROGRAM);
Read_Write_Byte((addr>>16)&0xFF);
Read_Write_Byte((addr>>8)&0xFF);
Read_Write_Byte(addr&0xFF);
while(numByteTorWrite--)
{
Read_Write_Byte(*pdata);
pdata++;
}
//拉高片选
CS_HIGH_DISABLE();
}
主函数:
uint8_t readBuff[4096] = {0x0};
uint8_t writeBuff[256] = {0x0};
int main(void)
{
uint32_t device_id = 0;
uint32_t i=0;
/*初始化USART 配置模式为 115200 8-N-1,中断接收*/
Debug_USART_Config();
FLASH_SPI_Config();
/* 发送一个字符串 */
Usart_SendString( DEBUG_USART,"这是一个FLASH实验\n");
printf("这是一个FLASH实验\n");
device_id = Read_Device_ID();
printf("device_id =0x%x",device_id);
erace_setor(0x00);//FLASH先擦除后写入
//读出擦除后的数据
Read_buffer(readBuff,0x00,4096);
printf("\r\n*************读出擦除后的数据**********\r\n");
for(i=0;i<4096;i++)
printf("0x%x ",readBuff[i]);
for(i=0;i<256;i++)
writeBuff[i] = i;
Write_buffer(writeBuff,0x00,256);
//读出擦除后的数据
Read_buffer(readBuff,0x00,256);
printf("\r\n*************读出写入后的数据**********\r\n");
for(i=0;i<256;i++)
printf("0x%x ",readBuff[i]);
while(1)
{
}
}
五、看库理清思路
下面的代码是已经进行初始化过后,使能NSS引脚后的操作:
(1)使用SPI发送和接收一个数据
/*
* @brief 使用SPI发送一个字节的数据
* @param byte:要发送的数据
* @retval 返回接收到的数据
*/
u8 SPI_FLASH_SendByte(u8 byte)
{
SPITimeout = SPIT_FLAG_TIMEOUT;
/* 等待发送缓冲区为空,TXE事件 */
while (SPI_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_TXE) == RESET)
{
if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);
}
/* 写入数据寄存器,把要写入的数据写入发送缓冲区 */
SPI_I2S_SendData(FLASH_SPI, byte);
SPITimeout = SPIT_FLAG_TIMEOUT;
/* 等待接收缓冲区非空,RXNE事件 */
while (SPI_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_RXNE) == RESET)
{
if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);
}
/* 读取数据寄存器,获取接收缓冲区数据 */
return SPI_I2S_ReceiveData(FLASH_SPI);
}
//当使用SPI进行读取时,我们需要先写入,再读出
/*
* @brief 使用SPI读取一个字节的数据
* @param 无
* @retval 返回接收到的数据
*/
u8 SPI_FLASH_ReadByte(void)
{
return (SPI_FLASH_SendByte(Dummy_Byte));
}
-
函数u8 SPI_FLASH_SendByte(u8 byte)实现了SPI的通讯过程。 -
上面两个函数都不包含SPI的起始和停止信号,只是收发的主要过程,所以以上两个函数都是拿来调用的,前后需要做好起始和停止信号的操作。 -
通过检测TXE,获取发送缓冲区状态,若发送缓冲区为空,则说明上一个数据已发送完毕,若不为空,则等待其为空后,再调用库函数SPI_I2S_SendData把要发送的数据写入到数据寄存器DR,写入SPI的数据会存储到发送缓冲区,由SPI外设发送出去。从这一点就可以说明,当你想要读取数据时,必须先写入,后再读出。 -
写入完毕后,等待RXNE事件,即接收缓冲区非空事件。由于 SPI 双线全双工模式下 MOSI 与 MISO 数据传输是同步的,当接收缓冲区非空时,表示上面的数据发送完毕,且接收缓冲区也收到新的数据。 -
等待至接收缓冲区非空时,通过调用库函数SPI_I2S_ReceiveData读取寄存器DR中的数据,最后将其return。 -
最后看一下读取一个字节数据,它只是简单地调用了一个任意值Dummy_Byte,然后获取返回值,其实发送值是什么无关紧要,然后获取其返回值。SPI接收过程和发送过程实质是一样的,收发同时进行,关键在于上层应用关注的是接收还是发送。
(2)写使能以及读取当前的状态
/*
* @brief 向FLASH发送 写使能 命令
* @param none
* @retval none
*/
void SPI_FLASH_WriteEnable(void)
{
/* 通讯开始:CS低 */
SPI_FLASH_CS_LOW();
/* 发送写使能命令*/
SPI_FLASH_SendByte(W25X_WriteEnable);
/*通讯结束:CS高 */
SPI_FLASH_CS_HIGH();
}
FLASH芯片向内部存储矩阵写入数据需要消耗一定的时间,并不是在总线通讯结束的一瞬间完成的,所以需要检验FLASH是否空闲。FLASH芯片定义了一个状态寄存器:
我们需要关注这个状态寄存器的第0位BUSY是否为1,表明FLASH芯片处于忙碌状态,也就是说这个时候它可能在进行擦除或者写入的操作。利用指令表中的“Read Status Register”指令可以获取FLASH芯片寄存器的内容。并校验第0位,判断当前是否可以写入。判断函数如下:
/*
* @brief 等待WIP(BUSY)标志被置0,即等待到FLASH内部数据写入完毕
* @param none
* @retval none
*/
void SPI_FLASH_WaitForWriteEnd(void)
{
u8 FLASH_Status = 0;
/* 选择 FLASH: CS 低 */
SPI_FLASH_CS_LOW();
/* 发送 读状态寄存器 命令 */
SPI_FLASH_SendByte(W25X_ReadStatusReg);
SPITimeout = SPIT_FLAG_TIMEOUT;
/* 若FLASH忙碌,则等待 */
do
{
/* 读取FLASH芯片的状态寄存器 */
FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);
{
if((SPITimeout--) == 0)
{
SPI_TIMEOUT_UserCallback(4);
return;
}
}
}
while ((FLASH_Status & WIP_Flag) == SET); /* 正在写入标志 */
/* 停止信号 FLASH: CS 高 */
SPI_FLASH_CS_HIGH();
}
(3)FLASH扇区擦除
FLASH的存储特性:由于 FLASH 存储器的特性决定了它只能把原来为“1”的数据位改写成“0”,而原 来为“0”的数据位不能直接改写为“1”。所以这里涉及到数据“擦除”的概念,在写入 前,必须要对目标存储矩阵进行擦除操作,把矩阵中的数据位擦除为“1”,在数据写入的 时候,如果要存储数据“1”,那就不修改存储矩阵 ,在要存储数据“0”时,才更改该位。
擦除有以下分类:扇区擦除(Sector Erase)、块擦除(Block Erase)、整片擦除(Chip Erase)
扇区擦除指令的第一个字节为指令编码,紧接着发送的 4 个字节用于表示要擦除的存储矩阵地址。要注意的是在扇区擦除指令前,还需要先发送“写使能”指令,发送扇区擦除指令后,通过读取寄存器状态等待扇区擦除操作完毕。注意发送擦除地址时高位在前即可。
void SPI_FLASH_SectorErase(u32 SectorAddr)
{
/* 发送FLASH写使能命令 */
SPI_FLASH_WriteEnable();
SPI_FLASH_WaitForWriteEnd();
/* 擦除扇区 */
/* 选择FLASH: CS低电平 */
SPI_FLASH_CS_LOW();
/* 发送扇区擦除指令*/
SPI_FLASH_SendByte(W25X_SectorErase);
/*发送擦除扇区地址的高8位*/
SPI_FLASH_SendByte((SectorAddr & 0xFF000000) >> 24);
/*发送擦除扇区地址的中前8位*/
SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);
/* 发送擦除扇区地址的中后8位 */
SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);
/* 发送擦除扇区地址的低8位 */
SPI_FLASH_SendByte(SectorAddr & 0xFF);
/* 停止信号 FLASH: CS 高电平 */
SPI_FLASH_CS_HIGH();
/* 等待擦除完毕*/
SPI_FLASH_WaitForWriteEnd();
}
(4)FLASH的页写入
FLASH的页写入命令最多一次可以传输256个字节数据,这个单位也是页大小。FLASH页写入的时序如图:
?
从时序图可知,第 1 个字节为“页写入指令”编码,24 字节为要写入的“地址 A”, 接着的是要写入的内容,最多个可以发送 256 字节数据,这些数据将会从“地址 A”开始, 按顺序写入到 FLASH 的存储矩阵。若发送的数据超出 256 个,则会覆盖前面发送的数据。
与擦除指令不一样,页写入指令的地址并不要求按 256 字节对齐,只要确认目标存储 单元是擦除状态即可(即被擦除后没有被写入过)。所以,若对“地址 x”执行页写入指令后, 发送了 200 个字节数据后终止通讯,下一次再执行页写入指令,从“地址(x+200)”开始写 入 200 个字节也是没有问题的(小于 256 均可)。
/*
* @brief 对FLASH按页写入数据,调用本函数写入数据前需要先擦除扇区
* @param pBuffer,要写入数据的指针
* @param WriteAddr,写入地址
* @param NumByteToWrite,写入数据长度,必须小于等于SPI_FLASH_PerWritePageSize
* @retval 无
*/
void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u32 NumByteToWrite)
{
/* 发送FLASH写使能命令 */
SPI_FLASH_WriteEnable();
/* 选择FLASH: CS低电平 */
SPI_FLASH_CS_LOW();
/* 写页写指令*/
SPI_FLASH_SendByte(W25X_PageProgram);
/*发送写地址的高8位*/
SPI_FLASH_SendByte((WriteAddr & 0xFF000000) >> 24);
/*发送写地址的中前8位*/
SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
/*发送写地址的中后8位*/
SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
/*发送写地址的低8位*/
SPI_FLASH_SendByte(WriteAddr & 0xFF);
if(NumByteToWrite > SPI_FLASH_PerWritePageSize)
{
NumByteToWrite = SPI_FLASH_PerWritePageSize;
FLASH_ERROR("SPI_FLASH_PageWrite too large!");
}
/* 写入数据*/
while (NumByteToWrite--)
{
/* 发送当前要写入的字节数据 */
SPI_FLASH_SendByte(*pBuffer);
/* 指向下一字节数据 */
pBuffer++;
}
/* 停止信号 FLASH: CS 高电平 */
SPI_FLASH_CS_HIGH();
/* 等待写入完毕*/
SPI_FLASH_WaitForWriteEnd();
}
先发送“写使能”命令,接着才开始页写入时序,然后发送指令 编码、地址,再把要写入的数据一个接一个地发送出去,发送完后结束通讯,检查 FLASH 状态寄存器,等待 FLASH 内部写入结束。
当我们有不定量数据写入时,大于256时,可以用下面的函数:
/**
* @brief 对FLASH写入数据,调用本函数写入数据前需要先擦除扇区
* @param pBuffer,要写入数据的指针
* @param WriteAddr,写入地址
* @param NumByteToWrite,写入数据长度
* @retval 无
*/
void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u32 NumByteToWrite)
{
u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
/*mod运算求余,若writeAddr是SPI_FLASH_PageSize整数倍,运算结果Addr值为0*/
Addr = WriteAddr % SPI_FLASH_PageSize;
/*差count个数据值,刚好可以对齐到页地址*/
count = SPI_FLASH_PageSize - Addr;
/*计算出要写多少整数页*/
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
/*mod运算求余,计算出剩余不满一页的字节数*/
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
/* Addr=0,则WriteAddr 刚好按页对齐 aligned */
if (Addr == 0)
{
/* NumByteToWrite < SPI_FLASH_PageSize */
if (NumOfPage == 0)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
}
else /* NumByteToWrite > SPI_FLASH_PageSize */
{
/*先把整数页都写了*/
while (NumOfPage--)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
WriteAddr += SPI_FLASH_PageSize;
pBuffer += SPI_FLASH_PageSize;
}
/*若有多余的不满一页的数据,把它写完*/
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
}
/* 若地址与 SPI_FLASH_PageSize 不对齐 */
else
{
/* NumByteToWrite < SPI_FLASH_PageSize */
if (NumOfPage == 0)
{
/*当前页剩余的count个位置比NumOfSingle小,写不完*/
if (NumOfSingle > count)
{
temp = NumOfSingle - count;
/*先写满当前页*/
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
WriteAddr += count;
pBuffer += count;
/*再写剩余的数据*/
SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);
}
else /*当前页剩余的count个位置能写完NumOfSingle个数据*/
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
}
}
else /* NumByteToWrite > SPI_FLASH_PageSize */
{
/*地址不对齐多出的count分开处理,不加入这个运算*/
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
WriteAddr += count;
pBuffer += count;
/*把整数页都写了*/
while (NumOfPage--)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
WriteAddr += SPI_FLASH_PageSize;
pBuffer += SPI_FLASH_PageSize;
}
/*若有多余的不满一页的数据,把它写完*/
if (NumOfSingle != 0)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
}
}
}
(5)从FLASH读取数据
相对于写入,FLASH 芯片的数据读取要简单得多,使用读取指令“Read Data”即可。
发送了指令编码及要读的起始地址后,FLASH 芯片就会按地址递增的方式返回存储矩 阵的内容,读取的数据量没有限制,只要没有停止通讯,FLASH 芯片就会一直返回数据。
/*
* @brief 读取FLASH数据
* @param pBuffer,存储读出数据的指针
* @param ReadAddr,读取地址
* @param NumByteToRead,读取数据长度
* @retval 无
*/
void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u32 NumByteToRead)
{
/* 选择FLASH: CS低电平 */
SPI_FLASH_CS_LOW();
/* 发送 读 指令 */
SPI_FLASH_SendByte(W25X_ReadData);
/* 发送 读 地址高8位 */
SPI_FLASH_SendByte((ReadAddr & 0xFF000000) >> 24);
/* 发送 读 地址中前8位 */
SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);
/* 发送 读 地址中后8位 */
SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);
/* 发送 读 地址低8位 */
SPI_FLASH_SendByte(ReadAddr & 0xFF);
/* 读取数据 */
while (NumByteToRead--)
{
/* 读取一个字节*/
*pBuffer = SPI_FLASH_SendByte(Dummy_Byte);
/* 指向下一个字节缓冲区 */
pBuffer++;
}
/* 停止信号 FLASH: CS 高电平 */
SPI_FLASH_CS_HIGH();
}
?六、FLASH存储小数和整数
需要注意的是,存储各种数据类型的时候,我们需要将不同的数据类型分在不同的扇区,不可以混着,主要原因是下面这个动态存储,因为不同的字节数存储的方式不一样,比如说,存储整数,我们将整数通过十六进制传入,占两个字节,当我们需要读取时,四个字节四个字节的读,即合为一个整数,若这个时候我们用浮点数的方式来运算,它就为八个字节八个字节的读,会出错。所以,当我们存储不管是浮点数还是整数,存储方式都一样,可是你想读出来的时候,你就应该区分他们之间的区别,不可以混为一谈,怎么读数据,还是取决于上位机的处理。
/*写入小数数据到第一页*/
SPI_FLASH_BufferWrite((void*)double_buffer, SPI_FLASH_PageSize*1, sizeof(double_buffer));
/*写入整数数据到第二页*/
SPI_FLASH_BufferWrite((void*)int_bufffer, SPI_FLASH_PageSize*2, sizeof(int_bufffer));
?SPI协议初步就学习到这啦,国庆也就结束了,好像任务量也没有完成很多,接下来也要忙比赛啦,希望自己再接再厉。
?
|