STM32学习笔记(16)——(SPI续)读写串行Flash

Posted Mount256

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STM32学习笔记(16)——(SPI续)读写串行Flash相关的知识,希望对你有一定的参考价值。

一、Flash的基本知识

1. Flash概要

Flash 存储器又称闪存,全称为 Flash EEPROM Memory。它结合了 ROM 和 RAM 的长处,不仅具备电子可擦除可编程(EEPROM)的性能,还可以快速读取数据,使数据不会因为断电而丢失。

Flash 主要分为两种:NOR Flash 和 NAND Flash,两者的主要区别待会会提到。一般情况下,我们所说的 SPI Flash 指的是 SPI NOR Flash。

2. Flash的存储方式与读写特性

我们以 Flash 芯片 W25Q64BV 为例,来说明 Flash 的存储方式:

如图所示,该存储器一共为 8MB 大小。设计者将存储器分为了 128 个块(Blocks),每个块的大小均为 64KB。而每个块又分为更小的 16 个部分,称为扇区(Sector),大小为 4KB。

那么为什么 Flash 被设计成这样呢?这跟它的读写特性有关。现在让我们来走一遍 Flash 的读写流程,仔细看看它是如何操作数据的:

  • 首先,一个未写入数据(空白)的 Flash ,它的所有数据位均为 1,这也是它的初始默认状态。这意味着,向 Flash 写入数据的实质是将数据位 1 改成 0
  • 现在我们开始写入一位数据,理想情况下我们将数据位 1 改成 0 就可以了。然而实际情况并没有这么简单,因为不同的芯片种类有不同的读写特性。
  • 对于 Nand Flash,需要将将整个扇区(块)A 擦除(重置为 1),然后找一个空白的扇区(块)B,最后在扇区(块)B 写入想要修改的数据位的同时,也要重新写回之前的数据。这意味着,我们每写入一次数据,整个扇区(块)的数据就会经历一次大搬家,本质是重写了一个数据块。
  • 对于 Nor Flash,可以一个一个字节地读写数据,即能像 RAM 一样随机访问数据(这也导致其价格昂贵,容量偏小),所以这种芯片能够比较好的支持 SPI 协议。比如接下来要讲到的 W25Q64 和 W25Q128 就是属于 Nor Flash。
  • 读数据则没有限制。

二、Flash芯片介绍:W25Q64

1. 引脚说明

引脚名方向功能简述
/CSI片选信号,低电平有效,对应 STM32 的 NSS 引脚
DO (IO1)I/O数据输出,对应 STM32 的 MISO 引脚
/WP (IO2)I/O写保护,低电平有效
GND-接地
DI (IO3)I/O数据输入,对应 STM32 的 MOSI 引脚
CLKI时钟信号,对应 STM32 的 CLK 引脚
/HOLD (IO3)I/O维持数据,起到暂停通讯的作用
VCC-提供电源

引脚说明标注的IO0 ~ IO3是什么意思呢?首先我们需要知道,之前所介绍的 SPI 协议其实是标准 SPI 协议。而针对 SPI Flash,为了加快数据传输速率,还添加了Dual SPI 协议Quad SPI 协议

  • Dual SPI 协议是半双工通信。该协议的由来是:我们发现在主机读取从机数据时,MISO 引脚被使用,而 MOSI 引脚处于空闲状态,为了加快通信速率,该协议规定 MOSI 引脚也将同时参与读操作;对于写操作也是类似的。所以一个时钟周期就能传输两位的数据。
  • Quad SPI 协议与前者类似,不过芯片增加了两个引脚 I/O 口,这样一个时钟周期就能传输四位的数据。

因为 STM32 本身不支持后两种拓展的 SPI 协议,因此我们用不到IO0 ~ IO3

时钟信号最高可达 80 MHz,因此我们可以设置 STM32 的 APB1 时钟为 36MHz,这样 SCLK = 72MHz。

最后来看看开发板上的电路图,开发板用的是 W25Q128(与 W25Q64 很类似,不过前者存储空间更大):

因为我们用不到HOLDWP引脚,所以直接连高电平。

2. 状态寄存器(STATUS REGISTER)

状态寄存器包括BUSY, WEL, BP2-BP0, TB, SEC, SRP0,
SRP1, QE
位。这里简单说一下状态寄存器中的最常用的状态位:

BUSY位

