Compare commits

..
Author SHA1 Message Date
Aaron e8e4061564 fix file header
continuous-integration/drone Build is failing
2026-11-07 10:19:01 +00:00
Aaron 10691a9e03 fix file headers 2026-11-07 10:18:33 +00:00
Aaron cebad0b583 Fix USB volume discovery, automounting, and file saves 2026-11-01 20:22:07 +00:00
Aaron 53cc758e60 Add standard license headers and Aaron Hance attribution 2026-10-31 20:56:15 +00:00
Aaron 6c7619bc2c Keep development plan local and remove it from version control 2026-10-31 20:50:31 +00:00
Aaron d318eca262 Refactor VFS shared operations and remove unused legacy storage wrappers 2026-10-31 20:46:48 +00:00
Aaron c28b9dd77f Implement USB mass storage and batch OHCI bulk transfers 2026-10-31 20:36:52 +00:00
Aaron c0c56a9c84 Add initial ext2 read/write and formatting support
Register ext2 at boot, integrate mode-aware VFS opens and volume cleanup, and queue asynchronous transfers through a persistent worker.

Validation: Pascal compiler check and git diff --check passed. Full build blocked by Docker engine HTTP 500; runtime and Linux interoperability remain unverified.
2026-10-31 19:49:36 +00:00
24 changed files with 2780 additions and 947 deletions

No files matched your search

