第4期 | SFUD,一款串行(SPI)Flash通用驱动库

2020-07-16 15:05:15 浏览数 (1)

嵌入式开源项目精选专栏

本专栏由Mculover666创建,主要内容为寻找嵌入式领域内的优质开源项目,一是帮助开发者使用开源项目实现更多的功能,二是通过这些开源项目,学习大佬的代码及背后的实现思想,提升自己的代码水平,和其它专栏相比,本专栏的优势在于:

不会单纯的介绍分享项目,还会包含作者亲自实践的过程分享,甚至还会有对它背后的设计思想解读

目前本专栏包含的开源项目有:

  • 第1期 | MultiButton,一个小巧简单易用的事件驱动型按键驱动模块
  • 第2期 | letter-shell,一个功能强大的嵌入式shell
  • 第3期 | EasyLogger,一款轻量级且高性能的日志库

如果您自己编写或者发现的开源项目不错,欢迎留言或者私信投稿到本专栏,分享获得双倍的快乐!

1. SFUD

本期给大家带来的开源项目是 SFUD,一款串行 Flash 通用驱动库,作者armink,目前收获 407 个 star,遵循 MIT 开源许可协议。

SFUD全称Serial Flash Universal Driver,是一款开源的串行 SPI Flash 通用驱动库,由于现有市面的串行 Flash 种类居多,各个 Flash 的规格及命令存在差异, SFUD 就是为了解决这些 Flash 的差异现状而设计

SFUD的特点在于:

  • 支持 SPI/QSPI 接口
  • 面向对象设计(同时支持多个 Flash 对象)
  • 可灵活裁剪、扩展性强
  • 支持 4 字节地址

项目地址:https://github.com/armink/SFUD

2. 移植SFUD

2.1. 移植思路

在移植过程中主要参考两个资料:项目的readme文档和demo工程。

对于这些开源项目,其实移植起来也就两步:

  • ① 添加源码到裸机工程中;
  • ② 实现需要的接口即可;

2.2. 准备裸机工程

本文中我使用的是小熊派IoT开发套件,主控芯片为STM32L431RCT6:

板载Flash型号为W25Q64JV,大小64Mbit,与STM32的QSPI接口相连:

移植之前需要准备一份裸机工程,我使用STM32CubeMX生成,需要初始化以下配置:

  • 配置SPI Flash通信接口(SPI或QSPI)
  • 配置一个串口用于打印信息
  • printf重定向

具体过程请参考:

  • STM32CubeMX_06 | 使用USART发送和接收数据(查询模式)
  • STM32CubeMX_09 | 重定向printf函数到串口输出的多种方法
  • STM32CubeMX_18 | 使用硬件QSPI读写SPI Flash(W25Q64)

使用CubeMX配置好SPI或QSPI通信即可,不用编写W25Q64驱动。

2.3. 添加SFUD到工程中

① 复制源码到工程中:

② 在keil中添加 SFUD 组件的源码文件:

  • srcsfud.c:SFUD核心功能源码;
  • srcsfud_sfdp.c:读取并分析SFDP功能源码;
  • portsfud_port.c:SFUD移植接口;

③ 将sfud/inc头文件路径添加到keil中:

2.4. 实现SFUD移植接口

SFUD的移植接口都已经写好了,在sfud_port.c文件中,只需要在函数体中添加代码即可。

① 底层SPI/QSPI读写接口:

代码语言:javascript复制
/**
 * SPI write data then read data
 */
static sfud_err spi_write_read(const sfud_spi *spi, const uint8_t *write_buf, size_t write_size, uint8_t *read_buf, size_t read_size);

② 如果使用的是QSPI通信方式,还需要实现快速读取数据的接口:

代码语言:javascript复制
/**
 * QSPI fast read data
 */
static sfud_err qspi_read(const struct __sfud_spi *spi, uint32_t addr, sfud_qspi_read_cmd_format *qspi_read_cmd_format, uint8_t *read_buf, size_t read_size);

③ SFUD底层使用的SPI/QSPI接口和SPI设备对象初始化接口:

代码语言:javascript复制
sfud_err sfud_spi_port_init(sfud_flash *flash);

关于SFUD底层所抽象出来的SPI设备对象,在接下来的设计思想解读章节中会详细讲述。

本文中所使用的裸机工程是基于HAL库的,在SFUD源码的Demo中也有一份HAL库的工程,因为基于HAL库的移植接口实现都是一样的,所以我直接将Demo中的sfud_port.c文件复制过来替换:

复制过来之后,如果使用的不是STM32L4系列的芯片,则需要修改sfud_port.c中包含的头文件:

2.5. 配置SFUD

SFUD的核心功能配置文件在sfud_cfg.h,修改说明如下:

修改完了之后,还需要去修改刚刚复制替换的sfud_port.c文件,与刚刚填写的配置信息相对应:

至此,SFUD移植、配置完成,接下来就可以愉快的使用了!

3. 使用SFUD

使用时包含头文件:

代码语言:javascript复制
#include <sfud.h>

3.1. 初始化SFUD

初始化SFUD的API如下,该函数会初始化 Flash 设备表中的全部设备:

代码语言:javascript复制
sfud_err sfud_init(void);

在QSPI模式下,SFUD 对于 QSPI 模式的支持仅限于快速读命令,通过该函数可以配置 Flash 所使用的 QSPI 总线的实际支持的数据线最大宽度,例如:1 线(默认值,即传统的 SPI 模式)、2 线、4 线:

代码语言:javascript复制
sfud_err sfud_qspi_fast_read_enable(sfud_flash *flash, uint8_t data_line_width);

所以,在main函数中编写如下初始化函数:

代码语言:javascript复制
/* USER CODE BEGIN 2 */

/* SFUD初始化 */
if(sfud_init() != SFUD_SUCCESS)
{
 printf("SFUD init fail.rn");
}
/* 使能QSPI快读 */
sfud_qspi_fast_read_enable(sfud_get_device(SFUD_W25Q64_DEVICE_INDEX), 1);

/* USER CODE END 2 */

编译、下载之后,可以在串口终端中看到SFUD打印的日志:

SFUD初始化Flash设备成功后进行接下来的读写测试。

3.2. Flash擦除/读写操作

① 读取Flash数据:

代码语言:javascript复制
sfud_err sfud_read(const sfud_flash *flash, uint32_t addr, size_t size, uint8_t *data);

② 擦除 Flash 数据:

代码语言:javascript复制
sfud_err sfud_erase(const sfud_flash *flash, uint32_t addr, size_t size);

③ 往Flash写数据:

代码语言:javascript复制
sfud_err sfud_write(const sfud_flash *flash, uint32_t addr, size_t size, const uint8_t *data);

接下来使用作者编写的demo测试。

首先在main.c开头编写代码,开辟一块缓冲区用于存放测试数据:

代码语言:javascript复制
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* SFUD读写Flash数据测试的缓冲区 */
#define SFUD_DEMO_TEST_BUFFER_SIZE                     1024
static uint8_t sfud_demo_test_buf[SFUD_DEMO_TEST_BUFFER_SIZE];

/* SFUD读写Flash数据测试函数 */
void sfud_demo(uint32_t addr, size_t size, uint8_t *data);

/* USER CODE END 0 */

然后在main.c最后添加测试函数:

代码语言:javascript复制
/* USER CODE BEGIN 4 */
/**
 * SFUD demo for the first flash device test.
 *
 * @param addr flash start address
 * @param size test flash size
 * @param size test flash data buffer
 */
