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 SPI_FLASH_WRITE_ENABLE                                  0x06
#define SPI_FLASH_WRITE_DISABLE                                 0x04

#define SPI_FLASH_READ_STATUS_REGISTER1                         0x05
#define SPI_FLASH_READ_STATUS_REGISTER2                         0x35
#define SPI_FLASH_READ_STATUS_REGISTER3                         0x15

#define SPI_FLASH_WRITE_STATUS_REGISTER1                        0x01
#define SPI_FLASH_WRITE_STATUS_REGISTER2                        0x31
#define SPI_FLASH_WRITE_STATUS_REGISTER3                        0x11

#define SPI_FLASH_READ_EXTENDED_ADDRESS_REGISTER                0xC8
#define SPI_FLASH_WRITE_EXTENDED_ADDRESS_REGISTER               0xC5

#define SPI_FLASH_ENTER_4BYTE_ADDRESS_MODE                      0xB7
#define SPI_FLASH_EXIT_4BYTE_ADDRESS_MODE                       0xE9

#define SPI_FLASH_READ_DATA                                     0x03
#define SPI_FLASH_READ_DATA_WITH_4BYTE_ADDRESS                  0x13
#define SPI_FLASH_FAST_READ                                     0x0B
#define SPI_FLASH_FAST_READ_WITH_4BYTE_ADDRESS                  0x0C
#define SPI_FLASH_FAST_READ_DUAL_OUTPUT                         0x3B
#define SPI_FLASH_FAST_READ_DUAL_OUTPUT_WITH_4BYTE_ADDRESS      0x3C
#define SPI_FLASH_FAST_READ_QUAD_OUTPUT                         0x6B
#define SPI_FLASH_FAST_READ_QUAD_OUTPUT_WITH_4BYTES_ADDRESS     0x6C
#define SPI_FLASH_FAST_READ_DUAL_IO                             0xBB
#define SPI_FLASH_FAST_READ_DUAL_IO_WITH_4BYTES_ADDRESS         0xBC
#define SPI_FLASH_FAST_READ_QUAD_IO                             0xEB
#define SPI_FLASH_FAST_READ_QUAD_IO_WITH_4BYTES_ADDRESS         0xEC

#define SPI_FLASH_WORD_READ_QUAD_IO                             0xE7
#define SPI_FLASH_OCTAL_WORD_READ_QUAD_IO                       0xE3

#define SPI_FLASH_SET_BURST_WITH_WARP                           0x77

#define SPI_FLASH_PAGE_PROGRAM                                  0x02
#define SPI_FLASH_QUAD_INPUT_PAGE_PROGRAM                       0x32

#define SPI_FLASH_SECTOR_ERASE_4KB                              0x20
#define SPI_FLASH_BLOCK_ERASE_32KB                              0x52
#define SPI_FLASH_BLOCK_ERASE_64KB                              0xD8
#define SPI_FLASH_CHIP_ERASE                                    0xC7

#define SPI_FLASH_ERASE_PROGRAM_SUSPEND                         0x75
#define SPI_FLASH_ERASE_PROGRAM_RESUME                          0x7A

#define SPI_FLASH_POWER_DOWN                                    0xB9

#define SPI_FLASH_RELEASE_POWER_DOWN_DEVICE_ID                  0xAB
#define SPI_FLASH_MANUFACTURER_DEVICE_ID                        0x90
#define SPI_FLASH_READ_UNIQUE_ID                                0x4B
#define SPI_FLASH_JEDEC_ID                                      0x9F

#define SPI_FLASH_HIGH_PERFORMANCE_MODE                         0xA3
#define SPI_FLASH_CONTINUOUS_READ_MODE_RESET                    0xFF

#define SPI_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操作步骤

#define W25Q16                              16
#define W25Q32                              32
#define W25Q64                              64
#define W25Q128                             128
#define W25Q256                             256

#define W25Qxxx                             W25Q128

#define SPI_FLASH_CS_GPIO_PORT              GPIOB
#define SPI_FLASH_CS_GPIO_PIN               GPIO_PIN_14
#define RCC_SPI_FLASH_CS_GPIO_CLK_ENABLE()  __HAL_RCC_GPIOB_CLK_ENABLE()

#define SPI_FLASH_CS(x)                     do{ x ? \
                                                HAL_GPIO_WritePin(SPI_FLASH_CS_GPIO_PORT, SPI_FLASH_CS_GPIO_PIN, GPIO_PIN_SET):\
                                                HAL_GPIO_WritePin(SPI_FLASH_CS_GPIO_PORT, SPI_FLASH_CS_GPIO_PIN, GPIO_PIN_RESET);\
                                            }while(0)
