Implement USB mass storage and batch OHCI bulk transfers

This commit is contained in:
Aaron committed 2026-10-31 20:36:52 +00:00
1 parent c0c56a9c84
commit c28b9dd77f
7 files changed
+914 -141

No files matched your search

@@ -1,49 +1,68 @@
# driver.storage.ctl.usb # USB mass storage
USB Mass Storage Class driver stub. `driver.storage.ctl.usb` implements SCSI transparent commands over USB Mass
Storage Bulk-Only Transport (interface class/subclass/protocol `08/06/50`).
## Overview It probes INQUIRY, TEST UNIT READY and READ CAPACITY(10), registers LUN 0 with
the storage manager, and exposes READ(10), WRITE(10), REQUEST SENSE and
SYNCHRONIZE CACHE(10). Existing partition discovery and filesystem mounting
apply to the resulting USB disk. Writes complete only after cache synchronization.
This unit registers a USB class driver with the driver manager (`driver.mgr`) to claim USB Mass Storage Class interfaces (class `$08`, subclass `$06` SCSI, protocol `$50` Bulk-Only). When the USB stack enumerates a matching interface, the `load` callback is invoked. Currently the callback only logs a detection message; no I/O infrastructure is established. ## Execution and ownership
Full Bulk-Only Transport (BOT) support — including Max LUN query, endpoint setup, CBW/CSW command exchange, and `driver.storage.mgr` device registration — is planned but not yet implemented. An attachment task probes and registers each disk after scheduling starts; boot
initialization never waits on a worker that cannot run yet.
## Boot Registration Storage dispatch only publishes a request. One persistent `usb.storage` worker
performs I/O in task context, with one command active per device and a reusable,
page-aligned 4 KiB bounce buffer. Caller buffers are never submitted for DMA.
Commands are split at the bounce-buffer boundary; all LBA/count and buffer-end
arithmetic is checked before submission. CBW/CSW wire sizes, tags, status,
residue, and actual transfer lengths are validated.
Registered with `boot.mgr` as `driver.storage.ctl.usb` at the `device` barrier. The barrier order places `device` after `bus`, ensuring `driver.bus.usb.core.init` has run before the storage class driver registers with the driver manager. OHCI batches bulk data into spans of at most 4 KiB per transfer descriptor,
using the endpoint's hardware toggle carry across packets. A 4 KiB full-speed
read uses one data descriptor instead of 64. Short packets still halt the chain
and report the actual byte count; interrupt endpoint packet sizing is unchanged.
## Dependencies The worker performs reset recovery (class reset, clear bulk-IN halt, clear
bulk-OUT halt) after malformed status or phase errors. Failed SCSI commands
request sense and return storage errors, including write protection.
- `boot.mgr` On disconnect the disk is marked removed immediately. Accepted requests are
completed with errors, and idle volumes are invalidated/unmounted by the worker;
busy volumes are retried. Storage-manager records remain stable tombstones.
Class-driver buffers and context are freed after pending hardware work drains.
- `driver.mgr` Host controllers currently have no cancellation API. A timed-out transfer is
- `memory.heap` therefore quarantined with its private device snapshot and DMA memory. No further
- `driver.storage.types` commands are issued to that device. If hardware never completes, those allocations
- `io.syslog` remain retained until reboot; freeing them would permit DMA into reused memory.
- `debug.tracer` The timeout uses the PIT's actual frequency and a bounded polling fallback.
- `driver.bus.usb.core`
- `driver.bus.usb.types`
- `core.util`, `arch.x86.util`
## Constants ## Supported scope
| Constant | Value | Description | - LUN 0 on a single-interface device with one bulk-IN and one bulk-OUT endpoint.
|---|---|---| - Direct-access disks with power-of-two sectors from 512 through 4096 bytes.
| `USB_CLASS_MASS_STORAGE` | `$08` | USB interface class for mass storage | - Capacity representable by the existing 32-bit storage-sector API; the
| `USB_SC_SCSI` | `$06` | SCSI transparent command set subclass | READ CAPACITY(16) sentinel is rejected.
| `USB_PROTO_BBB` | `$50` | Bulk-Only (BOT) transport protocol | - OHCI, EHCI and xHCI bulk transfers. OHCI/EHCI report actual data lengths and
terminate bulk reads on short packets.
## Functions and Procedures UAS, additional LUNs, composite/alternate-setting interfaces, UHCI storage,
optical devices, and media replacement without USB reconnect are not supported.
The current xHCI driver lacks endpoint-ring reset support: a halted bulk endpoint
fails the device closed rather than attempting an unsafe ring reuse.
### init ## Validation
```pascal `UnitTest` is called by the boot test suite and is available as `USBMSCTEST`.
procedure init; Tests use a memory-only mock host controller: wire layout, byte order, capacity
``` limits, reads, writes, sense errors, short transfers, status stalls, reset ordering,
and request bounds. They perform no disk I/O.
Constructs a `TDeviceIdentifier` matching any USB device with `bInterfaceClass = $08`, `bInterfaceSubClass = $06`, `bInterfaceProtocol = $50`, and registers the `load` callback with `driver.mgr`. For hardware/VM checks, boot with a disposable USB disk, confirm `USBMSC` discovery
and filesystem mounting, test file read/write/read-back, then unplug while idle
and during I/O. Do not run destructive disk tests against a disk containing data.
## Notes Protocol: [USB-IF Bulk-Only Transport 1.0](https://www.usb.org/sites/default/files/usbmassbulk_10.pdf).
- The `load` callback currently returns true unconditionally after logging a detection message.
- Once full BOT support is implemented, `load` should query the Max LUN, allocate endpoint structures, and call `driver.storage.mgr.register_device` for each logical unit.
+24 -6
View File
@@ -658,6 +658,7 @@ begin
{ Step 2: Create device at address 0 } { Step 2: Create device at address 0 }
dev := PUSBDevice(kalloc(sizeof(TUSBDevice))); dev := PUSBDevice(kalloc(sizeof(TUSBDevice)));
if dev = nil then begin pop_trace; exit; end;
dev^.Address := 0; dev^.Address := 0;
dev^.Speed := speed; dev^.Speed := speed;
dev^.MaxPacket0 := 8; { Default, will be updated from descriptor } dev^.MaxPacket0 := 8; { Default, will be updated from descriptor }
@@ -667,6 +668,8 @@ begin
dev^.ParentPort := parentPort; dev^.ParentPort := parentPort;
dev^.NumEndpoints := 0; dev^.NumEndpoints := 0;
dev^.Configured := false; dev^.Configured := false;
dev^.InterfaceNumber := 0;
dev^.InterfaceCount := 0;
dev^.fnDisconnect := nil; dev^.fnDisconnect := nil;
{ Step 3: Get first 8 bytes of device descriptor to learn MaxPacketSize0 } { Step 3: Get first 8 bytes of device descriptor to learn MaxPacketSize0 }
@@ -730,7 +733,14 @@ begin
end; end;
totalLen := cfgDesc.wTotalLength; totalLen := cfgDesc.wTotalLength;
if totalLen < sizeof(TUSBConfigDescriptor) then begin
kfree(void(dev));
pop_trace;
exit;
end;
dev^.InterfaceCount := cfgDesc.bNumInterfaces;
cfgBuf := Pointer(kalloc(totalLen)); cfgBuf := Pointer(kalloc(totalLen));
if cfgBuf = nil then begin kfree(void(dev)); pop_trace; exit; end;
status := usb_get_descriptor(dev, USB_DESC_CONFIGURATION, 0, cfgBuf, totalLen); status := usb_get_descriptor(dev, USB_DESC_CONFIGURATION, 0, cfgBuf, totalLen);
if status <> tsSuccess then begin if status <> tsSuccess then begin
@@ -744,12 +754,14 @@ begin
{ Step 7: Parse interface and endpoint descriptors } { Step 7: Parse interface and endpoint descriptors }
offset := sizeof(TUSBConfigDescriptor); offset := sizeof(TUSBConfigDescriptor);
dev^.NumEndpoints := 0; dev^.NumEndpoints := 0;
while offset < totalLen do begin while offset + 2 <= totalLen do begin
{ Read descriptor length and type } { Read descriptor length and type }
if PUint8(uint32(cfgBuf) + offset)^ = 0 then break; { Safety: zero-length = corrupt } if PUint8(uint32(cfgBuf) + offset)^ < 2 then break;
if offset + PUint8(uint32(cfgBuf) + offset)^ > totalLen then break;
if PUint8(uint32(cfgBuf) + offset + 1)^ = USB_DESC_ENDPOINT then begin if PUint8(uint32(cfgBuf) + offset + 1)^ = USB_DESC_ENDPOINT then begin
if dev^.NumEndpoints < USB_MAX_ENDPOINTS then begin if (PUint8(uint32(cfgBuf) + offset)^ >= sizeof(TUSBEndpointDescriptor)) and
(dev^.NumEndpoints < USB_MAX_ENDPOINTS) then begin
epDesc := PUSBEndpointDescriptor(uint32(cfgBuf) + offset); epDesc := PUSBEndpointDescriptor(uint32(cfgBuf) + offset);
dev^.Endpoints[dev^.NumEndpoints].Address := usb_ep_number(epDesc^.bEndpointAddress); dev^.Endpoints[dev^.NumEndpoints].Address := usb_ep_number(epDesc^.bEndpointAddress);
dev^.Endpoints[dev^.NumEndpoints].Direction := usb_ep_direction(epDesc^.bEndpointAddress); dev^.Endpoints[dev^.NumEndpoints].Direction := usb_ep_direction(epDesc^.bEndpointAddress);
@@ -780,16 +792,22 @@ begin
{ Find the first interface descriptor for class/subclass/protocol } { Find the first interface descriptor for class/subclass/protocol }
offset := sizeof(TUSBConfigDescriptor); offset := sizeof(TUSBConfigDescriptor);
ifDesc := nil; ifDesc := nil;
while offset < totalLen do begin while offset + 2 <= totalLen do begin
if PUint8(uint32(cfgBuf) + offset)^ = 0 then break; if PUint8(uint32(cfgBuf) + offset)^ < 2 then break;
if PUint8(uint32(cfgBuf) + offset + 1)^ = USB_DESC_INTERFACE then begin if offset + PUint8(uint32(cfgBuf) + offset)^ > totalLen then break;
if (PUint8(uint32(cfgBuf) + offset + 1)^ = USB_DESC_INTERFACE) and
(PUint8(uint32(cfgBuf) + offset)^ >= sizeof(TUSBInterfaceDescriptor)) then begin
ifDesc := PUSBInterfaceDescriptor(uint32(cfgBuf) + offset); ifDesc := PUSBInterfaceDescriptor(uint32(cfgBuf) + offset);
dev^.InterfaceNumber := ifDesc^.bInterfaceNumber;
break; break;
end; end;
offset := offset + PUint8(uint32(cfgBuf) + offset)^; offset := offset + PUint8(uint32(cfgBuf) + offset)^;
end; end;
DevID := PDeviceIdentifier(kalloc(sizeof(TDeviceIdentifier))); DevID := PDeviceIdentifier(kalloc(sizeof(TDeviceIdentifier)));
if DevID = nil then begin
kfree(void(cfgBuf)); kfree(void(dev)); pop_trace; exit;
end;
DevID^.Bus := biUSB; DevID^.Bus := biUSB;
DevID^.id0 := (uint32(devDesc.idVendor) SHL 16) OR devDesc.idProduct; DevID^.id0 := (uint32(devDesc.idVendor) SHL 16) OR devDesc.idProduct;
DevID^.id1 := devDesc.bDeviceClass; DevID^.id1 := devDesc.bDeviceClass;
+26 -7
View File
@@ -963,6 +963,8 @@ end;
{ ========================= Submit Transfer ========================= } { ========================= Submit Transfer ========================= }
procedure ehci_free_qtd_chain(first : PEHCI_qTD); forward;
function ehci_submit_control(hc : PUSBHCDriver; transfer : PUSBTransfer) : boolean; function ehci_submit_control(hc : PUSBHCDriver; transfer : PUSBTransfer) : boolean;
var var
priv : PEHCI_PrivData; priv : PEHCI_PrivData;
@@ -1004,7 +1006,7 @@ begin
{ SETUP qTD } { SETUP qTD }
qtd := ehci_alloc_qtd; qtd := ehci_alloc_qtd;
if qtd = nil then begin pop_trace; exit; end; if qtd = nil then begin ehci_free_qtd_chain(firstqTD); pop_trace; exit; end;
firstqTD := qtd; firstqTD := qtd;
qtd^.Token := ehci_make_qtd_token(EHCI_QTD_PID_SETUP, false, 8, false, 3); qtd^.Token := ehci_make_qtd_token(EHCI_QTD_PID_SETUP, false, 8, false, 3);
ehci_fill_qtd_buffers(qtd, @transfer^.Setup, 8); ehci_fill_qtd_buffers(qtd, @transfer^.Setup, 8);
@@ -1021,7 +1023,7 @@ begin
while remaining > 0 do begin while remaining > 0 do begin
qtd := ehci_alloc_qtd; qtd := ehci_alloc_qtd;
if qtd = nil then begin pop_trace; exit; end; if qtd = nil then begin ehci_free_qtd_chain(firstqTD); pop_trace; exit; end;
if remaining > maxPkt then if remaining > maxPkt then
pktLen := maxPkt pktLen := maxPkt
@@ -1034,6 +1036,7 @@ begin
pid := EHCI_QTD_PID_OUT; pid := EHCI_QTD_PID_OUT;
qtd^.Token := ehci_make_qtd_token(pid, toggle, pktLen, false, 3); qtd^.Token := ehci_make_qtd_token(pid, toggle, pktLen, false, 3);
qtd^.SWPad0 := pktLen; { Original data length, excluding SETUP/status }
ehci_fill_qtd_buffers(qtd, Pointer(uint32(transfer^.Buffer) + offset), pktLen); ehci_fill_qtd_buffers(qtd, Pointer(uint32(transfer^.Buffer) + offset), pktLen);
qtd^.NextqTD := EHCI_QTD_T; qtd^.NextqTD := EHCI_QTD_T;
qtd^.AltNextqTD := EHCI_QTD_T; qtd^.AltNextqTD := EHCI_QTD_T;
@@ -1052,7 +1055,7 @@ begin
{ STATUS qTD (opposite direction, DATA1) } { STATUS qTD (opposite direction, DATA1) }
qtd := ehci_alloc_qtd; qtd := ehci_alloc_qtd;
if qtd = nil then begin pop_trace; exit; end; if qtd = nil then begin ehci_free_qtd_chain(firstqTD); pop_trace; exit; end;
if (transfer^.Setup.bmRequestType AND $80) <> 0 then if (transfer^.Setup.bmRequestType AND $80) <> 0 then
pid := EHCI_QTD_PID_OUT pid := EHCI_QTD_PID_OUT
@@ -1070,7 +1073,7 @@ begin
{ Build QH } { Build QH }
qh := ehci_alloc_qh; qh := ehci_alloc_qh;
if qh = nil then begin pop_trace; exit; end; if qh = nil then begin ehci_free_qtd_chain(firstqTD); pop_trace; exit; end;
qh^.EPChars := ehci_make_qh_epchars(devAddr, 0, speedBits, maxPkt, qh^.EPChars := ehci_make_qh_epchars(devAddr, 0, speedBits, maxPkt,
true, false, isCtrlEP, 4); true, false, isCtrlEP, 4);
@@ -1152,7 +1155,7 @@ begin
repeat repeat
qtd := ehci_alloc_qtd; qtd := ehci_alloc_qtd;
if qtd = nil then begin pop_trace; exit; end; if qtd = nil then begin ehci_free_qtd_chain(firstqTD); pop_trace; exit; end;
if firstqTD = nil then firstqTD := qtd; if firstqTD = nil then firstqTD := qtd;
@@ -1164,6 +1167,7 @@ begin
pktLen := 0; pktLen := 0;
qtd^.Token := ehci_make_qtd_token(pid, toggle, pktLen, false, 3); qtd^.Token := ehci_make_qtd_token(pid, toggle, pktLen, false, 3);
qtd^.SWPad0 := pktLen; { Original data length, excluding SETUP/status }
if pktLen > 0 then if pktLen > 0 then
ehci_fill_qtd_buffers(qtd, Pointer(uint32(transfer^.Buffer) + offset), pktLen); ehci_fill_qtd_buffers(qtd, Pointer(uint32(transfer^.Buffer) + offset), pktLen);
qtd^.NextqTD := EHCI_QTD_T; qtd^.NextqTD := EHCI_QTD_T;
@@ -1200,7 +1204,7 @@ begin
{ Build QH } { Build QH }
qh := ehci_alloc_qh; qh := ehci_alloc_qh;
if qh = nil then begin pop_trace; exit; end; if qh = nil then begin ehci_free_qtd_chain(firstqTD); pop_trace; exit; end;
qh^.EPChars := ehci_make_qh_epchars(devAddr, epNum, speedBits, maxPkt, qh^.EPChars := ehci_make_qh_epchars(devAddr, epNum, speedBits, maxPkt,
true, false, false, 4); true, false, false, 4);
@@ -1297,6 +1301,11 @@ begin
ehci_transfer_done := true; ehci_transfer_done := true;
exit; exit;
end; end;
if (qh^.SWTransfer <> nil) and
(PUSBTransfer(qh^.SWTransfer)^.PipeType = ptBulk) and
(((qtd^.Token SHR 16) AND $7FFF) <> 0) then begin
ehci_transfer_done := true; exit;
end;
qtd := qtd^.SWNext; qtd := qtd^.SWNext;
end; end;
ehci_transfer_done := true; ehci_transfer_done := true;
@@ -1385,6 +1394,8 @@ var
transfer : PUSBTransfer; transfer : PUSBTransfer;
head : PEHCI_QH; head : PEHCI_QH;
listIdx : uint32; listIdx : uint32;
qtd : PEHCI_qTD;
residual : uint32;
begin begin
if (hc = nil) or (hc^.PrivData = nil) then exit; if (hc = nil) or (hc^.PrivData = nil) then exit;
priv := PEHCI_PrivData(hc^.PrivData); priv := PEHCI_PrivData(hc^.PrivData);
@@ -1422,7 +1433,15 @@ begin
if transfer^.Status = tsInProgress then begin if transfer^.Status = tsInProgress then begin
if ehci_transfer_done(qh) then begin if ehci_transfer_done(qh) then begin
transfer^.Status := ehci_transfer_status(qh); transfer^.Status := ehci_transfer_status(qh);
transfer^.ActualLen := transfer^.BufferLen; transfer^.ActualLen := 0;
qtd := qh^.SWFirstqTD;
while qtd <> nil do begin
if (qtd^.Token AND EHCI_QTD_STS_ACTIVE) <> 0 then break;
residual := (qtd^.Token SHR 16) AND $7FFF;
if (qtd^.SWPad0 > 0) and (residual <= qtd^.SWPad0) then
inc(transfer^.ActualLen, qtd^.SWPad0 - residual);
qtd := qtd^.SWNext;
end;
{ Remove QH from schedule and free qTD chain } { Remove QH from schedule and free qTD chain }
if listIdx = 0 then if listIdx = 0 then
ehci_remove_async_qh(priv, qh) ehci_remove_async_qh(priv, qh)
File diff suppressed because it is too large. Load diff
@@ -249,6 +249,8 @@ type
Endpoints : array[0..USB_MAX_ENDPOINTS-1] of TUSBEndpoint; Endpoints : array[0..USB_MAX_ENDPOINTS-1] of TUSBEndpoint;
NumEndpoints : uint8; NumEndpoints : uint8;
Configured : boolean; Configured : boolean;
InterfaceNumber : uint8; { First interface, alternate setting zero }
InterfaceCount : uint8; { Class drivers may reject composite devices }
fnDisconnect : TUSBDisconnectCallback; { Called before device is freed on unplug } fnDisconnect : TUSBDisconnectCallback; { Called before device is freed on unplug }
end; end;
File diff suppressed because it is too large. Load diff
+2
View File
@@ -37,6 +37,7 @@ uses
driver.storage.metatest, driver.storage.metatest,
driver.storage.perftest, driver.storage.perftest,
driver.storage.ctl.ahci, driver.storage.ctl.ahci,
driver.storage.ctl.usb,
core.enc.fnv1a, core.enc.fnv1a,
core.enc.djb2, core.enc.djb2,
core.ds.bloom, core.ds.bloom,
@@ -76,6 +77,7 @@ begin
driver.storage.metatest.UnitTest; driver.storage.metatest.UnitTest;
driver.storage.perftest.UnitTest; driver.storage.perftest.UnitTest;
driver.storage.ctl.ahci.UnitTest; driver.storage.ctl.ahci.UnitTest;
driver.storage.ctl.usb.UnitTest;
core.enc.fnv1a.UnitTest; core.enc.fnv1a.UnitTest;
core.enc.djb2.UnitTest; core.enc.djb2.UnitTest;
core.ds.bloom.UnitTest; core.ds.bloom.UnitTest;