Artemis Milestone 2g: CBW construction and send for SCSI READ(10)

First real use of the bulk Transfer Rings Configure Endpoint wired up.
xhci_bot_send_read10() builds a 31-byte Command Block Wrapper (USB Mass
Storage Class Bulk-Only Transport spec section 5.1) and submits it as a
single Normal TRB on the bulk OUT ring via a new
xhci_bulk_out_enqueue_and_ring() helper -- a CBW is always exactly one
TRB, so unlike the EP0 helper this one rings its own doorbell rather
than leaving that to a caller assembling a group.

usb_bot_cbw_t is a real struct (every field up to the CDB array is
naturally aligned, and this driver's targets are all little-endian
already assumed everywhere else), but its DMA length is the explicit
USB_BOT_CBW_LENGTH (31) constant, never sizeof(*cbw), since the
compiler may pad the struct to 32 bytes. The SCSI READ(10) CDB itself
is written byte-by-byte since its LBA/Transfer Length fields are
big-endian on the wire, unlike everything else in this driver -- the
one place two byte orders are both live in the same function.

Completion is correlated via the existing pending_transfer_slot_id/
transfer_purpose gate (new XHCI_XFER_CBW_SENT purpose) -- no
ring-specific dispatch needed, since this driver's single-outstanding-
transfer scope already implies which ring produced an event.

This covers construction and send only (one third of a full READ(10):
CBW -> Data-In stage -> CSW) -- reading the Data-In stage and CSW
receive/validation are separate, explicitly not-yet-implemented items.

