littlefs-project / littlefs

A little fail-safe filesystem designed for microcontrollers
BSD 3-Clause "New" or "Revised" License
5.18k stars 794 forks source link

Writing 64 Bytes is taking 2.5 seconds @ 40 MHz #1019

Open cnitroy opened 2 months ago

cnitroy commented 2 months ago

Hello all,

I am using LittleFS with an chip running at 40MHz and interfacing an 8MByte QSPI Flash chip. I am able to perform all the file functions needed of creating, editing, deleting, etc, but my main concern is the speed of operation. I am writing 64 bytes of data to SPI flash, and it takes 2.5 seconds, and this is after ramping up the clock speed to 40MHz from 16MHz. At 16MHz, it was even closer to 5-6 seconds.

Reading posts online and in this repo, I see speed is a common performance issue, but can someone tell me if the times I am seeing are common or is there possibly a configuration issue here? I've put my configuration below.

_Sidenote: My File_System_FlashSync function currently only returns 0, I don't have a cache flush yet.

Thank you!


uint8_t FS_prog_buffer[0x200] = {0};
uint8_t FS_read_buffer[0x200] = {0};
uint8_t FS_lookahead_buffer[0x16] = {0};

struct lfs_config cfg = {
    // Block device operations
    .read  = File_System_Flash_Read,
    .prog  = File_System_Flash_Prog,
    .erase = File_System_Flash_Erase,
    .sync  = File_System_Flash_Sync,

