17. 模拟SPI读取FLASH

一、W25Q128简介

  EN25Q128 是大容量 SPI FLASH 产品,EN25Q128 的容量为 128Mb(16M 字节)。学习这个芯片可以参考华邦公司的 W25Q128 芯片,因为它们是完全兼容的。

  FLASH 是常见的用于存储数据的半导体器件,它具有容量大、可重复擦写、按“扇区/块”擦除、掉电后数据可继续保存的特性。常见的 FLASH 主要有 NOR FLASH 和 NAND FLASH 两种类型。

  NOR FLASH 的地址线和数据线分开,它可以按“字节”读写数据,符合 CPU 的指令译码执行要求,所以假如 NOR FLASH 上存储了代码指令,CPU 给 NOR FLASH 一个地址,NOR FLASH 就能向 CPU 返回一个数据让 CPU 执行,中间不需要额外的处理操作。因此可以用 NOR FLASH 直接作为嵌入式 MCU 的程序存储空间。

  NAND FLASH 的数据和地址线共用,只能按“块”来读写数据,假如 NAND FLASH 上存储了代码指令,CPU 给 NAND FLASH 地址后,它无法直接返回该地址的数据,所以不符合指令译码要求。若代码存储在 NAND FLASH 上,可以把它先加载到 RAM 存储器上,再由 CPU 执行。所以在功能上可以认为 NOR FLASH 是一种断电后数据不丢失的 RAM,但它的擦除单位与 RAM 有区别,且读写速度比 RAM 要慢得多。

  NOR FLASH与 NAND FLASH 在数据写入前都需要有擦除操作,但实际上 NOR Flash 的一个 bit 可以从 1变成 0,而要从 0 变 1 就要擦除后再写入,NAND Flash 这两种情况都需要擦除。擦除操作的最小单位为 “扇区/块” ,这意味着有时候即使只写一字节的数据,则这个“扇区/块”上之前的数据都可能会被擦除。

  FLASH 也有对应的缺点,我们在使用过程中需要尽量去规避这些问题:一是 FLASH 的使用寿命,另一个是可能的位反转。

  使用寿命体现在:读写上是 FLASH 的擦除次数都是有限的(NOR FLASH 普遍是 10 万次左右),当它的使用接近寿命的时候,可能会出现写操作失败。由于 NAND FLASH 通常是整块擦写,块内有一位失效整个块就会失效,这被称为坏块。使用 NAND FLASH 最好通过算法扫描介质找出坏块并标记为不可用,因为坏块上的数据是不准确的。

  位反转是数据位写入时为 1,但经过一定时间的环境变化后可能实际变为 0 的情况,反之亦然。位反转的原因很多,可能是器件特性也可能与环境、干扰有关,由于位反转的问题可能存在,所以 FLASH 存储器需要“探测/错误更正(EDC/ECC)”算法来确保数据的正确性。

  W25Q128 将 16M 的容量分为 256 个块( Block),每个块大小为 64K 字节,每个块又分为 16 个扇区( Sector),每个扇区 4K 个字节。 W25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。这样我们需要给 W25Q128 开辟一个至少 4K 的缓存区,这样对 SRAM 要求比较高,要求芯片必须有 4K 以上 SRAM 才能很好的操作。

  W25Q128 的擦写周期多达 10W 次,具有 20 年的数据保存期限,支持电压为 2.7~3.6V,W25Q128 支持标准的 SPI,还支持双输出/四输出的 SPI,最大 SPI 时钟可以到 80Mhz(双输出时相当于 160Mhz,四输出时相当于 320M)。

W25Q128内存结构框图

二、W25Q128常用指令

  NOR FLASH 的指令非常多,一般我们值需要 5 条指令就可以完成对 NOR FLASH 的基本操作。

W25Q128常用命令

指令 名称 作用
0x06 写使能 写入数据/擦除之前,必须先发送写使能
0x05 读 SR1 判定 FLASH 是否处于空闲状态,擦除用
0x03 读数据 用于读取 NOR FLASH 数据
0x02 页写 用于写入 NOR FLAS 数据,最多写 246 字节
0x20 扇区擦除 扇区擦除指令,最小擦除单位(4096 字节)
#define FLASH_WRITE_ENABLE                          0x06
#define FLASH_WRITE_DISABLE                         0x04

#define FLASH_READ_STATUS_REGISTER1                 0x05
#define FLASH_READ_STATUS_REGISTER2                 0x35
#define FLASH_READ_STATUS_REGISTER3                 0x15

