From b128734c209bd282afef18adbfa75933cbe6c515 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 17 Apr 2025 09:10:22 -0400 Subject: [PATCH 1/4] usb: add USB mass storage class support --- src/machine/usb.go | 2 + src/machine/usb/descriptor/endpoint.go | 45 ++++ src/machine/usb/descriptor/msc.go | 75 ++++++ src/machine/usb/msc/cbw.go | 57 +++++ src/machine/usb/msc/csw/csw.go | 14 ++ src/machine/usb/msc/disk.go | 73 ++++++ src/machine/usb/msc/msc.go | 335 +++++++++++++++++++++++++ src/machine/usb/msc/scsi.go | 238 ++++++++++++++++++ src/machine/usb/msc/scsi/scsi.go | 81 ++++++ src/machine/usb/msc/scsi_inquiry.go | 170 +++++++++++++ src/machine/usb/msc/scsi_readwrite.go | 104 ++++++++ src/machine/usb/msc/scsi_unmap.go | 121 +++++++++ src/machine/usb/msc/setup.go | 172 +++++++++++++ src/machine/usb/usb.go | 12 +- 14 files changed, 1496 insertions(+), 3 deletions(-) create mode 100644 src/machine/usb/descriptor/msc.go create mode 100644 src/machine/usb/msc/cbw.go create mode 100644 src/machine/usb/msc/csw/csw.go create mode 100644 src/machine/usb/msc/disk.go create mode 100644 src/machine/usb/msc/msc.go create mode 100644 src/machine/usb/msc/scsi.go create mode 100644 src/machine/usb/msc/scsi/scsi.go create mode 100644 src/machine/usb/msc/scsi_inquiry.go create mode 100644 src/machine/usb/msc/scsi_readwrite.go create mode 100644 src/machine/usb/msc/scsi_unmap.go create mode 100644 src/machine/usb/msc/setup.go diff --git a/src/machine/usb.go b/src/machine/usb.go index 8663348157..5ded0171c7 100644 --- a/src/machine/usb.go +++ b/src/machine/usb.go @@ -134,6 +134,8 @@ var ( usb.HID_ENDPOINT_OUT: (usb.ENDPOINT_TYPE_DISABLE), // Interrupt Out usb.MIDI_ENDPOINT_IN: (usb.ENDPOINT_TYPE_DISABLE), // Bulk In usb.MIDI_ENDPOINT_OUT: (usb.ENDPOINT_TYPE_DISABLE), // Bulk Out + usb.MSC_ENDPOINT_IN: (usb.ENDPOINT_TYPE_DISABLE), // Bulk In + usb.MSC_ENDPOINT_OUT: (usb.ENDPOINT_TYPE_DISABLE), // Bulk Out } ) diff --git a/src/machine/usb/descriptor/endpoint.go b/src/machine/usb/descriptor/endpoint.go index c7fa011fad..04e98b79cb 100644 --- a/src/machine/usb/descriptor/endpoint.go +++ b/src/machine/usb/descriptor/endpoint.go @@ -4,6 +4,17 @@ import ( "internal/binary" ) +/* Endpoint Descriptor +USB 2.0 Specification: 9.6.6 Endpoint +*/ + +const ( + TransferTypeControl uint8 = iota + TransferTypeIsochronous + TransferTypeBulk + TransferTypeInterrupt +) + var endpointEP1IN = [endpointTypeLen]byte{ endpointTypeLen, TypeEndpoint, @@ -74,6 +85,36 @@ var EndpointEP5OUT = EndpointType{ data: endpointEP5OUT[:], } +// Mass Storage Class bulk in endpoint +var endpointEP8IN = [endpointTypeLen]byte{ + endpointTypeLen, + TypeEndpoint, + 0x88, // EndpointAddress + TransferTypeBulk, // Attributes + 0x40, // MaxPacketSizeL (64 bytes) + 0x00, // MaxPacketSizeH + 0x00, // Interval +} + +var EndpointEP8IN = EndpointType{ + data: endpointEP8IN[:], +} + +// Mass Storage Class bulk out endpoint +var endpointEP9OUT = [endpointTypeLen]byte{ + endpointTypeLen, + TypeEndpoint, + 0x09, // EndpointAddress + TransferTypeBulk, // Attributes + 0x40, // MaxPacketSizeL (64 bytes) + 0x00, // MaxPacketSizeH + 0x00, // Interval +} + +var EndpointEP9OUT = EndpointType{ + data: endpointEP9OUT[:], +} + const ( endpointTypeLen = 7 ) @@ -109,3 +150,7 @@ func (d EndpointType) MaxPacketSize(v uint16) { func (d EndpointType) Interval(v uint8) { d.data[6] = byte(v) } + +func (d EndpointType) GetMaxPacketSize() uint16 { + return binary.LittleEndian.Uint16(d.data[4:6]) +} diff --git a/src/machine/usb/descriptor/msc.go b/src/machine/usb/descriptor/msc.go new file mode 100644 index 0000000000..83df1cd336 --- /dev/null +++ b/src/machine/usb/descriptor/msc.go @@ -0,0 +1,75 @@ +package descriptor + +const ( + interfaceClassMSC = 0x08 + mscSubclassSCSI = 0x06 + mscProtocolBOT = 0x50 +) + +var interfaceAssociationMSC = [interfaceAssociationTypeLen]byte{ + interfaceAssociationTypeLen, + TypeInterfaceAssociation, + 0x02, // FirstInterface + 0x01, // InterfaceCount + interfaceClassMSC, // FunctionClass + mscSubclassSCSI, // FunctionSubClass + mscProtocolBOT, // FunctionProtocol + 0x00, // Function +} + +var InterfaceAssociationMSC = InterfaceAssociationType{ + data: interfaceAssociationMSC[:], +} + +var interfaceMSC = [interfaceTypeLen]byte{ + interfaceTypeLen, // Length + TypeInterface, // DescriptorType + 0x02, // InterfaceNumber + 0x00, // AlternateSetting + 0x02, // NumEndpoints + interfaceClassMSC, // InterfaceClass (Mass Storage) + mscSubclassSCSI, // InterfaceSubClass (SCSI Transparent) + mscProtocolBOT, // InterfaceProtocol (Bulk-Only Transport) + 0x00, // Interface +} + +var InterfaceMSC = InterfaceType{ + data: interfaceMSC[:], +} + +var configurationMSC = [configurationTypeLen]byte{ + configurationTypeLen, + TypeConfiguration, + 0x6a, 0x00, // wTotalLength + 0x03, // number of interfaces (bNumInterfaces) + 0x01, // configuration value (bConfigurationValue) + 0x00, // index to string description (iConfiguration) + 0xa0, // attributes (bmAttributes) + 0x32, // maxpower (100 mA) (bMaxPower) +} + +var ConfigurationMSC = ConfigurationType{ + data: configurationMSC[:], +} + +// Mass Storage Class +var MSC = Descriptor{ + Device: DeviceCDC.Bytes(), + Configuration: Append([][]byte{ + ConfigurationMSC.Bytes(), + InterfaceAssociationCDC.Bytes(), + InterfaceCDCControl.Bytes(), + ClassSpecificCDCHeader.Bytes(), + ClassSpecificCDCACM.Bytes(), + ClassSpecificCDCUnion.Bytes(), + ClassSpecificCDCCallManagement.Bytes(), + EndpointEP1IN.Bytes(), + InterfaceCDCData.Bytes(), + EndpointEP2OUT.Bytes(), + EndpointEP3IN.Bytes(), + InterfaceAssociationMSC.Bytes(), + InterfaceMSC.Bytes(), + EndpointEP8IN.Bytes(), + EndpointEP9OUT.Bytes(), + }), +} diff --git a/src/machine/usb/msc/cbw.go b/src/machine/usb/msc/cbw.go new file mode 100644 index 0000000000..c60302d708 --- /dev/null +++ b/src/machine/usb/msc/cbw.go @@ -0,0 +1,57 @@ +package msc + +import ( + "encoding/binary" + "machine/usb/msc/csw" + "machine/usb/msc/scsi" +) + +const ( + cbwMsgLen = 31 // Command Block Wrapper (CBW) message length + Signature = 0x43425355 // "USBC" in little endian +) + +type CBW struct { + Data []byte +} + +func (c *CBW) length() int { + return len(c.Data) +} + +func (c *CBW) validLength() bool { + return len(c.Data) == cbwMsgLen +} + +func (c *CBW) validSignature() bool { + return binary.LittleEndian.Uint32(c.Data[:4]) == Signature +} + +func (c *CBW) scsiCmd() scsi.Cmd { + return scsi.Cmd{Data: c.Data[15:]} +} + +func (c *CBW) transferLength() uint32 { + return binary.LittleEndian.Uint32(c.Data[8:12]) +} + +// isIn returns true if the command direction is from the device to the host. +func (c *CBW) isIn() bool { + return c.Data[12]>>7 != 0 +} + +// isOut returns true if the command direction is from the host to the device. +func (c *CBW) isOut() bool { + return !c.isIn() +} + +func (c *CBW) CSW(status csw.Status, residue uint32, b []byte) { + // Signature: "USBS" 53425355h (little endian) + binary.LittleEndian.PutUint32(b[:4], csw.Signature) + // Tag: (same as CBW) + copy(b[4:8], c.Data[4:8]) + // Data Residue: (untransferred bytes) + binary.LittleEndian.PutUint32(b[8:12], residue) + // Status: + b[12] = byte(status) +} diff --git a/src/machine/usb/msc/csw/csw.go b/src/machine/usb/msc/csw/csw.go new file mode 100644 index 0000000000..29f44ef0e1 --- /dev/null +++ b/src/machine/usb/msc/csw/csw.go @@ -0,0 +1,14 @@ +package csw + +type Status uint8 + +const ( + StatusPassed Status = iota + StatusFailed + StatusPhaseError +) + +const ( + MsgLen = 13 + Signature = 0x53425355 // "USBS" in little endian +) diff --git a/src/machine/usb/msc/disk.go b/src/machine/usb/msc/disk.go new file mode 100644 index 0000000000..6423c4ceaf --- /dev/null +++ b/src/machine/usb/msc/disk.go @@ -0,0 +1,73 @@ +package msc + +import ( + "encoding/binary" + "machine" +) + +var _ machine.BlockDevice = (*DefaultDisk)(nil) + +// DefaultDisk is a placeholder disk implementation +type DefaultDisk struct { +} + +// NewDefaultDisk creates a new DefaultDisk instance +func NewDefaultDisk() *DefaultDisk { + return &DefaultDisk{} +} + +func (d *DefaultDisk) Size() int64 { + return 4096 * int64(d.WriteBlockSize()) // 2MB +} + +func (d *DefaultDisk) WriteBlockSize() int64 { + return 512 // 512 bytes +} + +func (d *DefaultDisk) EraseBlockSize() int64 { + return 2048 // 4 blocks of 512 bytes +} + +func (d *DefaultDisk) EraseBlocks(startBlock, numBlocks int64) error { + return nil +} + +func (d *DefaultDisk) ReadAt(buffer []byte, offset int64) (int, error) { + n := uint8(offset) + for i := range buffer { + n++ + buffer[i] = n + } + return len(buffer), nil +} + +func (d *DefaultDisk) WriteAt(buffer []byte, offset int64) (int, error) { + return len(buffer), nil +} + +// RegisterBlockDevice registers a BlockDevice provider with the MSC driver +func (m *msc) RegisterBlockDevice(dev machine.BlockDevice) { + m.dev = dev + + // Set VPD UNMAP fields + for i := range vpdPages { + if vpdPages[i].PageCode == 0xb0 { + // 0xb0 - 5.4.5 Block Limits VPD page (B0h) + if len(vpdPages[i].Data) >= 28 { + // Set the OPTIMAL UNMAP GRANULARITY (write blocks per erase block) + granularity := uint32(dev.EraseBlockSize()) / uint32(dev.WriteBlockSize()) + binary.BigEndian.PutUint32(vpdPages[i].Data[24:28], granularity) + } + /* TODO: Add method for working out the optimal unmap granularity alignment + if len(vpdPages[i].Data) >= 32 { + // Set the UNMAP GRANULARITY ALIGNMENT (first sector of first full erase block) + // The unmap granularity alignment is used to calculate an optimal unmap request starting LBA as follows: + // optimal unmap request starting LBA = (n * OPTIMAL UNMAP GRANULARITY) + UNMAP GRANULARITY ALIGNMENT + // where n is zero or any positive integer value + // https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf + } + */ + break + } + } +} diff --git a/src/machine/usb/msc/msc.go b/src/machine/usb/msc/msc.go new file mode 100644 index 0000000000..cfc88dc308 --- /dev/null +++ b/src/machine/usb/msc/msc.go @@ -0,0 +1,335 @@ +package msc + +import ( + "fmt" + "machine" + "machine/usb" + "machine/usb/descriptor" + "machine/usb/msc/csw" + "machine/usb/msc/scsi" +) + +type mscState uint8 + +const ( + mscStateCmd mscState = iota + mscStateData + mscStateStatus + mscStateStatusSent + mscStateNeedReset +) + +const ( + mscInterface = 2 +) + +var MSC *msc + +type msc struct { + buf []byte // Buffer for incoming/outgoing data + waitTxc bool // Wait for transmission completion confirmation + rxStalled bool // Flag to indicate if the RX endpoint is stalled + txStalled bool // Flag to indicate if the TX endpoint is stalled + maxPacketSize uint32 // Maximum packet size for the IN endpoint + respStatus csw.Status // Response status for the last command + sendZLP bool // Flag to indicate if a zero-length packet should be sent before sending CSW + + cbw *CBW // Last received Command Block Wrapper + queuedBytes uint32 // Number of bytes queued for sending + sentBytes uint32 // Number of bytes sent + totalBytes uint32 // Total bytes to send + cswBuf []byte // CSW response buffer + state mscState + + maxLUN uint8 // Maximum Logical Unit Number (n-1 for n LUNs) + dev machine.BlockDevice + readOnly bool + + vendorID [8]byte // Max 8 ASCII characters + productID [16]byte // Max 16 ASCII characters + productRev [4]byte // Max 4 ASCII characters + + senseKey scsi.Sense + addlSenseCode scsi.SenseCode + addlSenseQualifier uint8 + + // TODO: Cleanup + loops uint32 // Number of loops in the current state + prevState mscState + savedState mscState + packetsSent uint32 // Number of packets sent + packetsRecv uint32 // Number of packets received + packetsAcked uint32 // Number of packets acknowledged + canaryA uint32 // Canaries for debugging + canaryB uint32 // Canaries for debugging + canaryC uint32 // Canaries for debugging + canaryBuf [31]byte // CBW buffer container +} + +func init() { + // Initialize the USB Mass Storage Class (MSC) port + MSC = newMSC() +} + +// Port returns the USB Mass Storage port +func Port() *msc { + if MSC == nil { + MSC = newMSC() + } + return MSC +} + +func newMSC() *msc { + // Size our buffer to match the maximum packet size of the IN endpoint + maxPacketSize := descriptor.EndpointEP8IN.GetMaxPacketSize() + m := &msc{ + buf: make([]byte, maxPacketSize), + cswBuf: make([]byte, csw.MsgLen), + cbw: &CBW{Data: make([]byte, 31)}, + maxPacketSize: uint32(maxPacketSize), + } + m.RegisterBlockDevice(NewDefaultDisk()) + + // Set default inquiry data fields + m.SetVendorID("TinyGo") + m.SetProductID("Mass Storage") + m.SetProductRev("1.0") + + // Initialize the USB Mass Storage Class (MSC) port + machine.ConfigureUSBEndpoint(descriptor.MSC, + []usb.EndpointConfig{ + { + Index: usb.MSC_ENDPOINT_IN, + IsIn: true, + Type: usb.ENDPOINT_TYPE_BULK, + TxHandler: txHandler, + StallHandler: setupPacketHandler, + }, + { + Index: usb.MSC_ENDPOINT_OUT, + IsIn: false, + Type: usb.ENDPOINT_TYPE_BULK, + RxHandler: rxHandler, + StallHandler: setupPacketHandler, + }, + }, + []usb.SetupConfig{ + { + Index: mscInterface, + Handler: setupPacketHandler, + }, + }, + ) + + return m +} + +func (m *msc) ready() bool { + return m.dev != nil +} + +func (m *msc) resetBuffer(length int) { + // Reset the buffer to the specified length + m.buf = m.buf[:length] + for i := 0; i < length; i++ { + m.buf[i] = 0 + } +} + +func (m *msc) sendUSBPacket(b []byte) { + if machine.USBDev.InitEndpointComplete { + m.packetsSent++ // TODO: Cleanup? + // Send the USB packet + m.waitTxc = true + machine.SendUSBInPacket(usb.MSC_ENDPOINT_IN, b) + } +} + +func (m *msc) sendCSW(status csw.Status) { + // Generate CSW packet into m.cswBuf and send it + residue := uint32(0) + if m.totalBytes >= m.sentBytes { + residue = m.totalBytes - m.sentBytes + } + m.cbw.CSW(status, residue, m.cswBuf) + m.state = mscStateStatusSent + m.sendUSBPacket(m.cswBuf) +} + +func txHandler() { + if MSC != nil { + MSC.packetsAcked++ // TODO: Cleanup? + MSC.txHandler() + } +} + +func (m *msc) txHandler() { + m.waitTxc = false + m.run([]byte{}, false) +} + +func rxHandler(b []byte) { + if MSC != nil { + MSC.packetsRecv++ // TODO: Cleanup? + MSC.run(b, true) + } +} + +/* + Connection Happy Path Overview: + +0. MSC starts out in mscStateCmd status. + +1. Host sends CBW (Command Block Wrapper) packet to MSC. + - CBW contains the SCSI command to be executed, the length of the data to be transferred, etc. + +2. MSC receives CBW. + - CBW is validated and saved. + - State is changed to mscStateData. + - MSC routes the command to the appropriate SCSI command handler. + +3. The MSC SCSI command handler responds with the initial data packet (if applicable). + - If no data packet is needed, state is changed to mscStateStatus and step 4 is skipped. + +4. The host acks the data packet and MSC calls m.scsiDataTransfer() to continue sending (or +receiving) data. + - This cycle continues until all data requested in the CBW is sent/received. + - State is changed to mscStateStatus. + - MSC waits for the host to ACK the final data packet. + +5. MSC then sends a CSW (Command Status Wrapper) to the host to report the final status of the +command execution and moves to mscStateStatusSent. + +6. The host ACKs the CSW and the MSC moves back to mscStateCmd, waiting for the next CBW. +*/ +func (m *msc) run(b []byte, isEpOut bool) { + // TODO: Cleanup? + if m.prevState != m.state { + m.savedState = m.prevState + m.loops = 0 + m.prevState = m.state + } + m.loops++ + + switch m.state { + case mscStateCmd: + // Receiving a new command block wrapper (CBW) + + // IN endpoint transfer complete confirmation, no action needed + if !isEpOut { + return + } + + // Create a temporary CBW wrapper to validate the incoming data. Has to be temporary + // to avoid it escaping into the heap since we're in interrupt context + cbw := CBW{Data: b} + + // Verify size and signature + if !cbw.validLength() || !cbw.validSignature() { + // 6.6.1 CBW Not Valid + // https://usb.org/sites/default/files/usbmassbulk_10.pdf + m.state = mscStateNeedReset + m.stallEndpoint(usb.MSC_ENDPOINT_IN) + m.stallEndpoint(usb.MSC_ENDPOINT_OUT) + m.stallEndpoint(usb.CONTROL_ENDPOINT) + return + } + + // Save the validated CBW for later reference + copy(m.cbw.Data, b) + + // Move on to the data transfer phase next go around (after sending the first message) + m.state = mscStateData + m.totalBytes = cbw.transferLength() + m.queuedBytes = 0 + m.sentBytes = 0 + m.respStatus = csw.StatusPassed + + m.scsiCmdBegin(b) + + case mscStateData: + // Transfer data + m.scsiDataTransfer(b) + + case mscStateStatus: + // Sending CSW status response + // Placed after the switch statement so we can send the CSW without having to send a packet + // to cycle back through this block, e.g. with TEST UNIT READY which sends only a CSW after + // setting the sense key/add'l code/qualifier internally + + case mscStateStatusSent: + // Wait for the status phase to complete + if !isEpOut && m.queuedBytes == csw.MsgLen { + // Status confirmed sent, wait for next CBW + m.state = mscStateCmd + } else { + // We're not expecting any data here, ignore it. Original log line: + // TU_LOG1(" Warning expect SCSI Status but received unknown data\r\n"); + } + + case mscStateNeedReset: + // Received an invalid CBW message, stop everything until we get reset + } + + // Send CSW status response + // Placed after the switch statement so we can send the CSW without having to send a packet + // to cycle back through this block, e.g. with TEST UNIT READY which sends only a CSW after + // setting the sense key/add'l code/qualifier internally + if m.state == mscStateStatus && !m.txStalled { + if m.totalBytes > m.sentBytes && m.cbw.isIn() { + // 6.7.2 The Thirteen Cases - Case 5 (Hi > Di): STALL before status + m.stallEndpoint(usb.MSC_ENDPOINT_IN) + } else if m.sendZLP || m.queuedBytes == m.maxPacketSize { + // If the last packet is wMaxPacketSize we need to first send a zero-length packet + // to indicate the end of the transfer before we can send a CSW + m.sendUSBPacket(m.buf[:0]) + m.queuedBytes = 0 + m.sendZLP = false + } else { + m.sendCSW(m.respStatus) + m.state = mscStateCmd + } + } +} + +// TODO: Cleanup +func (m *msc) CanaryBuf() string { + if m.canaryA > 0 { + return fmt.Sprintf("% x", m.canaryBuf) + } + return "" +} + +func stateToString(state mscState) string { + switch state { + case mscStateCmd: + return "CmdWait" + case mscStateData: + return "Data" + case mscStateStatus: + return "SendStatus" + case mscStateStatusSent: + return "StatusSent" + case mscStateNeedReset: + return "NeedReset" + default: + return "Unknown" + } +} + +func (m *msc) State() string { + switch m.state { + case mscStateCmd: + return fmt.Sprintf("(%d/%d/%d) [%d/%d/%d] CmdWait (%d) <- %s", m.canaryA, m.canaryB, m.canaryC, m.packetsRecv, m.packetsSent, m.packetsAcked, m.loops, stateToString(m.savedState)) + case mscStateData: + return fmt.Sprintf("(%d/%d/%d) [%d/%d/%d] Data [%d/%d] (%d) <- %s", m.canaryA, m.canaryB, m.canaryC, m.packetsRecv, m.packetsSent, m.packetsAcked, m.sentBytes, m.totalBytes, m.loops, stateToString(m.savedState)) + case mscStateStatus: + return fmt.Sprintf("(%d/%d/%d) [%d/%d/%d] SendStatus (%d) <- %s", m.canaryA, m.canaryB, m.canaryC, m.packetsRecv, m.packetsSent, m.packetsAcked, m.loops, stateToString(m.savedState)) + case mscStateStatusSent: + return fmt.Sprintf("(%d/%d/%d) [%d/%d/%d] StatusSent (%d) <- %s", m.canaryA, m.canaryB, m.canaryC, m.packetsRecv, m.packetsSent, m.packetsAcked, m.loops, stateToString(m.savedState)) + case mscStateNeedReset: + return fmt.Sprintf("(%d/%d/%d) [%d/%d/%d] NeedReset (%d) <- %s", m.canaryA, m.canaryB, m.canaryC, m.packetsRecv, m.packetsSent, m.packetsAcked, m.loops, stateToString(m.savedState)) + default: + return "Unknown" + } +} diff --git a/src/machine/usb/msc/scsi.go b/src/machine/usb/msc/scsi.go new file mode 100644 index 0000000000..c810c9b13e --- /dev/null +++ b/src/machine/usb/msc/scsi.go @@ -0,0 +1,238 @@ +package msc + +import ( + "encoding/binary" + "machine/usb" + "machine/usb/msc/csw" + "machine/usb/msc/scsi" +) + +func (m *msc) scsiCmdBegin(b []byte) { + cmd := m.cbw.scsiCmd() + cmdType := cmd.CmdType() + + // Handle multi-packet commands + switch cmdType { + case scsi.CmdRead, scsi.CmdWrite: + m.scsiCmdReadWrite(cmd, b) + return + case scsi.CmdUnmap: + m.scsiCmdUnmap(cmd) + return + } + + if m.totalBytes > 0 && m.cbw.isOut() { + // Reject any other multi-packet commands + if m.totalBytes > uint32(cap(m.buf)) { + m.sendScsiError(csw.StatusFailed, scsi.SenseIllegalRequest, scsi.SenseCodeInvalidCmdOpCode) + return + } else { + // Original comment from TinyUSB: + // Didn't check for case 9 (Ho > Dn), which requires examining scsi command first + // but it is OK to just receive data then responded with failed status + } + } + switch cmdType { + case scsi.CmdTestUnitReady: + m.scsiTestUnitReady() + case scsi.CmdReadCapacity: + m.scsiCmdReadCapacity(cmd) + case scsi.CmdReadFormatCapacity: + m.scsiCmdReadFormatCapacity(cmd) + case scsi.CmdInquiry: + m.scsiCmdInquiry(cmd) + case scsi.CmdModeSense: + m.scsiCmdModeSense() + case scsi.CmdRequestSense: + m.scsiCmdRequestSense() + default: + // We don't support this command, error out + m.sendScsiError(csw.StatusFailed, scsi.SenseIllegalRequest, scsi.SenseCodeInvalidCmdOpCode) + } + + if len(m.buf) == 0 { + if m.totalBytes > 0 { + // 6.7.2 The Thirteen Cases - Case 4 (Hi > Dn) + // https://usb.org/sites/default/files/usbmassbulk_10.pdf + m.sendScsiError(csw.StatusFailed, scsi.SenseIllegalRequest, 0) + } else { + // 6.7.1 The Thirteen Cases - Case 1 Hn = Dn: all good + // https://usb.org/sites/default/files/usbmassbulk_10.pdf + m.state = mscStateStatus + } + } else { + if m.totalBytes == 0 { + // 6.7.1 The Thirteen Cases - Case 2 (Hn < Di) + // https://usb.org/sites/default/files/usbmassbulk_10.pdf + m.sendScsiError(csw.StatusFailed, scsi.SenseIllegalRequest, 0) + } else { + // Make sure we don't return more data than the host is expecting + if m.cbw.transferLength() < uint32(len(m.buf)) { + m.buf = m.buf[:m.cbw.transferLength()] + } + m.sendUSBPacket(m.buf) + } + } +} + +func (m *msc) scsiDataTransfer(b []byte) { + cmd := m.cbw.scsiCmd() + cmdType := cmd.CmdType() + + switch cmdType { + case scsi.CmdWrite: + m.scsiWrite(cmd, b) + return + case scsi.CmdUnmap: + m.scsiUnmap(cmd, b) + return + } + + // Update our sent bytes count to include the just-confirmed bytes + m.sentBytes += m.queuedBytes + + if m.sentBytes >= m.totalBytes { + // Transfer complete, send CSW after transfer confirmed + m.state = mscStateStatus + } else if cmdType == scsi.CmdRead { + m.scsiRead(cmd) + } else { + // Other multi-packet commands are rejected in m.scsiCmdBegin() + } +} + +func (m *msc) scsiTestUnitReady() { + m.resetBuffer(0) + m.queuedBytes = 0 + + // Check if the device is ready + if !m.ready() { + // If not ready set sense data + m.senseKey = scsi.SenseNotReady + m.addlSenseCode = scsi.SenseCodeMediumNotPresent + m.addlSenseQualifier = 0x00 + } else { + m.senseKey = 0 + m.addlSenseCode = 0 + m.addlSenseQualifier = 0 + } +} + +func (m *msc) scsiCmdReadCapacity(cmd scsi.Cmd) { + m.resetBuffer(scsi.ReadCapacityRespLen) + m.queuedBytes = scsi.ReadCapacityRespLen + + blockSize := uint32(m.dev.WriteBlockSize()) + blockCount := uint32(m.dev.Size()) / blockSize + lastBlock := blockCount - 1 + + // Last LBA address (big endian) + binary.BigEndian.PutUint32(m.buf[:4], lastBlock) + // Block size (big endian) + binary.BigEndian.PutUint32(m.buf[4:8], blockSize) +} + +func (m *msc) scsiCmdReadFormatCapacity(cmd scsi.Cmd) { + m.resetBuffer(scsi.ReadFormatCapacityRespLen) + m.queuedBytes = scsi.ReadFormatCapacityRespLen + + // bytes 0-2 - reserved + m.buf[3] = 8 // Capacity list length + + blockSize := uint32(m.dev.WriteBlockSize()) + + // Number of blocks (big endian) + binary.BigEndian.PutUint32(m.buf[4:8], uint32(m.dev.Size())/blockSize) + // Block size (24-bit, big endian) + binary.BigEndian.PutUint32(m.buf[8:12], blockSize) + // Descriptor Type - formatted media + m.buf[8] = 2 +} + +// MODE SENSE(6) - Only used here to indicate that the device is write protected +func (m *msc) scsiCmdModeSense() { + m.resetBuffer(scsi.ModeSenseRespLen) + m.queuedBytes = scsi.ModeSenseRespLen + + // The host allows a good amount of leeway in response size + // Reset total bytes to what we'll actually send + if m.totalBytes > scsi.ModeSenseRespLen { + m.totalBytes = scsi.ModeSenseRespLen + } + + // byte 0 - Number of bytes after this one + m.buf[0] = scsi.ModeSenseRespLen - 1 + // byte 1 - Medium type (0x00 for direct access block device) + if m.readOnly { + // Bit 7 indicates write protected + m.buf[2] = 0x80 + } + // byte 3 - Block descriptor length (not supported) +} + +// REQUEST SENSE - Returns error status codes when an error status is sent +func (m *msc) scsiCmdRequestSense() { + // Set the buffer size to the SCSI sense message size and clear + m.resetBuffer(scsi.RequestSenseRespLen) + m.queuedBytes = scsi.RequestSenseRespLen + + // 0x70 - current error, 0x71 - deferred error (not used) + m.buf[0] = 0xF0 // 0x70 for current error plus 0x80 for valid flag bit + // byte 1 - reserved + m.buf[2] = uint8(m.senseKey) & 0x0F // Incorrect Length Indicator bit not supported + // bytes 3-6 - Information (not used) + // byte 7 - Additional Sense Length (bytes remaining in the message) + m.buf[7] = scsi.RequestSenseRespLen - 8 + // bytes 8-11 - Command Specific Information (not used) + m.buf[12] = byte(m.addlSenseCode) // Additional Sense Code (optional) + m.buf[13] = m.addlSenseQualifier // Additional Sense Code Qualifier (optional) + // bytes 14-17 - reserved + + // Clear sense data after copied to buffer + m.senseKey = 0 + m.addlSenseCode = 0 + m.addlSenseQualifier = 0 +} + +func (m *msc) scsiCmdUnmap(cmd scsi.Cmd) { + // Unmap sends a header in the CBW and a parameter list in the data stage + // The parameter list header is 4 bytes and each parameter list item is 16 bytes + + // The parameter list has an 8 byte header and 16 bytes per item. If it's less than 24 bytes it's + // not the format we're expecting and we won't be able to decode it. Same for if there isn't a + // 8 byte header plus multiples of 16 bytes after that + paramLen := binary.BigEndian.Uint16(m.cbw.Data[7:9]) + if paramLen < 24 || (paramLen-8)%16 != 0 { + m.sendScsiError(csw.StatusFailed, scsi.SenseIllegalRequest, scsi.SenseCodeInvalidFieldInCDB) + return + } + + // The parameter list header has an 8 byte header and 16 bytes per item so if there are more than + // three items in the list one of them will overflow into the next packet. We'll save those 8 bytes + // in m.buf and use its length (starting at zero) to indicate whether we're currently split between + // packets + m.resetBuffer(0) +} + +func (m *msc) sendScsiError(status csw.Status, key scsi.Sense, code scsi.SenseCode) { + // Generate CSW into m.cswBuf + residue := m.totalBytes - m.sentBytes + + // Prepare to send CSW + m.sendZLP = true // Ensure the transaction is signaled as ended before a CSW is sent + m.respStatus = status + m.state = mscStateStatus + + // Set the sense data + m.senseKey = key + m.addlSenseCode = code + m.addlSenseQualifier = 0x00 // Not used + + if m.totalBytes > 0 && residue > 0 { + if m.cbw.isIn() { + m.stallEndpoint(usb.MSC_ENDPOINT_IN) + } else { + m.stallEndpoint(usb.MSC_ENDPOINT_OUT) + } + } +} diff --git a/src/machine/usb/msc/scsi/scsi.go b/src/machine/usb/msc/scsi/scsi.go new file mode 100644 index 0000000000..7ccd902abb --- /dev/null +++ b/src/machine/usb/msc/scsi/scsi.go @@ -0,0 +1,81 @@ +package scsi + +import "encoding/binary" + +type Cmd struct { + Data []byte +} + +func (c *Cmd) CmdType() CmdType { + return CmdType(c.Data[0]) +} + +func (c *Cmd) BlockCount() uint32 { + return uint32(binary.BigEndian.Uint16(c.Data[7:9])) +} + +func (c *Cmd) LBA() uint32 { + return binary.BigEndian.Uint32(c.Data[2:6]) +} + +type CmdType uint8 + +const ( + CmdTestUnitReady CmdType = 0x00 // TEST UNIT READY is used to determine if a device is ready to transfer data (read/write). The device does not perform a self-test operation + CmdRequestSense CmdType = 0x03 // REQUEST SENSE returns the current sense data (status or error information) + CmdInquiry CmdType = 0x12 // INQUIRY is used to obtain basic information from a target device + CmdModeSelect CmdType = 0x15 // MODE SELECT (6) provides a means for the application client to specify medium, logical unit, or peripheral device parameters to the device server + CmdModeSense CmdType = 0x1A // MODE SENSE (6) provides a means for a device server to report parameters to an application client + CmdStartStopUnit CmdType = 0x1B // START STOP UNIT is used to start or stop the medium in a device server + CmdPreventAllowMediumRemoval CmdType = 0x1E // PREVENT ALLOW MEDIUM REMOVAL is used to prevent or allow the removal of storage medium from a device server + CmdReadFormatCapacity CmdType = 0x23 // READ FORMAT CAPACITY allows the Host to request a list of the possible format capacities for an installed writable media + CmdReadCapacity CmdType = 0x25 // READ CAPACITY command is used to obtain data capacity information from a target device + CmdRead CmdType = 0x28 // READ (10) requests that the device server read the specified logical block(s) and transfer them to the data-in buffer + CmdWrite CmdType = 0x2A // WRITE (10) requests that the device server transfer the specified logical block(s) from the data-out buffer and write them + CmdUnmap CmdType = 0x42 // UNMAP command is used to inform the device server that the specified logical block(s) are no longer in use +) + +type Sense uint8 + +const ( + // 4.5.6 Sense key and sense code definitions + // https://www.t10.org/ftp/t10/document.08/08-309r0.pdf + SenseNone Sense = 0x00 // No specific Sense Key. This indicates no error condition + SenseRecoveredError Sense = 0x01 // The last command completed successfully, but with some recovery action performed + SenseNotReady Sense = 0x02 // The LUN addressed is not ready to be accessed + SenseMediumError Sense = 0x03 // The command terminated with an unrecoverable error condition + SenseHardwareError Sense = 0x04 // The drive detected an unrecoverable hardware failure while performing the command or during a self test + SenseIllegalRequest Sense = 0x05 // An illegal parameter was provided in the command descriptor block or the additional parameters + SenseUnitAttention Sense = 0x06 // The disk drive may have been reset + SenseDataProtect Sense = 0x07 // A read or write command was attempted on a block that is protected from this operation and was not performed + SenseBlankCheck Sense = 0x08 // A write-once device or a sequential-access device encountered blank medium or format-defined end-of-data indication while reading or that a write-once device encountered a non-blank medium while writing + SenseFirmwareError Sense = 0x09 // Vendor specific sense key + SenseAbortedCommand Sense = 0x0B // The disk drive aborted the command + SenseVolumeOverflow Sense = 0x0D // A buffered peripheral device has reached the end of medium partition and data remains in the buffer that has not been written to the medium + SenseMiscompare Sense = 0x0E // The source data did not match the data read from the medium +) + +type SenseCode uint8 + +const ( + // SenseNotReady + SenseCodeMediumNotPresent SenseCode = 0x3A // The storage medium is not present in the device (e.g. empty CD-ROM drive or flash card reader) + + // SenseIllegalRequest + SenseCodeInvalidCmdOpCode SenseCode = 0x20 // The command operation code is not supported by the device + SenseCodeInvalidFieldInCDB SenseCode = 0x24 // The command descriptor block (CDB) contains an invalid field + + // SenseDataProtect + SenseCodeWriteProtected SenseCode = 0x27 // The media is write protected + + // SenseVolumeOverflow + SenseCodeLBAOutOfRange SenseCode = 0x21 // The logical block address (LBA) is beyond the end of the volume +) + +const ( + InquiryRespLen = 36 + ModeSenseRespLen = 4 + ReadCapacityRespLen = 8 + ReadFormatCapacityRespLen = 12 + RequestSenseRespLen = 18 +) diff --git a/src/machine/usb/msc/scsi_inquiry.go b/src/machine/usb/msc/scsi_inquiry.go new file mode 100644 index 0000000000..1f1ac4e508 --- /dev/null +++ b/src/machine/usb/msc/scsi_inquiry.go @@ -0,0 +1,170 @@ +package msc + +import ( + "encoding/binary" + "machine/usb/msc/csw" + "machine/usb/msc/scsi" +) + +type vpdPage struct { + PageCode uint8 + PageLength uint8 + // Page data + // First four bytes are always Device Type, Page Code, and Page Length (2 bytes) and are omitted here + Data []byte +} + +// These must be sorted in ascending order by PageCode +var vpdPages = []vpdPage{ + { + // 0xb0 - 5.4.5 Block Limits VPD page (B0h) + // https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf + PageCode: 0xb0, + PageLength: 0x3c, // 60 bytes + Data: []byte{ + 0x00, 0x00, // WSNZ, MAXIMUM COMPARE AND WRITE LENGTH - Not supported + 0x00, 0x00, // OPTIMAL TRANSFER LENGTH GRANULARITY - Not supported + 0x00, 0x00, 0x00, 0x00, // MAXIMUM TRANSFER LENGTH - Not supported + 0x00, 0x00, 0x00, 0x00, // OPTIMAL TRANSFER LENGTH - Not supported + 0x00, 0x00, 0x00, 0x00, // MAXIMUM PREFETCH LENGTH - Not supported + 0xFF, 0xFF, 0xFF, 0xFF, // MAXIMUM UNMAP LBA COUNT - Maximum count supported + 0xFF, 0xFF, 0xFF, 0xFF, // MAXIMUM UNMAP BLOCK DESCRIPTOR COUNT - Maximum count supported + 0x00, 0x00, 0x00, 0x00, // OPTIMAL UNMAP GRANULARITY + 0x00, 0x00, 0x00, 0x00, // UNMAP GRANULARITY ALIGNMENT (bit 7 on byte 28 sets UGAVALID) + // From here on all bytes are zero and can be omitted from the response + // 0x00, 0x00, 0x00, 0x00, // MAXIMUM WRITE SAME LENGTH - Not supported + // 0x00, 0x00, 0x00, 0x00, // (8-bytes) + // 0x00, 0x00, 0x00, 0x00, // MAXIMUM ATOMIC TRANSFER LENGTH - Not supported + // 0x00, 0x00, 0x00, 0x00, // ATOMIC ALIGNMENT - Not supported + // 0x00, 0x00, 0x00, 0x00, // ATOMIC TRANSFER LENGTH GRANULARITY - Not supported + // 0x00, 0x00, 0x00, 0x00, // MAXIMUM ATOMIC TRANSFER LENGTH WITH ATOMIC BOUNDARY - Not supported + // 0x00, 0x00, 0x00, 0x00, // MAXIMUM ATOMIC BOUNDARY SIZE - Not supported + }, + }, + { + // 0xb1 - 5.4.3 Block Device Characteristics VPD page (B1h) + // https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf + PageCode: 0xb1, + PageLength: 0x3c, // 60 bytes (bytes 9+ are all reserved/zero) + Data: []byte{ + 0x00, 0x01, // Rotation rate (0x0001 - non-rotating medium) + 0x00, // Product type - 0x00: Not indicated, 0x04: MMC/eMMC, 0x05: SD card + 0x00, // WABEREQ/WACEREQ/Form Factor - Not specified + 0x00, // ZBC/BOCS/FUAB/VBULS + // Reserved (55 bytes) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + }, + }, + { + // 0xb2 - 5.4.13 Logical Block Provisioning VPD page (B2h) + // https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf + PageCode: 0xB2, + PageLength: 0x04, + Data: []byte{ + 0x00, // Logical Block Provisioning Threshold Exponent + 0x80, // 0x80 - LBPU (UNMAP command supported) + 0x00, // Minimum percentage/Provisioning type - Not specified + 0x00, // Threshold percentage - Not supported + }, + }, +} + +func (m *msc) scsiCmdInquiry(cmd scsi.Cmd) { + evpd := cmd.Data[1] & 0x01 + pageCode := cmd.Data[2] + + // PAGE CODE (byte 2) can't be set if the EVPD bit is not set + if evpd == 0 { + if pageCode == 0 { + // Standard INQUIRY command + m.scsiStdInquiry(cmd) + } else { + // 3.6.1 INQUIRY command introduction + // https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf + m.sendScsiError(csw.StatusFailed, scsi.SenseIllegalRequest, scsi.SenseCodeInvalidFieldInCDB) + return + } + } else { + m.scsiEvpdInquiry(cmd, pageCode) + } +} + +func (m *msc) scsiEvpdInquiry(cmd scsi.Cmd, pageCode uint8) { + var pageLength int + switch pageCode { + case 0x00: + // 5.4.18 Supported Vital Product Data pages (00h) + // https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf + + pageLength = len(vpdPages) + 1 // Number of pages + 1 for 0x00 (excluded from vpdPages[]) + m.resetBuffer(pageLength + 4) // n+4 supported VPD pages + // bytes 4+ - Supported VPD pages in ascending order + for i := 0; i < len(vpdPages); i++ { + m.buf[4+i] = vpdPages[i].PageCode + } + default: + found := false + for i := range vpdPages { + if vpdPages[i].PageCode == pageCode { + // Our advertised page length is "for entertainment use only". Some pages have dozens of + // reserved (zero) bytes at the end that don't actually need to be sent. If we omit them + // from our response they are (correctly) presumed to be zero bytes by the host + pageLength = int(vpdPages[i].PageLength) + // We actually just send the length of the bytes we have plus the same four byte header, + // but declare the length of the response according to the spec as appropriate + m.resetBuffer(len(vpdPages[i].Data) + 4) + copy(m.buf[4:], vpdPages[i].Data) + found = true + break + } + } + if !found { + // VPD page not found, send error + m.sendScsiError(csw.StatusFailed, scsi.SenseIllegalRequest, scsi.SenseCodeInvalidFieldInCDB) + return + } + } + + // byte 0 - Peripheral Qualifier/Peripheral Device Type (0x00 for direct access block device) + m.buf[1] = pageCode + binary.BigEndian.PutUint16(m.buf[2:4], uint16(pageLength)) + + // Set total bytes to the length of our response + m.queuedBytes = uint32(len(m.buf)) + m.totalBytes = uint32(len(m.buf)) +} + +func (m *msc) scsiStdInquiry(cmd scsi.Cmd) { + m.resetBuffer(scsi.InquiryRespLen) + m.queuedBytes = scsi.InquiryRespLen + m.totalBytes = scsi.InquiryRespLen + + // byte 0 - Device Type (0x00 for direct access block device) + // byte 1 - Removable bit + m.buf[1] = 0x80 + // byte 2 - Version 0x00 - We claim conformance to no standard + // byte 3 - Response data format + m.buf[3] = 2 + // byte 4 - Additional length (number of bytes after this one) + m.buf[4] = scsi.InquiryRespLen - 5 + // byte 5 - Not used + // byte 6 - Not used + // byte 7 - Not used + // bytes 8-15 - Vendor ID + for i := 0; i < 8; i++ { + m.buf[8+i] = m.vendorID[i] + } + // bytes 16-31 - Product ID + for i := 0; i < 16; i++ { + m.buf[16+i] = m.productID[i] + } + // bytes 32-35 - Product revision level + for i := 0; i < 4; i++ { + m.buf[32+i] = m.productRev[i] + } +} diff --git a/src/machine/usb/msc/scsi_readwrite.go b/src/machine/usb/msc/scsi_readwrite.go new file mode 100644 index 0000000000..3b582b102f --- /dev/null +++ b/src/machine/usb/msc/scsi_readwrite.go @@ -0,0 +1,104 @@ +package msc + +import ( + "machine/usb/msc/csw" + "machine/usb/msc/scsi" +) + +func (m *msc) scsiCmdReadWrite(cmd scsi.Cmd, b []byte) { + status := m.validateScsiReadWrite(cmd) + if status != csw.StatusPassed { + m.sendScsiError(status, scsi.SenseIllegalRequest, scsi.SenseCodeInvalidCmdOpCode) + } else if m.totalBytes > 0 { + if cmd.CmdType() == scsi.CmdRead { + m.scsiRead(cmd) + } else { + m.scsiWrite(cmd, b) + } + } else { + // Zero byte transfer. No practical use case + m.state = mscStateStatus + } +} + +// Validate SCSI READ(10) and WRITE(10) commands +func (m *msc) validateScsiReadWrite(cmd scsi.Cmd) csw.Status { + blockCount := cmd.BlockCount() + // CBW wrapper transfer length + if m.totalBytes == 0 { + // If the SCSI command's block count doesn't loosely match the wrapper's transfer length something's wrong + if blockCount > 0 { + return csw.StatusPhaseError + } + // Zero length transfer. No practical use case, but explicitly not an error according to the spec + return csw.StatusPassed + } + if (cmd.CmdType() == scsi.CmdRead && m.cbw.isOut()) || (cmd.CmdType() == scsi.CmdWrite && m.cbw.isIn()) { + // If the command is READ(10) and the data direction is from host to device that's a problem + // 6.7.3 The Thirteen Cases - Case 10 (Ho <> Di) + // If the command is WRITE(10) and the data direction is from device to host that's also a problem + // 6.7.2 The Thirteen Cases - Case 8 (Hi <> Do) + // https://usb.org/sites/default/files/usbmassbulk_10.pdf + return csw.StatusPhaseError + } + if blockCount == 0 { + // We already checked for zero length transfer above, so this is a problem + // 6.7.2 The Thirteen Cases - Case 4 (Hi > Dn) + // https://usb.org/sites/default/files/usbmassbulk_10.pdf + return csw.StatusFailed + } + if m.totalBytes/blockCount == 0 { + // Block size shouldn't be small enough to round to zero + // 6.7.2 The Thirteen Cases - Case 7 (Hi < Di) READ(10) or + // 6.7.3 The Thirteen Cases - Case 13 (Ho < Do) WRITE(10) + // https://usb.org/sites/default/files/usbmassbulk_10.pdf + return csw.StatusPhaseError + } + return csw.StatusPassed +} + +func (m *msc) scsiRead(cmd scsi.Cmd) { + // Make sure we don't exceed the buffer size + readEnd := m.totalBytes - m.sentBytes + if readEnd > uint32(cap(m.buf)) { + readEnd = uint32(cap(m.buf)) + } + m.buf = m.buf[:readEnd] + + // Calculate our read address given the already sent bytes and the block size + offset := int64(cmd.LBA()*uint32(m.dev.WriteBlockSize())) + int64(m.sentBytes) + + // Read data from the emulated block device + n, err := m.dev.ReadAt(m.buf[:readEnd], offset) + if err != nil || n == 0 { + m.sendScsiError(csw.StatusFailed, scsi.SenseNotReady, 0x3a) + return + } + + m.queuedBytes = readEnd + m.sendUSBPacket(m.buf) +} + +func (m *msc) scsiWrite(cmd scsi.Cmd, b []byte) { + if m.readOnly { + m.sendScsiError(csw.StatusFailed, scsi.SenseDataProtect, scsi.SenseCodeWriteProtected) + return + } + + // Calculate our write address given the already sent bytes and the block size + offset := int64(cmd.LBA()*uint32(m.dev.WriteBlockSize())) + int64(m.sentBytes) + + // Write data to the emulated block device + n, err := m.dev.WriteAt(b, offset) + if err != nil || n < len(b) { + m.sentBytes += uint32(n) + m.sendScsiError(csw.StatusFailed, scsi.SenseNotReady, scsi.SenseCodeMediumNotPresent) + } else { + m.sentBytes += uint32(len(b)) + } + + if m.sentBytes >= m.totalBytes { + // Data transfer is complete, send CSW + m.state = mscStateStatus + } +} diff --git a/src/machine/usb/msc/scsi_unmap.go b/src/machine/usb/msc/scsi_unmap.go new file mode 100644 index 0000000000..c387050bf4 --- /dev/null +++ b/src/machine/usb/msc/scsi_unmap.go @@ -0,0 +1,121 @@ +package msc + +import ( + "encoding/binary" + "machine/usb/msc/csw" + "machine/usb/msc/scsi" +) + +type Error int + +const ( + errorLBAOutOfRange Error = iota +) + +func (e Error) Error() string { + switch e { + case errorLBAOutOfRange: + return "LBA out of range" + default: + return "unknown error" + } +} + +func (m *msc) scsiUnmap(cmd scsi.Cmd, b []byte) { + // Execute Order 66 (0x42) to wipe out the blocks + // 3.54 Unmap Command (SBC-4) + // https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf + if m.readOnly { + m.sendScsiError(csw.StatusFailed, scsi.SenseDataProtect, scsi.SenseCodeWriteProtected) + return + } + + // blockDescLen is the remaining length of block descriptors in the message, offset 8 bytes from + // the start of this packet + var blockDescLen uint16 + numBlocks := uint64(m.dev.Size() / m.dev.WriteBlockSize()) + + // m.buf being zero-length indicates we're in the first packet in the data stage + if len(m.buf) == 0 { + // Decode the parameter list + msgLen := binary.BigEndian.Uint16(b[:2]) + // Length of the block descriptor portion of the message + blockDescLen = binary.BigEndian.Uint16(b[2:4]) + // Do some sanity checks on the message lengths + if msgLen < 8 || blockDescLen < 16 || msgLen-blockDescLen != 6 { + m.sendScsiError(csw.StatusFailed, scsi.SenseIllegalRequest, scsi.SenseCodeInvalidFieldInCDB) + return + } + + // If the message is going to overflow our packet we need to save the first half of the last + // block descriptor (last 8 bytes of the message) in m.buf to be used in the next packet + if msgLen > uint16(len(b)) { + m.resetBuffer(18) + // Save the overall block descriptor length + m.buf[0] = b[2] + m.buf[1] = b[3] + // And the remaining bytes + bufLen := len(b) - 8 + binary.BigEndian.PutUint16(m.buf[2:4], uint16(bufLen)) + } + } else { + // On subsequent packets we need to extract the remaining block descriptor length from m.buf + blockDescLen = binary.BigEndian.Uint16(m.buf[0:2]) + + // Take care of the last descriptor block from the previous packet + // Copy the first 8 blocks from b to the end of m.buf to make it a full descriptor + copy(m.buf[10:], b) + err := m.unmapBlocksFromDescriptor(m.buf[2:], numBlocks) + if err != nil { + // TODO: Might need a better error code here for device errors? + m.sendScsiError(csw.StatusFailed, scsi.SenseVolumeOverflow, scsi.SenseCodeLBAOutOfRange) + return + } + blockDescLen -= 16 + } + + // descEnd marks the end of the last full block descriptor in this packet + descEnd := int(blockDescLen + 8) + if descEnd > len(b) { + descEnd = len(b) - 8 + + // If the command overflows the current packet, save the leftover bytes in m.buf and decrement + // blockDescLen to reflect the full block descriptors received so far + copy(m.buf[2:], b[descEnd:]) + remaining := blockDescLen - uint16(descEnd) + binary.BigEndian.PutUint16(m.buf[:2], remaining) + } + + // Unmap the blocks we can from this packet + for i := 8; i < descEnd; i += 16 { + err := m.unmapBlocksFromDescriptor(b[i:], numBlocks) + if err != nil { + // TODO: Might need a better error code here for device errors? + m.sendScsiError(csw.StatusFailed, scsi.SenseVolumeOverflow, scsi.SenseCodeLBAOutOfRange) + return + } + } + + m.sentBytes += uint32(len(b)) + if m.sentBytes >= m.totalBytes { + // Order 66 complete, send CSW to establish galactic empire + m.state = mscStateStatus + } +} + +func (m *msc) unmapBlocksFromDescriptor(b []byte, numBlocks uint64) error { + blockCount := binary.BigEndian.Uint32(b[8:12]) + if blockCount == 0 { + // No blocks to unmap. Explicitly not an error per the spec + return nil + } + lba := binary.BigEndian.Uint64(b[0:8]) + + // Make sure the unmap command doesn't extend past the end of the volume + if lba+uint64(blockCount) > numBlocks { + return errorLBAOutOfRange + } + + // Unmap the blocks + return m.dev.EraseBlocks(int64(lba), int64(blockCount)) +} diff --git a/src/machine/usb/msc/setup.go b/src/machine/usb/msc/setup.go new file mode 100644 index 0000000000..733ceb4259 --- /dev/null +++ b/src/machine/usb/msc/setup.go @@ -0,0 +1,172 @@ +package msc + +import ( + "machine" + "machine/usb" + "machine/usb/msc/csw" +) + +func setupPacketHandler(setup usb.Setup) bool { + if MSC != nil { + return MSC.setupPacketHandler(setup) + } + return false +} + +func (m *msc) setupPacketHandler(setup usb.Setup) bool { + ok := false + wValue := (uint16(setup.WValueH) << 8) | uint16(setup.WValueL) + switch setup.BRequest { + case usb.CLEAR_FEATURE: + ok = m.handleClearFeature(setup, wValue) + case usb.GET_MAX_LUN: + ok = m.handleGetMaxLun(setup, wValue) + case usb.MSC_RESET: + ok = m.handleReset(setup, wValue) + } + return ok +} + +// Handles the CLEAR_FEATURE request for clearing ENDPOINT_HALT/stall +func (m *msc) handleClearFeature(setup usb.Setup, wValue uint16) bool { + ok := false + // wValue is the feature selector (0x00 for ENDPOINT_HALT) + // We aren't handling any other feature selectors + // https://wiki.osdev.org/Universal_Serial_Bus#CLEAR_FEATURE + if wValue != 0 { + return ok + } + // Clearing the stall is not enough, continue stalling until a reset is received first + // 6.6.1 CBW Not Valid + // If the CBW is not valid, the device shall STALL the Bulk-In pipe. Also, the device + // shall either STALL the Bulk-Out pipe, or the device shall accept and discard any + // Bulk-Out data. The device shall maintain this state until a Reset Recovery + // For Reset Recovery the host shall issue in the following order: : + // (a) a Bulk-Only Mass Storage Reset (handleReset()) + // (b) a Clear Feature HALT to the Bulk-In endpoint (clear stall IN) + // (c) a Clear Feature HALT to the Bulk-Out endpoint (clear stall OUT) + // https://usb.org/sites/default/files/usbmassbulk_10.pdf + if m.state == mscStateNeedReset { + wIndex := setup.WIndex & 0x7F // Clear the direction bit from the endpoint address for comparison + if wIndex == usb.MSC_ENDPOINT_IN { + m.stallEndpoint(usb.MSC_ENDPOINT_IN) + } else if wIndex == usb.MSC_ENDPOINT_OUT { + m.stallEndpoint(usb.MSC_ENDPOINT_OUT) + } + return ok + } + + // Clear the direction bit from the endpoint address for comparison + wIndex := setup.WIndex & 0x7F + + // Clear the IN/OUT stalls if addressed to the endpoint, or both if addressed to the interface + if wIndex == usb.MSC_ENDPOINT_IN || wIndex == mscInterface { + m.clearStallEndpoint(usb.MSC_ENDPOINT_IN) + ok = true + } + if wIndex == usb.MSC_ENDPOINT_OUT || wIndex == mscInterface { + m.clearStallEndpoint(usb.MSC_ENDPOINT_OUT) + ok = true + } + // Send a CSW if needed to resume after the IN endpoint stall is cleared + if m.state == mscStateStatus && wIndex == usb.MSC_ENDPOINT_IN { + m.sendCSW(csw.StatusPassed) + ok = true + } + + if ok { + machine.SendZlp() + } + return ok +} + +// 3.2 Get Max LUN +// https://usb.org/sites/default/files/usbmassbulk_10.pdf +func (m *msc) handleGetMaxLun(setup usb.Setup, wValue uint16) bool { + if setup.WIndex != mscInterface || setup.WLength != 1 || wValue != 0 { + return false + } + // Send the maximum LUN ID number (zero-indexed, so n-1) supported by the device + m.resetBuffer(1) // Shrink buffer to 1 byte + m.buf[0] = m.maxLUN + return machine.SendUSBInPacket(usb.CONTROL_ENDPOINT, m.buf) +} + +// 3.1 Bulk-Only Mass Storage Reset +// https://usb.org/sites/default/files/usbmassbulk_10.pdf +func (m *msc) handleReset(setup usb.Setup, wValue uint16) bool { + if setup.WIndex != mscInterface || setup.WLength != 0 || wValue != 0 { + return false + } + // Reset to command waiting state + m.state = mscStateCmd + + // Reset transfer state + m.resetBuffer(0) + m.senseKey = 0 + m.addlSenseCode = 0 + m.addlSenseQualifier = 0 + + // Send a zero-length packet (ZLP) to indicate the reset is complete + machine.SendZlp() + + // Return true to indicate successful reset + return true +} + +func (m *msc) stallEndpoint(ep uint8) { + if ep == usb.MSC_ENDPOINT_IN { + m.txStalled = true + machine.SetStallEPIn(usb.MSC_ENDPOINT_IN) + } else if ep == usb.MSC_ENDPOINT_OUT { + m.rxStalled = true + machine.SetStallEPOut(usb.MSC_ENDPOINT_OUT) + } else if ep == usb.CONTROL_ENDPOINT { + machine.SetStallEPIn(usb.CONTROL_ENDPOINT) + } +} + +func (m *msc) clearStallEndpoint(ep uint8) { + if ep == usb.MSC_ENDPOINT_IN { + machine.ClearStallEPIn(usb.MSC_ENDPOINT_IN) + m.txStalled = false + } else if ep == usb.MSC_ENDPOINT_OUT { + machine.ClearStallEPOut(usb.MSC_ENDPOINT_OUT) + m.rxStalled = false + } +} + +func (m *msc) setStringField(field []byte, value string) { + copy(field, []byte(value)) + for i := len(value); i < len(field); i++ { + field[i] = 0x20 // Fill remaining bytes with spaces + } +} + +func (m *msc) SetVendorID(vendorID string) { + m.setStringField(m.vendorID[:], vendorID) +} + +func (m *msc) SetProductID(productID string) { + m.setStringField(m.productID[:], productID) +} + +func (m *msc) SetProductRev(productRev string) { + m.setStringField(m.productRev[:], productRev) +} + +func SetVendorID(vendorID string) { + if MSC != nil { + MSC.SetVendorID(vendorID) + } +} +func SetProductID(productID string) { + if MSC != nil { + MSC.SetProductID(productID) + } +} +func SetProductRev(productRev string) { + if MSC != nil { + MSC.SetProductRev(productRev) + } +} diff --git a/src/machine/usb/usb.go b/src/machine/usb/usb.go index 360ac39026..3c54ddb4ce 100644 --- a/src/machine/usb/usb.go +++ b/src/machine/usb/usb.go @@ -28,7 +28,7 @@ const ( EndpointPacketSize = 64 // 64 for Full Speed, EPT size max is 1024 - // standard requests + // bRequest - standard requests GET_STATUS = 0 CLEAR_FEATURE = 1 SET_FEATURE = 3 @@ -40,7 +40,7 @@ const ( GET_INTERFACE = 10 SET_INTERFACE = 11 - // non standard requests + // bRequest - HID class-specific requests GET_REPORT = 1 GET_IDLE = 2 GET_PROTOCOL = 3 @@ -49,6 +49,10 @@ const ( SET_PROTOCOL = 11 SET_REPORT_TYPE = 33 + // bRequest - MSC class-specific requests + GET_MAX_LUN = 0xFE + MSC_RESET = 0xFF + DEVICE_CLASS_COMMUNICATIONS = 0x02 DEVICE_CLASS_HUMAN_INTERFACE = 0x03 DEVICE_CLASS_STORAGE = 0x08 @@ -75,7 +79,9 @@ const ( HID_ENDPOINT_OUT = 5 // for Interrupt Out MIDI_ENDPOINT_IN = 6 // for Bulk In MIDI_ENDPOINT_OUT = 7 // for Bulk Out - NumberOfEndpoints = 8 + MSC_ENDPOINT_IN = 8 // for Bulk In + MSC_ENDPOINT_OUT = 9 // for Bulk Out + NumberOfEndpoints = 10 // bmRequestType REQUEST_HOSTTODEVICE = 0x00 From 14fa7e9ec217216921c5114794fd91464986cfc8 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 17 Apr 2025 09:42:21 -0400 Subject: [PATCH 2/4] fix: clean up endpoint masking --- src/machine/machine_rp2_usb.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/machine/machine_rp2_usb.go b/src/machine/machine_rp2_usb.go index 96d8cda4f7..2b46ce43c6 100644 --- a/src/machine/machine_rp2_usb.go +++ b/src/machine/machine_rp2_usb.go @@ -152,23 +152,25 @@ func sendViaEPIn(ep uint32, data []byte, count int) { // Set ENDPOINT_HALT/stall status on a USB IN endpoint. func (dev *USBDevice) SetStallEPIn(ep uint32) { + ep = ep & 0x7F // Prepare buffer control register value if ep == 0 { armEPZeroStall() } val := uint32(usbBuf0CtrlFull) - _usbDPSRAM.EPxBufferControl[ep&0x7F].In.Set(val) + _usbDPSRAM.EPxBufferControl[ep].In.Set(val) val |= uint32(usbBuf0CtrlStall) - _usbDPSRAM.EPxBufferControl[ep&0x7F].In.Set(val) + _usbDPSRAM.EPxBufferControl[ep].In.Set(val) } // Set ENDPOINT_HALT/stall status on a USB OUT endpoint. func (dev *USBDevice) SetStallEPOut(ep uint32) { + ep = ep & 0x7F if ep == 0 { panic("SetStallEPOut: EP0 OUT not valid") } val := uint32(usbBuf0CtrlStall) - _usbDPSRAM.EPxBufferControl[ep&0x7F].Out.Set(val) + _usbDPSRAM.EPxBufferControl[ep].Out.Set(val) } // Clear the ENDPOINT_HALT/stall on a USB IN endpoint. @@ -178,7 +180,7 @@ func (dev *USBDevice) ClearStallEPIn(ep uint32) { _usbDPSRAM.EPxBufferControl[ep].In.ClearBits(val) if epXPIDReset[ep] { // Reset the PID to DATA0 - setEPDataPID(ep&0x7F, false) + setEPDataPID(ep, false) } } From 8db610fc36fbd3b32ae450dec1f02b8335ee2977 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 17 Apr 2025 09:42:48 -0400 Subject: [PATCH 3/4] fix: update stall function references --- src/machine/usb/msc/setup.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/machine/usb/msc/setup.go b/src/machine/usb/msc/setup.go index 733ceb4259..3d5bef2d5a 100644 --- a/src/machine/usb/msc/setup.go +++ b/src/machine/usb/msc/setup.go @@ -117,21 +117,21 @@ func (m *msc) handleReset(setup usb.Setup, wValue uint16) bool { func (m *msc) stallEndpoint(ep uint8) { if ep == usb.MSC_ENDPOINT_IN { m.txStalled = true - machine.SetStallEPIn(usb.MSC_ENDPOINT_IN) + machine.USBDev.SetStallEPIn(usb.MSC_ENDPOINT_IN) } else if ep == usb.MSC_ENDPOINT_OUT { m.rxStalled = true - machine.SetStallEPOut(usb.MSC_ENDPOINT_OUT) + machine.USBDev.SetStallEPOut(usb.MSC_ENDPOINT_OUT) } else if ep == usb.CONTROL_ENDPOINT { - machine.SetStallEPIn(usb.CONTROL_ENDPOINT) + machine.USBDev.SetStallEPIn(usb.CONTROL_ENDPOINT) } } func (m *msc) clearStallEndpoint(ep uint8) { if ep == usb.MSC_ENDPOINT_IN { - machine.ClearStallEPIn(usb.MSC_ENDPOINT_IN) + machine.USBDev.ClearStallEPIn(usb.MSC_ENDPOINT_IN) m.txStalled = false } else if ep == usb.MSC_ENDPOINT_OUT { - machine.ClearStallEPOut(usb.MSC_ENDPOINT_OUT) + machine.USBDev.ClearStallEPOut(usb.MSC_ENDPOINT_OUT) m.rxStalled = false } } From d5bc91c7c2819a03b5b35c1d0b32cbf76309b4d3 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Mon, 12 May 2025 22:38:25 -0400 Subject: [PATCH 4/4] chore: add task queueing for flash writes --- src/machine/machine_rp2040_usb.go | 5 +- src/machine/machine_rp2350_usb.go | 5 +- src/machine/machine_rp2_usb.go | 4 +- src/machine/usb.go | 9 +- src/machine/usb/config.go | 13 +- src/machine/usb/msc/cbw.go | 9 +- src/machine/usb/msc/disk.go | 212 +++++++++++++++++++++----- src/machine/usb/msc/msc.go | 181 ++++++++++++++++++---- src/machine/usb/msc/scsi.go | 131 +++++++++++----- src/machine/usb/msc/scsi/scsi.go | 67 +++++++- src/machine/usb/msc/scsi_inquiry.go | 16 +- src/machine/usb/msc/scsi_readwrite.go | 17 ++- src/machine/usb/msc/scsi_unmap.go | 68 ++------- 13 files changed, 536 insertions(+), 201 deletions(-) diff --git a/src/machine/machine_rp2040_usb.go b/src/machine/machine_rp2040_usb.go index 2ef08fcbe4..4fcce537da 100644 --- a/src/machine/machine_rp2040_usb.go +++ b/src/machine/machine_rp2040_usb.go @@ -95,10 +95,9 @@ func handleUSBIRQ(intr interrupt.Interrupt) { for i := 0; i < 16; i++ { if s2&(1<<(i*2+1)) > 0 { buf := handleEndpointRx(uint32(i)) - if usbRxHandler[i] != nil { - usbRxHandler[i](buf) + if usbRxHandler[i] == nil || usbRxHandler[i](buf) { + AckEndpointRxMessage(uint32(i)) } - handleEndpointRxComplete(uint32(i)) } } diff --git a/src/machine/machine_rp2350_usb.go b/src/machine/machine_rp2350_usb.go index d29a3ae980..890826b902 100644 --- a/src/machine/machine_rp2350_usb.go +++ b/src/machine/machine_rp2350_usb.go @@ -98,10 +98,9 @@ func handleUSBIRQ(intr interrupt.Interrupt) { for i := 0; i < 16; i++ { if s2&(1<<(i*2+1)) > 0 { buf := handleEndpointRx(uint32(i)) - if usbRxHandler[i] != nil { - usbRxHandler[i](buf) + if usbRxHandler[i] == nil || usbRxHandler[i](buf) { + AckEndpointRxMessage(uint32(i)) } - handleEndpointRxComplete(uint32(i)) } } diff --git a/src/machine/machine_rp2_usb.go b/src/machine/machine_rp2_usb.go index 2b46ce43c6..53f8db89fb 100644 --- a/src/machine/machine_rp2_usb.go +++ b/src/machine/machine_rp2_usb.go @@ -115,7 +115,9 @@ func handleEndpointRx(ep uint32) []byte { return _usbDPSRAM.EPxBuffer[ep].Buffer0[:sz] } -func handleEndpointRxComplete(ep uint32) { +// AckEndpointRxMessage is called to acknowledge the completion of a delayed USB OUT transfer. +func AckEndpointRxMessage(ep uint32) { + ep = ep & 0x7F setEPDataPID(ep, !epXdata0[ep]) } diff --git a/src/machine/usb.go b/src/machine/usb.go index 5ded0171c7..b80caa11f7 100644 --- a/src/machine/usb.go +++ b/src/machine/usb.go @@ -121,7 +121,7 @@ var usb_trans_buffer [255]uint8 var ( usbTxHandler [usb.NumberOfEndpoints]func() - usbRxHandler [usb.NumberOfEndpoints]func([]byte) + usbRxHandler [usb.NumberOfEndpoints]func([]byte) bool usbSetupHandler [usb.NumberOfInterfaces]func(usb.Setup) bool usbStallHandler [usb.NumberOfEndpoints]func(usb.Setup) bool @@ -332,7 +332,12 @@ func ConfigureUSBEndpoint(desc descriptor.Descriptor, epSettings []usb.EndpointC } else { endPoints[ep.Index] = uint32(ep.Type | usb.EndpointOut) if ep.RxHandler != nil { - usbRxHandler[ep.Index] = ep.RxHandler + usbRxHandler[ep.Index] = func(b []byte) bool { + ep.RxHandler(b) + return true + } + } else if ep.DelayRxHandler != nil { + usbRxHandler[ep.Index] = ep.DelayRxHandler } } if ep.StallHandler != nil { diff --git a/src/machine/usb/config.go b/src/machine/usb/config.go index 4ce7cd803a..47cce9b2dd 100644 --- a/src/machine/usb/config.go +++ b/src/machine/usb/config.go @@ -1,12 +1,13 @@ package usb type EndpointConfig struct { - Index uint8 - IsIn bool - TxHandler func() - RxHandler func([]byte) - StallHandler func(Setup) bool - Type uint8 + Index uint8 + IsIn bool + TxHandler func() + RxHandler func([]byte) + DelayRxHandler func([]byte) bool + StallHandler func(Setup) bool + Type uint8 } type SetupConfig struct { diff --git a/src/machine/usb/msc/cbw.go b/src/machine/usb/msc/cbw.go index c60302d708..7ebf346c2d 100644 --- a/src/machine/usb/msc/cbw.go +++ b/src/machine/usb/msc/cbw.go @@ -12,7 +12,12 @@ const ( ) type CBW struct { - Data []byte + HasCmd bool + Data []byte +} + +func (c *CBW) Tag() uint32 { + return binary.LittleEndian.Uint32(c.Data[4:8]) } func (c *CBW) length() int { @@ -27,7 +32,7 @@ func (c *CBW) validSignature() bool { return binary.LittleEndian.Uint32(c.Data[:4]) == Signature } -func (c *CBW) scsiCmd() scsi.Cmd { +func (c *CBW) SCSICmd() scsi.Cmd { return scsi.Cmd{Data: c.Data[15:]} } diff --git a/src/machine/usb/msc/disk.go b/src/machine/usb/msc/disk.go index 6423c4ceaf..7b4148b908 100644 --- a/src/machine/usb/msc/disk.go +++ b/src/machine/usb/msc/disk.go @@ -2,72 +2,206 @@ package msc import ( "encoding/binary" + "errors" + "fmt" "machine" + "runtime/interrupt" + "time" ) -var _ machine.BlockDevice = (*DefaultDisk)(nil) +var ( + errWriteOutOfBounds = errors.New("WriteAt offset out of bounds") +) + +// RegisterBlockDevice registers a BlockDevice provider with the MSC driver +func (m *msc) RegisterBlockDevice(dev machine.BlockDevice) { + m.dev = dev + + m.blockRatio = 1 + m.blockSize = uint32(m.dev.WriteBlockSize()) + if m.blockSize < 512 { + // If the block size is less than 512 bytes, we'll scale it up to 512 for compatibility + m.blockRatio = 512 / m.blockSize + m.blockSize = 512 + } + m.blockCount = uint32(m.dev.Size()) / m.blockSize + // FIXME: Figure out what to do if the emulated write block size is larger than the erase block size + + // Set VPD UNMAP fields + for i := range vpdPages { + if vpdPages[i].PageCode == 0xb0 { + // 0xb0 - 5.4.5 Block Limits VPD page (B0h) + if len(vpdPages[i].Data) >= 28 { + // Set the OPTIMAL UNMAP GRANULARITY (write blocks per erase block) + granularity := uint32(dev.EraseBlockSize()) / m.blockSize + binary.BigEndian.PutUint32(vpdPages[i].Data[24:28], granularity) + } + if len(vpdPages[i].Data) >= 32 { + // Set the UNMAP GRANULARITY ALIGNMENT (first sector of first full erase block) + // The unmap granularity alignment is used to calculate an optimal unmap request starting LBA as follows: + // optimal unmap request starting LBA = (n * OPTIMAL UNMAP GRANULARITY) + UNMAP GRANULARITY ALIGNMENT + // where n is zero or any positive integer value + // https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf + + // We assume the block device is aligned to the end of the underlying block device + blockOffset := uint32(dev.EraseBlockSize()) % m.blockSize + binary.BigEndian.PutUint32(vpdPages[i].Data[28:32], blockOffset) + } + break + } + } +} + +var _ machine.BlockDevice = (*RecorderDisk)(nil) -// DefaultDisk is a placeholder disk implementation -type DefaultDisk struct { +// RecorderDisk is a block device that records actions taken on it +type RecorderDisk struct { + data map[int64][]byte + log []RecorderRecord + last time.Time + time time.Time } -// NewDefaultDisk creates a new DefaultDisk instance -func NewDefaultDisk() *DefaultDisk { - return &DefaultDisk{} +type RecorderRecord struct { + OpCode RecorderOpCode + Offset int64 + Length int + Data []byte + Time int64 + valid bool } -func (d *DefaultDisk) Size() int64 { +type RecorderOpCode uint8 + +const ( + RecorderOpCodeRead RecorderOpCode = iota + RecorderOpCodeWrite + RecorderOpCodeEraseBlocks +) + +// NewRecorderDisk creates a new RecorderDisk instance +func NewRecorderDisk(count int) *RecorderDisk { + d := &RecorderDisk{ + data: make(map[int64][]byte), + log: make([]RecorderRecord, 0, count), + last: time.Now(), + } + for i := 0; i < count; i++ { + d.log = append(d.log, RecorderRecord{ + OpCode: RecorderOpCodeRead, + Offset: 0, + Length: 0, + Data: make([]byte, 0, 64), + Time: 0, + }) + } + return d +} + +func (d *RecorderDisk) Size() int64 { return 4096 * int64(d.WriteBlockSize()) // 2MB } -func (d *DefaultDisk) WriteBlockSize() int64 { +func (d *RecorderDisk) WriteBlockSize() int64 { return 512 // 512 bytes } -func (d *DefaultDisk) EraseBlockSize() int64 { +func (d *RecorderDisk) EraseBlockSize() int64 { return 2048 // 4 blocks of 512 bytes } -func (d *DefaultDisk) EraseBlocks(startBlock, numBlocks int64) error { +func (d *RecorderDisk) EraseBlocks(startBlock, numBlocks int64) error { + d.Record(RecorderOpCodeEraseBlocks, startBlock, int(numBlocks), []byte{}) + if interrupt.In() { + // Flash erase commands are not allowed in interrupt context + panic("EraseBlocks attempted in interrupt context") + } return nil } -func (d *DefaultDisk) ReadAt(buffer []byte, offset int64) (int, error) { - n := uint8(offset) - for i := range buffer { - n++ - buffer[i] = n +func (d *RecorderDisk) ReadAt(buffer []byte, offset int64) (int, error) { + d.Record(RecorderOpCodeRead, offset, len(buffer), []byte{}) + sector := offset / d.WriteBlockSize() + if sector < 0 || offset+int64(len(buffer)) > d.Size() { + return 0, fmt.Errorf("read out of bounds: %d", offset) + } + + sectorCount := int64(len(buffer)) / d.WriteBlockSize() + for i := int64(0); i < sectorCount; i++ { + if _, ok := d.data[sector+i]; ok { + n := int(offset % d.WriteBlockSize()) + copy(buffer, d.data[sector+i][n:]) + } } + return len(buffer), nil } -func (d *DefaultDisk) WriteAt(buffer []byte, offset int64) (int, error) { +func (d *RecorderDisk) WriteAt(buffer []byte, offset int64) (int, error) { + if interrupt.In() { + // Flash writes aren't possible in interrupt context + panic("WriteAt attempted in interrupt context") + } + if offset < 0 || offset+int64(len(buffer)) > d.Size() { + return 0, errWriteOutOfBounds + } + + d.Record(RecorderOpCodeWrite, offset, len(buffer), buffer) + + sector := offset / d.WriteBlockSize() + sectorCount := int64(len(buffer)) / d.WriteBlockSize() + for i := int64(0); i < sectorCount; i++ { + _, ok := d.data[sector+i] + if !ok { + d.data[sector+i] = make([]byte, d.WriteBlockSize()) + } + n := int(offset % d.WriteBlockSize()) + copy(d.data[sector+i][n:], buffer) + } return len(buffer), nil } -// RegisterBlockDevice registers a BlockDevice provider with the MSC driver -func (m *msc) RegisterBlockDevice(dev machine.BlockDevice) { - m.dev = dev - - // Set VPD UNMAP fields - for i := range vpdPages { - if vpdPages[i].PageCode == 0xb0 { - // 0xb0 - 5.4.5 Block Limits VPD page (B0h) - if len(vpdPages[i].Data) >= 28 { - // Set the OPTIMAL UNMAP GRANULARITY (write blocks per erase block) - granularity := uint32(dev.EraseBlockSize()) / uint32(dev.WriteBlockSize()) - binary.BigEndian.PutUint32(vpdPages[i].Data[24:28], granularity) - } - /* TODO: Add method for working out the optimal unmap granularity alignment - if len(vpdPages[i].Data) >= 32 { - // Set the UNMAP GRANULARITY ALIGNMENT (first sector of first full erase block) - // The unmap granularity alignment is used to calculate an optimal unmap request starting LBA as follows: - // optimal unmap request starting LBA = (n * OPTIMAL UNMAP GRANULARITY) + UNMAP GRANULARITY ALIGNMENT - // where n is zero or any positive integer value - // https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf - } - */ - break +func (d *RecorderDisk) Record(opCode RecorderOpCode, offset int64, length int, data []byte) { + n := len(d.log) + if n == 0 { + return + } else if n == cap(d.log) { + for i := 0; i < n-1; i++ { + d.log[i] = d.log[i+1] } } + + // Append the new record + d.log[n-1].OpCode = opCode + d.log[n-1].Offset = offset + d.log[n-1].Length = length + d.log[n-1].Data = d.log[n-1].Data[:len(data)] + copy(d.log[n-1].Data, data) + d.log[n-1].Time = time.Since(d.time).Microseconds() + d.time = d.time.Add(time.Since(d.time)) + d.log[n-1].valid = true +} + +func (d *RecorderDisk) ClearLog() { + for i := range d.log { + d.log[i].valid = false + } + d.time = time.Now() +} + +func (d *RecorderDisk) GetLog() []RecorderRecord { + return d.log +} + +func (r RecorderRecord) String() (string, bool) { + opCode := "Unknown" + switch r.OpCode { + case RecorderOpCodeRead: + opCode = "Read" + case RecorderOpCodeWrite: + opCode = "Write" + case RecorderOpCodeEraseBlocks: + opCode = "EraseBlocks" + } + return fmt.Sprintf("%s: %05d+%02d t:%d| % 0x", opCode, r.Offset, r.Length, r.Time, r.Data), r.valid } diff --git a/src/machine/usb/msc/msc.go b/src/machine/usb/msc/msc.go index cfc88dc308..1fe4c96bf9 100644 --- a/src/machine/usb/msc/msc.go +++ b/src/machine/usb/msc/msc.go @@ -7,6 +7,7 @@ import ( "machine/usb/descriptor" "machine/usb/msc/csw" "machine/usb/msc/scsi" + "time" ) type mscState uint8 @@ -27,7 +28,7 @@ var MSC *msc type msc struct { buf []byte // Buffer for incoming/outgoing data - waitTxc bool // Wait for transmission completion confirmation + taskQueued bool // Flag to indicate if the buffer has a task queued rxStalled bool // Flag to indicate if the RX endpoint is stalled txStalled bool // Flag to indicate if the TX endpoint is stalled maxPacketSize uint32 // Maximum packet size for the IN endpoint @@ -41,9 +42,12 @@ type msc struct { cswBuf []byte // CSW response buffer state mscState - maxLUN uint8 // Maximum Logical Unit Number (n-1 for n LUNs) - dev machine.BlockDevice - readOnly bool + maxLUN uint8 // Maximum Logical Unit Number (n-1 for n LUNs) + dev machine.BlockDevice + blockCount uint32 // Number of blocks in the device + blockRatio uint32 // Number of real blocks per emulated block + blockSize uint32 // Write block size of the device + readOnly bool vendorID [8]byte // Max 8 ASCII characters productID [16]byte // Max 16 ASCII characters @@ -60,26 +64,24 @@ type msc struct { packetsSent uint32 // Number of packets sent packetsRecv uint32 // Number of packets received packetsAcked uint32 // Number of packets acknowledged + packetErrors uint32 // Number of error packets sent canaryA uint32 // Canaries for debugging canaryB uint32 // Canaries for debugging canaryC uint32 // Canaries for debugging canaryBuf [31]byte // CBW buffer container -} - -func init() { - // Initialize the USB Mass Storage Class (MSC) port - MSC = newMSC() + cbwLog []CBW // CBW log container + msgLog []MsgLog // TX/RX log container } // Port returns the USB Mass Storage port -func Port() *msc { +func Port(dev machine.BlockDevice) *msc { if MSC == nil { - MSC = newMSC() + MSC = newMSC(dev) } return MSC } -func newMSC() *msc { +func newMSC(dev machine.BlockDevice) *msc { // Size our buffer to match the maximum packet size of the IN endpoint maxPacketSize := descriptor.EndpointEP8IN.GetMaxPacketSize() m := &msc{ @@ -88,7 +90,7 @@ func newMSC() *msc { cbw: &CBW{Data: make([]byte, 31)}, maxPacketSize: uint32(maxPacketSize), } - m.RegisterBlockDevice(NewDefaultDisk()) + m.RegisterBlockDevice(dev) // Set default inquiry data fields m.SetVendorID("TinyGo") @@ -106,11 +108,11 @@ func newMSC() *msc { StallHandler: setupPacketHandler, }, { - Index: usb.MSC_ENDPOINT_OUT, - IsIn: false, - Type: usb.ENDPOINT_TYPE_BULK, - RxHandler: rxHandler, - StallHandler: setupPacketHandler, + Index: usb.MSC_ENDPOINT_OUT, + IsIn: false, + Type: usb.ENDPOINT_TYPE_BULK, + DelayRxHandler: rxHandler, + StallHandler: setupPacketHandler, }, }, []usb.SetupConfig{ @@ -121,9 +123,31 @@ func newMSC() *msc { }, ) + go m.processTasks() + return m } +func (m *msc) processTasks() { + // Process tasks that cannot be done in an interrupt context + for { + if m.taskQueued { + cmd := m.cbw.SCSICmd() + switch cmd.CmdType() { + case scsi.CmdWrite: + m.scsiWrite(m.buf) + case scsi.CmdUnmap: + m.scsiUnmap(m.buf) + } + + // If we were waiting for the task buffer to empty, clear the flag + m.taskQueued = false + machine.AckEndpointRxMessage(usb.MSC_ENDPOINT_OUT) + } + time.Sleep(10 * time.Microsecond) + } +} + func (m *msc) ready() bool { return m.dev != nil } @@ -138,9 +162,11 @@ func (m *msc) resetBuffer(length int) { func (m *msc) sendUSBPacket(b []byte) { if machine.USBDev.InitEndpointComplete { + if len(m.msgLog) > 0 { + m.saveMsgLog(b, true) // FIXME: Cleanup + } m.packetsSent++ // TODO: Cleanup? // Send the USB packet - m.waitTxc = true machine.SendUSBInPacket(usb.MSC_ENDPOINT_IN, b) } } @@ -164,15 +190,19 @@ func txHandler() { } func (m *msc) txHandler() { - m.waitTxc = false m.run([]byte{}, false) } -func rxHandler(b []byte) { +func rxHandler(b []byte) bool { + ack := true if MSC != nil { + if len(MSC.msgLog) > 0 { + MSC.saveMsgLog(b, false) // FIXME: Cleanup + } MSC.packetsRecv++ // TODO: Cleanup? - MSC.run(b, true) + ack = MSC.run(b, true) } + return ack } /* @@ -202,7 +232,7 @@ command execution and moves to mscStateStatusSent. 6. The host ACKs the CSW and the MSC moves back to mscStateCmd, waiting for the next CBW. */ -func (m *msc) run(b []byte, isEpOut bool) { +func (m *msc) run(b []byte, isEpOut bool) bool { // TODO: Cleanup? if m.prevState != m.state { m.savedState = m.prevState @@ -211,13 +241,20 @@ func (m *msc) run(b []byte, isEpOut bool) { } m.loops++ + ack := true + switch m.state { case mscStateCmd: // Receiving a new command block wrapper (CBW) // IN endpoint transfer complete confirmation, no action needed if !isEpOut { - return + return ack + } + + // Save debug log of received CBW commands + if len(m.cbwLog) > 0 { + m.saveCBW(b) } // Create a temporary CBW wrapper to validate the incoming data. Has to be temporary @@ -232,7 +269,7 @@ func (m *msc) run(b []byte, isEpOut bool) { m.stallEndpoint(usb.MSC_ENDPOINT_IN) m.stallEndpoint(usb.MSC_ENDPOINT_OUT) m.stallEndpoint(usb.CONTROL_ENDPOINT) - return + return ack } // Save the validated CBW for later reference @@ -249,7 +286,7 @@ func (m *msc) run(b []byte, isEpOut bool) { case mscStateData: // Transfer data - m.scsiDataTransfer(b) + ack = m.scsiDataTransfer(b) case mscStateStatus: // Sending CSW status response @@ -282,14 +319,16 @@ func (m *msc) run(b []byte, isEpOut bool) { } else if m.sendZLP || m.queuedBytes == m.maxPacketSize { // If the last packet is wMaxPacketSize we need to first send a zero-length packet // to indicate the end of the transfer before we can send a CSW - m.sendUSBPacket(m.buf[:0]) m.queuedBytes = 0 m.sendZLP = false + m.sendUSBPacket(m.buf[:0]) } else { m.sendCSW(m.respStatus) m.state = mscStateCmd } } + + return ack } // TODO: Cleanup @@ -320,16 +359,94 @@ func stateToString(state mscState) string { func (m *msc) State() string { switch m.state { case mscStateCmd: - return fmt.Sprintf("(%d/%d/%d) [%d/%d/%d] CmdWait (%d) <- %s", m.canaryA, m.canaryB, m.canaryC, m.packetsRecv, m.packetsSent, m.packetsAcked, m.loops, stateToString(m.savedState)) + return fmt.Sprintf("(%d/%d/%d) [%d/%d/%d/%d] CmdWait (%d) <- %s", m.canaryA, m.canaryB, m.canaryC, m.packetsRecv, m.packetsSent, m.packetsAcked, m.packetErrors, m.loops, stateToString(m.savedState)) case mscStateData: - return fmt.Sprintf("(%d/%d/%d) [%d/%d/%d] Data [%d/%d] (%d) <- %s", m.canaryA, m.canaryB, m.canaryC, m.packetsRecv, m.packetsSent, m.packetsAcked, m.sentBytes, m.totalBytes, m.loops, stateToString(m.savedState)) + return fmt.Sprintf("(%d/%d/%d) [%d/%d/%d/%d] Data [%d/%d/%d] (%d) <- %s", m.canaryA, m.canaryB, m.canaryC, m.packetsRecv, m.packetsSent, m.packetsAcked, m.packetErrors, m.queuedBytes, m.sentBytes, m.totalBytes, m.loops, stateToString(m.savedState)) case mscStateStatus: - return fmt.Sprintf("(%d/%d/%d) [%d/%d/%d] SendStatus (%d) <- %s", m.canaryA, m.canaryB, m.canaryC, m.packetsRecv, m.packetsSent, m.packetsAcked, m.loops, stateToString(m.savedState)) + return fmt.Sprintf("(%d/%d/%d) [%d/%d/%d/%d] SendStatus (%d) <- %s", m.canaryA, m.canaryB, m.canaryC, m.packetsRecv, m.packetsSent, m.packetsAcked, m.packetErrors, m.loops, stateToString(m.savedState)) case mscStateStatusSent: - return fmt.Sprintf("(%d/%d/%d) [%d/%d/%d] StatusSent (%d) <- %s", m.canaryA, m.canaryB, m.canaryC, m.packetsRecv, m.packetsSent, m.packetsAcked, m.loops, stateToString(m.savedState)) + return fmt.Sprintf("(%d/%d/%d) [%d/%d/%d/%d] StatusSent (%d) <- %s", m.canaryA, m.canaryB, m.canaryC, m.packetsRecv, m.packetsSent, m.packetsAcked, m.packetErrors, m.loops, stateToString(m.savedState)) case mscStateNeedReset: - return fmt.Sprintf("(%d/%d/%d) [%d/%d/%d] NeedReset (%d) <- %s", m.canaryA, m.canaryB, m.canaryC, m.packetsRecv, m.packetsSent, m.packetsAcked, m.loops, stateToString(m.savedState)) + return fmt.Sprintf("(%d/%d/%d) [%d/%d/%d/%d] NeedReset (%d) <- %s", m.canaryA, m.canaryB, m.canaryC, m.packetsRecv, m.packetsSent, m.packetsAcked, m.packetErrors, m.loops, stateToString(m.savedState)) default: return "Unknown" } } + +func (m *msc) saveCBW(b []byte) { + n := len(m.cbwLog) - 1 + // Shift the log entries up to make room for a new entry + for i := 0; i < n; i++ { + copy(m.cbwLog[i].Data, m.cbwLog[i+1].Data) + m.cbwLog[i].HasCmd = m.cbwLog[i+1].HasCmd + } + copy(m.cbwLog[n].Data, b) + m.cbwLog[n].HasCmd = true +} + +func (m *msc) SetCBWLogSize(n int) { + // Initialize the CBW log with a fixed size + m.cbwLog = make([]CBW, n) + for i := range m.cbwLog { + m.cbwLog[i].Data = make([]byte, 31) + } +} + +func (m *msc) CBWLog() []CBW { + return m.cbwLog +} + +type MsgLog struct { + CBW CBW + Data []byte + Length int + Tx bool + Valid bool +} + +func (m *msc) saveMsgLog(b []byte, tx bool) { + n := len(m.msgLog) - 1 + // Shift the log entries up to make room for a new entry + for i := 0; i < n; i++ { + copy(m.msgLog[i].CBW.Data, m.msgLog[i+1].CBW.Data) + copy(m.msgLog[i].Data, m.msgLog[i+1].Data) + m.msgLog[i].Length = m.msgLog[i+1].Length + m.msgLog[i].Tx = m.msgLog[i+1].Tx + m.msgLog[i].Valid = m.msgLog[i+1].Valid + } + copy(m.msgLog[n].CBW.Data, m.cbw.Data) + copy(m.msgLog[n].Data, b) + m.msgLog[n].Length = len(b) + m.msgLog[n].Tx = tx + m.msgLog[n].Valid = true +} + +func (m *msc) SetMsgLogSize(n int) { + // Initialize the TX log with a fixed size + m.msgLog = make([]MsgLog, n) + for i := range m.msgLog { + m.msgLog[i].CBW = CBW{Data: make([]byte, cbwMsgLen)} + m.msgLog[i].Data = make([]byte, m.maxPacketSize) + } +} + +func (m *msc) MsgLog() []MsgLog { + return m.msgLog +} + +func (m *msc) ClearMsgLog() { + // Clear the TX log + for i := range m.msgLog { + m.msgLog[i].Valid = false + } +} + +func allZero(b []byte) bool { + // FIXME: Cleanup + for i := 0; i < len(b); i++ { + if b[i] != 0 { + return false + } + } + return len(b) != 0 +} diff --git a/src/machine/usb/msc/scsi.go b/src/machine/usb/msc/scsi.go index c810c9b13e..86925138e1 100644 --- a/src/machine/usb/msc/scsi.go +++ b/src/machine/usb/msc/scsi.go @@ -8,7 +8,7 @@ import ( ) func (m *msc) scsiCmdBegin(b []byte) { - cmd := m.cbw.scsiCmd() + cmd := m.cbw.SCSICmd() cmdType := cmd.CmdType() // Handle multi-packet commands @@ -41,10 +41,12 @@ func (m *msc) scsiCmdBegin(b []byte) { m.scsiCmdReadFormatCapacity(cmd) case scsi.CmdInquiry: m.scsiCmdInquiry(cmd) - case scsi.CmdModeSense: - m.scsiCmdModeSense() + case scsi.CmdModeSense6, scsi.CmdModeSense10: + m.scsiCmdModeSense(cmd) case scsi.CmdRequestSense: m.scsiCmdRequestSense() + case scsi.CmdPreventAllowMediumRemoval: + m.scsiCmdPreventAllowMediumRemoval(cmd) default: // We don't support this command, error out m.sendScsiError(csw.StatusFailed, scsi.SenseIllegalRequest, scsi.SenseCodeInvalidCmdOpCode) @@ -75,17 +77,17 @@ func (m *msc) scsiCmdBegin(b []byte) { } } -func (m *msc) scsiDataTransfer(b []byte) { - cmd := m.cbw.scsiCmd() +func (m *msc) scsiDataTransfer(b []byte) bool { + cmd := m.cbw.SCSICmd() cmdType := cmd.CmdType() switch cmdType { - case scsi.CmdWrite: - m.scsiWrite(cmd, b) - return - case scsi.CmdUnmap: - m.scsiUnmap(cmd, b) - return + case scsi.CmdWrite, scsi.CmdUnmap: + if m.readOnly { + m.sendScsiError(csw.StatusFailed, scsi.SenseDataProtect, scsi.SenseCodeWriteProtected) + return true + } + return m.scsiQueueTask(b) } // Update our sent bytes count to include the just-confirmed bytes @@ -99,6 +101,8 @@ func (m *msc) scsiDataTransfer(b []byte) { } else { // Other multi-packet commands are rejected in m.scsiCmdBegin() } + + return true } func (m *msc) scsiTestUnitReady() { @@ -122,14 +126,10 @@ func (m *msc) scsiCmdReadCapacity(cmd scsi.Cmd) { m.resetBuffer(scsi.ReadCapacityRespLen) m.queuedBytes = scsi.ReadCapacityRespLen - blockSize := uint32(m.dev.WriteBlockSize()) - blockCount := uint32(m.dev.Size()) / blockSize - lastBlock := blockCount - 1 - // Last LBA address (big endian) - binary.BigEndian.PutUint32(m.buf[:4], lastBlock) + binary.BigEndian.PutUint32(m.buf[:4], m.blockCount-1) // Block size (big endian) - binary.BigEndian.PutUint32(m.buf[4:8], blockSize) + binary.BigEndian.PutUint32(m.buf[4:8], m.blockSize) } func (m *msc) scsiCmdReadFormatCapacity(cmd scsi.Cmd) { @@ -139,35 +139,70 @@ func (m *msc) scsiCmdReadFormatCapacity(cmd scsi.Cmd) { // bytes 0-2 - reserved m.buf[3] = 8 // Capacity list length - blockSize := uint32(m.dev.WriteBlockSize()) - // Number of blocks (big endian) - binary.BigEndian.PutUint32(m.buf[4:8], uint32(m.dev.Size())/blockSize) + binary.BigEndian.PutUint32(m.buf[4:8], m.blockCount) // Block size (24-bit, big endian) - binary.BigEndian.PutUint32(m.buf[8:12], blockSize) + binary.BigEndian.PutUint32(m.buf[8:12], m.blockSize) // Descriptor Type - formatted media m.buf[8] = 2 } -// MODE SENSE(6) - Only used here to indicate that the device is write protected -func (m *msc) scsiCmdModeSense() { - m.resetBuffer(scsi.ModeSenseRespLen) - m.queuedBytes = scsi.ModeSenseRespLen +// MODE SENSE(6) / MODE SENSE(10) - Only used here to indicate that the device is write protected +func (m *msc) scsiCmdModeSense(cmd scsi.Cmd) { + respLen := uint32(scsi.ModeSense6RespLen) + if cmd.CmdType() == scsi.CmdModeSense10 { + respLen = scsi.ModeSense10RespLen + } + m.resetBuffer(int(respLen)) + m.queuedBytes = respLen // The host allows a good amount of leeway in response size // Reset total bytes to what we'll actually send - if m.totalBytes > scsi.ModeSenseRespLen { - m.totalBytes = scsi.ModeSenseRespLen + if m.totalBytes > respLen { + m.totalBytes = respLen + m.sendZLP = true } - // byte 0 - Number of bytes after this one - m.buf[0] = scsi.ModeSenseRespLen - 1 - // byte 1 - Medium type (0x00 for direct access block device) + readOnly := byte(0) if m.readOnly { + readOnly = 0x80 + } + + switch cmd.CmdType() { + case scsi.CmdModeSense6: + // byte 0 - Number of bytes after this one + m.buf[0] = byte(respLen) - 1 + // byte 1 - Medium type (0x00 for direct access block device) // Bit 7 indicates write protected - m.buf[2] = 0x80 + m.buf[2] = readOnly + // byte 3 - Block descriptor length: 0 (not supported) + case scsi.CmdModeSense10: + // bytes 0-1 - Number of bytes after this one + m.buf[1] = byte(respLen) - 2 + // byte 2 - Medium type (0x00 for direct access block device) + // Bit 7 indicates write protected + m.buf[3] = readOnly } - // byte 3 - Block descriptor length (not supported) +} + +// PREVENT/ALLOW MEDIUM REMOVAL - A flash drive doesn't have a removable medium, so this is a no-op +func (m *msc) scsiCmdPreventAllowMediumRemoval(cmd scsi.Cmd) { + m.resetBuffer(0) + m.queuedBytes = 0 + + // Check if the device is ready + if !m.ready() { + // If not ready set sense data + m.senseKey = scsi.SenseNotReady + m.addlSenseCode = scsi.SenseCodeMediumNotPresent + m.addlSenseQualifier = 0x00 + } else { + m.senseKey = 0 + m.addlSenseCode = 0 + m.addlSenseQualifier = 0 + } + + m.state = mscStateStatus } // REQUEST SENSE - Returns error status codes when an error status is sent @@ -196,8 +231,6 @@ func (m *msc) scsiCmdRequestSense() { func (m *msc) scsiCmdUnmap(cmd scsi.Cmd) { // Unmap sends a header in the CBW and a parameter list in the data stage - // The parameter list header is 4 bytes and each parameter list item is 16 bytes - // The parameter list has an 8 byte header and 16 bytes per item. If it's less than 24 bytes it's // not the format we're expecting and we won't be able to decode it. Same for if there isn't a // 8 byte header plus multiples of 16 bytes after that @@ -206,15 +239,35 @@ func (m *msc) scsiCmdUnmap(cmd scsi.Cmd) { m.sendScsiError(csw.StatusFailed, scsi.SenseIllegalRequest, scsi.SenseCodeInvalidFieldInCDB) return } +} - // The parameter list header has an 8 byte header and 16 bytes per item so if there are more than - // three items in the list one of them will overflow into the next packet. We'll save those 8 bytes - // in m.buf and use its length (starting at zero) to indicate whether we're currently split between - // packets - m.resetBuffer(0) +func (m *msc) scsiQueueTask(b []byte) bool { + // Check if the incoming data is larger than our buffer + if len(b) > cap(m.buf) { + m.sendScsiError(csw.StatusFailed, scsi.SenseIllegalRequest, scsi.SenseCodeInvalidFieldInCDB) + return true + } + + // Save the incoming data in our buffer for processing outside of interrupt context + if m.taskQueued { + // If we already have a full task queue we can't accept this data + m.sendScsiError(csw.StatusFailed, scsi.SenseAbortedCommand, scsi.SenseCodeMsgReject) + return true + } + + // Copy the queued task data into our buffer + m.buf = m.buf[:len(b)] + copy(m.buf, b) + m.queuedBytes = uint32(len(b)) + m.taskQueued = true + + // Don't acknowledge the incoming data until we can process it + return false } func (m *msc) sendScsiError(status csw.Status, key scsi.Sense, code scsi.SenseCode) { + m.packetErrors++ // TODO: Cleanup? + // Generate CSW into m.cswBuf residue := m.totalBytes - m.sentBytes diff --git a/src/machine/usb/msc/scsi/scsi.go b/src/machine/usb/msc/scsi/scsi.go index 7ccd902abb..5b63f67b3b 100644 --- a/src/machine/usb/msc/scsi/scsi.go +++ b/src/machine/usb/msc/scsi/scsi.go @@ -1,6 +1,9 @@ package scsi -import "encoding/binary" +import ( + "encoding/binary" + "fmt" +) type Cmd struct { Data []byte @@ -18,14 +21,28 @@ func (c *Cmd) LBA() uint32 { return binary.BigEndian.Uint32(c.Data[2:6]) } +func (c Cmd) String() string { + cmdType := c.CmdType() + switch cmdType { + case CmdRead: + return fmt.Sprintf("%-28s LBA: % 3d, Block Count: %d", cmdType, c.LBA(), c.BlockCount()) + case CmdWrite: + return fmt.Sprintf("%-28s LBA: % 3d, Block Count: %d", cmdType, c.LBA(), c.BlockCount()) + default: + return fmt.Sprintf("%-28s % x", cmdType, c.Data) + } +} + type CmdType uint8 const ( CmdTestUnitReady CmdType = 0x00 // TEST UNIT READY is used to determine if a device is ready to transfer data (read/write). The device does not perform a self-test operation CmdRequestSense CmdType = 0x03 // REQUEST SENSE returns the current sense data (status or error information) CmdInquiry CmdType = 0x12 // INQUIRY is used to obtain basic information from a target device - CmdModeSelect CmdType = 0x15 // MODE SELECT (6) provides a means for the application client to specify medium, logical unit, or peripheral device parameters to the device server - CmdModeSense CmdType = 0x1A // MODE SENSE (6) provides a means for a device server to report parameters to an application client + CmdModeSelect6 CmdType = 0x15 // MODE SELECT (6) provides a means for the application client to specify medium, logical unit, or peripheral device parameters to the device server + CmdModeSelect10 CmdType = 0x55 // MODE SELECT (10) provides a means for the application client to specify medium, logical unit, or peripheral device parameters to the device server + CmdModeSense6 CmdType = 0x1A // MODE SENSE (6) provides a means for a device server to report parameters to an application client + CmdModeSense10 CmdType = 0x5A // MODE SENSE (10) provides a means for a device server to report parameters to an application client with 64-bit logical block addressing CmdStartStopUnit CmdType = 0x1B // START STOP UNIT is used to start or stop the medium in a device server CmdPreventAllowMediumRemoval CmdType = 0x1E // PREVENT ALLOW MEDIUM REMOVAL is used to prevent or allow the removal of storage medium from a device server CmdReadFormatCapacity CmdType = 0x23 // READ FORMAT CAPACITY allows the Host to request a list of the possible format capacities for an installed writable media @@ -35,6 +52,41 @@ const ( CmdUnmap CmdType = 0x42 // UNMAP command is used to inform the device server that the specified logical block(s) are no longer in use ) +func (c CmdType) String() string { + switch c { + case CmdTestUnitReady: + return "TEST UNIT READY" + case CmdRequestSense: + return "REQUEST SENSE" + case CmdInquiry: + return "INQUIRY" + case CmdModeSelect6: + return "MODE SELECT (6)" + case CmdModeSelect10: + return "MODE SELECT (10)" + case CmdModeSense6: + return "MODE SENSE (6)" + case CmdModeSense10: + return "MODE SENSE (10)" + case CmdStartStopUnit: + return "START STOP UNIT" + case CmdPreventAllowMediumRemoval: + return "PREVENT ALLOW MEDIUM REMOVAL" + case CmdReadFormatCapacity: + return "READ FORMAT CAPACITY" + case CmdReadCapacity: + return "READ CAPACITY" + case CmdRead: + return "READ (10)" + case CmdWrite: + return "WRITE (10)" + case CmdUnmap: + return "UNMAP" + default: + return fmt.Sprintf("Unknown Command (0x%0x)", byte(c)) + } +} + type Sense uint8 const ( @@ -68,13 +120,20 @@ const ( // SenseDataProtect SenseCodeWriteProtected SenseCode = 0x27 // The media is write protected + // SenseAbortedCommand + SenseCodeLUNCommFailure SenseCode = 0x08 // LUN communication failure + SenseCodeAbortedCmd SenseCode = 0x0B // The command was aborted by the device + SenseCodeMsgReject SenseCode = 0x43 // The command was rejected by the device + SenseCodeOverlapCmdAttempted SenseCode = 0x4E // The command was rejected by the device because it was overlapped by another command + // SenseVolumeOverflow SenseCodeLBAOutOfRange SenseCode = 0x21 // The logical block address (LBA) is beyond the end of the volume ) const ( InquiryRespLen = 36 - ModeSenseRespLen = 4 + ModeSense6RespLen = 4 + ModeSense10RespLen = 8 ReadCapacityRespLen = 8 ReadFormatCapacityRespLen = 12 RequestSenseRespLen = 18 diff --git a/src/machine/usb/msc/scsi_inquiry.go b/src/machine/usb/msc/scsi_inquiry.go index 1f1ac4e508..ae7028f63f 100644 --- a/src/machine/usb/msc/scsi_inquiry.go +++ b/src/machine/usb/msc/scsi_inquiry.go @@ -28,7 +28,7 @@ var vpdPages = []vpdPage{ 0x00, 0x00, 0x00, 0x00, // OPTIMAL TRANSFER LENGTH - Not supported 0x00, 0x00, 0x00, 0x00, // MAXIMUM PREFETCH LENGTH - Not supported 0xFF, 0xFF, 0xFF, 0xFF, // MAXIMUM UNMAP LBA COUNT - Maximum count supported - 0xFF, 0xFF, 0xFF, 0xFF, // MAXIMUM UNMAP BLOCK DESCRIPTOR COUNT - Maximum count supported + 0x00, 0x00, 0x00, 0x03, // MAXIMUM UNMAP BLOCK DESCRIPTOR COUNT - Max 3 descriptors 0x00, 0x00, 0x00, 0x00, // OPTIMAL UNMAP GRANULARITY 0x00, 0x00, 0x00, 0x00, // UNMAP GRANULARITY ALIGNMENT (bit 7 on byte 28 sets UGAVALID) // From here on all bytes are zero and can be omitted from the response @@ -145,7 +145,7 @@ func (m *msc) scsiStdInquiry(cmd scsi.Cmd) { m.totalBytes = scsi.InquiryRespLen // byte 0 - Device Type (0x00 for direct access block device) - // byte 1 - Removable bit + // byte 1 - Removable media bit m.buf[1] = 0x80 // byte 2 - Version 0x00 - We claim conformance to no standard // byte 3 - Response data format @@ -156,15 +156,9 @@ func (m *msc) scsiStdInquiry(cmd scsi.Cmd) { // byte 6 - Not used // byte 7 - Not used // bytes 8-15 - Vendor ID - for i := 0; i < 8; i++ { - m.buf[8+i] = m.vendorID[i] - } + copy(m.buf[8:16], m.vendorID[:]) // bytes 16-31 - Product ID - for i := 0; i < 16; i++ { - m.buf[16+i] = m.productID[i] - } + copy(m.buf[16:32], m.productID[:]) // bytes 32-35 - Product revision level - for i := 0; i < 4; i++ { - m.buf[32+i] = m.productRev[i] - } + copy(m.buf[32:36], m.productRev[:]) } diff --git a/src/machine/usb/msc/scsi_readwrite.go b/src/machine/usb/msc/scsi_readwrite.go index 3b582b102f..c8af37f48d 100644 --- a/src/machine/usb/msc/scsi_readwrite.go +++ b/src/machine/usb/msc/scsi_readwrite.go @@ -13,7 +13,7 @@ func (m *msc) scsiCmdReadWrite(cmd scsi.Cmd, b []byte) { if cmd.CmdType() == scsi.CmdRead { m.scsiRead(cmd) } else { - m.scsiWrite(cmd, b) + // WRITE(10) and UNMAP commands don't take any action until the data stage begins } } else { // Zero byte transfer. No practical use case @@ -63,15 +63,16 @@ func (m *msc) scsiRead(cmd scsi.Cmd) { if readEnd > uint32(cap(m.buf)) { readEnd = uint32(cap(m.buf)) } - m.buf = m.buf[:readEnd] + // Resize the buffer to fit the read size + m.resetBuffer(int(readEnd)) - // Calculate our read address given the already sent bytes and the block size - offset := int64(cmd.LBA()*uint32(m.dev.WriteBlockSize())) + int64(m.sentBytes) + // Calculate our read address given the already sent bytes and the block size and emulated block size + offset := int64(cmd.LBA()*m.blockSize) + int64(m.sentBytes) // Read data from the emulated block device n, err := m.dev.ReadAt(m.buf[:readEnd], offset) if err != nil || n == 0 { - m.sendScsiError(csw.StatusFailed, scsi.SenseNotReady, 0x3a) + m.sendScsiError(csw.StatusFailed, scsi.SenseNotReady, scsi.SenseCodeMediumNotPresent) return } @@ -79,14 +80,15 @@ func (m *msc) scsiRead(cmd scsi.Cmd) { m.sendUSBPacket(m.buf) } -func (m *msc) scsiWrite(cmd scsi.Cmd, b []byte) { +func (m *msc) scsiWrite(b []byte) { if m.readOnly { m.sendScsiError(csw.StatusFailed, scsi.SenseDataProtect, scsi.SenseCodeWriteProtected) return } // Calculate our write address given the already sent bytes and the block size - offset := int64(cmd.LBA()*uint32(m.dev.WriteBlockSize())) + int64(m.sentBytes) + cmd := m.cbw.SCSICmd() + offset := int64(cmd.LBA()*m.blockSize) + int64(m.sentBytes) // Write data to the emulated block device n, err := m.dev.WriteAt(b, offset) @@ -100,5 +102,6 @@ func (m *msc) scsiWrite(cmd scsi.Cmd, b []byte) { if m.sentBytes >= m.totalBytes { // Data transfer is complete, send CSW m.state = mscStateStatus + m.run([]byte{}, true) } } diff --git a/src/machine/usb/msc/scsi_unmap.go b/src/machine/usb/msc/scsi_unmap.go index c387050bf4..8a23534c00 100644 --- a/src/machine/usb/msc/scsi_unmap.go +++ b/src/machine/usb/msc/scsi_unmap.go @@ -21,7 +21,7 @@ func (e Error) Error() string { } } -func (m *msc) scsiUnmap(cmd scsi.Cmd, b []byte) { +func (m *msc) scsiUnmap(b []byte) { // Execute Order 66 (0x42) to wipe out the blocks // 3.54 Unmap Command (SBC-4) // https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf @@ -33,62 +33,23 @@ func (m *msc) scsiUnmap(cmd scsi.Cmd, b []byte) { // blockDescLen is the remaining length of block descriptors in the message, offset 8 bytes from // the start of this packet var blockDescLen uint16 - numBlocks := uint64(m.dev.Size() / m.dev.WriteBlockSize()) - // m.buf being zero-length indicates we're in the first packet in the data stage - if len(m.buf) == 0 { - // Decode the parameter list - msgLen := binary.BigEndian.Uint16(b[:2]) - // Length of the block descriptor portion of the message - blockDescLen = binary.BigEndian.Uint16(b[2:4]) - // Do some sanity checks on the message lengths - if msgLen < 8 || blockDescLen < 16 || msgLen-blockDescLen != 6 { - m.sendScsiError(csw.StatusFailed, scsi.SenseIllegalRequest, scsi.SenseCodeInvalidFieldInCDB) - return - } - - // If the message is going to overflow our packet we need to save the first half of the last - // block descriptor (last 8 bytes of the message) in m.buf to be used in the next packet - if msgLen > uint16(len(b)) { - m.resetBuffer(18) - // Save the overall block descriptor length - m.buf[0] = b[2] - m.buf[1] = b[3] - // And the remaining bytes - bufLen := len(b) - 8 - binary.BigEndian.PutUint16(m.buf[2:4], uint16(bufLen)) - } - } else { - // On subsequent packets we need to extract the remaining block descriptor length from m.buf - blockDescLen = binary.BigEndian.Uint16(m.buf[0:2]) - - // Take care of the last descriptor block from the previous packet - // Copy the first 8 blocks from b to the end of m.buf to make it a full descriptor - copy(m.buf[10:], b) - err := m.unmapBlocksFromDescriptor(m.buf[2:], numBlocks) - if err != nil { - // TODO: Might need a better error code here for device errors? - m.sendScsiError(csw.StatusFailed, scsi.SenseVolumeOverflow, scsi.SenseCodeLBAOutOfRange) - return - } - blockDescLen -= 16 + // Decode the parameter list + msgLen := binary.BigEndian.Uint16(b[:2]) + // Length of the block descriptor portion of the message + blockDescLen = binary.BigEndian.Uint16(b[2:4]) + // Do some sanity checks on the message lengths (max 3 block descriptors to fit in one 64 byte packet) + if msgLen < 8 || blockDescLen < 16 || msgLen-blockDescLen != 6 || blockDescLen > (3*16) { + m.sendScsiError(csw.StatusFailed, scsi.SenseIllegalRequest, scsi.SenseCodeInvalidFieldInCDB) + return } // descEnd marks the end of the last full block descriptor in this packet descEnd := int(blockDescLen + 8) - if descEnd > len(b) { - descEnd = len(b) - 8 - - // If the command overflows the current packet, save the leftover bytes in m.buf and decrement - // blockDescLen to reflect the full block descriptors received so far - copy(m.buf[2:], b[descEnd:]) - remaining := blockDescLen - uint16(descEnd) - binary.BigEndian.PutUint16(m.buf[:2], remaining) - } // Unmap the blocks we can from this packet for i := 8; i < descEnd; i += 16 { - err := m.unmapBlocksFromDescriptor(b[i:], numBlocks) + err := m.unmapBlocksFromDescriptor(b[i:], uint64(m.blockCount)) if err != nil { // TODO: Might need a better error code here for device errors? m.sendScsiError(csw.StatusFailed, scsi.SenseVolumeOverflow, scsi.SenseCodeLBAOutOfRange) @@ -100,6 +61,7 @@ func (m *msc) scsiUnmap(cmd scsi.Cmd, b []byte) { if m.sentBytes >= m.totalBytes { // Order 66 complete, send CSW to establish galactic empire m.state = mscStateStatus + m.run([]byte{}, true) } } @@ -109,13 +71,15 @@ func (m *msc) unmapBlocksFromDescriptor(b []byte, numBlocks uint64) error { // No blocks to unmap. Explicitly not an error per the spec return nil } - lba := binary.BigEndian.Uint64(b[0:8]) + // This is technically a 64-bit LBA, but we can't address that many bytes + // let alone blocks, so we just use the lower 32 bits + lba := binary.BigEndian.Uint32(b[4:8]) // Make sure the unmap command doesn't extend past the end of the volume - if lba+uint64(blockCount) > numBlocks { + if lba+blockCount > m.blockCount { return errorLBAOutOfRange } // Unmap the blocks - return m.dev.EraseBlocks(int64(lba), int64(blockCount)) + return m.dev.EraseBlocks(int64(lba*m.blockRatio), int64(blockCount)) }