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) } } 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..3d5bef2d5a --- /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.USBDev.SetStallEPIn(usb.MSC_ENDPOINT_IN) + } else if ep == usb.MSC_ENDPOINT_OUT { + m.rxStalled = true + machine.USBDev.SetStallEPOut(usb.MSC_ENDPOINT_OUT) + } else if ep == usb.CONTROL_ENDPOINT { + machine.USBDev.SetStallEPIn(usb.CONTROL_ENDPOINT) + } +} + +func (m *msc) clearStallEndpoint(ep uint8) { + if ep == usb.MSC_ENDPOINT_IN { + machine.USBDev.ClearStallEPIn(usb.MSC_ENDPOINT_IN) + m.txStalled = false + } else if ep == usb.MSC_ENDPOINT_OUT { + machine.USBDev.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