W25Q32 SPI Flash开发入门到精通:详细讲解+代码解析

目录

一、认识 SPI NOR Flash

1.什么是 NOR Flash?

2.Page/Sector/Block 概念

二、读取设备ID案例

三、写入数据流程案例

1. 写使能

2.器件忙判断

3.扇区擦除

4.写入数据

5.读取数据

四、SPI FLASH验证


一、认识 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 KB16 × 4 KB 扇区0xD8 “一次擦除整块”
扇区(Sector)4 KB16 × 256 B 页面0x20 “一次擦除一扇区”
页(Page)256 B1 个“写入最小单位”缓冲区(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 字节。

那么地址就这么用:

  1. 页地址(高 2 字节):告诉芯片要操作哪一页(第几块里的第几页)。

  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。

读取步骤:

  1. 将CS端拉低为低电平;

  2. 发送指令 90H(1001_0000);

  3. 发送地址 000000H(0000_0000_0000_0000_0000_0000);

  4. 读取制造商ID,根据数据手册可以知道制造商ID为EFh;

  5. 读取设备ID,根据数据手册可以知道设备ID为16h;

  6. 恢复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)命令。写使能的作用是启用对闪存芯片的写入操作。在默认情况下,闪存芯片处于保护状态,禁止对其进行写入操作,主要是为了防止误操作对数据的损坏。

操作步骤:

  1. 将CS端拉低为低电平;

  2. 发送指令 06H(0000_0110);

  3. 恢复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。

读取状态寄存器的时序图如下:

  1. 拉低CS端为低电平;

  2. 发送指令05h(0000_0101);

  3. 接收状态寄存器值;

  4. 恢复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),且执行前要备份好重要数据,因为一旦擦除所有内容都无法恢复。

扇区擦除的时序图如下:

  1. 拉低CS端为低电平;

  2. 发送指令20h(0010_0000);

  3. 发送24位的扇区首地址;

  4. 恢复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端为高电平;
>     

![](https://i-blog.csdnimg.cn/direct/f3d8846d1cda49f8afedef4d4dcca210.png)

```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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值