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.
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.
5.1 Claiming xHCI from the firmware
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.
// 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.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.
5.2 Bulk-Only Transport
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.
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
}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.
5.3 Reading FAT32
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.
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);
}
}5.4 Writing files safely
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.
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.
5.5 A file manager
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.
- 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
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 →