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. 引脚说明
引脚名 | 方向 | 功能简述 |
---|
/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 ,声明了相关函数,并宏定义了命令。
#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 位长度,高位先行。
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 芯片发送回来的数据。由于发送和接收的间隔很短,因此可以视作“同时发生”。后面的函数中,无论是想要发送命令,还是发送数据或地址,都可以使用本函数。
程序如下:
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 号,该命令的时序图如下,按照时序图的规则写程序即可:
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)
接下来,下面的几个函数都涉及到读、写和擦除的操作。在写这些函数之前,我们需要先使能写操作:
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):
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)
注意:形参中的地址最好是扇区的首地址(即地址对齐),否则可能会出错。
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)
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)
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函数进行测试
#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 循环内部加入倒计时,如果时间已到,但仍未跳出循环,说明已经超时,需要进行超时处理。例如:
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;
}
至此,STM32 所有基本的外设都已经介绍完了,之后更新的内容待定。
|