当执行命令Page Program(写入), Sector Erase(擦除), Block Erase(擦除), Chip Erase(擦除), Write Status Register(写状态)时,BUSY 位置 1,此时芯片会忽略其他命令(除了Read Status Register(读状态), Erase Suspend(擦除延迟))。当上述命令执行完毕后,BUSY 位清零,表示芯片可以开始执行下面的命令了。

其他状态位的作用可以参考数据手册。

3. 命令(INSTRUCTIONS)

以上两个表格列出了 W25Q64 的命令,这些命令可以方便单片机操作芯片。类似于汇编,命令的基本格式分为两个部分:

  • 第 1 个字节:指令代码,用于区分不同的命令。
  • 第 2-6 个字节:操作数,又分为不同的种类。操作数的括号表示传输方向为从芯片到主机,而没有括号的表示传输方向为从主机到芯片。

操作数的种类:

  • S:代表状态寄存器。例如 S7 表示状态寄存器中的第 7 位。这个操作数总是用来被输出到主机。 因此读写状态寄存器的命令其实只需要一个字节就够了。
  • A:代表芯片的存储地址。例如 A23-A16 表示存储地址的高位字节。
  • D:代表想要写入的数据。需要注意,命令Page Program和命令Quad Page Program后面的 D7-D0 其实是没有括号的(数据手册有误),这两个命令用于写入操作。
  • Dummy:表示可以发送任意的一字节数据。
  • ID 和 MF:表示芯片的 ID 号和生厂商 ID 号。一般主机访问这些 ID 号的目的,是用来确认主机是否正确连接了芯片。该芯片的 ID 号如下表所示:

命令的含义可从表中读出,这里就不再赘述了。关于命令的时序问题,留待下一部分实践时再解说。

三、使用 STM32 读写 Flash

下面为头文件spi_flash.h,声明了相关函数,并宏定义了命令。

#ifndef __SPI_FLASH_H
#define __SPI_FLASH_H

#include "stm32f10x.h"

/*********** Definition ***********/

#define FLASH_SPIx 				SPI2

#define FLASH_SPI_CLK 			RCC_APB1Periph_SPI2
#define FLASH_SPI_GPIO_CLK 		RCC_APB2Periph_GPIOB

#define FLASH_SPI_CS_PORT 		GPIOB
#define FLASH_SPI_CS_PIN 		GPIO_Pin_12

#define FLASH_SPI_SCK_PORT 		GPIOB
#define FLASH_SPI_SCK_PIN 		GPIO_Pin_13

#define FLASH_SPI_MISO_PORT 	GPIOB
#define FLASH_SPI_MISO_PIN 		GPIO_Pin_14

#define FLASH_SPI_MOSI_PORT 	GPIOB
#define FLASH_SPI_MOSI_PIN 		GPIO_Pin_15

#define FLASH_SPI_CS_HIGH  		GPIO_SetBits  (FLASH_SPI_CS_PORT, FLASH_SPI_CS_PIN)
#define FLASH_SPI_CS_LOW 		GPIO_ResetBits(FLASH_SPI_CS_PORT, FLASH_SPI_CS_PIN)

/*********** Function Declaration ***********/

void SPI_Init_FUN(void);  					// 初始化SPI
uint32_t SPI_FLASH_JEDEC_ID(void);			// 读取Flash的ID号
void SPI_FLASH_Sector_Erase(uint32_t addr); // 擦除Flash扇区
void SPI_FLASH_Write_Enable(void);			// 读写使能
void SPI_FLASH_Wait(void);					// 等待写/读/擦除操作完毕
void SPI_FLASH_Sector_Erase(uint32_t addr); // 擦除指定扇区
void SPI_FLASH_Page_Program(uint32_t addr, uint32_t num, uint8_t *data); // 写入数据
void SPI_FLASH_Read_Data(uint32_t addr, uint32_t num, uint8_t *data); // 读取数据

/************** Instructions ****************/

#define DUMMY 			0x00
#define JEDEC_ID 		0x9F
#define WRITE_ENABLE	0x06
#define READ_STATUS_1	0x05
#define READ_STATUS_2	0x35
#define SECTOR_ERASE 	0x20
#define PAGE_PROGRAM 	0x02
#define READ_DATA		0x03

#endif  /* __SPI_FLASH_H */

1. 初始化 SPI 结构体

下图表格来源于STM32F10xxx中文参考手册,说明了 SPI 引脚应该配置的 GPIO 模式:

初始化的程序如下所示。注意 SPI 选择的模式是模式 3,数据传送模式为高位先行。因为该型号的 Flash 芯片工作的时序图已经指出必须使用模式 0 或模式 3,而且数据是 8 位长度,高位先行。

