Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

MicroPython for Microcontrollers  Module 8 – Files, storage and data formats

Filesystems: LittleFS, FAT and vfs.mount

How a Pico or an ESP32 keeps files in flash: block devices, LittleFS or FAT, vfs.mount() and os.statvfs(), and a RAM block device for tests.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain why changing data in flash needs an erase of a whole sector, and what a block device gives a filesystem
  • Choose LittleFS or FAT for a board's flash or an SD card
  • List the mounts with vfs.mount() and work out the free space from os.statvfs()
  • Build a RAM block device for filesystem tests

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain why changing data in flash needs an erase of a whole sector, and what a block device gives a filesystem
  • Choose LittleFS or FAT for a board's flash or an SD card
  • List the mounts with vfs.mount() and work out the free space from os.statvfs()
  • Build a RAM block device for filesystem tests

Before you start

On this page

A Pico keeps every file you copy to it in the last 1,408 KiB of its 2 MiB flash chip, and that chip cannot change a byte the way RAM does: a bit that has been cleared to 0 becomes a 1 again only when the whole 4,096-byte sector around it is erased. What open() shows you as files is the work of two layers in between, a filesystem that decides where data goes and a block device that reads, programs and erases the flash.

You will follow a file down those layers, compare the two filesystems MicroPython puts on flash, LittleFS and FAT, read vfs.mount() and os.statvfs() the way a board reports them, and build a block device in RAM, so that code which writes files can be tested on your computer.

Flash is erased in sectors, so files sit on a block device

Say a file holds the text 21.5 and the new value is 19.0. The Pico’s flash has two operations that change data. Erasing sets every bit of a sector (of flash): The smallest part of a flash chip that can be erased at once, 4,096 bytes on the Pico. An erase sets all its bits to 1; programming can then clear bits to 0, and only another erase sets them back. to 1, so all its bytes read 0xff; the pico-sdk’s flash functions erase in sectors of 4,096 bytes, and an ESP32 board’s partitions erase in blocks of the same size. Programming then turns bits from 1 to 0, and nothing turns a 0 back into a 1 except another erase of the whole sector. The next program models one sector by those two rules and writes the new value straight over the old one:

One flash sector: erase, program, and a write without an erase Python · flash/nor_flash.py
"""One 4,096-byte sector of a Pico's flash, by the rules of the pico-sdk's flash functions.

Erasing a sector sets all its bits to 1, so every byte reads 0xff. Programming can only turn
bits from 1 to 0, so a byte programmed over old data becomes the AND of the old and the new.
"""
SECTOR = 4096


class Sector:
    def __init__(self):
        self.data = bytearray(b"\xff" * SECTOR)  # a new chip comes erased
        self.erases = 0

    def erase(self):
        self.data[:] = b"\xff" * SECTOR
        self.erases += 1

    def program(self, offset, buf):
        for i, b in enumerate(buf):
            self.data[offset + i] &= b  # 1 can become 0, never 0 back to 1

    def read(self, offset, n):
        return bytes(self.data[offset:offset + n])


def show(label, data):
    print("%-22s %-8r %s" % (label, data, data.hex(" ")))


s = Sector()
show("erased:", s.read(0, 4))
s.program(0, b"21.5")
show("programmed 21.5:", s.read(0, 4))
s.program(0, b"19.0")  # the new value over the old, no erase
show("19.0 over it:", s.read(0, 4))
s.program(4, b"22.0")  # into erased bytes after it
show("22.0 after it:", s.read(4, 4))
s.erase()
s.program(0, b"19.0")
show("erase, then 19.0:", s.read(0, 4))
print("erases: %d, each one of %d bytes" % (s.erases, SECTOR))

Output

erased:                b'\xff\xff\xff\xff' ff ff ff ff
programmed 21.5:       b'21.5'  32 31 2e 35
19.0 over it:          b'01.0'  30 31 2e 30
22.0 after it:         b'22.0'  32 32 2e 30
erase, then 19.0:      b'19.0'  31 39 2e 30
erases: 1, each one of 4096 bytes

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 nor_flash.py

Programming 19.0 over 21.5 left 01.0: every bit that was already 0 stayed 0, and the result is a different number that still looks like a number. The bytes after it were still erased, so 22.0 went in cleanly: data can always be added in erased space. Changing data in place, though, means erasing and rewriting all 4,096 bytes of the sector for a change of 4.

A value that comes out right by luck

Press Edit, write 20.5 instead of 19.0 over 21.5 and run it. The sector now reads 20.5, correct, because that change only clears bits. Code that skips the erase can pass a test like this one and fail on the next value.

So a filesystem never writes to the chip directly. It works through a block device: An object that stores data in numbered blocks of one size and offers readblocks(), writeblocks() and ioctl() for the block count, the block size and erasing. MicroPython's filesystems sit on one, such as rp2.Flash or an SD card., an object that splits the storage into numbered blocks of one size. On a Pico, rp2.Flash() is the block device for the part of the flash kept for files: 352 blocks of 4,096 bytes, the 1,441,792 bytes the RPI_PICO firmware sets aside. On an ESP32 board it is the partition named vfs, an esp32.Partition. Every block device has the same three methods:

