SPI—读写串行FLASH实验

本文详细介绍了如何在STM32平台上通过SPI协议控制FLASH存储器,包括选择硬件模块、初始化、设备指令的理解、以及读写操作的步骤,涉及了SPI协议、NORFLASH芯片W25Q64的使用和示例代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

实际上,编写设备驱动都是有一定的规律可循的。首先我们要确定设备使用的是什么通讯协议。如上一章的EEPROM 使用的是I2C,本章的FLASH 使用的是SPI。那么我们就先根据它的通讯协议,选择好STM32 的硬件模块,并进行相应的I2C 或SPI 模块初始化。接着,我们要了解目标设备的相关指令,因为不同的设备,都会有相应的不同的指令。如EEPROM 中会把第一个数据解释为内部存储矩阵的地址(实质就是指令)。而FLASH 则定义了更多的指令,有写指令,读指令,读ID 指令等等。最后,我们根据这些指令的格式要求,使用通讯协议向设备发送指令,达到控制设备的目标。

本章参考资料:《STM32F10X-中文参考手册》SPI 章节及《SPI 总线协议介绍》。
若对SPI 通讯协议不了解,可先阅读《SPI 总线协议介绍》文档的内容学习。
关于FLASH 存储器,请参考“常用存储器介绍”章节,实验中FLASH 芯片的具体参数,请参考
其规格书《W25Q64》来了解。

1 SPI协议简介

SPI 协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设备接口,是一种高速全双工的通信总线。它被广泛地使用在ADC、LCD 等设备与MCU 间,要求通讯速率较高的场合。

4 SPI—读写串行FLASH实验

https://blog.youkuaiyun.com/chenhuanqiangnihao/article/details/123742940?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169613405116800225590406%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=169613405116800225590406&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-2-123742940-null-null.142v94insert_down28v1&utm_term=eeprom%E5%92%8Cflash%E8%AF%A6%E8%A7%A3&spm=1018.2226.3001.4187

FLSAH 存储器又称闪存,它与EEPROM 都是掉电后数据不丢失的存储器,但FLASH 存储器容量普遍大于EEPROM,现在基本取代了它的地位。我们生活中常用的U 盘、SD 卡、SSD 固态硬盘以及我们STM32 芯片内部用于存储程序的设备,都是FLASH 类型的存储器。在存储控制上,最主要的区别是FLASH 芯片只能一大片一大片地擦写,而在“I2C 章节”中我们了解到EEPROM可以单个字节擦写

4-1 硬件设计

版本1

在这里插入图片描述

版本2

在这里插入图片描述
本实验板中的FLASH 芯片(型号:W25Q64) 是一种使用SPI 通讯协议的NOR FLASH 存储器,它的CS/CLK/DIO/DO 引脚分别连接到了STM32 对应的SPI 引脚NSS/SCK/MOSI/MISO 上,其中STM32 的NSS 引脚虽然是其片上SPI 外设的硬件引脚,但实际上后面的程序只是把它当成一个普通的GPIO,使用软件的方式控制NSS 信号,所以在SPI 的硬件设计中,NSS 可以随便选择普通的GPIO,不必纠结于选择硬件NSS 信号。
FLASH 芯片中还有WP 和HOLD 引脚。WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。

4-2 软件设计

为了使工程更加有条理,我们把读写FLASH 相关的代码独立分开存储,方便以后移植。在“工程模板”之上新建“bsp_spi_flash.c”及“bsp_spi_ flash.h”文件,这些文件也可根据您的喜好命名,它们不属于STM32 标准库的内容,是由我们自己根据应用需要编写的。

编程要点

(1) 初始化通讯使用的目标引脚及端口时钟;
(2) 使能SPI 外设的时钟;
(3) 配置SPI 外设的模式、地址、速率等参数并使能SPI 外设;
(4) 编写基本SPI 按字节收发的函数;
(5) 编写对FLASH 擦除及读写操作的的函数;
(6) 编写测试程序,对读写数据进行校验。

代码分析

1、SPI硬件相关宏定义

把SPI 硬件相关的配置都以宏的形式定义到“bsp_spi_ flash.h”文件中

2、初始化SPI的GPIO

利用上面的宏,编写SPI 的初始化函数

3、配置SPI模式(CPOL CPHA)
4、使用SPI发送和接收一个字节的数据

初始化好SPI 外设后,就可以使用SPI 通讯了,复杂的数据通讯都是由单个字节数据收发组成的

5、控制FLASH的指令

