usb: plug and play -- hot-plug, unplug, every interface, real mice
Devices can now arrive and leave after boot. A "usbhotplug" kernel thread handles root-port changes, which it is told about by Port Status Change events and also checks for every quarter second. It also handles changes behind hubs, which each hub reports on its status-change endpoint. An unplugged device has its endpoints closed and any transfer in flight failed. Then its drivers' disconnect hooks run, the slot is disabled and its memory is freed. Everything behind a hub that goes goes with it. This needed the interrupt handler to file command and endpoint-0 completions for their waiters, so that enumeration can run while keyboards interrupt. On real hardware: the controller is taken from the firmware through USB Legacy Support before reset, with SMIs off. On Intel 7/8/9 series parts the ports are switched over from EHCI, which is where firmware leaves them. Every interface of a device is offered to the class drivers, not just the first, so a receiver's mouse on interface 1 gets a driver. Configure Endpoint now copies the slot context from the controller. Before, it rebuilt it and lost the route string and TT fields of devices behind hubs. HID: pointers are read in report protocol from their report descriptor. That covers buttons, X/Y and wheel, relative or absolute, and report IDs. Boot protocol is kept as the fallback for mice and for keyboards. Non-pointer HID interfaces are declined rather than treated as tablets. USB keyboards get software key repeat. Mass storage handles up to four sticks, each unregistered from the block layer when it is pulled out, along with its cached blocks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HH7xukt4aaiGj1nQEAoQf1
This commit is contained in:
parent
c1e52e172f
commit
fa7669254e
13 changed files with 1367 additions and 240 deletions
|
|
@ -61,9 +61,12 @@ struct csw {
|
|||
|
||||
#define SECTOR_SIZE 512
|
||||
|
||||
static struct {
|
||||
// One per stick. A desk with a stick in each port is a reasonable thing to
|
||||
// have, and the first one to arrive is the one the fixed USBDISK_DEV means.
|
||||
struct ums {
|
||||
struct usb_dev *dev;
|
||||
int present;
|
||||
int gone; // unplugged; fail everything
|
||||
uint32 nsectors;
|
||||
uint32 sector_size;
|
||||
uint32 tag;
|
||||
|
|
@ -71,7 +74,23 @@ static struct {
|
|||
struct cbw *cbw; // allocated once, not per command
|
||||
struct csw *csw;
|
||||
struct sleeplock lock; // one command at a time
|
||||
} ums;
|
||||
char name[8]; // usb0, usb1, ...
|
||||
int blkdev; // its number in the registry, or -1
|
||||
};
|
||||
|
||||
#define NUMS 4
|
||||
static struct ums disks[NUMS];
|
||||
|
||||
// What USBDISK_DEV (3) means: the first stick still present. It is the whole
|
||||
// medium, which is how a stick holding a bare SchFS volume is mounted.
|
||||
static struct ums *
|
||||
legacy(void)
|
||||
{
|
||||
for(int i = 0; i < NUMS; i++)
|
||||
if(disks[i].present && !disks[i].gone)
|
||||
return &disks[i];
|
||||
return 0;
|
||||
}
|
||||
|
||||
// SCSI is big-endian, everywhere, for everything. x86 is not.
|
||||
static void
|
||||
|
|
@ -92,12 +111,13 @@ rd_be32(const uint8 *p)
|
|||
// `buf`, when there is data, must be DMA memory -- it is handed to the
|
||||
// controller as a physical address.
|
||||
static int
|
||||
bot_command(const uint8 *cdb, int cdb_len, void *buf, int len, int in)
|
||||
bot_command(struct ums *u, const uint8 *cdb, int cdb_len, void *buf, int len,
|
||||
int in)
|
||||
{
|
||||
struct usb_dev *dev = ums.dev;
|
||||
struct cbw *cbw = ums.cbw;
|
||||
struct csw *csw = ums.csw;
|
||||
uint32 tag = ++ums.tag;
|
||||
struct usb_dev *dev = u->dev;
|
||||
struct cbw *cbw = u->cbw;
|
||||
struct csw *csw = u->csw;
|
||||
uint32 tag = ++u->tag;
|
||||
int i, data_ok = 1;
|
||||
|
||||
memset(cbw, 0, sizeof(*cbw));
|
||||
|
|
@ -160,13 +180,13 @@ bot_command(const uint8 *cdb, int cdb_len, void *buf, int len, int in)
|
|||
}
|
||||
|
||||
static int
|
||||
scsi_inquiry(char *vendor, char *product)
|
||||
scsi_inquiry(struct ums *u, char *vendor, char *product)
|
||||
{
|
||||
uint8 cdb[6] = { SCSI_INQUIRY, 0, 0, 0, 36, 0 };
|
||||
uint8 *r = ums.xfer;
|
||||
uint8 *r = u->xfer;
|
||||
int i;
|
||||
|
||||
if(bot_command(cdb, sizeof(cdb), r, 36, 1) < 0)
|
||||
if(bot_command(u, cdb, sizeof(cdb), r, 36, 1) < 0)
|
||||
return -1;
|
||||
|
||||
// Bytes 8-15 are the vendor, 16-31 the product, space padded and not
|
||||
|
|
@ -181,22 +201,22 @@ scsi_inquiry(char *vendor, char *product)
|
|||
}
|
||||
|
||||
static int
|
||||
scsi_read_capacity(void)
|
||||
scsi_read_capacity(struct ums *u)
|
||||
{
|
||||
uint8 cdb[10] = { SCSI_READ_CAPACITY10, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
uint8 *r = ums.xfer;
|
||||
uint8 *r = u->xfer;
|
||||
|
||||
if(bot_command(cdb, sizeof(cdb), r, 8, 1) < 0)
|
||||
if(bot_command(u, cdb, sizeof(cdb), r, 8, 1) < 0)
|
||||
return -1;
|
||||
|
||||
// The first word is the address of the *last* sector, not the count.
|
||||
ums.nsectors = rd_be32(r) + 1;
|
||||
ums.sector_size = rd_be32(r + 4);
|
||||
u->nsectors = rd_be32(r) + 1;
|
||||
u->sector_size = rd_be32(r + 4);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
scsi_rw(uint32 lba, int count, void *buf, int write)
|
||||
scsi_rw(struct ums *u, uint32 lba, int count, void *buf, int write)
|
||||
{
|
||||
uint8 cdb[10];
|
||||
|
||||
|
|
@ -206,7 +226,7 @@ scsi_rw(uint32 lba, int count, void *buf, int write)
|
|||
cdb[7] = (count >> 8) & 0xFF;
|
||||
cdb[8] = count & 0xFF;
|
||||
|
||||
return bot_command(cdb, sizeof(cdb), buf, count * SECTOR_SIZE, !write);
|
||||
return bot_command(u, cdb, sizeof(cdb), buf, count * SECTOR_SIZE, !write);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -216,7 +236,7 @@ scsi_rw(uint32 lba, int count, void *buf, int write)
|
|||
int
|
||||
usb_storage_present(void)
|
||||
{
|
||||
return ums.present;
|
||||
return legacy() != 0;
|
||||
}
|
||||
|
||||
// Sync one buffer-cache block with the stick. Same contract as iderw() and
|
||||
|
|
@ -229,32 +249,33 @@ usb_storage_rw(struct buf *b)
|
|||
uint32 lba = b->blockno * per_block;
|
||||
int write = (b->flags & B_DIRTY) != 0;
|
||||
int i, n = -1;
|
||||
struct ums *u = legacy();
|
||||
|
||||
if(!holdingsleep(&b->lock))
|
||||
panic("usb_storage_rw: buf not locked");
|
||||
if(!ums.present)
|
||||
if(u == 0)
|
||||
panic("usb_storage_rw: no USB disk");
|
||||
if(lba + per_block > ums.nsectors)
|
||||
if(lba + per_block > u->nsectors)
|
||||
panic("usb_storage_rw: block out of range");
|
||||
|
||||
// One command at a time. Bulk-only transport has no tagging beyond the CBW
|
||||
// tag and no queueing: a second command sent while the first is mid-phase
|
||||
// desynchronises the endpoint and every command after it fails.
|
||||
acquiresleep(&ums.lock);
|
||||
acquiresleep(&u->lock);
|
||||
|
||||
// Removable media fails transiently in ways a fixed disk does not, so a
|
||||
// couple of retries are worth having before giving up.
|
||||
for(i = 0; i < 3; i++){
|
||||
if(write)
|
||||
memmove(ums.xfer, b->data, BSIZE);
|
||||
n = scsi_rw(lba, per_block, ums.xfer, write);
|
||||
memmove(u->xfer, b->data, BSIZE);
|
||||
n = scsi_rw(u, lba, per_block, u->xfer, write);
|
||||
if(n == BSIZE)
|
||||
break;
|
||||
cprintf("usb-storage: %s of block %d failed, retrying\n",
|
||||
write ? "write" : "read", b->blockno);
|
||||
}
|
||||
|
||||
releasesleep(&ums.lock);
|
||||
releasesleep(&u->lock);
|
||||
|
||||
// A read that failed must not return quietly. bread() hands the buffer
|
||||
// straight back to its caller, which has no way to ask whether it worked --
|
||||
|
|
@ -265,7 +286,7 @@ usb_storage_rw(struct buf *b)
|
|||
panic("usb_storage_rw: transfer failed");
|
||||
|
||||
if(!write)
|
||||
memmove(b->data, ums.xfer, BSIZE);
|
||||
memmove(b->data, u->xfer, BSIZE);
|
||||
b->flags |= B_VALID;
|
||||
b->flags &= ~B_DIRTY;
|
||||
}
|
||||
|
|
@ -280,21 +301,18 @@ usb_storage_rw(struct buf *b)
|
|||
// than ignored, because the caller is about to write an uberblock on the
|
||||
// strength of it -- and a barrier that silently does nothing is worse than no
|
||||
// barrier at all, since it makes an unsafe volume look safe.
|
||||
int
|
||||
usb_storage_flush(void)
|
||||
static int
|
||||
ums_flush(struct ums *u)
|
||||
{
|
||||
uint8 cdb[10];
|
||||
int r;
|
||||
|
||||
if(!ums.present)
|
||||
return 0;
|
||||
|
||||
memset(cdb, 0, sizeof(cdb));
|
||||
cdb[0] = SCSI_SYNC_CACHE10; // LBA 0, count 0: the entire medium
|
||||
|
||||
acquiresleep(&ums.lock);
|
||||
r = bot_command(cdb, sizeof(cdb), 0, 0, 0);
|
||||
releasesleep(&ums.lock);
|
||||
acquiresleep(&u->lock);
|
||||
r = u->gone ? -1 : bot_command(u, cdb, sizeof(cdb), 0, 0, 0);
|
||||
releasesleep(&u->lock);
|
||||
|
||||
if(r < 0){
|
||||
cprintf("usb-storage: SYNCHRONIZE CACHE failed; writes are not durable\n");
|
||||
|
|
@ -303,6 +321,13 @@ usb_storage_flush(void)
|
|||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
usb_storage_flush(void)
|
||||
{
|
||||
struct ums *u = legacy();
|
||||
return u ? ums_flush(u) : 0;
|
||||
}
|
||||
|
||||
// Read block 1 through the ordinary buffer cache and report what is there.
|
||||
//
|
||||
// This is the only test that proves the whole path rather than a piece of it:
|
||||
|
|
@ -317,14 +342,15 @@ usb_storage_dump(void)
|
|||
{
|
||||
struct buf *b;
|
||||
struct superblock *sb;
|
||||
struct ums *u = legacy();
|
||||
|
||||
if(!ums.present){
|
||||
if(u == 0){
|
||||
cprintf("usb-storage: no disk\n");
|
||||
return;
|
||||
}
|
||||
|
||||
cprintf("usb-storage: %d sectors of %d bytes\n", ums.nsectors,
|
||||
ums.sector_size);
|
||||
cprintf("usb-storage: %d sectors of %d bytes\n", u->nsectors,
|
||||
u->sector_size);
|
||||
|
||||
b = bread(USBDISK_DEV, 1);
|
||||
sb = (struct superblock*)b->data;
|
||||
|
|
@ -341,25 +367,28 @@ usb_storage_dump(void)
|
|||
static int
|
||||
ums_blk_rw(void *priv, uint64 lba, uint32 count, void *buf, int write)
|
||||
{
|
||||
struct ums *u = priv;
|
||||
int n;
|
||||
(void)priv;
|
||||
if(count * SECTOR_SIZE > BSIZE || lba + count > ums.nsectors)
|
||||
if(count * SECTOR_SIZE > BSIZE || lba + count > u->nsectors)
|
||||
return -1;
|
||||
acquiresleep(&ums.lock);
|
||||
acquiresleep(&u->lock);
|
||||
if(u->gone){
|
||||
releasesleep(&u->lock);
|
||||
return -1;
|
||||
}
|
||||
if(write)
|
||||
memmove(ums.xfer, buf, count * SECTOR_SIZE);
|
||||
n = scsi_rw((uint32)lba, count, ums.xfer, write);
|
||||
memmove(u->xfer, buf, count * SECTOR_SIZE);
|
||||
n = scsi_rw(u, (uint32)lba, count, u->xfer, write);
|
||||
if(n == (int)(count * SECTOR_SIZE) && !write)
|
||||
memmove(buf, ums.xfer, count * SECTOR_SIZE);
|
||||
releasesleep(&ums.lock);
|
||||
memmove(buf, u->xfer, count * SECTOR_SIZE);
|
||||
releasesleep(&u->lock);
|
||||
return n == (int)(count * SECTOR_SIZE) ? 0 : -1;
|
||||
}
|
||||
|
||||
static int
|
||||
ums_blk_flush(void *priv)
|
||||
{
|
||||
(void)priv;
|
||||
return usb_storage_flush();
|
||||
return ums_flush(priv);
|
||||
}
|
||||
|
||||
static const struct blkdev_ops ums_blk_ops = {
|
||||
|
|
@ -373,28 +402,40 @@ static int
|
|||
storage_probe(struct usb_dev *dev)
|
||||
{
|
||||
char vendor[9], product[17];
|
||||
struct ums *u = 0;
|
||||
int i;
|
||||
|
||||
if(ums.present)
|
||||
return -1; // one disk is enough for now
|
||||
for(i = 0; i < NUMS; i++)
|
||||
if(!disks[i].present && disks[i].dev == 0){ u = &disks[i]; break; }
|
||||
if(u == 0){
|
||||
cprintf("usb-storage: %d sticks already; this one is not used\n", NUMS);
|
||||
return -1;
|
||||
}
|
||||
if(!dev->has_bulk_in || !dev->has_bulk_out){
|
||||
cprintf("usb-storage: needs a bulk endpoint in each direction\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
initsleeplock(&ums.lock, "usbms");
|
||||
ums.dev = dev;
|
||||
initsleeplock(&u->lock, "usbms");
|
||||
u->dev = dev;
|
||||
u->gone = 0;
|
||||
u->blkdev = -1;
|
||||
// The name is the slot's, not a count: pull usb0 out and put it back and
|
||||
// it comes back as usb0.
|
||||
safestrcpy(u->name, "usb0", sizeof(u->name));
|
||||
u->name[3] = '0' + (u - disks);
|
||||
|
||||
// One block, used for every transfer. bread() hands us b->data, which lives
|
||||
// in the buffer cache rather than in DMA memory, so everything bounces
|
||||
// through here. The wrappers are allocated once too: they are used on every
|
||||
// single I/O, and churning the DMA extent list twice per block read would
|
||||
// fragment an arena that has no business fragmenting.
|
||||
ums.xfer = dma_alloc(BSIZE, 4096);
|
||||
ums.cbw = dma_alloc(sizeof(struct cbw), 64);
|
||||
ums.csw = dma_alloc(sizeof(struct csw), 64);
|
||||
if(ums.xfer == 0 || ums.cbw == 0 || ums.csw == 0)
|
||||
u->xfer = dma_alloc(BSIZE, 4096);
|
||||
u->cbw = dma_alloc(sizeof(struct cbw), 64);
|
||||
u->csw = dma_alloc(sizeof(struct csw), 64);
|
||||
if(u->xfer == 0 || u->cbw == 0 || u->csw == 0)
|
||||
goto fail;
|
||||
dev->drvdata = u;
|
||||
|
||||
// Deliberately OUT first, which is the order that used to break.
|
||||
//
|
||||
|
|
@ -418,46 +459,82 @@ storage_probe(struct usb_dev *dev)
|
|||
// first command or two while it spins up or reads its own media.
|
||||
for(i = 0; i < 5; i++){
|
||||
uint8 cdb[6] = { SCSI_TEST_UNIT_READY, 0, 0, 0, 0, 0 };
|
||||
if(bot_command(cdb, sizeof(cdb), 0, 0, 0) >= 0)
|
||||
if(bot_command(u, cdb, sizeof(cdb), 0, 0, 0) >= 0)
|
||||
break;
|
||||
microdelay(100000);
|
||||
}
|
||||
|
||||
if(scsi_inquiry(vendor, product) < 0)
|
||||
if(scsi_inquiry(u, vendor, product) < 0)
|
||||
goto fail;
|
||||
if(scsi_read_capacity() < 0)
|
||||
if(scsi_read_capacity(u) < 0)
|
||||
goto fail;
|
||||
|
||||
if(ums.sector_size != SECTOR_SIZE){
|
||||
if(u->sector_size != SECTOR_SIZE){
|
||||
cprintf("usb-storage: %d-byte sectors are not supported\n",
|
||||
ums.sector_size);
|
||||
u->sector_size);
|
||||
goto fail;
|
||||
}
|
||||
if(ums.nsectors == 0){
|
||||
if(u->nsectors == 0){
|
||||
cprintf("usb-storage: device reports no capacity\n");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
ums.present = 1;
|
||||
cprintf("usb-storage: %s %s, %d sectors (%d MB)\n",
|
||||
vendor, product, ums.nsectors,
|
||||
(int)((uint64)ums.nsectors * SECTOR_SIZE / (1024 * 1024)));
|
||||
blkdev_register("usb0", &ums_blk_ops, 0, ums.nsectors, SECTOR_SIZE);
|
||||
u->present = 1;
|
||||
cprintf("usb-storage: %s: %s %s, %d sectors (%d MB)\n", u->name,
|
||||
vendor, product, u->nsectors,
|
||||
(int)((uint64)u->nsectors * SECTOR_SIZE / (1024 * 1024)));
|
||||
u->blkdev = blkdev_register(u->name, &ums_blk_ops, u, u->nsectors,
|
||||
SECTOR_SIZE);
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
// Give the memory back and forget the device. Leaving ums.dev set would let
|
||||
// Give the memory back and forget the device. Leaving u->dev set would let
|
||||
// a later block read reach a device we decided we could not drive.
|
||||
if(ums.xfer) dma_free(ums.xfer, BSIZE);
|
||||
if(ums.cbw) dma_free(ums.cbw, sizeof(struct cbw));
|
||||
if(ums.csw) dma_free(ums.csw, sizeof(struct csw));
|
||||
ums.xfer = 0;
|
||||
ums.cbw = 0;
|
||||
ums.csw = 0;
|
||||
ums.dev = 0;
|
||||
if(u->xfer) dma_free(u->xfer, BSIZE);
|
||||
if(u->cbw) dma_free(u->cbw, sizeof(struct cbw));
|
||||
if(u->csw) dma_free(u->csw, sizeof(struct csw));
|
||||
u->xfer = 0;
|
||||
u->cbw = 0;
|
||||
u->csw = 0;
|
||||
u->dev = 0;
|
||||
dev->drvdata = 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Pulled out. The controller has already failed whatever transfer was in
|
||||
// flight, so the lock comes free; from then on every request fails at once,
|
||||
// and the registry forgets the disk and its partitions. Mounted from, it
|
||||
// fails the filesystem's next read -- which panics, by the rule in
|
||||
// blkdev_rw(), because a filesystem that carried on would be guessing.
|
||||
static void
|
||||
storage_disconnect(struct usb_dev *dev)
|
||||
{
|
||||
struct ums *u = dev->drvdata;
|
||||
|
||||
if(u == 0)
|
||||
return;
|
||||
acquiresleep(&u->lock);
|
||||
u->gone = 1;
|
||||
releasesleep(&u->lock);
|
||||
|
||||
cprintf("usb-storage: %s unplugged\n", u->name);
|
||||
if(u->blkdev >= 0)
|
||||
blkdev_unregister(u->blkdev);
|
||||
// USBDISK_DEV may have been this stick; nothing it cached is true now.
|
||||
binval(USBDISK_DEV);
|
||||
|
||||
dma_free(u->xfer, BSIZE);
|
||||
dma_free(u->cbw, sizeof(struct cbw));
|
||||
dma_free(u->csw, sizeof(struct csw));
|
||||
u->xfer = 0;
|
||||
u->cbw = 0;
|
||||
u->csw = 0;
|
||||
u->present = 0;
|
||||
u->blkdev = -1;
|
||||
u->dev = 0;
|
||||
dev->drvdata = 0;
|
||||
}
|
||||
|
||||
const struct usb_driver usb_storage_driver = {
|
||||
.name = "usb-storage",
|
||||
.class = USB_CLASS_MASS_STORAGE,
|
||||
|
|
@ -465,4 +542,5 @@ const struct usb_driver usb_storage_driver = {
|
|||
.protocol = USB_MSC_PROTO_BOT,
|
||||
.devclass = DEVCLASS_STORAGE,
|
||||
.probe = storage_probe,
|
||||
.disconnect = storage_disconnect,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue