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.
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 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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.
Checkpoint
Check: Flash sectors and block devices
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:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| 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
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:
The drawing shows the whole scene; its 9 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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.
- FAT gets the rest. The VFS cuts the mount point off and hands /log.csv to VfsFat, the FAT filesystem on the card.
- 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.
- /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.
- 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:
"""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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.
Checkpoint
Check: Mounts and statvfs
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 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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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
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 blockarg(every byte back to0xff) and returns 0, and ops 1, 2 and 3 return 0. Any other op returnsNone. Count the erases of each block in the listself.erase_counts, one number per block. - **
readblocks(block, buf, offset=0):** fillsbufwith the bytes that start atoffsetinsideblock. - **
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 thatbufcovers, then storesbufas it is;bufis 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 ofEINVAL. A call that succeeds returns0.
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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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.
-
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?
-
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?
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
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.
-
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.
-
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.
-
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.
-
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).
-
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.
-
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.
-
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 usesvfsrather thanos.mount().- Free bytes are
st[0] * st[3]ofos.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
- Working with filesystems (MicroPython project)
- vfs – virtual filesystem control (MicroPython project)
- os – basic “operating system” services (statvfs, filesystem mounting) (MicroPython project)
- MicroPython 2.0 Migration Guide (MicroPython project)
- MicroPython v1.25.0 release notes (vfs.mount() with no arguments) (MicroPython project)
- MicroPython remote control: mpremote (df) (MicroPython project)
- littlefs README (power-loss resilience, wear leveling) (littlefs project)
- The design of littlefs (DESIGN.md) (littlefs project)
- pico-sdk hardware_flash: flash_range_erase and flash_range_program (Raspberry Pi)
- pico-sdk board definition for the Pico (boards/pico.h: flash size) (Raspberry Pi)
- RPI_PICO board configuration (ports/rp2/boards/RPI_PICO/mpconfigboard.cmake, v1.29.0) (MicroPython project)
- The rp2.Flash block device (ports/rp2/rp2_flash.c, v1.29.0) (MicroPython project)
- The rp2 port's boot script (ports/rp2/modules/_boot.py, v1.29.0) (MicroPython project)
- The esp32 port's boot script (ports/esp32/modules/_boot.py, v1.29.0) (MicroPython project)
- The esp32 port's first-start setup (ports/esp32/modules/inisetup.py, v1.29.0) (MicroPython project)
- The esp32 port's start-up and its vfs partition (ports/esp32/main.c, v1.29.0) (MicroPython project)
- ESP32 partition table (ports/esp32/partitions-4MiBplus.csv, v1.29.0) (MicroPython project)
- ESP32 OTA partition table (ports/esp32/partitions-4MiB-ota.csv, v1.29.0) (MicroPython project)
- The VFS mount table and path lookup (extmod/vfs.c, v1.29.0) (MicroPython project)
- The LittleFS driver's statvfs (extmod/vfs_lfsx.c, v1.29.0) (MicroPython project)
- The FAT driver's statvfs (extmod/vfs_fat.c, v1.29.0) (MicroPython project)
- micropython-lib SD card driver (micropython/drivers/storage/sdcard) (MicroPython project)
- class SDCard – secure digital memory card (MicroPython project)
- The unix port's build options (ports/unix/mpconfigport.mk, v1.29.0) (MicroPython project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress