【STM32Cube HAL】SPI(十)

  实验内容:使用硬件SPI读写串行FLASH(W25Q64) 。

一、原理图

二、 CubeMX配置

Step1.打开 STM32CubeMX,点击“New Project”,选择芯片型号,STM32F103VETx。

 Step2.选择时钟源,并配置时钟树。选择Crystal/Ceramic Resonator,并配置系统时钟为72M。

  

Step3.配置SYS,我们这里选择的是Serial Wire。(正常情况配置不配置不影响,debug可以使用。但是你不可以把这两个引脚用于其他复用功能,如果用于其他复用功能,debug就不起作用了。)

 

  Step4.串口配置(主要为了在串口调试助手显示读写数据),因为没有用到中断和DMA所以我们就不过多讲解。

 step5.SPI外设配置,这里选用的SPI1。因为没有使用到中断和DMA所以只需要把硬件SPI基本参数配置好就行。

 

step6.因为是采用软件NSS,所以还需要配置相应的IO口。

 到这里关于硬件IIC参数配置基本已经完成,只需要根据之前文章《STM32Cube HAL:GPIO输入/输出(一)》Step4-Step8,设置相关工程参数和生成代码。

三、添加功能代码

1、我们等会会向串口调试助手发送数据,进行实验结果的验证。 发送数据我们采用printf函数,所有需要重定向c库函数printf到串口。注意使用时需要在keil设置中勾选微库(use mircolib),同时需要添加头文件#include <stdio.h>。

//重定向c库函数printf到串口DEBUG_USART,重定向后可使用printf函数
int fputc(int ch, FILE *f)
{
	/* 发送一个字节数据到串口DEBUG_USART */
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);	
	
	return (ch);
}

//重定向c库函数scanf到串口DEBUG_USART,重写向后可使用scanf、getchar等函数
int fgetc(FILE *f)
{		
	int ch;
	HAL_UART_Receive(&huart1, (uint8_t *)&ch, 1, 1000);	
	return (ch);
}

2、在gpio.h文件中添加相关的宏定义。因为是使用软件控制NSS,下面宏定义通过IO口模拟NSS信号。(NSS低电平控制,NSS高电平释放)

#define NSS_HIGH() HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_SET)
#define NSS_LOW()  HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_RESET)

3、在spi.h添加宏定义,和函数的声明。方便移植和提高可读性。

#define FLASH_PAGESIZE	 256			  //W25Q64的页面大小 
#define _Flash_ID	0xEF4017
extern SPI_HandleTypeDef hspi1;
void MX_SPI1_Init(void);
uint32_t SPI_FLASH_ReadID(void);
void SPI_FLASH_WriteEnable(void);
void SPI_FLASH_WaitForWriteEnd(void);
void SPI_FLASH_SectorErase(uint32_t SectorAddr);
void SPI_FLASH_PageWrite(uint8_t * pBuffer, uint32_t WriteAddr,uint16_t NumByteToWrite);
void SPI_FLASH_BufferWrite(uint8_t * pBuffer, uint32_t WriteAddr,uint16_t NumByteToWrite);
void SPI_FLASH_BufferRead(uint8_t * pBuffer, uint32_t  ReadAddr,uint16_t NumByteToWrite);

 然后在spi.c中实现声明函数的具体功能。

/*读取制造商和设备ID*/
uint32_t SPI_FLASH_ReadID(void)
{
	uint8_t W25X_JEDEC_ID=0X9F;
	uint8_t temp0[3];
	uint32_t temp;
	
	NSS_LOW();//选择FLASH:NSS低电平
	
	HAL_SPI_Transmit(&hspi1,&W25X_JEDEC_ID,1,10);//发送指令
	
	HAL_SPI_Receive(&hspi1,temp0,3, 10);//读取制造商和设备ID
	
	NSS_HIGH();//停止信号 FLASH:NSS高电平
	
	temp=(temp0[0])<<16|(temp0[1]<<8)|temp0[2];//把数据组合起来,作为函数的返回值

	return temp;
}

