SPI读写FLASH原理和完整代码详解

引言

       实现SPI通讯,对FLASH进行读写。读取FLASH的ID信息,写入数据,并读取出来进行校验,通过串口打印写入与读取出来的数据,输出测试结果。

一、SPI总线

SPI通信的基础知识

       SPI是串行外设接口(Serial Peripheral Interface),即串行外围设备接口,是一种高速全双工的通信总线,正是出于这种简单易用的特性,如今越来越多的芯片集成了这种通信协议,最大SPI速度可达到18MHz 。

        通常SPI通过4个管脚与外部器件相连:

       MISO:主设备输入/从设备输出管脚。

       MOSI:主设备输出/从设备输入管脚。

       SCK:串口时钟,作为主设备的输出,从设备的输入。

       NSS:从设备选择。这是一个可选的管脚,用来选择主/从设备。它的功能是用来作为“片选管脚”,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。

时钟信号的相位和极性

       SPI_CR寄存器的CPOL和CPHA位,能够组合成四种可能的时序关系,其中使用的最为广泛的是SPI0和SPI3方式。

      SPI模块为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟极性和相位可以进行配置,时钟极性(CPOL)对传输协议没有重大的影响。如果CPOL=0,串行同步时钟的空闲状态为低电平;如果CPOL=1,串行同步时钟的空闲状态为高电平。时钟相位(CPHA)能够配置用于选择两种不同的传输协议之一进行数据传输。如果CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;如果CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。SPI主模块和与之通信的外设音时钟相位和极性应该一致。

这些时序体现了SPI_CR1寄存器的LSBFIRST被重置(置0)时的情况,即MSB模式。 

SPI模式

CPOL

CPHA

空闲时SCK时钟

采样时刻

0

0

0

低电平

奇数边沿

1

0

1

低电平

偶数边沿

2

1

0

高电平

奇数边沿

3

1

1

高电平

偶数边沿

SPI主模式配置

        在主配置时,串行时钟在SCK脚产生,学会使用寄存器,了解底层逻辑(用库函数开发也会更加明了),配置步骤如下:

        1、通过SPI_CR1寄存器的BR[2:0]位定义串行时钟波特率。

SPI控制寄存器 1(SPI_CR1)

        2、选择CPOL和CPHA位,定义数据传输和串行时钟间的相位关系。

       3、设置DFF位来定义8或16位数据帧格式。 

       4、配置SPI_CR1寄存器的LSBFIRST位定义帧格式。

        5、如果NSS引脚需要工作在输入模式,硬件模式中在整个数据帧传输期间应把NSS脚连接到高 电平;在软件模式中,需设置SPI_CR1寄存器的SSM和SSI位。

       6、必须设置MSTR和SPE位(只当NSS脚被连到高电平,这些位才能保持置位)。在这个配置中,MOSI脚是数据输出,而MISO脚是数据输入。

 

举例:

void SPI1_Init(void)
{	 
    RCC->APB2ENR|=1<<2;  	    //PORTA时钟使能 	 
    RCC->APB2ENR|=1<<12;   	    //SPI1时钟使能 
    GPIOA->CRL&=0X000FFFFF;     //PA5/6/7清零
    GPIOA->CRL|=0XBBB00000;	    //PA5/6/7复用
    GPIOA->ODR|=0X7<<5;         //PA5.6.7上拉   
    GPIOA->CRL|=0X000000300;    //PA2 SPI主设备时输出
    SPI1->CR1|=0<<10;		    //全双工模式	
    SPI1->CR1|=1<<9; 		    //软件NSS管理
    SPI1->CR1|=1<<8;  
    SPI1->CR1|=1<<2; 		    //SPI主机
    SPI1->CR1|=0<<11;		    //8bit数据格式	
    SPI1->CR1|=1<<1; 		    //空闲模式下SCK为1 CPOL=1
    SPI1->CR1|=1<<0; 		    //数据采样从第二个时间边沿开始,CPHA=1  
    SPI1->CR1|=2<<5; 		    //Fpclk1/8
    SPI1->CR1|=0<<7; 		    //先发送MSB   
    SPI1->CR1|=1<<6; 		    //SPI设备使能
}

数据发送过程 

       当一字节写进发送缓冲器时,发送过程开始。在发送第一个数据位时,数据字被并行地(通过内部总线)传入移位寄存器,而后串行地移出到MOSI脚上;MSB在先还是LSB在先,取决于SPI_CR1寄存器中的LSBFIRST位。如果设置SPI_CR2寄存器中的TXEIE位,将产生中断。

