Files
Asuro/src/driver/storage/ctl/driver.storage.ctl.usb.pas
T

806 lines
33 KiB
ObjectPascal

// Copyright 2021 Aaron Hance
// Copyright 2026 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.
{ 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 }
{
@author(Aaron Hance <[email protected]>)
}
unit driver.storage.ctl.usb;
interface
procedure init;
procedure UnitTest;
implementation
uses arch.platform.diagnostics, arch.common.legacyirq, driver.mgr, memory.heap, driver.storage.types,
driver.storage.mgr, driver.bus.usb.core, driver.bus.usb.types,
proc.mgr, proc.types, arch.platform.process,
core.util, arch.platform.cpu, io.syslog, io.stdio, core.strings,
driver.storage.vfs, driver.storage.vol.mgr, core.ds.lists;
const
CBW_SIGNATURE = $43425355;
CSW_SIGNATURE = $53425355;
BUFFER_SIZE = 4096;
TRANSFER_TICKS = (PIT_INPUT_HZ div PIT_DIVISOR) * 5;
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;
PVolumeSlot = ^PStorage_Volume;
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;
SyncUnsupported : boolean;
end;
var
Devices : PMSDevice;
Worker : PProcessContext;
AttachBusy : boolean;
function be32(p : puint8) : uint32;
begin
be32 := (uint32(p[0]) shl 24) or (uint32(p[1]) shl 16) or
(uint32(p[2]) shl 8) or uint32(p[3]);
end;
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 : PtrUInt;
begin
{ Poll and its descriptor cleanup must not be preempted by another poll. }
flags := arch.platform.cpu.lock;
if c^.USB^.HC^.fnPoll <> nil then c^.USB^.HC^.fnPoll(c^.USB^.HC);
arch.platform.cpu.unlock(flags);
end;
function run_transfer(c : PMSDevice) : TUSBTransferStatus;
var started, spins : uint32; flags : PtrUInt; 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. }
flags := arch.platform.cpu.lock;
if c^.USB^.HC^.HCType = USB_HC_XHCI then arch.platform.cpu.unlock(flags);
if c^.Offline then submitted := false
else submitted := c^.USB^.HC^.fnSubmit(c^.USB^.HC, @c^.Transfer);
arch.platform.cpu.unlock(flags);
if not submitted then begin
c^.InFlight := false;
exit;
end;
started := arch.platform.process.timerTicks;
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
(arch.platform.process.timerTicks - 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;
flags := arch.platform.cpu.lock;
arch.platform.cpu.unlock(flags);
if (flags and $200) <> 0 then arch.platform.cpu.idle;
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(PtrUInt(@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(PtrUInt(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(PtrUInt(@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(PtrUInt(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;
io.syslog.log('USBMSC', 'SCSI sense key='); io.syslog.writeint(puint8(c^.Buffer)[2] and $0F);
io.syslog.writestring(' ASC='); io.syslog.writeint(puint8(c^.Buffer)[12]);
io.syslog.writestring(' ASCQ='); io.syslog.writeintln(puint8(c^.Buffer)[13]);
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, opcode : uint8;
begin
opcode := c^.CBW.Command[0];
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;
if execute <> eNone then begin
io.syslog.log('USBMSC', 'Command failed: opcode='); io.syslog.writeint(opcode);
io.syslog.writestring(' error='); io.syslog.writeintln(ord(execute));
end;
end;
function sync_cache(c : PMSDevice) : TError;
begin
sync_cache := eNone;
if c^.SyncUnsupported then exit;
command(c, $35, 10, 0, false);
sync_cache := execute(c, 0);
{ Some removable flash devices reject SYNCHRONIZE CACHE with ILLEGAL
REQUEST despite accepting WRITE(10). This optional command must not turn
an acknowledged data write into a zero-byte failure. Other errors remain
failures; do not retry or suppress an unsuccessful WRITE(10). }
if sync_cache = eNotSupported then begin
c^.SyncUnsupported := true;
io.syslog.logln('USBMSC', 'SYNCHRONIZE CACHE unsupported; using device write completion.');
sync_cache := eNone;
end;
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;
core.util.waitTicks((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(PtrUInt(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 PtrUInt(r^.Buffer) > High(PtrUInt) - 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(PtrUInt(r^.Buffer) + offset, PtrUInt(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(PtrUInt(c^.Buffer), PtrUInt(r^.Buffer) + offset, bytes);
inc(doneBlocks, blocks); inc(offset, bytes);
end;
end;
{ Flush devices which support it; unsupported removable media use the
WRITE command completion contract. }
if (error = eNone) and (r^.RequestType = ioWrite) then begin
error := sync_cache(c);
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 : PtrUInt;
begin
flags := arch.platform.cpu.lock;
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;
arch.platform.cpu.unlock(flags);
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 := PVolumeSlot(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 : PtrUInt;
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. }
flags := arch.platform.cpu.lock;
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;
arch.platform.cpu.unlock(flags);
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 : uint32; flags : PtrUInt;
claimed : boolean;
begin
c := PMSDevice(ctx^.Local);
{ xHCI command submission and manager registration are not reentrant. }
repeat
flags := arch.platform.cpu.lock;
claimed := not AttachBusy;
if claimed then AttachBusy := true;
arch.platform.cpu.unlock(flags);
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(PtrUInt(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 := PtrUInt(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 = PtrUInt(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 : uint32;
flags : PtrUInt;
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(PtrUInt(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(PtrUInt(dev), PtrUInt(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. }
flags := arch.platform.cpu.lock;
c^.Next := Devices; Devices := c;
dev^.fnDisconnect := @disconnect;
arch.platform.cpu.unlock(flags);
{ 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;
SenseKey : uint8;
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(PtrUInt(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(PtrUInt(t^.Buffer)+4), 512); end;
$03: begin memset(PtrUInt(t^.Buffer),0,t^.BufferLen); puint8(t^.Buffer)[0]:=$70;
if m^.SenseKey = 0 then puint8(t^.Buffer)[2]:=7 else puint8(t^.Buffer)[2]:=m^.SenseKey; 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(PtrUInt(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(PtrUInt(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^.Fault := 3; m^.SenseKey := 3;
Assert(sync_cache(@m^.C) = eBadSector, 'flush media errors propagate');
Assert(not m^.C.SyncUnsupported, 'media error does not disable flush');
m^.Fault := 3; m^.SenseKey := 5;
Assert(sync_cache(@m^.C) = eNone, 'unsupported flush accepts completed write');
sectors := m^.C.Tag;
Assert((sync_cache(@m^.C) = eNone) and (m^.C.Tag = sectors), 'unsupported flush is not repeatedly submitted');
m^.Fault := 3; command(@m^.C, $2A, 10, 512, false);
Assert(execute(@m^.C,512) = eNotSupported, 'unsupported data write remains a failure');
m^.Fault := 0; m^.SenseKey := 0;
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(High(PtrUInt) - $FF);
Assert(not valid_request(@m^.Disk,@m^.Request), 'wrapped buffer rejected');
kfree_aligned(m^.C.Buffer);
end;
kfree(void(m));
end;
testSummary('USBMSC', 'USBMSC_TESTS_PASSED', 'USBMSC_TESTS_FAILED', passed, 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 id : TDeviceIdentifier;
begin
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;
end.