#define FLASH_WRITE_STATUS_REGISTER                 0x01

#define FLASH_READ_DATA                             0x03
#define FLASH_FAST_READ                             0x0B
#define FLASH_FAST_READ_DUAL_OUTPUT                 0x3B
#define FLASH_FAST_READ_QUAD_OUTPUT                 0x6B
#define FLASH_FAST_READ_DUAL_IO	                    0xBB
#define FLASH_FAST_READ_QUAD_IO                     0xEB

#define FLASH_WORD_READ_QUAD_IO                     0xE7
#define FLASH_OCTAL_WORD_READ_QUAD_IO               0xE3

#define FLASH_PAGE_PROGRAM                          0x02
#define FLASH_QUAD_INPUT_PAGE_PROGRAM               0x32

#define FLASH_SECTOR_ERASE_4KB                      0x20
#define FLASH_BLOCK_ERASE_32KB                      0x52
#define FLASH_BLOCK_ERASE_64KB                      0xD8
#define FLASH_CHIP_ERASE                            0xC7

#define FLASH_ERASE_PROGRAM_SUSPEND                 0x75
#define FLASH_ERASE_PROGRAM_RESUME                  0x7A

#define FLASH_POWER_DOWN                            0xB9

#define FLASH_RELEASE_POWER_DOWN_DEVICE_ID          0xAB
#define FLASH_MANUFACTURER_DEVICE_ID                0x90
#define FLASH_READ_UNIQUE_ID                        0x4B
#define FLASH_JEDEC_ID                              0x9F

#define FLASH_HIGH_PERFORMANCE_MODE                 0xA3
#define FLASH_CONTINUOUS_READ_MODE_RESET            0xFF

#define FLASH_DUMMY_BYTE                            0xFF

2.1、写使能指令(0x06)

W25Q128写使能命令

  写使能指令将状态寄存器中的写使能锁存器(WEL)位设置为 1。WEL 位必须设置在写数据、扇区擦除、块擦除、芯片擦除、写状态寄存器和擦除/程序安全寄存器指令之前。

  写使能指令的输入方式是拉低片选线,然后发送指令 "0x06",最后在拉高片选线。

2.2、读状态寄存器1指令(0x05)

W25Q128读状态寄存器指令

  读状态寄存器指令允许读取 8 位状态寄存器。

  首先拉低片选线,然后发送指令 "0x05",返回状态寄存器 1 的数据,最后在拉高片选线。

2.3、读数据指令(0x03)

W25Q128读数据寄存器

  读取数据指令允许从内存中按顺序读取一个或多个数据字节。在每个字节的数据被移出后,地址自动增加到下一个更高的地址,允许连续的数据流。这意味着只要时钟继续,就可以用一条指令访问整个内存。读状态寄存器指令允许读取 8 位状态寄存器。

  首先拉低片选线,然后发送指令 "0x03",然后在发送 24 位地址,返回读取的数据,最后在拉高片选线。

2.4、页写指令(0x02)

W25Q128页写指令

  页写指令允许在先前擦除(FFh)内存位置对 1 到 256 字节(一页)的数据进行写入。在设备接受页写指令(状态寄存器位WEL=1)之前,必须执行写使能指令。

  首先拉低片选线,然后发送指令代码 "02h",接着发送一个 24 位地址和至少一个数据字节。当数据被发送到设备时,片选线必须在指令的整个长度内保持低电平。

2.5、扇区擦除指令(0x20)

W25Q128扇区擦除指令

  由于 FLASH 的特性决定它只能把原来为 "1" 的数据位改写为 "0",而原来为 "0" 的数据位不能直接改写为 "1"。因此,我们需要扇区擦除指令,将指定扇区的内容全部擦除为 "1"。在设备接受扇区擦除之前,必须执行写使能指令指令(状态寄存器位 WEL必须等于 1)。

  首先拉低片选线,然后发送指令代码 "20h",接着发送一个 24 位地址,FLASH 会把当前地址所在的扇区中的数据全部擦除为 "1",最后在拉高片选,等待擦除完成。

三、W25Q128常用寄存器

W25Q128状态寄存器1

  状态寄存器 1 的 位 0 BUSY 指示当前状态,0:空闲状态(硬件自动完成);1:当前处于忙碌状态;

  状态寄存器 1 的 位 1 WEL 写使能位,0:写禁止,不能页编程/扇区、块、片擦除/写状态寄存器;1:写使能;