/*写使能*/
void SPI_FLASH_WriteEnable(void)
{
	uint8_t W25X_WriteEnable=0x06;
	
	NSS_LOW();//选择FLASH:NSS低电平
	
	HAL_SPI_Transmit(&hspi1,&W25X_WriteEnable,1,10);//发送指令
	
	NSS_HIGH();//停止信号 FLASH:NSS高电平
}

/*扇区擦除*/
void SPI_FLASH_SectorErase(uint32_t SectorAddr)
{
	uint8_t W25X_SectorErase=0x20;
	uint8_t temp1,temp2,temp3;
	
	SPI_FLASH_WriteEnable();//写使能

	NSS_LOW();//选择FLASH:NSS低电平
	
	HAL_SPI_Transmit(&hspi1,&W25X_SectorErase,1,10);//发送指令
	
	temp1=(SectorAddr&(0xFF0000))>>16;//发送地址
	HAL_SPI_Transmit(&hspi1,&temp1,1,10);
	temp2=(SectorAddr&(0x00FF00))>>8;
	HAL_SPI_Transmit(&hspi1,&temp2,1,10);
	temp3=(SectorAddr&(0x0000FF));
	HAL_SPI_Transmit(&hspi1,&temp3,1,10);
	NSS_HIGH();//停止信号 FLASH:NSS高电平
	
	SPI_FLASH_WaitForWriteEnd();//等待擦除完毕
}

/*等待 WIP(BUSY) 标志被置 0,即等待到 FLASH 内部数据写入完毕*/
void SPI_FLASH_WaitForWriteEnd(void)
{
	uint8_t W25X_ReadStatusReg=0x05;
	uint8_t temp;
	
	NSS_LOW();//选择FLASH:NSS低电平
	
	HAL_SPI_Transmit(&hspi1,&W25X_ReadStatusReg,1,10);//发送指令
	do
	{
		HAL_SPI_Receive(&hspi1,&temp,1,10);// 读取 FLASH 芯片的状态寄存器
	}
	while((temp&0x01)==1);
	
	NSS_HIGH();//停止信号 FLASH:NSS高电平
}

/*在FLASH的一个写循环中可以写多个字节,但一次写入
的字节数不能超过FLASH页的大小,W25Q64每页有256个字节*/
void SPI_FLASH_PageWrite(uint8_t * pBuffer, uint32_t WriteAddr,uint16_t NumByteToWrite)
{
	uint8_t W25X_PageProgram=0x02;
	uint8_t temp1,temp2,temp3;
	
	SPI_FLASH_WriteEnable();//写使能
	
	NSS_LOW();//选择FLASH:NSS低电平
	
	HAL_SPI_Transmit(&hspi1,&W25X_PageProgram,1,10);//发送指令
	
	temp1=(WriteAddr&(0xFF0000))>>16;//发送地址
	HAL_SPI_Transmit(&hspi1,&temp1,1,10);
	
	temp2=(WriteAddr&(0x00FF00))>>8;
	HAL_SPI_Transmit(&hspi1,&temp2,1,10);
	
	temp3=(WriteAddr&(0x0000FF));
	HAL_SPI_Transmit(&hspi1,&temp3,1,10);
	
	HAL_SPI_Transmit(&hspi1,pBuffer,NumByteToWrite,10);//发送数据
	
	NSS_HIGH();//停止信号 FLASH:NSS高电平
	
	SPI_FLASH_WaitForWriteEnd();//等待写入完毕
}

