【Ai-WB2高级篇】 Flash编程原理

[复制链接]
查看813 | 回复5 | 2024-9-13 22:09:18 | 显示全部楼层 |阅读模式

本帖最后由 浅末哈哈 于 2024-9-18 08:37 编辑

本帖最后由 浅末哈哈 于 2024-9-18 08:36 编辑

本帖最后由 浅末哈哈 于 2024-9-18 08:35 编辑

本帖最后由 浅末哈哈 于 2024-9-17 17:12 编辑

本帖最后由 浅末哈哈 于 2024-9-14 08:11 编辑

本帖最后由 浅末哈哈 于 2024-9-14 08:10 编辑

本帖最后由 浅末哈哈 于 2024-9-13 22:09 编辑

本帖最后由 浅末哈哈 于 2024-9-13 20:44 编辑

本帖最后由 浅末哈哈 于 2024-9-12 23:15 编辑

本帖最后由 浅末哈哈 于 2024-9-12 22:35 编辑

本帖最后由 浅末哈哈 于 2024-9-11 23:02 编辑

Flash编程原理

上一篇SPI详解:【Ai-WB2中级篇】Ai-WB2+SPI - Ai-WB2系列 - 物联网开发者社区-安信可论坛 - Powered by Discuz! (ai-thinker.com)

1、实验结果展示

  1. 在写入填充值之前,先进行一次读取数据,读取1K数据并打印;

    image.png

  2. 在相同的地址中进行填充值(0x55),再进行读取; image.png 驱动能够正确对flash进行读写操作。

2、Flash主要内容

2.1、Flash的主要知识

  • Flash中单位描述为块(Block)、扇区(Sector)、页(Page)。三者之间的关系是:块 > 扇区 > 页;
  • Flash擦写规则:
    • 最小可擦除单位:扇区;
    • 可选择擦除单位:扇区、块、全片擦除;
    • 最小可写入单位:1 byte;
    • 未写入Flash初始值:0xFF;
    • Flash不存在覆写功能。Flash只能从1 -> 0,不能从0 -> 1,所以在写入有数据的部分前,需要对写入部分进行擦除,再将数据进行写入;

2.2、W25Q64中需要关注的部分

Flash部分选择的是W25Q64。根据W25Q64的数据手册上,我们需要关注以下几个部分:

  1. 硬件封装部分,主要规格参数;
  2. SPI通信模式;
  3. 存储区内部结构;
  4. 对Flash操作的寄存器与指令;

2.2.1、硬件规格参数

W25Q64的封装是8-pin SOIC类型。 image.png 根据数据手册上基本介绍,W25Q64有以下特性:

  1. 容量:8MB(64Mbits)。
  2. 支持标准SPI(单线),Dual SPI(2线),Qual SPI(4线)。(注:这里几线的意思是几个输出数据线)
  3. W25Q64 支持的最高时钟是 133MHz。
  4. 每个扇区最少支持 10 万次擦写,可以保存 20 年数据。
  5. 页大小是 256 字节,支持页编程,也就是一次编写 256 个字节,也可以一个一个编写。
  6. 支持 4KB 为单位的扇区擦除,也可以 32KB 或者 64KB 为单位的擦除。

擦写耗时image.png

  • 页编程时间典型值0.4ms,最大值3ms;
  • 扇区擦除时间典型值45ms,最大值400ms;
  • 块擦除(32KB)时间典型值120ms,最大值1600ms;
  • 块擦除(64KB)时间典型值150ms,最大值2000ms;
  • 整片擦除典型值20s,最大值100s;

还有一个比较关键的参数,支持时钟频率image.png 关于读取数据指令 0x03,所支持最大频率为50MHz,其他指令支持频率最大为133MHz(3.0-3.6V)。

2.2.2、SPI通信模式

W25Q64支持三种SPI的连接方式:标准SPI(单线),Dual SPI(2线),Qual SPI(4线)

  • 支持两线 SPI,用到引脚 CLK、CS、IO0、IO1 。
  • 支持四线 SPI,用到引脚 CLK、CS、IO0、IO1,IO2、IO3。 都能对W25Q64进行操作,不过Dual/Qual SPI模式一般使用在XIP(executed in place)的情况。

这里我使用的flash模块使用的是标准SPI接口。

W25Q64能够在SPI的mode0mode3的场景下进行通信。

以读取JEDEC ID指令为例:

image.png SPI中mode0和mode3都能与模块进行正常通信。