    // Block device configuration
    .read_size          = 0x10,      // Min. size of block read
    .prog_size          = 0x10,      // Min. size of block program
    .block_size         = 0x10000,     // Size of erasable block in bytes
    .block_count        = 0x80,    // 128 blocks based on 64kB sized blocks
    .cache_size         = 0x200,        // Size of block caches in bytes (must = read/prog buffer size)
    .lookahead_size     = 0x16, // Size of the lookahead buffer in bytes
    .block_cycles       = 0x200,   // Number of erase cycles before moving to another block
    .prog_buffer        = FS_prog_buffer,
    .read_buffer        = FS_read_buffer,
    .lookahead_buffer   = FS_lookahead_buffer,
};```
cnitroy commented 2 months ago

For what it's worth, here are my helper functions for the configurations as well. I've seen in some examples, these functions write to a cache, rather than directly to SPI Flash. Is that the recommended method?

int8_t File_System_Flash_Read( const struct lfs_config *c,
                                lfs_block_t block,
                                lfs_off_t offset, 
                                void *buffer,
                                lfs_size_t size) 
{
    int8_t error = LFS_ERR_OK;
    uint8_t HAL_error = HAL_OK;

    // Calculate the address to read from
    uint32_t addr = block * (cfg.block_size) + offset;
    HAL_error = MX25R6435F_ReadData(addr, buffer, size);

    if(HAL_OK < HAL_error)
    {
        error = LFS_ERR_IO;
    }
    return error;
}

int8_t File_System_Flash_Prog( const struct lfs_config *c,
                                lfs_block_t block,
                                lfs_off_t offset, 
                                const void *buffer,
                                lfs_size_t size) 
{
    int8_t error = LFS_ERR_OK;
    uint8_t HAL_error = HAL_OK;

    // Calculate the address to program
    uint32_t addr = block * (cfg.block_size) + offset;

    // Ensure the size is a multiple of prog_size
    if (size % c->prog_size != 0)
    {
        return -1;  // Error, size should be a multiple of prog_size
    }

    HAL_error= MX25R6435F_WriteData(addr, (uint8_t*)buffer, size);
    if(HAL_OK < HAL_error)
    {
        error = LFS_ERR_IO;
    }
    return error;
}

int8_t File_System_Flash_Erase( const struct lfs_config *cfg,
                                lfs_block_t block)
{
    int8_t error = LFS_ERR_OK;
    uint8_t HAL_error = HAL_OK;

    // Calculate the address to erase
    uint32_t addr = block * (cfg->block_size);
    HAL_error = MX25R6435F_EraseSector(addr);

    if(HAL_OK < HAL_error)
    {
        error = LFS_ERR_IO;
    }

    return error;
}

int8_t File_System_Flash_Sync(const struct lfs_config *c)
{
    // Sync to ensure all flash memory operations are completed.
    // If using a cache or buffer, this is where we flush it.

    return 0; // Return 0 on success or a negative error code on failure.
}
XiaoSenLuo commented 2 months ago

LittleFS uses a copy-on-write mechanism to ensure that the file system remains consistent even in the event of a power failure. This means that instead of directly overwriting data, it writes new data to a new location and then updates pointers to the new data. This extra step can introduce some latency.

cnitroy commented 2 months ago

Xiao, thanks for your response. That makes sense that it would add some time to copy-on-write. I wish I had an idea of what "normal" performance is, if that makes sense. This is my first time working with a file system like this, so I don't know if I've implemented the system poorly and this is leading to long write times (5 seconds) for a small amount of data (64 bytes).

cnitroy commented 2 months ago

Changed the structure of my file system so I would create a new 64 byte file instead of write to the 32kB file, now the full create/write/close cycle is only ~100ms. I'm finding each successive file created adds ~5-10 ms each time which I am trying to now minimize.

geky commented 1 month ago

Hi @cnitroy, it looks like you're using a 64KiB block size, but the MX25R can actually erase in 4KiB blocks with the sector-erase command.

Large block sizes are one of the main sources of performance issues for littlefs right now, so this may help things. There is also metadata_max that can artificially limit metadata block size, but you may not need this with 4KiB blocks.

I realize it's confusing with littlefs calling these blocks and MX25R calling these sectors, but other storage technology sometimes calls these blocks so you can't really win naming-wise...

cnitroy commented 3 weeks ago

Hi @geky, thanks so much for taking the time to comment on this thread. The reason I am using the larger block and subblock (64 and 32kB) size is because when testing the switch to a sector-erase (4kB), I end up with a corrupt filesystem, and I cannot figure out why. I change the block size(0x1000) and block count(0x800) to their respective values, change the erase function to be a sector erase, and always end up with this after just a few file writes: ../Drivers/LittleFS/lfs.c:2278:debug: Bad block at 0x0 ../Drivers/LittleFS/lfs.c:2283:warn: Superblock 0x0 has become unwritable Not sure if I'm missing another crucial step for switching to sector erases?

geky commented 3 weeks ago

The reason I am using the larger block and subblock (64 and 32kB) size is because when testing the switch to a sector-erase (4kB), I end up with a corrupt filesystem, and I cannot figure out why. I change the block size(0x1000) and block count(0x800) to their respective values, change the erase function to be a sector erase, and always end up with this after just a few file writes: ../Drivers/LittleFS/lfs.c:2278:debug: Bad block at 0x0 ../Drivers/LittleFS/lfs.c:2283:warn: Superblock 0x0 has become unwritable

Hmm, my first thought would be the wrong erase command, but you mentioned switching to sector erases.

Is it possible the address is getting the lower bits masked out somewhere?

Unfortunately the low-level SPI drivers/libraries vary quite a lot so it's hard to know more. It may be easier to debug outside of littlefs, by manually writing/reading back blocks on the flash and checking that nothing clobbers block 0.

XiaoSenLuo commented 1 week ago

for your read, write, erase function, you can try use these configuration for lfs:

struct lfs_config cfg = {
    // Block device operations
    .read  = File_System_Flash_Read,
    .prog  = File_System_Flash_Prog,
    .erase = File_System_Flash_Erase,
    .sync  = File_System_Flash_Sync,

    // Block device configuration
    .read_size          = sizeof(FS_read_buffer),      // Min. size of block read
    .prog_size          = sizeof(FS_prog_buffer),      // Min. size of block program
    .block_size         = 4096,     // Size of erasable block in bytes
    .block_count        = 64 * 1024 * 128 / 4096,    // 128 blocks based on 64kB sized blocks
    .cache_size         = sizeof(FS_read_buffer),        // Size of block caches in bytes (must = read/prog buffer size)
    .lookahead_size     = 64 * 1024 * 128 / 4096 / 8, // Size of the lookahead buffer in bytes
    .block_cycles       = 0x200,   // Number of erase cycles before moving to another block
    .prog_buffer        = FS_prog_buffer,
    .read_buffer        = FS_read_buffer,
    .lookahead_buffer   = FS_lookahead_buffer, // lookahead buffrer size must be 64 * 1024 * 128 / 4096 / 8 Byte.
};