背景介绍
LittleFS是一个应用于单片机内部flash和外挂NOR flash的文件系统。由于它相比传统的FAT文件系统更适合于小型嵌入式系统,所以越来越多人把它应用于自己的项目中。那么除了NOR/NANDflash类型的存储设备外,LittleFS是否可以应用于SD卡中呢?其实也是可以的。本文将使用i.mxRT1050 SDK中的littlefs_shell项目和sdcard_fatfs项目,改造出一个读写SD卡的littefs_shell。
操作步骤
本次实验采用的是MCUXpresso IDE v11.7,SDK使用2.13版本。littleFS文件系统一共只有4个文件,其中lfs.h中显示了当前的版本是littleFS 2.5。
1. 首先当然是把SD相关的代码加入littlefs_shell工程。最简单的方法莫过于再导入一个sdcard_fatfs项目,随后将其中的sdmmc目录全部复制到我们的工程下面。随后还要复制board目录下的sdmmc_config.c和sdmmc_config.h,drivers目录下的fsl_usdhc.c和fsl_usdhc.h。
2. 修改程序,包括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(" SD host init fail "); return kStatus_Fail; } /* wait card insert */ if (SD_PollingCardInsert(&m_sdCard, kSD_Inserted) == kStatus_Success) { PRINTF(" Card inserted. "); /* power off card */ SD_SetCardPower(&m_sdCard, false); /* power on the card */ SD_SetCardPower(&m_sdCard, true); // SdMmc_Init(); } else { PRINTF(" Card detect fail. "); 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();
3.新建一个c文件,lfs_sdmmc.c。调用顺序是littlefs->lfs_sdmmc.c->lfs_sdmmc_bridge.c->fsl_sd.c。
lfs_sdmmc.c和lfs_sdmmc_bridge.c作为中间层,可以连接littlefs和sd上层驱动。其中必须要注意的是地址的映射关系。littleFS给出的地址是块地址 + 偏移地址。见下图。这是一次mount命令所发出的读指令。其中的块地址指的是擦除块(sector)的地址。而读写操作使用的是最小的读写块地址(BLOCK),具体在下文中说明。
因此在lfs_sdmmc.c中先把littleFS给的地址转换成byte地址。再在lfs_sdmmc_bridge.c中把SD卡读写地址改为BLOCK地址。由于目前大多数SD卡都超过了4GB,byte地址需用64位变量。
下图是littleFS在mount的时候读BLOCK的情况:
下面是lfs_sdmmc.c中read和erase的函数:
int lfs_sdmmc_read(const struct lfs_config *lfsc, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) { struct lfs_sdmmc_ctx *ctx; uint64_t flash_addr; assert(lfsc); flash_addr = block * lfsc->block_size + off; if (lfssd_Read (flash_addr, size, buffer ) != kStatus_Success) return LFS_ERR_IO; return LFS_ERR_OK; } int lfs_sdmmc_erase(const struct lfs_config *lfsc, lfs_block_t block) { status_t status = kStatus_Success; struct lfs_sdmmc_ctx *ctx; uint64_t sdmmc_addr; assert(lfsc); sdmmc_addr = block * lfsc->block_size; for (uint32_t sector_ofs = 0; sector_ofs < lfsc->block_size; sector_ofs +=lfsc->block_size) { status = lfssd_EraseBlocks (sdmmc_addr + sector_ofs, 512); if (status != kStatus_Success) break; } if (status != kStatus_Success) return LFS_ERR_IO; return LFS_ERR_OK; }
这是lfs_sdmmc_bridge.c中read和erase函数。可以分辨其中的地址映射关系:
bool lfssd_EraseBlocks (uint64_t address, uint32_t len) { if (address % BLOCK_SIZE > 0) return kStatus_Fail; uint32_t startDataBlockIndex = address / BLOCK_SIZE; if(SD_EraseBlocks (&m_sdCard, startDataBlockIndex, len/BLOCK_SIZE) == kStatus_Success) return kStatus_Success; else return kStatus_Fail; } bool lfssd_Read (uint64_t address, uint32_t dataLen, void* buff) { if (dataLen == 0) return true; if (kStatus_Success != SD_ReadBlocks (&m_sdCard, buff, address/BLOCK_SIZE, SD_CARD_DATA_BLOCK_COUNT)) { return kStatus_Fail; } return kStatus_Success; }4. 最重要的一步是littleFS参数配置。在peripherals.c中有一个结构体LittlsFS_config,这个结构体中不但包含了SD卡的操作函数,还包括读写扇区和缓存大小。这个结构体的设置非常关键。如果设的不好,不但影响性能,更可能会运行出错。在设置之前,让我们先来介绍一下SD卡和littleFS的大致原理。
SD卡的存储单元是BLOCK,读写都可以按照BLOCK进行。不同的卡每个BLOCK的大小是可以不同的。对于标准SD卡,可以用CMD16设置块命令的长度,对于SDHC卡块命令长度固定为512字节。SD卡的擦除是按照扇区或者说SECTOR进行的。每个扇区的大小需要查SD卡的CSD寄存器。
如果CSD寄存器ERASE_BLK_EN= 0时,Sector是最小的擦除单元,它的单位是“块”。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接口驱动都是以block为单位进行的。
下面来看一下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 };
其中,第一项在本项目没有什么用,在SDK中用来保存文件系统在Flash中存放的偏移量;
第二项(.read)到第五项(.sync)指向各项操作的处理函数;
第六项.read_size是读操作的最小单位。这个值大致等于SD卡的BLOCK大小。在SD卡驱动程序中,这个大小已经固定设为512。所以为了方便这里也一样设为512。
第七项.prog_size就是每次写入的字节数,这里和.read_size一样都是512字节。
第八项是.block_size。这一项可以认为就是进行擦除操作时SD卡支持的最小擦除块。这里默认值不重要,需要在SD卡初始化后根据实际情况在程序中设置。
第九项(.block_count)是用来表示一共有多少可擦除块的。和.block_size相乘就可以得到卡的大小。本次实验中使用的卡就是64k字节为一个擦除块,所以这里直接使用65536。如果卡是可换的则需要在SD卡初始化后再根据参数确定。
第十项(.block_cycles)是每个block的擦写循环次数。
第十一项(.cache_size)缓存大小。给人的感觉应该是越大越好,但实际上修改这个值后会无法工作。所以还是512。
第十二项(lookahead_size)littlefs中使用一个lookahead buffer来管理和分配块。lookahead buffer是一个固定大小的bitmap,记录一片区域内块分配的信息。lookaheadbuffer只记录了一片区域内块分配的信息,当需要知道其他区域块分配的情况时,就需要进行扫描文件系统来查找已分配的块。如lookahead buffer中已经没有空闲块、需要推移lookaheadbuffer来查找文件系统中的其他空闲块。每次lookahead buffer位置推移一个lookahead_size。这里使用原来的值即可。
好了,到此为止基本上都改好了。插上卡试一试。
果然,移植非常成功,format以后,可以写可以读可以建目录。还可以在已有的文件后面添加。
可我们还是在多次测试后发现一个问题,如果对一个文件进行反复的添加->关闭->添加->关闭操作后,这个文件的打开会越来越慢,甚至需要几秒钟。这是应为添加的内容并不是直接写在文件最后一个BLOCK里,而是会新申请一个BLOCK,不管之前的BLOCK是否写满。如图:
上图是把每次write命令中用到的所有读、写、擦除操作的次数打印出来。可以看到每次在lfs_file_open中都要比上次写操作多一次读。这样在经过几十上百次循环后一个文件会涉及很多个BLOCK。这些BLOCK依次读下来非常耗费时间。测试中发现超过100次写操作后所用的时间超过秒级。为了加快速度,建议在一个文件添加几十次后,把内容复制到另一个文件中去。这样分散的内容会整合起来写入少量的BLOCK。这可以大大加快读写的速度。
总结
LittleFS作为一个轻量级的文件系统,具有比FAT小的多的footprint。同时,它又比FAT更加可靠,更适合嵌入式环境下使用。而SD卡不但容量远远超过NOR flash,同时又能支持SPI接口,并且可以随意插拔,具有极大的灵活性。将两者结合可以使单片机系统具有很强的数据记录能力。
审核编辑:刘清
全部0条评论
快来发表一下你的评论吧 !