使用 LittleFS 作为 SD 卡文件系统
LittleFS 是用于微控制器内部闪存和外部 NOR 闪存的文件系统。由于它比传统的 FAT 文件系统更适合小型嵌入式系统,因此越来越多的人在他们的项目中使用它。那么除了NOR/NAND flash类型的存储设备之外,LittleFS还能用在SD卡上吗?好像也还可以。本文将利用i.mxRT1050 SDK中的littlefs_shell和sdcard_fatfs demo工程,新建一个littefs_shell工程,用于读写SD卡。
本次实验采用MCUXpresso IDE v11.7,SDK采用2.13版本。littleFS文件系统只有4个文件,其中lfs.h中显示的当前版本是littleFS 2.5。
第一步当然是将SD相关的代码添加到littlefs_shell项目里。最简单的方法是导入另一个 sdcard_fatfs 项目并将所有 sdmmc 目录复制到我们的项目中。然后复制 sdmmc_config.c以及 /board 目录中的 sdmmc_config.h 和 fsl_usdhc.c以及 /drivers 目录中的 fsl_usdhc.h。
第二步是修改程序,包括SD卡检测和初始化,添加从LittleFS到SD驱动程序的桥梁。将以下代码添加到littlefs_shell.c。
extern sd_card_t m_sdCard;
status_t sdcardWaitCardInsert(void)
{
BOARD_SD_Config(&m_sdCard, NULL, BOARD_SDMMC_SD_HOST_IRQ_PRIORITY, NULL);
/* SD host init function */
if (SD_HostInit(&m_sdCard) != kStatus_Success)
{
PRINTF("\r\nSD host init fail\r\n");
return kStatus_Fail;
}
/* wait card insert */
if (SD_PollingCardInsert(&m_sdCard, kSD_Inserted) == kStatus_Success)
{
PRINTF("\r\nCard inserted.\r\n");
/* power off card */
SD_SetCardPower(&m_sdCard, false);
/* power on the card */
SD_SetCardPower(&m_sdCard, true);
// SdMmc_Init();
}
else
{
PRINTF("\r\nCard detect fail.\r\n");
return kStatus_Fail;
}
return kStatus_Success;
}
status_t sd_disk_initialize()
{
static bool isCardInitialized = false;
/* demostrate the normal flow of card re-initialization. If re-initialization is not neccessary, return RES_OK directly will be fine */
if(isCardInitialized)
{
SD_Deinit(&m_sdCard);
}
if (kStatus_Success != SD_Init(&m_sdCard))
{
SD_Deinit(&m_sdCard);
memset(&m_sdCard, 0U, sizeof(m_sdCard));
return kStatus_Fail;
}
isCardInitialized = true;
return kStatus_Success;
}
在main()函数中添加如下代码
if (sdcardWaitCardInsert() != kStatus_Success)
{
return -1;
}
status = sd_disk_initialize();
接下来创建两个新的c文件,lfs_sdmmc.c和 lfs_sdmmc_bridge.c。调用顺序为littlefs->lfs_sdmmc.c->lfs_sdmmc_bridge.c->fsl_sd.c。lfs_sdmmc.c和 lfs_sdmmc_bridge.c充当连接 LITTLEFS 和 SD 上层驱动程序的中间层。其中必须要注意的一点就是地址的映射。littleFS给出的地址是块地址+偏移地址。参见下图。这是由‘mount’命令发出的读取命令。块地址指的是SD中被擦除扇区的地址。读写操作采用SD的最小读写块地址(BLOCK),如下所述。因此,在 lfs_sdmmc.c 中,littleFS给出的地址首先被转换为字节地址。然后在lfs_sdmmc_bridge.c中将SD卡读写地址改为BLOCK地址。由于目前大多数 SD 卡都超过 4GB,因此字节地址需要一个 64 位变量。
最后,最重要的一步是littleFS参数配置。peripherals.c 中有一个结构体 LittlsFS_config,其中不仅包含了SD卡的操作函数,还包含了读写扇区和缓存大小。这个结构的设置至关重要。如果设置不好,不仅会影响性能,还会引起运行错误。在设置之前我们先来介绍一下SD卡和littleFS的一些通用概念。
SD卡的存储单位是BLOCK,读写都可以按照BLOCK进行。对于不同的卡,每个块的大小可以不同。对于标准SD卡,可以用CMD16设置块命令的长度,对于SDHC卡,块命令长度固定为512字节。SD 卡被逐个扇区地擦除。需要在SD卡的CSD寄存器中检查每个扇区的大小。若CSD寄存器ERASE_BLK_EN = 0,表示Sector为最小擦除单位,其单位为“块”。扇区大小的值等于CSD寄存器中SECTOR_SIZE字段的值加1。例如,如果SECTOR_SIZE为127,则最小擦除单位为512*(127+1)=65536字节。另外,有时候会有疑问,现在的很多SD卡其实都具备磨损功能,以减少频繁擦写带来的损失,延长使用寿命。所以实际上删除操作或者读写操作不一定是真实的物理地址。相反,它是由 SD 控制器映射的。但对于用户来说,这种映射是透明的。所以不必担心这会影响正常运行。
LittleFS 是一种轻量级文件系统,与 FAT 系统相比,它具有断电恢复和动态磨损均衡功能。一旦挂载,littleFS 就会提供一整套类似 POSIX 的文件和目录功能,因此可以像普通文件系统一样进行操作。LittleFS一共只有4个文件,使用时基本不需要修改。由于LittleFS所要操作的NOR/NAND Flash本质上是一个块设备,为了方便使用,LittleFS是以块为单位进行读写的,底层NOR/NAND Flash接口驱动也是以块为单位进行。
我们先来看看LittleFS配置参数的具体内容。
const struct lfs_config LittleFS_config = {
.context = (void*)0,
.read = lfs_sdmmc_read,
.prog = lfs_sdmmc_prog,
.erase = lfs_sdmmc_erase,
.sync = lfs_sdmmc_sync,
.read_size = 512,
.prog_size = 512,
.block_size = 65536,
.block_count = 128,
.block_cycles = 100,
.cache_size = 512,
.lookahead_size = LITTLEFS_LOOKAHEAD_SIZE
};
其中第一项(.context)在本工程中没有用到,在原工程中用来保存文件系统存放在Flash中的偏移量。
第二项(.read)到第五项(.sync)指向每个操作的处理程序。
第六项(.read_size)是读操作的最小单位。该值大约等于SD卡的BLOCK大小。在SD卡驱动中,这个大小已经固定为512。所以为了方便,也设置为512。
第七项(.prog_size)为每次写入的字节数,与.read_size一样,都是512字节。
第八项是.block_size。这可以被认为是SD卡在执行擦除操作时支持的最小擦除块。这里的默认值并不重要,需要在SD卡初始化后根据实际值在程序中设置。本实验所用的卡是64k字节作为擦除块,所以这里直接使用65536。
第9项(.block_count)用于指示有多少个可擦除块。将 .block_size 相乘即可得到卡片的尺寸。如果可以更换卡,需要根据SD卡初始化后的参数来判断。
第十项(.block_cycles)是每个块的擦除周期数。
第 11 项(.cache_size)与缓存缓冲区有关。感觉好像越大越好,但实际上修改这个值是不行的。所以仍然是 512。
第 12 项(lookahead_size),littleFS 使用前瞻缓冲区来管理和分配块。前瞻缓冲区是一个固定大小的位图,用于记录有关区域内块分配的信息。前瞻缓冲区只记录一个区域中的块分配信息,当需要了解其他区域的分配情况时,需要扫描文件系统来查找已分配的块。如果前瞻缓冲区中没有空闲块,则需要移动前瞻缓冲区以在文件系统中查找其他空闲块。前瞻缓冲区位置每次移动一个 lookahead_size。这里使用原始值。
移植工作就到这里。现在我们可以测试项目了。
您可以看到它运行良好。littleFS-SD项目可以读取/写入/创建文件夹和擦除。并且它还支持附加到现有文件。
但是经过更多的测试,发现一个问题,如果反复添加->-关闭->-添加->关闭一个文件,文件打开的速度会越来越慢,甚至需要几秒钟。这个才是应该添加的,并不是直接写在文件的最后一个块里,而是会申请一个新的块,不管前面的块是否满了。参见下图。
上图打印出了每个写入命令中使用的所有读取、写入和擦除操作。可以看到lfs_file_open中每次读取都比上次写入操作多一次。这样经过几十、几百次循环之后,一个文件就会涉及很多个块。依次读取这些块非常耗时。测试发现读取超过100次需要的时间超过秒。为了加快速度,建议在添加数十次之后将一个文件的内容复制到另一个文件。这样,分散的内容就会被整合起来,写成少量的块。这可以大大加快阅读和写作的速度。
i.MXRT 101x i.MXRT 102x i.MXRT 105x i.MXRT 106x i.MXRT 600
View full article