void sfud_demo(uint32_t addr, size_t size, uint8_t *data)
{
    sfud_err result = SFUD_SUCCESS;
    extern sfud_flash *sfud_dev;
    const sfud_flash *flash = sfud_get_device(SFUD_W25Q64_DEVICE_INDEX);
    size_t i;
    /* prepare write data */
    for (i = 0; i < size; i  )
    {
        data[i] = i;
    }
    /* erase test */
    result = sfud_erase(flash, addr, size);
    if (result == SFUD_SUCCESS)
    {
        printf("Erase the %s flash data finish. Start from 0xX, size is %zu.rn", flash->name, addr, size);
    }
    else
    {
        printf("Erase the %s flash data failed.rn", flash->name);
        return;
    }
    /* write test */
    result = sfud_write(flash, addr, size, data);
    if (result == SFUD_SUCCESS)
    {
        printf("Write the %s flash data finish. Start from 0xX, size is %zu.rn", flash->name, addr, size);
    }
    else
    {
        printf("Write the %s flash data failed.rn", flash->name);
        return;
    }
    /* read test */
    result = sfud_read(flash, addr, size, data);
    if (result == SFUD_SUCCESS)
    {
        printf("Read the %s flash data success. Start from 0xX, size is %zu. The data is:rn", flash->name, addr, size);
        printf("Offset (h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0Frn");
        for (i = 0; i < size; i  )
        {
            if (i % 16 == 0)
            {
                printf("[X] ", addr   i);
            }
            printf("X ", data[i]);
            if (((i   1) % 16 == 0) || i == size - 1)
            {
                printf("rn");
            }
        }
        printf("rn");
    }
    else
    {
        printf("Read the %s flash data failed.rn", flash->name);
    }
    /* data check */
    for (i = 0; i < size; i  )
    {
    
        if (data[i] != i % 256)
        {
            printf("Read and check write data has an error. Write the %s flash data failed.rn", flash->name);
            break;
        }
    }
    if (i == size)
    {
        printf("The %s flash test is success.rn", flash->name);
    }
}

/* USER CODE END 4 */

main函数中,SFUD初始化代码之后,调用该函数进行Flash测试:

代码语言:javascript复制
/* 测试Flash读写 */
sfud_demo(0, sizeof(sfud_demo_test_buf), sfud_demo_test_buf);

编译、下载,在串口终端中查看结果:

3.3. 移植前后内存占用情况

SFUD中获取Flash信息有两种方式:

  • 使用SFDP 参数方式:开关宏SFUD_USING_SFDP
  • 使用库自带的 Flash 参数信息表:开关宏SFUD_USING_FLASH_INFO_TABLE

本文中两种方式都开启,所以移植之后较大,实际使用中可以视情况关闭这两个功能。

SFDP功能关闭后,只会查询该库在 /sfud/inc/sfud_flash_def.h 中提供的 Flash 信息表,代码量会降低,但是软件适配性也随之降低。

查表功能关闭后,该库只驱动支持 SFDP 规范的 Flash,也会适当的降低部分代码量。

一般情况下上述二者必须要选择一个,在实际使用时视情况而定,但是也可以两者都不开启,直接指定好具体的某款 Flash 参数。

4. SFUD设计思想解读

4.1. Flash设备对象

SFUD中最重要的就是Flash设备对象,一切操作都是对这个Flash设备对象进行的,每个Flash设备对象独立,所以SFUD也支持系统中存在多个Flash设备对象。

Flash设备对象管理着Flash存储器的所有信息,原型在sfud_def.h中,定义如下:

代码语言:javascript复制
/**
 * serial flash device
 */