W25Q128状态寄存器2

四、W25Q128操作步骤

4.1、W25Q128读操作命令

  1. 发送读命令(03H)。
  2. 发送 24 位地址,分 3 次发送。
  3. 发送空字节(0xFF),读取数据,支持连续读。
#define FLASH_CS_GPIO_PORT              GPIOB
#define FLASH_CS_GPIO_PIN               GPIO_PIN_14
#define RCC_FLASH_CS_GPIO_CLK_ENABLE()  __HAL_RCC_GPIOB_CLK_ENABLE()

#define FLASH_CS(x)                     do{ x ? \
                                            HAL_GPIO_WritePin(FLASH_CS_GPIO_PORT,FLASH_CS_GPIO_PIN, GPIO_PIN_SET):\
                                            HAL_GPIO_WritePin(FLASH_CS_GPIO_PORT,FLASH_CS_GPIO_PIN, GPIO_PIN_RESET);\
                                        }while(0)
/**
 * @brief 往FLASH读写一个字节
 * 
 * @param data 写入的一个字节数数据
 * @return uint8_t 读取的一个字节的数据
 */
uint8_t FLASH_ReadAndWriteOneByte(uint8_t data)
{
    return SPI_SwapOneByte(data);
}
/**
 * @brief FLASH发送地址函数
 * 
 * @param address 内存地址
 */
void FLASH_SendAddress(uint32_t address)
{
    FLASH_ReadAndWriteOneByte((uint8_t)((address)>>16));                                  // 发送 bit23 ~ bit16 地址
    FLASH_ReadAndWriteOneByte((uint8_t)((address)>>8));                                   // 发送 bit15 ~ bit8  地址
    FLASH_ReadAndWriteOneByte((uint8_t)address);                                          // 发送 bit7 ~ bit0  地址
}
/**
 * @brief FLASH读数据函数
 * 
 * @param address 待写入数据的内存地址
 * @param data 指向数据指针
 * @param length 待写入的数据长度
 */
void FLASH_ReadData(uint32_t address, uint8_t *data, uint16_t length)
{
    // 拉低片选
    FLASH_CS(0);

    // 发送读数据命令
    FLASH_ReadAndWriteOneByte(FLASH_READ_DATA);

    // 发送扇区地址
    FLASH_SendAddress(address);

    // 读取数据
    for (uint32_t i = 0; i < length; i++)
    {
        data[i] = FLASH_ReadAndWriteOneByte(FLASH_DUMMY_BYTE);
    }

    // 拉高片选
    FLASH_CS(1);
}

4.2、W25Q128擦除命令

  1. 发送写使能命令(06H)。
  2. 发送擦除删除命令(02H)。
  3. 发送要擦除扇区的 24 位地址,分 3 次发送,会自动清除该地址所在的扇区。
  4. 等待擦除完成。
/**
 * @brief FLASH写使能函数
 * 
 */
void FLASH_WriteEnable(void)
{
    // 拉低片选
    FLASH_CS(0);
    // 发送写使能命令
    FLASH_ReadAndWriteOneByte(FLASH_WRITE_ENABLE);
    // 拉高片选
    FLASH_CS(1);
}
/**
 * @brief W25Q64等待完成函数
 * 
 */
void FLASH_WaitBusy(void)
{
    uint32_t time = 0xFFFF;

    // 拉低片选
    FLASH_CS(0);
    // 发送读状态寄存器1命令
    FLASH_ReadAndWriteOneByte(FLASH_READ_STATUS_REGISTER1);
    // 等待写使能完成
    while((FLASH_ReadAndWriteOneByte(FLASH_DUMMY_BYTE) & 0x01) || time--);
    // 拉高片选
    FLASH_CS(1);
}
/**
 * @brief FLASH扇区擦除函数
 * 
 * @param address 待删除的扇区的内存地址
 */
void FLASH_SectorErase(uint32_t address)
{
    // 写使能
    FLASH_WriteEnable();

    // 拉低片选
    FLASH_CS(0);

    // 发送扇区擦除命令
    FLASH_ReadAndWriteOneByte(FLASH_SECTOR_ERASE_4KB);

    // 发送扇区地址
    FLASH_SendAddress(address);

    // 拉高片选
    FLASH_CS(1);

    // 等待擦除完成
    FLASH_WaitBusy();
}