static void SPI_GPIO_Config(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure; 
	
	RCC_APB2PeriphClockCmd(FLASH_SPI_GPIO_CLK, ENABLE);
	
	// CS -> PB12
	GPIO_InitStructure.GPIO_Pin 	= FLASH_SPI_CS_PIN;
	GPIO_InitStructure.GPIO_Mode 	= GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Speed 	= GPIO_Speed_50MHz;
	GPIO_Init(FLASH_SPI_CS_PORT, &GPIO_InitStructure);
	
	// SCK -> PB13
	GPIO_InitStructure.GPIO_Pin 	= FLASH_SPI_SCK_PIN;
	GPIO_InitStructure.GPIO_Mode 	= GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Speed 	= GPIO_Speed_50MHz;
	GPIO_Init(FLASH_SPI_SCK_PORT, &GPIO_InitStructure);
	
	// MISO -> PB14
	GPIO_InitStructure.GPIO_Pin 	= FLASH_SPI_MISO_PIN;
	GPIO_InitStructure.GPIO_Mode 	= GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Speed 	= GPIO_Speed_50MHz;
	GPIO_Init(FLASH_SPI_MISO_PORT, &GPIO_InitStructure);
	
	// MOSI -> PB15
	GPIO_InitStructure.GPIO_Pin 	= FLASH_SPI_MOSI_PIN;
	GPIO_InitStructure.GPIO_Mode 	= GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Speed 	= GPIO_Speed_50MHz;
	GPIO_Init(FLASH_SPI_MOSI_PORT, &GPIO_InitStructure);
}

static void SPI_Mode_Config(void)
{
	SPI_InitTypeDef  SPI_InitStructure; 
	
	RCC_APB1PeriphClockCmd(FLASH_SPI_CLK, ENABLE);
	
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;  // SCK = f(PCLK)/2
	SPI_InitStructure.SPI_CPHA 			= SPI_CPHA_2Edge; 		// CPHA偶数边沿
	SPI_InitStructure.SPI_CPOL 			= SPI_CPOL_High; 		// CPOL空闲时高电平(模式3)
	SPI_InitStructure.SPI_CRCPolynomial = 0;					// 不使用CRC校验,数值随意写
	SPI_InitStructure.SPI_DataSize 		= SPI_DataSize_8b;		// 8位数据(可参考芯片数据手册的时序图)
	SPI_InitStructure.SPI_Direction 	= SPI_Direction_2Lines_FullDuplex; // 双线全双工
	SPI_InitStructure.SPI_FirstBit 		= SPI_FirstBit_MSB; 	// 高位先行(可参考芯片数据手册的时序图)
	SPI_InitStructure.SPI_Mode 			= SPI_Mode_Master;		// STM32作为主机
	SPI_InitStructure.SPI_NSS 			= SPI_NSS_Soft; 		// NSS引脚由软件控制
	
	SPI_Init(FLASH_SPIx, &SPI_InitStructure);
	SPI_Cmd (FLASH_SPIx, ENABLE);
	
	FLASH_SPI_CS_HIGH; // 片选信号为高电平,表示未选中
}

void SPI_Init_FUN(void)
{
	SPI_GPIO_Config();
	SPI_Mode_Config();
}

2. 主机发送一字节数据

主机发送之前,需要先循环检测发送缓冲区是否为空,若发送缓冲区为空,则可以将数据传入到数据寄存器,然后传到 Flash 芯片。

Flash 芯片接收到从主机发送的数据后,就要执行相应的命令、或取出相应的数据,因此会将数据传到主机的接受缓冲区。从完成发送至检测到接收缓冲区非空有一段时间,这段时间正是主机在循环检测接收缓冲区是否有数据。主机检测接收缓冲区非空后,读取数据,一次发送过程结束。

这整个过程就是:在主机每次发送一字节数据或命令后,同时也会接收从 Flash 芯片发送回来的数据。由于发送和接收的间隔很短,因此可以视作“同时发生”。后面的函数中,无论是想要发送命令,还是发送数据或地址,都可以使用本函数。

程序如下:

