一、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. 引脚说明
| 引脚名 |
方向 |
功能简述 |
/CS |
I |
片选信号,低电平有效,对应 STM32 的 NSS 引脚 |
DO (IO1) |
I/O |
数据输出,对应 STM32 的 MISO 引脚 |
/WP (IO2) |
I/O |
写保护,低电平有效 |
GND |
- |
接地 |
DI (IO3) |
I/O |
数据输入,对应 STM32 的 MOSI 引脚 |
CLK |
I |
时钟信号,对应 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 很类似,不过前者存储空间更大):
因为我们用不到HOLD和WP引脚,所以直接连高电平。
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,声明了相关函数,并宏定义了命令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| #ifndef __SPI_FLASH_H #define __SPI_FLASH_H
#include "stm32f10x.h"
#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)
void SPI_Init_FUN(void); uint32_t SPI_FLASH_JEDEC_ID(void); void SPI_FLASH_Sector_Erase(uint32_t addr); 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);
#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
|
1. 初始化 SPI 结构体
下图表格来源于STM32F10xxx中文参考手册,说明了 SPI 引脚应该配置的 GPIO 模式:
初始化的程序如下所示。注意 SPI 选择的模式是模式 3,数据传送模式为高位先行。因为该型号的 Flash 芯片工作的时序图已经指出必须使用模式 0 或模式 3,而且数据是 8 位长度,高位先行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| static void SPI_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(FLASH_SPI_GPIO_CLK, ENABLE); 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); 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); 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); 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; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; SPI_InitStructure.SPI_CRCPolynomial = 0; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; 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 芯片发送回来的数据。由于发送和接收的间隔很短,因此可以视作“同时发生”。后面的函数中,无论是想要发送命令,还是发送数据或地址,都可以使用本函数。
程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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 号,该命令的时序图如下,按照时序图的规则写程序即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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; FLASH_ID |= SPI_FLASH_Send_Byte(DUMMY); FLASH_ID <<= 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)
接下来,下面的几个函数都涉及到读、写和擦除的操作。在写这些函数之前,我们需要先使能写操作:
1 2 3 4 5 6 7 8 9
| 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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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)
注意:形参中的地址最好是扇区的首地址(即地址对齐),否则可能会出错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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); 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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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); 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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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); 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函数进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| #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) { } }
|
附:超时处理
某些情况下,因为某些原因,导致缓冲区一直为空或一直非空的时候,程序就会陷入检测死循环,从而卡住下面的代码。因此我们可以在while循环内部加入倒计时,如果时间已到,但仍未跳出循环,说明已经超时,需要进行超时处理。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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) { FLASH_ERROR("SPI 等待超时!errorCode = %d",errorCode); return 0; }
|