+1
View File
@@ -15,6 +15,7 @@ localenv.json
dockerout.txt
AGENTS.md
lessons_learnt.md
/plan.md
*.log
wasuro
src/core/core.version.pas
@@ -1,49 +1,85 @@
# driver.storage.ctl.usb
<!--
Copyright 2026 Aaron Hance
Author: Aaron Hance <[email protected]>
USB Mass Storage Class driver stub.
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
## Overview
http://www.apache.org/licenses/LICENSE-2.0
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.
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.
-->
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.
# USB mass storage
## Boot Registration
`driver.storage.ctl.usb` implements SCSI transparent commands over USB Mass
Storage Bulk-Only Transport (interface class/subclass/protocol `08/06/50`).
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.
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.
## Dependencies
## Execution and ownership
- `boot.mgr`
An attachment task probes and registers each disk after scheduling starts; boot
initialization never waits on a worker that cannot run yet.
- `driver.mgr`
- `memory.heap`
- `driver.storage.types`
- `io.syslog`
- `debug.tracer`
- `driver.bus.usb.core`
- `driver.bus.usb.types`
- `core.util`, `arch.x86.util`
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.
## Constants
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.
| 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 |
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.
## Functions and Procedures
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.
### init
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.
```pascal
procedure init;
```
## Supported scope
Constructs a `TDeviceIdentifier` matching any USB device with `bInterfaceClass = $08`, `bInterfaceSubClass = $06`, `bInterfaceProtocol = $50`, and registers the `load` callback with `driver.mgr`.
- 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.
## Notes
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.
- 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.
## Validation
`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.
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.
Protocol: [USB-IF Bulk-Only Transport 1.0](https://www.usb.org/sites/default/files/usbmassbulk_10.pdf).
+18 -10
View File
@@ -1,3 +1,20 @@
<!--
Copyright 2026 Aaron Hance
Author: Aaron Hance <[email protected]>
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.storage.mgr
Physical storage device registry and I/O dispatch manager.
@@ -14,7 +31,7 @@ The I/O model operates as follows:
- `storage_read` / `storage_write` are convenience wrappers that build a stack-local `TIORequest` and call `submit_io_wait`.
- `storage_read_async` / `storage_write_async` build and submit a request with a caller-supplied callback, returning immediately.
Legacy `storage_read_legacy` / `storage_write_legacy` procedure wrappers are retained for callers not yet migrated to the function-returning API.
Blocking callers use `storage_read` / `storage_write` and check the returned `TError`.
## Dependencies
@@ -205,15 +222,6 @@ function storage_write_async(device : PStorage_Device; addr : uint32; sectors :
Non-blocking write. Analogous to `storage_read_async`.
### storage_read_legacy / storage_write_legacy
```pascal
procedure storage_read_legacy(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32);
procedure storage_write_legacy(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32);
```
Drop-in procedure replacements for callers using the old void-returning API. Forward to `storage_read` / `storage_write` and discard the error code. Marked for removal once all callers are migrated.
## Notes
- `dispatch_next` is an internal procedure called with interrupts disabled. It dequeues the head of the CFIFO, allocates a heap copy via `ioreq_copy` (for ISR safety), and invokes the appropriate `dispatchRead` or `dispatchWrite` on the device. If neither dispatch pointer is set, the request is failed with `eDeviceNotFound`.
-500
View File
File diff suppressed because it is too large. Load diff
+13 -11
View File
@@ -1200,7 +1200,8 @@ begin
vol := findVolumeForPartition(device, part.LBA_start);
if vol <> nil then begin
freeB := uint64(vol^.freeSectors) * vol^.sectorSize;
freeStr := uiConcat(formatSizeStr(freeB), ' free');
if (vol^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0 then freeStr := 'Free: unknown'
else freeStr := uiConcat(formatSizeStr(freeB), ' free');
lbl := lv_label_create(row);
lv_label_set_text(lbl, freeStr);
lv_obj_set_style_text_color(lbl, lv_color_make(100, 180, 130), 0);
@@ -1240,7 +1241,6 @@ var
dw : sint32;
i : uint32;
totalBytes : uint64;
usedSectors : uint64;
freeBytes : uint64;
volCnt : uint32;
devVolCnt : uint32;
@@ -1274,22 +1274,18 @@ begin
totalBytes := uint64(device^.maxSectorCount) * device^.sectorSize;
addInfoRow(detail, dw, 'Total Size', formatSizePrecise(totalBytes));
{ Compute free space from in-memory volume data (no I/O) }
usedSectors := 0;
{ Use the same cached partition geometry as Add Partition. Volumes may
still be undergoing filesystem probing after the device is published. }
devVolCnt := 0;
volCnt := driver.storage.vol.mgr.get_volume_count();
if volCnt > 0 then for i := 0 to volCnt - 1 do begin
vol := driver.storage.vol.mgr.get_volume(i);
if (vol <> nil) and (vol^.device = device) then begin
devVolCnt := devVolCnt + 1;
usedSectors := usedSectors + vol^.sectorCount;
end;
end;
if device^.maxSectorCount > usedSectors then
freeBytes := uint64(device^.maxSectorCount - usedSectors) * device^.sectorSize
else
freeBytes := 0;
addInfoRow(detail, dw, 'Free Space', formatSizePrecise(freeBytes));
freeBytes := uint64(driver.storage.vol.mgr.get_free_sector_count(device)) * device^.sectorSize;
addInfoRow(detail, dw, 'Unallocated Space', formatSizePrecise(freeBytes));
addInfoRow(detail, dw, 'Volumes', uiInt(devVolCnt));
{ Usage bar }
@@ -1352,7 +1348,8 @@ begin
{ Free space }
freeB := uint64(vol^.freeSectors) * vol^.sectorSize;
freeStr := uiConcat(formatSizeStr(freeB), ' free');
if (vol^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0 then freeStr := 'Free: unknown'
else freeStr := uiConcat(formatSizeStr(freeB), ' free');
lbl := lv_label_create(row);
lv_label_set_text(lbl, freeStr);
lv_obj_set_style_text_color(lbl, lv_color_make(100, 180, 130), 0);
@@ -1494,6 +1491,10 @@ begin
usedBytes := totalBytes - freeBytes;
addInfoRow(detail, dw, 'Total Size', formatSizePrecise(totalBytes));
if (vol^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0 then begin
addInfoRow(detail, dw, 'Free Space', 'Unknown');
addInfoRow(detail, dw, 'Used Space', 'Unknown');
end else begin
addInfoRow(detail, dw, 'Free Space', formatSizePrecise(freeBytes));
addInfoRow(detail, dw, 'Used Space', formatSizePrecise(usedBytes));
@@ -1502,6 +1503,7 @@ begin
addInfoRow(detail, dw, 'Usage', uiConcat(uiInt(pctUsed), '%'));
addUsageBar(detail, dw, usedBytes, totalBytes);
end;
end;
addInfoRow(detail, dw, 'Boot Drive', boolStr(vol^.isBootDrive));
+4 -2
View File
@@ -355,8 +355,10 @@ begin
proc.mgr.kill(state^.pid);
state^.pid := 0;
end;
freeState(state);
if state^.ta <> nil then lv_group_remove_obj(state^.ta);
state^.ta := nil;
driver.video.windows.destroyWindow(wid);
freeState(state);
end;
{ ============================================================
@@ -440,7 +442,7 @@ begin
written := driver.storage.vfs.WriteFile(fHandle, 0, puint8(buf), len);
driver.storage.vfs.CloseFile(fHandle);
kfree(void(buf));
if written = 0 then begin
if written <> len then begin
io.syslog.logln('NOTEPAD', 'saveFile: WriteFile returned 0');
showErrorMsgbox('Save Failed', 'File write failed. Check filesystem.');
updateStatusBar(state);
+3 -1
View File
@@ -179,7 +179,9 @@ begin
print('Size: '); printsize(vol^.sectorCount * vol^.sectorSize); println('');
print('Free: '); printsize(vol^.freeSectors * vol^.sectorSize); println('');
print('Free: ');
if (vol^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0 then println('Unknown')
else begin printsize(vol^.freeSectors * vol^.sectorSize); println(''); end;
print('Boot Drive: ');
if vol^.isBootDrive then println('yes') else println('no');
+1
View File
@@ -96,6 +96,7 @@ uses
driver.net.dev.e1000,
driver.bus.usb.ehci,
driver.storage.fs.fat32,
driver.storage.fs.ext2,
arch.x86.fault,
core.ds.fifo,
driver.storage.fs.mgr,
+39 -6
View File
@@ -1,6 +1,21 @@
// Copyright 2026 Kieron Morris
//
// 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->USBCore - driver.bus.usb Core Enumeration, Transfer API & HC Management.
@author(Aaron Hance <[email protected]>)
@author(Kieron Morris <[email protected]>)
}
unit driver.bus.usb.core;
@@ -658,6 +673,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 +683,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 +748,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 +769,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 +807,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;
+41 -7
View File
@@ -1,3 +1,17 @@
// Copyright 2026 Kieron Morris
//
// 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->driver.bus.usb.ehci - Enhanced Host Controller Interface Driver.
@@ -8,6 +22,7 @@
driver.bus.usb.ehci also handles companion controller (driver.bus.usb.uhci/driver.bus.usb.ohci) routing for
full/low-speed devices via the CONFIGFLAG and PORTSC port-owner bits.
@author(Aaron Hance <[email protected]>)
@author(Kieron Morris <[email protected]>)
}
unit driver.bus.usb.ehci;
@@ -963,6 +978,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 +1021,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 +1038,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 +1051,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 +1070,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 +1088,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 +1170,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 +1182,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 +1219,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 +1316,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 +1409,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 +1448,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)
File diff suppressed because it is too large. Load diff
@@ -1,6 +1,21 @@
// Copyright 2026 Kieron Morris
//
// 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->USBTypes - Shared driver.bus.usb Types, Constants & HC Abstraction.
@author(Aaron Hance <ah@aaronhance.me>)
@author(Kieron Morris <kjm@kieronmorris.me>)
}
unit driver.bus.usb.types;
@@ -249,6 +264,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;
File diff suppressed because it is too large. Load diff
+52 -1
View File
@@ -15,6 +15,7 @@ type
TMock = record
Data : puint8;
Writes, FailWrite, Callbacks, Depth, MaxDepth : uint32;
MaxReadSectors, FailReadLBA : uint32;
Error : TError;
OutOfBounds, FailRead : boolean;
end;
@@ -27,7 +28,10 @@ begin
(request^.SectorCount > device^.maxSectorCount-request^.LBA) then begin
m^.OutOfBounds := true; complete_io(request, false, eInvalidArgument); exit;
end;
if (request^.RequestType <> ioWrite) and m^.FailRead then begin
if request^.RequestType <> ioWrite then
if request^.SectorCount > m^.MaxReadSectors then m^.MaxReadSectors := request^.SectorCount;
if (request^.RequestType <> ioWrite) and (m^.FailRead or
((m^.FailReadLBA <> 0) and (request^.LBA = m^.FailReadLBA))) then begin
Inc(m^.Depth); if m^.Depth > m^.MaxDepth then m^.MaxDepth := m^.Depth;
complete_io(request,false,eIOError); Dec(m^.Depth); exit;
end;
@@ -197,6 +201,53 @@ begin
mock^.FailWrite := 0;
Assert(remove_partition(device,0) = eNone,'delete commits');
Assert(get_volume_count() = oldCount,'delete detaches volume after write');
{ More than eight FAT sectors, with a partial final batch and FAT padding. }
kfree(void(mock^.Data));
mock^.Data := puint8(kalloc(4096*512));
if mock^.Data <> nil then begin
memset(uint32(mock^.Data),0,1500*512);
device^.maxSectorCount := 1500;
volume^.sectorStart := 0; volume^.sectorCount := 1500;
Assert(create_volume(volume,1500,0,@params) = eNone,'format multi-batch FAT fixture');
mock^.MaxReadSectors := 0;
Assert(identify_volume(volume),'identify multi-batch FAT');
Assert(volume^.freeSectors = 1454,'batched count excludes padding and allocated clusters');
Assert(mock^.MaxReadSectors = 8,'FAT scan batches eight sectors');
mock^.FailReadLBA := 40;
Assert(not identify_volume(volume),'failure in final FAT batch rejects probe');
mock^.FailReadLBA := 0;
Assert(not mock^.OutOfBounds,'batched scan stays within device');
device^.maxSectorCount := 4096; volume^.sectorCount := 4096;
Assert(create_volume(volume,4096,0,@params) = eNone,'format large FAT fixture');
mock^.FailReadLBA := 32; mock^.MaxReadSectors := 0;
Assert(identify_volume(volume),'large FAT mounts without reading allocation table');
Assert((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0,'missing FSInfo reports unknown free space');
Assert(mock^.MaxReadSectors = 1,'large volume discovery uses only metadata sector reads');
PBootRecord(mock^.Data)^.FSInfoCluster := 1;
puint32(uint32(mock^.Data)+512)^ := $41615252;
puint32(uint32(mock^.Data)+512+484)^ := $61417272;
puint32(uint32(mock^.Data)+512+488)^ := 100;
puint32(uint32(mock^.Data)+512+508)^ := $AA550000;
Assert(identify_volume(volume),'valid FSInfo avoids FAT scan');
Assert((volume^.freeSectors = 100) and
((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) = 0),'FSInfo count displayed');
puint32(uint32(mock^.Data)+512+488)^ := $FFFFFFFF;
Assert(identify_volume(volume),'unknown FSInfo count does not prevent mount');
Assert((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0,'unknown FSInfo count stays unknown');
puint32(uint32(mock^.Data)+512+488)^ := 4096;
Assert(identify_volume(volume),'out-of-range FSInfo count does not prevent mount');
Assert((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0,'out-of-range FSInfo count rejected');
puint32(uint32(mock^.Data)+512+488)^ := 0;
Assert(identify_volume(volume),'full volume FSInfo accepted');
Assert((volume^.freeSectors = 0) and
((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) = 0),'known zero differs from unknown');
puint32(uint32(mock^.Data)+512+508)^ := 0;
Assert(identify_volume(volume),'invalid FSInfo signature does not prevent mount');
Assert((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0,'invalid FSInfo signature rejected');
mock^.FailReadLBA := 1;
Assert(identify_volume(volume),'optional FSInfo read failure does not prevent mount');
mock^.FailReadLBA := 0;
end else Assert(false,'allocate multi-batch FAT fixture');
kfree(device^.cachedMBR); CFIFO_Free(device^.requestQueue);
kfree(void(mock^.Data)); kfree(void(mock)); kfree(void(volume)); kfree(void(device)); kfree(void(br));
io.syslog.log('STORMETA','Passed: '); io.syslog.writeint(passed);
+7 -23
View File
@@ -1,4 +1,5 @@
// 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.
@@ -19,14 +20,14 @@
and provides the submit_io / complete_io request-based I/O path.
Device drivers call register_device() when they discover a new device.
Phase 2 I/O model:
Request-based I/O model:
- submit_io enqueues a TIORequest into the device's per-device CFIFO,
dispatches to hardware, and sleeps the calling task (proc_await).
- complete_io is called from ISR/completion context, wakes the caller.
dispatches to hardware, and returns without waiting for completion.
- complete_io delivers callbacks from the driver's completion context.
- storage_read / storage_write are thin wrappers that build a stack-local
TIORequest, call submit_io, and return the resulting TError.
- Legacy sync/async callbacks are still supported as fallback for drivers
not yet migrated to the TDriverDispatch interface.
TIORequest, call submit_io_wait, and return the resulting TError.
- Drivers must provide the TDriverDispatch interface; there is no legacy
callback fallback.
@author(Aaron Hance <ah@aaronhance.me>)
}
@@ -112,12 +113,6 @@ function storage_read_async(device : PStorage_Device; addr : uint32; sectors : u
function storage_write_async(device : PStorage_Device; addr : uint32; sectors : uint32;
buffer : puint32; callback : TIOCallback; callbackData : pointer) : TError;
{ Legacy wrappers — procedure signatures matching the old API.
Forward to the new functions, silently discard errors.
TODO: migrate all callers and remove these. }
procedure storage_read_legacy(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32);
procedure storage_write_legacy(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32);
implementation
uses
@@ -744,17 +739,6 @@ begin
storage_write_async := submit_io(@req);
end;
{ Legacy procedure wrappers — drop-in replacements for old callers }
procedure storage_read_legacy(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32);
begin
storage_read(device, addr, sectors, buffer);
end;
procedure storage_write_legacy(device : PStorage_Device; addr : uint32; sectors : uint32; buffer : puint32);
begin
storage_write(device, addr, sectors, buffer);
end;
initialization
boot.mgr.registerBoot('driver.storage.mgr', @init, 'Storage Manager', 'driver.storage.vfs');
@@ -222,6 +222,8 @@ type
to read/write hooks. Also returns the current file size through the
var parameter. Returning nil is legal (disables per-call fast-path). }
PPOpenFileHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar; var fileSize : uint32) : pointer;
{ Mode-aware open avoids creating a missing file during a read-only open. }
PPOpenFileModeHook = function(volume : PStorage_Volume; directory : pchar; fileName : pchar; allowCreate : boolean; var fileSize : uint32) : pointer;
{ Per-file close hook: frees the context allocated by openFileCallback. }
PPCloseFileHook = procedure(ctx : pointer);
@@ -324,6 +326,7 @@ type
renameFileCallback : PPRenameFileHook;
{ Per-file open/close — nil if FS does not cache per-file metadata }
openFileCallback : PPOpenFileHook;
openFileModeCallback : PPOpenFileModeHook;
closeFileCallback : PPCloseFileHook;
formatParamFlags : uint32;
validateLabelCallback : PPValidateLabelHook;
@@ -354,6 +357,7 @@ const
STORAGE_VOLUME_FLAG_FORMATTING = 2;
STORAGE_VOLUME_FLAG_INVALIDATING = 4;
STORAGE_VOLUME_FLAG_REMOVED = 8;
STORAGE_VOLUME_FLAG_FREE_UNKNOWN = 16;
implementation
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Loaded 20 of 24 files, more files were not shown because too many files have changed in this diff. Show more