/* 主机发送并接收一字节数据 */
static uint8_t SPI_FLASH_Send_Byte(uint8_t data)
{
	// 当发送缓冲区非空时,循环等待直至发送缓冲区为空
	while(SPI_I2S_GetFlagStatus(FLASH_SPIx, SPI_I2S_FLAG_TXE) == RESET);
	
	// 发送缓冲区为空,可以发送数据
	SPI_I2S_SendData(FLASH_SPIx, data);
	
	// 当接收缓冲区为空时,循环等待直至接收缓冲区非空
	while(SPI_I2S_GetFlagStatus(FLASH_SPIx, SPI_I2S_FLAG_RXNE) == RESET);
	
	// 接收缓冲区非空,可以接收数据
	return SPI_I2S_ReceiveData(FLASH_SPIx);
}

3. 读取 Flash ID 号(JEDEC ID, 0x9F)

每次使用 Flash 之前,都应该读取其 ID 号,用以判断是否连接正常。我们使用命令JEDEC ID来获取 ID 号,该命令的时序图如下,按照时序图的规则写程序即可:

/* JEDEC ID */
uint32_t SPI_FLASH_JEDEC_ID(void)
{
	uint32_t FLASH_ID;
	
	FLASH_SPI_CS_LOW; // 片选信号为低电平,表示已选中
	
	SPI_FLASH_Send_Byte(JEDEC_ID);
	
	FLASH_ID = SPI_FLASH_Send_Byte(DUMMY);
	FLASH_ID <<= 8; // 左移8位
	
	FLASH_ID |= SPI_FLASH_Send_Byte(DUMMY);
	FLASH_ID <<= 8; // 左移8位
	
	FLASH_ID |= SPI_FLASH_Send_Byte(DUMMY);
	
	FLASH_SPI_CS_HIGH; // 片选信号为高电平,表示未选中
	
	return FLASH_ID;
}

注意以下两点:

  • 每次主机发送命令之前,应该选中芯片,以表明芯片应开始工作了;而当芯片完成命令后,主机最好应该禁选芯片,以防止其他误操作,从而改变内部数据。
  • ID 号是一个 24 位的数据,因此需要分三次接收,也就需要发三次随意的数据(DUMMY)。主机首先接收的是最高位的数据,然后是次低位,最后是最低位,因此需要分别将最高位和次低位左移 16 位和 8 位。

4. 写使能(Write Enable, 0x06)和读取状态寄存器(Read Status Register, 0x05/0x35)

接下来,下面的几个函数都涉及到读、写和擦除的操作。在写这些函数之前,我们需要先使能写操作:

/* Write Enable */
void SPI_FLASH_Write_Enable(void)
{
	FLASH_SPI_CS_LOW; // 片选信号为低电平,表示已选中
	
	SPI_FLASH_Send_Byte(WRITE_ENABLE);
	
	FLASH_SPI_CS_HIGH; // 片选信号为高电平,表示未选中
}

一般来说,由于读取/写入/擦除数据所耗费的时间会比较长,为了知道芯片什么时候完成操作,我们需要读取状态寄存器中的 BUSY 位,当主机检测到 BUSY 位从 1 置 0 时,我们便认为芯片完成了操作(注意想要读取 BUSY 位用的是命令 0x05):

/* Read Status Register */
void SPI_FLASH_Wait(void)
{
	uint8_t reg = 0;
	
	FLASH_SPI_CS_LOW; // 片选信号为低电平,表示已选中
	
	SPI_FLASH_Send_Byte(READ_STATUS_1);
	
	do{
		reg = SPI_FLASH_Send_Byte(DUMMY);
	}while( (reg & 0x01) == 1 );
	
	FLASH_SPI_CS_HIGH; // 片选信号为高电平,表示未选中
}

5. 擦除指定扇区(Erase Sector, 0x20)

注意:形参中的地址最好是扇区的首地址(即地址对齐),否则可能会出错。

/* Sector Erase */
void SPI_FLASH_Sector_Erase(uint32_t addr)
{
	FLASH_SPI_CS_LOW; // 片选信号为低电平,表示已选中
	
	SPI_FLASH_Write_Enable(); // 开启写使能,允许写入芯片
	
	SPI_FLASH_Send_Byte(SECTOR_ERASE);
	SPI_FLASH_Send_Byte((addr<<16) & 0xFF); // 这是从地址高位字节开始发送,0xFF:掩码
	SPI_FLASH_Send_Byte((addr<<8)  & 0xFF);
	SPI_FLASH_Send_Byte((addr)     & 0xFF);
	
	FLASH_SPI_CS_HIGH; // 片选信号为高电平,表示未选中
	
	SPI_FLASH_Wait(); // 等待擦除完毕
}

6. 向 Flash 写入数据(Page Program, 0x02)