/*不定量数据写入*/
void SPI_FLASH_BufferWrite(uint8_t * pBuffer, uint32_t WriteAddr,uint16_t NumByteToWrite)
{
  uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;

  Addr = WriteAddr % FLASH_PAGESIZE;//判断写入的首地址是否与EEPROM页的首地址对齐,0为对齐
  count = FLASH_PAGESIZE - Addr;//计算从写入的首地址需要写多少数据才能填满当前页
  NumOfPage =  NumByteToWrite / FLASH_PAGESIZE;//计算写入数据需要写几个完整页(地址对齐的情况)
  NumOfSingle = NumByteToWrite % FLASH_PAGESIZE;//计算写完完整页剩下的数据个数(地址对齐的情况)
 
  if(Addr == 0) //判断写入的首地址是否与页地址对齐
  {
    if(NumOfPage == 0) //如果页对齐,判断数据是否不满一页
    {
      SPI_FLASH_PageWrite(pBuffer,WriteAddr,NumOfSingle);//如果不满一页,直接写入数据
    }
    else  //在数据满一页的情况下,通过地址自增方式,循环写入数据(页写入的形式)
    {
      while(NumOfPage--)//循环写入数据:先写入完整页
      {
				SPI_FLASH_PageWrite(pBuffer, WriteAddr, FLASH_PAGESIZE); 
        WriteAddr +=  FLASH_PAGESIZE;
        pBuffer += FLASH_PAGESIZE;
      }

      if(NumOfSingle!=0)//循环写入数据:再写入不满一页的数据
      {
				SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle); 
      }
    }
  }
  else 
  {
    if(NumOfPage== 0) //如果页不对齐,判断数据是否不满一页
    {
			if(NumOfSingle<=count)//如果不满一页,判断数据是否跨页
			{	 
				SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);//如果不跨页,直接写入数据
			}
			else
			{
				SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);//如果跨页,先写首页数据,再写次页数据
				SPI_FLASH_PageWrite(pBuffer+count, WriteAddr+count, NumOfSingle-count);
			}
    }
    
    else
    {
			/*如果数据满一页,对数据进行分离*/
      NumByteToWrite -= count;//扣除第一页数据个数
      NumOfPage =  NumByteToWrite / FLASH_PAGESIZE;//计算写入数据需要写几个完整页
      NumOfSingle = NumByteToWrite % FLASH_PAGESIZE;	//计算写完完整页剩下的数据个数
      
      if(count != 0)
      {  
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);//写入首页数据
        WriteAddr += count;//写地址自增
        pBuffer += count;//缓冲区指针自增
      } 
      
      while(NumOfPage--)//依次写入完整页的数据
      {
				SPI_FLASH_PageWrite(pBuffer, WriteAddr, FLASH_PAGESIZE);
        WriteAddr +=  FLASH_PAGESIZE;//写地址自增
        pBuffer += FLASH_PAGESIZE; //缓冲区指针自增 
      }
      if(NumOfSingle != 0)//判断最后一页的数据是否是填满完整一页的
      {
				SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);//写入最后一页的数据
      }
    }
  }  
}

/*读取FLASH数据,读取的数据量没有限制*/
void SPI_FLASH_BufferRead(uint8_t * pBuffer, uint32_t  ReadAddr,uint16_t NumByteToWrite)
{
	uint8_t W25X_ReadData=0x03;
	uint8_t temp1,temp2,temp3;
	
	SPI_FLASH_WriteEnable();//写使能
	
	NSS_LOW();//选择FLASH:NSS低电平
	
	HAL_SPI_Transmit(&hspi1,&W25X_ReadData,1,10);//发送指令
	
	temp1=(ReadAddr&(0xFF0000))>>16;//发送地址
	HAL_SPI_Transmit(&hspi1,&temp1,1,10);
	
	temp2=(ReadAddr&(0x00FF00))>>8;
	HAL_SPI_Transmit(&hspi1,&temp2,1,10);
	
	temp3=(ReadAddr&(0x0000FF));
	HAL_SPI_Transmit(&hspi1,&temp3,1,10);
	
	HAL_SPI_Receive(&hspi1,pBuffer,NumByteToWrite, 10);//这里超时时间不能设置为1,否则会读取错误
	
	NSS_HIGH();//停止信号 FLASH:NSS高电平
}

 3、最后在main.c文件中,编写测试代码进行验证。

/*测试代码涉及到变量,宏定义*/
/* 获取缓冲区的长度 */

/*sizeof():数组占用字节除以数组类型所占字节,结果为数组元素个数*/
/*使用方法:sizeof(数组名)/ sizeof(数组类型名) */
#define countof(a)      (sizeof(a) / sizeof(*(a)))
#define  BufferSize (countof(Tx_Buffer)-1)

#define  FLASH_WriteAddress     0x000006
#define  FLASH_ReadAddress      FLASH_WriteAddress
#define  FLASH_SectorToErase    FLASH_WriteAddress

uint8_t Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2, uint16_t BufferLength);

