本专栏由Mculover666创建,主要内容为寻找嵌入式领域内的优质开源项目,一是帮助开发者使用开源项目实现更多的功能,二是通过这些开源项目,学习大佬的代码及背后的实现思想,提升自己的代码水平,和其它专栏相比,本专栏的优势在于:
不会单纯的介绍分享项目,还会包含作者亲自实践的过程分享,甚至还会有对它背后的设计思想解读。
目前本专栏包含的开源项目有:
如果您自己编写或者发现的开源项目不错,欢迎留言或者私信投稿到本专栏,分享获得双倍的快乐!
本期给大家带来的开源项目是 SFUD,一款串行 Flash 通用驱动库,作者armink,目前收获 407 个 star,遵循 MIT 开源许可协议。
SFUD全称Serial Flash Universal Driver,是一款开源的串行 SPI Flash 通用驱动库,由于现有市面的串行 Flash 种类居多,各个 Flash 的规格及命令存在差异, SFUD 就是为了解决这些 Flash 的差异现状而设计。
SFUD的特点在于:
项目地址:https://github.com/armink/SFUD
在移植过程中主要参考两个资料:项目的readme文档和demo工程。
对于这些开源项目,其实移植起来也就两步:
本文中我使用的是小熊派IoT开发套件,主控芯片为STM32L431RCT6:
板载Flash型号为W25Q64JV
,大小64Mbit,与STM32的QSPI接口相连:
移植之前需要准备一份裸机工程,我使用STM32CubeMX生成,需要初始化以下配置:
具体过程请参考:
使用CubeMX配置好SPI或QSPI通信即可,不用编写W25Q64驱动。
① 复制源码到工程中:
② 在keil中添加 SFUD 组件的源码文件:
src\sfud.c
:SFUD核心功能源码;src\sfud_sfdp.c
:读取并分析SFDP功能源码;port\sfud_port.c
:SFUD移植接口;③ 将sfud/inc
头文件路径添加到keil中:
SFUD的移植接口都已经写好了,在sfud_port.c
文件中,只需要在函数体中添加代码即可。
① 底层SPI/QSPI读写接口:
/**
* 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通信方式,还需要实现快速读取数据的接口:
/**
* 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设备对象初始化接口:
sfud_err sfud_spi_port_init(sfud_flash *flash);
关于SFUD底层所抽象出来的SPI设备对象,在接下来的设计思想解读章节中会详细讲述。
本文中所使用的裸机工程是基于HAL库的,在SFUD源码的Demo中也有一份HAL库的工程,因为基于HAL库的移植接口实现都是一样的,所以我直接将Demo中的sfud_port.c
文件复制过来替换:
复制过来之后,如果使用的不是STM32L4系列的芯片,则需要修改sfud_port.c
中包含的头文件:
SFUD的核心功能配置文件在sfud_cfg.h
,修改说明如下:
修改完了之后,还需要去修改刚刚复制替换的sfud_port.c
文件,与刚刚填写的配置信息相对应:
至此,SFUD移植、配置完成,接下来就可以愉快的使用了!
使用时包含头文件:
#include <sfud.h>
初始化SFUD的API如下,该函数会初始化 Flash 设备表中的全部设备:
sfud_err sfud_init(void);
在QSPI模式下,SFUD 对于 QSPI 模式的支持仅限于快速读命令,通过该函数可以配置 Flash 所使用的 QSPI 总线的实际支持的数据线最大宽度,例如:1 线(默认值,即传统的 SPI 模式)、2 线、4 线:
sfud_err sfud_qspi_fast_read_enable(sfud_flash *flash, uint8_t data_line_width);
所以,在main函数中编写如下初始化函数:
/* USER CODE BEGIN 2 */
/* SFUD初始化 */
if(sfud_init() != SFUD_SUCCESS)
{
printf("SFUD init fail.\r\n");
}
/* 使能QSPI快读 */
sfud_qspi_fast_read_enable(sfud_get_device(SFUD_W25Q64_DEVICE_INDEX), 1);
/* USER CODE END 2 */
编译、下载之后,可以在串口终端中看到SFUD打印的日志:
SFUD初始化Flash设备成功后进行接下来的读写测试。
① 读取Flash数据:
sfud_err sfud_read(const sfud_flash *flash, uint32_t addr, size_t size, uint8_t *data);
② 擦除 Flash 数据:
sfud_err sfud_erase(const sfud_flash *flash, uint32_t addr, size_t size);
③ 往Flash写数据:
sfud_err sfud_write(const sfud_flash *flash, uint32_t addr, size_t size, const uint8_t *data);
接下来使用作者编写的demo测试。
首先在main.c
开头编写代码,开辟一块缓冲区用于存放测试数据:
/* 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
最后添加测试函数:
/* 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 0x%08X, size is %zu.\r\n", flash->name, addr, size);
}
else
{
printf("Erase the %s flash data failed.\r\n", 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 0x%08X, size is %zu.\r\n", flash->name, addr, size);
}
else
{
printf("Write the %s flash data failed.\r\n", 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 0x%08X, size is %zu. The data is:\r\n", flash->name, addr, size);
printf("Offset (h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\r\n");
for (i = 0; i < size; i++)
{
if (i % 16 == 0)
{
printf("[%08X] ", addr + i);
}
printf("%02X ", data[i]);
if (((i + 1) % 16 == 0) || i == size - 1)
{
printf("\r\n");
}
}
printf("\r\n");
}
else
{
printf("Read the %s flash data failed.\r\n", 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.\r\n", flash->name);
break;
}
}
if (i == size)
{
printf("The %s flash test is success.\r\n", flash->name);
}
}
/* USER CODE END 4 */
在main函数中,SFUD初始化代码之后,调用该函数进行Flash测试:
/* 测试Flash读写 */
sfud_demo(0, sizeof(sfud_demo_test_buf), sfud_demo_test_buf);
编译、下载,在串口终端中查看结果:
SFUD中获取Flash信息有两种方式:
SFUD_USING_SFDP
;SFUD_USING_FLASH_INFO_TABLE
;本文中两种方式都开启,所以移植之后较大,实际使用中可以视情况关闭这两个功能。
SFDP功能关闭后,只会查询该库在 /sfud/inc/sfud_flash_def.h
中提供的 Flash 信息表,代码量会降低,但是软件适配性也随之降低。
查表功能关闭后,该库只驱动支持 SFDP 规范的 Flash,也会适当的降低部分代码量。
一般情况下上述二者必须要选择一个,在实际使用时视情况而定,但是也可以两者都不开启,直接指定好具体的某款 Flash 参数。
SFUD中最重要的就是Flash设备对象,一切操作都是对这个Flash设备对象进行的,每个Flash设备对象独立,所以SFUD也支持系统中存在多个Flash设备对象。
Flash设备对象管理着Flash存储器的所有信息,原型在sfud_def.h
中,定义如下:
/**
* 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读写数据函数,加锁解锁函数定义如下:
/**
* 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;
SFDP全称 Serial Flash Discoverable Parameter,它是 JEDEC (固态技术协会)制定的串行 Flash 功能的参数表标准。
该标准规定了,每个 Flash 中会存在一个参数表,该表中会存放 Flash 容量、写粒度、擦除命令、地址模式等 Flash 规格参数。目前,除了部分厂家旧款 Flash 型号会不支持该标准,其他绝大多数新出厂的 Flash 均已支持 SFDP 标准。
所以 SFUD 在初始化时会优先读取 SFDP 表参数,以达到SFUD在支持SFDP标准的Flash上全部适用的效果,更加通用。
那么SFDP标准的内容是什么呢?SFDP标准强制规范必须要有:
SFDP标题头一般为“S”“F”“U”“D”,如果能读取出这四个字符,则认为该款Flash支持SFDP标准,比如在sfud_sfdp
源码中校验代码如下:
/* 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基本参数表格里面规范和定义了该器件的一些最基本的读取方式、指令内容、扇区大小和芯片容量等信息:
如果你使用的Flash型号比较老或者不支持SFDP,SFUD库当然考虑到了这一点,所以提供了Flash设备参数表,在sfdu_flash_def.h
文件的 SFUD_FLASH_CHIP_TABLE 就能看到当前所有支持的 Flash:
如果你使用的Flash型号既不支持SFDP,也不在此Flash设备参数表中,那么就需要手动添加到该设备参数表中才可以正常使用。
具体的添加方式请参考SFUD项目的README文档中2.5节,讲述的非常详细。