搞定SPI 的基本收发单元后,还需要了解如何对FLASH 芯片进行读写。FLASH 芯片自定义了很多指令,我们通过控制STM32 利用SPI 总线向FLASH 芯片发送指令,FLASH 芯片收到后就会执行相应的操作。
而这些指令,对主机端(STM32) 来说,只是它遵守最基本的SPI 通讯协议发送出的数据,但在设备端(FLASH 芯片) 把这些数据解释成不同的意义,所以才成为指令。查看FLASH 芯片的数据手册《W25Q64》,可了解各种它定义的各种指令的功能及指令格式,见表FLASH 常用芯片指令表。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
主机首先通过MOSI 线向FLASH 芯片发送第一个字节数据为“9F h”,当FLASH 芯片收到该数据后,它会解读成主机向它发送了“JEDEC 指令”,然后它就作出该命令的响应:通过MISO 线把它的厂商ID(M7-M0) 及芯片类型(ID15-0) 发送给主机,主机接收到指令响应后可进行校验。常见的应用是主机端通过读取设备ID 来测试硬件是否连接正常,或用于识别设备。

实际上,编写设备驱动都是有一定的规律可循的。首先我们要确定设备使用的是什么通讯协议。如上一章的EEPROM 使用的是I2C,本章的FLASH 使用的是SPI。那么我们就先根据它的通讯协议,选择好STM32 的硬件模块,并进行相应的I2C 或SPI 模块初始化。接着,我们要了解目标设备的相关指令,因为不同的设备,都会有相应的不同的指令。如EEPROM 中会把第一个数据解释为内部存储矩阵的地址(实质就是指令)。而FLASH 则定义了更多的指令,有写指令,读指令,读ID 指令等等。最后,我们根据这些指令的格式要求,使用通讯协议向设备发送指令,达到控制设备的目标。

6、定义FLASH指令编码表

为了方便使用,我们把FLASH 芯片的常用指令编码使用宏来封装起来,后面需要发送指令编码的时候我们直接使用这些宏即可。

7、读取FLASH芯片ID

根据“JEDEC”指令的时序,我们把读取FLASH ID 的过程编写成一个函数

8、FLASH写使能以及读取当前状态

在向FLASH 芯片存储矩阵写入数据前,首先要使能写操作,通过“Write Enable”命令即可写使能。
与EEPROM 一样,由于FLASH 芯片向内部存储矩阵写入数据需要消耗一定的时间,并不是在总线通讯结束的一瞬间完成的,所以在写操作后需要确认FLASH 芯片“空闲”时才能进行再次写入。为了表示自己的工作状态,FLASH 芯片定义了一个状态寄存器,见图FLASH 芯片的状态寄存器。
在这里插入图片描述

9、FLASH扇区擦除

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
使用扇区擦除指令“Sector Erase”可控制FLASH 芯片开始擦写,其指令时序见图扇区擦除时序。扇区擦除指令的第一个字节为指令编码,紧接着发送的3 个字节用于表示要擦除的存储矩阵地址。要注意的是在扇区擦除指令前,还需要先发送**“写使能”**指令,发送扇区擦除指令后,通过读取寄存器状态等待扇区擦除操作完毕

10、FLASH的页写入

目标扇区被擦除完毕后,就可以向它写入数据了。与EEPROM 类似,FLASH 芯片也有页写入命令,使用页写入命令最多可以一次向FLASH 传输256 个字节的数据,我们把这个单位为页大小。FLASH 页写入的时序见图FLASH 芯片页写入。
在这里插入图片描述
从时序图可知,第1 个字节为“页写入指令”编码,2-4 字节为要写入的“地址A”,接着的是要写入的内容,最多个可以发送256 字节数据,这些数据将会从“地址A”开始,按顺序写入到FLASH 的存储矩阵。若发送的数据超出256 个,则会覆盖前面发送的数据。
与擦除指令不一样,页写入指令的地址并不要求按256 字节对齐,只要确认目标存储单元是擦除状态即可(即被擦除后没有被写入过)。所以,若对“地址x”执行页写入指令后,发送了200 个字节数据后终止通讯,下一次再执行页写入指令,从“地址(x+200)”开始写入200 个字节也是没有问题的(小于256 均可)。只是在实际应用中由于基本擦除单元是4KB,一般都以扇区为单位进行读写,想深入了解,可学习我们的“FLASH 文件系统”相关的例子。

 /**
  * @brief  对FLASH按页写入数据,调用本函数写入数据前需要先擦除扇区
  * @param	pBuffer,要写入数据的指针
  * @param WriteAddr,写入地址
  * @param  NumByteToWrite,写入数据长度,必须小于等于SPI_FLASH_PerWritePageSize
  * @retval 无
  */