SPI控制寄存器 2(SPI_CR2)

 

数据接收过程

        对于接收器来说,当发送完一帧数据的时候,“状态寄存器SR”中的“TXE标志位”会被置1,表示传输完一帧,发送缓冲区已空;类似地,当接收完一帧数据的时候,“RXNE标志位”会被置1,表示传输完一帧,接收缓冲区非空;读SPI_DR寄存器时,将清除RXNE位,SPI设备返回接收到的数据。

SPI状态寄存器(SPI_SR)

 

代码:
//SPI1 读写一个字节
//TxData:要写入的字节
//返回值:读取到的字节
uint8_t SPI_FLASH_SendByte(uint8_ TxData)
{
    uint16_t  retry=0;
    while((SPI2->SR&1<<1)==0)       //等待发送区空	
    {
        retry++;
        if(retry>=0XFFFE)return 0; 	//超时退出
    }
    SPI1->DR=TxData;	 	  	    //发送一个byte 
    retry=0;
    while((SPI2->SR&1<<0)==0)       //等待接收完一个byte  
    {
        retry++;
        if(retry>=0XFFFE)return 0;	//超时退出
    }
    return SPI2->DR;                //返回收到的数据	
}

二、FLASH控制

        本文使用的FLASH与SPI总线的硬件连接如下图所示。

        软件方面FLASH定义了很多的指令,确定通讯协议模块,通过协议收发命令、数据,进而驱动设备,因此首先需要了解芯片时序,通过熟悉底层逻辑从而编写驱动,也能为日后学习打下坚实的基础。下面仔细罗列为FLASH其中读取厂商ID的驱动实现步骤,其他驱动见文后代码清单。

 FLASH指令集

 读取Device ID时序图

      通过结合指令中厂商ID所需指令和时序图,拉低片选,使能FLASH,利用用户函数SPI_FLASH_SendByte()来向FLASH发送第一个命令字节编码:“W25X_DeviceID”(自定义的宏:0xAB);根据指令表,发送完这个指令后,后面紧跟着三个字节的“Dummy byte”,可以自定义任意值,这里定义为“0xFF”,根据时序,在第5个字节,FLASH通过SO端口输出它的器件ID,我们调用函数SPI_FLASH_SendByte()接收返回的数据,并赋值给Temp变量。SPI_FLASH_ReadDeviceID()函数的返回值即为读取得的器件 ID。把片选信号拉高,结束通讯。这就完成了读FLASH 厂商ID。

代码:

uint32_t SPI_FLASH_ReadDeviceID(void) 
{
    uint32_t Temp=0; 
    SPI_FLASH_CS_LOW(); 
    SPI_FLASH_SendByte(W25X_DeviceID);       //发送读取指令
    SPI_FLASH_SendByte(Dummy_Byte);          //三个Dummy_Byte
    SPI_FLASH_SendByte(Dummy_Byte); 
    SPI_FLASH_SendByte(Dummy_Byte); 
    Temp = SPI_FLASH_SendByte(Dummy_Byte);   //接收返回值
    SPI_FLASH_CS_HIGH(); 
    return Temp;
}

        编译调试后获得厂商ID,与下表进行对比,确定芯片选择正确,W25Q16厂商ID为0x14。

制造厂商和设备ID

 三、代码编写

main.c

#define  FLASH_WriteAddress     0x00000
#define  FLASH_ReadAddress      FLASH_WriteAddress
#define  FLASH_SectorToErase    FLASH_WriteAddress


//#define countof(a)    (sizeof(a) / sizeof(*(a)))
//#define  BufferSize (countof(Tx_Buffer)-1)
#define  BufferSize 255

uint8_t Tx_Buffer[255]={0};
//uint8_t Tx_Buffer[] = "士不可以不弘毅,任重而道远\r\n";
uint8_t Rx_Buffer[BufferSize];


