目录
[1. 创建工程](#1. 创建工程)
[2. 移植(port)](#2. 移植(port))
[2.1 little_flash_spi_transfer](#2.1 little_flash_spi_transfer)
[2.2 little_flash_wait_10us](#2.2 little_flash_wait_10us)
[2.3 其他](#2.3 其他)
[3. 添加新的nor flash](#3. 添加新的nor flash)
[3.1 获取新flash的ID](#3.1 获取新flash的ID)
[3.2 擦除](#3.2 擦除)
[3.3 写](#3.3 写)
[3.4 读](#3.4 读)
[4. 添加新的nand flash](#4. 添加新的nand flash)
[4.1 读写状态寄存器](#4.1 读写状态寄存器)
[4.2 修改status读写函数](#4.2 修改status读写函数)
[5. littlefs](#5. littlefs)
[5.1 初始化lfs_config变量](#5.1 初始化lfs_config变量)
[5.2 挂载文件系统](#5.2 挂载文件系统)
[5.3 读写测试](#5.3 读写测试)
类似SFUD,支持spi nand flash,对于nand flash,最好增加文件系统,可使用little_flash直接进行flash操作,但不推荐NAND FLASH直接操作,推荐使用文件系统littlefs。little_flash的下载地址:
little_flash: SPI FLASH 通用驱动库 - Gitee.com
不过,注意这个little_flash是比较新的开源软件,还不是很完善,目前的版本是0.0.1。
littlefs主要用在微控制器和flash上,是一种嵌入式文件系统。主要有3个特点:
1)掉电恢复
在写入时即使复位或者掉电也可以恢复到上一个正确的状态。
2)擦写均衡
有效延长flash的使用寿命
3)有限的RAM/ROM
节省ROM和RAM空间
littlefs的下载地址:
littlefs-project/littlefs: A little fail-safe filesystem designed for microcontrollers
硬件平台采用FT4222H模块+SPI NAND FLASH,采用官方库v1.4.8移植,下载链接如下:
https://ftdichip.com/wp-content/uploads/2025/06/LibFT4222-v1.4.8.zip
1. 创建工程
本文档对应的工程下载地址:https://gitee.com/pq113_6/lib-ft4222.git
在官方库文件夹FT4222H\LibFT4222-v1.4.8\samples\flash_example中,复制spi_flash_quad_test_mxic,将文件夹名和里面的文件名均改为app_little_flash

用记事本将这4个文件编辑,将其中的"spi_flash_quad_test_mxic"改为"app_little_flash"
打开LibFT4222-v1.4.8\samples\LibFT4222_examples.sln,移除原来的源代码,导入新的app_little_flash.cpp。
在该项目目录内新建文件夹little_flash,将little_flash的src/inc/port三个文件夹拷贝到这个文件夹内,将这些文件导入工程,将该工程设置为启动项目,整个工程目录如下:

配置include路径,讲little_flash的inc目录和port目录路径添加到项目配置中。

将LF_PRINTF宏定义改为
cpp
#define LF_PRINTF printf
不过这里有一个问题,little_flash底层打印都没有换行,所以这里改为函数的方式,增加换行。在little_flash_port.c中添加lf_printf函数
cpp
static char log_buf[256];
void lf_printf(const char* format, ...)
{
va_list args;
va_start(args, format);
printf("[little_flash]");
/* must use vprintf to print */
vsnprintf(log_buf, sizeof(log_buf), format, args);
printf("%s\n", log_buf);
va_end(args);
}
将这个宏定义改为:
cpp
#define LF_PRINTF lf_printf
将little_flash_wait_ms改为
cpp
static void little_flash_wait_ms(uint32_t ms){
Sleep(ms);
}
将main函数改为(这点很奇怪)
cpp
int main(int argc, char* argv[])
最后将mix部分的代码删除,此时应该可以编译通过。
2. 移植(port)
作者在little_flash_port.c里面已经放了需要的接口函数,逐步实现这些函数即可。
2.1 little_flash_spi_transfer
这个是SPI读写的函数,参考SFDU的方式开源软件学习笔记 - 移植SFUD-CSDN博客
新建一个little_flash_spi_transfer.h,同样增加一个用户数据,同样记录FT4222H的句柄
cpp
typedef struct {
FT_HANDLE handle;
}lf_user_data_s;
作者应该还没有实现QSPI部分,所以这里也是只实现了SPI
cpp
static lf_err_t little_flash_spi_transfer(const little_flash_t *lf,uint8_t *tx_buf, uint32_t tx_len, uint8_t *rx_buf, uint32_t rx_len){
lf_err_t result = LF_ERR_OK;
lf_user_data_s* spi_dev = (lf_user_data_s *)(lf->spi.user_data);
FT4222_STATUS ftStatus = FT4222_OK;
uint16_t sizeTransferred = 0;
bool endTransfer = true;
if ((rx_buf != NULL) && (rx_len > 0))
endTransfer = false;
if ((tx_buf != NULL) && (tx_len > 0))
{
ftStatus = FT4222_SPIMaster_SingleWrite(
spi_dev->handle,
(uint8_t*)tx_buf,
tx_len,
&sizeTransferred,
endTransfer
);
if (ftStatus != FT4222_OK)
{
LF_DEBUG("spi write fail");
return LF_ERR_NO_FLASH;
}
if (sizeTransferred != tx_len)
{
LF_DEBUG("spi write size is not match");
return LF_ERR_WRITE;
}
}
if ((rx_buf != NULL) && (rx_len > 0))
{
ftStatus = FT4222_SPIMaster_SingleRead(
spi_dev->handle,
rx_buf,
rx_len,
&sizeTransferred,
true
);
if (ftStatus != FT4222_OK)
{
LF_DEBUG("spi read fail");
return LF_ERR_NO_FLASH;
}
if (sizeTransferred != rx_len)
{
LF_DEBUG("spi read size is not match");
return LF_ERR_READ;
}
}
return result;
}
2.2 little_flash_wait_10us
10us延时程序,在windows里面需要改一下,只能是非精准的延时
cpp
static void little_flash_wait_10us(void){
for (volatile uint32_t i = 0; i < 20; i++) {
// 每个迭代约0.5微秒(根据CPU速度调整)
}
}
2.3 其他
其他函数little_flash_wait_ms、little_flash_malloc、little_flash_free和little_flash_port_init都不需要改动。
3. 添加新的nor flash
当前版本只支持2个Flash,两种类型(nor/nand),在little_flash_table.h里面有相应的配置:
cpp
#define LITTLE_FLASH_CHIP_TABLE \
{ \
{.name="W25N01GVZEIG", .manufacturer_id=LF_MF_ID_WINBOND, .device_id=0xAA21, .type=LF_DRIVER_NAND_FLASH, .capacity=128L*1024L*1024L, .erase_cmd=0xD8, .erase_size=64L*2048L}, \
{.name="W25Q128FVSG", .manufacturer_id=LF_MF_ID_WINBOND, .device_id=0x4018, .type=LF_DRIVER_NOR_FLASH, .capacity=16L*1024L*1024L, .erase_cmd=0x20, .erase_size=4096L} \
}
所以一般需要自己根据自己的平台添加。先从简单的开始,添加新的nor flash。
3.1 获取新flash的ID
在main函数中添加初始化代码,先定义全局变量lf_flash_user
cpp
lf_user_data_s lf_flash_user[1] = { 0 };
将打开的FT4222H句柄赋值给这个变量
cpp
lf_flash_user->handle = ftHandle;
little_flash_t lf_flash = { 0 };
lf_flash.spi.user_data = (void*)lf_flash_user;
初始化little_flash
cpp
little_flash_init();
little_flash_device_init(&lf_flash);
printf("little_flash_device_init OK\n");
找到little_flash_device_init,将ID信息打印部分打开
cpp
LF_DEBUG("recv_data [0]:0x%02X [1]:0x%02X [2]:0x%02X [3]:0x%02X",recv_data[0],recv_data[1],recv_data[2],recv_data[3]);
运行这个代码,可以看到log信息:
bash
[little_flash]Welcome to use little flash V0.0.1 .
[little_flash]Github Repositories https://github.com/PeakRacing/little_flash .
[little_flash]Gitee Repositories https://gitee.com/PeakRacing/little_flash .
[little_flash]recv_data [0]:0xC8 [1]:0x40 [2]:0x16 [3]:0xC8
[little_flash]NOT fonud flash
little_flash_device_init OK
在little_flash_table.h里面根据ID信息添加新的flash
cpp
{.name="GD25Q32", .manufacturer_id=LF_MF_ID_GIGADEVICE, .device_id=0x4016, .type=LF_DRIVER_NOR_FLASH, .capacity=4L*1024L*1024L, .erase_cmd=0x20, .erase_size=4096L}
而LF_MF_ID_GIGADEVICE的定义如下:
cpp
#define LF_MF_ID_GIGADEVICE 0xC8
device_id对应9FH命令读回来的第2、3字节。
capacity设置的是flash的容量,字节为单位。
erase_cmd是flash的擦除命令,一般会支持多个擦除方式,一般选择Sector Erase,对应命令是0x20.

擦除大小erase_size对应擦除命令配置。

再运行程序后,log信息如下:
bash
[little_flash]Welcome to use little flash V0.0.1 .
[little_flash]Github Repositories https://github.com/PeakRacing/little_flash .
[little_flash]Gitee Repositories https://gitee.com/PeakRacing/little_flash .
[little_flash]recv_data [0]:0xC8 [1]:0x40 [2]:0x16 [3]:0xC8
[little_flash]JEDEC ID: manufacturer_id:0xC8 device_id:0x4016
[little_flash]little flash fonud flash GD25Q32
3.2 擦除
擦除对应的函数是little_flash_erase,其原型如下:
cpp
lf_err_t little_flash_erase(const little_flash_t *lf, uint32_t addr, uint32_t len)
擦除地址和长度最好都符合erase_size的整数倍。这里是擦除一个sector:
cpp
little_flash_erase(&lf_flash, 0, lf_flash.chip_info.erase_size);
3.3 写
写数据的函数是little_flash_write,其原型如下:
cpp
lf_err_t little_flash_write(const little_flash_t *lf, uint32_t addr, const uint8_t *data, uint32_t len)
测试代码是写入16个字节数据
cpp
const uint8_t wr_buf[16] = {
0xAA, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x55,
};
little_flash_write(&lf_flash, 0, wr_buf, 16);
3.4 读
读数据的函数是little_flash_read,其原型如下:
cpp
lf_err_t little_flash_read(const little_flash_t *lf, uint32_t addr, uint8_t *data, uint32_t len)
测试代码是将之前写入的数据读回来判断
cpp
uint8_t rd_buf[16];
int i = 0;
for (i = 0; i < 16; i++)
rd_buf[i] = 0;
little_flash_read(&lf_flash, 0, rd_buf, 16);
printf("******SPI Read Test******\n");
for (i = 0; i < 16; i++)
{
printf("0x%02x ", rd_buf[i]);
if (((i + 1) % 8) == 0)
printf("\n");
}
printf("\n");
log信息如下:
cpp
******SPI Read Test******
0xaa 0x01 0x02 0x03 0x04 0x05 0x06 0x07
0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x55
说明读写已经可以了。
4. 添加新的nand flash
对nand flash的支持不是太完善,需要修改一些源代码。
4.1 读写状态寄存器
Nand Flash的状态寄存器的读写方式与Nor Flash的不同,WInbond的W25N01GV和MX35LF1G24AD(MXIC这部分是Feature)为例:


即支持更多的Feature,第一个字节Winbond的支持2种方式,而MXIC只支持0F/1F,第二个字节对应的是寄存器地址,Winbond和MXIC的地址定义分别如下:


可以看到MXIC的寄存器更多,但是最重要的tatus寄存器地址是一样的。
给结构体little_flash_chipinfo_t增加2个变量
cpp
uint8_t read_status_cmd;
uint8_t write_status_cmd;
修改LITTLE_FLASH_CHIP_TABLE,增加这2个变量的初始化值。
cpp
#define LITTLE_FLASH_CHIP_TABLE \
{ \
{.name="W25N01GVZEIG", .manufacturer_id=LF_MF_ID_WINBOND, .device_id=0xAA21, \
.type=LF_DRIVER_NAND_FLASH, .capacity=128L*1024L*1024L, .erase_cmd=0xD8, \
.erase_size=64L*2048L, .read_status_cmd = 0x05, .write_status_cmd = 0x01}, \
{.name="MX35LF512", .manufacturer_id=LF_MF_ID_MXIC, .device_id=0x12C2, \
.type=LF_DRIVER_NAND_FLASH, .capacity=64L*1024L*1024L, .erase_cmd=0xD8, \
.erase_size=64L*2048L, .read_status_cmd = 0x0f, .write_status_cmd = 0x1f}, \
{.name="W25Q128FVSG", .manufacturer_id=LF_MF_ID_WINBOND, .device_id=0x4018, \
.type=LF_DRIVER_NOR_FLASH, .capacity=16L*1024L*1024L, .erase_cmd=0x20, \
.erase_size=4096L, .read_status_cmd = 0x05, .write_status_cmd = 0x01}, \
{.name="GD25Q32", .manufacturer_id=LF_MF_ID_GIGADEVICE, .device_id=0x4016, \
.type=LF_DRIVER_NOR_FLASH, .capacity=4L*1024L*1024L, .erase_cmd=0x20, \
.erase_size=4096L, .read_status_cmd = 0x05, .write_status_cmd = 0x01} \
}
4.2 修改status读写函数
将little_flash_write_status和little_flash_read_status里面第一个字节命令改为配置值。
cpp
//cmd_data[0]=LF_CMD_WRITE_STATUS_REGISTER;
cmd_data[0] = lf->chip_info.write_status_cmd;;
//cmd_data[0]=LF_CMD_READ_STATUS_REGISTER;
cmd_data[0] = lf->chip_info.read_status_cmd;
此时flash读写测试可以看到log信息
bash
[little_flash]Welcome to use little flash V0.0.1 .
[little_flash]Github Repositories https://github.com/PeakRacing/little_flash .
[little_flash]Gitee Repositories https://gitee.com/PeakRacing/little_flash .
[little_flash]JEDEC ID [0]:0x00 [1]:0xC2 [2]:0x12 [3]:0xC2
[little_flash]JEDEC ID: manufacturer_id:0xC2 device_id:0x12C2
[little_flash]little flash fonud flash MX35LF512
little_flash_device_init OK
[little_flash]erase command:d8,0,0,0
[little_flash]erase wait time:2
******SPI Read Test******
0xaa 0x01 0x02 0x03 0x04 0x05 0x06 0x07
0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x55
5. littlefs
Nand Flash推荐使用文件系统littlefs实现读写,将littlefs源代码拷贝到工程目录,将对应的源文件增加到工程中。

然后加入这2个头文件
cpp
#include "littlefs\lfs.h"
#include "littlefs\lfs_util.h"
项目属性 → C/C++ → 语言 → C 语言标准 → 选择"ISO C11 标准"

5.1 初始化lfs_config变量
lfs的核心移植部分就是初始这个lfs_config变量,新增一个函数lsf_init_cfg初始化这个变量
cpp
void lsf_init_cfg(struct lfs_config *cfg, little_flash_t* lf_flash)
{
memset(cfg, 0, sizeof(lfs_config)); // 清零结构体
cfg->context = lf_flash;
cfg->read = lf_block_device_read;
cfg->prog = lf_block_device_prog;
cfg->erase = lf_block_device_erase;
cfg->sync = lf_block_device_sync;
// 配置块设备参数
cfg->read_size = lf_flash->chip_info.read_size;
cfg->prog_size = lf_flash->chip_info.prog_size;
cfg->block_size = lf_flash->chip_info.erase_size;
cfg->block_count = lf_flash->chip_info.capacity / lf_flash->chip_info.erase_size;
cfg->cache_size = lf_flash->chip_info.prog_size;
cfg->lookahead_size = lf_flash->chip_info.prog_size;
cfg->block_cycles = 500;
}
- context:可用于将信息传递给块设备操作
- read:读取块中的区域
- prog:对块中的区域进行编程。该块必须之前已被擦除。
- erase:擦除块。在编程之前,必须擦除块。已擦除块的状态未定义
- sync:同步底层块设备的状态。这个函数用于将数据与flash同步使用,对于使用缓存的情况需要添加。
- read_size:读取的块的最小大小(以字节为单位)。所有读取操作都将是该值的倍数。
- prog_size:块编程的最小大小(字节)。所有编程操作都将是该值的倍数。
- block_size:可擦除块的大小(字节)。这不会影响ram消耗,并且可能大于物理擦除大小。但是,非内联文件至少占用一个块。必须是读取和编程大小的倍数。
- block_count:设备上可擦除的块数。为零时默认使用存储在磁盘上的block_count。
- cache_size:块缓存的大小(字节)。每个缓存在RAM中缓冲一个块的一部分。这些小块需要一个读缓存、一个编程缓存和每个文件一个额外的缓存。更大的缓存可以通过存储更多数据和减少磁盘访问次数来提高性能。必须是读取和程序大小的倍数,以及块大小的因子。
- lookahead_size:先行缓冲区的大小(以字节为单位)。更大的先行缓冲区会增加分配过程中发现的块数。先行缓冲区存储为紧凑的位图,因此RAM的每个字节可以跟踪8个块。
- block_cycles:ittlefs清除元数据日志并将元数据移动到另一个块之前的擦除周期数。建议值在100-1000的范围内,较大的值具有更好的性能,但擦写均衡分布不太一致。
然后调用这个函数初始化cfg变量
cpp
struct lfs_config cfg;
lsf_init_cfg(&cfg, &lf_flash);
5.2 挂载文件系统
cpp
lfs_t lfs;
// mount the filesystem
int err = lfs_mount(&lfs, &cfg);
// reformat if we can't mount the filesystem
// this should only happen on the first boot
if (err) {
lfs_format(&lfs, &cfg);
lfs_mount(&lfs, &cfg);
}
如果挂载文件系统失败,则格式化存储设备,重新挂载一次。
5.3 读写测试
往文件test中写入16个字节数据,然后在读回来这16个字节数据,打印出来看。
cpp
lfs_file_t file;
const uint8_t wr_buf[16] = {
0xAA, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x55,
};
uint8_t rd_buf[16];
uint8_t i;
for (i = 0; i < 16; i++)
rd_buf[i] = 0;
lfs_file_open(&lfs, &file, "test", LFS_O_RDWR | LFS_O_CREAT);
size_t wr_size = lfs_file_write(&lfs, &file, wr_buf, 16);
if (wr_size != 16) {
printf("Write failed! Written: %d\n", wr_size);
}
lfs_file_seek(&lfs, &file, 0, LFS_SEEK_SET);
size_t rd_size = lfs_file_read(&lfs, &file, rd_buf, 16);
if (rd_size != 16) {
printf("Read failed! Read: %d\n", rd_size);
}
lfs_file_close(&lfs, &file);
printf("******lfs read test******\n");
for (i = 0; i < 16; i++)
{
printf("0x%02x ", rd_buf[i]);
if (((i + 1) % 8) == 0)
printf("\n");
}
printf("\n");