/* Page Program */
void SPI_FLASH_Page_Program(uint32_t addr, uint32_t num, uint8_t *data)
{
	SPI_FLASH_Write_Enable(); // 开启写使能,允许写入芯片
	
	FLASH_SPI_CS_LOW; // 片选信号为低电平,表示已选中
	
	SPI_FLASH_Send_Byte(PAGE_PROGRAM);
	SPI_FLASH_Send_Byte((addr<<16) & 0xFF); // 0xFF:掩码
	SPI_FLASH_Send_Byte((addr<<8)  & 0xFF);
	SPI_FLASH_Send_Byte((addr)     & 0xFF);
	
	while(num--)
	{
		SPI_FLASH_Send_Byte(*data); // 传送字节数据
		data++; // 指针指向下一字节数据
	}
	
	FLASH_SPI_CS_HIGH; // 片选信号为高电平,表示未选中
	
	SPI_FLASH_Wait(); // 等待写入完毕
}

7. 从 Flash 读取数据(Read Data, 0x03)

/* Read Data */
void SPI_FLASH_Read_Data(uint32_t addr, uint32_t num, uint8_t *data)
{
	FLASH_SPI_CS_LOW; // 片选信号为低电平,表示已选中
	
	SPI_FLASH_Send_Byte(READ_DATA);
	SPI_FLASH_Send_Byte((addr<<16) & 0xFF); // 0xFF:掩码
	SPI_FLASH_Send_Byte((addr<<8)  & 0xFF);
	SPI_FLASH_Send_Byte((addr)     & 0xFF);
	
	while(num--)
	{
		*data = SPI_FLASH_Send_Byte(DUMMY); // 接收字节数据
		data++; // 准备下一字节空间用以接收新的数据
	}
	
	FLASH_SPI_CS_HIGH; // 片选信号为高电平,表示未选中
}

8. main函数进行测试

#include "stm32f10x.h"
#include "spi_flash.h"
#include "usart.h"

int main(void)
{	
	uint32_t id, i;
	uint8_t array_write[100], array_read[100];
	
	USART_Config();
	printf("\\r\\nSPI读写Flash开始测试!!!\\r\\n");
	
	SPI_Init_FUN();
	id = SPI_FLASH_JEDEC_ID();
	printf("\\r\\n Flash ID = 0x%x \\r\\n", id);
	
	SPI_FLASH_Sector_Erase(0x000000);
	printf("\\r\\n Flash某扇区已擦除完毕!!!\\r\\n");
	
	for(i = 0; i < 25; i++)
		array_write[i] = i + 15;
	SPI_FLASH_Page_Program(0x000000, 25, array_write);
	printf("\\r\\n数据写入完毕!!!\\r\\n");
	
	SPI_FLASH_Read_Data(0x000000, 100, array_read); 
	printf("\\r\\n读取数据如下:\\r\\n");
	for(i = 0; i < 100; i++)
	{
		printf("0x%x ", array_read[i]);
		if(i % 10 == 0)
			printf("\\r\\n");
	}
	
	while(1)
	{
		//...	
	}
 }

 /*************** END OF FILE **************/

附:超时处理

某些情况下,因为某些原因,导致缓冲区一直为空或一直非空的时候,程序就会陷入检测死循环,从而卡住下面的代码。因此我们可以在while循环内部加入倒计时,如果时间已到,但仍未跳出循环,说明已经超时,需要进行超时处理。例如:

// 当发送缓冲区非空时,循环等待直至发送缓冲区为空
while(SPI_I2S_GetFlagStatus(FLASH_SPIx,SPI_I2S_FLAG_TXE) == RESET)
{
	if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);
}

// 超时处理:
static  uint32_t SPI_TIMEOUT_UserCallback(uint8_t errorCode)
{
  /* Block communication and all processes */
  FLASH_ERROR("SPI 等待超时!errorCode = %d",errorCode);
  
  return 0;
}

至此,STM32 所有基本的外设都已经介绍完了,之后更新的内容待定。

以上是关于STM32学习笔记(16)——(SPI续)读写串行Flash的主要内容,如果未能解决你的问题,请参考以下文章

STM32(十三)SPI-读写串行flash

stm32学习第十四天

STM32F10x_SPI(硬件接口 + 软件模拟)读写Flash(25Q16)

STM32.SPI(25Q16)

STM32学习笔记(15)——SPI协议

STM32CubeMX学习笔记(26)——SDIO接口使用(读写SD卡)