AntOS Filesystem & Drive Letters
Overview
AntOS presents storage to users and applications through a small set of drive letters — A: through H: — each mapped to a specific physical device by physical port, not by attach order. The mapping is fixed by the hardware design: a given drive letter always refers to the same connector on the case, regardless of what's plugged into it, what order USB enumeration completes in, or whether the device is currently present.
Underneath the letters, AntOS uses standard POSIX file APIs on the Linux kernel's FAT/exFAT mounts. The letter layer is a thin translation: B:/foo/bar maps to the volume's Linux mount point (e.g. /mnt/b/foo/bar) and is handed to the standard VFS. The translation is invisible to applications — they see only the drive-letter view.
The system's identity, configuration, files, tags, and metadata live in DBFS — a SQLite-backed database that lives as a file (sys.db) on a dedicated drive. DBFS is described in detail in its own section below.
Drive Letter Assignments
| Letter | Purpose | Physical location | Kind | Notes |
|---|---|---|---|---|
| A: | SD card | CM5 second SD controller (GPIO alt-fn) | Plain FAT/exFAT | Removable user card — native SD, not the boot media |
| B: | Internal drive | HS USB hub port 2 | Plain FAT/exFAT | App data, scratch, expansion |
| C: | Cartridge | USB hub port (cartridge connector) | Plain FAT/exFAT (USB MSC) | Game / personality cartridges |
| D: | DBFS master | HS USB hub port 0 | DBFS (SQLite on FAT) | The system database lives here |
| D!: | DBFS shadow | HS USB hub port 1 | DBFS (SQLite on FAT) | Hidden continuous backup of D: |
| E: | External drive | HS USB hub port 3 (external) | Plain FAT/exFAT | Bulk storage, removable media |
| F: | Asset files | Built-in system store | Read-only | Fonts, icons, sounds, default resources — shipped with AntOS |
| G:, H: | Reserved | — | — | Future expansion / dynamic mounts |
Letters are never re-used for different physical positions. If D: is empty (no stick in port 0), D: simply doesn't exist that boot — the system does not promote a stick from another slot to fill the gap.
The letters are mnemonic. From C: on, the letter hints at the drive's job — Cartridge, DBFS, External, Files — a small nicety that makes a path a little self-describing. A: and B: keep the classic roles: the removable card and the first fixed drive.
Why port-based assignment?
The earlier design assigned letters by attach order (first eligible USB device became B:, second became B!:). In practice this was racy: USB enumeration timing varies boot-to-boot, and the same physical setup could produce different letter assignments across reboots. The accompanying "blank-swap arbitration" logic that tried to recover from wrong assignments was complex and didn't fully solve the problem.
Port-based assignment eliminates the race entirely:
- Each physical USB port has a stable topology identifier (
parent_addr+parent_portfrom the USB host stack) - A fixed table maps port → letter
- Whatever device is in the port gets that letter, full stop
- Empty ports leave their letter unmapped
This means slot identity is part of the system's published interface. A user labelling their machine ("this case slot is the DBFS drive") gets an answer that's correct forever, not "usually, except when enumeration order shifts."
A: — SD Card
A removable SD/microSD card on a native SD interface, not a USB card reader. The CM5 lite has two SD/MMC controllers: it boots from the first, and the second is routed out on the module's GPIO alt functions — that second controller is A:. So A: is a genuine hardware SD host, entirely separate from the boot media: swapping or losing the A: card never touches boot. Mounted as plain FAT/exFAT, treated like any other user storage. Suitable for:
- Bulk file transfer between an Ant64 and a PC
- Application user data that the user explicitly wants to move between machines
- Photo / audio / video import (camera SD cards plug straight in)
A: is not used for system files. Loss of A: never affects boot or system operation.
B: — Internal Drive
A USB Mass Storage device on HS USB hub port 2, plain FAT/exFAT. Inside the case, not user-removable in normal use.
B: exists for:
- Bulk application data that the user shouldn't accidentally remove
- Pre-installed game / demo libraries
- Sample banks for the music sequencer
- Cached downloads, OTA staging
- Whatever doesn't fit in D: (DBFS is great but SQLite-in-a-file isn't ideal for multi-gigabyte streamed media)
B: is unmounted cleanly on detach (when the internal stick is physically removed — uncommon but supported). Loss of B: does not affect system boot.
C: — Cartridge
The Ant64 cartridge slot is — at the storage layer — a USB Mass Storage device hosted inside a cartridge shell: a downstream port of DeMon's USB hub has its D+/D− routed out to the cartridge edge connector, so an inserted cartridge simply enumerates as a USB volume — C:. Mounted as plain FAT/exFAT when a cartridge is inserted, unmounted when it isn't.
Cartridges typically contain:
- A personality bitstream for the FPGA (the cartridge IS the personality)
- A manifest (
manifest.json) describing what the cartridge is and what it provides - Application data — game ROMs, demo assets, music banks, sample libraries
- OSD theming and other personality-specific UI
The full cartridge port and format live in Cartridges — the 30-pin edge connector, the active-cartridge lines (I²C, a 4-lane MIPI display input, the rpiboot flash path), and the port pinout — and Personality Cartridges for the FPGA personalities. From the filesystem layer, C: is just "a removable FAT volume that appears when a cartridge is inserted and disappears when it's pulled."
C: is read-only by convention. Cartridges are immutable artifacts the user owns and shares; writes that need to persist go to D: (DBFS) or E: (bulk storage).
D: — DBFS Master
The system database drive. A FAT-formatted USB stick on HS USB hub port 0 containing a /DBFS/sys.db SQLite file that holds:
- The active filesystem (files are BLOBs in tables, not directory entries)
- File metadata, tags, and arbitrary key-value config
- The script library (
scripts/*.lua) - The videotext page database (
vt_pagestable) - System state, history, telemetry, user preferences
D: is the foundation of "nothing is hidden, everything is queryable." Storage isn't an opaque block of sectors — it's a real database you can query.
Multiple user DBFS files on D:
A single physical D: drive can hold multiple DBFS files side-by-side:
D:/DBFS/sys.db ← default shared environment
D:/DBFS/anthony.db ← Anthony's personal DBFS
D:/DBFS/sarah.db ← Sarah's personal DBFS
The active DBFS is selected at runtime via the dbfs command (the dbfs.lua script) or by tapping a personal NFC card. Switching DBFS warm-restarts the system into the chosen file:
dbfs → show active and list available
dbfs anthony → warm-restart into anthony.db
dbfs sys → return to shared sys.db
A cold boot always returns to sys.db — the override is in RAM only and does not survive power-off. This means the shared family environment is always the default; personal DBFS switches are explicit per-session decisions.
Creating a new user DBFS is a mkdbfs <name> command — creates D:/DBFS/<name>.db with the right schema, ready to be switched into.
D!: — DBFS Shadow
A second USB stick on HS USB hub port 1, holding a continuously-updated backup of D:'s active DBFS file. The shadow is hidden from user-facing APIs — it doesn't show up in dir, ls, the volume iterator's default view, or any UI surface. It exists purely as a recovery mechanism.
How shadowing works
When D: and D!: are both present and both contain /DBFS/sys.db files marked with matching ROLE=MAIN / ROLE=SHADOW pair markers, the system enables continuous shadow backup:
- Every successful write to D:'s
sys.dbis also written to D!:'ssys.db - The shadow lags the master by at most a few seconds
- If the master fails (corrupt, missing, unmountable), the shadow can be promoted to master with no data loss beyond the most recent in-flight transactions
The two databases are paired by a shared PAIR identifier stored in each's meta table. Paired backup runs automatically; unpaired sticks (no matching pair marker) are treated as separate plain drives and shadowing is disabled.
Failure-mode handling
| Scenario | System response |
|---|---|
| D: present, D!: missing | D: works, no shadow, log a warning |
| D: missing, D!: present | D!: promoted to active master, log "RECOVERED_FROM_SHADOW" |
| D: corrupt, D!: present and matching pair | D!: promoted, D: dropped to plain drive for inspection |
| D: and D!: both ROLE=MAIN | "TWO_MASTERS" — both demoted to plain drives, user must dbfs init to resolve |
| D: and D!: present, PAIR mismatch | Backup disabled, both remain mounted as separate DBFS volumes |
These reasons are surfaced via sys_storage_dbfs_reason() for the boot UI to advise the user on what to do.
E: — External Drive
A USB Mass Storage device on the HS USB hub's external port (port 3, the one most accessible to the user). Plain FAT/exFAT, no DBFS layer.
The purpose is large-capacity bulk storage for content that doesn't need DBFS semantics: media files, downloaded games, builds, ROM dumps, large datasets, backups of other AntOS machines. The user can plug in arbitrarily-large drives without affecting the system database, and can unplug them without losing system state.
E: is unmounted cleanly on detach. No system data is ever written to E: by default; only applications that explicitly target E: use it. Being the external, user-accessible port, E: is also where removable media live — a USB floppy, optical drive or Greaseweazle plugs in here (see below).
Removable media
The USB ports are ordinary USB host ports, so beyond memory sticks they accept removable-media devices. Several matter for a retro machine, each doing a different job.
USB floppy drives
A standard USB floppy drive is just a USB Mass Storage device, so it mounts at its port's drive letter like any stick — a 1.44 MB (or 720 KB) PC-format FAT disk appears as a FAT volume and is read and written through the normal drive-letter APIs. The only twist over a fixed stick is media change: the disk is removable inside the drive, so AntOS watches for a disk swap (the drive reports it) and remounts on the new disk, the way A: handles an SD-card swap.
That is the convenient path for PC-format floppies. What it cannot do is read a disk that isn't PC-MFM — an Amiga, Commodore, Atari or Spectrum disk is invisible to a plain USB floppy drive, whose controller only understands the one encoding. For those you go to the flux level.
Greaseweazle — real floppies as a drive letter
A Greaseweazle is an open-source USB floppy adaptor that works at the flux level — it captures the raw magnetic transitions off the disk rather than relying on a fixed controller, so it can read (and write) essentially any floppy format ever shipped: Amiga ADF, PC IMG, Commodore D64 / G64, Apple II .woz, Atari, Spectrum +3 DSK / TRD, and flux containers such as SCP / HFE.
Plugged into the external E: port it does not stay a bare serial gadget — AntOS presents it as a drive. Its driver reads the inserted disk's flux, auto-detects the format, decodes it to a filesystem, and mounts it at the port's letter, so a real MFM Amiga or GCR Apple disk simply appears under E: and you browse its files like any other drive. Swap the physical disk and AntOS re-reads and remounts, the way A: handles an SD-card swap; an empty drive shows no media. Writes are committed back to the physical disk for formats that can be written safely, and the disk is read-only otherwise. For everyday use, then, a real floppy is just a drive letter — no tooling, no image-hunting.
A lower-level flux API sits underneath for everything the auto-mounter can't or shouldn't hide:
- Read what won't decode — an unknown, damaged or copy-protected disk that can't be turned into a clean filesystem — at the raw flux level.
- Choose the format explicitly, overriding auto-detect.
- Archive at the flux level (
.scp/.hfe): a faithful preservation copy, weak bits and protection included, rather than a decoded image. - Image — write a disk image (
.adf,.img,.d64, …) onto a blank floppy, or dump a real one to an image onD:/E:. - Feed a core — hand a decoded or flux image straight to the matching personality / emulation core to boot from.
It is a scriptable AntOS API (a floppy library over the Greaseweazle protocol, running on DeMon — the standard host tooling ports cleanly now that DeMon is a Linux compute module). The mounted drive and the flux API are the same device at two levels: convenient on top, complete underneath — so an original disk is simply E:, and the one that needs flux is a call away.
USB optical drives — CD / DVD / Blu-ray
A USB CD, DVD or Blu-ray drive is USB Mass Storage like any other, so a disc mounts at the port's letter — E: — when one is present: a data disc (ISO 9660 / UDF) appears as a read-only volume you browse like a folder, swapping the disc remounts the way a floppy or SD swap does, and an empty tray shows no media.
For a retro machine this is the optical counterpart to the Greaseweazle's floppies — real CD/DVD/BD media read directly:
- Data discs — load software, assets or backups straight off the original disc, or hand it to a personality / emulation core that wants a real one (a CD-based console core reading its actual game disc).
- Audio CDs — CDDA tracks are exposed for playback and ripping, so an audio CD plays or rips into the music tools.
- Writers — a CD/DVD/BD writer also burns: data discs, audio CDs, or a disc image — for making and archiving media, not only reading it.
As with the other removable devices, the mounted-disc view is the everyday one; lower-level access (raw sectors, sub-channel, CDDA extraction) is a scriptable API over the drive.
F: — Asset Files
A read-only, built-in store of system assets that ship with AntOS — fonts, icons, system sounds, default wallpapers, boot and UI resources. It is part of the system image rather than a pluggable device, so it is always present: applications and the OS can rely on F:/… paths existing on every machine, every boot. Read-only by design — user content goes to the writable drives (B:, E:, D:), never here. Being built-in, F: has no USB port and is not part of the port topology below.
Port Topology Reference
The USB Mass Storage ports map to letters as follows (all on DeMon's HS USB hub):
HS USB hub port 0 → D: (DBFS master)
HS USB hub port 1 → D!: (DBFS shadow, hidden)
HS USB hub port 2 → B: (internal)
HS USB hub port 3 → E: (external / removable media)
HS USB hub port 4 → C: (cartridge connector)
(A: is the SD card slot, on the CM's second SDIO controller — not the USB hub.)
Port identity comes from the USB topology the Linux host stack exposes — the physical bus-and-port path a device sits on (its /sys/bus/usb/… location), which is stable across reboots and across enumeration-order variations. What matters is which physical connector the device is plugged into, not what order it was detected.
A device plugged into a port with no assigned letter (currently G:, H: — reserved for future expansion) is logged but not mounted. The system never auto-promotes an "extra" stick to D:, B:, or E: if its slot is empty.
Boot Sequence
- Power-on / reset — the CM5 boots Linux, AntOS starts, the USB host stack initialises
- SD card check — A: mounts immediately if a card is present (no enumeration needed)
- USB enumeration — the storage sticks and the cartridge attach asynchronously over the next 1-2 seconds; each is assigned its letter from its port identifier (
C:is now a USB device on the hub, so it comes up here rather than as a separate immediate mount) - DBFS pairing — once both D: and D!: have settled (or after a short timeout),
sys_storagearbitrates pairing and decides whether shadow backup is enabled - DBFS startup — when D: appears with a valid
sys.db,db_start()is called (via the B-ready callback — to be renamed to D-ready) and the system database comes online - Late attachments — sticks plugged in after boot follow the same port-to-letter rules; no order dependency
If the master stick is plugged in late (user plugs it in after boot), D: simply appears when it appears. No re-arbitration needed — the port determines the role.
Hot-Plug Behaviour
All drive letters except A: (the SD card slot, on the SDIO bus) support hot plug/unplug at any time — and C:, now a USB device, hot-plugs like any other cartridge insert/remove. The system reacts:
- Detach — letter immediately unmaps, open file handles return errors on next access
- Re-attach — letter remaps, applications can resume
For D: specifically, detach during a write transaction is handled by SQLite's WAL mode: the master gets unmounted mid-flight, the shadow (if present and recent) is promoted automatically, and the system continues running. The user sees a brief "switching to shadow" log line but nothing visible breaks.
Detaching D!: while D: is active simply disables shadow backup until D!: returns.
First-Boot UX
A factory-fresh Ant64 has empty USB sticks in ports 0 and 1. On first boot, AntOS detects no DBFS on either, and surfaces a guided first-boot setup:
- Confirm with the user that the stick in port 0 is the intended DBFS master
- Create
/DBFS/sys.dbwith the canonical schema, ROLE=MAIN marker, and a fresh PAIR id - If port 1 has a stick: confirm intent and create matching shadow with ROLE=SHADOW and the same PAIR
- Run initial import: bake the built-in script library and videotext content into the new DBFS
- Boot is complete
If either intended slot is empty (or has a non-DBFS stick), the user is asked to plug in / format the right slot. The system never silently reformats user data.
This first-boot flow can be re-invoked manually via dbfs init if a user wants to start fresh on a new pair of sticks.
Drive Letter Lookup APIs
From C code:
const char* sys_storage_path_for_letter(char letter);
char sys_storage_letter_for_path(const char* path);
int sys_storage_get_volume(char letter, sys_storage_volume_t* out);
void sys_storage_iterate(sys_storage_iter_cb cb, void* user);
From Luau:
os.list_drives() -- {"A", "B", "C", "D", "E"}
os.drive_info("D") -- {kind="DBFS", path="/mnt/d", total=..., free=...}
os.path_for("D:/foo") -- "/mnt/d/foo"
Standard POSIX paths starting with a drive letter and colon (e.g. B:/games/asteroids.ant) work transparently through sys_fs — applications don't need to call the lookup explicitly.
Implementation Status
The drive-letter scheme above is canonical: DBFS lives on D: (master) with D!: as its shadow, assigned by physical port. An earlier attach-order prototype put DBFS on B: / B!:; that scheme is superseded, and any reference to DBFS on B: is out of date — DBFS lives on D:, and the plain bulk drives are B: (internal) and E: (external). Remaining build-out:
- Done (on the ESP32-P4 prototype): boot detection of USB topology by bus/port path; on the CM5 the same identity comes from the Linux USB stack (
/sys/bus/usb) - In progress: port-stability validation across reboots; the port→letter table in
sys_storage.cthat finalisesD:/D!:assignment - Pending:
mkdbfs <name>command for creating user DBFS files - Pending: first-boot setup UX
Related Documents
- AntOS — the operating system this filesystem layer serves
- DeMon — the CM5 (Raspberry Pi Compute Module) supervisor hosting AntOS and the USB host stack
- Cartridges — the cartridge port behind
C:: USB storage, active-cartridge lines, and therpibootflash path - Memory Architecture — the broader system memory map including this filesystem's place in it
- AntOS Text Architecture — the text rendering subsystem that displays filesystem content
- NFC — how
dbfs://URIs trigger DBFS switching from a tapped card