LittleFS是否可以应用于SD卡中呢?

描述

背景介绍

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的情况:

NOR flash

下面是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。这里使用原来的值即可。

好了,到此为止基本上都改好了。插上卡试一试。

NOR flash

果然,移植非常成功,format以后,可以写可以读可以建目录。还可以在已有的文件后面添加。

可我们还是在多次测试后发现一个问题,如果对一个文件进行反复的添加->关闭->添加->关闭操作后,这个文件的打开会越来越慢,甚至需要几秒钟。这是应为添加的内容并不是直接写在文件最后一个BLOCK里,而是会新申请一个BLOCK,不管之前的BLOCK是否写满。如图:

NOR flash

上图是把每次write命令中用到的所有读、写、擦除操作的次数打印出来。可以看到每次在lfs_file_open中都要比上次写操作多一次读。这样在经过几十上百次循环后一个文件会涉及很多个BLOCK。这些BLOCK依次读下来非常耗费时间。测试中发现超过100次写操作后所用的时间超过秒级。为了加快速度,建议在一个文件添加几十次后,把内容复制到另一个文件中去。这样分散的内容会整合起来写入少量的BLOCK。这可以大大加快读写的速度。

总结

LittleFS作为一个轻量级的文件系统,具有比FAT小的多的footprint。同时,它又比FAT更加可靠,更适合嵌入式环境下使用。而SD卡不但容量远远超过NOR flash,同时又能支持SPI接口,并且可以随意插拔,具有极大的灵活性。将两者结合可以使单片机系统具有很强的数据记录能力。







审核编辑:刘清

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分