mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-15 16:33:40 +00:00
usb: add USB mass storage class support
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user