目录
一、认识 SPI NOR Flash
1.什么是 NOR Flash?
-
NOR Flash 以随机读快、擦写慢、寿命高著称,广泛用作固件存储或小容量数据日志。
-
GD25Q32:容量 32 Mbit(4 MB),内部按 64 KB 块(Block)、4 KB 扇区(Sector)、256 B 页(Page)三级划分。
2.Page/Sector/Block 概念
单位 | 大小 | 颗粒划分 | 擦写/编程方式 |
---|---|---|---|
块(Block) | 64 KB | 16 × 4 KB 扇区 | 0xD8 “一次擦除整块” |
扇区(Sector) | 4 KB | 16 × 256 B 页面 | 0x20 “一次擦除一扇区” |
页(Page) | 256 B | 1 个“写入最小单位”缓冲区(Page Buffer) | 0x02 “程序一个页面” |
- 擦除:只能以扇区或块为单位,一次把所有 bit 置 1;
- 编程:只能把 1 → 0,且一次最多 256 B(Page Program)。
总结:
整颗 Flash 分成 256 块(Block),每块大小 64 KB。
每块又分成 16 个扇区(Sector),所以一共 256 × 16 = 4096 个扇区。
每个扇区大小 4 KB。
扇区里再细分成 16 页(Page),因为 4 KB ÷ 256 B = 16。
每一页刚好 256 字节。
那么地址就这么用:
页地址(高 2 字节):告诉芯片要操作哪一页(第几块里的第几页)。
字节地址(低 1 字节):告诉芯片在那一页里的第几个字节。
操作时先擦除整个扇区(只针对 4 KB 对齐的扇区),再按页写入(一次最多写 256 B),读的话可以直接按地址连续读。这样一来,你就能像找书页和页内位置一样,精准地在 Flash 里读写数据。
二、读取设备ID案例
根据GD25Q32的数据手册可知,读取ID的指令使用90H就好。在数据手册中给出了其读取的时序图。
Read Manufacturer/Device ID命令是Power-Down /Device ID命令的替代方案,该命令提供JEDEC分配的制造商ID和特定的设备ID。该命令通过驱动cs#引脚低电平并移动命令代码“90H”,后面跟着一个000000H的24位地址(A23-A0)来启动。之后,制造商ID和设备ID首先在SCLK的降沿上以最高有效位(MSB)移出,如图28所示。如果24位地址最初设置为000001H,则首先读取设备ID。
读取步骤:
将CS端拉低为低电平;
发送指令 90H(1001_0000);
发送地址 000000H(0000_0000_0000_0000_0000_0000);
读取制造商ID,根据数据手册可以知道制造商ID为EFh;
读取设备ID,根据数据手册可以知道设备ID为16h;
恢复CS端为高电平;
实现代码:
//读取芯片ID
//读取设备ID
uint16_t GD25Q32_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;
}
三、写入数据流程案例
1. 写使能
在进行写入操作之前,需要使用到写使能(Write Enable)命令。写使能的作用是启用对闪存芯片的写入操作。在默认情况下,闪存芯片处于保护状态,禁止对其进行写入操作,主要是为了防止误操作对数据的损坏。
操作步骤:
将CS端拉低为低电平;
发送指令 06H(0000_0110);
恢复CS端为高电平;
具体实现代码如下:
//发送写使能
void GD25Q32_write_enable(void)
{
//拉低CS端为低电平
gpio_bit_write(GPIOF, GPIO_PIN_6, RESET);
//发送指令06h
spi_read_write_byte(0x06);
//拉高CS端为高电平
gpio_bit_write(GPIOF, GPIO_PIN_6, SET);
}
2.器件忙判断
在GD25Q322的数据手册中,有3个状态寄存器,可以判断当前GD25Q32是否正在传输、写入、读取数据等,我们每一次要对GD25Q32进行操作时,需要先判断GD25Q32是否在忙。如果在忙的状态,我们去操作GD25Q32,很可能会导致数据丢失,并且操作失败。而判断是否忙,是通过状态寄存器1的S0为进行判断,状态寄存器1的地址为0X05。
读取状态寄存器的时序图如下:
拉低CS端为低电平;
发送指令05h(0000_0101);
接收状态寄存器值;
恢复CS端为高电平;
具体实现代码如下:
/**********************************************************
* 函 数 名 称:GD25Q32_wait_busy
* 函 数 功 能:检测线路是否繁忙
* 传 入 参 数:无
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
**********************************************************/
void GD25Q32_wait_busy(void)
{
unsigned char byte = 0;
do
{
//拉低CS端为低电平
W25QXX_CS_ON(0);
//发送指令05h
spi_read_write_byte(0x05);
//接收状态寄存器值
byte = spi_read_write_byte(0Xff);
//恢复CS端为高电平
W25QXX_CS_ON(1);
//判断BUSY位是否为1 如果为1说明在忙,重新读写BUSY位直到为0
}while( ( byte & 0x01 ) == 1 );
}
3.扇区擦除
Flash 的最小擦除单位是扇区,每个扇区大小固定为 4 KB(块大小是 16 个扇区、64 KB)。要把一个扇区“清空”,需要先发送写使能命令(0x06),解除写保护,然后拉低 CS 发送扇区擦除命令(0x20)加上目标扇区的 24 位对齐地址,再拉高 CS。擦除开始后,芯片内部会把该扇区所有位都变成 1(也就是 0xFF),这一步通常要几十毫秒才能完成。期间可以反复读取状态寄存器(0x05)里的 WIP(忙)位,等它变成 0 才说明擦除完成;切记扇区地址一定要是 4 KB 边界(低 12 位全 0),且执行前要备份好重要数据,因为一旦擦除所有内容都无法恢复。
扇区擦除的时序图如下:
拉低CS端为低电平;
发送指令20h(0010_0000);
发送24位的扇区首地址;
恢复CS端为高电平;
/**********************************************************
* 函 数 名 称:GD25Q32_erase_sector
* 函 数 功 能:擦除一个扇区
* 传 入 参 数:addr=擦除的扇区号
* 函 数 返 回:无
* 作 者:LC
* 备 注:addr=擦除的扇区号,范围=0~15
**********************************************************/
void GD25Q32_erase_sector(uint32_t addr)
{
//计算扇区号,一个扇区4KB=4096
addr *= 4096;
GD25Q32_write_enable(); //写使能
GD25Q32_wait_busy(); //判断忙,如果忙则一直等待
//拉低CS端为低电平
W25QXX_CS_ON(0);
//发送指令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端为高电平
W25QXX_CS_ON(1);
//等待擦除完成
GD25Q32_wait_busy();
}
4.写入数据
现在写入数据的前置步骤:擦除数据->写使能->判断忙 我们都完成了,只剩下将数据写入到对应地址中保存即可。
/**********************************************************
* 函 数 名 称:GD25Q32_write
* 函 数 功 能:写数据到GD25Q32进行保存
* 传 入 参 数:buffer=写入的数据内容 addr=写入地址 numbyte=写入数据的长度
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
**********************************************************/
void GD25Q32_write(uint8_t* buffer, uint32_t addr, uint16_t numbyte)
{
unsigned int i = 0;
//擦除扇区数据
GD25Q32_erase_sector(addr/4096);
//写使能
GD25Q32_write_enable();
//忙检测
GD25Q32_wait_busy();
//写入数据
//拉低CS端为低电平
W25QXX_CS_ON(0);
//发送指令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端为高电平
W25QXX_CS_ON(1);
//忙检测
GD25Q32_wait_busy();
}```
### 5.读取数据
> 读取数据的时序图如下:
>
> 1. 拉低CS端为低电平;
>
> 2. 发送指令03h(0000\_0011);
>
> 3. 发送24位读取数据地址;
>
> 4. 接收读取到的数据;
>
> 5. 恢复CS端为高电平;
>

```cpp
/**********************************************************
* 函 数 名 称:GD25Q32_read
* 函 数 功 能:读取GD25Q32的数据
* 传 入 参 数:buffer=读出数据的保存地址 read_addr=读取地址 read_length=读去长度
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
**********************************************************/
void GD25Q32_read(uint8_t* buffer,uint32_t read_addr,uint16_t read_length)
{
uint16_t i;
//拉低CS端为低电平
W25QXX_CS_ON(0);
//发送指令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端为高电平
W25QXX_CS_ON(1);
}
四、SPI FLASH验证
创建两个文件,分别命名为spi_flash.c和spi_flash.h。往里面写入完整代码:
spi_flash.c
#include "spi_flash.h"
/**********************************************************
* 函 数 名 称:bsp_spi_init
* 函 数 功 能:初始化SPI
* 传 入 参 数:无
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
**********************************************************/
void bsp_spi_init(void)
{
rcu_periph_clock_enable(BSP_GPIO_RCU); // 使用A端口
rcu_periph_clock_enable(BSP_SPI_RCU); // 使能SPI0
//引脚复用
gpio_af_set(BSP_GPIO_PORT, GPIO_AF_5, BSP_SPI_SCK);
gpio_af_set(BSP_GPIO_PORT, GPIO_AF_5, BSP_SPI_MISO);
gpio_af_set(BSP_GPIO_PORT, GPIO_AF_5, BSP_SPI_MOSI);
//引脚模式
gpio_mode_set(BSP_GPIO_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, BSP_SPI_SCK);
gpio_mode_set(BSP_GPIO_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, BSP_SPI_MISO);
gpio_mode_set(BSP_GPIO_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, BSP_SPI_MOSI);
//输出模式
gpio_output_options_set(BSP_GPIO_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, BSP_SPI_SCK);
gpio_output_options_set(BSP_GPIO_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, BSP_SPI_MISO);
gpio_output_options_set(BSP_GPIO_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, BSP_SPI_MOSI);
//开启CS引脚时钟
rcu_periph_clock_enable(BSP_SPI_NSS_RCU);
//配置CS引脚模式
gpio_mode_set(BSP_GPIO_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, BSP_SPI_NSS);
//配置CS输出模式
gpio_output_options_set(BSP_GPIO_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, BSP_SPI_NSS);
//GD25Q32不选中
gpio_bit_write(BSP_GPIO_PORT, BSP_SPI_NSS, SET);
//SPI参数定义结构体
spi_parameter_struct spi_init_struct;
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; // SPI时钟预调因数为4
spi_init_struct.endian = SPI_ENDIAN_MSB; // 高位在前
//将参数填入SPI0
spi_init(BSP_SPI, &spi_init_struct);
//使能SPI
spi_enable(BSP_SPI);
}
uint8_t spi_read_write_byte(uint8_t dat)
{
//等待发送缓冲区为空
while(RESET == spi_i2s_flag_get(BSP_SPI, SPI_FLAG_TBE) );
//通过SPI4发送一个字节数据
spi_i2s_data_transmit(BSP_SPI, dat);
//等待接收缓冲区不空标志
while(RESET == spi_i2s_flag_get(BSP_SPI, SPI_FLAG_RBNE) );
//读取并返回在SPI4读取到的单字节数据
return spi_i2s_data_receive(BSP_SPI);
}
//读取芯片ID
//读取设备ID
uint16_t GD25Q32_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;
}
//发送写使能
void GD25Q32_write_enable(void)
{
//拉低CS端为低电平
W25QXX_CS_ON(0);
//发送指令06h
spi_read_write_byte(0x06);
//拉高CS端为高电平
W25QXX_CS_ON(1);
}
/**********************************************************
* 函 数 名 称:GD25Q32_wait_busy
* 函 数 功 能:检测线路是否繁忙
* 传 入 参 数:无
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
**********************************************************/
void GD25Q32_wait_busy(void)
{
unsigned char byte = 0;
do
{
//拉低CS端为低电平
W25QXX_CS_ON(0);
//发送指令05h
spi_read_write_byte(0x05);
//接收状态寄存器值
byte = spi_read_write_byte(0Xff);
//恢复CS端为高电平
W25QXX_CS_ON(1);
//判断BUSY位是否为1 如果为1说明在忙,重新读写BUSY位直到为0
}while( ( byte & 0x01 ) == 1 );
}
/**********************************************************
* 函 数 名 称:GD25Q32_erase_sector
* 函 数 功 能:擦除一个扇区
* 传 入 参 数:addr=擦除的扇区号
* 函 数 返 回:无
* 作 者:LC
* 备 注:addr=擦除的扇区号,范围=0~15
**********************************************************/
void GD25Q32_erase_sector(uint32_t addr)
{
//计算扇区号,一个扇区4KB=4096
addr *= 4096;
GD25Q32_write_enable(); //写使能
GD25Q32_wait_busy(); //判断忙,如果忙则一直等待
//拉低CS端为低电平
W25QXX_CS_ON(0);
//发送指令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端为高电平
W25QXX_CS_ON(1);
//等待擦除完成
GD25Q32_wait_busy();
}
/**********************************************************
* 函 数 名 称:GD25Q32_write
* 函 数 功 能:写数据到GD25Q32进行保存
* 传 入 参 数:buffer=写入的数据内容 addr=写入地址 numbyte=写入数据的长度
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
**********************************************************/
void GD25Q32_write(uint8_t* buffer, uint32_t addr, uint16_t numbyte)
{
unsigned int i = 0;
//擦除扇区数据
GD25Q32_erase_sector(addr/4096);
//写使能
GD25Q32_write_enable();
//忙检测
GD25Q32_wait_busy();
//写入数据
//拉低CS端为低电平
W25QXX_CS_ON(0);
//发送指令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端为高电平
W25QXX_CS_ON(1);
//忙检测
GD25Q32_wait_busy();
}
/**********************************************************
* 函 数 名 称:GD25Q32_read
* 函 数 功 能:读取GD25Q32的数据
* 传 入 参 数:buffer=读出数据的保存地址 read_addr=读取地址 read_length=读去长度
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
**********************************************************/
void GD25Q32_read(uint8_t* buffer,uint32_t read_addr,uint16_t read_length)
{
uint16_t i;
//拉低CS端为低电平
W25QXX_CS_ON(0);
//发送指令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端为高电平
W25QXX_CS_ON(1);
}
spi_flash.h
#ifndef __SPI_FLASH_H__
#define __SPI_FLASH_H__
#include "gd32f4xx.h"
#define BSP_GPIO_RCU RCU_GPIOA
#define BSP_SPI_RCU RCU_SPI0
#define BSP_SPI_NSS_RCU RCU_GPIOA
#define BSP_GPIO_PORT GPIOA
#define BSP_GPIO_AF GPIO_AF_5
#define BSP_SPI SPI0
#define BSP_SPI_NSS GPIO_PIN_4
#define BSP_SPI_SCK GPIO_PIN_5
#define BSP_SPI_MISO GPIO_PIN_6
#define BSP_SPI_MOSI GPIO_PIN_7
#define W25QXX_CS_ON(x) (gpio_bit_write(BSP_GPIO_PORT, BSP_SPI_NSS, x?1:0))
void bsp_spi_init(void);
uint8_t spi_read_write_byte(uint8_t dat);
uint16_t GD25Q32_readID(void);
void GD25Q32_write_enable(void);
void GD25Q32_wait_busy(void);
void GD25Q32_erase_sector(uint32_t addr);
void GD25Q32_write(uint8_t* buffer, uint32_t addr, uint16_t numbyte);
void GD25Q32_read(uint8_t* buffer,uint32_t read_addr,uint16_t read_length);
#endif
注意:如果你的串口助手打出来的是乱码,那就需要使用能把接收格式转为UTF-8的串口助手了。
注:参考来自嘉立创开源网站
https://lceda001.feishu.cn/wiki/Zawdwg0laig3Qnk2XuxcKrQRn2g