From c28b9dd77f58f91ad56144c3cebd9234bce66716 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 31 Oct 2026 20:36:52 +0000 Subject: [PATCH] Implement USB mass storage and batch OHCI bulk transfers --- .../storage/ctl/driver.storage.ctl.usb.md | 85 +- src/driver/bus/usb/driver.bus.usb.core.pas | 30 +- src/driver/bus/usb/driver.bus.usb.ehci.pas | 33 +- src/driver/bus/usb/driver.bus.usb.ohci.pas | 116 ++- src/driver/bus/usb/driver.bus.usb.types.pas | 2 + .../storage/ctl/driver.storage.ctl.usb.pas | 797 ++++++++++++++++-- src/test/test.pas | 2 + 7 files changed, 919 insertions(+), 146 deletions(-) diff --git a/doc/src/driver/storage/ctl/driver.storage.ctl.usb.md b/doc/src/driver/storage/ctl/driver.storage.ctl.usb.md index a9a95ab9..7ff06d8b 100644 --- a/doc/src/driver/storage/ctl/driver.storage.ctl.usb.md +++ b/doc/src/driver/storage/ctl/driver.storage.ctl.usb.md @@ -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` -- `memory.heap` -- `driver.storage.types` -- `io.syslog` -- `debug.tracer` -- `driver.bus.usb.core` -- `driver.bus.usb.types` -- `core.util`, `arch.x86.util` +Host controllers currently have no cancellation API. A timed-out transfer is +therefore quarantined with its private device snapshot and DMA memory. No further +commands are issued to that device. If hardware never completes, those allocations +remain retained until reboot; freeing them would permit DMA into reused memory. +The timeout uses the PIT's actual frequency and a bounded polling fallback. -## Constants +## Supported scope -| Constant | Value | Description | -|---|---|---| -| `USB_CLASS_MASS_STORAGE` | `$08` | USB interface class for mass storage | -| `USB_SC_SCSI` | `$06` | SCSI transparent command set subclass | -| `USB_PROTO_BBB` | `$50` | Bulk-Only (BOT) transport protocol | +- 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. +- Capacity representable by the existing 32-bit storage-sector API; the + READ CAPACITY(16) sentinel is rejected. +- 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 -procedure init; -``` +`UnitTest` is called by the boot test suite and is available as `USBMSCTEST`. +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 - -- 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. +Protocol: [USB-IF Bulk-Only Transport 1.0](https://www.usb.org/sites/default/files/usbmassbulk_10.pdf). diff --git a/src/driver/bus/usb/driver.bus.usb.core.pas b/src/driver/bus/usb/driver.bus.usb.core.pas index 1ce08a85..afebffd1 100644 --- a/src/driver/bus/usb/driver.bus.usb.core.pas +++ b/src/driver/bus/usb/driver.bus.usb.core.pas @@ -658,6 +658,7 @@ begin { Step 2: Create device at address 0 } dev := PUSBDevice(kalloc(sizeof(TUSBDevice))); + if dev = nil then begin pop_trace; exit; end; dev^.Address := 0; dev^.Speed := speed; dev^.MaxPacket0 := 8; { Default, will be updated from descriptor } @@ -667,6 +668,8 @@ begin dev^.ParentPort := parentPort; dev^.NumEndpoints := 0; dev^.Configured := false; + dev^.InterfaceNumber := 0; + dev^.InterfaceCount := 0; dev^.fnDisconnect := nil; { Step 3: Get first 8 bytes of device descriptor to learn MaxPacketSize0 } @@ -730,7 +733,14 @@ begin end; totalLen := cfgDesc.wTotalLength; + if totalLen < sizeof(TUSBConfigDescriptor) then begin + kfree(void(dev)); + pop_trace; + exit; + end; + dev^.InterfaceCount := cfgDesc.bNumInterfaces; 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); if status <> tsSuccess then begin @@ -744,12 +754,14 @@ begin { Step 7: Parse interface and endpoint descriptors } offset := sizeof(TUSBConfigDescriptor); dev^.NumEndpoints := 0; - while offset < totalLen do begin + while offset + 2 <= totalLen do begin { 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 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); dev^.Endpoints[dev^.NumEndpoints].Address := usb_ep_number(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 } offset := sizeof(TUSBConfigDescriptor); ifDesc := nil; - while offset < totalLen do begin - if PUint8(uint32(cfgBuf) + offset)^ = 0 then break; - if PUint8(uint32(cfgBuf) + offset + 1)^ = USB_DESC_INTERFACE then begin + while offset + 2 <= totalLen do begin + 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_INTERFACE) and + (PUint8(uint32(cfgBuf) + offset)^ >= sizeof(TUSBInterfaceDescriptor)) then begin ifDesc := PUSBInterfaceDescriptor(uint32(cfgBuf) + offset); + dev^.InterfaceNumber := ifDesc^.bInterfaceNumber; break; end; offset := offset + PUint8(uint32(cfgBuf) + offset)^; end; DevID := PDeviceIdentifier(kalloc(sizeof(TDeviceIdentifier))); + if DevID = nil then begin + kfree(void(cfgBuf)); kfree(void(dev)); pop_trace; exit; + end; DevID^.Bus := biUSB; DevID^.id0 := (uint32(devDesc.idVendor) SHL 16) OR devDesc.idProduct; DevID^.id1 := devDesc.bDeviceClass; diff --git a/src/driver/bus/usb/driver.bus.usb.ehci.pas b/src/driver/bus/usb/driver.bus.usb.ehci.pas index b78652a8..56b1a53d 100644 --- a/src/driver/bus/usb/driver.bus.usb.ehci.pas +++ b/src/driver/bus/usb/driver.bus.usb.ehci.pas @@ -963,6 +963,8 @@ end; { ========================= Submit Transfer ========================= } +procedure ehci_free_qtd_chain(first : PEHCI_qTD); forward; + function ehci_submit_control(hc : PUSBHCDriver; transfer : PUSBTransfer) : boolean; var priv : PEHCI_PrivData; @@ -1004,7 +1006,7 @@ begin { SETUP 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; qtd^.Token := ehci_make_qtd_token(EHCI_QTD_PID_SETUP, false, 8, false, 3); ehci_fill_qtd_buffers(qtd, @transfer^.Setup, 8); @@ -1021,7 +1023,7 @@ begin while remaining > 0 do begin 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 pktLen := maxPkt @@ -1034,6 +1036,7 @@ begin pid := EHCI_QTD_PID_OUT; 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); qtd^.NextqTD := EHCI_QTD_T; qtd^.AltNextqTD := EHCI_QTD_T; @@ -1052,7 +1055,7 @@ begin { STATUS qTD (opposite direction, DATA1) } 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 pid := EHCI_QTD_PID_OUT @@ -1070,7 +1073,7 @@ begin { Build 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, true, false, isCtrlEP, 4); @@ -1152,7 +1155,7 @@ begin repeat 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; @@ -1164,6 +1167,7 @@ begin pktLen := 0; qtd^.Token := ehci_make_qtd_token(pid, toggle, pktLen, false, 3); + qtd^.SWPad0 := pktLen; { Original data length, excluding SETUP/status } if pktLen > 0 then ehci_fill_qtd_buffers(qtd, Pointer(uint32(transfer^.Buffer) + offset), pktLen); qtd^.NextqTD := EHCI_QTD_T; @@ -1200,7 +1204,7 @@ begin { Build 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, true, false, false, 4); @@ -1297,6 +1301,11 @@ begin ehci_transfer_done := true; exit; 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; end; ehci_transfer_done := true; @@ -1385,6 +1394,8 @@ var transfer : PUSBTransfer; head : PEHCI_QH; listIdx : uint32; + qtd : PEHCI_qTD; + residual : uint32; begin if (hc = nil) or (hc^.PrivData = nil) then exit; priv := PEHCI_PrivData(hc^.PrivData); @@ -1422,7 +1433,15 @@ begin if transfer^.Status = tsInProgress then begin if ehci_transfer_done(qh) then begin 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 } if listIdx = 0 then ehci_remove_async_qh(priv, qh) diff --git a/src/driver/bus/usb/driver.bus.usb.ohci.pas b/src/driver/bus/usb/driver.bus.usb.ohci.pas index b40e30b3..642954a0 100644 --- a/src/driver/bus/usb/driver.bus.usb.ohci.pas +++ b/src/driver/bus/usb/driver.bus.usb.ohci.pas @@ -678,6 +678,8 @@ begin end; { Build a control transfer: SETUP TD, optional DATA TDs, STATUS TD, all on a new ED } +procedure ohci_free_td_chain(firstTD : POHCI_TD); forward; + function ohci_submit_control(hc : PUSBHCDriver; transfer : PUSBTransfer) : boolean; var priv : POHCI_PrivData; @@ -713,7 +715,7 @@ begin { === SETUP TD === } td := ohci_alloc_td; - if td = nil then begin pop_trace; exit; end; + if td = nil then begin ohci_free_td_chain(firstTD); pop_trace; exit; end; firstTD := td; td^.Control := ohci_make_td_control(OHCI_TD_DP_SETUP, OHCI_TD_DT_DATA0, @@ -732,7 +734,7 @@ begin while remaining > 0 do begin td := ohci_alloc_td; - if td = nil then begin pop_trace; exit; end; + if td = nil then begin ohci_free_td_chain(firstTD); pop_trace; exit; end; if remaining > maxPkt then pktLen := maxPkt @@ -752,6 +754,7 @@ begin td^.BE := vtop(uint32(transfer^.Buffer) + offset + uint32(pktLen) - 1) else td^.BE := 0; + td^.SWPad := pktLen; { Original data length, excluding SETUP/status } td^.NextTD := 0; td^.SWTransfer := Pointer(transfer); td^.SWNext := nil; @@ -773,7 +776,7 @@ begin { === STATUS TD === } td := ohci_alloc_td; - if td = nil then begin pop_trace; exit; end; + if td = nil then begin ohci_free_td_chain(firstTD); pop_trace; exit; end; { Status stage direction is opposite of data stage } if (transfer^.Setup.bmRequestType AND $80) <> 0 then @@ -794,13 +797,13 @@ begin { Allocate a dummy/sentinel TD for the tail pointer (driver.bus.usb.ohci requires HeadP <> TailP to process) } tailTD := ohci_alloc_td; - if tailTD = nil then begin pop_trace; exit; end; + if tailTD = nil then begin ohci_free_td_chain(firstTD); pop_trace; exit; end; td^.NextTD := vtop(uint32(tailTD)); td^.SWNext := tailTD; { === Create ED === } ed := ohci_alloc_ed; - if ed = nil then begin pop_trace; exit; end; + if ed = nil then begin ohci_free_td_chain(firstTD); pop_trace; exit; end; ed^.Control := ohci_make_ed_control(devAddr, 0, OHCI_ED_DIR_FROM_TD, speedBit, maxPkt, false); @@ -825,6 +828,18 @@ begin pop_trace; end; +{ A general TD covers at most two physical pages. A 4 KiB span is safe + even for an unaligned buffer; intermediate TDs must end on packet boundaries. } +function ohci_async_length(remaining, maxPkt : uint32; bulk : boolean) : uint32; +var limit : uint32; +begin + limit := maxPkt; + if bulk and (maxPkt > 0) and (maxPkt <= 4096) then + limit := 4096 - (4096 mod maxPkt); + if remaining < limit then limit := remaining; + ohci_async_length := limit; +end; + { Build TDs for an interrupt or bulk transfer on a new ED } function ohci_submit_async(hc : PUSBHCDriver; transfer : PUSBTransfer) : boolean; var @@ -882,24 +897,25 @@ begin { Build TD chain } repeat td := ohci_alloc_td; - if td = nil then begin pop_trace; exit; end; + if td = nil then begin ohci_free_td_chain(firstTD); pop_trace; exit; end; if firstTD = nil then firstTD := td; - if remaining > maxPkt then - pktLen := maxPkt - else if remaining > 0 then - pktLen := remaining - else - pktLen := 0; + pktLen := ohci_async_length(remaining, maxPkt, transfer^.PipeType = ptBulk); + + { Hardware advances the carry after every packet in a bulk TD. } + if transfer^.PipeType = ptBulk then toggle := OHCI_TD_DT_FROM_ED; td^.Control := ohci_make_td_control(dirPID, toggle, - OHCI_TD_DI_NONE, true); - td^.CBP := vtop(uint32(transfer^.Buffer) + offset); - if pktLen > 0 then + OHCI_TD_DI_NONE, transfer^.PipeType <> ptBulk); + if pktLen > 0 then begin + td^.CBP := vtop(uint32(transfer^.Buffer) + offset); td^.BE := vtop(uint32(transfer^.Buffer) + offset + uint32(pktLen) - 1) - else + end else begin + td^.CBP := 0; td^.BE := 0; + end; + td^.SWPad := pktLen; { Original data length } td^.NextTD := 0; td^.SWTransfer := Pointer(transfer); td^.SWNext := nil; @@ -916,13 +932,8 @@ begin else toggle := OHCI_TD_DT_DATA1; - if remaining > maxPkt then begin - offset := offset + maxPkt; - remaining := remaining - maxPkt; - end else begin - offset := offset + remaining; - remaining := 0; - end; + inc(offset, pktLen); + dec(remaining, pktLen); until remaining = 0; { Mark last TD with immediate interrupt } @@ -930,24 +941,28 @@ begin prevTD^.Control := (prevTD^.Control AND (NOT OHCI_TD_DI_MASK)) OR OHCI_TD_DI_IMMEDIATE; { Update endpoint toggle } - if toggle = OHCI_TD_DT_DATA1 then - transfer^.Endpoint^.Toggle := 1 - else - transfer^.Endpoint^.Toggle := 0; + if transfer^.PipeType <> ptBulk then begin + if toggle = OHCI_TD_DT_DATA1 then + transfer^.Endpoint^.Toggle := 1 + else + transfer^.Endpoint^.Toggle := 0; + end; { Allocate tail sentinel TD } tailTD := ohci_alloc_td; - if tailTD = nil then begin pop_trace; exit; end; + if tailTD = nil then begin ohci_free_td_chain(firstTD); pop_trace; exit; end; prevTD^.NextTD := vtop(uint32(tailTD)); prevTD^.SWNext := tailTD; { Create ED } ed := ohci_alloc_ed; - if ed = nil then begin pop_trace; exit; end; + if ed = nil then begin ohci_free_td_chain(firstTD); pop_trace; exit; end; ed^.Control := ohci_make_ed_control(devAddr, epNum, edDir, speedBit, maxPkt, false); ed^.HeadP := vtop(uint32(firstTD)); + if transfer^.PipeType = ptBulk then + ed^.HeadP := ed^.HeadP or ((uint32(transfer^.Endpoint^.Toggle) and 1) shl 1); ed^.TailP := vtop(uint32(tailTD)); ed^.NextED := 0; ed^.SWTransfer := Pointer(transfer); @@ -1078,6 +1093,22 @@ begin io.syslog.logln('driver.bus.usb.ohci', 'Hardware interrupts enabled.'); end; +{ Data bytes completed by one TD. SWPad excludes SETUP and status bytes. } +function ohci_td_data_length(td : POHCI_TD) : uint32; +var residual : uint32; cc : uint8; +begin + ohci_td_data_length := 0; + cc := ohci_td_cc(td); + if (td^.SWPad = 0) or ((cc <> OHCI_CC_NO_ERROR) and (cc <> OHCI_CC_DATA_UNDERRUN)) then exit; + residual := 0; + if td^.CBP <> 0 then begin + residual := (td^.BE AND $FFF) + 1; + if (td^.CBP AND $FFFFF000) <> (td^.BE AND $FFFFF000) then inc(residual, 4096); + residual := residual - (td^.CBP AND $FFF); + end; + if residual <= td^.SWPad then ohci_td_data_length := td^.SWPad - residual; +end; + procedure ohci_poll(hc : PUSBHCDriver); var priv : POHCI_PrivData; @@ -1133,31 +1164,40 @@ begin if (ed^.HeadP AND $FFFFFFF0) = (ed^.TailP AND $FFFFFFF0) then begin { All TDs consumed — walk chain via virtual SWNext to check CCs } transfer^.Status := tsSuccess; - transfer^.ActualLen := transfer^.BufferLen; + transfer^.ActualLen := 0; td := POHCI_TD(ed^.SWPad1); { Virtual firstTD } while td <> nil do begin cc := ohci_td_cc(td); + inc(transfer^.ActualLen, ohci_td_data_length(td)); if (cc <> OHCI_CC_NO_ERROR) and (cc <> OHCI_CC_NOT_ACCESSED) then begin transfer^.Status := ohci_cc_to_status(cc); break; end; td := td^.SWNext; end; + if transfer^.PipeType = ptBulk then + transfer^.Endpoint^.Toggle := (ed^.HeadP shr 1) and 1; + transfer^.HCPriv := nil; ohci_remove_ed(sentinel, ed); ohci_free_td_chain(POHCI_TD(ed^.SWPad1)); ohci_free_ed(ed); end else if (ed^.HeadP AND $01) <> 0 then begin { ED is halted — walk virtual chain to find the error CC } transfer^.Status := tsStall; + transfer^.ActualLen := 0; td := POHCI_TD(ed^.SWPad1); { Virtual firstTD } while td <> nil do begin cc := ohci_td_cc(td); + inc(transfer^.ActualLen, ohci_td_data_length(td)); if (cc <> OHCI_CC_NO_ERROR) and (cc <> OHCI_CC_NOT_ACCESSED) then begin transfer^.Status := ohci_cc_to_status(cc); break; end; td := td^.SWNext; end; + if transfer^.PipeType = ptBulk then + transfer^.Endpoint^.Toggle := (ed^.HeadP shr 1) and 1; + transfer^.HCPriv := nil; ohci_remove_ed(sentinel, ed); ohci_free_td_chain(POHCI_TD(ed^.SWPad1)); ohci_free_ed(ed); @@ -1413,6 +1453,12 @@ begin Assert((tdCtrl AND OHCI_TD_DP_MASK) = OHCI_TD_DP_OUT, 'td dp=OUT'); Assert((tdCtrl AND OHCI_TD_DT_MASK) = OHCI_TD_DT_FROM_ED, 'td dt=FROM_ED'); + Assert(ohci_async_length(4096,64,true)=4096, 'bulk page uses one TD, not 64'); + Assert(ohci_async_length(8192,64,true)=4096, 'bulk TD bounded to two pages'); + Assert(ohci_async_length(31,64,true)=31, 'bulk CBW retains short length'); + Assert(ohci_async_length(0,64,true)=0, 'bulk zero length packet'); + Assert(ohci_async_length(4096,64,false)=64, 'interrupt packet sizing unchanged'); + { === Condition Code Mapping === } Assert(ohci_cc_to_status(OHCI_CC_NO_ERROR) = tsSuccess, 'cc NO_ERROR->tsSuccess'); Assert(ohci_cc_to_status(OHCI_CC_CRC) = tsCRCError, 'cc CRC->tsCRCError'); @@ -1441,6 +1487,16 @@ begin Assert(td^.CBP = 0, 'alloc_td zeroed cbp'); Assert(td^.NextTD = 0, 'alloc_td zeroed nexttd'); Assert(td^.BE = 0, 'alloc_td zeroed be'); + td^.SWPad := 4096; + td^.Control := OHCI_CC_NO_ERROR shl OHCI_TD_CC_SHIFT; + Assert(ohci_td_data_length(td)=4096, 'multi-packet TD complete length'); + td^.Control := OHCI_CC_DATA_UNDERRUN shl OHCI_TD_CC_SHIFT; + td^.CBP := $1200; td^.BE := $1FFF; + Assert(ohci_td_data_length(td)=512, 'multi-packet TD short read length'); + td^.CBP := $1F00; td^.BE := $8EFF; + Assert(ohci_td_data_length(td)=0, 'two noncontiguous pages residual'); + td^.CBP := $8100; + Assert(ohci_td_data_length(td)=512, 'short read across physical page boundary'); ohci_free_td(td); { === HCCA Size / Alignment === } diff --git a/src/driver/bus/usb/driver.bus.usb.types.pas b/src/driver/bus/usb/driver.bus.usb.types.pas index 3d6a8441..984421b3 100644 --- a/src/driver/bus/usb/driver.bus.usb.types.pas +++ b/src/driver/bus/usb/driver.bus.usb.types.pas @@ -249,6 +249,8 @@ type Endpoints : array[0..USB_MAX_ENDPOINTS-1] of TUSBEndpoint; NumEndpoints : uint8; 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 } end; diff --git a/src/driver/storage/ctl/driver.storage.ctl.usb.pas b/src/driver/storage/ctl/driver.storage.ctl.usb.pas index b7d694f5..5c9654f9 100644 --- a/src/driver/storage/ctl/driver.storage.ctl.usb.pas +++ b/src/driver/storage/ctl/driver.storage.ctl.usb.pas @@ -1,92 +1,749 @@ -// Copyright 2021 Aaron Hance -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -{ - Driver->Bus->driver.bus.usb->USB_Storage - driver.bus.usb Mass Storage Class Driver (stub). - - Registers with driver.mgr as a driver.bus.usb class driver matching - mass storage devices (class $08, subclass $06 SCSI, protocol $50 BBB). - Currently a skeleton that recognises the device but performs no I/O — - full Bulk-Only Transport support is planned. - - @author(Aaron Hance ) -} +{ USB Mass Storage: SCSI transparent commands over Bulk-Only Transport. + LUN 0, one interface, READ/WRITE(10), 512..4096 byte sectors. + A persistent worker owns all storage I/O. No caller buffer is ever DMA-visible. + Protocol reference: https://www.usb.org/sites/default/files/usbmassbulk_10.pdf } unit driver.storage.ctl.usb; interface - -uses - boot.mgr, - driver.mgr, - memory.heap, - driver.storage.types, - io.syslog, - debug.tracer, - driver.bus.usb.core, - driver.bus.usb.types, - core.util, arch.x86.util; - procedure init; +procedure UnitTest; implementation -{ ========================= Constants ========================= } +uses boot.mgr, driver.mgr, memory.heap, driver.storage.types, + driver.storage.mgr, driver.bus.usb.core, driver.bus.usb.types, + proc.mgr, proc.types, arch.x86.bda, arch.x86.isr.tmr0, + core.util, arch.x86.util, io.syslog, io.stdio, core.strings, + driver.storage.vfs, driver.storage.vol.mgr, core.ds.lists; const - USB_CLASS_MASS_STORAGE = $08; - USB_SC_SCSI = $06; { SCSI transparent command set } - USB_PROTO_BBB = $50; { Bulk-Only (BBB) transport } + CBW_SIGNATURE = $43425355; + CSW_SIGNATURE = $53425355; + BUFFER_SIZE = 4096; + TRANSFER_TICKS = (PIT_INPUT_HZ div PIT_DIVISOR) * 5; -{ ========================= Class driver callback ========================= } +type + TCBW = packed record + Signature, Tag, DataLength : uint32; + Flags, Lun, CommandLength : uint8; + Command : array[0..15] of uint8; + end; + TCSW = packed record + Signature, Tag, Residue : uint32; + Status : uint8; + end; + PCSW = ^TCSW; + PMSDevice = ^TMSDevice; + TMSDevice = record + Next : PMSDevice; + Original : PUSBDevice; + USB : PUSBDevice; { Private snapshot survives physical disconnect. } + Disk : PStorage_Device; { Canonical manager record, retained as tombstone. } + InEP, OutEP : PUSBEndpoint; + ControlEP : TUSBEndpoint; + Transfer : TUSBTransfer; + Buffer : pointer; { Page aligned, private DMA scratch, also used for control. } + CBW : TCBW; + CSW : TCSW; + Request : PIORequest; + Tag : uint32; + Offline, Ready, InFlight, Attaching, VolumesDetached : boolean; + end; -{ Called by driver.mgr when a mass-storage interface is matched. - For now we just log it. } -function load(ptr : void) : boolean; +var + Devices : PMSDevice; + Worker : PProcessContext; + AttachBusy : boolean; + +function be32(p : puint8) : uint32; begin - push_trace('driver.storage.ctl.usb.load'); - io.syslog.logln('driver.bus.usb-Storage', 'Mass-storage device detected (stub - no I/O yet).'); - - { TODO: read Max LUN, set up Bulk-Only endpoints, register with - driver.storage.mgr via driver.storage.mgr.register_device. } - - load := true; - pop_trace; + be32 := (uint32(p[0]) shl 24) or (uint32(p[1]) shl 16) or + (uint32(p[2]) shl 8) or uint32(p[3]); end; -{ ========================= Init ========================= } +procedure put32(p : puint8; value : uint32); +begin + p[0] := value shr 24; p[1] := value shr 16; + p[2] := value shr 8; p[3] := value; +end; + +function valid_capacity(last, size : uint32) : boolean; +begin + valid_capacity := (last <> $FFFFFFFF) and (size >= 512) and + (size <= BUFFER_SIZE) and ((size and (size - 1)) = 0); +end; + +function valid_csw(csw : PCSW; tag, requested, actual : uint32) : boolean; +begin + valid_csw := (csw^.Signature = CSW_SIGNATURE) and (csw^.Tag = tag) and + (csw^.Status <= 2) and (csw^.Residue <= requested) and + (actual = sizeof(TCSW)); +end; + +function transfer_pending(c : PMSDevice) : boolean; +var s : TUSBTransferStatus; +begin + { Read status afresh: completion may have been delivered by an ISR. } + s := PUSBTransfer(@c^.Transfer)^.Status; + transfer_pending := c^.InFlight and ((s = tsInProgress) or (s = tsNotStarted)); +end; + +procedure poll_controller(c : PMSDevice); +var flags : uint32; +begin + { Poll and its descriptor cleanup must not be preempted by another poll. } + asm pushfd; cli; pop flags end; + if c^.USB^.HC^.fnPoll <> nil then c^.USB^.HC^.fnPoll(c^.USB^.HC); + if (flags and $200) <> 0 then asm sti end; +end; + +function run_transfer(c : PMSDevice) : TUSBTransferStatus; +var started, spins, flags : uint32; submitted : boolean; +begin + run_transfer := tsTimeout; + if c^.Offline then exit; + c^.Transfer.Status := tsNotStarted; + c^.Transfer.ActualLen := 0; + c^.Transfer.HCPriv := nil; + c^.InFlight := true; + { fnSubmit can configure xHCI endpoints and wait for command completion; + never call it under the storage manager's IRQ-disabled dispatch lock. } + asm pushfd; pop flags end; + if c^.USB^.HC^.HCType <> USB_HC_XHCI then asm cli end; + if c^.Offline then submitted := false + else submitted := c^.USB^.HC^.fnSubmit(c^.USB^.HC, @c^.Transfer); + if (flags and $200) <> 0 then asm sti end; + if not submitted then begin + c^.InFlight := false; + exit; + end; + started := puint32(@Counters.c32)^; + spins := 0; + while transfer_pending(c) do begin + poll_controller(c); + if not transfer_pending(c) then break; + inc(spins); + if (c^.Original = nil) or + (puint32(@Counters.c32)^ - started >= TRANSFER_TICKS) or + (spins >= 10000000) then begin + { No HC cancellation contract exists yet. Fail closed and retain the + private transfer/buffers until completion; NEVER free live DMA. } + c^.Offline := true; + if c^.Disk <> nil then c^.Disk^.removed := true; + io.syslog.logln('USBMSC', 'Transfer timed out/disconnected; device offline, DMA quarantined.'); + exit; + end; + asm pushfd; pop flags end; + if (flags and $200) <> 0 then asm hlt end; + end; + c^.InFlight := false; + run_transfer := c^.Transfer.Status; +end; + +function bulk(c : PMSDevice; ep : PUSBEndpoint; buffer : pointer; + length : uint32; var actual : uint32) : TUSBTransferStatus; +var toggle, packets : uint32; status : TUSBTransferStatus; +begin + toggle := ep^.Toggle; + c^.Transfer.Device := c^.USB; + c^.Transfer.Endpoint := ep; + c^.Transfer.Direction := ep^.Direction; + c^.Transfer.PipeType := ptBulk; + c^.Transfer.Buffer := buffer; + c^.Transfer.BufferLen := length; + status := run_transfer(c); + actual := c^.Transfer.ActualLen; + if (status = tsSuccess) and (ep^.MaxPacket > 0) then begin + packets := actual div ep^.MaxPacket; + if ((actual mod ep^.MaxPacket) <> 0) or (actual < length) or (length = 0) then inc(packets); + ep^.Toggle := toggle xor (packets and 1); + end; + bulk := status; +end; + +function control(c : PMSDevice; kind, request : uint8; index, length : uint16) : TUSBTransferStatus; +begin + c^.Transfer.Device := c^.USB; + c^.Transfer.Endpoint := @c^.ControlEP; + c^.Transfer.Direction := dirSetup; + c^.Transfer.PipeType := ptControl; + c^.Transfer.Buffer := c^.Buffer; + c^.Transfer.BufferLen := length; + c^.Transfer.Setup := make_setup_packet(kind, request, 0, index, length); + control := run_transfer(c); +end; + +function clear_halt(c : PMSDevice; ep : PUSBEndpoint) : boolean; +var address : uint16; +begin + address := ep^.Address; + if ep^.Direction = dirIn then address := address or $80; + clear_halt := control(c, $02, USB_REQ_CLEAR_FEATURE, address, 0) = tsSuccess; + if clear_halt then ep^.Toggle := 0; + { xHCI also needs Reset Endpoint/Set TR Dequeue after a halted ring. + Until the HC exposes that operation, fail closed instead of reusing it. } + if c^.USB^.HC^.HCType = USB_HC_XHCI then clear_halt := false; +end; + +procedure recover(c : PMSDevice); +var ok : boolean; +begin + if c^.Offline then exit; + ok := control(c, $21, $FF, c^.USB^.InterfaceNumber, 0) = tsSuccess; + if ok then ok := clear_halt(c, c^.InEP); + if ok then ok := clear_halt(c, c^.OutEP); + if not ok then c^.Offline := true; +end; + +procedure command(c : PMSDevice; opcode, length : uint8; bytes : uint32; input : boolean); +begin + memset(uint32(@c^.CBW), 0, sizeof(TCBW)); + inc(c^.Tag); + c^.CBW.Signature := CBW_SIGNATURE; + c^.CBW.Tag := c^.Tag; + c^.CBW.DataLength := bytes; + if input then c^.CBW.Flags := $80; + c^.CBW.CommandLength := length; + c^.CBW.Command[0] := opcode; +end; + +{ 0=passed, 1=SCSI command failed (request sense), 2=transport failure. + Only one CBW/data/CSW sequence may be in flight on an interface. } +function transport(c : PMSDevice; var transferred : uint32) : uint8; +var status : TUSBTransferStatus; + actual, offset, count : uint32; + ep : PUSBEndpoint; +begin + transport := 2; transferred := 0; + if c^.CBW.DataLength > BUFFER_SIZE then exit; + status := bulk(c, c^.OutEP, @c^.CBW, sizeof(TCBW), actual); + if (status <> tsSuccess) or (actual <> sizeof(TCBW)) then begin recover(c); exit; end; + if c^.CBW.Flags = $80 then ep := c^.InEP else ep := c^.OutEP; + offset := 0; + while offset < c^.CBW.DataLength do begin + count := c^.CBW.DataLength - offset; + { One bounded data transfer per command, at most one page of DMA. } + status := bulk(c, ep, pointer(uint32(c^.Buffer) + offset), count, actual); + if status = tsStall then begin + if not clear_halt(c, ep) then begin recover(c); exit; end; + break; + end; + if (status <> tsSuccess) or (actual > count) then begin recover(c); exit; end; + inc(offset, actual); + if actual < count then break; + end; + transferred := offset; + memset(uint32(@c^.CSW), 0, sizeof(TCSW)); + status := bulk(c, c^.InEP, @c^.CSW, sizeof(TCSW), actual); + if status = tsStall then begin + if clear_halt(c, c^.InEP) then + status := bulk(c, c^.InEP, @c^.CSW, sizeof(TCSW), actual); + end; + if (status <> tsSuccess) or not valid_csw(@c^.CSW, c^.CBW.Tag, c^.CBW.DataLength, actual) then begin + recover(c); exit; + end; + if c^.CSW.Status = 2 then begin recover(c); exit; end; + if c^.CSW.Status = 1 then begin transport := 1; exit; end; + if (c^.CSW.Residue <> c^.CBW.DataLength - offset) then begin recover(c); exit; end; + transport := 0; +end; + +function sense(c : PMSDevice) : TError; +var actual : uint32; +begin + sense := eIOError; + command(c, $03, 6, 18, true); + c^.CBW.Command[4] := 18; + memset(uint32(c^.Buffer), 0, 18); + if transport(c, actual) <> 0 then exit; + if actual < 14 then exit; + if (puint8(c^.Buffer)[0] and $7E) <> $70 then exit; + case puint8(c^.Buffer)[2] and $0F of + 2: sense := eDeviceNotReady; + 3: sense := eBadSector; + 5: sense := eNotSupported; + 6: sense := eDeviceNotReady; + 7: sense := eReadOnly; + end; +end; + +function execute(c : PMSDevice; required : uint32) : TError; +var actual : uint32; resultCode : uint8; +begin + resultCode := transport(c, actual); + execute := eIOError; + if resultCode = 1 then execute := sense(c) + else if (resultCode = 0) and (actual = required) then execute := eNone; + if c^.Offline then execute := eIOTimeout; + if c^.Original = nil then execute := eDeviceRemoved; +end; + +function probe(c : PMSDevice; var sectors, sectorSize : uint32) : boolean; +var status : TUSBTransferStatus; attempt : uint32; error : TError; +begin + probe := false; + puint8(c^.Buffer)^ := 0; + status := control(c, $A1, $FE, c^.USB^.InterfaceNumber, 1); + { A STALL is explicitly allowed for a single-LUN device. } + if (status <> tsSuccess) and (status <> tsStall) then exit; + if status = tsSuccess then begin + if (c^.Transfer.ActualLen <> 1) or (puint8(c^.Buffer)^ > 15) then exit; + if puint8(c^.Buffer)^ <> 0 then + io.syslog.logln('USBMSC', 'Multiple LUNs reported; exposing LUN 0 only.'); + end; + command(c, $12, 6, 36, true); + c^.CBW.Command[4] := 36; + if execute(c, 36) <> eNone then exit; + if (puint8(c^.Buffer)[0] and $1F) <> 0 then exit; { Direct-access disks only. } + error := eDeviceNotReady; + for attempt := 1 to 5 do begin + command(c, $00, 6, 0, false); + error := execute(c, 0); + if error <> eDeviceNotReady then break; + psleep((PIT_INPUT_HZ div PIT_DIVISOR) div 5); + end; + if error <> eNone then exit; + command(c, $25, 10, 8, true); + if execute(c, 8) <> eNone then exit; + sectors := be32(puint8(c^.Buffer)); + sectorSize := be32(puint8(uint32(c^.Buffer) + 4)); + if not valid_capacity(sectors, sectorSize) then exit; + inc(sectors); + probe := true; +end; + +function valid_request(d : PStorage_Device; r : PIORequest) : boolean; +begin + valid_request := false; + if (r^.Buffer = nil) or (r^.SectorCount = 0) or (d^.sectorSize = 0) then exit; + if r^.LBA >= d^.maxSectorCount then exit; + if r^.SectorCount > d^.maxSectorCount - r^.LBA then exit; + if r^.SectorCount > $FFFFFFFF div d^.sectorSize then exit; + if uint32(r^.Buffer) > $FFFFFFFF - r^.SectorCount * d^.sectorSize then exit; + valid_request := true; +end; + +procedure dispatch(d : PStorage_Device; r : PIORequest); +var c : PMSDevice; +begin + { Called with interrupts masked: publish work only, no USB calls/waits. } + if d^.removed or (d^.controllerId0 = 0) then begin + driver.storage.mgr.complete_io(r, false, eDeviceRemoved); exit; + end; + c := PMSDevice(d^.controllerId0); + if c^.Offline or (c^.Original = nil) then begin + driver.storage.mgr.complete_io(r, false, eDeviceRemoved); exit; + end; + if not valid_request(d, r) then begin + driver.storage.mgr.complete_io(r, false, eInvalidArgument); exit; + end; + if (r^.RequestType = ioWrite) and not d^.writable then begin + driver.storage.mgr.complete_io(r, false, eReadOnly); exit; + end; + if c^.Request <> nil then begin + driver.storage.mgr.complete_io(r, false, eQueueFull); exit; + end; + c^.Disk := d; + c^.Request := r; +end; + +procedure process_request(c : PMSDevice); +var r : PIORequest; offset, blocks, bytes, doneBlocks : uint32; + error : TError; +begin + r := c^.Request; + if r = nil then exit; + error := eNone; doneBlocks := 0; offset := 0; + if c^.Offline or (c^.Original = nil) then error := eDeviceRemoved; + while (doneBlocks < r^.SectorCount) and (error = eNone) do begin + blocks := r^.SectorCount - doneBlocks; + if blocks > BUFFER_SIZE div c^.Disk^.sectorSize then + blocks := BUFFER_SIZE div c^.Disk^.sectorSize; + bytes := blocks * c^.Disk^.sectorSize; + if r^.RequestType = ioRead then command(c, $28, 10, bytes, true) + else begin + command(c, $2A, 10, bytes, false); + memcpy(uint32(r^.Buffer) + offset, uint32(c^.Buffer), bytes); + end; + put32(@c^.CBW.Command[2], r^.LBA + doneBlocks); + c^.CBW.Command[7] := blocks shr 8; + c^.CBW.Command[8] := blocks; + error := execute(c, bytes); + if error = eNone then begin + if r^.RequestType = ioRead then + memcpy(uint32(c^.Buffer), uint32(r^.Buffer) + offset, bytes); + inc(doneBlocks, blocks); inc(offset, bytes); + end; + end; + { Complete writes only after the device has committed its cache. } + if (error = eNone) and (r^.RequestType = ioWrite) then begin + command(c, $35, 10, 0, false); + error := execute(c, 0); + end; + if error = eReadOnly then c^.Disk^.writable := false; + r^.ByteCount := offset; + c^.Request := nil; + driver.storage.mgr.complete_io(r, error = eNone, error); +end; + +procedure disconnect(dev : PUSBDevice); +var c : PMSDevice; flags : uint32; +begin + asm pushfd; cli; pop flags end; + c := Devices; + while c <> nil do begin + if c^.Original = dev then begin + c^.Original := nil; + c^.Offline := true; + if c^.Disk <> nil then c^.Disk^.removed := true; + break; + end; + c := c^.Next; + end; + if (flags and $200) <> 0 then asm sti end; + io.syslog.logln('USBMSC', 'Disk disconnected; further I/O rejected.'); +end; + +function detach_volumes(c : PMSDevice) : boolean; +var i : uint32; v : PStorage_Volume; +begin + detach_volumes := true; + if c^.Disk = nil then exit; + if c^.Disk^.metadataBusy or (c^.Disk^.activeCount <> 0) then begin + detach_volumes := false; exit; + end; + if c^.Disk^.volumes = nil then exit; + i := 0; + while i < DL_Size(c^.Disk^.volumes) do begin + v := PStorage_Volume(puint32(DL_Get(c^.Disk^.volumes, i))^); + if (v <> nil) and ((v^.lifecycleFlags and STORAGE_VOLUME_FLAG_MOUNTED) <> 0) then begin + if driver.storage.vol.mgr.volume_is_busy(v) then detach_volumes := false + else if not driver.storage.vfs.InvalidateVolume(v) then detach_volumes := false; + end; + inc(i); + end; +end; + +procedure worker_loop(ctx : PProcessContext); +var c, next, prev, scan : PMSDevice; flags : uint32; +begin + while true do begin + c := Devices; + while c <> nil do begin + next := c^.Next; + if c^.Ready then process_request(c); + if c^.Offline and c^.InFlight and not c^.Attaching then begin + poll_controller(c); + if not transfer_pending(c) then c^.InFlight := false; + end; + if c^.Offline and not c^.InFlight and (c^.Request = nil) and + c^.Ready and not c^.Attaching then begin + if c^.Disk <> nil then c^.Disk^.removed := true; + if not c^.VolumesDetached then c^.VolumesDetached := detach_volumes(c); + if c^.Buffer <> nil then begin + kfree_aligned(c^.Buffer); c^.Buffer := nil; + end; + if (c^.Original = nil) and c^.VolumesDetached then begin + { Keep the manager's removed-device/volume tombstones stable; + release every class-driver allocation once DMA is finished. } + asm pushfd; cli; pop flags end; + prev := nil; scan := Devices; + while (scan <> nil) and (scan <> c) do begin prev := scan; scan := scan^.Next; end; + if prev = nil then Devices := c^.Next else prev^.Next := c^.Next; + if c^.Disk <> nil then c^.Disk^.controllerId0 := 0; + if (flags and $200) <> 0 then asm sti end; + kfree(void(c^.USB)); kfree_aligned(c); + end; + end; + c := next; + end; + proc.mgr.proc_sleep_ms(1); + end; +end; + +procedure attach_worker(ctx : PProcessContext); +var c : PMSDevice; d : PStorage_Device; sectors, sectorSize, i, flags : uint32; + claimed : boolean; +begin + c := PMSDevice(ctx^.Local); + { xHCI command submission and manager registration are not reentrant. } + repeat + asm pushfd; cli; pop flags end; + claimed := not AttachBusy; + if claimed then AttachBusy := true; + if (flags and $200) <> 0 then asm sti end; + if not claimed then proc.mgr.proc_sleep_ms(1); + until claimed; + d := PStorage_Device(kalloc(sizeof(TStorage_Device))); + if d = nil then begin + c^.Offline := true; c^.Ready := true; c^.Attaching := false; AttachBusy := false; exit; + end; + if not probe(c, sectors, sectorSize) then begin + c^.Offline := true; c^.Ready := true; + kfree(void(d)); + c^.Attaching := false; AttachBusy := false; + io.syslog.logln('USBMSC', 'Probe failed or unsupported disk geometry.'); + exit; + end; + memset(uint32(d), 0, sizeof(TStorage_Device)); + d^.volumes := DL_New(sizeof(pointer)); + if d^.volumes = nil then begin + kfree(void(d)); c^.Offline := true; c^.Ready := true; c^.Attaching := false; AttachBusy := false; exit; + end; + d^.controller := ControllerUSB; + d^.controllerId0 := uint32(c); + d^.writable := true; + d^.sectorSize := sectorSize; + d^.maxSectorCount := sectors; + d^.maxActive := 1; + d^.mergeMaxSectors := BUFFER_SIZE div sectorSize; + d^.dispatchRead := @dispatch; d^.dispatchWrite := @dispatch; + c^.Ready := true; + driver.storage.mgr.register_device(d); + { Manager copies the record; never keep the temporary descriptor. } + i := 0; + while i < driver.storage.mgr.get_device_count do begin + if driver.storage.mgr.get_device(i)^.controllerId0 = uint32(c) then begin + c^.Disk := driver.storage.mgr.get_device(i); break; + end; + inc(i); + end; + kfree(void(d)); + c^.Attaching := false; AttachBusy := false; + io.syslog.logln('USBMSC', 'LUN 0 registered with storage manager.'); + +end; + +function load(ptr : void) : boolean; +var dev : PUSBDevice; c : PMSDevice; + i, flags : uint32; +begin + load := false; + dev := PUSBDevice(ptr); + if (dev = nil) or (Worker = nil) then exit; + if (dev^.HC = nil) or (dev^.HC^.fnSubmit = nil) then exit; + { Core currently binds the first interface and flattens alternate settings. + Reject ambiguous endpoint sets rather than talking to the wrong pipe. } + if (dev^.InterfaceCount <> 1) or (dev^.NumEndpoints <> 2) or + (dev^.Speed = USB_SPEED_LOW) then exit; + if dev^.HC^.HCType = USB_HC_UHCI then begin + io.syslog.logln('USBMSC', 'UHCI storage requires HC queue/cleanup support.'); exit; + end; + c := PMSDevice(kalloc_aligned(sizeof(TMSDevice), 4096)); + if c = nil then exit; + memset(uint32(c), 0, sizeof(TMSDevice)); + c^.USB := PUSBDevice(kalloc(sizeof(TUSBDevice))); + c^.Buffer := kalloc_aligned(BUFFER_SIZE, 4096); + if (c^.USB = nil) or (c^.Buffer = nil) then begin + if c^.USB <> nil then kfree(void(c^.USB)); + kfree_aligned(c^.Buffer); kfree_aligned(c); exit; + end; + memcpy(uint32(dev), uint32(c^.USB), sizeof(TUSBDevice)); + c^.Original := dev; + c^.Attaching := true; + for i := 0 to 1 do begin + if (c^.USB^.Endpoints[i].PipeType = ptBulk) and + (c^.USB^.Endpoints[i].Address <> 0) and + (c^.USB^.Endpoints[i].MaxPacket > 0) and + (c^.USB^.Endpoints[i].MaxPacket <= 1024) then begin + if c^.USB^.Endpoints[i].Direction = dirIn then c^.InEP := @c^.USB^.Endpoints[i] + else c^.OutEP := @c^.USB^.Endpoints[i]; + end; + end; + if (c^.InEP = nil) or (c^.OutEP = nil) then begin + kfree_aligned(c^.Buffer); kfree(void(c^.USB)); kfree_aligned(c); exit; + end; + c^.ControlEP.PipeType := ptControl; + c^.ControlEP.Direction := dirSetup; + c^.ControlEP.MaxPacket := dev^.MaxPacket0; + { Publish before probing so disconnect can invalidate the private context. } + asm pushfd; cli; pop flags end; + c^.Next := Devices; Devices := c; + dev^.fnDisconnect := @disconnect; + if (flags and $200) <> 0 then asm sti end; + { The scheduler starts only after boot.mgr.run. Probe and register from + a task so partition discovery can wait for the persistent I/O worker. } + if proc.mgr.create('usb.attach', @attach_worker, void(c), 1) = nil then begin + c^.Offline := true; c^.Ready := true; c^.Attaching := false; exit; + end; + load := true; +end; + +{ Memory-only host controller used by UnitTest; never touches a real disk. } +type + PMock = ^TMock; + TMock = record + HC : TUSBHCDriver; + USB : TUSBDevice; + Disk : TStorage_Device; + Request : TIORequest; + C : TMSDevice; + Stage, Fault, Controls, DataBytes : uint32; + GoodWrite, GoodRecovery : boolean; + end; + +function mock_submit(hc : PUSBHCDriver; t : PUSBTransfer) : boolean; +var m : PMock; csw : PCSW; i : uint32; +begin + m := PMock(hc^.PrivData); + mock_submit := true; + t^.Status := tsSuccess; t^.ActualLen := t^.BufferLen; + if t^.PipeType = ptControl then begin + if t^.Setup.bRequest = $FE then begin puint8(t^.Buffer)^ := 0; exit; end; + case m^.Controls of + 0: m^.GoodRecovery := (t^.Setup.bRequest = $FF) and (t^.Setup.wIndex = 3); + 1: m^.GoodRecovery := m^.GoodRecovery and (t^.Setup.bRequest = 1) and (t^.Setup.wIndex = $81); + 2: m^.GoodRecovery := m^.GoodRecovery and (t^.Setup.bRequest = 1) and (t^.Setup.wIndex = 2); + end; + inc(m^.Controls); + exit; + end; + case m^.Stage of + 0: begin + m^.Stage := 2; + m^.DataBytes := 0; + if m^.C.CBW.DataLength <> 0 then m^.Stage := 1; + if m^.Fault = 6 then begin t^.ActualLen := 30; m^.Stage := 0; end; + end; + 1: begin + if m^.C.CBW.Flags = $80 then begin + memset(uint32(t^.Buffer), $A5, t^.BufferLen); + case m^.C.CBW.Command[0] of + $12: puint8(t^.Buffer)[0] := 0; + $25: begin put32(puint8(t^.Buffer), 8191); put32(puint8(uint32(t^.Buffer)+4), 512); end; + $03: begin memset(uint32(t^.Buffer),0,t^.BufferLen); puint8(t^.Buffer)[0]:=$70; puint8(t^.Buffer)[2]:=7; end; + end; + end else begin + m^.GoodWrite := true; + if t^.BufferLen > 0 then for i := 0 to t^.BufferLen-1 do + if puint8(t^.Buffer)[i] <> $5A then m^.GoodWrite := false; + end; + if m^.Fault = 4 then t^.ActualLen := t^.BufferLen div 2; + m^.DataBytes := t^.ActualLen; + m^.Stage := 2; + end; + 2: begin + if m^.Fault = 5 then begin m^.Fault := 0; t^.Status := tsStall; t^.ActualLen := 0; exit; end; + csw := PCSW(t^.Buffer); + csw^.Signature := CSW_SIGNATURE; csw^.Tag := m^.C.CBW.Tag; + csw^.Residue := m^.C.CBW.DataLength - m^.DataBytes; + csw^.Status := 0; + case m^.Fault of + 1: inc(csw^.Tag); + 2: csw^.Status := 2; + 3: begin csw^.Status := 1; m^.Fault := 0; end; + 7: inc(csw^.Residue); + 8: t^.ActualLen := 12; + end; + m^.Stage := 0; + end; + end; +end; + +procedure UnitTest; +var passed, failed, actual, sectors, size : uint32; csw : TCSW; bytes : array[0..3] of uint8; m : PMock; + procedure Assert(ok : boolean; name : pchar); + begin + if ok then inc(passed) else begin + inc(failed); io.syslog.logln('USBMSC FAIL', name); + end; + end; +begin + passed := 0; failed := 0; + Assert(sizeof(TCBW) = 31, 'CBW wire size'); + Assert(sizeof(TCSW) = 13, 'CSW wire size'); + put32(@bytes[0], $12345678); + Assert((bytes[0] = $12) and (bytes[3] = $78), 'SCSI byte order'); + Assert(be32(@bytes[0]) = $12345678, 'capacity byte order'); + Assert(valid_capacity(0, 512), 'single sector'); + Assert(valid_capacity($FFFFFFFE, 4096), 'largest representable sector count'); + Assert(not valid_capacity($FFFFFFFF, 512), 'reject READ CAPACITY(16) sentinel'); + Assert(not valid_capacity(1, 0), 'reject zero block size'); + Assert(not valid_capacity(1, 513), 'reject non-power-of-two block size'); + Assert(not valid_capacity(1, 8192), 'reject oversized block size'); + csw.Signature := CSW_SIGNATURE; csw.Tag := 7; csw.Residue := 0; csw.Status := 0; + Assert(valid_csw(@csw, 7, 512, 13), 'valid CSW'); + Assert(not valid_csw(@csw, 8, 512, 13), 'reject stale tag'); + Assert(not valid_csw(@csw, 7, 512, 12), 'reject short CSW'); + csw.Residue := 513; + Assert(not valid_csw(@csw, 7, 512, 13), 'reject excessive residue'); + csw.Residue := 0; csw.Status := 3; + Assert(not valid_csw(@csw, 7, 512, 13), 'reject reserved status'); + csw.Status := 0; csw.Signature := 0; + Assert(not valid_csw(@csw, 7, 512, 13), 'reject signature'); + m := PMock(kalloc(sizeof(TMock))); + Assert(m <> nil, 'mock allocation'); + if m <> nil then begin + memset(uint32(m), 0, sizeof(TMock)); + m^.HC.fnSubmit := @mock_submit; m^.HC.HCType := USB_HC_EHCI; m^.HC.PrivData := m; + m^.USB.HC := @m^.HC; m^.USB.InterfaceNumber := 3; + m^.USB.Endpoints[0].Address := 1; m^.USB.Endpoints[0].Direction := dirIn; + m^.USB.Endpoints[1].Address := 2; m^.USB.Endpoints[1].Direction := dirOut; + m^.USB.Endpoints[0].MaxPacket := 512; m^.USB.Endpoints[1].MaxPacket := 512; + m^.C.USB := @m^.USB; m^.C.Original := @m^.USB; + m^.C.InEP := @m^.USB.Endpoints[0]; m^.C.OutEP := @m^.USB.Endpoints[1]; + m^.C.Buffer := kalloc_aligned(BUFFER_SIZE, 4096); + Assert(m^.C.Buffer <> nil, 'mock DMA scratch allocation'); + if m^.C.Buffer <> nil then begin + Assert(probe(@m^.C, sectors, size) and (sectors = 8192) and (size = 512), 'probe SCSI disk'); + command(@m^.C, $28, 10, 512, true); + Assert((execute(@m^.C,512) = eNone) and (puint8(m^.C.Buffer)[511] = $A5), 'read data phase'); + command(@m^.C, $2A, 10, 512, false); memset(uint32(m^.C.Buffer),$5A,512); + Assert((execute(@m^.C,512) = eNone) and m^.GoodWrite, 'write data phase'); + m^.Fault := 3; command(@m^.C, $2A, 10, 512, false); + Assert(execute(@m^.C,512) = eReadOnly, 'command failure requests sense'); + m^.Fault := 4; command(@m^.C, $28, 10, 512, true); + Assert(execute(@m^.C,512) = eIOError, 'short read is not full success'); + m^.Fault := 5; command(@m^.C, $28, 10, 512, true); + Assert(execute(@m^.C,512) = eNone, 'CSW stall cleared and retried'); + m^.Fault := 1; m^.Controls := 0; command(@m^.C, $28, 10, 512, true); + Assert((transport(@m^.C,actual) = 2) and (m^.Controls = 3) and m^.GoodRecovery, 'bad tag triggers ordered reset recovery'); + m^.Fault := 2; m^.Controls := 0; command(@m^.C, $28, 10, 512, true); + Assert((transport(@m^.C,actual) = 2) and m^.GoodRecovery, 'phase error reset recovery'); + m^.Fault := 6; m^.Controls := 0; command(@m^.C, $28, 10, 512, true); + Assert((transport(@m^.C,actual) = 2) and m^.GoodRecovery, 'short CBW reset recovery'); + m^.Fault := 7; command(@m^.C, $28, 10, 512, true); + Assert(transport(@m^.C,actual) = 2, 'inconsistent CSW residue rejected'); + m^.Fault := 8; command(@m^.C, $28, 10, 512, true); + Assert(transport(@m^.C,actual) = 2, 'short CSW rejected'); + m^.Fault := 0; command(@m^.C, $35, 10, 0, false); + Assert(execute(@m^.C,0) = eNone, 'cache synchronization without data stage'); + m^.Disk.sectorSize := 512; m^.Disk.maxSectorCount := 8192; + m^.Request.Buffer := m^.C.Buffer; m^.Request.LBA := 8191; m^.Request.SectorCount := 1; + Assert(valid_request(@m^.Disk,@m^.Request), 'last sector valid'); + m^.Request.SectorCount := 2; + Assert(not valid_request(@m^.Disk,@m^.Request), 'cross-end request rejected'); + m^.Request.SectorCount := 0; + Assert(not valid_request(@m^.Disk,@m^.Request), 'zero-sector request rejected'); + m^.Request.LBA := $FFFFFFFE; m^.Request.SectorCount := 4; + Assert(not valid_request(@m^.Disk,@m^.Request), 'wrapped LBA rejected'); + m^.Request.LBA := 0; m^.Request.SectorCount := 1; m^.Request.Buffer := pointer($FFFFFF00); + Assert(not valid_request(@m^.Disk,@m^.Request), 'wrapped buffer rejected'); + kfree_aligned(m^.C.Buffer); + end; + kfree(void(m)); + end; + io.syslog.log('USBMSC', 'Tests passed: '); io.syslog.writeint(passed); + io.syslog.writestring(' failed: '); io.syslog.writeintln(failed); +end; + +procedure test_command(params : PParamList; stdin_buf, stdout_buf, stderr_buf : POutBuf); +begin + UnitTest; + io.stdio.bufWriteStrLn(stdout_buf, 'USB mass storage protocol tests written to serial log.'); +end; procedure init; -var - MSID : TDeviceIdentifier; +var id : TDeviceIdentifier; begin - push_trace('driver.storage.ctl.usb.init'); - io.syslog.logln('driver.bus.usb-Storage', 'INIT BEGIN.'); - - MSID.Bus := biUSB; - MSID.id0 := idANY; { Any VID:PID } - MSID.id1 := $FFFFFFFF; { Any device class } - MSID.id2 := USB_CLASS_MASS_STORAGE;{ bInterfaceClass = $08 } - MSID.id3 := USB_SC_SCSI; { bInterfaceSubClass = $06 } - MSID.id4 := USB_PROTO_BBB; { bInterfaceProtocol = $50 } - MSID.ex := nil; - - driver.mgr.register_driver('driver.bus.usb Storage Driver', @MSID, @load); - - io.syslog.logln('driver.bus.usb-Storage', 'INIT END.'); - pop_trace; + io.stdio.registerCommand('USBMSCTEST', @test_command, 'Test USB mass storage protocol without disk I/O'); + Worker := proc.mgr.create('usb.storage', @worker_loop, nil, 1); + if Worker = nil then exit; + id.Bus := biUSB; id.id0 := idANY; id.id1 := $FFFFFFFF; + id.id2 := $08; id.id3 := $06; id.id4 := $50; id.ex := nil; + driver.mgr.register_driver('USB Mass Storage', @id, @load); end; initialization boot.mgr.registerBoot('driver.storage.ctl.usb', @init, 'USB Mass Storage Driver', BOOT_MGR_BARRIER_DEVICE); - end. diff --git a/src/test/test.pas b/src/test/test.pas index ea33ad07..520b7402 100644 --- a/src/test/test.pas +++ b/src/test/test.pas @@ -37,6 +37,7 @@ uses driver.storage.metatest, driver.storage.perftest, driver.storage.ctl.ahci, + driver.storage.ctl.usb, core.enc.fnv1a, core.enc.djb2, core.ds.bloom, @@ -76,6 +77,7 @@ begin driver.storage.metatest.UnitTest; driver.storage.perftest.UnitTest; driver.storage.ctl.ahci.UnitTest; + driver.storage.ctl.usb.UnitTest; core.enc.fnv1a.UnitTest; core.enc.djb2.UnitTest; core.ds.bloom.UnitTest;