Drivers — storage (ATA PIO / AHCI SATA) and network (NE2000 / E1000)¶
Equinox 0.4 ships four hardware drivers organized in two layers: a
block layer (blk.cpp) that every disk driver plugs into, and a
NIC registry (nic.c) that every network card plugs into. Higher
subsystems (FAT32, Qfs, eggkg / lwIP) never talk to hardware directly.
Storage stack¶

Block layer — kernel/library/drivers/blk.cpp¶
Eight fixed-index slots (holes keep their index, so a PATA disk is
always ata0..ata3 and a SATA disk always lands on slot 4 or higher):
#define BLK_MAX_DRIVES 8 // 0..3 legacy PATA, 4..7 AHCI
#define BLK_PATA_SLOTS 4 // first AHCI slot
#define BLK_SECTOR_SIZE 512
struct blk_desc {
int present, kind; // BLK_PATA / BLK_ATAPI / BLK_AHCI
int lba48; // drive supports 48-bit LBA
uint64_t total_sectors;
char name[16]; char bus[24]; char model[41];
void* ctx; // driver context, handed back on I/O
int (*read)(void* ctx, uint64_t lba, uint32_t n, void* buf);
int (*write)(void* ctx, uint64_t lba, uint32_t n, const void* buf);
};
blk_init() brings the drivers up in order (ata_init() first, so
legacy disks keep slots 0–3; then ahci_init() claims 4+). On a
machine without AHCI, ahci_init() returns immediately and the boot is
identical to a PATA-only one. Consumers: blk_read / blk_write
(bounds-checked, ≤128 sectors per call), mbr_read_partitions (bus
neutral MBR parse), blk_format_size. Qfs -list-disk and
fat32_slot_scan are built on top.
ATA PIO — ata.cpp (PATA, hda–hdd)¶
- Four drives: primary/secondary × master/slave, task-file programmed I/O, polling, no IRQ — deliberately simple and interrupt-safe.
- IDENTIFY DEVICE on init (model string, LBA support, capacity,
LBA48 capability from word 83); ATAPI devices are listed as
BLK_ATAPIbut not mountable. - Reads/writes use LBA28, transparently upgrading to LBA48 when the drive supports it and the LBA exceeds 28 bits.
- FLUSH CACHE after every write so data is durable in the backing file even if QEMU is killed.
- Every transfer runs under
task_sched_lock()— no preemption can split a multi-sector operation, so no other locking is needed.
AHCI SATA — ahci.cpp (hde+)¶
Full AHCI 1.x host controller driver (PCI class 01h/06h):
- Controller: claim BAR5 as the ABAR, set PCI command
MEM | BUSMASTER | IO, setGHC.AE(without it ports sit in legacy IDE mode). - Per port (
CAP.PImask, up to 8 tracked): stop both DMA engines and wait for CR/FR to drop (SeaBIOS may have left the port running against reclaimed buffers), requirePxSSTS.DET == 3, skip ATAPI (PxSIG == 0xEB140101— CD over AHCI is deferred), program PxCLB/PxFB, clear PxIS/PxSERR, start withST|FRE|SPIN_UP|POD. - Command model: one slot (slot 0) — command header + command
table with a PRDT split on 4 KB boundaries (24 entries ≈ 64 KB per
command, 128-sector chunks at most through the block layer), poll
PxCIclearing withPxIS.TFESas an early bail-out, then check PxTFD for ERR/DF. On any failure the port is restarted (dropping ST resets PxCI in hardware) — a failed command can never wedge the controller. - Commands:
IDENTIFY,READ/WRITE DMA+_EXT(LBA48 when the drive reports it),FLUSH CACHE(_EXT)after writes. - Discovered disks are handed to the block layer as
BLK_AHCI(ahci0, …) on slotsBLK_PATA_SLOTS + n.
DMA buffers are static, identity-mapped BSS allocations (command list 1024-byte, FIS receive 256-byte, command table 128-byte aligned), the same assumption the E1000 rings make — no bounce buffers needed at these RAM sizes.
PCI matching — pci.cpp¶
Boot-time enumeration records vendor/device, class/subclass, IRQ line
and BARs per function (lspci prints the table). Drivers match on
class, not on model lists:
| Device | Match | Notes |
|---|---|---|
| PATA IDE | class 01h, subclass 01h |
legacy PIIX in QEMU |
| SATA/AHCI | class 01h, subclass 06h |
QEMU ich9-ahci 8086:2922; BAR5 = ABAR |
Network drivers¶

