PHASE III · SUBSTANCE / MODULE 05 ·

USB & a Filesystem

An OS that can't save anything is a toy. Claim the xHCI USB controller from the firmware, talk to a mass-storage device, and read & write real FAT32 — then put a file manager on top.

Module 05 — walkthrough
module introcoming soon

This is the module where the hardware fights back. USB is genuinely complex, and you're taking a controller the firmware was using and making it yours. The payoff is enormous: persistent storage, real files, and a filesystem your whole OS can use.

Lesson 5.1

5.1 Claiming xHCI from the firmware

the BIOS handoff — take ownership cleanly
5.1 — the BIOS-to-OS handoff
~24 mincoming soon

The firmware's own USB driver is running when you take over. You must ask it to release the controller via the USB Legacy Support capability — set the "OS owned" bit and wait for the firmware to clear its "BIOS owned" bit. Skip this and the firmware's System Management Mode fights you for every register access.

xhci.c — legacy handoffcopy
// USBLEGSUP: bit 24 = HC OS Owned, bit 16 = HC BIOS Owned
UINT32 legsup = xhci_read_ext_cap(USBLEGSUP);
legsup |= (1 << 24);              // claim OS ownership
xhci_write_ext_cap(USBLEGSUP, legsup);

// wait for the firmware to let go (clear bit 16)
while (xhci_read_ext_cap(USBLEGSUP) & (1 << 16))
    cpu_pause();
// controller is now yours. SMM will stop touching it.
The bug that has no tutorial

In ForgeOS, resetting the xHCI controller after the handoff detached the firmware's own storage driver — and the firmware started spamming write errors to a device it no longer owned. If your logs fill with phantom errors after an xHCI reset, this is why. The handoff order matters: claim ownership, then reset, and expect the firmware to be noisy about losing its device.

Lesson 5.2

5.2 Bulk-Only Transport

speaking SCSI over USB
5.2 — reading blocks over BOT
~22 mincoming soon

A USB drive speaks Bulk-Only Transport: you send a Command Block Wrapper (a SCSI command wrapped for USB), the device sends data, then a status wrapper. To read a block you send SCSI READ(10) with a logical block address, and 512 bytes come back on the bulk-in endpoint.

usb_storage.c — read a blockcopy
int usb_read_block(UINT32 lba, void *out) {
    struct cbw cmd = {0};
    cmd.signature   = 0x43425355;      // "USBC"
    cmd.data_len    = 512;
    cmd.flags       = 0x80;             // data IN (device→host)
    cmd.cb[0]       = 0x28;             // SCSI READ(10)
    cmd.cb[2]       = (lba >> 24);         // LBA, big-endian
    cmd.cb[3]       = (lba >> 16);
    cmd.cb[4]       = (lba >> 8);
    cmd.cb[5]       = (lba);
    cmd.cb[8]       = 1;                // one block
    bulk_out(&cmd, sizeof cmd);       // send command
    bulk_in(out, 512);                // receive data
    return read_status();             // receive status
}
The 64 KB DMA boundary

xHCI transfer descriptors can't cross a 64 KB physical boundary. If a single transfer's buffer straddles one, the controller silently corrupts the data — no error, just wrong bytes. ForgeOS hit this as chained TRBs quietly returning garbage. Allocate transfer buffers so they never cross a 64 KB line, or split the transfer at the boundary.

Lesson 5.3

5.3 Reading FAT32

blocks become files
5.3 — parsing FAT32
~24 mincoming soon

FAT32 is beautifully simple. A boot sector tells you the layout; a File Allocation Table is a linked list of clusters; the root directory is a list of 32-byte entries. To read a file: find its directory entry, get its starting cluster, then follow the FAT chain cluster to cluster until the end marker.

fat.c — follow the cluster chaincopy
UINT32 fat_next_cluster(UINT32 cluster) {
    UINT32 fat_offset = cluster * 4;         // 4 bytes per entry
    UINT32 sector = fat_start + (fat_offset / 512);
    UINT8 buf[512];
    usb_read_block(sector, buf);
    UINT32 next = *(UINT32*)&buf[fat_offset % 512] & 0x0FFFFFFF;
    return next;   // >= 0x0FFFFFF8 means end of chain
}

// read a whole file: walk clusters until the end marker
void fat_read_file(UINT32 start, void *out) {
    UINT32 c = start;
    while (c < 0x0FFFFFF8) {
        read_cluster(c, out);
        out += cluster_bytes;
        c = fat_next_cluster(c);
    }
}
Lesson 5.4

5.4 Writing files safely

persistence, without corrupting the disk
5.4 — allocating clusters & writing
~20 mincoming soon

Writing is reading in reverse plus one dangerous step: allocating clusters. You find free entries in the FAT, chain them, write the data, then update the directory entry's size and start cluster. The danger is that a half-finished write corrupts the filesystem — so you write data first, and update the metadata that points to it last.

Metadata last, always

Write the file's data clusters and the FAT chain before you update the directory entry. If power fails midway, you've leaked some clusters (recoverable) instead of pointing a directory entry at garbage (corruption). Order your writes so an interruption is always survivable.

Lesson 5.5

5.5 A file manager

the filesystem, on screen
5.5 — Files app: list, grid, navigate
~18 mincoming soon

Now it's just UI over the filesystem you built. List a directory's entries, draw them as rows or a grid of icons, and let clicks navigate into folders. Everything you built in Module 04 — windows, hit-testing, chrome — carries the file manager for free. This is the moment the modules start compounding.

Module 05 checkpoint
  • The xHCI controller claimed from firmware via the legacy handoff
  • Block reads over Bulk-Only Transport (SCSI READ(10))
  • FAT32 parsing: boot sector, FAT chains, directory entries
  • Safe file writes with metadata updated last
  • A file manager listing and navigating real directories
next module

Module 06 — A Network Stack

Drive the Ethernet NIC directly, then hand-write ARP, IP, ICMP, and TCP. Watch your OS answer a ping and open a socket using nothing but code you wrote.

Get lifetime access — $149 →
or see all plans →