Initial commit: Schunix as it stands before the SchOS rename
An x86-64 operating system built from xv6, now largely rewritten: SMP, a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF rasteriser, and a Linux-numbered syscall ABI. No history before this point -- the tree was never under version control. This commit exists so the rename that follows has something to undo to.
This commit is contained in:
commit
d0f7acf9eb
275 changed files with 51417 additions and 0 deletions
395
kernel/drivers/usb/storage/usb_storage.c
Normal file
395
kernel/drivers/usb/storage/usb_storage.c
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
// USB mass storage: bulk-only transport, SCSI transparent command set.
|
||||
//
|
||||
// Almost every USB stick, external drive and card reader is this, and the
|
||||
// name is a fair description of the whole protocol: there are exactly two
|
||||
// bulk endpoints and everything happens by sending three things over them.
|
||||
//
|
||||
// command a 31-byte wrapper (CBW) carrying a SCSI command block, OUT
|
||||
// data the payload, IN or OUT, if the command has one
|
||||
// status a 13-byte wrapper (CSW) saying whether it worked, IN
|
||||
//
|
||||
// That is the entire transport. The interesting content is SCSI, which is
|
||||
// what the wrapper carries and what the device actually speaks -- so a USB
|
||||
// stick is a SCSI disk that happens to be reached over USB, and the commands
|
||||
// below (INQUIRY, READ CAPACITY, READ(10), WRITE(10)) are the same ones a
|
||||
// SCSI drive answered in 1990.
|
||||
|
||||
#include "types.h"
|
||||
#include "defs.h"
|
||||
#include "param.h"
|
||||
#include "memlayout.h"
|
||||
#include "mmu.h"
|
||||
#include "spinlock.h"
|
||||
#include "sleeplock.h"
|
||||
#include "fs.h"
|
||||
#include "buf.h"
|
||||
#include "dma.h"
|
||||
#include "usb.h"
|
||||
|
||||
#define CBW_SIGNATURE 0x43425355 // "USBC"
|
||||
#define CSW_SIGNATURE 0x53425355 // "USBS"
|
||||
|
||||
struct cbw {
|
||||
uint32 signature;
|
||||
uint32 tag;
|
||||
uint32 data_len;
|
||||
uint8 flags; // 0x80 = device-to-host
|
||||
uint8 lun;
|
||||
uint8 cb_len; // 1..16
|
||||
uint8 cb[16];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct csw {
|
||||
uint32 signature;
|
||||
uint32 tag;
|
||||
uint32 residue;
|
||||
uint8 status; // 0 = passed, 1 = failed, 2 = phase error
|
||||
} __attribute__((packed));
|
||||
|
||||
// SCSI opcodes
|
||||
#define SCSI_TEST_UNIT_READY 0x00
|
||||
#define SCSI_REQUEST_SENSE 0x03
|
||||
#define SCSI_INQUIRY 0x12
|
||||
#define SCSI_READ_CAPACITY10 0x25
|
||||
#define SCSI_READ10 0x28
|
||||
#define SCSI_WRITE10 0x2A
|
||||
|
||||
#define SECTOR_SIZE 512
|
||||
|
||||
static struct {
|
||||
struct usb_dev *dev;
|
||||
int present;
|
||||
uint32 nsectors;
|
||||
uint32 sector_size;
|
||||
uint32 tag;
|
||||
void *xfer; // DMA bounce buffer, one block
|
||||
struct cbw *cbw; // allocated once, not per command
|
||||
struct csw *csw;
|
||||
struct sleeplock lock; // one command at a time
|
||||
} ums;
|
||||
|
||||
// SCSI is big-endian, everywhere, for everything. x86 is not.
|
||||
static void
|
||||
be32(uint8 *p, uint32 v)
|
||||
{
|
||||
p[0] = v >> 24; p[1] = v >> 16; p[2] = v >> 8; p[3] = v;
|
||||
}
|
||||
|
||||
static uint32
|
||||
rd_be32(const uint8 *p)
|
||||
{
|
||||
return ((uint32)p[0] << 24) | ((uint32)p[1] << 16) |
|
||||
((uint32)p[2] << 8) | p[3];
|
||||
}
|
||||
|
||||
// One complete command-data-status exchange.
|
||||
//
|
||||
// `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)
|
||||
{
|
||||
struct usb_dev *dev = ums.dev;
|
||||
struct cbw *cbw = ums.cbw;
|
||||
struct csw *csw = ums.csw;
|
||||
uint32 tag = ++ums.tag;
|
||||
int i, data_ok = 1;
|
||||
|
||||
memset(cbw, 0, sizeof(*cbw));
|
||||
cbw->signature = CBW_SIGNATURE;
|
||||
cbw->tag = tag;
|
||||
cbw->data_len = len;
|
||||
cbw->flags = in ? 0x80 : 0x00;
|
||||
cbw->lun = 0;
|
||||
cbw->cb_len = cdb_len;
|
||||
for(i = 0; i < cdb_len && i < 16; i++)
|
||||
cbw->cb[i] = cdb[i];
|
||||
|
||||
if(dev->ops->bulk(dev, dev->bulk_out.bEndpointAddress, cbw,
|
||||
sizeof(*cbw)) != (int)sizeof(*cbw)){
|
||||
cprintf("usb-storage: the device would not take the command\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(len > 0){
|
||||
int addr = in ? dev->bulk_in.bEndpointAddress
|
||||
: dev->bulk_out.bEndpointAddress;
|
||||
if(dev->ops->bulk(dev, addr, buf, len) < 0)
|
||||
data_ok = 0;
|
||||
}
|
||||
|
||||
// The status wrapper is not optional and not advisory: a device that has
|
||||
// sent its data is still waiting to be asked how it went.
|
||||
//
|
||||
// It is read even when the data phase failed, which is the whole point --
|
||||
// returning early there would leave the device still holding a status it
|
||||
// has not been asked for, so the *next* command's data phase would receive
|
||||
// this command's status instead. One failed read would turn into every
|
||||
// subsequent read failing, which is a far worse fault than the one that
|
||||
// started it, and one that looks nothing like its cause.
|
||||
memset(csw, 0, sizeof(*csw));
|
||||
if(dev->ops->bulk(dev, dev->bulk_in.bEndpointAddress, csw,
|
||||
sizeof(*csw)) != (int)sizeof(*csw)){
|
||||
cprintf("usb-storage: no status returned for command 0x%x\n", cdb[0]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(csw->signature != CSW_SIGNATURE || csw->tag != tag){
|
||||
cprintf("usb-storage: status wrapper does not match the command\n");
|
||||
return -1;
|
||||
}
|
||||
if(csw->status != 0){
|
||||
cprintf("usb-storage: command 0x%x failed (status %d)\n",
|
||||
cdb[0], csw->status);
|
||||
return -1;
|
||||
}
|
||||
if(!data_ok)
|
||||
return -1;
|
||||
|
||||
// Residue is what was *not* transferred, and a device is allowed to report
|
||||
// more than was asked for only if it is broken -- clamp rather than return
|
||||
// a negative length that a caller would read as an error code.
|
||||
if(csw->residue > (uint32)len)
|
||||
return -1;
|
||||
return len - (int)csw->residue;
|
||||
}
|
||||
|
||||
static int
|
||||
scsi_inquiry(char *vendor, char *product)
|
||||
{
|
||||
uint8 cdb[6] = { SCSI_INQUIRY, 0, 0, 0, 36, 0 };
|
||||
uint8 *r = ums.xfer;
|
||||
int i;
|
||||
|
||||
if(bot_command(cdb, sizeof(cdb), r, 36, 1) < 0)
|
||||
return -1;
|
||||
|
||||
// Bytes 8-15 are the vendor, 16-31 the product, space padded and not
|
||||
// NUL-terminated -- the only fixed-width strings left in computing.
|
||||
for(i = 0; i < 8; i++)
|
||||
vendor[i] = r[8 + i];
|
||||
vendor[8] = 0;
|
||||
for(i = 0; i < 16; i++)
|
||||
product[i] = r[16 + i];
|
||||
product[16] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
scsi_read_capacity(void)
|
||||
{
|
||||
uint8 cdb[10] = { SCSI_READ_CAPACITY10, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
uint8 *r = ums.xfer;
|
||||
|
||||
if(bot_command(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);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
scsi_rw(uint32 lba, int count, void *buf, int write)
|
||||
{
|
||||
uint8 cdb[10];
|
||||
|
||||
memset(cdb, 0, sizeof(cdb));
|
||||
cdb[0] = write ? SCSI_WRITE10 : SCSI_READ10;
|
||||
be32(&cdb[2], lba);
|
||||
cdb[7] = (count >> 8) & 0xFF;
|
||||
cdb[8] = count & 0xFF;
|
||||
|
||||
return bot_command(cdb, sizeof(cdb), buf, count * SECTOR_SIZE, !write);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Block device
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int
|
||||
usb_storage_present(void)
|
||||
{
|
||||
return ums.present;
|
||||
}
|
||||
|
||||
// Sync one buffer-cache block with the stick. Same contract as iderw() and
|
||||
// ramdiskrw(): on a read fill b->data and set B_VALID, on a write push it out
|
||||
// and clear B_DIRTY.
|
||||
void
|
||||
usb_storage_rw(struct buf *b)
|
||||
{
|
||||
int per_block = BSIZE / SECTOR_SIZE;
|
||||
uint32 lba = b->blockno * per_block;
|
||||
int write = (b->flags & B_DIRTY) != 0;
|
||||
int i, n = -1;
|
||||
|
||||
if(!holdingsleep(&b->lock))
|
||||
panic("usb_storage_rw: buf not locked");
|
||||
if(!ums.present)
|
||||
panic("usb_storage_rw: no USB disk");
|
||||
if(lba + per_block > ums.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);
|
||||
|
||||
// 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);
|
||||
if(n == BSIZE)
|
||||
break;
|
||||
cprintf("usb-storage: %s of block %d failed, retrying\n",
|
||||
write ? "write" : "read", b->blockno);
|
||||
}
|
||||
|
||||
releasesleep(&ums.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 --
|
||||
// so leaving B_VALID clear would deliver whatever the buffer happened to
|
||||
// contain as though it were the block on disk. Corrupting a filesystem
|
||||
// silently is worse than stopping.
|
||||
if(n != BSIZE)
|
||||
panic("usb_storage_rw: transfer failed");
|
||||
|
||||
if(!write)
|
||||
memmove(b->data, ums.xfer, BSIZE);
|
||||
b->flags |= B_VALID;
|
||||
b->flags &= ~B_DIRTY;
|
||||
}
|
||||
|
||||
// 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:
|
||||
// bread() -> bdevrw() -> usb_storage_rw() -> bulk-only transport -> SCSI
|
||||
// READ(10) -> an interrupt -> a sleeping process woken with data. Everything
|
||||
// before it -- INQUIRY answering, a plausible capacity -- can be true of a
|
||||
// device that cannot actually read a sector.
|
||||
//
|
||||
// Reached with `echo s > /proc/sysrq`.
|
||||
void
|
||||
usb_storage_dump(void)
|
||||
{
|
||||
struct buf *b;
|
||||
struct superblock *sb;
|
||||
|
||||
if(!ums.present){
|
||||
cprintf("usb-storage: no disk\n");
|
||||
return;
|
||||
}
|
||||
|
||||
cprintf("usb-storage: %d sectors of %d bytes\n", ums.nsectors,
|
||||
ums.sector_size);
|
||||
|
||||
b = bread(USBDISK_DEV, 1);
|
||||
sb = (struct superblock*)b->data;
|
||||
cprintf("usb-storage: block 1 magic %x size %d nblocks %d ninodes %d\n",
|
||||
sb->magic, sb->size, sb->nblocks, sb->ninodes);
|
||||
brelse(b);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int
|
||||
storage_probe(struct usb_dev *dev)
|
||||
{
|
||||
char vendor[9], product[17];
|
||||
int i;
|
||||
|
||||
if(ums.present)
|
||||
return -1; // one disk is enough for now
|
||||
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;
|
||||
|
||||
// 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)
|
||||
goto fail;
|
||||
|
||||
// Deliberately OUT first, which is the order that used to break.
|
||||
//
|
||||
// The slot context's Context Entries field must cover the highest DCI in
|
||||
// use, and xhci_open_ep originally wrote whichever endpoint it was adding.
|
||||
// Bulk OUT here has the higher DCI, so opening it first and then IN would
|
||||
// *lower* the field and silently invalidate the endpoint that was already
|
||||
// configured. Opening IN first hid that. Now that xhci_open_ep tracks the
|
||||
// maximum itself the order genuinely does not matter -- and doing it in the
|
||||
// formerly-broken order is what keeps that true.
|
||||
if(dev->ops->open_ep(dev, &dev->bulk_out) < 0 ||
|
||||
dev->ops->open_ep(dev, &dev->bulk_in) < 0)
|
||||
goto fail;
|
||||
|
||||
// Deliberately no GET MAX LUN. It is answered with a STALL by
|
||||
// single-LUN devices, and recovering from a STALL on endpoint 0 needs a
|
||||
// Reset Endpoint command this driver does not implement yet. LUN 0 is the
|
||||
// only one used below, so asking would gain nothing and could wedge us.
|
||||
|
||||
// A device that has just been configured is usually "not ready" for the
|
||||
// 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)
|
||||
break;
|
||||
microdelay(100000);
|
||||
}
|
||||
|
||||
if(scsi_inquiry(vendor, product) < 0)
|
||||
goto fail;
|
||||
if(scsi_read_capacity() < 0)
|
||||
goto fail;
|
||||
|
||||
if(ums.sector_size != SECTOR_SIZE){
|
||||
cprintf("usb-storage: %d-byte sectors are not supported\n",
|
||||
ums.sector_size);
|
||||
goto fail;
|
||||
}
|
||||
if(ums.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)));
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
// Give the memory back and forget the device. Leaving ums.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;
|
||||
return -1;
|
||||
}
|
||||
|
||||
const struct usb_driver usb_storage_driver = {
|
||||
.name = "usb-storage",
|
||||
.class = USB_CLASS_MASS_STORAGE,
|
||||
.subclass = USB_MSC_SUBCLASS_SCSI,
|
||||
.protocol = USB_MSC_PROTO_BOT,
|
||||
.devclass = DEVCLASS_STORAGE,
|
||||
.probe = storage_probe,
|
||||
};
|
||||
Loading…
Reference in a new issue