W25Q64介绍
W25Q64
是一种常见的串行闪存器件,它采用SPI(Serial Peripheral Interface)
接口协议,具有高速读写和擦除功能,可用于存储和读取数据。W25Q64
芯片容量为64 Mbit(8 MB)
,其中名称后的数字代表不同的容量选项。不同的型号和容量选项可以满足不同应用的需求,比如W25Q16
、W25Q32
、W25Q128
等。通常被用于嵌入式设备、存储设备、路由器等高性能电子设备中。
W25Q64
闪存芯片的内存分配是按照扇区(Sector)
和块(Block)
进行的,每个扇区的大小为4KB
,每个块包含16
个扇区,即一个块的大小为64KB
。
硬件接口
我们使用一款模块作为案例其引脚的说明,见下表。
引脚 | 功能说明 |
---|---|
CLK | 从外部获取时间,为输入输出功能提供时钟 |
DI | 标准SPI使用单向的DI,来串行的写入指令,地址,或者数据到FLASH中,在时钟的上升沿 |
DO | 标准SPI使用单向的DO,来从处于下降边沿时钟的设备,读取数据或者状态 |
WP | 防止状态寄存器被写入 |
HOLD | 当它有效时允许设备暂停,低电平:DO引脚高阻态,DICLK引脚的信号被忽略。高电平:设备重新开始,当多个设备共享相同的SPI信号的时候该功能可能会被用到 |
CS | CS高电平的时候其他引脚成高阻态,处于低电平的时候,可以读写数据 |
需要注意的是,我们使用的是硬件SPI方式驱动W25Q64,因此我们需要确定我们设置的引脚是否有硬件SPI外设接口。在GD32VW55X数据手册中,PA9~11可以复用为SPI4的3根通信线。
我们为什么不给CS引脚使用专门的硬件接口? 因为硬件SPI的CS片选引脚只有一根,我们知道SPI协议是支持一主机与多从机进行通信的,多通信就是通过片选引脚进行确定的,如果我们使用了硬件片选引脚,那么我们就只能使用一个从机,所以大多数情况下都是使用软件模拟片选线。
配置引脚
使用到了硬件SPI外设,我们需要开启对应的硬件SPI时钟,并且开启引脚的复用功能,绑定复用线。
rcu_periph_clock_enable(RCU_GPIOA); // 使用A端口
rcu_periph_clock_enable(RCU_SPI); // 使能SPI
//引脚复用
gpio_af_set(GPIOA, GPIO_AF_0, GPIO_PIN_9);
gpio_af_set(GPIOA, GPIO_AF_0, GPIO_PIN_10);
gpio_af_set(GPIOA, GPIO_AF_0, GPIO_PIN_11);
//引脚模式
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_9);
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_10);
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_11);
//输出模式
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_9);
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_10);
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_11);
//开启CS引脚时钟
rcu_periph_clock_enable(RCU_GPIOA);
//配置CS引脚模式
gpio_mode_set(GPIOA, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_PIN_12);
//配置CS输出模式
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_12);
//W25Q64不选中
gpio_bit_write(GPIOA, GPIO_PIN_12, SET);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
配置SPI
根据W25Q64的数据手册,得到以下配置:
- SPI配置为了全双工模式,可以同时发送与接收数据;
- MCU配置为主机模式,由MCU产生时钟,与从机W25Q64进行通信;
- 数据的传输以8位进行传输,因为W25Q64有时需要传输8位,有时需要传输16位,有时直接传输24位。为了同时兼容3种长度的传输,选择了以8位数据位进行传输。
- W25Q64支持两种模式,模式0和模式3,这里极性相位配置选择了模式3,即时钟极性为1,时钟相位为1。
- 片选方式选择软件控制片选。在硬件SPI中,一个SPI只有一个片选线,这会导致硬件SPI如果选择硬件控制片选信号,只能控制一个从机。我们是希望能够一个SPI可以控制多个从机,因此选择软件方式片选,片选线可以随意设定。
- 时钟分频选择4分频,根据W25Q64的数据手册说明,W25Q64的SPI时钟可以达到133MHz,而我们的SPI4的时钟来源为PCLK2=160MHz,虽然SPI的配置中必须要求进行分频,但是分频之后的频率也是低于133Mhz,W25Q64完全可以兼容。
- 字节顺序选择高位在前。
//SPI参数定义结构体
spi_parameter_struct spi_init_struct;
/* 配置 SPI 参数 */
spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX; // 传输模式全双工
spi_init_struct.device_mode = SPI_MASTER; // 配置为主机
spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT; // 8位数据
spi_init_struct.clock_polarity_phase = SPI_CK_PL_HIGH_PH_2EDGE;
spi_init_struct.nss = SPI_NSS_SOFT; // 软件cs
spi_init_struct.prescale = SPI_PSC_4;//4分频
spi_init_struct.endian = SPI_ENDIAN_MSB;//高位在前
//将参数填入SPI
spi_init(&spi_init_struct);
/* 使能 SPI */
spi_enable();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SPI的初始化完成,我们需要准备SPI读写步骤。SPI我们配置为了全双工模式,即可以读也可以写。为确保发送和接收数据成功,在发送时,需要确保发送缓冲区里的数据发送完毕,即发送缓冲区为空,才可以进行下一个数据的发送;在接收时,需要确保接收缓冲区里有数据才能够进行接收。
uint8_t spi_read_write_byte(uint8_t dat)
{
//等待发送缓冲区为空
while(RESET == spi_flag_get(SPI_FLAG_TBE) );
//通过SPI发送一个字节数据
spi_data_transmit(dat);
//等待接收缓冲区不空标志
while(RESET == spi_flag_get(SPI_FLAG_RBNE) );
//读取并返回在SPI读取到的单字节数据
return spi_data_receive();
}
2
3
4
5
6
7
8
9
10
11
12
读取设备ID案例
根据W25Q64的数据手册可知,读取ID的指令有很多个,ABH/90H/92H/94H/8FH
等。而90H的命令,在数据手册中给出了其读取的时序图。
读取步骤:
- 将CS端拉低为低电平;
- 发送指令 90H(1001_0000);
- 发送地址 000000H(0000_0000_0000_0000_0000_0000);
- 读取制造商ID,根据数据手册可以知道制造商ID为EFh;
- 读取设备ID,根据数据手册可以知道设备ID为16h;
- 恢复CS端为高电平;
实现代码:
//读取芯片ID
//返回值如下:
//0XEF13,表示芯片型号为W25Q80
//0XEF14,表示芯片型号为W25Q16
//0XEF15,表示芯片型号为W25Q32
//0XEF16,表示芯片型号为W25Q64
//0XEF17,表示芯片型号为W25Q128
//读取设备ID
uint16_t W25Q64_readID(void)
{
uint16_t temp = 0;
//将CS端拉低为低电平
W25QXX_CS_ON(0);
//发送指令90h
spi_read_write_byte(0x90);//发送读取ID命令
//发送地址 000000H
spi_read_write_byte(0x00);
spi_read_write_byte(0x00);
spi_read_write_byte(0x00);
//接收数据
//接收制造商ID
temp |= spi_read_write_byte(0xFF)<<8;
//接收设备ID
temp |= spi_read_write_byte(0xFF);
//恢复CS端为高电平
W25QXX_CS_ON(1);
//返回ID
return temp;
}
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
其中W25QXX_CS_ON(0);是一个自己定义的宏,具体如下:
#define W25QXX_CS_ON(x) gpio_bit_write(GPIOA, GPIO_PIN_12, x?SET:RESET)
写入数据流程案例
写入数据步骤如右图。在FLASH存储器中,每次写入数据都要确保其中的数据为0xFF,是因为FLASH存储器的写入操作是一种擦除-写入操作。擦除操作是将存储单元中的数据全部置为1,也就是0xFF。然后,只有将要写入的数据位为0的位置才能进行写入操作,将其改变为0。这个过程是不可逆的,所以在写入数据之前,需要先确保要写入的位置为0xFF,然后再写入数据。
这种擦除-写入的操作是由于FLASH存储器的特殊结构决定的。FLASH存储器中的存储单元是通过电子门的状态进行控制的,每个门可以存储一个二进制位。擦除操作需要将门的状态恢复为初始状态,即全部为1。然后通过改变门的状态,将需要存储的数据位改变为0。所以在写入数据之前,需要确保存储单元的状态为1,以便进行正确的写入操作。
另外,FLASH存储器的擦除操作是以块为单位进行的,而不是单个存储单元。所以如果要写入数据的位置上已经有数据存在,需要进行擦除操作,将整个块的数据都置为1,然后再写入新的数据。这也是为什么在FLASH写入数据之前需要确保其中的数据为0xFF的原因。
写使能
在进行写入操作之前,需要使用到写使能(Write Enable)命令。写使能的作用是启用对闪存芯片的写入操作。在默认情况下,闪存芯片处于保护状态,禁止对其进行写入操作,主要是为了防止误操作对数据的损坏。写使能命令可以解除这种保护状态,将闪存芯片设置为可以进行写入操作。
通过发送写使能命令,闪存芯片将进入一个特定的状态,使得后续的写入命令可以被接受和执行。在写入数据之前,需要发送写使能命令来确保闪存芯片处于可写状态。然后,才能发送写入命令将数据写入指定的存储位置。
使用写使能命令可以有效地保护数据的完整性和安全性,防止误操作对数据进行写入或者修改。同时,也能够确保数据的一致性,避免写入过程中出现错误或者干扰。因此,在使用W25Q64进行写入操作时,需要先发送写使能命令,以确保闪存芯片处于可写状态,再进行数据的写入操作。
W25Q64的数据手册中,关于写使能的时序如下:
操作步骤:
- 将CS端拉低为低电平;
- 发送指令 06H(0000_0110);
- 恢复CS端为高电平;
具体实现代码如下:
//发送写使能
void W25Q64_write_enable(void)
{
//拉低CS端为低电平
W25QXX_CS_ON(0);
//发送指令06h
spi_read_write_byte(0x06);
//拉高CS端为高电平
W25QXX_CS_ON(1);
}
2
3
4
5
6
7
8
9
10
器件忙判断
在W25Q64的数据手册中,有3个状态寄存器,可以判断当前W25Q64是否正在传输、写入、读取数据等,我们每一次要对W25Q64进行操作时,需要先判断W25Q64是否在忙。如果在忙的状态,我们去操作W25Q64,很可能会导致数据丢失,并且操作失败。而判断是否忙,是通过状态寄存器1的S0为进行判断,状态寄存器1的地址为0X05。
读取状态寄存器的时序图如下:
- 拉低CS端为低电平;
- 发送指令05h(0000_0101);
- 接收状态寄存器值;
- 恢复CS端为高电平;
具体实现代码如下:
void W25Q64_wait_busy(void)
{
unsigned char byte = 0;
do
{
//拉低CS端为低电平
gpio_bit_write(GPIOF, GPIO_PIN_6, RESET);
//发送指令05h
spi_read_write_byte(0x05);
//接收状态寄存器值
byte = spi_read_write_byte(0Xff);
//恢复CS端为高电平
gpio_bit_write(GPIOF, GPIO_PIN_6, SET);
//判断BUSY位是否为1 如果为1说明在忙,重新读写BUSY位直到为0
}while( ( byte & 0x01 ) == 1 );
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
扇区擦除
W25Q64闪存芯片的内存分配是按照扇区(Sector)和块(Block)进行的,每个扇区的大小为4KB,每个块包含16个扇区,即一个块的大小为64KB。
W25Q64闪存芯片的扇区擦除是指将某个特定扇区中的数据全部擦除的操作。擦除操作会将扇区中的所有数据都置为1(即0xFF),恢复到初始状态。下面是W25Q64扇区擦除的一般流程:
- 写使能(Write Enable):首先,要确保闪存芯片处于可写状态。发送写使能命令,将闪存芯片设置为可写模式,解除写保护。
- 扇区擦除设置(Sector Erase Setup):向W25Q64发送扇区擦除设置命令,并指定要擦除的扇区地址。W25Q64支持多种扇区擦除命令,可以根据需要选择擦除一个或多个扇区。
- 扇区擦除确认(Sector Erase Confirm):等待扇区擦除确认。W25Q64芯片进行擦除操作需要一定的时间,具体时间可参考该芯片的规格书。在擦除操作进行期间,通常会读取状态寄存器忙位的方法来确定擦除是否完成。过早地读取擦除操作中的数据可能会导致不正确的结果。
- 扇区擦除完成:当扇区擦除成功后,状态寄存器将指示擦除操作完成。此时,该扇区中的数据已经全部被擦除为1。
扇区擦除操作是一种高级操作,需要小心谨慎地使用。在实际应用中,通常会结合编程逻辑和相应的控制器来管理闪存芯片的擦除和写入操作,以确保数据的安全性和完整性。
在使用扇区擦除操作时,有几个注意事项需要特别关注:
- 擦除范围:要确保擦除的范围是正确的,仅擦除目标扇区,避免误擦除其他扇区中的数据。在执行擦除操作之前,请务必仔细检查要擦除的扇区地址,并确保没有错误。
- 数据备份:由于扇区擦除操作将数据全部擦除为1(0xFF),在执行擦除之前,应该确保重要数据已经备份。擦除后,数据将无法恢复,因此在执行重要数据的扇区擦除操作之前,请务必做好数据备份的工作。
扇区擦除的时序图如下:
- 拉低CS端为低电平;
- 发送指令20h(0010_0000);
- 发送24位的扇区首地址;
- 恢复CS端为高电平;
具体实现代码如下:
以下代码跟扇区擦除时序图有一些差别,多了忙判断和写使能。
/**********************************************************
* 函 数 名 称:W25Q64_erase_sector
* 函 数 功 能:擦除一个扇区
* 传 入 参 数:addr=擦除的扇区号
* 函 数 返 回:无
* 作 者:LC
* 备 注:addr=擦除的扇区号,范围=0~15
**********************************************************/
void W25Q64_erase_sector(uint32_t addr)
{
//计算扇区号,一个扇区4KB=4096
addr *= 4096;
W25Q64_write_enable(); //写使能
W25Q64_wait_busy(); //判断忙,如果忙则一直等待
//拉低CS端为低电平
gpio_bit_write(GPIOF, GPIO_PIN_6, RESET);
//发送指令20h
spi_read_write_byte(0x20);
//发送24位扇区地址的高8位
spi_read_write_byte((uint8_t)((addr)>>16));
//发送24位扇区地址的中8位
spi_read_write_byte((uint8_t)((addr)>>8));
//发送24位扇区地址的低8位
spi_read_write_byte((uint8_t)addr);
//恢复CS端为高电平
gpio_bit_write(GPIOF, GPIO_PIN_6, SET);
//等待擦除完成
W25Q64_wait_busy();
}
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
写入数据
现在写入数据的前置步骤:擦除数据->写使能->判断忙 我们都完成了,只剩下将数据写入到对应地址中保存即可。
具体写入数据代码如下:
/**********************************************************
* 函 数 名 称:W25Q64_write
* 函 数 功 能:写数据到W25Q64进行保存
* 传 入 参 数:buffer=写入的数据内容 addr=写入地址 numbyte=写入数据的长度
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
**********************************************************/
void W25Q64_write(uint8_t* buffer, uint32_t addr, uint16_t numbyte)
{
unsigned int i = 0;
//擦除扇区数据
W25Q64_erase_sector(addr/4096);
//写使能
W25Q64_write_enable();
//忙检测
W25Q64_wait_busy();
//写入数据
//拉低CS端为低电平
gpio_bit_write(GPIOF, GPIO_PIN_6, RESET);
//发送指令02h
spi_read_write_byte(0x02);
//发送写入的24位地址中的高8位
spi_read_write_byte((uint8_t)((addr)>>16));
//发送写入的24位地址中的中8位
spi_read_write_byte((uint8_t)((addr)>>8));
//发送写入的24位地址中的低8位
spi_read_write_byte((uint8_t)addr);
//根据写入的字节长度连续写入数据buffer
for(i=0;i<numbyte;i++)
{
spi_read_write_byte(buffer[i]);
}
//恢复CS端为高电平
gpio_bit_write(GPIOF, GPIO_PIN_6, SET);
//忙检测
W25Q64_wait_busy();
}
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
读取数据
读取数据的时序图如下:
- 拉低CS端为低电平;
- 发送指令03h(0000_0011);
- 发送24位读取数据地址;
- 接收读取到的数据;
- 恢复CS端为高电平;
具体实现代码如下:
/**********************************************************
* 函 数 名 称:W25Q64_read
* 函 数 功 能:读取W25Q64的数据
* 传 入 参 数:buffer=读出数据的保存地址 read_addr=读取地址 read_length=读去长度
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
**********************************************************/
void W25Q64_read(uint8_t* buffer,uint32_t read_addr,uint16_t read_length)
{
uint16_t i;
//拉低CS端为低电平
gpio_bit_write(GPIOF, GPIO_PIN_6, RESET);
//发送指令03h
spi_read_write_byte(0x03);
//发送24位读取数据地址的高8位
spi_read_write_byte((uint8_t)((read_addr)>>16));
//发送24位读取数据地址的中8位
spi_read_write_byte((uint8_t)((read_addr)>>8));
//发送24位读取数据地址的低8位
spi_read_write_byte((uint8_t)read_addr);
//根据读取长度读取出地址保存到buffer中
for(i=0;i<read_length;i++)
{
buffer[i]= spi_read_write_byte(0XFF);
}
//恢复CS端为高电平
gpio_bit_write(GPIOF, GPIO_PIN_6, SET);
}
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
SPI FLASH验证
在main.c中编写如下代码:
#include "gd32vw55x.h"
#include "systick.h"
#include <stdio.h>
#include "bsp_usart.h"
#include "bsp_spi.h"
int main(void)
{
unsigned char buff[10] = {0};
/* 使能全局中断 */
eclic_global_interrupt_enable();
/* 中断分组 */
eclic_priority_group_set(ECLIC_PRIGROUP_LEVEL2_PRIO2);
//滴答定时器初始化
systick_config();
//串口0初始化115200
uart0InitConfig();
//W25Q64初始化
w25q64_init_config();
//获取W25Q64的设备ID
printf("ID = %X\r\n",W25Q64_readID());
//读取0地址长度为12个字节的数据到buff
W25Q64_read(buff, 0, 12);
//输出读取到的数据
printf("buff = %s\r\n",buff);
//往0地址写入12个字节的数据 “HELLO WORLD”
W25Q64_write("HELLO WORLD", 0, 12);
//读取0地址长度为12个字节的数据到buff
W25Q64_read(buff, 0, 12);
//输出读取到的数据
printf("buff = %s\r\n",buff);
while(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
验证现象:验证读写功能。
读取到ID为 0XEF16
;
从0地址读取出数据为空或者乱码,因为里面没有数据;
写入数据后,再读出数据为HELLO WORLD
;
关于这一章节的代码,可以在开发板资料/03 - 软件资料/代码例程/里面的015SPI-FLASH应用
。
下载中心跳转📦
资料下载中心:点击跳转🚀