2.2.3、存储区的内部结构

W25Q64内部存储区结构: image.png

块(Block):包含128个块,每个块大小为64KB;

扇区(Sector):每个块中包含16个扇区,每个扇区大小为4KB;

最小擦除单位:扇区,但是写入数据最小单位是1byte,所以在写入数据之前应该对相应扇区进行擦除。

2.2.4、W25Q64中的状态寄存器

W25Q64中有三个寄存器,但是只是使用标准SPI进行Flash的读写,就只需要关注Status Register-1

Status Register-1: image.png 我们只需要关注BUSY这一位,其他位不用关注。 BUSY位是只读的状态寄存器(S0)被设置为1状态时,表示设备正在执行程序(可能是在擦除芯片)或写状态寄存器指令。 这个时候设备将忽略传来的指令, 除了读状态寄存器和擦除暂停指令,写指令或写状态指令无效,当 BUSY位为 0 状态时,指示设备已经执行完毕,可以进行下一步操作。

2.2.5、Flash的相关操作与指令

需要编写flash的驱动,就需要关注如何与flash进行通信。

现将W25Q64的操作指令分为:擦除,写入数据,写入状态寄存器,读取数据,读取设备信息,读取寄存器这几种分类,但是对于flash的基本功能只需要其中的几种指令。

所有指令可以查阅W25Q64的数据手册,这里只介绍驱动中使用了的指令:

#define W25X_WRITE_ENABLE                0x06   //写使能
 #define W25X_WRITE_DISABLE               0x04   //写失能
 #define W25X_READ_STATUS_REG             0x05   //读取状态寄存器1
 #define W25X_WRITE_STATUS_REG            0x01   //写状态寄存器1
 #define W25X_READ_DATA                   0x03   //读取数据
 #define W25X_PAGE_PROGRAM                0x02   //页编程
 #define W25X_BLOCK_ERASE                 0xD8   //块擦除
 #define W25X_SECTOR_ERASE                0x20   //扇区擦除
 #define W25X_CHIP_ERASE                  0xC7   //整片擦除
 #define W25X_POWER_DOWN                  0xB9   //关机
 #define W25X_DEVICE_ID                   0xAB   //读取设备ID
 #define W25X_MANBUFAT_DEVICE_ID          0x90   //读取厂商ID
 #define W25X_JEDEC_DEVICE_ID             0x9F   //读取JEDEC ID

特别注意:根据不同的连接方式,所使用的指令都是不同的,这里所描述背景是基于标准SPI的连接方式。若需要Dual/Qual SPI的连接方式,需要查阅数据手册。

3、主要文件结构

  • 文件目录结构: image.png
  • 程序主要框架: image.png
  • 核心构建思想:bsp_spi_bus.c中,有三个非常关键的参数贯穿整个Flash驱动:
uint16_t g_buffer_len;

 uint8_t g_tx_buffer[SPI_BUFFER_SIZE];
 uint8_t g_rx_buffer[SPI_BUFFER_SIZE];
  • g_buffer_len– buffer长度;
  • g_tx_buffer– 开辟的tx_buffer空间,方便后期增加DMA功能;
  • g_rx_buffer– 开辟的rx_buffer空间,方便后期增加DMA功能;

首先要明确,标准SPI通信是全双工的通信,即在发送数据的同时能够接收数据

所以这里直接使用 hosal_spi.c中的api:

/**
  * @brief spi send data and recv
  *
  * @param[in]  spi      the spi device
  * @param[in]  tx_data  spi send data
  * @param[out] rx_data  spi recv data
  * @param[in]  size     spi data to be sent and recived
  * @param[in]  timeout  timeout in milisecond, set this value to HAL_WAIT_FOREVER
  *                      if you want to wait forever
  *
  * @return  
  *        - 0 : success 
  *        - other : error
  */
 int hosal_spi_send_recv(hosal_spi_dev_t *spi, uint8_t *tx_data, uint8_t *rx_data, uint16_t size, uint32_t timeout);

将其封装为底层SPI发送和接收函数,提供给驱动层(bsp_spi_flash.c)调用:

/**
  * @brief send and recivce
  * 
  */
 void bsp_spi_bus_transfer(void)
 {
     if (g_buffer_len > SPI_BUFFER_SIZE)
     {
         printf("buffer size overflow\r\n");
         return;
     }
     if (hosal_spi_send_recv(&spi0, (uint8_t *)g_tx_buffer, (uint8_t *)g_rx_buffer, g_buffer_len, 1000000) != 0)
     {
         printf("spi transfer error\r\n");
     }
 }

