首页 > 其他分享 >16. SPI读取FLASH

16. SPI读取FLASH

时间:2024-03-19 19:58:10浏览次数:25  
标签:16 FLASH 扇区 SPI W25Q128 擦除 address hspi

一、EN25Q128简介

  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内存结构框图

二、EN25Q128常用指令

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

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

#define W25Q128_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",最后在拉高片选,等待擦除完成。

三、EN25Q128常用寄存器

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),读取数据,支持连续读。
void W25Q128_ReadData(uint32_t address, uint8_t *data, uint16_t length)
{
    // 拉低片选
    W25Q128_CS(0);

    // 发送读数据命令
    SPI_SwapOneByte(W25Q128_READ_DATA);

    // 发送扇区地址
    SPI_SwapOneByte((address >> 16));
    SPI_SwapOneByte((address >> 8));
    SPI_SwapOneByte(address);

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

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

4.2、W25Q128擦除命令

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

    // 拉低片选
    W25Q128_CS(0);
    // 发送读状态寄存器1命令
    SPI_SwapOneByte(W25Q128_READ_STATTUS_REGISTER_1);
    // 等待写使能完成
    while((SPI_SwapOneByte(W25Q128_DUMMY_BYTE) & 0x01) || time--);
    // 拉高片选
    W25Q128_CS(1);
}
/**
 * @brief W25Q128扇区擦除函数
 * 
 * @param address 待删除的扇区的内存地址
 */
void W25Q128_SectorErase(uint32_t address)
{
    // 写使能
    W25Q128_WriteEnable();

    // 拉低片选
    W25Q128_CS(0);

    // 发送扇区擦除命令
    SPI_SwapOneByte(W25Q128_SECTOR_ERASE);

    // 发送扇区地址
    SPI_SwapOneByte((address >> 16));
    SPI_SwapOneByte((address >> 8));
    SPI_SwapOneByte(address);

    // 拉高片选
    W25Q128_CS(1);

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

4.3、W25Q128写操作命令

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

    // 拉低片选
    W25Q128_CS(0);

    // 发送页写入命令
    SPI_SwapOneByte(W25Q128_PAGE_PROGRAM);

    // 发送扇区地址
    SPI_SwapOneByte((address >> 16));
    SPI_SwapOneByte((address >> 8));
    SPI_SwapOneByte(address);

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

    // 拉高片选
    W25Q128_CS(1);

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

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

五、源码实现

5.1、原理图

FLASH原理图

SPI1引脚

FLASH片选引脚

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

5.2、程序源码

  SPI1 初始化函数内容如下:

SPI_HandleTypeDef g_spi1_handle;

/**
 * @brief SPI1初始化
 * 
 */
void SPI1_Init(void)
{
    g_spi1_handle.Instance = SPI1;                                              // SPI基地址
    g_spi1_handle.Init.Mode = SPI_MODE_MASTER;                                  // SPI主机模式
    g_spi1_handle.Init.Direction = SPI_DIRECTION_2LINES;                        // SPI全双工模式
    g_spi1_handle.Init.DataSize = SPI_DATASIZE_8BIT;                            // SPI帧格式
    g_spi1_handle.Init.CLKPolarity = SPI_POLARITY_LOW;                          // SPI时钟极性
    g_spi1_handle.Init.CLKPhase = SPI_PHASE_1EDGE;                              // SPI时钟相位
    g_spi1_handle.Init.NSS = SPI_NSS_SOFT;                                      // SPI软件NSS控制
    g_spi1_handle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;             // SPI时钟分频因子
    g_spi1_handle.Init.FirstBit = SPI_FIRSTBIT_MSB;                             // SPI数据高位先发送
    g_spi1_handle.Init.TIMode = SPI_TIMODE_DISABLE;                             // SPI不使用TI模式
    g_spi1_handle.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;             // SPI不使用CRC校验
    g_spi1_handle.Init.CRCPolynomial = 7;                                       // 设置CRC校验多项式

    HAL_SPI_Init(&g_spi1_handle);
}

  SPI 底层初始化函数内容如下:

/**
 * @brief SPI底层初始化函数
 * 
 * @param hspi SPI句柄
 */
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    if (hspi->Instance == SPI1)
    {
	__HAL_RCC_SPI1_CLK_ENABLE();                                            // 使能SPI1时钟
        __HAL_RCC_GPIOB_CLK_ENABLE();                                           // 使能SPI1对应的GPIO时钟

        GPIO_InitStruct.Pin = GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 ;            // SPI1的SCK引脚、MISO引脚和MOSI引脚
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;                                 // 复用推完输出
        GPIO_InitStruct.Pull = GPIO_NOPULL;                                     // 不使用上下拉电阻
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;                           // 输出速度
        GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;                              // 复用功能
        HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    }
}

  SPI 发送接收一个字节函数内容如下:

/**
 * @brief SPI发送接收一个字节函数
 * 
 * @param hspi SPI句柄
 * @param data 要发送的数据
 * @return uint8_t 接收的数据
 */