void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
  /* 发送FLASH写使能命令 */
  SPI_FLASH_WriteEnable();

  /* 选择FLASH: CS低电平 */
  SPI_FLASH_CS_LOW();
  /* 写页写指令*/
  SPI_FLASH_SendByte(W25X_PageProgram);
  /*发送写地址的高位*/
  SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
  /*发送写地址的中位*/
  SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 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 内部写入结束

11、不定量数据写入
12、从FLASH读取数据

相对于写入,FLASH 芯片的数据读取要简单得多,使用读取指令“Read Data”即可,其指令时序见图SPI_FLASH 读取数据时序。
在这里插入图片描述
发送了指令编码及要读的起始地址后,FLASH 芯片就会按地址递增的方式返回存储矩阵的内容,读取的数据量没有限制,只要没有停止通讯,FLASH 芯片就会一直返回数据。

 /**
  * @brief  读取FLASH数据
  * @param 	pBuffer,存储读出数据的指针
  * @param   ReadAddr,读取地址
  * @param   NumByteToRead,读取数据长度
  * @retval 无
  */
void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead)
{
  /* 选择FLASH: CS低电平 */
  SPI_FLASH_CS_LOW();

  /* 发送 读 指令 */
  SPI_FLASH_SendByte(W25X_ReadData);

  /* 发送 读 地址高位 */
  SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);
  /* 发送 读 地址中位 */
  SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);
  /* 发送 读 地址低位 */
  SPI_FLASH_SendByte(ReadAddr & 0xFF);
	
	/* 读取数据 */
  while (NumByteToRead--) /* while there is data to be read */
  {
    /* 读取一个字节*/
    *pBuffer = SPI_FLASH_SendByte(Dummy_Byte);
    /* 指向下一个字节缓冲区 */
    pBuffer++;
  }

  /* 停止信号 FLASH: CS 高电平 */
  SPI_FLASH_CS_HIGH();
}
main.c

函数中初始化了LED、串口、SPI 外设,然后读取FLASH 芯片的ID 进行校验,若ID 校验通过
则向FLASH 的特定地址写入测试数据,然后再从该地址读取数据,测试读写是否正常。

/*
 * 函数名:main
 * 描述  :主函数
 * 输入  :无
 * 输出  :无
 */
int main(void)
{ 	
	LED_GPIO_Config();
	LED_BLUE;
	
	/* 配置串口为:115200 8-N-1 */
	USART_Config();
	printf("\r\n 这是一个8Mbyte串行flash(W25Q64)实验 \r\n");
	
	/* 8M串行flash W25Q64初始化 */
	SPI_FLASH_Init();
	
	/* 获取 Flash Device ID */
	DeviceID = SPI_FLASH_ReadDeviceID();	
	Delay( 200 );
	
	/* 获取 SPI Flash ID */
	FlashID = SPI_FLASH_ReadID();	
	printf("\r\n FlashID is 0x%X,\
	Manufacturer Device ID is 0x%X\r\n", FlashID, DeviceID);
	
	/* 检验 SPI Flash ID */
	if (FlashID == sFLASH_ID)
	{	
		printf("\r\n 检测到串行flash W25Q64 !\r\n");
		
		/* 擦除将要写入的 SPI FLASH 扇区,FLASH写入前要先擦除 */
		// 这里擦除4K,即一个扇区,擦除的最小单位是扇区
		SPI_FLASH_SectorErase(FLASH_SectorToErase);	 	 
		
		/* 将发送缓冲区的数据写到flash中 */
		// 这里写一页,一页的大小为256个字节
		SPI_FLASH_BufferWrite(Tx_Buffer, FLASH_WriteAddress, BufferSize);		
		printf("\r\n 写入的数据为:%s \r\t", Tx_Buffer);
		
		/* 将刚刚写入的数据读出来放到接收缓冲区中 */
		SPI_FLASH_BufferRead(Rx_Buffer, FLASH_ReadAddress, BufferSize);
		printf("\r\n 读出的数据为:%s \r\n", Rx_Buffer);
		
		/* 检查写入的数据与读出的数据是否相等 */
		TransferStatus1 = Buffercmp(Tx_Buffer, Rx_Buffer, BufferSize);
		
		if( PASSED == TransferStatus1 )
		{ 
			LED_GREEN;
			printf("\r\n 8M串行flash(W25Q64)测试成功!\n\r");
		}
		else
		{        
			LED_RED;
			printf("\r\n 8M串行flash(W25Q64)测试失败!\n\r");
		}
	}// if (FlashID == sFLASH_ID)
	else// if (FlashID == sFLASH_ID)
	{ 
		LED_RED;
		printf("\r\n 获取不到 W25Q64 ID!\n\r");
	}
	
	while(1);  
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值