Repository navigation
USB
CupidOS P4 — USB 1.1 + 2.0
CupidOS implements a full USB host controller stack supporting UHCI (USB 1.1) and EHCI (USB 2.0)
host controllers. Three class drivers are provided: HID boot protocol (keyboard + mouse), Hub,
and Mass Storage (BBB + SCSI). HID events are merged into the existing PS/2 event queue so the
shell sees a unified input stream. Mass storage registers as a block device (usb0, usb1, …).
- Overview
- Boot Order — Critical
- PCI Layer
- USB Core
- UHCI Driver
- EHCI Driver
- HID Driver
- Hub Driver
- Mass Storage Driver
- Shell Commands
- QEMU Test Invocations
- Known Limits
| Subsystem | File | Notes |
|---|---|---|
| PCI enumeration | kernel/pci.c |
Bus 0, dev 0-31, multi-function via header type bit 7 |
| USB core | kernel/usb.c |
Device model, enumeration FSM, work queue |
| HC vtable | kernel/usb_hc.h |
usb_hc_t interface |
| UHCI driver | kernel/uhci.c |
IO-port MMIO, USB 1.1 |
| EHCI driver | kernel/ehci.c |
MMIO, USB 2.0, companion routing |
| HID class | kernel/usb_hid.c |
Boot protocol keyboard + mouse |
| Hub class | kernel/usb_hub.c |
Hub descriptor, per-port power + reset |
| Mass storage | kernel/usb_msc.c |
BBB + SCSI, block device registration |
┌─────────────────────────────────┐
│ USB Core │
│ usb_device_t[], work queue, │
│ enumeration FSM, usb_control() │
└──────────┬──────────────────────┘
│ usb_hc_t vtable
┌──────────────┼──────────────┐
▼ ▼ │
┌────────┐ ┌────────┐ │
│ UHCI │ │ EHCI │ (companion routing)
│ (1.1) │ │ (2.0) │
└────────┘ └────────┘
│
┌─────────┼──────────────────┐
▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────────┐
│ HID │ │ Hub │ │ MSC │
│(kbd/ │ │class │ │(BBB+SCSI)│
│mouse)│ │ │ │ │
└──────┘ └──────┘ └──────────┘
│ │
▼ ▼
PS/2 event queue block_device_t
(keyboard_inject_scancode (usb0, usb1, ...)
mouse_inject_event)
- Filesystem.md — FAT16 block device layer that MSC devices will eventually mount into
- Swap.md — another block-device consumer; shows how CupidOS handles disk-backed storage
- Architecture.md — ring-0 memory layout, PCI I/O space, MMIO mapping
usb_init()
└─► ehci_init_all() ← MUST be first
└─► uhci_init_all() ← second
Why order matters: EHCI claims all ports on the PCI bus by writing CONFIGFLAG = 1 to its
operational register. After claiming, it inspects each port's PORTSC.LINE_STATUS. Ports with
a low-speed or full-speed device (LS/FS detected by D+/D− line state) have their
PORTSC.PORT_OWNER bit set, releasing them to the companion UHCI controller.
If UHCI initialises first, it starts driving its ports before EHCI has a chance to claim them. This creates a port-ownership race: a high-speed device may be reset in full-speed mode, permanently capping it at USB 1.1 speeds for that boot session. The reversed init order (EHCI → UHCI) is not optional — changing it breaks companion handoff.
Source: kernel/pci.c, kernel/pci.h
pci_init() performs a flat scan of PCI bus 0, devices 0–31. For each device:
- Read vendor/device ID at
(0, dev, 0, 0x00). - Skip if vendor is
0xFFFF(no device). - Read header type at offset
0x0E. - If bit 7 of header type is set → multi-function device; probe all 8 functions.
- Otherwise probe function 0 only.
// Header-type constants
#define PCI_HEADER_TYPE_GENERAL 0x00 // 6 BARs
#define PCI_HEADER_TYPE_BRIDGE 0x01 // 2 BARs
#define PCI_HEADER_TYPE_CARDBUS 0x02 // 0 BARs| Function | Purpose |
|---|---|
pci_init() |
Enumerate bus 0, populate global device table |
pci_find_by_class(class, subclass, prog_if, start_index) |
Iterate devices by class code |
pci_read32(bus, dev, fn, offset) |
Config-space 32-bit read via CF8/CFC |
pci_write32(bus, dev, fn, offset, val) |
Config-space 32-bit write |
pci_enable_bus_master(dev) |
Set bit 2 of Command register |
The PCI Status register has R/WC (read/write-1-to-clear) bits. A naive read-modify-write that ORs Status as well as Command will inadvertently clear error flags. The correct pattern:
uint32_t cmd_status = pci_read32(dev->bus, dev->dev, dev->fn, 0x04);
// Preserve only Command word; zero Status word before writing
cmd_status = (cmd_status & 0x0000FFFF) | PCI_CMD_BUS_MASTER;
pci_write32(dev->bus, dev->dev, dev->fn, 0x04, cmd_status);Masking with 0x0000FFFF before OR ensures the upper 16 bits (Status) are written as zero,
which is the safe value for R/WC bits — zeros do not clear anything.
| Class | Subclass | Prog-IF | Controller type |
|---|---|---|---|
| 0x0C | 0x03 | 0x00 | OHCI |
| 0x0C | 0x03 | 0x10 | UHCI |
| 0x0C | 0x03 | 0x20 | EHCI |
| 0x0C | 0x03 | 0x30 | xHCI |
Source: kernel/usb.c, kernel/usb_hc.h, kernel/usb.h
typedef struct usb_hc {
int (*submit_sync) (struct usb_hc*, usb_transfer_t*);
int (*submit_interrupt) (struct usb_hc*, usb_transfer_t*, usb_callback_t, void*);
int (*port_count) (struct usb_hc*);
int (*port_status) (struct usb_hc*, int port);
int (*port_reset) (struct usb_hc*, int port);
void (*irq_handler) (struct usb_hc*);
void *priv; // driver-private data (uhci_t* / ehci_t*)
} usb_hc_t;Each HC driver fills in this vtable at init time and passes a pointer to usb_register_hc().
typedef struct usb_device {
uint8_t addr; // assigned USB address (1-127)
uint8_t speed; // USB_SPEED_LOW / FULL / HIGH
uint8_t class; // bDeviceClass from descriptor
uint16_t vid, pid; // vendor / product IDs
uint8_t depth; // hub depth (0 = root)
uint8_t tt_hub_addr; // transaction translator hub addr (HS hubs)
uint8_t tt_port; // TT port on that hub
usb_hc_t *hc; // owning HC
void *class_priv; // class driver private state
} usb_device_t;Capacity: 32 devices global. Hub nesting: maximum depth 5.
USB hot-plug events arrive in IRQ context (EHCI port-change interrupt) or from polled callbacks (UHCI 256 ms idle poll). Both paths call into:
void usb_port_change(usb_hc_t *hc, int port);
void usb_hub_port_change(usb_device_t *hub_dev, int port);These functions push a {hc, port} tuple onto a ring buffer without taking a lock. The main
idle loop drains the queue by calling usb_process_pending().
usb_process_pending() runs the following FSM for each pending port change:
port_reset(hc, port)
│
▼
GET_DESCRIPTOR(dev_addr=0, len=8) ← fetch first 8 bytes only (bMaxPacketSize0)
│
▼
SET_ADDRESS(new_addr)
│
▼
GET_DESCRIPTOR(new_addr, len=18) ← full device descriptor
│
▼
GET_CONFIGURATION(len=9) ← config descriptor header (get wTotalLength)
│
▼
GET_CONFIGURATION(len=wTotalLength) ← full config + interface + endpoint descriptors
│
▼
SET_CONFIGURATION(bConfigurationValue)
│
▼
driver probe match ← iterate class drivers, first match wins
int usb_control(usb_device_t *dev,
uint8_t bmRequestType, uint8_t bRequest,
uint16_t wValue, uint16_t wIndex, uint16_t wLength,
void *data);Builds an 8-byte SETUP packet and submits a 3-phase transfer (SETUP → optional DATA → STATUS)
via hc->submit_sync.
Source: kernel/uhci.c
UHCI uses I/O-port registers located at BAR4 (IO BAR, bit 0 set). Base address obtained via
pci_read32(..., 0x20) & ~3.
Key registers (offset from IO base):
| Offset | Name | Purpose |
|---|---|---|
0x00 |
USBCMD | Run/Stop, Host Reset, Global Suspend |
0x02 |
USBSTS | Interrupt status (write 1 to clear) |
0x04 |
USBINTR | Interrupt enable mask |
0x06 |
FRNUM | Current frame number (11-bit) |
0x08 |
FLBASEADD | Frame list base address (physical, 4KB aligned) |
0x0C |
SOFMOD | Start-of-frame timing |
0x10 |
PORTSC0 | Port 0 status/control |
0x12 |
PORTSC1 | Port 1 status/control |
- 1024-entry frame list, each entry is a 32-bit physical pointer to a QH or TD.
- Single skeleton QH (
skel_qh) inserted into every frame list slot. - Transfers use 32-byte Transfer Descriptors (TDs) linked off the skeleton QH.
kmalloc in CupidOS returns pointers at offset +12 from a 16-byte-aligned block header
(the block header consumes 12 bytes). UHCI TDs and QHs require 16-byte alignment per the
UHCI spec.
The driver allocates a raw buffer (skel_qh_raw, td_raw) large enough to hold the structure
plus 15 padding bytes, then aligns the pointer manually:
skel_qh_raw = kmalloc(sizeof(uhci_qh_t) + 15);
skel_qh = (uhci_qh_t*)(((uintptr_t)skel_qh_raw + 15) & ~15);
// At kfree time: kfree(skel_qh_raw) ← NOT skel_qhThe _raw pointer is saved for kfree(); the aligned pointer is used for hardware access.
Some BIOSes take ownership of USB via SMI (System Management Interrupt). CupidOS forces
handoff by writing to the LEGSUP register at PCI config offset 0xC0:
pci_write16(dev->bus, dev->dev, dev->fn, 0xC0, 0x8F00);Bit 13 (0x2000) in 0x8F00 disables the SMI; bits 0x000F clear pending status.
UHCI provides no reliable hot-plug interrupt. The driver relies on the 256 ms idle-loop tick
to call uhci_poll_ports(), which reads PORTSC0/PORTSC1 and calls usb_port_change() on
any port whose CONNECT_STATUS_CHANGE bit is set.
Source: kernel/ehci.c
EHCI registers live at BAR0, a memory BAR (bit 0 clear). On typical PC hardware BAR0 is
above 0xFEB00000 — well beyond the 128 MB identity-map that CupidOS sets up at boot.
The driver calls:
paging_map_mmio(bar0_phys, 4096);to create a 4KB kernel-virtual mapping before any register access.
BAR0 + 0x00 ┌──────────────────────────────┐
│ Capability Registers (RO) │ length = CAPLENGTH
│ CAPLENGTH, HCIVERSION, │
│ HCSPARAMS, HCCPARAMS │
BAR0 + CAPLENGTH
├──────────────────────────────┤
│ Operational Registers (R/W) │
│ USBCMD, USBSTS, USBINTR, │
│ FRINDEX, CTRLDSSEGMENT, │
│ PERIODICLISTBASE, │
│ ASYNCLISTADDR, CONFIGFLAG, │
│ PORTSC[0..N] │
└──────────────────────────────┘
caplength is read from the first byte of BAR0; the operational base is bar0 + caplength.
Before taking ownership, the driver reads the extended capability list pointer from
HCCPARAMS[15:8], then walks the linked list to find a capability with CapID = 0x01
(USBLEGSUP). It sets OS_OWNED = 1 and polls BIOS_OWNED for up to 1 second:
uint32_t leg = ehci_ext_cap_read(base, usblegsup_off);
leg |= (1 << 24); // set OS_OWNED
ehci_ext_cap_write(base, usblegsup_off, leg);
// wait up to 1 s for BIOS_OWNED to clear
for (int i = 0; i < 1000 && (ehci_ext_cap_read(...) & (1<<16)); i++)
pit_sleep_ms(1);
// force clear if BIOS did not release
ehci_ext_cap_write(base, usblegsup_off, leg & ~(1<<16));EHCI QHs (Queue Heads) and qTDs (queue Transfer Descriptors) require 32-byte alignment. Same manual pattern as UHCI:
qh_raw = kmalloc(sizeof(ehci_qh_t) + 31);
qh = (ehci_qh_t*)(((uintptr_t)qh_raw + 31) & ~31);// 1. Set CONFIGFLAG=1 — EHCI claims all ports
op_base->CONFIGFLAG = 1;
// 2. For each port: if LINE_STATUS indicates LS or FS device → release to UHCI
for (int p = 0; p < port_count; p++) {
uint32_t ps = op_base->PORTSC[p];
if (LINE_STATUS(ps) != K_HISPEED) {
op_base->PORTSC[p] = ps | PORT_OWNER; // hand off to companion
}
}A port with PORT_OWNER=1 is invisible to EHCI and fully controlled by the companion UHCI.
High-speed (USB 2.0) bulk and control transfers use the asynchronous schedule:
- Circular doubly-linked list of QHs.
- Dummy head QH (
async_head) withH=1(reclamation list head bit). - New QHs inserted before the dummy head; removed by relinking + door-bell.
Interrupt transfers use the periodic schedule:
- 1024-entry frame list; each slot points to a chain of periodic QHs or iTDs.
- CupidOS uses a fixed 256 ms poll interval for HID and Hub interrupt pipes
(slot index =
frame & 0x3FF, inserting the same QH in every 256th slot).
Source: kernel/usb_hid.c
The HID driver claims any device with bInterfaceClass = 0x03 (HID) and
bInterfaceSubClass = 0x01 (Boot Interface). On probe:
// 1. Switch to boot protocol
usb_control(dev, 0x21, SET_PROTOCOL, 0, interface, 0, NULL);
// 2. Disable idle report — only send on change
usb_control(dev, 0x21, SET_IDLE, 0, interface, 0, NULL);The HID boot keyboard sends an 8-byte report on every interrupt endpoint poll:
Byte 0: Modifier bitmap (Ctrl/Shift/Alt/GUI L/R)
Byte 1: Reserved (0x00)
Byte 2: Keycode 0
Byte 3: Keycode 1
Byte 4: Keycode 2
Byte 5: Keycode 3
Byte 6: Keycode 4
Byte 7: Keycode 5
The driver diffs each new report against the previous to detect key-down and key-up events.
Each new keycode is translated through a ~100-entry HID-usage → PS/2-scancode table and
injected via keyboard_inject_scancode().
Covered keycodes: printable ASCII, modifier keys (Shift/Ctrl/Alt), F1–F10, cursor arrows, Backspace, Enter, Escape, Tab, Delete, Insert, Home, End, Page Up/Down.
The HID boot mouse sends a 3-byte report:
Byte 0: Button bitmap (bit0=left, bit1=right, bit2=middle)
Byte 1: X displacement (signed, relative)
Byte 2: Y displacement (signed, relative)
Injected via mouse_inject_event(dx, dy, buttons), which feeds the existing PS/2 mouse handler.
The shell and GUI window manager receive USB mouse events identically to PS/2 mouse events.
Source: kernel/usb_hub.c
Claimed when bDeviceClass = 0x09 (Hub). On probe:
-
GET_DESCRIPTOR(HUB)→ 7+ byte hub descriptor. - Read
bNbrPorts(number of downstream ports) andbPwrOn2PwrGood(power-on delay in 2ms units). - Call
SET_FEATURE(PORT_POWER)for each port, then waitbPwrOn2PwrGood * 2ms.
The hub's status-change endpoint (interrupt IN, typically endpoint 1) is polled at 256 ms intervals. A set bit N in the returned bitmap means port N has a status change pending.
For each changed port:
1. GET_PORT_STATUS(port) ← read wPortStatus + wPortChange
2. Check C_PORT_CONNECTION ← connection change bit
3. CLEAR_FEATURE(C_PORT_CONNECTION)
4. Debounce 100 ms
5. If device present:
SET_FEATURE(PORT_RESET)
Poll for PORT_RESET to clear
CLEAR_FEATURE(C_PORT_RESET)
Call usb_hub_port_change(this_hub_dev, port) ← triggers enumeration
The maximum hub nesting depth is 5. Before recursing into a newly found hub device, the
driver checks parent_depth + 1 <= 5. Devices discovered beyond depth 5 are silently ignored.
(The USB 2.0 spec allows 7; CupidOS caps at 5 for simplicity.)
When a full-speed or low-speed device is connected behind a high-speed hub, the HS hub's built-in Transaction Translator bridges speed domains. CupidOS propagates TT info to child device descriptors:
child->tt_hub_addr = hub_dev->addr;
child->tt_port = port;EHCI uses these fields to set the PORTSC.SPLIT_EN and TT* fields in the QH for that device.
Source: kernel/usb_msc.c
Every MSC operation consists of three phases:
Host → Device: CBW (31 bytes, OUT bulk endpoint)
Host ↔ Device: DATA (optional, direction from CBW flags)
Device → Host: CSW (13 bytes, IN bulk endpoint)
CBW fields used by CupidOS:
| Field | Value |
|---|---|
| dCBWSignature |
0x43425355 ("USBC") |
| dCBWTag | monotonically increasing tag |
| dCBWDataTransferLength | byte count for DATA phase |
| bmCBWFlags |
0x80 = device→host, 0x00 = host→device |
| bCBWLUN | 0 (single-LUN assumption) |
| bCBWCBLength | length of embedded SCSI command |
| CBWCB | SCSI Command Descriptor Block (CDB) |
CSW bCSWStatus: 0x00 = success, 0x01 = command failed, 0x02 = phase error.
| Command | Opcode | Direction | Use |
|---|---|---|---|
| INQUIRY | 0x12 |
D→H | Device identification |
| TEST_UNIT_READY | 0x00 |
none | Check ready |
| READ_CAPACITY(10) | 0x25 |
D→H | Get LBA count + block size |
| READ(10) | 0x28 |
D→H | Read sectors |
| WRITE(10) | 0x2A |
H→D | Write sectors |
After a successful READ_CAPACITY, the driver calls:
block_device_t bd = {
.name = "usb0", // usb1, usb2, ...
.read_block = usb_msc_read,
.write_block= usb_msc_write,
.block_size = lba_block_size,
.block_count= lba_count,
.priv = msc_state,
};
block_register(&bd);Immediately after registration, the driver reads sector 0 and checks for the MBR signature
0x55AA at bytes 510–511. If found, the four 16-byte partition entries at offsets
0x01BE–0x01FD are parsed:
| Offset | Field | Size |
|---|---|---|
| +0 | Status (0x80 = active) | 1 |
| +4 | Partition type | 1 |
| +8 | LBA start | 4 |
| +12 | LBA count | 4 |
FAT16 partition types detected: 0x04, 0x06, 0x0E.
Not yet wired. The FAT16 VFS implementation (
kernel/fat16.c) is currently a single-instance driver hardcoded to the ATA block device. Mounting a second FAT16 volume would require per-instance state throughout the driver.Current behaviour: USB mass storage devices register as raw block devices (
usb0etc.) and MBR partitions are detected and logged. Direct block reads/writes work. FAT16 filesystem operations on USB drives are not available yet.Planned fix: Refactor
fat16.cto carry all state in a heap-allocated instance struct passed through every VFS call, then wireusb_msc_probe()intofat16_mount().
See Filesystem.md for the current FAT16 architecture.
CupidOS> usb
[0] addr=1 speed=HIGH vid=8086 pid=1234 class=09 parent=root
[1] addr=2 speed=FULL vid=045e pid=0745 class=03 parent=0
[2] addr=3 speed=FULL vid=093a pid=2510 class=03 parent=0
[3] addr=4 speed=FULL vid=0781 pid=5567 class=08 parent=0
Fields: device index, assigned USB address, speed (LOW/FULL/HIGH), vendor ID, product ID, class code, parent device (root = root hub, number = hub device index).
CupidOS> usb hubs
depth=0 addr=1 ports=4 (root hub via EHCI)
depth=1 addr=2 port=1 speed=FULL (HID keyboard)
depth=1 addr=3 port=2 speed=FULL (HID mouse)
depth=1 addr=4 port=3 speed=FULL (mass storage)
CupidOS> usb hc
EHCI: BAR0=0xfebf0000 ports=4 BIOS_handoff=OK
UHCI: IO=0xc080 ports=2 legacy_SMI=disabled
Displays the host controller summary captured during usb_init(). Useful for verifying that
EHCI initialised before UHCI and that BIOS handoff completed.
qemu-system-i386 \
-drive if=ide,format=raw,file=cupidos.img \
-device piix3-usb-uhci \
-device usb-ehci \
-device usb-kbd \
-device usb-mouse \
-serial stdio# Create a test FAT16 image
dd if=/dev/zero of=test.img bs=1M count=64
mkfs.fat -F 16 test.img
qemu-system-i386 \
-drive if=ide,format=raw,file=cupidos.img \
-device piix3-usb-uhci \
-device usb-ehci \
-drive if=none,id=ustick,file=test.img,format=raw \
-device usb-storage,drive=ustick \
-serial stdioqemu-system-i386 \
-drive if=ide,format=raw,file=cupidos.img \
-device piix3-usb-uhci \
-device usb-ehci \
-device usb-kbd \
-device usb-mouse \
-drive if=none,id=ustick,file=test.img,format=raw \
-device usb-storage,drive=ustick \
-serial stdioNote on OHCI: QEMU's
-device piix3-usb-uhciemulates a UHCI controller. OHCI (used on VIA/SiS real hardware) is not emulated by the above flags and is not supported by CupidOS.
| Limitation | Details |
|---|---|
| No OHCI | VIA/SiS chipsets use OHCI. QEMU emulates UHCI; real OHCI hardware not supported. |
| No xHCI | USB 3.x is out of scope for the P4 milestone. |
| No isochronous transfers | Audio, webcam, video capture not supported. |
| BBB only (no UAS) | USB Attached SCSI (UAS) is a USB 3.0 feature; not implemented. |
| Flat PCI bus 0 scan | PCI bridges not traversed. Devices behind bridges are invisible. |
| No power management | No USB suspend/resume, no remote wakeup, no selective suspend. |
| Boot protocol only (HID) | No HID report descriptor parser. Devices that do not support boot protocol not usable. |
| 32-device global limit |
USB_MAX_DEVICES = 32. Large hubs with many attached devices may hit this. |
| Hub depth 5 | USB 2.0 spec allows 7 levels; CupidOS enforces 5. |
| EHCI+UHCI companion timing | Full-speed devices on a system with both EHCI and UHCI may occasionally fail enumeration due to companion handoff timing. UHCI-only configurations are reliable. |
| FAT16 auto-mount not wired | USB mass storage registers as raw block device only. File-level access requires the FAT16 refactor described in Mass Storage Driver. |
kernel/
├── pci.h — PCI device struct, BAR helpers, class codes
├── pci.c — pci_init, pci_find_by_class, pci_enable_bus_master
├── usb_hc.h — usb_hc_t vtable, usb_transfer_t
├── usb.h — usb_device_t, speed/class constants, work queue API
├── usb.c — USB core: register_hc, enumeration FSM, usb_control
├── uhci.c — UHCI 1.1 host controller driver
├── ehci.c — EHCI 2.0 host controller driver
├── usb_hid.c — HID boot protocol: keyboard + mouse class driver
├── usb_hub.c — Hub class driver
└── usb_msc.c — Mass storage BBB + SCSI + block_device registration
Getting Started
Shell & Scripting
Compilers
- CupidC Compiler
- CupidC Language Reference
- CupidC 2D Graphics Library
- CupidASM Assembler
- Toolchain Bootstrap
- Floating Point
Programs & Docs
GUI
Kernel
Filesystems
Hardware