int main()
{
    uint16_t DeviceID;
   	uint16_t FlashID;
    int cnt=0;
	
	USART1_GPIO_Config(); 
    USART1_Config();
	
	SPI_FLASH_Init();

	printf("\r\n 这是一个 2M 串行 flash(W25X16)实验 \r\n");
						
	/* 获得 Flash Device ID */
    DeviceID = SPI_FLASH_ReadDeviceID();		
	DelayMS(200);
	
	/* 获得 Flash ID */
    FlashID = SPI_FLASH_ReadID();
	printf("\r\n FlashID is 0x%X, Manufacturer Device ID is 0x%X\r\n", FlashID, DeviceID);
	 
    /* 擦除Flash数据 */
	SPI_FLASH_BulkErase();
    SPI_FLASH_SectorErase(0);
    printf("\r\n擦除完毕\r\n");

    /* 将发送缓冲区的数据写到 flash 中 */ 
    SPI_FLASH_Buffer_Write(Tx_Buffer, FLASH_WriteAddress, BufferSize);
   { 
	      printf("\r\n 写入的数据为: \r\t");
	      for(cnt=0;cnt<BufferSize;cnt++)
	      {
		       Tx_Buffer [cnt]=cnt;
	           printf(" 0x%02X   ", Tx_Buffer[cnt]);
	       }
		}
    //printf("\r\n 写入的数据为:%d \r\t", Tx_Buffer[cnt]);	
    /* 将写入的数据读出来放到接收缓冲区 */
	SPI_FLASH_Buffer_Read(Rx_Buffer, FLASH_ReadAddress, BufferSize);
    {
      	  printf("\r\n 读出的数据为: \r\t");
	      for(cnt=0;cnt<BufferSize;cnt++)
		 {
		        Rx_Buffer [cnt]=cnt;
			    printf(" 0x%02X   ", Rx_Buffer[cnt]);
	       }
     }
	 //printf("\r\n 读出的数据为:%d \r\n", Rx_Buffer[cnt]);

     printf("\r\n 2M串行flash(W25Q16)测试成功!\n\r");
     SPI_Flash_PowerDown();

     while(1)
	 {
		    
	 } 

}		
	
spi.c

uint32_t time_out = SPI_FLASH_WAIT_TIMEOUT;
static  uint16_t SPI_TIMEOUT_UserCallback(uint8_t errorCode);

void SPI_FLASH_Init(void)
{
     SPI_InitTypeDef SPI_InitStructure;
     GPIO_InitTypeDef GPIO_InitStructure;
     
     /* 使能SPI1 和 GPI时钟 */
     RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
     RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
     
     /* SCK */
     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
     GPIO_Init(GPIOA, &GPIO_InitStructure);
     
     /* MISO */
     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
     GPIO_Init(GPIOA, &GPIO_InitStructure);
     
     /* MOSI */
     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
     GPIO_Init(GPIOA, &GPIO_InitStructure);
     
     /* CS */
     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
     GPIO_Init(GPIOA, &GPIO_InitStructure);
     
     SPI_FLASH_CS_HIGH();

     /* SPI1配置 */
     SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
     SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
     SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
     SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
     SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
     SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
     SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
     SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
     SPI_InitStructure.SPI_CRCPolynomial = 7;
     SPI_Init(SPI1, &SPI_InitStructure);
     
     SPI_Cmd(SPI1, ENABLE);
}


//发送一个字节(这里原理同前文寄存器编写代码)
 uint8_t SPI_FLASH_SendByte(uint8_t byte) 
{ 
     uint8_t read_temp;	
	 time_out = SPI_FLASH_WAIT_TIMEOUT;
	 
     while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) 
	 {                                                                                          
		  if(time_out-- == 0)        //超时
		  {
			   return SPI_TIMEOUT_UserCallback(1);
		   }
	  }
    
    SPI_I2S_SendData(SPI1, byte); 
    time_out = SPI_FLASH_WAIT_TIMEOUT;
    /* Wait to receive a byte */
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET) 
    {
		  if(time_out-- == 0)
		  {
			   return SPI_TIMEOUT_UserCallback(2);
		  }
	 }	
    read_temp = (uint8_t)SPI_I2S_ReceiveData (SPI1);	
    return read_temp; 
 }


 
 //接收一个字节
uint8_t SPI_FLASH_Receive_Byte(void)
{
	return SPI_FLASH_SendByte(0xFF);
}
 

 //出错时调用这个函数
static  uint16_t SPI_TIMEOUT_UserCallback(uint8_t errorCode)
{
	printf("\r\nSPI 检测超时,错误代码:%d",errorCode);
	return 0;
}
 
flash.h
#ifndef __FLASH_H
#define __FLASH_H		

#include "sys.h" 

#define W25Q16 	0XEF14
#define FLASH_ID 0XEF14