在驱动层对 g_tx_buffer进行填充,或者等待 g_rx_buffer接收,伪代码:

//扇区擦除示例伪代码

g_buffer_len = 0;
g_tx_buffer[g_buffer_len ++] = W25X_SECTOR_ERASE;

g_tx_buffer[g_buffer_len ++] = ((sector_addr & 0xFF0000) >> 16);
g_tx_buffer[g_buffer_len ++] = ((sector_addr & 0xFF00) >> 8);
g_tx_buffer[g_buffer_len ++] = (sector_addr & 0xFF);

bsp_spi_bus_transfer();

4、编写驱动思路

主要思路有两个:

  1. 在操作flash前,需要将写使能打开,操作结束后关闭写使能
  2. 在写入数据之前,需要先对相应位置进行擦除;

4.1、主要函数讲解

flash的基本功能:擦除,读取数据,写入数据

4.1.1、擦除数据功能函数

包含全片擦除,块擦除,扇区擦除三个功能函数。

/**
 * @brief erase chip
 * 
 */
void flash_erase_chip(void);

/**
 * @brief erase block
 * 
 * @param block_addr - the first block address
 * @return uint8_t 
 *      - 1:erase block done
 *      - 0:erase block error
 */
uint8_t flash_erase_block(uint32_t block_addr);

/**
 * @brief erase sector
 * 
 * @param sector_addr - the first sector address
 * @return uint8_t 
 *      - 1: erase sector done
 *      - 0: erase sector error
 */
uint8_t flash_erase_sector(uint32_t sector_addr);

flash_erase_sector函数为例:

uint8_t flash_erase_sector(uint32_t sector_addr)
{
    if(flash_get_sector_first_addr_flag(sector_addr) == 0)
    {   
        printf("erase sector error, first sector address is wrong\r\n");
        return 0;
    }
    else
    {
        flash_write_enable();

        flash_set_cs(0);
        g_buffer_len = 0;
        g_tx_buffer[g_buffer_len ++] = W25X_SECTOR_ERASE;

        g_tx_buffer[g_buffer_len ++] = ((sector_addr & 0xFF0000) >> 16);
        g_tx_buffer[g_buffer_len ++] = ((sector_addr & 0xFF00) >> 8);
        g_tx_buffer[g_buffer_len ++] = (sector_addr & 0xFF);

        bsp_spi_bus_transfer();

        flash_wait_for_write_end();
        flash_set_cs(1);

        printf("erase sector done\r\n");
    }
    return 1;

}

程序流程框图:

image.png

4.1.2、读取数据功能函数

读取功能函数包含读取JEDEC_ID,读取Flash存储内容两个功能函数。

/**
 * @brief read flash JEDEC ID
 * 
 * @return uint32_t - JEDEC ID
 */
uint32_t flash_read_id(void);

/**
 * @brief read flash data
 * 
 * @param pbuf save data when read flash data
 * @param read_addr read address
 * @param size read size
 */
void flash_buffer_read(uint8_t * pbuf, uint32_t read_addr, uint32_t size);

针对 flash_buffer_read进行展开

void flash_buffer_read(uint8_t * pbuf, uint32_t read_addr, uint32_t size)
{
    uint16_t rem;
    uint16_t i;

    if((size == 0) || (read_addr + size) > flash_dev_info.total_size)
    {
        printf("size = %d, (read_addr + size) = %d\r\n", size, (read_addr + size));
        printf("flash read buffer error\r\n");
        return;
    }

    flash_write_enable();

    flash_set_cs(0);
    g_buffer_len = 0;

    g_tx_buffer[g_buffer_len ++] = W25X_READ_DATA;
    g_tx_buffer[g_buffer_len ++] = ((read_addr & 0xFF0000) >> 16);
    g_tx_buffer[g_buffer_len ++] = ((read_addr & 0xFF00) >> 8);
    g_tx_buffer[g_buffer_len ++] = (read_addr & 0xFF);

    bsp_spi_bus_transfer();     //这里先让flash进入读取模式,避免在读取的时候遗漏前4个bytes

    for(i = 0; i < (size / SPI_BUFFER_SIZE); i++)
    {
        g_buffer_len = SPI_BUFFER_SIZE;
        bsp_spi_bus_transfer();     //开始正式读取数据

        memcpy(pbuf, g_rx_buffer, SPI_BUFFER_SIZE);
        pbuf += SPI_BUFFER_SIZE;
    }

    rem = size % SPI_BUFFER_SIZE;

    if(rem > 0)
    {
        g_buffer_len = rem;
        bsp_spi_bus_transfer();
        memcpy(pbuf, g_rx_buffer, rem);
    }

    flash_wait_for_write_end();
    flash_set_cs(1);
}