Call What it does
readblocks(n, buf, offset) Reads len(buf) bytes from block n, starting offset bytes into it
writeblocks(n, buf, offset) Programs those bytes, after the caller has erased the block
writeblocks(n, buf) Writes whole blocks, which the method erases first
ioctl(4, 0) and ioctl(5, 0) Return the number of blocks and the bytes in a block
ioctl(6, n) Erases block n

The forms with an offset make up the extended interface, the forms without one the simple interface. LittleFS needs the extended one: it erases a block with ioctl(6, n) and then programs parts of it, the add-in-erased-space pattern the program showed to be safe.

Key idea

Flash clears bits one by one, but only an erase of a whole sector sets them back, so a filesystem writes new data into erased space and reaches the chip through a block device that reads, programs and erases blocks.

Hex Viewer & File Inspector Look at the bytes of a file copied off a board, and spot the 0xff of erased flash.

Checkpoint

Check: Flash sectors and block devices

  1. Question 1 of 2 A byte of flash holds 0x35, the character 5. Without an erase, 0x30 is programmed over it, then 0x39. What does the byte read now?

    Choose one answer.

    A hint

    Programming leaves each bit at the AND of what was there and what was written.

    Show the answer: Question 1 of 2

    Answer: 0x30, the character 0

    Each programming step can only clear bits, so the byte ends as the AND of every value written since the last erase: 0x35 & 0x30 & 0x39 = 0x30. The program of this section showed the same thing with 19.0 written over 21.5.

  2. Question 2 of 2 Why does LittleFS need a block device that answers ioctl(6, n)?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It erases a block with it before programming parts of that block at an offset

    The extended interface separates erasing, ioctl(6, n), from programming, writeblocks(n, buf, offset), and LittleFS uses both: it erases a block, then programs it in parts as data arrives.

LittleFS survives power cuts; FAT is what a PC can read

Two filesystems matter on a board’s block devices, and both the rp2 and the esp32 port start with the same one. Their boot scripts decide what happens before your boot.py runs:

At boot rp2 (Pico) esp32
The filesystem LittleFS v2 at /, programmed in 256-byte pages LittleFS v2 on the vfs partition at /
No filesystem yet Formats the flash with LittleFS Formats it, if the first block is still blank
One it cannot mount Formats the flash, and every file is gone Prints that the filesystem appears to be corrupted, in a loop, and changes nothing

The two ports treat a broken filesystem in opposite ways: a Pico starts again empty, while an ESP32 board waits, with your data still on the flash for a recovery attempt.

LittleFS: A small filesystem designed for flash: it writes changes to free blocks, commits them in small checked logs and spreads erases over the blocks. The rp2 and esp32 ports of MicroPython format their flash with LittleFS v2. was designed for flash. Its documentation lists what that buys: it is copy-on-write: Writing changed data to free blocks and switching to it in one small commit, instead of writing over the old data, so that a power cut leaves the last committed version intact., writing changed data to free blocks instead of over the old data; it keeps its metadata in small logs, two blocks each, whose commits carry a 32-bit CRC; a file’s changes count only once the file is closed or synced; and rename and remove are atomic even if power is lost. A power cut leaves the filesystem in its last committed state. LittleFS also spreads its erases over the blocks it allocates.

FAT: The File Allocation Table filesystem of SD cards and USB drives, which every computer can read. MicroPython's documentation warns that a power failure during a write can corrupt it., the filesystem of SD cards and USB drives, has the opposite strength: any computer reads it. MicroPython’s filesystem documentation warns that FAT is not tolerant to power failure during writes, which can corrupt the filesystem, and recommends LittleFS wherever a PC does not need to read the storage. The littlefs design notes count FAT among the block-based filesystems that bind data to fixed places on the storage, which leaves them no way to manage wear. Answer the three questions below for a device of your own:

Interactive · answer and compare Which filesystem fits your storage? Answer three questions about your device. Each answer rules options out or adds a caveat, and says why.
Where do the files live?
Must a computer read the files straight off the storage?
Can the power fail while the board is writing?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

LittleFS and FAT, on a board's flash or on an SD card, against four things that decide the choice
Filesystem A power cut mid-write Read on a PC Wear on the flash What it needs
LittleFS on the board's flash Mounted as: VfsLfs2 at /, as rp2 and esp32 do Good a cut keeps the last committed state; changes count at close or sync Poor a PC needs the littlefs FUSE driver; copy files off with mpremote instead Good spreads erases over the blocks it allocates Good rp2.Flash and the vfs partition have the extended interface it needs
FAT on the board's flash Mounted as: VfsFat on the same block device Poor the docs warn that a cut during a write can corrupt the filesystem Mixed shown to a PC only by boards with USB drive support; others use mpremote Poor it binds data to fixed places on the flash, so it cannot spread wear Mixed you format it yourself with VfsFat.mkfs; the ports start with LittleFS
FAT on an SD card Mounted as: VfsFat at /sd Poor the same FAT risk, and pulling the card out while writing is a cut too Good take the card out and read it in any card reader Mixed FAT spreads no wear itself; a card rewritten all day still wears Good whole 512-byte blocks from the sdcard driver suit FAT
LittleFS on an SD card Mounted as: VfsLfs2 at /sd Good the same committed writes as on the board's flash Poor a card reader shows a PC nothing it can read without the FUSE driver Good spreads erases over the card's blocks Poor needs the extended interface; the sdcard driver has only the simple one
How the answers decide
  • “Where do the files live?” In the board's own flash: rules out FAT on an SD card and LittleFS on an SD card. Without a card, only the board's own flash is there to format.
  • “Where do the files live?” On a removable SD card: rules out LittleFS on the board's flash and FAT on the board's flash. The files go on the card, so the board's flash keeps only the code.
  • “Where do the files live?” On a removable SD card: adds a caveat to LittleFS on an SD card. LittleFS needs a block device with the extended interface, which micropython-lib's sdcard driver does not have.
  • “Must a computer read the files straight off the storage?” Yes, people take the data away on the card or drive: adds a caveat to LittleFS on the board's flash and LittleFS on an SD card. A PC reads LittleFS only through the littlefs FUSE driver, set up with the device's block layout.
  • “Must a computer read the files straight off the storage?” Yes, people take the data away on the card or drive and “Where do the files live?” In the board's own flash: adds a caveat to FAT on the board's flash. A PC sees the board's flash as a drive only on boards with USB drive support; on a Pico or an ESP32 board, copy files with mpremote.
  • “Can the power fail while the board is writing?” Yes: batteries, field sites, people unplugging it: adds a caveat to FAT on the board's flash and FAT on an SD card. The filesystem docs warn that a power cut during a write can corrupt FAT. Keep files that change often on LittleFS.
  • “Can the power fail while the board is writing?” Yes: batteries, field sites, people unplugging it and “Must a computer read the files straight off the storage?” No, mpremote or the network can fetch them: rules out FAT on the board's flash. With no PC to serve and power that can fail, FAT on the flash adds a corruption risk and gains nothing.

In short: LittleFS for the board’s own flash, which is what both ports format anyway; FAT for an SD card that people carry to a PC, with the files that change often kept on LittleFS; and LittleFS on a card only through a driver with the extended interface.

A Pico formats what it cannot mount

Before you experiment with mkfs or with a second filesystem on the Pico’s flash, copy your files off with mpremote. A Pico that finds no filesystem it can mount at boot formats the flash with LittleFS.

Checkpoint

Check: LittleFS or FAT

  1. Question 1 of 2 A Pico's flash holds a filesystem that LittleFS can no longer mount. What happens at the next boot?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The boot script formats the flash with LittleFS, and every file on it is gone

    The two ports differ here: the Pico starts again with an empty filesystem, while an ESP32 board refuses to format a partition whose first block is not blank. Copy files off before you experiment.

  2. Question 2 of 2 A logger stores readings on an SD card that a technician reads on a laptop each month, and keeps settings that change every day. Which layout fits?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: FAT on the card for the readings; the settings in LittleFS on the board's flash

    Choose by who reads the storage and how often it changes: FAT where a PC must read it, LittleFS for everything the board rewrites, so a power cut can cost a reading but never the settings.

vfs.mount() shows what is mounted, os.statvfs() how full it is

Every path goes through a table of mounts. A filesystem is mounted at a mount point: The folder at which a mounted filesystem appears, / or a name such as /sd. MicroPython sends every path that starts with it, followed by a slash or nothing, to that filesystem., / or a name such as /sd, with vfs.mount(fs, "/sd"), and vfs.umount("/sd") takes it away again. vfs.mount() with no arguments returns the table itself: a list of (filesystem, mount point) pairs, one pair on a fresh Pico, its LittleFS object and /. Follow two paths through a Pico with an SD card mounted at /sd:

Interactive · step by step Two paths through the mounts of a Pico Step through two open() calls on a Pico with LittleFS at / and an SD card at /sd, and see which filesystem and which device each one reaches.
Two paths through the mounts of a Pico, step 9 of 9. Showing Your code, VFS mount table, VfsLfs2 at / (new blocks for cfg.json), VfsFat at /sd, rp2.Flash, sdcard.SDCard. In focus: VFS mount table.a pathevery other path/sd/...offsets and eraseswhole blocksYour codeopen(), os.statvfs()VFS mount table/ and /sdVfsLfs2 at /new blocks for cfg.jsonVfsFat at /sdFATrp2.Flash352 blocks of 4,096 Bsdcard.SDCard512-byte blocks over SPI

The drawing shows the whole scene; its 9 steps are listed below.