uint8_t SPI_TransmitReceiveOneByte(SPI_HandleTypeDef *hspi, uint8_t data)
{
    uint8_t receive = 0;

    HAL_SPI_TransmitReceive(hspi, &data, &receive, 1, 1000);

    return receive;
}

  W25Q128 初始化函数内容如下:

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

    // 使能W25Q128 CS对应GPIO引脚的时钟
    RCC_W25Q128_CS_GPIO_CLK_ENABLE();

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

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

  W25Q128 读取 ID 函数内容如下:

/**
 * @brief W25Q128读取ID函数
 * 
 * @param hspi SPI句柄
 * @return uint32_t W25Q128的ID
 */
uint32_t W25Q128_ReadId(SPI_HandleTypeDef *hspi)
{
    uint8_t temp[3] = {0};
    uint32_t id = 0;

    // 拉低片选
    W25Q128_CS(0);

    // 发送读ID命令
    SPI_TransmitReceiveOneByte(hspi, W25Q128_JEDEC_ID);

    HAL_SPI_Receive(hspi, temp, 3, 1000);

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

    // 拉高片选
    W25Q128_CS(1);

    // 返回ID
    return id;
}

  W25Q128 写使能函数内容如下:

/**
 * @brief W25Q128写使能函数
 * 
 * @param hspi 
 */
void W25Q128_WriteEnable(SPI_HandleTypeDef *hspi)
{
    // 拉低片选
    W25Q128_CS(0);
    // 发送写使能命令
    SPI_TransmitReceiveOneByte(hspi, W25Q128_WIRITE_ENABLE);
    // 拉高片选
    W25Q128_CS(1);
}

  W25Q128 等待完成函数内容如下:

/**
 * @brief W25Q64等待完成函数
 * 
 */
void W25Q128_WaitBusy(SPI_HandleTypeDef *hspi)
{
    uint32_t time = 0xFFFF;
    uint8_t status = 1;

    // 拉低片选
    W25Q128_CS(0);

    // 发送读状态寄存器1命令
    SPI_TransmitReceiveOneByte(hspi, W25Q128_READ_STATTUS_REGISTER_1);

    // 等待写使能完成
    while ((status & 0x01) && time--)
    {
        HAL_SPI_Receive(hspi, &status, 1, 1000);
    }
  
    // 拉高片选
    W25Q128_CS(1);
}

  WW25Q128 页写入函数内容如下:

/**
 * @brief W25Q128页写入函数
 * 
 * @param address 待写入数据的内存地址
 * @param data 待写入的数据
 * @param length 待写入数据的长度
 */