4.3、W25Q128写操作命令

  1. 发送写使能命令(06H)。
  2. 发送页写命令(02H),一次最多写入 256 字节。
  3. 发送要写入数据的 24 位内存地址,分 3 次发送。
  4. 发送要写入的数据,一次最多写入 256 字节。
  5. 等待写入完成。
/**
 * @brief FLASH页写入函数
 * 
 * @param address 待写入数据的内存地址
 * @param data 待写入的数据
 * @param length 待写入数据的长度
 */
void FLASH_PageProgram(uint32_t address, uint8_t *data, uint16_t length)
{
    // 写使能
    FLASH_WriteEnable();

    // 拉低片选
    FLASH_CS(0);

    // 发送页写入命令
    FLASH_ReadAndWriteOneByte(FLASH_PAGE_PROGRAM);

    // 发送扇区地址
    FLASH_SendAddress(address);

    // 发送数据
    for (uint16_t i = 0; i < length; i++)
    {
        FLASH_ReadAndWriteOneByte(data[i]);
    }

    // 拉高片选
    FLASH_CS(1);

    // 等待写使能完成
    FLASH_WaitBusy();
}

在向 FLASH 写入前,我们最后先擦除数据,否则,容易造成数据错乱。这是因为 FLASH 的只能把原来为 "1" 的数据位改写为 "0",而原来为 "0" 的数据位不能直接改写为 "1"。

五、原理图

FLASH原理图

SPI1接口引脚

FLASH片选引脚

  通过原理图,可知 FLASH 使用 SPI1,它的片选引脚接着 PB14 引脚,使用软件管理的方式。

六、程序源码

  FLASH 初始化函数内容如下:

/** * @brief FLASH初始化函数
 * 
 */
void FLASH_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    RCC_FLASH_CS_GPIO_CLK_ENABLE();

    GPIO_InitStruct.Pin = FLASH_CS_GPIO_PIN;                                    // FLASH的CS引脚
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;                                 // 推挽输出
    GPIO_InitStruct.Pull = GPIO_NOPULL;                                         // 不使用上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;                               // 输出速度
    HAL_GPIO_Init(FLASH_CS_GPIO_PORT, &GPIO_InitStruct);

    FLASH_CS(1);                                                                // 片选引脚默认为高电平,不选中从机
}

  FLASH 读取 ID 函数内容如下:

/**
 * @brief FLASH读取ID函数
 * 
 * @return uint32_t FLASH的ID
 */
uint32_t FLASH_ReadID(void)
{
    uint8_t temp[3] = {0};
    uint32_t id = 0;

    // 拉低片选
    FLASH_CS(0);

    // 发送读ID命令
    SPI_SwapOneByte(FLASH_JEDEC_ID);

    // 读取ID
    for (uint8_t i = 0; i < 3; i++)
    {
        temp[i] = SPI_SwapOneByte(FLASH_DUMMY_BYTE);
        id <<= 8;
        id |= temp[i];
    }

    // 拉高片选
    FLASH_CS(1);

    // 返回ID
    return id;
}

  无校验写 FLASH 函数内容如下:

/**
 * @brief 无校验写FLASH函数
 * 
 * @param address 待写入数据的内存地址
 * @param data 待写入的数据
 * @param length 待写入数据的个数
 * 
 * @note 
 *      确保所写地址范围的数据全为0xFF,否则在非0xFF处写入失败。
 *      该函数具有自动换页的功能
 *  
 */
void FLASH_WriteData_NoCheck(uint32_t address, uint8_t *data, uint16_t length)
{
    uint16_t pageRemain = 256 - address % 256;                                  // 单页剩余的字节数,得到地址在某页的位置

    // 当写入的数据小于单页剩余的字节数,将写入的字节数赋值给单页剩余的字节数
    pageRemain =  (length <= pageRemain ? length : pageRemain);
  
    while (1)
    {
        // 当写入字节比页内剩余地址还少的时候,一次性写完
        // 当写入字节比页内剩余地址多的时候,先写完页内剩余地址,然后根据剩余长度进行不同的处理
        FLASH_PageProgram(address, data, pageRemain);

  
        if (length == pageRemain)                                               // 写入完成
        {
            break;
        }
        else
        {
            address += pageRemain;                                              // 写地址偏移
            data += pageRemain;                                                 // 写数据指针偏移
            length -= pageRemain;                                               // 写入总长度减去已经写入的个数

            // 当剩余长度大于一页256时,可以一次写一页
            // 当剩余数据小于一页,可以一次写完
            pageRemain = (length > 256 ? 256 : length);
        }
    }
}

  该函数通过判断传参中的写入字节的长度与单页剩余的字节数,来决定是否是需要在新页写入剩下的字节。

  FLASH 写数据函数内容如下:

