Flash 存储器又称闪存,全称为 Flash EEPROM Memory。它结合了 ROM 和 RAM 的长处,不仅具备电子可擦除可编程(EEPROM)的性能,还可以快速读取数据,使数据不会因为断电而丢失。
Flash 主要分为两种:NOR Flash 和 NAND Flash,两者的主要区别待会会提到。一般情况下,我们所说的 SPI Flash 指的是 SPI NOR Flash。
我们以 Flash 芯片 W25Q64BV 为例,来说明 Flash 的存储方式:
如图所示,该存储器一共为 8MB 大小。设计者将存储器分为了 128 个块(Blocks),每个块的大小均为 64KB。而每个块又分为更小的 16 个部分,称为扇区(Sector),大小为 4KB。
那么为什么 Flash 被设计成这样呢?这跟它的读写特性有关。现在让我们来走一遍 Flash 的读写流程,仔细看看它是如何操作数据的:
引脚名 | 方向 | 功能简述 |
---|---|---|
/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 协议。
因为 STM32 本身不支持后两种拓展的 SPI 协议,因此我们用不到IO0 ~ IO3
。
时钟信号最高可达 80 MHz,因此我们可以设置 STM32 的 APB1 时钟为 36MHz,这样 SCLK = 72MHz。
最后来看看开发板上的电路图,开发板用的是 W25Q128(与 W25Q64 很类似,不过前者存储空间更大):
因为我们用不到HOLD
和WP
引脚,所以直接连高电平。
状态寄存器包括BUSY, WEL, BP2-BP0, TB, SEC, SRP0,
位。这里简单说一下状态寄存器中的最常用的状态位:
SRP1, QE
当执行命令Page Program(写入), Sector Erase(擦除), Block Erase(擦除), Chip Erase(擦除), Write Status Register(写状态)
时,BUSY 位置 1,此时芯片会忽略其他命令(除了Read Status Register(读状态), Erase Suspend(擦除延迟)
)。当上述命令执行完毕后,BUSY 位清零,表示芯片可以开始执行下面的命令了。
其他状态位的作用可以参考数据手册。
以上两个表格列出了 W25Q64 的命令,这些命令可以方便单片机操作芯片。类似于汇编,命令的基本格式分为两个部分:
操作数的种类:
Page Program
和命令Quad Page Program
后面的 D7-D0 其实是没有括号的(数据手册有误),这两个命令用于写入操作。命令的含义可从表中读出,这里就不再赘述了。关于命令的时序问题,留待下一部分实践时再解说。
下面为头文件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 */
下图表格来源于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();
}
主机发送之前,需要先循环检测发送缓冲区是否为空,若发送缓冲区为空,则可以将数据传入到数据寄存器,然后传到 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);
}
每次使用 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;
}
注意以下两点:
DUMMY
)。主机首先接收的是最高位的数据,然后是次低位,最后是最低位,因此需要分别将最高位和次低位左移 16 位和 8 位。接下来,下面的几个函数都涉及到读、写和擦除的操作。在写这些函数之前,我们需要先使能写操作:
/* 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; // 片选信号为高电平,表示未选中
}
注意:形参中的地址最好是扇区的首地址(即地址对齐),否则可能会出错。
/* 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(); // 等待擦除完毕
}
/* 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(); // 等待写入完毕
}
/* 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; // 片选信号为高电平,表示未选中
}
#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 所有基本的外设都已经介绍完了,之后更新的内容待定。