void W25Q128_PageProgram(SPI_HandleTypeDef *hspi, uint32_t address, uint8_t *data, uint16_t length)
{
    // 写使能
    W25Q128_WriteEnable(hspi);

    // 拉低片选
    W25Q128_CS(0);

    // 发送页写入命令
    SPI_TransmitReceiveOneByte(hspi, W25Q128_PAGE_PROGRAM);

    // 发送扇区地址
    SPI_TransmitReceiveOneByte(hspi, address >> 16);
    SPI_TransmitReceiveOneByte(hspi, address >> 8);
    SPI_TransmitReceiveOneByte(hspi, address);

    // 发送数据
    HAL_SPI_Transmit(hspi, data, length, 1000);

    // 拉高片选
    W25Q128_CS(1);

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

  W25Q128 扇区擦除函数内容如下:

/**
 * @brief W25Q128扇区擦除函数
 * 
 * @param address 待删除的扇区的内存地址
 */
void W25Q128_SectorErase(SPI_HandleTypeDef *hspi, uint32_t address)
{
    // 写使能
    W25Q128_WriteEnable(hspi);

    // 拉低片选
    W25Q128_CS(0);

    // 发送扇区擦除命令
    SPI_TransmitReceiveOneByte(hspi, W25Q128_SECTOR_ERASE);

    // 发送扇区地址
    SPI_TransmitReceiveOneByte(hspi, address >> 16);
    SPI_TransmitReceiveOneByte(hspi, address >> 8);
    SPI_TransmitReceiveOneByte(hspi, address);

    // 拉高片选
    W25Q128_CS(1);

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

  W25Q128 读取数据函数内容如下:

/**
 * @brief W25Q128读取数据函数
 * 
 * @param address 待读取数据的内存地址
 * @param data 保存读取的数据
 * @param length 读取数据的长度
 */
void W25Q128_ReadData(SPI_HandleTypeDef *hspi, uint32_t address, uint8_t *data, uint16_t length)
{
    // 拉低片选
    W25Q128_CS(0);

    // 发送读数据命令
    SPI_TransmitReceiveOneByte(hspi, W25Q128_READ_DATA);

    // 发送扇区地址
    SPI_TransmitReceiveOneByte(hspi, address >> 16);
    SPI_TransmitReceiveOneByte(hspi, address >> 8);
    SPI_TransmitReceiveOneByte(hspi, address);

    // 读取数据
    HAL_SPI_Receive(hspi, data, length, 1000);

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

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

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

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

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

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

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

  W25Q128 写数据函数内容如下:

/**
 * @brief W25Q128写数据函数
 * 
 * @param hspi SPI句柄
 * @param address 待写入数据的内存地址
 * @param data 待写入的数据
 * @param length 待写入数据的个数
 */
void W25Q128_WriteData(SPI_HandleTypeDef *hspi, 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_w25q128_buffer;
    uint16_t i = 0;

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

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

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

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

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

  有关时钟配置函数请在 STM32 的时钟系统 篇章查看。

  有关 USART1 的配置请在 串口通信 篇章查看。

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);
    HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);                         // 设置中断优先级分组

    USART1_Init(115200);
    SPI1_Init();

    W25Q128_Init();

    id = W25Q128_ReadId(&g_spi1_handle);
    printf("id:%#X\r\n", id);

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

    while (1)
    {
   
    }
  
    return 0;
}

标签:16,FLASH,扇区,SPI,W25Q128,擦除,address,hspi
From: https://www.cnblogs.com/kurome/p/18083785

相关文章

  • 02_STM32软件+硬件SPI读写W25Q64(HAL库)
    目录1、SPI简介2、SPI时序单元2.1模式0(应用最多)2.2模式12.3 模式22.4 模式33、SPI移位示意图4、简单软件SPI代码(HAL库)5、简单硬件SPI读写W25Q64(HAL库)6、例程下载1、SPI简介2、SPI时序单元2.1模式0(应用最多)2.2模式12.3 模式2模式2与模式0类似,区别在......
  • GTC 2024 开幕,英伟达发布新一代 GPU 架构;Apple ID 或将淘汰丨 RTE 开发者日报 Vol.168
       开发者朋友们大家好: 这里是「RTE开发者日报」,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享RTE(RealTimeEngagement)领域内「有话题的新闻」、「有态度的观点」、「有意思的数据」、「有思考的文章」、「有看点的会议」,但内容仅代表编辑的个人......
  • SPI协议
    学习资料来源:https://www.bilibili.com/video/BV14o4y1Y7A1?p=13&vd_source=432ba293ecfc949a4174ab91ccc526d6 SPI(serialperipheralinterface)是串行外设接口的缩写,SPI是一种高速的、全双工、同步的串行通信总线;SPI采用主从方式工作,一般有一个主设备和多个从设备;SPI需要至少4......
  • LY1169 [ 20230328 CQYC省选模拟赛 T1 ] 传奇特级超空间
    题意设\(f_{n,m}\)表示\(m\)维空间能被\(n\)个\(m-1\)维空间划分的最大区域数。求\(\sum_{i=0}^mf_{n,i}\)\(n,m\le10^{18},p\le2\times10^7\)Sol注意到:\(f_{n,m}=f_{n-1,m-1}+f_{n-1,m}\)。不难想到\(f\)应该是组合数的前缀......
  • 1948.Educational Codeforces Round 163 - sol
    202403补题效率低下。场上发挥并不是很好,A~E都是简单的,而场上没有去推F的式子,只是找了找规律,然后发现是一个不可做的东西就下播了。如果直接推式子就会很快地做出来,还是非常可惜。A.SpecialCharactersYouaregivenaninteger\(n\).Yourtaskistobuildast......
  • UVM - 16(TLM)
    uvm搭建分层的testbench,组件之间的通信通过tlm组件的通信接口sequencer和driver之家通信机制uvm已经做好,在agent的connect_phase将driver和sequencer的接口进行连接组件接口SV实现组件之间的通信使用中间接口对象UVM组件之间的通信:TLM1.0/2.0UVMTLM1.0p......
  • NAND和NOR Flash 完全应用笔记(应用调试篇)
    本文要点:给出了华为三星及英特尔的常用FLASH参考电路;结合datasheet解说2bit以及4bit的NANDFLASH;调试思路总结以及简单的调试经验分享;最新的3DXpoint技术介绍。首先,我要用这张图来说明存储器近70年的发展历程,纵观这70年的发展,可以发现主要是在容量,速度以及寿命等方面......
  • GEE C16 Change Detection 变化检测
    导读:1.创建和探索如何读取一个假彩色无云的Landsat合成影像;2.计算归一化燃烧指数;3.定位变化的区域;4.生成变更图并使用阈值对变更进行分类。一、准备影像1.1select用法 1.2多时相配准、辐射大气校正这里用的LANDSAT/LC08/C02/T1_L2数据集。 二、创建假彩色合成影......
  • 692、基于51单片机的自行车(调速,LCD1602)
    毕设帮助、开题指导、技术解答(有偿)见文末。目录一、设计功能二、proteus仿真三、原理图四、程序源码五、资料包括一、设计功能自行车调速系统1、使用LCD1602显示当前速度和设置速度2、使用电位器设置当前速度,模拟把手调速3、按键启停系统4、速度控制......
  • 694、基于51单片机的报警器(模拟量,上限,TLC1543,LCD1602)
    毕设帮助、开题指导、技术解答(有偿)见文末。目录一、设计功能二、proteus仿真三、原理图四、程序源码五、资料包括一、设计功能简易报警器(可用作CO2、CH4、CO、粉尘、酒精、NH2等模拟量传感器报警)1、测量物理量的浓度值2、如果浓度值超过阀值,报警。3、......