typedef struct {
    char *name;                                  /**< serial flash name */
    size_t index;                                /**< index of flash device information table  @see flash_table */
    sfud_flash_chip chip;                        /**< flash chip information */
    sfud_spi spi;                                /**< SPI device */
    bool init_ok;                                /**< initialize OK flag */
    bool addr_in_4_byte;                         /**< flash is in 4-Byte addressing */
    struct {
        void (*delay)(void);                     /**< every retry's delay */
        size_t times;                            /**< default times for error retry */
    } retry;
    void *user_data;                             /**< some user data */

#ifdef SFUD_USING_QSPI
    sfud_qspi_read_cmd_format read_cmd_format;   /**< fast read cmd format */
#endif

#ifdef SFUD_USING_SFDP
    sfud_sfdp sfdp;                              /**< serial flash discoverable parameters by JEDEC standard */
#endif

} sfud_flash, *sfud_flash_t;

其中Flash设备的通信接口信息由 sfud_spi 对象管理,包括SPI读写数据函数,加锁解锁函数定义如下:

代码语言:javascript复制
/**
 * SPI device
 */
typedef struct __sfud_spi {
    /* SPI device name */
    char *name;
    /* SPI bus write read data function */
    sfud_err (*wr)(const struct __sfud_spi *spi, const uint8_t *write_buf, size_t write_size, uint8_t *read_buf,
                   size_t read_size);
#ifdef SFUD_USING_QSPI
    /* QSPI fast read function */
    sfud_err (*qspi_read)(const struct __sfud_spi *spi, uint32_t addr, sfud_qspi_read_cmd_format *qspi_read_cmd_format,
                          uint8_t *read_buf, size_t read_size);
#endif
    /* lock SPI bus */
    void (*lock)(const struct __sfud_spi *spi);
    /* unlock SPI bus */
    void (*unlock)(const struct __sfud_spi *spi);
    /* some user data */
    void *user_data;
} sfud_spi, *sfud_spi_t;

4.2. JESD216 SFDP标准

SFDP全称 Serial Flash Discoverable Parameter,它是 JEDEC (固态技术协会)制定的串行 Flash 功能的参数表标准。

该标准规定了,每个 Flash 中会存在一个参数表,该表中会存放 Flash 容量、写粒度、擦除命令、地址模式等 Flash 规格参数。目前,除了部分厂家旧款 Flash 型号会不支持该标准,其他绝大多数新出厂的 Flash 均已支持 SFDP 标准。

所以 SFUD 在初始化时会优先读取 SFDP 表参数,以达到SFUD在支持SFDP标准的Flash上全部适用的效果,更加通用。

那么SFDP标准的内容是什么呢?SFDP标准强制规范必须要有:

  • SFDP标题头
  • 1st参数头
  • JEDEC Flash基本参数表格

SFDP标题头一般为“S”“F”“U”“D”,如果能读取出这四个字符,则认为该款Flash支持SFDP标准,比如在sfud_sfdp源码中校验代码如下:

代码语言:javascript复制
/* check SFDP header */
if (!(header[0] == 'S' &&
      header[1] == 'F' &&
      header[2] == 'D' &&
      header[3] == 'P')) {
    SFUD_DEBUG("Error: Check SFDP signature error. It's must be 50444653h('S' 'F' 'D' 'P').");
    return false;
}

接下来是一些预留空内容,属于厂商可选内容,Flash厂商可以在这些空白内容中添加自己的厂商ID识别号、SFDP版本号、参数长度以及存放参数表格的地址指针,比如读取W25Q64的结果中显示:

接下来的 JEDEC Flash基本参数表格里面规范和定义了该器件的一些最基本的读取方式、指令内容、扇区大小和芯片容量等信息:

4.3. 添加库目前不支持的 Flash

如果你使用的Flash型号比较老或者不支持SFDP,SFUD库当然考虑到了这一点,所以提供了Flash设备参数表,在sfdu_flash_def.h文件的 SFUD_FLASH_CHIP_TABLE 就能看到当前所有支持的 Flash:

如果你使用的Flash型号既不支持SFDP,也不在此Flash设备参数表中,那么就需要手动添加到该设备参数表中才可以正常使用。

具体的添加方式请参考SFUD项目的README文档中2.5节,讲述的非常详细。

0 人点赞