实验内容:使用硬件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);