Verified live via a temporary probe (written, run once, log captured,
reverted per this project's own probe convention) -- all three
architectures, byte-identical: CBW submitted -> CBW send completed,
then a clean disconnect even with the Data-In stage never drained
(confirms no wedge on a dangling BOT transaction). Probe-free
re-verification afterward on all three architectures.

FABRIC-2.md Section X Milestone 2g's CBW checklist item marked done.
Also records a monitoring gotcha hit three times this session: `ls -t`
over the logs/ tree can return a stale leftover log from an earlier
run in the same session -- fixed going forward by reading the log path
off the actual running QEMU process's own command line instead, and a
memory note added so it doesn't recur next session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R4VMX6VSKCten8nGgaMkq4
This commit is contained in:
Robert Allan James
2026-08-25 09:07:33 -04:00
co-authored by Claude Sonnet 5
parent 92ce1f85dd
commit a88c004ecb
18 changed files with 54562 additions and 3 deletions
+96
View File
@@ -130,6 +130,8 @@ int xhci_cmd_disable_slot(xhci_dev_t *dev, uint32_t slot_id);
int xhci_cmd_address_device(xhci_dev_t *dev, uint32_t slot_id,
uint32_t port_id, uint32_t speed);
int xhci_cmd_configure_endpoint(xhci_dev_t *dev, uint32_t slot_id);
int xhci_bot_send_read10(xhci_dev_t *dev, uint32_t slot_id, uint32_t lba,
uint16_t num_blocks, uint32_t block_size);
int xhci_ep0_get_device_descriptor(xhci_dev_t *dev, uint32_t slot_id);
int xhci_ep0_get_config_descriptor(xhci_dev_t *dev, uint32_t slot_id, uint16_t length);
int xhci_ep0_set_configuration(xhci_dev_t *dev, uint32_t slot_id, uint8_t config_value);
@@ -306,6 +308,7 @@ int xhci_bringup(xhci_dev_t *dev)
dev->bulk_out_ring = NULL;
dev->bulk_out_ring_cycle = 1;
dev->bulk_out_ring_enq = 0;
dev->bot_next_tag = 1;
dev->next_action = XHCI_NEXT_ACTION_NONE;
dev->next_action_slot_id = 0;
dev->next_action_length = 0;
@@ -618,6 +621,90 @@ int xhci_cmd_configure_endpoint(xhci_dev_t *dev, uint32_t slot_id)
return 0;
}
/* Enqueue one Normal TRB to the bulk OUT Transfer Ring -- same fixed-
* ring-plus-Link-TRB wraparound pattern as xhci_ep0_enqueue_trb(),
* operating on bulk_out_ring/bulk_out_ring_enq/bulk_out_ring_cycle
* instead of the EP0 ring's fields. A CBW is always exactly one TRB
* (no Setup/Data/Status split -- that's a control-transfer-only
* concept), so unlike xhci_ep0_enqueue_trb() this rings the doorbell
* itself rather than leaving that to a caller assembling a group. */
static void xhci_bulk_out_enqueue_and_ring(xhci_dev_t *dev, uint32_t slot_id,
uint64_t parameter, uint32_t status,
uint32_t control_bits)
{
xhci_trb_t *trb = &dev->bulk_out_ring[dev->bulk_out_ring_enq];
trb->parameter = parameter;
trb->status = status;
trb->control = control_bits | (dev->bulk_out_ring_cycle ? XHCI_TRB_CONTROL_CYCLE : 0);
dev->bulk_out_ring_enq++;
if (dev->bulk_out_ring_enq == XHCI_RING_TRB_COUNT - 1) {
dev->bulk_out_ring[XHCI_RING_TRB_COUNT - 1].control =
(XHCI_TRB_TYPE_LINK << XHCI_TRB_CONTROL_TYPE_SHIFT) |
XHCI_TRB_CONTROL_TC |
(dev->bulk_out_ring_cycle ? XHCI_TRB_CONTROL_CYCLE : 0);
dev->bulk_out_ring_enq = 0;
dev->bulk_out_ring_cycle ^= 1u;
}
/* Doorbell target is the bulk OUT endpoint's own DCI, not target 1
* (EP0) -- distinct rings need distinct doorbell targets so the
* controller knows which Transfer Ring just gained a new TRB. */
dev->doorbell[slot_id] = XHCI_DB_TARGET(XHCI_EP_ADDR_TO_DCI(dev->bulk_out_ep_addr));
}
int xhci_bot_send_read10(xhci_dev_t *dev, uint32_t slot_id, uint32_t lba,
uint16_t num_blocks, uint32_t block_size)
{
if (!dev || !dev->bulk_out_ring) return -1;
if (dev->bulk_out_ep_addr == 0) return -1;
usb_bot_cbw_t *cbw = &dev->bot_cbw;
cbw->dCBWSignature = USB_BOT_CBW_SIGNATURE;
cbw->dCBWTag = dev->bot_next_tag++;
cbw->dCBWDataTransferLength = (uint32_t)num_blocks * block_size;
cbw->bmCBWFlags = USB_BOT_CBW_FLAG_DATA_IN; /* READ(10): device -> host data stage */
cbw->bCBWLUN = USB_BOT_CBW_LUN_DEFAULT;
cbw->bCBWCBLength = SCSI_CDB_LEN_READ10;
for (uint32_t i = 0; i < sizeof(cbw->CBWCB); i++) cbw->CBWCB[i] = 0;
/* SCSI READ(10) CDB (SBC-3 section 5.13): opcode, then LBA and
* Transfer Length as big-endian fields -- SCSI multi-byte fields are
* big-endian on the wire regardless of host or USB byte order, unlike
* every other multi-byte value in this driver (TRBs, contexts, CBW
* itself), which are all little-endian. Written byte-by-byte rather
* than via a struct + byte-swap helper, matching this codebase's
* existing preference for explicit field layout over struct-based
* binary formats wherever the layout isn't naturally what a C struct
* would produce (see usb_bot_cbw_t's own doc comment, and the
* Interface/Endpoint descriptor offset macros). */
cbw->CBWCB[0] = SCSI_CMD_READ10;
cbw->CBWCB[1] = 0; /* flags: no FUA/DPO/RDPROTECT for this increment */
cbw->CBWCB[2] = (uint8_t)(lba >> 24);
cbw->CBWCB[3] = (uint8_t)(lba >> 16);
cbw->CBWCB[4] = (uint8_t)(lba >> 8);
cbw->CBWCB[5] = (uint8_t)(lba);
cbw->CBWCB[6] = 0; /* group number */
cbw->CBWCB[7] = (uint8_t)(num_blocks >> 8);
cbw->CBWCB[8] = (uint8_t)(num_blocks);
cbw->CBWCB[9] = 0; /* control */
dev->transfer_purpose = XHCI_XFER_CBW_SENT;
dev->pending_transfer_slot_id = slot_id;
/* IOC set -- the CBW is always exactly one TRB, so it alone signals
* "this transfer is done" (matching the EP0 control-read pattern of
* IOC on the one TRB whose completion means something). Length is
* USB_BOT_CBW_LENGTH (31), not sizeof(*cbw) -- see usb_bot_cbw_t's
* own doc comment on why. */
xhci_bulk_out_enqueue_and_ring(dev, slot_id, (uint64_t)(uintptr_t)cbw,
USB_BOT_CBW_LENGTH,
(XHCI_TRB_TYPE_NORMAL << XHCI_TRB_CONTROL_TYPE_SHIFT) |
XHCI_TRB_CONTROL_IOC);
console_println("xhci: CBW (READ10) submitted");
return 0;
}
/* Enqueue one TRB to the EP0 Transfer Ring without ringing the doorbell
* -- Setup/Data/Status stage TRBs are enqueued as a group, then the
* doorbell is rung once after all three are posted, matching how a real
@@ -1131,6 +1218,15 @@ void xhci_poll_events(void)
console_println("xhci: device configured");
break;
}
case XHCI_XFER_CBW_SENT: {
/* CBW send is confirmed done; the Data-In
* stage and CSW receive/validation are 2g's
* next items, not implemented yet -- nothing
* further chains from here in this
* increment. */
console_println("xhci: CBW send completed");
break;
}
default:
console_println("xhci: transfer event");
break;