Every step as text
  1. A path arrives. The app calls open('/data/cfg.json', 'w'). The call goes first to the VFS, which keeps the table of what is mounted where.
  2. No mount named /data. The VFS compares the path with each mount point. /sd does not match, so the root mount, kept last in the table, takes the whole path.
  3. LittleFS picks the blocks. VfsLfs2 decides which blocks will hold the new file and its directory entry. It writes new data to free blocks and never over data it has committed.
  4. The block device does the work. rp2.Flash erases each block with ioctl(6) before LittleFS programs it with writeblocks() at an offset. The device knows blocks and bytes, never files.
  5. A second path. Next the app calls open('/sd/log.csv', 'a'). The path starts with /sd followed by a slash, so it belongs to the filesystem mounted at /sd.
  6. FAT gets the rest. The VFS cuts the mount point off and hands /log.csv to VfsFat, the FAT filesystem on the card.
  7. Whole blocks to the card. VfsFat reads and writes whole 512-byte blocks through micropython-lib's sdcard driver, the simple interface, with no erase calls.
  8. /sdcard is not /sd. A path such as /sdcard/x does not match /sd, because the character after /sd is not a slash. It goes to LittleFS at / like any other path.
  9. Ask the table. vfs.mount() with no arguments returns both mounts as (filesystem, mount point) pairs, and os.statvfs('/sd') asks the card's filesystem how full it is.

The rule from the last steps is the one to remember: a mount point catches a path only when the path continues with a slash or ends there, and the root mount takes every other path.

vfs.mount() lists the mounts since 1.25

The call without arguments returns the mount table from MicroPython 1.25 on. os.mount() and os.umount() still work, but only for older code: MicroPython 2.0 moves them out of os into vfs, so new code calls vfs already.

How full a filesystem is comes from os.statvfs(path), a tuple of ten numbers, of which the first four matter: the block size twice, the number of blocks and the free blocks. Free bytes are st[0] * st[3]. MicroPython’s LittleFS driver counts the free blocks by walking every block the filesystem uses, sets the inode fields to 0, and puts the longest name, 255 bytes, last. On FAT the block is a cluster. mpremote df prints the same figures from your computer. The next program works out each board’s filesystem from its configuration, then reads a statvfs result:

Filesystem sizes, and a statvfs result in bytes Python · report/fs_report.py
"""How big each board's filesystem is, and what an os.statvfs() tuple says in bytes.

Sizes follow MicroPython 1.29.0: RPI_PICO keeps 1,441,792 bytes of its flash for the
filesystem; an ESP32 board gives it the flash after the last partition of its table.
The LittleFS driver reports block size, block count and free blocks in statvfs.
"""
BLOCK = 4096
MiB = 1024 * 1024


def esp32_vfs(table_end, flash):
    return flash - table_end


