mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-03 02:27:48 +00:00
usb: add USB mass storage class support
This commit is contained in:
@@ -794,6 +794,10 @@ endif
|
||||
@$(MD5SUM) test.hex
|
||||
$(TINYGO) build -size short -o test.hex -target=feather-nrf52840 examples/usb-midi
|
||||
@$(MD5SUM) test.hex
|
||||
$(TINYGO) build -size short -o test.hex -target=pico examples/usb-storage
|
||||
@$(MD5SUM) test.hex
|
||||
$(TINYGO) build -size short -o test.hex -target=pico2 examples/usb-storage
|
||||
@$(MD5SUM) test.hex
|
||||
$(TINYGO) build -size short -o test.hex -target=nrf52840-s140v6-uf2-generic examples/machinetest
|
||||
@$(MD5SUM) test.hex
|
||||
ifneq ($(STM32), 0)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
"machine/usb/msc"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
msc.Port(machine.Flash)
|
||||
|
||||
for {
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
@@ -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 endpointMSCIN = [endpointTypeLen]byte{
|
||||
endpointTypeLen,
|
||||
TypeEndpoint,
|
||||
0x86, // EndpointAddress
|
||||
TransferTypeBulk, // Attributes
|
||||
0x40, // MaxPacketSizeL (64 bytes)
|
||||
0x00, // MaxPacketSizeH
|
||||
0x00, // Interval
|
||||
}
|
||||
|
||||
var EndpointMSCIN = EndpointType{
|
||||
data: endpointMSCIN[:],
|
||||
}
|
||||
|
||||
// Mass Storage Class bulk out endpoint
|
||||
var endpointMSCOUT = [endpointTypeLen]byte{
|
||||
endpointTypeLen,
|
||||
TypeEndpoint,
|
||||
0x07, // EndpointAddress
|
||||
TransferTypeBulk, // Attributes
|
||||
0x40, // MaxPacketSizeL (64 bytes)
|
||||
0x00, // MaxPacketSizeH
|
||||
0x00, // Interval
|
||||
}
|
||||
|
||||
var EndpointMSCOUT = EndpointType{
|
||||
data: endpointMSCOUT[:],
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
EndpointMSCIN.Bytes(),
|
||||
EndpointMSCOUT.Bytes(),
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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 {
|
||||
HasCmd bool
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (c *CBW) Tag() uint32 {
|
||||
return binary.LittleEndian.Uint32(c.Data[4:8])
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package csw
|
||||
|
||||
type Status uint8
|
||||
|
||||
const (
|
||||
StatusPassed Status = iota
|
||||
StatusFailed
|
||||
StatusPhaseError
|
||||
)
|
||||
|
||||
const (
|
||||
MsgLen = 13
|
||||
Signature = 0x53425355 // "USBS" in little endian
|
||||
)
|
||||
@@ -0,0 +1,180 @@
|
||||
package msc
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"machine"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
if cap(m.blockCache) != int(dev.WriteBlockSize()) {
|
||||
m.blockCache = make([]byte, dev.WriteBlockSize())
|
||||
m.buf = make([]byte, dev.WriteBlockSize())
|
||||
}
|
||||
|
||||
m.blockSizeRaw = uint32(m.dev.WriteBlockSize())
|
||||
m.blockCount = uint32(m.dev.Size()) / m.blockSizeUSB
|
||||
// Read/write/erase operations must be aligned to the underlying hardware blocks. In order to align
|
||||
// them we assume the provided block device is aligned to the end of the underlying hardware block
|
||||
// device and offset all reads/writes by the remaining bytes that don't make up a full block.
|
||||
m.blockOffset = uint32(m.dev.Size()) % m.blockSizeUSB
|
||||
// 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.blockSizeUSB
|
||||
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.blockSizeUSB
|
||||
// Set the UGAVALID bit to indicate that the UNMAP GRANULARITY ALIGNMENT is valid
|
||||
blockOffset |= 0x80000000
|
||||
binary.BigEndian.PutUint32(vpdPages[i].Data[28:32], blockOffset)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var _ machine.BlockDevice = (*RecorderDisk)(nil)
|
||||
|
||||
// RecorderDisk is a block device that records actions taken on it
|
||||
type RecorderDisk struct {
|
||||
dev machine.BlockDevice
|
||||
log []RecorderRecord
|
||||
last time.Time
|
||||
time time.Time
|
||||
}
|
||||
|
||||
type RecorderRecord struct {
|
||||
OpCode RecorderOpCode
|
||||
Offset int64
|
||||
Length int
|
||||
Data []byte
|
||||
Time int64
|
||||
valid bool
|
||||
}
|
||||
|
||||
type RecorderOpCode uint8
|
||||
|
||||
const (
|
||||
RecorderOpCodeRead RecorderOpCode = iota
|
||||
RecorderOpCodeWrite
|
||||
RecorderOpCodeEraseBlocks
|
||||
)
|
||||
|
||||
// NewRecorderDisk creates a new RecorderDisk instance
|
||||
func NewRecorderDisk(dev machine.BlockDevice, count int) *RecorderDisk {
|
||||
d := &RecorderDisk{
|
||||
dev: dev,
|
||||
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, dev.WriteBlockSize()),
|
||||
Time: 0,
|
||||
})
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *RecorderDisk) Size() int64 {
|
||||
return d.dev.Size()
|
||||
}
|
||||
|
||||
func (d *RecorderDisk) WriteBlockSize() int64 {
|
||||
return d.dev.WriteBlockSize()
|
||||
}
|
||||
|
||||
func (d *RecorderDisk) EraseBlockSize() int64 {
|
||||
return d.dev.EraseBlockSize()
|
||||
}
|
||||
|
||||
func (d *RecorderDisk) EraseBlocks(startBlock, numBlocks int64) error {
|
||||
d.Record(RecorderOpCodeEraseBlocks, startBlock, int(numBlocks), []byte{})
|
||||
return d.dev.EraseBlocks(startBlock, numBlocks)
|
||||
}
|
||||
|
||||
func (d *RecorderDisk) ReadAt(buffer []byte, offset int64) (int, error) {
|
||||
n, err := d.dev.ReadAt(buffer, offset)
|
||||
d.Record(RecorderOpCodeRead, offset, n, buffer)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (d *RecorderDisk) WriteAt(buffer []byte, offset int64) (int, error) {
|
||||
n, err := d.dev.WriteAt(buffer, offset)
|
||||
d.Record(RecorderOpCodeWrite, offset, n, buffer)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (d *RecorderDisk) Record(opCode RecorderOpCode, offset int64, length int, data []byte) {
|
||||
n := len(d.log) - 1
|
||||
// Shift the log entries up to make room for a new entry
|
||||
for i := 0; i < n; i++ {
|
||||
d.log[i].OpCode = d.log[i+1].OpCode
|
||||
d.log[i].Offset = d.log[i+1].Offset
|
||||
d.log[i].Length = d.log[i+1].Length
|
||||
d.log[i].Data = d.log[i].Data[:len(d.log[i+1].Data)]
|
||||
copy(d.log[i].Data, d.log[i+1].Data)
|
||||
d.log[i].Time = d.log[i+1].Time
|
||||
d.log[i].valid = d.log[i+1].valid
|
||||
}
|
||||
|
||||
// Append the new record
|
||||
d.log[n].OpCode = opCode
|
||||
d.log[n].Offset = offset
|
||||
d.log[n].Length = length
|
||||
d.log[n].Data = d.log[n].Data[:len(data)]
|
||||
copy(d.log[n].Data, data)
|
||||
d.log[n].Time = time.Since(d.time).Microseconds()
|
||||
d.time = d.time.Add(time.Since(d.time))
|
||||
d.log[n].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
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package msc
|
||||
|
||||
import (
|
||||
"machine"
|
||||
"machine/usb"
|
||||
"machine/usb/descriptor"
|
||||
"machine/usb/msc/csw"
|
||||
"machine/usb/msc/scsi"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
blockCache []byte // Buffer for block read/write data
|
||||
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
|
||||
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
|
||||
blockCount uint32 // Number of blocks in the device
|
||||
blockOffset uint32 // Byte offset of the first block in the device for aligned writes
|
||||
blockSizeUSB uint32 // Write block size as presented to the host over USB
|
||||
blockSizeRaw uint32 // Write block size of the underlying device hardware
|
||||
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
|
||||
}
|
||||
|
||||
// Port returns the USB Mass Storage port
|
||||
func Port(dev machine.BlockDevice) *msc {
|
||||
if MSC == nil {
|
||||
MSC = newMSC(dev)
|
||||
}
|
||||
return MSC
|
||||
}
|
||||
|
||||
func newMSC(dev machine.BlockDevice) *msc {
|
||||
// Size our buffer to match the maximum packet size of the IN endpoint
|
||||
maxPacketSize := descriptor.EndpointMSCIN.GetMaxPacketSize()
|
||||
m := &msc{
|
||||
// Some platforms require reads/writes to be aligned to the full underlying hardware block
|
||||
blockCache: make([]byte, dev.WriteBlockSize()),
|
||||
blockSizeUSB: 512,
|
||||
buf: make([]byte, dev.WriteBlockSize()),
|
||||
cswBuf: make([]byte, csw.MsgLen),
|
||||
cbw: &CBW{Data: make([]byte, 31)},
|
||||
maxPacketSize: uint32(maxPacketSize),
|
||||
}
|
||||
m.RegisterBlockDevice(dev)
|
||||
|
||||
// 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,
|
||||
DelayRxHandler: rxHandler,
|
||||
StallHandler: setupPacketHandler,
|
||||
},
|
||||
},
|
||||
[]usb.SetupConfig{
|
||||
{
|
||||
Index: mscInterface,
|
||||
Handler: setupPacketHandler,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
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(cmd, m.buf)
|
||||
case scsi.CmdUnmap:
|
||||
m.scsiUnmap(m.buf)
|
||||
}
|
||||
|
||||
// Acknowledge the received data from the host
|
||||
m.queuedBytes = 0
|
||||
m.taskQueued = false
|
||||
machine.AckUsbOutTransfer(usb.MSC_ENDPOINT_OUT)
|
||||
}
|
||||
time.Sleep(100 * time.Microsecond)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
// Send the USB packet
|
||||
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.txHandler()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *msc) txHandler() {
|
||||
m.run([]byte{}, false)
|
||||
}
|
||||
|
||||
func rxHandler(b []byte) bool {
|
||||
ack := true
|
||||
if MSC != nil {
|
||||
ack = MSC.run(b, true)
|
||||
}
|
||||
return ack
|
||||
}
|
||||
|
||||
/*
|
||||
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) bool {
|
||||
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 ack
|
||||
}
|
||||
|
||||
// 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 ack
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
case mscStateData:
|
||||
// Transfer data
|
||||
ack = 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 {
|
||||
// Send a zero-length packet to force the end of the transfer before we send a CSW
|
||||
m.queuedBytes = 0
|
||||
m.sendZLP = false
|
||||
m.sendUSBPacket(m.buf[:0])
|
||||
} else {
|
||||
m.sendCSW(m.respStatus)
|
||||
m.state = mscStateCmd
|
||||
}
|
||||
}
|
||||
|
||||
return ack
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package msc
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"machine/usb"
|
||||
"machine/usb/msc/csw"
|
||||
"machine/usb/msc/scsi"
|
||||
)
|
||||
|
||||
func (m *msc) scsiCmdBegin() {
|
||||
cmd := m.cbw.SCSICmd()
|
||||
cmdType := cmd.CmdType()
|
||||
|
||||
// Handle multi-packet commands
|
||||
switch cmdType {
|
||||
case scsi.CmdRead, scsi.CmdWrite:
|
||||
m.scsiCmdReadWrite(cmd)
|
||||
return
|
||||
case scsi.CmdUnmap:
|
||||
m.scsiCmdUnmap(cmd)
|
||||
return
|
||||
}
|
||||
|
||||
if m.totalBytes > 0 && m.cbw.isOut() {
|
||||
// Reject any other multi-packet commands
|
||||
if m.totalBytes > m.maxPacketSize {
|
||||
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.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)
|
||||
}
|
||||
|
||||
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) bool {
|
||||
cmd := m.cbw.SCSICmd()
|
||||
cmdType := cmd.CmdType()
|
||||
|
||||
switch cmdType {
|
||||
case scsi.CmdWrite, scsi.CmdUnmap:
|
||||
if m.readOnly {
|
||||
m.sendScsiError(csw.StatusFailed, scsi.SenseDataProtect, scsi.SenseCodeWriteProtected)
|
||||
return true
|
||||
}
|
||||
return m.scsiQueueTask(cmdType, b)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// Last LBA address (big endian)
|
||||
binary.BigEndian.PutUint32(m.buf[:4], m.blockCount-1)
|
||||
// Block size (big endian)
|
||||
binary.BigEndian.PutUint32(m.buf[4:8], m.blockSizeUSB)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// Number of blocks (big endian)
|
||||
binary.BigEndian.PutUint32(m.buf[4:8], m.blockCount)
|
||||
// Block size (24-bit, big endian)
|
||||
binary.BigEndian.PutUint32(m.buf[8:12], m.blockSizeUSB)
|
||||
// Descriptor Type - formatted media
|
||||
m.buf[8] = 2
|
||||
}
|
||||
|
||||
// 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 > respLen {
|
||||
m.totalBytes = respLen
|
||||
m.sendZLP = true
|
||||
}
|
||||
|
||||
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] = 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
func (m *msc) scsiCmdRequestSense() {
|
||||
// Set the buffer size to the SCSI sense message size and clear
|
||||
m.resetBuffer(scsi.RequestSenseRespLen)
|
||||
m.queuedBytes = scsi.RequestSenseRespLen
|
||||
m.totalBytes = 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 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
|
||||
}
|
||||
}
|
||||
|
||||
func (m *msc) scsiQueueTask(cmdType scsi.CmdType, b []byte) bool {
|
||||
// Check if the incoming data is larger than our buffer
|
||||
if int(m.queuedBytes)+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
|
||||
start := m.queuedBytes
|
||||
end := start + uint32(len(b))
|
||||
m.buf = m.buf[:end]
|
||||
copy(m.buf[start:end], b)
|
||||
m.queuedBytes += uint32(len(b))
|
||||
|
||||
switch cmdType {
|
||||
case scsi.CmdWrite:
|
||||
// If we're writing data wait until we have a full write block of data that can be processed.
|
||||
if m.queuedBytes == uint32(cap(m.blockCache)) {
|
||||
m.taskQueued = true
|
||||
}
|
||||
case scsi.CmdUnmap:
|
||||
m.taskQueued = true
|
||||
}
|
||||
|
||||
// Don't acknowledge the incoming data until we can process it.
|
||||
return !m.taskQueued
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package scsi
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
)
|
||||
|
||||
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 (
|
||||
// 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
|
||||
|
||||
// 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
|
||||
ModeSense6RespLen = 4
|
||||
ModeSense10RespLen = 8
|
||||
ReadCapacityRespLen = 8
|
||||
ReadFormatCapacityRespLen = 12
|
||||
RequestSenseRespLen = 18
|
||||
)
|
||||
@@ -0,0 +1,164 @@
|
||||
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
|
||||
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
|
||||
// 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 media 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
|
||||
copy(m.buf[8:16], m.vendorID[:])
|
||||
// bytes 16-31 - Product ID
|
||||
copy(m.buf[16:32], m.productID[:])
|
||||
// bytes 32-35 - Product revision level
|
||||
copy(m.buf[32:36], m.productRev[:])
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package msc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"machine/usb/msc/csw"
|
||||
"machine/usb/msc/scsi"
|
||||
)
|
||||
|
||||
var invalidWriteError = errors.New("invalid write offset or length")
|
||||
|
||||
func (m *msc) scsiCmdReadWrite(cmd scsi.Cmd) {
|
||||
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 {
|
||||
// WRITE(10) and UNMAP commands don't take any action until the data stage begins
|
||||
}
|
||||
} 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) usbToRawOffset(lba, offset uint32) (int64, int64) {
|
||||
// Convert the emulated block address to the underlying hardware block's start and offset
|
||||
rawLBA := (lba*m.blockSizeUSB + offset) / m.blockSizeRaw
|
||||
rawBlockOffset := int64((lba*m.blockSizeUSB + offset) % m.blockSizeRaw)
|
||||
return int64(m.blockOffset + rawLBA*m.blockSizeRaw), rawBlockOffset
|
||||
}
|
||||
|
||||
func (m *msc) readBlock(b []byte, lba, offset uint32) (n int, err error) {
|
||||
// Convert the emulated block address to the underlying hardware block's start and offset
|
||||
blockStart, blockOffset := m.usbToRawOffset(lba, offset)
|
||||
|
||||
// Read a full block from the underlying device into the block cache
|
||||
n, err = m.dev.ReadAt(m.blockCache, blockStart)
|
||||
n -= int(blockOffset)
|
||||
if n > len(b) {
|
||||
n = len(b)
|
||||
}
|
||||
|
||||
copy(b, m.blockCache[blockOffset:])
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (m *msc) writeBlock(b []byte, lba, offset uint32) (n int, err error) {
|
||||
// Convert the emulated block address to the underlying hardware block's start and offset
|
||||
blockStart, blockOffset := m.usbToRawOffset(lba, offset)
|
||||
|
||||
if blockOffset != 0 || len(b) != int(m.blockSizeRaw) {
|
||||
return 0, invalidWriteError
|
||||
}
|
||||
|
||||
// Write the full block to the underlying device
|
||||
n, err = m.dev.WriteAt(b, blockStart)
|
||||
n -= int(blockOffset)
|
||||
if n > len(b) {
|
||||
n = len(b)
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (m *msc) scsiRead(cmd scsi.Cmd) {
|
||||
// Make sure we don't exceed the buffer size
|
||||
readEnd := m.totalBytes - m.sentBytes
|
||||
if readEnd > m.maxPacketSize {
|
||||
readEnd = m.maxPacketSize
|
||||
}
|
||||
// Resize the buffer to fit the read size
|
||||
m.resetBuffer(int(readEnd))
|
||||
|
||||
// Read data from the emulated block device
|
||||
n, err := m.readBlock(m.buf[:readEnd], cmd.LBA(), m.sentBytes)
|
||||
if err != nil || n == 0 {
|
||||
m.sendScsiError(csw.StatusFailed, scsi.SenseNotReady, scsi.SenseCodeMediumNotPresent)
|
||||
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
|
||||
}
|
||||
|
||||
// Write data to the block device
|
||||
n, err := m.writeBlock(b, cmd.LBA(), m.sentBytes)
|
||||
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
|
||||
m.run([]byte{}, true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
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(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
|
||||
|
||||
// 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)
|
||||
|
||||
// Unmap the blocks we can from this packet
|
||||
for i := 8; i < descEnd; i += 16 {
|
||||
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)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: We need to handle erase block alignment
|
||||
|
||||
m.sentBytes += uint32(len(b))
|
||||
if m.sentBytes >= m.totalBytes {
|
||||
// Order 66 complete, send CSW to establish galactic empire
|
||||
m.state = mscStateStatus
|
||||
m.run([]byte{}, true)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// 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+blockCount > m.blockCount {
|
||||
return errorLBAOutOfRange
|
||||
}
|
||||
|
||||
// Convert the emulated block size to the underlying hardware erase block size
|
||||
blockStart := int64(lba*m.blockSizeUSB) / m.dev.EraseBlockSize()
|
||||
rawBlockCount := int64(blockCount*m.blockSizeUSB) / m.dev.EraseBlockSize()
|
||||
|
||||
// Unmap the blocks
|
||||
return m.dev.EraseBlocks(blockStart, rawBlockCount)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,8 @@ 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 = 6 // for Bulk In
|
||||
MSC_ENDPOINT_OUT = 7 // for Bulk Out
|
||||
|
||||
// bmRequestType
|
||||
REQUEST_HOSTTODEVICE = 0x00
|
||||
|
||||
Reference in New Issue
Block a user