Skip to content

Floppy Disk Controller (FDC)

The floppy disk is no longer relevant hardware in practice, but its controller remains one of the first storage drivers many hobby kernels write specifically because it’s simple enough to implement without modern DMA or a complex command stack: unlike ATA PIO and AHCI, the two storage drivers actually worth targeting on real hardware today, the FDC exists on this wiki purely for its didactic value.

The primary floppy controller sits at a fixed base I/O port, 0x3F0, with a handful of registers reached at fixed offsets from it: the Main Status Register (0x3F4) reports whether the controller is ready for a command byte or a result byte, and whether it’s currently busy; the Data Register (0x3F5) is where command bytes are written and status/result bytes are read back, both through the same port.

#define FDC_MSR 0x3F4 // Main Status Register (read)
#define FDC_DATA 0x3F5 // Data Register (read/write)
#define FDC_DOR 0x3F2 // Digital Output Register (write)
#define MSR_RQM 0x80 // Request For Master: FDC ready for data transfer
#define MSR_DIO 0x40 // Data I/O direction: 1 = FDC->CPU, 0 = CPU->FDC

The Digital Output Register (0x3F2) controls the physical drive itself, separate from the controller’s own command interface: a bit to spin the selected drive’s motor on or off, a bit to select which of up to four drives subsequent commands target, and, critically, a reset bit that has to be pulsed to bring the controller out of reset before any command will be accepted at all.

Every FDC command follows the same pattern: poll MSR until RQM is set and DIO reads 0 (ready to accept a command byte), write the command and its parameter bytes one at a time to the data register, then, for commands that produce a result, poll MSR again for RQM set with DIO reading 1 and read back each result byte the same way.

void fdc_send_byte(uint8_t byte) {
for (int timeout = 0; timeout < 1000; timeout++) {
if ((inb(FDC_MSR) & 0xC0) == 0x80) { // RQM=1, DIO=0
outb(FDC_DATA, byte);
return;
}
}
}
uint8_t fdc_read_byte(void) {
for (int timeout = 0; timeout < 1000; timeout++) {
if ((inb(FDC_MSR) & 0xC0) == 0xC0) { // RQM=1, DIO=1
return inb(FDC_DATA);
}
}
return 0xFF;
}

The Sense Interrupt Status command has to be issued after every command that generates an interrupt, including the reset itself, before the controller will accept a further command; skipping it is a common source of a controller that appears to hang after its very first successful operation, since the FDC is specifically waiting for that acknowledgment rather than any new command.

A floppy read moves data through the system’s legacy ISA DMA controller, channel 2, entirely separate from the DMA engine a modern PCIe device like AHCI uses for its own transfers: the driver programs channel 2 with the destination buffer’s physical address and transfer length before issuing the FDC’s own Read Data command, and the two controllers, DMA and FDC, coordinate the actual transfer between themselves once both are set up.

void fdc_setup_dma_read(uint32_t phys_addr, uint16_t length) {
outb(0x0A, 0x06); // mask DMA channel 2
outb(0x0C, 0xFF); // clear flip-flop
outb(0x04, phys_addr & 0xFF);
outb(0x04, (phys_addr >> 8) & 0xFF);
outb(0x81, (phys_addr >> 16) & 0xFF); // page register for channel 2
outb(0x05, (length - 1) & 0xFF);
outb(0x05, ((length - 1) >> 8) & 0xFF);
outb(0x0B, 0x46); // single transfer, read mode, channel 2
outb(0x0A, 0x02); // unmask channel 2
}

Sectors are read one at a time, specified by cylinder, head, and sector number (CHS addressing, the same legacy scheme Partition Tables already covers for the MBR itself) rather than a flat LBA the way modern drives address storage, since the floppy predates LBA addressing existing at all as a standard.

A floppy image requires no partition table, no filesystem beyond a plain FAT12 volume at most, and no PCI enumeration at all, since the controller sits at a fixed, non-discoverable legacy I/O address rather than needing to be located; a kernel’s very first storage driver can be tested against a raw floppy image under QEMU (-fda disk.img) with essentially no setup beyond the controller commands themselves. This is specifically what makes it a reasonable teaching vehicle despite its complete lack of practical relevance: the command/status handshake, the reset sequence, and the legacy DMA setup all transfer conceptually to more relevant hardware, without the additional complexity PCI discovery and modern DMA add on top.

The 64 KB legacy ISA DMA channel cannot address a transfer that crosses a 64 KB physical boundary, and unlike modern scatter-gather DMA, has no way to split a transfer automatically when this happens: a buffer allocated without checking for this straddles the boundary silently and corrupts part of the transfer, which is why floppy DMA buffers are conventionally allocated from a pool guaranteed not to cross that boundary rather than from an arbitrary physical address. The FDC’s motor has a real spin-up delay (commonly several hundred milliseconds) before the drive is actually ready to read, and a command issued before that delay elapses fails or returns garbage rather than waiting automatically, so a driver has to track motor state itself and insert an explicit delay after turning it on rather than assuming the hardware handles that timing on its own.

  1. ^ Intel, 8272A/8272 Floppy Disk Controller Datasheet: the original command and register reference the 82077AA and later controllers remain compatible with.
  • ATA/IDE in PIO Mode: a more practically relevant storage driver sharing this article’s polling-based, register-level approach.
  • AHCI: the storage interface actually relevant on modern hardware.