/**
 * @brief 往SPI FLASH读写一个字节
 * 
 * @param data 写入的一个字节数数据
 * @return uint8_t 读取的一个字节的数据
 */
static uint8_t SPI_FLASH_ReadAndWriteOneByte(uint8_t data)
{
    return SPI_Simulate_SwapOneByte(data);
}
/**
 * @brief SPI FLASH发送地址函数
 * 
 * @param address 内存地址
 */
static void SPI_FLASH_SendAddress(uint32_t address)
{
    if (W25Qxxx == W25Q256)                                                     // 只有W25Q256支持4字节地址模式
    {
        SPI_FLASH_ReadAndWriteOneByte((uint8_t)((address) >> 24));              // 发送 bit31 ~ bit24 地址
    } 
    SPI_FLASH_ReadAndWriteOneByte((uint8_t)((address) >> 16));                  // 发送 bit23 ~ bit16 地址
    SPI_FLASH_ReadAndWriteOneByte((uint8_t)((address) >> 8));                   // 发送 bit15 ~ bit8  地址
    SPI_FLASH_ReadAndWriteOneByte((uint8_t)address);                            // 发送 bit7 ~ bit0  地址
}

4.1、W25Q128读操作命令

  1. 发送读命令(03H)。
  2. 发送 24 位地址,分 3 次发送。
  3. 发送空字节(0xFF),读取数据,支持连续读。
/**
 * @brief SPI FLASH读数据函数
 * 
 * @param address 待写入数据的内存地址
 * @param data 指向数据指针
 * @param length 待写入的数据长度
 */
void SPI_FLASH_ReadData(uint32_t address, uint8_t *data, uint16_t length)
{
    // 拉低片选
    SPI_FLASH_CS(0);

    // 发送读数据命令
    SPI_FLASH_ReadAndWriteOneByte(SPI_FLASH_READ_DATA);

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

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

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

4.2、W25Q128擦除命令

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

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

    // 拉低片选
    SPI_FLASH_CS(0);

    // 发送扇区擦除命令
    SPI_FLASH_ReadAndWriteOneByte(SPI_FLASH_SECTOR_ERASE_4KB);

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

    // 拉高片选
    SPI_FLASH_CS(1);

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

4.3、W25Q128写操作命令

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

    // 拉低片选
    SPI_FLASH_CS(0);

    // 发送页写入命令
    SPI_FLASH_ReadAndWriteOneByte(SPI_FLASH_PAGE_PROGRAM);

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

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

    // 拉高片选
    SPI_FLASH_CS(1);

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

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

五、原理图

FLASH原理图

SPI1接口引脚

FLASH片选引脚

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

六、程序源码

  FLASH 初始化函数内容如下:

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

    RCC_SPI_FLASH_CS_GPIO_CLK_ENABLE();

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

    SPI_FLASH_CS(1);                                                            // 片选引脚默认为高电平,不选中从机
  
    if (W25Qxxx == W25Q256)                                                     // SPI_FLASH为W25Q256,必须使能4字节地址模式
    {
        int temp = SPI_FLASH_ReadRegisterSR(3);                                 // 读取状态寄存器3,判断地址模式

        if ((temp & 0x01) == 0)                                                 // 如果不是4字节地址模式,则进入4字节地址模式
        {
            SPI_FLASH_WriteEnable();                                            // 写使能
            temp |= (1 << 1);                                                   //  ADP=1,上电4位地址模式 
            SPI_FLASH_WriteRegisterSR(3, temp);                                 // 写SR3
  
            SPI_FLASH_CS(0);
            SPI_FLASH_ReadAndWriteOneByte(SPI_FLASH_ENTER_4BYTE_ADDRESS_MODE);  // 使能4字节地址指令
            SPI_FLASH_CS(1);
        }
    }
}

  读取 FLASH 的状态寄存器函数:

/**
 * @brief 读取SPI FLASH的状态寄存器函数
 * 
 * @param regno 寄存器编号,范围: 1~3
 * @return uint8_t 寄存器的值
 * 
 * @note
 *  状态寄存器1:
 *      BIT7  6   5   4   3   2   1   0
 *      SPR   RV  TB BP2 BP1 BP0 WEL BUSY
 *      SPR: 默认0,状态寄存器保护位,配合WP使用
 *      TB,BP2,BP1,BP0: SPI FLASH区域写保护设置
 *      WEL: 写使能锁定
 *      BUSY: 忙标记位(1: 忙; 0: 空闲)
 *      默认: 0x00
 *
 *  状态寄存器2:
 *      BIT7  6   5   4   3   2   1   0
 *      SUS   CMP LB3 LB2 LB1 (R) QE  SRP1
 *
 *  状态寄存器3:
 *      BIT7      6    5    4   3   2   1   0
 *      HOLD/RST  DRV1 DRV0 (R) (R) WPS ADP ADS
 */