程序流程框图:

image.png

4.1.3、写入数据功能函数

写入数据功能函数包含页写入(不推荐使用),buffer写入(推荐):

/**
 * @brief write data on page
 * 
 * @param pbuf prepare to write data
 * @param write_addr write address
 * @param size buffer size
 */
void flash_page_write(uint8_t * pbuf, uint32_t write_addr, uint16_t size);

/**
 * @brief write buffer to flash anywhere you want
 * 
 * @param pbuf prepare to write data
 * @param write_addr write address
 * @param size buffer size
 * @return uint8_t 
 *      - 1: write done
 *      - 0: write error
 */
uint8_t flash_buffer_write(uint8_t * pbuf, uint32_t write_addr, uint16_t size);

这里不对这一部分做太多展开,因为内容稍微复杂,有兴趣可以查阅代码,实现了自动擦除,可以在任意地址,写入任意大小数据

flash_buffer_write函数设计框图:

image.png

4.2、demo部分

在demo中,实现了对于1K的数据进行读写来验证驱动是否正常工作:

static void write_1K(uint8_t data)
{
    uint32_t i;

    for(i = 0; i < TEST_SIZE; i++)
    {
        buf[i] = data;
    }

    if(flash_buffer_write(buf, TEST_ADDR, TEST_SIZE) == 0)
    {
        printf("write 1K Flash error\r\n");
        return;
    }

    printf("write Flash 1K end, write addr:0x%08X\r\n", TEST_ADDR);
}

static void read_1K(void)
{
    uint32_t i;

    flash_buffer_read(buf, TEST_ADDR,  1024);       /* 读数据 */
    printf("addr: 0x%08X, data size = 1024\r\n", TEST_ADDR);

    /* 打印数据 */
    for (i = 0; i < 1024; i++)
    {
        printf(" %02X", buf[i]);

        if ((i & 31) == 31)
        {
            printf("\r\n"); /* 每行显示16字节数据 */
        }
        else if ((i & 31) == 15)
        {
            printf(" - ");
        }
    }
}


void demo_flash(void)
{
    uint8_t ret;
    bsp_init_flash();

    ret = flash_erase_sector(0x011000);

    if(ret == 1)
    {
        printf("read 1K before write:\r\n");
        read_1K();

        printf("writing data\r\n");
        write_1K(0x55);

        printf("read 1K after write:\r\n");
        read_1K();

    }
    else
    {
        printf("demo is wrong\r\n");
    }

}

5、实验过程

5.1、首要操作

在下载完附件之后,将其放入 ..\Ai-Thinker-WB2\applications\iot-solution\文件路径内。

再打开附件内的 proj_config.mk,将这句话注释掉(如果自己移植了这个flash的驱动文件就需要注释

LOG_ENABLED_COMPONENTS:= blog_testc hosal

将BL的blog组件在这几个组件中关掉,避免在查看串口输出日志的时候有太多杂乱的信息。

5.2、编译下载

正常编译下载。

附件

git clone https://gitee.com/HenleyEn/example_w25qxx.git

视频

Ai-WB2Flash编译原理_哔哩哔哩_bilibili

TODO

  1. dma引入;
  2. 线程保护;
  3. 中间件SFUD+FAL+FlashDB(有空再写);

参考资料

本帖被以下淘专辑推荐:

回复

使用道具 举报

putin | 2024-9-14 07:57:02 | 显示全部楼层
学习了
回复

使用道具 举报

爱笑 | 2024-9-14 08:40:03 | 显示全部楼层
写的不错!
用心做好保姆工作
回复

使用道具 举报

bzhou830 | 2024-9-14 08:44:38 | 显示全部楼层
写的不错!
选择去发光,而不是被照亮
回复

使用道具 举报

lovzx | 2024-9-14 12:11:09 | 显示全部楼层
学习
回复

使用道具 举报

WT_0213 | 2024-9-14 16:15:33 | 显示全部楼层
质量很高
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则