#define	SPI_FLASH_CS PAout(2)
#define SPI_FLASH_CS_HIGH()  GPIO_SetBits(GPIOA, GPIO_Pin_2)		
#define SPI_FLASH_CS_LOW()    GPIO_ResetBits(GPIOA, GPIO_Pin_2)
#define SPI_FLASH_PageSize         256
#define SPI_FLASH_PerWritePageSize  256
#define  SPI_FLASH_WAIT_TIMEOUT  10000
 
//指令表
#define W25X_WriteEnable		0x06 
#define W25X_WriteDisable		0x04 
#define W25X_ReadStatusReg		0x05 
#define W25X_WriteStatusReg		0x01 
#define W25X_ReadData			0x03 
#define W25X_FastReadData		0x0B 
#define W25X_FastReadDual		0x3B 
#define W25X_PageProgram		0x02 
#define W25X_BlockErase			0xD8 
#define W25X_SectorErase		0x20 
#define W25X_ChipErase			0xC7 
#define W25X_PowerDown			0xB9 
#define W25X_ReleasePowerDown	0xAB 
#define W25X_DeviceID			0xAB 
#define W25X_ManufactDeviceID	0x90 
#define W25X_JedecDeviceID		0x9F 

#define Dummy_Byte 0xFF

uint32_t SPI_FLASH_ReadDeviceID(void);
uint8_t SPI_FLASH_SendByte(uint8_t byte);
uint8_t SPI_FLASH_Receive_Byte(void);
uint32_t SPI_FLASH_ReadID(void);
void SPI_FLASH_SectorErase(uint32_t SectorAddr);
void SPI_FLASH_BulkErase(void);
void SPI_FLASH_WriteEnable(void);
void SPI_FLASH_WaitForWriteEnd(void);
void SPI_FLASH_Page_Write(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite);
void SPI_FLASH_Buffer_Read(uint8_t* pBuffer, uint8_t ReadAddr, uint8_t NumByteToRead);	
void SPI_FLASH_Buffer_Write(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite);
void SPI_Flash_PowerDown(void);

#endif   /* __FLASH_H */
usart.c
void USART1_GPIO_Config()                                                /* GPIO配置 */
{		
    GPIO_InitTypeDef GPIO_InitStruct;                                    /* 结构体定义 */

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);               /* USART1时钟 */ 
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);                /* GPIOA使能 */
    GPIO_InitStruct.GPIO_Pin=GPIO_Pin_9;                                 /* PA2引脚 ,USART的TX*/
    GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF_PP;                           /* 复用推挽输出 */
    GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;                         /* 50MHz*/
    GPIO_Init(GPIOA,&GPIO_InitStruct);                                   /* 初始化GPIOA */
	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_10;                                /* PA3引脚 ,USART的RX*/
    GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IN_FLOATING;                     /* 浮空输入,用来接收数据 */
	GPIO_Init(GPIOA,&GPIO_InitStruct);                                   /* 初始化GPIOA */
}

void USART1_Config()
{
    USART_InitTypeDef USART_InitStructure;                                 /* 结构体定义 */
	  
    USART_InitStructure.USART_BaudRate = 115200;                           /* 配置波特率 */
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;            /* 8位数据 */
    USART_InitStructure.USART_StopBits = USART_StopBits_1;                 /* 1位停止位 */
    USART_InitStructure.USART_Parity = USART_Parity_No ;                   /* 无奇偶校验 */
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;/* 无硬件流控制 */
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;        /* 允许接收和发送 */
	
    USART_Init(USART1, &USART_InitStructure);                              /* 完成串口初始化 */
	
    USART_Cmd(USART1, ENABLE);                                             /* 串口使能 */
	 
}


//重定义printf函数
int fputc(int ch, FILE *f)
{
    /* 将Printf内容发往串口 */
    USART_SendData(USART1, (unsigned char)ch);
    while (!USART_GetFlagStatus(USART1, USART_FLAG_TC));
    USART_ClearFlag(USART1, USART_FLAG_TC);
    return (ch);

}


int fgetc(FILE *f)
{
    /* 等待串口输入数据 */
    while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
    return (int)USART_ReceiveData(USART1);
}

四、运行结果

通过编译调试后,使用串口监听运行结果如下:

五、结论

           当SPI时钟频率较高时,建议采用DMA模式以避免SPI速度性能的降低,且SPI有一个缺点:有指定的流控制,没有应答机制确认是否接收到了数据,不像I2C没传输数据会有一个ACK应答机制,学习时序弄清底层逻辑尤为重要,不可得过且过。

 

 

 

物联沃分享整理
物联沃-IOTWORD物联网 » SPI读写FLASH原理和完整代码详解

发表评论