8000 LittleFS Filesystem Support by andrewleech · Pull Request #5167 · micropython/micropython · GitHub
[go: up one dir, main page]

Skip to content

LittleFS Filesystem Support #5167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 17 commits into from
Closed

Conversation

andrewleech
Copy link
Contributor

This is based predominantly on a rebased version of #3847 with some additions for library version support, minor bugs and mpconfigboard based configuration.

I don't expect it to be ready to merge in the current form, but wanted to share it as a working example.

I've been using this implementation for some months now with no issues of reliability, no filesystem corruption.

My custom hardware has an external spiflash chip which is divided in half, the first half being used as a FAT filesystem (written once during calibration), the second half formatted as littlefs which has runtime information, logging, etc written to it quite regularly.

Neither of these filesystems are exposed on usb mass storage in my application.

The littlefs bindings support the library at both version v1.7.2 (currently pinned) and the newer v2.0.x with no extra configuration needed, however in my previous testing of v2.0.3 I saw some (suspected) fs corruption so rolled back to the stable v1.7.2
I was battling some other FS corruption causing problems around the same time, so it might not have been a littlefs version/bug at fault.

mpconfigboard.h

// Disable internal flash, use external spi flash instead
#define MICROPY_HW_ENABLE_INTERNAL_FLASH_STORAGE (0)

// QSPI Flash Interface - 32MB NOR Flash
#define MICROPY_HW_QSPIFLASH_SIZE_BYTES (32*1024*1024)
#define MICROPY_HW_QSPIFLASH_CS         (pin_B10)
#define MICROPY_HW_QSPIFLASH_SCK        (pin_F10)
#define MICROPY_HW_QSPIFLASH_IO0        (pin_F8)
#define MICROPY_HW_QSPIFLASH_IO1        (pin_F9)
#define MICROPY_HW_QSPIFLASH_IO2        (pin_F7)
#define MICROPY_HW_QSPIFLASH_IO3        (pin_F6)

#define MICROPY_HW_SPIFLASH_SIZE_BITS (MICROPY_HW_QSPIFLASH_SIZE_BYTES*8)

// this formula computes the log2 of "m"
#define BITS_TO_LOG2(m) ((m) - 1) / (((m) - 1) % 255 + 1) / 255 % 255 * 8 + 7 - 86 / (((m) - 1) % 255 + 12)

#define MICROPY_HW_QSPIFLASH_SIZE_BITS_LOG2 BITS_TO_LOG2(MICROPY_HW_SPIFLASH_SIZE_BITS)

// First half of the flash (minus 512KB) is used for first FatFS Partition
#define PART_OFFSET (MBOOT_SPIFLASH_ERASE_BLOCKS_PER_PAGE * MP_SPIFLASH_ERASE_BLOCK_SIZE)
#define MICROPY_HW_SPIFLASH_PART1_BITS ((MICROPY_HW_QSPIFLASH_SIZE_BYTES/2-PART_OFFSET)*8)

// Starting the second partition 512KB (1 erase page) before the half way mark means older
// bootloaders without the qspi addressing fix can still wipe the second partitions' fs table

// Second half of the flash is used for LittleFS Partition
#define MICROPY_VFS_LITTLEFS                    (1)
#define MICROPY_HW_ENABLE_NATIVE_LFS            (1)
#define MICROPY_VFS_LITTLEFS_START_OFFSET_BYTES (MICROPY_HW_SPIFLASH_PART1_BITS/8)

// spiblock device config for SPI flash
extern const struct _mp_spiflash_config_t spiflash_config;
extern struct _spi_bdev_t spi_bdev;
#define MICROPY_HW_BDEV_IOCTL(op, arg) ( \
    (op) == BDEV_IOCTL_NUM_BLOCKS ? (MICROPY_HW_SPIFLASH_PART1_BITS / 8 / FLASH_BLOCK_SIZE) : \
    (op) == BDEV_IOCTL_INIT ? spi_bdev_ioctl(&spi_bdev, (op), (uint32_t)&spiflash_config) : \
    spi_bdev_ioctl(&spi_bdev, (op), (arg)) \
)
#define MICROPY_HW_BDEV_READBLOCKS(dest, bl, n) spi_bdev_readblocks(&spi_bdev, (dest), (bl), (n))
#define MICROPY_HW_BDEV_WRITEBLOCKS(src, bl, n) spi_bdev_writeblocks(&spi_bdev, (src), (bl), (n))

bdev.c

#include "qspi.h"
#include "storage.h"
#include "factoryreset.h"

STATIC const mp_soft_qspi_obj_t qspi_bus = {
    .cs  = MICROPY_HW_QSPIFLASH_CS,
    .clk = MICROPY_HW_QSPIFLASH_SCK,
    .io0 = MICROPY_HW_QSPIFLASH_IO0,
    .io1 = MICROPY_HW_QSPIFLASH_IO1,
    .io2 = MICROPY_HW_QSPIFLASH_IO2,
    .io3 = MICROPY_HW_QSPIFLASH_IO3,
};

STATIC mp_spiflash_cache_t spi_bdev_cache;

const mp_spiflash_config_t spiflash_config = {
    .bus_kind = MP_SPIFLASH_BUS_QSPI,
    .bus.u_qspi.data = (void*)&qspi_bus,
    .bus.u_qspi.proto = &qspi_proto,
    .cache = &spi_bdev_cache,
};

spi_bdev_t spi_bdev;

Note our hardware has a 32MB spiflash chip which also requires #5166

@dmartauz
Copy link
dmartauz commented Oct 3, 2019

Is this PR port-agnostic? Will it work on ESP32? I would also like to introduce second partition for logging in my application.

@dpgeorge
Copy link
Member
dpgeorge commented Oct 9, 2019

