STM32F429入门(二十一):SPI协议及SPI读写FLASH

2023-05-16

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协议初步就学习到这啦,国庆也就结束了,好像任务量也没有完成很多,接下来也要忙比赛啦,希望自己再接再厉。

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

STM32F429入门(二十一):SPI协议及SPI读写FLASH 的相关文章

随机推荐

  • 关于单片机内存详解

    引言 xff1a 内存是单片机的重要组成部分 xff0c 那么如何操作 分配内存对于一个嵌入式软件工程师来说便是极为重要的 一 存储器的定义 xff1a 存储器单元实际上是时序逻辑电路 的一种 按存储器的使用类型可分为只读存储器 xff08
  • Keil编译报错--IAP\IAP.sct(7): error: L6236E: No section matches selector - no section to be FIRST/LAST.

    之前在用CUBE生成文件时发生了这样的报错 xff1a IAP IAP sct 7 error L6236E No section matches selector no section to be FIRST LAST 发生这样的报错是因
  • Keil编程环境背景颜色--护眼色

    在Edit中选择Configuration 选择Colors amp Fonts 点击下载那个按钮 按照这个参数输入自定义颜色并添加 xff0c 就保存为一个护眼的绿色啦
  • win10下MissionPlanner地面站的安装

    win10下MissionPlanner地面站的安装 编辑器 xff1a Viaual Studio2019社区版 安装时工作负荷和单个组件的选择如下图所示 xff0c 然后自定义安装位置进行安装 安装完成后启动MP地面站 启动Visual
  • git fatal: The remote end hung up unexpectedly 错误

    使用git将本地项目添加到远程仓库报以下错误 git push u origin master Counting objects 2053 done Delta compression using up to 2 threads Compr
  • git submodule update --init --recursive

    转自https blog csdn net wangjia55 article details 24400501 转自http webfrogs me 2013 03 20 git submodule 开发过程中 xff0c 经常会有一些通
  • C++简介( C++ Primer Plus)

    C 43 43 历史 xff1a 1980年 xff0c 贝尔实验室的 Bjarne Stroustrup 本贾尼 斯特劳斯 开始对C进行改进和扩充 1983年正式命名为C 43 43 支持3钟不同的程序设计 过程化程序设计 数据 43 算
  • 树莓派3B+ 引脚图说明

    如上图所示 xff0c 我们可以很清楚的看到各个引脚的功能 例如我们想使用pwm引脚来控制舵机 xff0c 则我们可以考虑使用其中的 BCM18 PWM0 和 BCM13 PWM1 在使用wiringPi库时 xff0c 我们定义的引脚即B
  • 树莓派3B+ 串口使用大全(实现串口通信功能)

    1 树莓派串口控制台功能 在2018 10 09 raspbian stretch img镜像中 xff0c 要使用串口来调试设备 xff0c 需要修改conig txt文件 1 sudo systemctl disable hciuart
  • Java学习笔记(三)函数——学习MOOC网翁恺老师课程记录

    七 函数 7 1 函数定义与调用 Java的函数必须定义在类的内部 xff0c 成为类的成员 定义一个函数 xff0c 要像这样写 xff1a lt 返回类型 gt lt 方法名称 gt lt 参数表 gt lt 方法体 gt 返回类型是这
  • STL笔试面试题总结(干货)

    STL笔试面试题总结 一 STL有哪些组件 STL提供六大组件彼此此可以组合套用 1 容器 容器就是各种数据结构 我就不多说 看看下面这张图回忆一下就好了 从实现角度看 STL容器是一种class template 2 算法 各种常见算法
  • Framebuffer 机制【转】

    本文转载自 xff1a http blog csdn net paul liao article details 7706477 Framebuffer Framebuffer是Linux系统为显示设备提供的一个接口 xff0c 它将显示缓
  • 单片机——蜂鸣器

    1 蜂鸣器 2 所用元件 2n5771 at89c51 button cap cap elec crystal res speaker 例图 xff1a 例图代码 xff1a include lt REGX51 H gt sbit BEEP
  • Linux获取机器码

    1 准备工作 安装php xff0c 并已经配置好环境变量path 2 运行hardware sh获取机器码 shell gt php span class token punctuation span span class token o
  • Windows远程桌面卡顿问题(包含网络调优)

    注 xff1a 以下操作需管理员权限执行CMD 关闭自动调节 xff1a netsh interface tcp span class token function set span global autotuninglevel 61 di
  • ESXI VIB升级报错

    一 兼容性问题 1 通过VIB升级ESXI时 xff0c 可能会出现类似报错 span class token namespace DependencyError span VIB LSI bootbank scsi mpt3sas 04
  • MySQL 8.0安装

    1 安装MySQL 8 0 Server shell gt dnf span class token operator span y install 64 mysql 2 开启服务 shell gt systemctl span class
  • 华为镜像启动报错

    shell gt span class token function rm span span class token operator span etc span class token operator span udev span c
  • ThinkPad T14s 安装Ubuntu22踩坑记

    讲一个我装机历经的一个小故事 首先 xff0c 花个万把块 xff0c 买个心仪的撸码神奇 xff0c 我买的是2022款ThinkPad T14s 官网关注了好久就是不出32G内存版本的 xff0c 无奈只能买一个16G内存版本的 xff
  • STM32F429入门(二十一):SPI协议及SPI读写FLASH

    IIC主要用于通讯速率一般的场合 xff0c 而SPI一般用于较高速的场合 一 SPI协议简介 SPI 协议是由摩托罗拉公司提出的通讯协议 Serial Peripheral Interface xff0c 即串行外围设 备接口 xff0c