uint8_t g_flash_buffer[4096];

/**
 * @brief FLASH写数据函数
 * 
 * @param address 待写入数据的内存地址
 * @param data 待写入的数据
 * @param length 待写入数据的个数
 */
void FLASH_WriteData(uint32_t address, uint8_t *data, uint16_t length)
{
    uint32_t sectorPosition = address / 4096;                                   // 扇区地址
    uint16_t sectorOffset = address % 4096;                                     // 在扇区中的偏移地址
    uint16_t sectorRemain = 4096 - sectorOffset;                                // 扇区剩余空间大小
    uint8_t *pBuff = g_flash_buffer;
    uint16_t i = 0;

    // 当写入的数据小扇区剩余空间大小,将写入的字节数赋值给扇区剩余空间大小
    sectorRemain = (length <= sectorRemain ? length : sectorRemain);
  
    while (1)
    {
        // 读出整个扇区的内容
        FLASH_ReadData(sectorPosition * 4096, pBuff, 4096);

        // 校验数据是否需要擦除
        for (i = 0; i < sectorRemain; i++)
        {
            if (pBuff[sectorOffset + i] != 0xFF)
            {
                break;                                                          // 需要擦除,直接退出for循环
            }
        }

        // 需要擦除
        if (i < sectorRemain)
        {
            // 擦除这个扇区
            FLASH_SectorErase(sectorPosition * 4096);

            // 将待写入的数据拷贝到缓冲区
            for (i = 0; i < sectorRemain; i++)
            {
                pBuff[i + sectorOffset] = data[i];
            }
            //写入整个扇区
            FLASH_WriteData_NoCheck(sectorPosition * 4096, pBuff, 4096);
        }
        else
        {
            // 对于已经擦除的,直接写入扇区剩余空间
            FLASH_WriteData_NoCheck(address, data, sectorRemain);
        }
  
        // 写入完成
        if (length == sectorRemain)
        {
            break;
        }
        // 写入未完成
        else
        {
            sectorPosition++;                                                   // 扇区加1,使用下一个扇区
            sectorOffset = 0;                                                   // 扇区偏移地址为0

            address += sectorRemain;                                            // 写地址偏移
            data += sectorRemain;                                               // 写数据指针偏移
            length -= sectorRemain;                                             // 写入总长度减去已经写入的个数

            // 当剩余长度大于扇区长度4096时,可以一次写一整个扇区
            // 当剩余长度小于扇区长度4096时,下一个扇区可以写完
            sectorRemain = (length > 4096 ? 4096 : length);
        }
    }
}

  该函数可以在 FLASH 的任意地址开始写入任意长度(必须不超过 FLASH 的容量)的数据。首先获得首地址(WriteAddress)所在的扇区,并计算在扇区内的偏移,然后判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,再先看看是否要擦除,如果不要,则直接写入数据即可,如果要则读出整个扇区,在偏移处开始写入指定长度的数据,然后擦除这个扇区,再一次性写入。当所需要写入的数据长度超过一个扇区的长度的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此循环,直到写入结束。这里我们还定义了一个 g_flash_buff 的全局变量,用于擦除时缓存扇区内的数据。

int main(void)
{
    uint32_t id = 0;
    uint8_t writeDataArray[] = {0x11, 0x22, 0x33, 0x44, 0x55};
    uint8_t readDataArray[100] = {0};

    HAL_Init();
    System_Clock_Init(8, 336, 2, 7);
    Delay_Init(168);

    UART_Init(&g_usart1_handle, USART1, 115200);
    SPI_Init();
    FLASH_Init();

    id = FLASH_ReadID();
    printf("id:%#X\r\n", (int)id);

    FLASH_WriteData(4096, writeDataArray, 5);
    FLASH_WriteData(4096 + 5, writeDataArray, 5);
    FLASH_ReadData(4096, readDataArray, 11);
    for (uint8_t i = 0; i < 11; i++)
    {
        printf("%d:%#x\r\n", i, readDataArray[i]);
    }

    while (1)
    {
   
    }
  
    return 0;
}
posted @ 2023-11-22 19:12  星光樱梦  阅读(75)  评论(0编辑  收藏  举报