Is this PR port-agnostic? Will it work on ESP32?

Yes most of it is port-agnostic. It should work on esp32 with a block device written in Python, interfacing between littlefs and the esp32's flash storage.

@dpgeorge
Copy link
Member
dpgeorge commented Oct 9, 2019

Thanks @andrewleech for publishing this. I do want to get littlefs support in master ASAP.

There are two main discussion points around this:

  1. Do we support both littlefs v1 and v2? They have incompatible on-disk formats so should be considered as logically different filesystems. I think it would be good to support both because it's not clear that v2 is always better than v1. To support both would mean having lfs1_xxx and lfs2_xxx C functions available at the same time, and then uos.VfsLittleFS1 and uos.VfsLittleFS2 classes. They don't both need to be available on any given port, but could be in principle if both are needed.
  2. We need to define a new "block device" protocol at the Python level to interface to littlefs.

The existing block device protocol is rather simple, with 3 methods defined: readblocks, writeblocks and ioctl. Operations are on blocks of a fixed size (same for read/write) and a call to writeblocks assumes that the data will be correctly written, ie that flash is first erased then written to.

For littlefs the requirements are different:

  • it does explicit erase calls, and write must only do an overwrite
  • read, write and erase "block" sizes can be different

(The existing block protocol could be used for littlefs with read=write=erase the same size, but it would be inefficient.)

As I see it there are two options to define the new/extended block device protocol:
a) have a "mode switch" ioctl which selects between the 2 variants
b) define a new set of independent methods/ioctls

I'd prefer (b), because it's less confusing, and allows an entity to implement both protocols at the same time (by implementing both sets of methods).

An idea for the new protocol, extending the old one, is:

class AbstractBlockDevice:
    def readchunks(self, rd_chunk_num, buf):
        # similar to readblocks, read starting at chunk_num into given buf
    def overwritechunks(self, wr_chunk_num, buf):
        # don't erase, just overwrite existing data from buf, starting at chunk_num
    def erasechunks(self, er_chunk_num, bytes_to_erase):
        # erase starting from chunk_num
    def ioctl(self, op, arg):
        # 1-5 are existing ops
        # 6 = get read chunk size
        # 7 = get write chunk size
        # 8 = get erase chunk size

(read, write and erase chunk sizes can be different)

@andrewleech
Copy link
Contributor Author

I feel supporting both v1 and v2 via different interfaces could be somewhat difficult as the lfs function interfaces are pretty much exactly the same, only selected by different pins of the submodule.

Perhaps if uos.VfsLittleFS has a required version argument it can use to check if the currently compiled version is as requested that would provide equivalent functionality without needing to duplicate the class?
I don't think both could be supported at once, unless two copies of the submodule were provided, but I doubt the function name clashes could be resolved?

v2 has had quite a few releases since I first started looking at this, perhaps it's now stable enough to ignore v1 altogether.

I like your suggestion for an extended version of the python block interface, that looks quite flexible without a lot of overhead.

On an unrelated note, might need some adjustments to the default enabled status of the module:

LINK build-B_L072Z_LRWAN1/firmware.elf
arm-none-eabi-ld: build-B_L072Z_LRWAN1/firmware.elf section `.text' will not fit in region `FLASH'
arm-none-eabi-ld: region `FLASH' overflowed by 7096 bytes

Seems the M0's will need it off by default as a minimum.

@andrewleech
Copy link
Contributor Author

If we want to extend the block interface for improved performance in lfs, we'll probably also want to expose some of the other lfs settings to the end user:

  • block_cycles
  • cache_size
  • lookahead_size
  • lookahead_buffer

An interesting discussion on the sizing and usage of blocks/cache: littlefs-project/littlefs#277

@dpgeorge
Copy link
Member

I don't think both could be supported at once, unless two copies of the submodule were provided, but I doubt the function name clashes could be resolved?

IMO all the technical issues supporting v1 and v2 at the same time can be overcome: like with oofatfs we just include the littlefs code verbatim in this repo (not as a submodule, it's pretty minimal so no real need to have another submodule just for a few files), then we can have two copies at once. Littlefs has a script to rename the API functions to include a prefix, so we can have the following directory/file layout:

lib/
  littlefs/
    lfs_v1.[ch]
    lfs_util_v1.[ch]
    lfs_v2.[ch]
    lfs_util_v2.[ch]

Then the extmod driver extmod/vfs_littlfs.c can be made macro conditional on v1 and v2 and we compile it twice, once for v1 and once for v2 (this is how the native emitter works, emitnative.c is compiled once per arch).

So supporting two versions is possible. It's then up to a port/board to decide whether to enable them both or not.

@dpgeorge
Copy link
Member

If we want to extend the block interface for improved performance in lfs, we'll probably also want to expose some of the other lfs settings to the end user:

I don't think these settings need to be part of the block device interface. Instead they belong to the constructor of LittleFS as mount parameters.

@andrewleech
Copy link
Contributor Author

Sounds good. Yes I did mean those arguments as parameters for LittleFS, not the block.

I'm having a go at implementing an initial chunks interface today, see how far I get!

return 0;
}
#if (LFS_VERSION >= 0x00020000)
static uint8_t __attribute__ ((aligned (64))) lookahead_buffer[128/8];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrewleech I'm pretty sure this should be lookahead_buffer[128]...! LFS2 changed this, compared to LFS1. Having a small buffer will lead to memory corruption.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, here's the malloc for that buffer when lfs is used with dynamic memory.
https://github.com/ARMmbed/mbed-littlefs/blob/master/littlefs/lfs2.c#L3218
It doesn't have the divide by 8, you're right. That'll be why I had some corruption when I first tested LFSv2!

@andrewleech
Copy link
Contributor Author

Closed in favor of newer work & PR's breaking down the feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants
0