static uint8_t SPI_FLASH_ReadRegisterSR(uint8_t regno)
{
    uint8_t byte = 0, command = 0;

    switch (regno)
    {
        case 1:
            command = SPI_FLASH_READ_STATUS_REGISTER1;                          // 读状态寄存器1指令
            break;

        case 2:
            command = SPI_FLASH_READ_STATUS_REGISTER2;                          // 读状态寄存器2指令
            break;

        case 3:
            command = SPI_FLASH_READ_STATUS_REGISTER3;                          // 读状态寄存器3指令
            break;

        default:
            command = SPI_FLASH_READ_STATUS_REGISTER1;
            break;
    }

    SPI_FLASH_CS(0);
    SPI_FLASH_ReadAndWriteOneByte(command);                                     // 发送读寄存器命令
    byte = SPI_FLASH_ReadAndWriteOneByte(SPI_FLASH_DUMMY_BYTE);                 // 读取一个字节
    SPI_FLASH_CS(1);
  
    return byte;
}

  写状态寄存器函数:

/**
 * @brief 写状态寄存器函数
 * 
 * @param regno 状态寄存器的编号,范围: 1~3
 * @param data 要写入状态寄存器的值
 */
static void SPI_FLASH_WriteRegisterSR(uint8_t regno, uint8_t data)
{
    uint8_t command = 0;

    switch (regno)
    {
        case 1:
            command = SPI_FLASH_WRITE_STATUS_REGISTER1;                         // 写状态寄存器1指令
            break;

        case 2:
            command = SPI_FLASH_WRITE_STATUS_REGISTER2;                         // 写状态寄存器2指令
            break;

        case 3:
            command = SPI_FLASH_WRITE_STATUS_REGISTER3;                         // 写状态寄存器3指令
            break;

        default:
            command = SPI_FLASH_WRITE_STATUS_REGISTER1;
            break;
    }

    SPI_FLASH_CS(0);
    SPI_FLASH_ReadAndWriteOneByte(command);                                     // 发送读寄存器命令
    SPI_FLASH_ReadAndWriteOneByte(data);                                        // 写入一个字节
    SPI_FLASH_CS(1);
}

  FLASH 读取 ID 函数内容如下:

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

    // 拉低片选
    SPI_FLASH_CS(0);

    // 发送读ID命令
    SPI_FLASH_ReadAndWriteOneByte(SPI_FLASH_JEDEC_ID);

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

    // 拉高片选
    SPI_FLASH_CS(1);

    // 返回ID
    return id;
}

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

/**
 * @brief 无校验写SPI FLASH函数
 * 
 * @param address 待写入数据的内存地址
 * @param data 待写入的数据
 * @param length 待写入数据的个数
 * 
 * @note 
 *      确保所写地址范围的数据全为0xFF,否则在非0xFF处写入失败。
 *      该函数具有自动换页的功能
 */
static void SPI_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)
    {
        // 当写入字节比页内剩余地址还少的时候,一次性写完
        // 当写入字节比页内剩余地址多的时候,先写完页内剩余地址,然后根据剩余长度进行不同的处理
        SPI_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 SPI FLASH写数据函数
 * 
 * @param address 待写入数据的内存地址
 * @param data 待写入的数据
 * @param length 待写入数据的个数
 */
void SPI_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_SPI_FLASH_buffer;
    uint16_t i = 0;

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

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

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

            // 将待写入的数据拷贝到缓冲区
            for (i = 0; i < sectorRemain; i++)
            {
                pBuff[i + sectorOffset] = data[i];
            }
            //写入整个扇区
            SPI_FLASH_WriteData_NoCheck(sectorPosition * 4096, pBuff, 4096);
        }
        else
        {
            // 对于已经擦除的,直接写入扇区剩余空间
            SPI_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();
    HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);

    UART_Init(&g_usart1_handle, USART1, 115200);
    SPI_Simulate_Init();
    SPI_FLASH_Init();

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

    SPI_FLASH_WriteData(4096, writeDataArray, 5);
    SPI_FLASH_WriteData(4096 + 5, writeDataArray, 5);
    SPI_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  星光映梦  阅读(799)  评论(0)    收藏  举报