uint32_t Flash_ID;
uint8_t CMP_RES;
uint8_t Tx_Buffer[] = 
"采薇采薇,薇亦作止。曰归曰归,岁亦莫止。靡室靡家,猃狁之故。不遑启居,猃狁之故。\
采薇采薇,薇亦柔止。曰归曰归,心亦忧止。忧心烈烈,载饥载渴。我戍未定,靡使归聘。\
采薇采薇,薇亦刚止。曰归曰归,岁亦阳止。王事靡盬,不遑启处。忧心孔疚,我行不来!\
彼尔维何?维常之华。彼路斯何?君子之车。戎车既驾,四牡业业。岂敢定居?一月三捷。\
驾彼四牡,四牡骙骙。君子所依,小人所腓。四牡翼翼,象弭鱼服。岂不日戒?猃狁孔棘!\
昔我往矣,杨柳依依。今我来思,雨雪霏霏。行道迟迟,载渴载饥。我心伤悲,莫知我哀!";
uint8_t Rx_Buffer[BufferSize];
/*比较函数,用于比较写入和读取的数据是否一致*/
uint8_t Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2, uint16_t BufferLength)
{
  while(BufferLength--)
  {
    if(*pBuffer1 != *pBuffer2)
    {
      return 0;
    }

    pBuffer1++;
    pBuffer2++;
  }
  return 1;
}
/*在主函数编写测试代码:ID验证,读写比较*/
Flash_ID=SPI_FLASH_ReadID();//读取Flash ID
	if (Flash_ID == _Flash_ID) //判断读取的ID和数据手册的ID是否一致
	{	
		printf("\r\n系统检测到SPI FLASH W25Q64 ! FlashID:0x%X\r\n",Flash_ID);
		
		/* 擦除将要写入的 SPI FLASH 扇区,FLASH写入前要先擦除 */
		SPI_FLASH_SectorErase(FLASH_SectorToErase);	 	 
		
		/* 将发送缓冲区的数据写到flash中 *///
		SPI_FLASH_BufferWrite(Tx_Buffer, FLASH_WriteAddress, BufferSize);
		printf("\r\n写入的数据为:\r\n%s", Tx_Buffer);
		
		/* 将刚刚写入的数据读出来放到接收缓冲区中 */
		SPI_FLASH_BufferRead(Rx_Buffer, FLASH_ReadAddress, BufferSize);
		printf("\r\n读出的数据为:\r\n%s", Rx_Buffer);
		
		/* 检查写入的数据与读出的数据是否相等 */
		CMP_RES= Buffercmp(Tx_Buffer, Rx_Buffer, BufferSize);
		
		if(CMP_RES)
		{    
			printf("\r\n16M串行flash(W25Q64)测试成功!\n\r");
		}
		else
		{        
			printf("\r\n16M串行flash(W25Q64)测试失败!\n\r");
		}
	}// if (FlashID == sFLASH_ID)
	else
	{    
		printf("\r\n获取不到 W25Q64 ID!\n\r");
	}			

调试过程碰到几个问题,也是比较无脑的。
1、编写页写入的时候,常规流程:写使能->NSS拉低->发送页编程指令->发送地址->写入数据。
我忘记写发送页编程指令,导致了每次读取的时候,数据前面总是多出一些空白。(数据内容:
空格(发送内容越多,空格越多)+数据)。刚开始在论坛看到了,一个网友也出现类似情况,评论区,都是说等待函数加个延时就可以了,试了下还是不行,后来才发现是指令忘记写了。

HAL_SPI_Transmit(&hspi1,&W25X_PageProgram,1,10);//发送指令

2、还出现一个问题,读取大量数据的时候(超过一页),后面的内容总是读不全。刚开始以为是自己本身写入就有问题,后来用了例程读取我写入的数据,可以正常读取。基本可以锁定是读取函数的问题,看了一下整个读取的流程没有问题,唯一需要更改就是库函数自带的接收函数的参数:超时时间的设置。果然把原先设置的1改成10就可以正常读写了。(因为这个原因,我索性把发送的超时时间也都设置为10,直接设置为1也会正常的就是了。)

HAL_SPI_Receive(&hspi1,pBuffer,NumByteToWrite, 10);

猜你喜欢

转载自blog.csdn.net/qq_29031103/article/details/119894140