boards = [
    ("RPI_PICO", 1441792),
    ("ESP32_GENERIC, 4 MB", esp32_vfs(0x200000, 4 * MiB)),
    ("ESP32_GENERIC, 8 MB", esp32_vfs(0x200000, 8 * MiB)),
    ("ESP32_GENERIC-OTA", esp32_vfs(0x310000, 4 * MiB)),
]
print("%-20s %10s %7s" % ("board", "bytes", "blocks"))
for name, size in boards:
    print("%-20s %10s %7s" % (name, "{:,}".format(size), "{:,}".format(size // BLOCK)))

# Suppose os.statvfs("/") on a Pico returns this:
st = (4096, 4096, 352, 160, 160, 0, 0, 0, 0, 255)
bsize, blocks, bfree = st[0], st[2], st[3]
print()
print("os.statvfs('/') =", st)
print("size {:,} B, used {:,} B, free {:,} B ({} %)".format(
    bsize * blocks, bsize * (blocks - bfree), bsize * bfree, 100 * bfree // blocks))


def blocks_for(nbytes):
    return -(-nbytes // bsize)  # rounded up


# LittleFS writes a new version of a file to free blocks and frees the old
# blocks only after it commits, so a rewrite needs room for the whole new copy,
# and a little more: each block of a file also holds a few pointers.
for size in (614400, 700000):
    need = blocks_for(size)
    print("new copy of {:,} B: needs at least {} blocks, {} free: {}".format(
        size, need, bfree, "fits" if need <= bfree else "no room"))

Output

board                     bytes  blocks
RPI_PICO              1,441,792     352
ESP32_GENERIC, 4 MB   2,097,152     512
ESP32_GENERIC, 8 MB   6,291,456   1,536
ESP32_GENERIC-OTA       983,040     240

os.statvfs('/') = (4096, 4096, 352, 160, 160, 0, 0, 0, 0, 255)
size 1,441,792 B, used 786,432 B, free 655,360 B (45 %)
new copy of 614,400 B: needs at least 150 blocks, 160 free: fits
new copy of 700,000 B: needs at least 171 blocks, 160 free: no room

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fs_report.py

An ESP32_GENERIC board gives its filesystem the flash after the end of its partition table: the firmware ends at 2 MiB, so a 4 MB module gets 2 MiB and an 8 MB module 6 MiB. The OTA firmware’s two application slots end at 0x310000, which leaves 983,040 bytes of a 4 MB module. The last two lines show a cost of copy-on-write. LittleFS writes the new version of a file to free blocks and frees the old blocks only after the commit, so a 700,000-byte rewrite needs at least 171 free blocks at once, however many blocks the version it replaces holds. The exact count is a little higher, because each block of a file also holds a few 4-byte pointers to earlier blocks.

Checking only the growth of a file

Comparing the free space with the difference between the new and the old size says a rewrite of a 690,000-byte file to 700,000 bytes needs 3 blocks. It needs at least 171, because the old copy stays until the new one is committed.

Data Storage Converter Turn the byte counts of statvfs into KiB and MiB.

Checkpoint

Check: Mounts and statvfs

  1. Question 1 of 2 os.statvfs('/') on a Pico returns (4096, 4096, 352, 96, 96, 0, 0, 0, 0, 255). How many bytes are free?

    Type a number.

    A hint

    Free bytes are the block size, the first number, times the free blocks, the fourth.

    Show the answer: Question 1 of 2

    Answer: 393216 bytes

    4,096 × 96 = 393,216 bytes, 384 KiB. The third number, 352, is the size of the filesystem in blocks, so 256 blocks, 1,048,576 bytes, are in use.

  2. Question 2 of 2 An SD card is mounted at /sd, LittleFS at /. Which filesystem receives open('/sdcard/notes.txt', 'w')?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: LittleFS at /, because the character after /sd is not a slash

    MicroPython's VFS compares the path with each mount point and accepts a match only when the path continues with a slash or ends there; everything else goes to the root mount.

A block device in RAM gives tests a filesystem of their own

Code that writes files needs tests a laptop can run: a full filesystem, a missing file, a save cut short. The unix port of MicroPython, the one that runs on your computer, is built with the same LittleFS and FAT drivers as the boards, so a test can make a small filesystem of its own in RAM. It needs a block device class with the three methods, vfs.VfsLfs2.mkfs(dev) to format it, and vfs.mount(vfs.VfsLfs2(dev), "/ram") to mount it. A filesystem of 64 blocks of 512 bytes holds less than 32 KiB, so a test fills it within a few writes and then meets the same OSError with errno 28 that a full board raises.

The program below is such a class, written so that it also runs on CPython, with a wrapper that counts the calls a filesystem makes:

A RAM block device and a call counter Python · ramdisk/ram_bdev.py
"""A block device in RAM, with the extended interface of vfs.AbstractBlockDev.

On the unix port or a board, a filesystem goes onto it with
    vfs.VfsLfs2.mkfs(dev)
    vfs.mount(vfs.VfsLfs2(dev), "/ram")
Here, on CPython, the calls a filesystem makes show what each method does.
"""


class RAMBlockDev:
    def __init__(self, block_size, block_count):
        self.block_size = block_size
        self.data = bytearray(block_size * block_count)

    def readblocks(self, block, buf, offset=0):
        start = block * self.block_size + offset
        buf[:] = self.data[start:start + len(buf)]

    def writeblocks(self, block, buf, offset=None):
        # Without an offset: whole blocks, erased by this method if needed (RAM needs none).
        # With an offset: only these bytes change, after the caller's ioctl(6, block).
        start = block * self.block_size + (offset or 0)
        self.data[start:start + len(buf)] = buf

    def ioctl(self, op, arg):
        if op == 4:  # number of blocks
            return len(self.data) // self.block_size
        if op == 5:  # bytes in a block
            return self.block_size
        if op in (1, 2, 3, 6):  # init, shutdown, sync, erase: nothing to do in RAM
            return 0
        return None


class Counting:
    """Wraps a block device and counts the calls a filesystem makes."""

    def __init__(self, dev):
        self.dev = dev
        self.reads = self.writes = self.erases = 0

    def readblocks(self, block, buf, offset=0):
        self.reads += 1
        return self.dev.readblocks(block, buf, offset)

    def writeblocks(self, block, buf, offset=None):
        self.writes += 1
        return self.dev.writeblocks(block, buf, offset)

    def ioctl(self, op, arg):
        if op == 6:
            self.erases += 1
        return self.dev.ioctl(op, arg)


dev = Counting(RAMBlockDev(512, 64))
count, size = dev.ioctl(4, 0), dev.ioctl(5, 0)
print("device: %d blocks of %d bytes = %d bytes" % (count, size, count * size))

dev.ioctl(6, 3)  # LittleFS erases a block before it programs it
dev.writeblocks(3, b"hello", 16)  # 5 bytes at offset 16 of block 3
buf = bytearray(5)
dev.readblocks(3, buf, 16)
print("block 3, offset 16:", bytes(buf), "at byte", 3 * size + 16)

dev.writeblocks(10, bytes(range(256)) * 2)  # FAT's way: one whole block
block = bytearray(size)
dev.readblocks(10, block)
print("block 10 ends with", block[-3:].hex(" "))
print("calls: %d reads, %d writes, %d erase" % (dev.reads, dev.writes, dev.erases))

Output

device: 64 blocks of 512 bytes = 32768 bytes
block 3, offset 16: b'hello' at byte 1552
block 10 ends with fd fe ff
calls: 2 reads, 2 writes, 1 erase

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ram_bdev.py

Block 3 at offset 16 is byte 1,552 of the device, 3 × 512 + 16: every address on a block device is that one sum. The erase in this class does nothing, which the documentation allows for storage that needs no erase, but it also means the class accepts the write without an erase that the flash program showed going wrong.

Key idea

Give tests a block device that is stricter than the hardware, never kinder: one that follows flash’s rules makes a write without an erase fail on your computer instead of on a board in the field.

Checkpoint

Check: RAM block devices for tests

  1. Question 1 of 2 A RAM block device has blocks of 512 bytes. At which byte of its bytearray does writeblocks(5, buf, 100) start?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 2660 bytes

    Block 5 starts at 5 × 512 = 2,560, and the offset adds 100: byte 2,660. Every address on a block device is that one sum.

  2. Question 2 of 2 The lesson's RAMBlockDev answers ioctl(6, n) with 0 and changes nothing. What can a test that uses it miss?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Code that writes over data without erasing it first, which works in RAM and corrupts flash

    A device that is kinder than the hardware hides the hardware's rules. The exercise's RAMFlash programs by AND and erases to 0xff, so a skipped erase shows up as wrong data in the test.

Practise: a RAM block device that behaves like flash

Write RAMFlash, the stricter device: it reads as erased flash when it is new, programs by AND, erases on ioctl(6, n), refuses addresses past its end, and counts every erase of every block, a count the next lesson uses to measure wear. The sample tests run in your browser:

Exercise · Medium · Python

A RAM block device that behaves like flash

Write the class RAMFlash(block_size, block_count) in ramflash.py: a block device with the extended interface of vfs.AbstractBlockDev that keeps its bytes in RAM but follows the rules of NOR flash, so tests catch code that writes without erasing first.

  • A new device reads as erased flash: every byte is 0xff.
  • **ioctl(op, arg):** op 4 returns the number of blocks, op 5 the bytes in a block, op 6 erases block arg (every byte back to 0xff) and returns 0, and ops 1, 2 and 3 return 0. Any other op returns None. Count the erases of each block in the list self.erase_counts, one number per block.
  • **readblocks(block, buf, offset=0):** fills buf with the bytes that start at offset inside block.
  • **writeblocks(block, buf, offset=None):** with an offset, even 0, it programs like flash: each byte becomes the AND of the byte stored and the byte written, and nothing is erased. Without an offset, it erases each block that buf covers, then stores buf as it is; buf is then a whole number of blocks.
  • Out of range: a read or a write that would reach past the last byte changes nothing and returns -22, the negative errno code of EINVAL. A call that succeeds returns 0.

The sample tests run in your browser, on devices of a few blocks.

Starter code · ramflash.py

EINVAL = 22


class RAMFlash:
    """A block device in RAM that follows the rules of NOR flash."""

    def __init__(self, block_size, block_count):
        self.block_size = block_size
        self.block_count = block_count
        self.data = bytearray(block_size * block_count)
        self.erase_counts = [0] * block_count

    def readblocks(self, block, buf, offset=0):
        start = block * self.block_size + offset
        buf[:] = self.data[start:start + len(buf)]
        return 0

    def writeblocks(self, block, buf, offset=None):
        start = block * self.block_size + (offset or 0)
        self.data[start:start + len(buf)] = buf
        return 0

    def ioctl(self, op, arg):
        if op == 4:
            return self.block_count
        if op == 5:
            return self.block_size
        return None
The sample tests · test_ramflash.py
from ramflash import RAMFlash


def test_new_device_is_erased():
    """a new device reads 0xff in every byte"""
    dev = RAMFlash(16, 4)
    buf = bytearray(64)
    assert dev.readblocks(0, buf) == 0
    assert buf == b"\xff" * 64


def test_sizes():
    """ioctl 4 and 5 give the block count and the block size"""
    dev = RAMFlash(512, 8)
    assert dev.ioctl(4, 0) == 8
    assert dev.ioctl(5, 0) == 512
    assert dev.ioctl(1, 0) == 0 and dev.ioctl(3, 0) == 0


def test_offset_write_lands_inside_its_block():
    """a write at an offset changes exactly those bytes of that block"""
    dev = RAMFlash(16, 4)
    assert dev.writeblocks(2, b"\x12\x34", 5) == 0
    whole = bytearray(64)
    dev.readblocks(0, whole)
    assert whole[37:39] == b"\x12\x34"
    assert whole.count(0xFF) == 62


def test_programming_only_clears_bits():
    """programming over old data gives the AND of the old and the new bytes"""
    dev = RAMFlash(16, 2)
    dev.writeblocks(0, b"21.5", 0)
    dev.writeblocks(0, b"19.0", 0)
    buf = bytearray(4)
    dev.readblocks(0, buf, 0)
    assert bytes(buf) == b"01.0"


def test_erase_sets_ones_and_counts():
    """ioctl 6 erases one block to 0xff and counts the erase"""
    dev = RAMFlash(16, 3)
    dev.writeblocks(1, b"\x00" * 16, 0)
    assert dev.ioctl(6, 1) == 0
    buf = bytearray(16)
    dev.readblocks(1, buf)
    assert buf == b"\xff" * 16
    assert dev.erase_counts == [0, 1, 0]
    assert dev.readblocks(3, bytearray(16)) == -22  # the device is still 3 blocks long


def test_whole_block_write_erases_first():
    """without an offset, the blocks are erased, then written"""
    dev = RAMFlash(8, 4)
    dev.writeblocks(1, b"\x0f" * 8, 0)
    assert dev.writeblocks(1, b"\xf0" * 16) == 0
    buf = bytearray(16)
    dev.readblocks(1, buf)
    assert buf == b"\xf0" * 16
    assert dev.erase_counts == [0, 1, 1, 0]


def test_out_of_range_changes_nothing():
    """a write past the end returns -22 and leaves the bytes as they were"""
    dev = RAMFlash(16, 2)
    assert dev.writeblocks(1, b"\x00" * 8, 12) == -22
    assert dev.writeblocks(2, b"\x00" * 16) == -22
    buf = bytearray(4)
    assert dev.readblocks(1, buf, 14) == -22
    whole = bytearray(32)
    dev.readblocks(0, whole)
    assert whole == b"\xff" * 32


def test_last_bytes_can_be_written():
    """a write that ends exactly at the last byte is in range"""
    dev = RAMFlash(16, 2)
    assert dev.writeblocks(1, b"\x00" * 4, 12) == 0
    buf = bytearray(4)
    assert dev.readblocks(1, buf, 12) == 0
    assert buf == b"\x00" * 4
A hint

Keep one bytearray(block_size * block_count) filled with 0xff and turn every call into a byte address: start = block * block_size + offset. Check start + len(buf) <= len(self.data) before anything else. Programming is a loop over the bytes of buf with self.data[start + i] &= b; erasing block n is a slice assignment of b"\xff" * block_size.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then check yourself on flash and block devices, the two filesystems, mounts and statvfs, and RAM devices for tests. The questions start with the idea you have answered worst so far, and each answer is explained:

Practice that adapts to you

Practise this lesson

10 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 What does erasing a sector of the Pico's flash do to its bytes?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: Every bit becomes 1, so every byte reads 0xff

    Erasing works on a whole sector of 4,096 bytes and leaves every bit set, so the sector reads as 0xff throughout. Programming can then clear bits wherever data goes.

  2. Question 2 of 10 A sector holds 100 bytes of data, then erased bytes. Which change cannot be made without erasing the sector?

    Choose one answer.

    A hint

    Write both characters in binary and look for a bit that has to go from 0 to 1.

    Show the answer: Question 2 of 10

    Answer: Byte 10 from 0x35, the character 5, to 0x37, the character 7

    A change that needs any bit to go from 0 to 1 needs an erase of the whole sector; a change that only clears bits, or writes into erased bytes, does not. Filesystems do not rely on lucky changes and write new data into erased space instead.

  3. Question 3 of 10 A driver for an external flash chip offers readblocks(n, buf) and writeblocks(n, buf) for whole blocks, and ioctl(4) and ioctl(5). Which filesystem can it hold?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: FAT, which uses the simple interface; LittleFS needs offsets and ioctl(6)

    The simple interface is enough for FAT. For LittleFS the driver needs the extended interface: readblocks and writeblocks with an offset, and ioctl(6, n) to erase.

  4. Question 4 of 10 What does an ESP32 board running MicroPython 1.29.0 do at boot when its vfs partition holds data it cannot mount?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: It prints that the filesystem appears to be corrupted, over and over, and formats nothing

    An ESP32 board keeps a damaged filesystem's data on the flash for a recovery attempt; a Pico formats it. Know which board you are on before you test what a broken filesystem does.

  5. Question 5 of 10 Power fails while a board is writing a file. What does LittleFS leave on the flash?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: The filesystem as it was at its last commit, with the file as it was when last closed or synced

    LittleFS is copy-on-write: new data goes to free blocks and becomes the file only at a commit, on close or sync. A power cut before that leaves the last committed version, which is why both ports use it for the flash.

  6. Question 6 of 10 When is FAT the better choice than LittleFS on a MicroPython device?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: When people take the storage, such as an SD card, to a computer that must read it directly

    FAT buys one thing, that any computer can read it, and pays for it with power-cut and wear risks. Use it where a PC must read the storage, and keep files that change often on LittleFS.

  7. Question 7 of 10 How does code on MicroPython 1.29.0 list the filesystems that are mounted?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: vfs.mount() with no arguments, which returns (filesystem, mount point) pairs

    vfs.mount() without arguments returns a list such as one pair of the LittleFS object and '/' on a fresh Pico. Use vfs rather than os.mount(), which MicroPython 2.0 removes from os.

  8. Question 8 of 10 A Pico has 120 free blocks of 4,096 bytes. Its data file of 600,000 bytes will be rewritten with 640,000 bytes. At least how many free blocks does the rewrite need?

    Type a number.

    A hint

    LittleFS frees the old copy only after the new one is committed.

    Show the answer: Question 8 of 10

    Answer: 157 blocks

    640,000 ÷ 4,096 = 156.25, so the new copy takes at least 157 blocks, and all of them must be free at once, because the old file's 147 blocks are freed only after the commit. With 120 free blocks the rewrite fails, although the file grows by only 10 blocks.

  9. Question 9 of 10 A test on your computer must check what an app does when the filesystem is full. Which set-up gives it a filesystem that fills like a board's?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: The unix port, a RAM block device of a few dozen blocks, VfsLfs2.mkfs on it and vfs.mount at /ram

    With a RAM block device the test chooses the size of the filesystem, so a full filesystem and its OSError with errno 28 arrive after a few writes, on any computer.

  10. Question 10 of 10 Why should a block device for tests program by AND and start out full of 0xff?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: So that code which skips an erase produces wrong data in the test, as it would on flash

    Tests are worth most when they are at least as strict as the hardware. A device that keeps flash's rules catches a write without an erase on your computer.

Interview questions

Warm-up (fresher): What is the difference between LittleFS and FAT on a MicroPython board?

LittleFS was designed for flash. It writes changes to free blocks instead of over old data, commits a file’s changes only when the file is closed or synced, makes rename and remove atomic, and spreads erases over the blocks it allocates, so a power cut leaves it in its last committed state. FAT is the filesystem of SD cards and USB drives, which every computer reads, but MicroPython’s documentation warns that a power failure during a write can corrupt it, and it binds data to fixed places, so it cannot spread wear. The rp2 and esp32 ports format their flash with LittleFS v2. FAT fits an SD card that people read on a PC, with the files that change often kept on LittleFS.

This lesson leads to two longer questions in the module’s bank:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    Your data logger must keep running when the Pico's filesystem fills up, and CI has no boards. How do you test what the logger does on a full filesystem, and what exactly should the tests check?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    An ESP32 data logger gets an SD card for a month of readings, which a technician copies to a laptop. The board also keeps settings and a calibration file. Which filesystem goes where, how is the card mounted, and what do you do about a pulled card or a power cut during a write?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What can programming change in flash, and what needs an erase?

    Programming only turns bits from 1 to 0; only erasing a whole sector, 4,096 bytes on a Pico, sets them back to 1. Writing 19.0 over 21.5 without an erase left 01.0.

  2. What does a block device give a filesystem?

    Numbered blocks of one size, with readblocks(), writeblocks() and ioctl() for the block count, the block size and erasing. On a Pico it is rp2.Flash(), on an ESP32 the partition named vfs.

  3. What does a board do at boot with a filesystem it cannot mount?

    A Pico formats its flash with LittleFS, and every file is gone. An ESP32 prints that the filesystem appears to be corrupted, in a loop, and changes nothing, so the data is still there for a recovery attempt.

  4. LittleFS or FAT: which for a board's flash, and which for an SD card read on a PC?

    LittleFS v2 for flash: it is copy-on-write, renames and removes atomically and survives power cuts. FAT for a card that a PC must read, though a power cut during a write can corrupt it.

  5. How do you list the mounts and work out the free bytes?

    vfs.mount() with no arguments returns (filesystem, mount point) pairs, since 1.25. Free bytes are st[0] * st[3] of st = os.statvfs(path).

  6. When does a mount point such as /sd catch a path?

    Only when the path goes on with a slash or ends there: /sd/log.txt reaches the card, while /sdlog.txt goes to the root mount.

  7. Why does rewriting a 690,000-byte file as 700,000 bytes need far more than 3 free blocks?

    LittleFS writes the whole new copy to free blocks and frees the old one only after the commit, so it needs at least 171 free blocks of 4,096 bytes at once.

  8. Should new code mount filesystems with os.mount()?

    No: call vfs.mount() and vfs.umount(). os keeps the names only for older code, and MicroPython 2.0 moves them out of os.

Key takeaways

  • Flash is erased in whole sectors, 4,096 bytes on the Pico, and programming only clears bits, so a filesystem writes new data into erased space through a block device that reads, programs and erases.
  • The rp2 and esp32 ports format their flash with LittleFS v2, which survives power cuts; FAT is for storage a PC must read, such as an SD card, and a power cut during a write can corrupt it.
  • vfs.mount() lists the mounts, a mount point catches a path only when a slash or the end follows it, and new code uses vfs rather than os.mount().
  • Free bytes are st[0] * st[3] of os.statvfs(), and a rewrite needs free blocks for the whole new copy, because LittleFS frees the old one only after the commit.
  • A RAM block device with flash’s rules gives the unix port a filesystem for tests that is stricter than the board.

Terms in this lesson

block device
An object that stores data in numbered blocks of one size and offers readblocks(), writeblocks() and ioctl() for the block count, the block size and erasing. MicroPython's filesystems sit on one, such as rp2.Flash or an SD card.
copy-on-write
Writing changed data to free blocks and switching to it in one small commit, instead of writing over the old data, so that a power cut leaves the last committed version intact.
FAT
The File Allocation Table filesystem of SD cards and USB drives, which every computer can read. MicroPython's documentation warns that a power failure during a write can corrupt it.
LittleFS
A small filesystem designed for flash: it writes changes to free blocks, commits them in small checked logs and spreads erases over the blocks. The rp2 and esp32 ports of MicroPython format their flash with LittleFS v2.
mount point
The folder at which a mounted filesystem appears, / or a name such as /sd. MicroPython sends every path that starts with it, followed by a slash or nothing, to that filesystem.
sector (of flash)
The smallest part of a flash chip that can be erased at once, 4,096 bytes on the Pico. An erase sets all its bits to 1; programming can then clear bits to 0, and only another erase sets them back.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.