NIC registry — kernel/net/nic.{c,h}¶
One struct per driver; the stack talks to "the active NIC" through thin wrappers and never names a driver:
struct nic_driver {
const char* name; /* "ne2000-isa", "e1000-pci" */
int (*probe)(void); /* detect + init; 1 = present */
int (*send)(const uint8_t* frame, int len);
int (*recv)(uint8_t* frame, int maxlen);
const uint8_t* (*mac)(void);
uint32_t (*irq_count)(void); /* optional stats */
uint32_t (*rx_overflow)(void); /* optional stats */
};
net_nic_init() probes in registration order; the first driver that
reports present wins — unless system.ecf pins one ([net] driver =
e1000, see CONFIGURATION.md). With no config the
fallback order is ne2000, then e1000. PCI NICs that are recognized
but have no driver are reported honestly at boot instead of being
silently ignored.
Driver model (both drivers share it): probe() maps the device,
enables PCI command bits and brings the rings up; the ISR only ACKs and
drains into a ring — lwIP is never touched from IRQ context;
recv() hands one completed RX descriptor to the stack and returns the
buffer to the NIC; send() waits for the descriptor-done flag before
returning so the stack can release the frame buffer.
NE2000 ISA — ne2000.c¶
- I/O ports
0x300, IRQ 9 — QEMU's default-device ne2k_isa. - 16-bit programmed I/O ring, the classic DP8390 register model.
Intel E1000 — e1000.c / e1000.h¶
- PCI
8086:100E(82540EM) /8086:100F(82545EM) — QEMU-device e1000, MMIO BAR0, INTx on the PCI IRQ line. - MAC from the EEPROM (
EERD) with RAL/RAH fallback. - RX ring: 32 × 2048-byte descriptors (
RCTL.BSIZE=00), replenished Linux-style (RDT= index just consumed). - TX ring: 8 × 16-byte descriptors (EOP|IFCS|RS), DD-flag polled send.
- ICR is read-to-clear; IMS masks only TXDW/RXT0/RXDMT0/LSC.
- Link up via
CTRL.SLU|ASDE;STATUS.LINKreported toifconfig.
QEMU device cheat-sheet¶
| Driver | make target | Manual device flag |
|---|---|---|
| ATA PIO (PATA) | run-disk |
-drive file=…,if=ide,index=0 |
| AHCI (SATA) | run-ahci |
-device ahci,id=ahci -drive …,if=none,id=hd0 -device ide-hd,drive=hd0,bus=ahci.0 |
| NE2000 | run |
-device ne2k_isa,netdev=net0,iobase=0x300,irq=9 |
| E1000 | run-e1000 |
-device e1000,netdev=net0 |
Adding a driver¶
A block device — implement two functions, fill one struct:
#include "header/blk.h"
static int myblk_read(void* ctx, uint64_t lba, uint32_t n, void* buf) { /* ... */ }
static int myblk_write(void* ctx, uint64_t lba, uint32_t n, const void* b) { /* ... */ }
/* during init: */
struct blk_desc* b = blk_at(FREE_SLOT);
b->present = 1; b->kind = BLK_PATA /* or BLK_AHCI-like kind */;
b->total_sectors = my_capacity();
snprintf(b->name, sizeof(b->name), "myblk0");
b->ctx = my_ctx; b->read = myblk_read; b->write = myblk_write;
FAT32, Qfs and the installer pick it up automatically.
A NIC — implement struct nic_driver (see above), register it in
nic.c's table, done: probe, send, recv, mac; lwIP, DHCP, mget,
httpd all work over it without further changes. (E1000 was added
exactly this way.)