Carry project memory in the repo, with tools/memory-sync.sh
Claude Code keeps per-project memory at <home>/.claude/projects/<key>/,
where <key> is the project's absolute path with every separator and the
drive colon turned into a dash. That makes it both rename-fragile and
machine-specific, and this project has hit each of those once:
- D:\DEV\Schunix -> D:\DEV\SchOS changed the key, and 43 memories were
invisible until moved across by hand.
- Development happens from Windows and from Linux, and each looks in a
different place for the same checkout.
Committing .claude/memory/ fixes both -- git carries it across renames
and between hosts -- and `import` puts it wherever the local machine
expects. .claude/settings.local.json stays ignored: those are per-machine
permissions.
The colon maps to a dash rather than being dropped. Deleting it gives
D-DEV-SchOS, which matches nothing and silently finds zero memories; that
was the first attempt. On Git Bash the key comes from `cygpath -w`,
because Claude Code is a Windows program and sees D:\DEV\SchOS, not
/d/DEV/SchOS.
This commit is contained in:
parent
cda5908660
commit
3182da3965
47 changed files with 2810 additions and 0 deletions
118
.claude/memory/schunix-usb-xhci.md
Normal file
118
.claude/memory/schunix-usb-xhci.md
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
---
|
||||
name: schunix-usb-xhci
|
||||
description: "xHCI enumerates USB2 and USB3 devices and does control transfers; written from the spec because porting BSD's stack means porting newbus first"
|
||||
metadata:
|
||||
node_type: memory
|
||||
type: project
|
||||
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
||||
modified: 2026-08-04T23:06:09.296Z
|
||||
---
|
||||
|
||||
**`kernel/drivers/xhci.c`, 2026-08-04.** Working: controller reset, DCBAA,
|
||||
command ring, event ring + ERST, scratchpad buffers, port reset, Enable Slot,
|
||||
Address Device, and control transfers. Three QEMU devices (usb-kbd, usb-mouse,
|
||||
usb-storage) enumerate and answer `GET_DESCRIPTOR`.
|
||||
|
||||
**Written from the xHCI 1.2 spec, not ported from BSD** — and that was the
|
||||
right call, not laziness. FreeBSD's `sys/dev/usb` is ~100k lines welded to
|
||||
newbus, devclass, `bus_dma`, callouts and taskqueues; porting it means porting
|
||||
FreeBSD's device framework first. BSD is worth far more as the reference you
|
||||
check yourself against. (Licensing agrees: BSD is Apache-compatible, Linux is
|
||||
not — [[schunix-licensing]].)
|
||||
|
||||
**Things that would cost hours to rediscover:**
|
||||
|
||||
- **Enumeration polls the event ring on purpose.** `pci_bind_all()` runs from
|
||||
`kmain()` with interrupts off and no scheduler; a probe that waited for an
|
||||
interrupt would wait forever.
|
||||
- **Context size is 32 *or* 64 bytes**, per `HCCPARAMS1.CSZ`. Hardcoding 32 on
|
||||
a 64-byte controller puts every field at half the right offset and commands
|
||||
are rejected with no explanation.
|
||||
- **PORTSC's change bits and PED are write-1-to-clear.** Writing back a value
|
||||
you just read clears whatever happened. Mask `PORT_RW1C` off every write.
|
||||
- **USB3 ports arrive already enabled; USB2 ports need `PORT_PR` first.**
|
||||
Check PED rather than trying to work out which is which.
|
||||
- **Full-speed devices' EP0 max packet size is unknowable until you ask.**
|
||||
Start at 8, read 8 bytes of the device descriptor, then Evaluate Context.
|
||||
- The link TRB's Toggle Cycle bit is what makes a fixed ring endless; set the
|
||||
cycle bit *last* when pushing a TRB.
|
||||
- A No-Op command before enumerating proves the ring and event path, so a
|
||||
failure points at our plumbing rather than at a device.
|
||||
|
||||
**The USB core and HID landed 2026-08-05.** `kernel/drivers/usb.c` reads the
|
||||
configuration, selects it, and offers the device to a class-driver table in
|
||||
`registry.c` (same one-table-you-can-read arrangement as the PCI drivers);
|
||||
`kernel/drivers/usb_hid.c` does boot-protocol keyboard and mouse. A command
|
||||
typed on an emulated USB keyboard runs in the shell.
|
||||
|
||||
- **Boot protocol, not report descriptors.** A device claiming the boot
|
||||
subclass promises a fixed 8-byte keyboard / 3-byte mouse layout, so no
|
||||
report-descriptor parser is needed. It still starts in *report* protocol --
|
||||
`SET_PROTOCOL(0)` is required or the reports arrive in an unknown shape.
|
||||
- `SET_IDLE 0` too, or a keyboard resends its state every few ms forever.
|
||||
- The keyboard report is a *state*, not an event: a held key appears in every
|
||||
report. Emit only usages absent from the previous report.
|
||||
- Characters go into a small queue that `consoleintr()` then drains through
|
||||
its getc argument -- that reuses the whole line discipline instead of
|
||||
reimplementing it. `mouse_inject()` in mouse.c is the same idea for the
|
||||
pointer; HID's Y axis points down and PS/2's points up.
|
||||
- A configuration descriptor is a *chain* of descriptors of varying types.
|
||||
Walk it by each one's length byte; class-specific ones you have never heard
|
||||
of sit in the middle and must be skipped, not tripped over.
|
||||
|
||||
**The interrupt bug that cost the most time is in
|
||||
[[schunix-driver-infrastructure]]** -- transfers completed perfectly, events
|
||||
sat in the ring, and the IRQ count stayed at zero.
|
||||
|
||||
**Mass storage landed 2026-08-05** (`kernel/drivers/usb_storage.c`): bulk
|
||||
endpoints, bulk-only transport, SCSI INQUIRY / READ CAPACITY / READ(10) /
|
||||
WRITE(10), and a block device at `USBDISK_DEV` (3) routed from `bdevrw()` in
|
||||
bio.c. A real superblock reads back through `bread()`.
|
||||
|
||||
- **A bulk transfer at probe time must not sleep.** This wedged the machine
|
||||
mid-enumeration with no message at all. `pci_bind_all()` runs from
|
||||
`kmain()` with interrupts off and *before* `pinit()` — so `sleep()` has no
|
||||
scheduler to switch to and `wakeup()` would take an uninitialised ptable
|
||||
lock. `struct xhci` has a `can_sleep` flag, set once probing is over; until
|
||||
then bulk transfers poll the event ring, exactly as enumeration does.
|
||||
- Bulk-only transport is strictly three phases: 31-byte CBW out, data,
|
||||
13-byte CSW in. **The CSW is not optional** — skipping it leaves the
|
||||
endpoint out of phase and every later command fails.
|
||||
- SCSI is big-endian everywhere, and READ CAPACITY returns the address of the
|
||||
*last* sector, not a count.
|
||||
- No GET MAX LUN: single-LUN devices answer it with a STALL, and recovering
|
||||
from an EP0 stall needs a Reset Endpoint command that is not written yet.
|
||||
- `b->data` lives in the buffer cache, not DMA memory, so everything bounces
|
||||
through one `dma_alloc`'d block.
|
||||
|
||||
**Bugs found in a deliberate audit pass (2026-08-05), all fixed:**
|
||||
|
||||
- **Context Entries must be the highest DCI in use, not the one being added.**
|
||||
Writing the new endpoint's own DCI works only while endpoints are opened in
|
||||
increasing order; open a higher DCI then a lower one and the field shrinks,
|
||||
silently invalidating the endpoint that already worked. `xhci_open_ep`
|
||||
tracks `slot[].max_dci` now, and `usb_storage.c` deliberately opens bulk
|
||||
OUT before bulk IN — the formerly-broken order — so the fix stays tested.
|
||||
- **The event ring had two unlocked consumers**: `xhci_irq` and the sysrq
|
||||
dump. Both take `x->lock` now; `transfer_done` runs with it held.
|
||||
- **A failed block read left B_VALID clear and returned the stale buffer**,
|
||||
which `bread()` hands straight to its caller as though it were disk
|
||||
content. Three retries, then panic.
|
||||
- **`bot_command` skipped the CSW when the data phase failed**, leaving the
|
||||
device holding a status nobody asked for — so the *next* command's data
|
||||
phase would receive this one's status, turning one failed read into every
|
||||
later read failing. The CSW is now always read.
|
||||
- **The shared descriptor scratch buffer was not cleared between devices**, so
|
||||
a short reply left the previous device's descriptor visible past the end of
|
||||
its own and the parser walked into it.
|
||||
- Max ESIT Payload was being set for bulk endpoints; it is periodic-only.
|
||||
- `irq_unregister` compacted a chain a dispatcher could be walking, and the
|
||||
comment above the lock claimed nothing was ever removed. It masks the line
|
||||
for the rewrite now.
|
||||
|
||||
**Two debug commands:** `echo u > /proc/sysrq` dumps controller state and
|
||||
drains the event ring by hand — that is what separates "no event" from
|
||||
"event, no interrupt". `echo s > /proc/sysrq` reads block 1 off the USB disk
|
||||
through the buffer cache.
|
||||
|
||||
**Still not done:** hubs. See `docs/roadmap.md` phase 1.
|
||||
Loading…
Reference in a new issue