From 76b87d5f34155681760ff7e4231fc9034ee1e220 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sat, 23 Aug 2025 12:21:34 -0400 Subject: [PATCH 1/7] feat: add usb msc support for nrf52840 --- src/machine/machine_nrf52840_usb.go | 66 +++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/machine/machine_nrf52840_usb.go b/src/machine/machine_nrf52840_usb.go index 1fa46945fa..f0bcb1a24a 100644 --- a/src/machine/machine_nrf52840_usb.go +++ b/src/machine/machine_nrf52840_usb.go @@ -379,3 +379,69 @@ func ReceiveUSBControlPacket() ([cdcLineInfoSize]byte, error) { return b, nil } + +// Set the USB endpoint Packet ID to DATA0 or DATA1. +// In endpoints must have bit 7 (0x80) set. +func setEPDataPID(ep uint32, dataOne bool) { + val := ep + if dataOne { + val |= USBD_DTOGGLE_VALUE_Data1 << USBD_DTOGGLE_VALUE_Pos + } else { + val |= USBD_DTOGGLE_VALUE_Data0 << USBD_DTOGGLE_VALUE_Pos + } + nrf.USBD.DTOGGLE.Set(val) +} + +// Set ENDPOINT_HALT/stall status on a USB IN endpoint. +func (dev *USBDevice) SetStallEPIn(ep uint32) { + if ep&0x7F == 0 { + nrf.USBD.TASKS_EP0STALL.Set(1) + } else if ep&0x7F < NumberOfUSBEndpoints { + // Stall In Endpoint + val := 0x100 | 0x80 | ep + nrf.USBD.EPSTALL.Set(val) + } +} + +// Set ENDPOINT_HALT/stall status on a USB OUT endpoint. +func (dev *USBDevice) SetStallEPOut(ep uint32) { + if ep == 0 { + nrf.USBD.TASKS_EP0STALL.Set(1) + } else if ep < NumberOfUSBEndpoints { + // Stall Out Endpoint + val := 0x100 | 0x00 | ep + nrf.USBD.EPSTALL.Set(val) + } +} + +// Clear the ENDPOINT_HALT/stall on a USB IN endpoint. +func (dev *USBDevice) ClearStallEPIn(ep uint32) { + if ep&0x7F == 0 { + nrf.USBD.TASKS_EP0STALL.Set(0) + } else if ep&0x7F < NumberOfUSBEndpoints { + // Reset the endpoint data PID to DATA0 + ep |= 0x80 // Set endpoint direction bit + setEPDataPID(ep, false) + + // No-stall In Endpoint + val := 0x000 | 0x80 | ep + nrf.USBD.EPSTALL.Set(val) + } +} + +// Clear the ENDPOINT_HALT/stall on a USB OUT endpoint. +func (dev *USBDevice) ClearStallEPOut(ep uint32) { + if ep == 0 { + nrf.USBD.TASKS_EP0STALL.Set(0) + } else if ep < NumberOfUSBEndpoints { + // Reset the endpoint data PID to DATA0 + setEPDataPID(ep, false) + + // No-stall Out Endpoint + val := 0x000 | 0x00 | ep + nrf.USBD.EPSTALL.Set(val) + + // Write a value to the SIZE register to allow nRF to ACK/accept data + nrf.USBD.SIZE.EPOUT[ep].Set(0) + } +} From ced1d5c54ec069d77caf16ff75088a69fcbdc3bc Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sat, 23 Aug 2025 12:45:55 -0400 Subject: [PATCH 2/7] fix: reference nrf device constants --- src/machine/machine_nrf52840_usb.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/machine/machine_nrf52840_usb.go b/src/machine/machine_nrf52840_usb.go index f0bcb1a24a..3587a69ac5 100644 --- a/src/machine/machine_nrf52840_usb.go +++ b/src/machine/machine_nrf52840_usb.go @@ -385,9 +385,9 @@ func ReceiveUSBControlPacket() ([cdcLineInfoSize]byte, error) { func setEPDataPID(ep uint32, dataOne bool) { val := ep if dataOne { - val |= USBD_DTOGGLE_VALUE_Data1 << USBD_DTOGGLE_VALUE_Pos + val |= nrf.USBD_DTOGGLE_VALUE_Data1 << nrf.USBD_DTOGGLE_VALUE_Pos } else { - val |= USBD_DTOGGLE_VALUE_Data0 << USBD_DTOGGLE_VALUE_Pos + val |= nrf.USBD_DTOGGLE_VALUE_Data0 << nrf.USBD_DTOGGLE_VALUE_Pos } nrf.USBD.DTOGGLE.Set(val) } From 156ea341e86c78b2421bd4f71ae43f785fa2a733 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 28 Nov 2025 15:29:59 -0500 Subject: [PATCH 3/7] fix: handle write block sizes < usb packet size refactor: clean up duplicated device init logic --- src/machine/usb/msc/msc.go | 49 +++++++++++++++++++- src/machine/usb/msc/{disk.go => recorder.go} | 45 ------------------ src/machine/usb/msc/scsi_readwrite.go | 2 +- 3 files changed, 48 insertions(+), 48 deletions(-) rename src/machine/usb/msc/{disk.go => recorder.go} (59%) diff --git a/src/machine/usb/msc/msc.go b/src/machine/usb/msc/msc.go index d3bf8d6e29..c94d680677 100644 --- a/src/machine/usb/msc/msc.go +++ b/src/machine/usb/msc/msc.go @@ -1,6 +1,7 @@ package msc import ( + "encoding/binary" "machine" "machine/usb" "machine/usb/descriptor" @@ -72,9 +73,7 @@ func newMSC(dev machine.BlockDevice) *msc { 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), @@ -297,3 +296,49 @@ func (m *msc) run(b []byte, isEpOut bool) bool { return ack } + +// RegisterBlockDevice registers a BlockDevice provider with the MSC driver +func (m *msc) RegisterBlockDevice(dev machine.BlockDevice) { + m.dev = dev + + maxPacketSize := descriptor.EndpointMSCIN.GetMaxPacketSize() + bufSize := max(dev.WriteBlockSize(), int64(maxPacketSize)) + + if cap(m.blockCache) != int(bufSize) { + m.blockCache = make([]byte, bufSize) + m.buf = make([]byte, bufSize) + } + + 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 + + // 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 + } + } +} diff --git a/src/machine/usb/msc/disk.go b/src/machine/usb/msc/recorder.go similarity index 59% rename from src/machine/usb/msc/disk.go rename to src/machine/usb/msc/recorder.go index 6624d38c01..b1fddccb3e 100644 --- a/src/machine/usb/msc/disk.go +++ b/src/machine/usb/msc/recorder.go @@ -1,7 +1,6 @@ package msc import ( - "encoding/binary" "errors" "fmt" "machine" @@ -12,50 +11,6 @@ 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 diff --git a/src/machine/usb/msc/scsi_readwrite.go b/src/machine/usb/msc/scsi_readwrite.go index 1b09e13418..2d5ed4aa2d 100644 --- a/src/machine/usb/msc/scsi_readwrite.go +++ b/src/machine/usb/msc/scsi_readwrite.go @@ -87,7 +87,7 @@ 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) { + if blockOffset != 0 || len(b)%int(m.blockSizeRaw) != 0 { return 0, invalidWriteError } From fbb102bee4c879fdf84d59493b0fd118b1b93ffe Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 28 Nov 2025 22:49:44 -0500 Subject: [PATCH 4/7] fix: remove hardcoded endpoint fix: add DataPID toggle re-init in case of soft reset --- src/machine/machine_nrf52840_usb.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/machine/machine_nrf52840_usb.go b/src/machine/machine_nrf52840_usb.go index 3587a69ac5..1741c6437b 100644 --- a/src/machine/machine_nrf52840_usb.go +++ b/src/machine/machine_nrf52840_usb.go @@ -229,19 +229,23 @@ func initEndpoint(ep, config uint32) { switch config { case usb.ENDPOINT_TYPE_INTERRUPT | usb.EndpointIn: enableEPIn(ep) + setEPDataPID(ep|usb.EndpointIn, false) case usb.ENDPOINT_TYPE_BULK | usb.EndpointOut: nrf.USBD.INTENSET.Set(nrf.USBD_INTENSET_ENDEPOUT0 << ep) nrf.USBD.SIZE.EPOUT[ep].Set(0) enableEPOut(ep) + setEPDataPID(ep, false) case usb.ENDPOINT_TYPE_INTERRUPT | usb.EndpointOut: nrf.USBD.INTENSET.Set(nrf.USBD_INTENSET_ENDEPOUT0 << ep) nrf.USBD.SIZE.EPOUT[ep].Set(0) enableEPOut(ep) + setEPDataPID(ep, false) case usb.ENDPOINT_TYPE_BULK | usb.EndpointIn: enableEPIn(ep) + setEPDataPID(ep|usb.EndpointIn, false) case usb.ENDPOINT_TYPE_CONTROL: enableEPIn(0) @@ -259,7 +263,7 @@ func SendUSBInPacket(ep uint32, data []byte) bool { sendUSBPacket(ep, data, 0) // clear transfer complete flag - nrf.USBD.INTENCLR.Set(nrf.USBD_INTENCLR_ENDEPOUT0 << 4) + nrf.USBD.INTENCLR.Set(nrf.USBD_INTENCLR_ENDEPOUT0 << ep) return true } From b9182ef907f9bb66c95ca1c445f6bdf47d8c5fe0 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 28 Nov 2025 22:51:12 -0500 Subject: [PATCH 5/7] fix: prevent persistent hang due when failing to send error status --- src/machine/usb/msc/msc.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/machine/usb/msc/msc.go b/src/machine/usb/msc/msc.go index c94d680677..9b9c1e861f 100644 --- a/src/machine/usb/msc/msc.go +++ b/src/machine/usb/msc/msc.go @@ -164,6 +164,7 @@ func (m *msc) sendCSW(status csw.Status) { } m.cbw.CSW(status, residue, m.cswBuf) m.state = mscStateStatusSent + m.queuedBytes = csw.MsgLen m.sendUSBPacket(m.cswBuf) } From e129a81cb1e25d074f5e14b08dfe81514cdb5ff5 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sat, 29 Nov 2025 15:03:26 -0500 Subject: [PATCH 6/7] fix: print error for msc buffer overflow that should never happen In this situation the host is sending a new packet of data before the last buffer has been handled. This should never happen and indicates a breakdown in the NAK-sending behavior of the greater USB stack for an MCU. When the buffer is full the USB stack should not ACK the final packet and should NAK any new packets until the buffer is handled. If an ACK is sent prematurely and NAKs are not sent in reply to overflow packets this message will occur and the protocol will de-sync. --- src/machine/usb/msc/scsi.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/machine/usb/msc/scsi.go b/src/machine/usb/msc/scsi.go index d7266ed40f..5439832b68 100644 --- a/src/machine/usb/msc/scsi.go +++ b/src/machine/usb/msc/scsi.go @@ -245,6 +245,7 @@ func (m *msc) scsiCmdUnmap(cmd scsi.Cmd) { 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) { + println("scsiQueueTask: OVERFLOW! queued:", m.queuedBytes, "len(b):", len(b), "cap:", cap(m.buf)) m.sendScsiError(csw.StatusFailed, scsi.SenseIllegalRequest, scsi.SenseCodeInvalidFieldInCDB) return true } From 44e83697e47b7cb3506b7cefd50cfee7ec687c22 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sat, 29 Nov 2025 21:18:35 -0500 Subject: [PATCH 7/7] fix: enable usb flow control on nrf52840 --- src/machine/machine_nrf52840_usb.go | 44 +++++++++++++++++++++++++++-- src/machine/usb/msc/msc.go | 3 +- src/machine/usb/msc/scsi.go | 3 +- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/machine/machine_nrf52840_usb.go b/src/machine/machine_nrf52840_usb.go index 1741c6437b..ba149783ba 100644 --- a/src/machine/machine_nrf52840_usb.go +++ b/src/machine/machine_nrf52840_usb.go @@ -22,6 +22,15 @@ var ( epinen uint32 epouten uint32 easyDMABusy volatile.Register8 + // epOutFlowControl contains the flow control state of the USB OUT endpoints. + epOutFlowControl [NumberOfUSBEndpoints]struct { + // nak indicates that we are NAKing any further OUT packets because the rxHandler isn't ready yet. + // When this is true, we do not restart the DMA for the endpoint, effectively pausing it. + nak bool + // dataPending indicates that we have data in the hardware buffer that hasn't been handled yet. + // Having one in the buffer is what generates the NAK responses, this is a signal to handle it. + dataPending bool + } endPoints = []uint32{ usb.CONTROL_ENDPOINT: usb.ENDPOINT_TYPE_CONTROL, @@ -196,7 +205,16 @@ func handleUSBIRQ(interrupt.Interrupt) { nrf.USBD.EPOUT[i].PTR.Set(uint32(uintptr(unsafe.Pointer(&udd_ep_out_cache_buffer[i])))) count := nrf.USBD.SIZE.EPOUT[i].Get() nrf.USBD.EPOUT[i].MAXCNT.Set(count) - nrf.USBD.TASKS_STARTEPOUT[i].Set(1) + if !epOutFlowControl[i].nak { + // Normal case: We want data, so start DMA immediately + nrf.USBD.TASKS_STARTEPOUT[i].Set(1) + epOutFlowControl[i].dataPending = false + } else { + // NAK case: We want to NAK, so DO NOT start DMA. + // The data stays in HW buffer. Host receives NAKs. + // Mark that we have data waiting so we can fetch it later. + epOutFlowControl[i].dataPending = true + } } } } @@ -208,6 +226,10 @@ func handleUSBIRQ(interrupt.Interrupt) { buf := handleEndpointRx(uint32(i)) if usbRxHandler[i] == nil || usbRxHandler[i](buf) { AckUsbOutTransfer(uint32(i)) + } else { + // usbRxHandler returned false, so NAK further OUT packets until we're ready + epOutFlowControl[i].nak = true + nrf.USBD.SIZE.EPOUT[i].Set(0) } exitCriticalSection() } @@ -308,8 +330,26 @@ func handleEndpointRx(ep uint32) []byte { } // AckUsbOutTransfer is called to acknowledge the completion of a USB OUT transfer. +// It also clears the NAK state and resumes data flow if it was paused. func AckUsbOutTransfer(ep uint32) { - // set ready for next data + epOutFlowControl[ep].nak = false + + // If we ignored a packet earlier (Buffer Full strategy), we must manually + // trigger the DMA now to pull it from the HW buffer. + if epOutFlowControl[ep].dataPending { + epOutFlowControl[ep].dataPending = false + + // Prepare DMA to move data from HW Buffer -> RAM + nrf.USBD.EPOUT[ep].PTR.Set(uint32(uintptr(unsafe.Pointer(&udd_ep_out_cache_buffer[ep])))) + count := nrf.USBD.SIZE.EPOUT[ep].Get() + nrf.USBD.EPOUT[ep].MAXCNT.Set(count) + + // Kick the DMA + nrf.USBD.TASKS_STARTEPOUT[ep].Set(1) + return + } + + // Otherwise, just re-arm the endpoint to accept the NEXT packet nrf.USBD.SIZE.EPOUT[ep].Set(0) } diff --git a/src/machine/usb/msc/msc.go b/src/machine/usb/msc/msc.go index 9b9c1e861f..cff39e0e7e 100644 --- a/src/machine/usb/msc/msc.go +++ b/src/machine/usb/msc/msc.go @@ -302,8 +302,7 @@ func (m *msc) run(b []byte, isEpOut bool) bool { func (m *msc) RegisterBlockDevice(dev machine.BlockDevice) { m.dev = dev - maxPacketSize := descriptor.EndpointMSCIN.GetMaxPacketSize() - bufSize := max(dev.WriteBlockSize(), int64(maxPacketSize)) + bufSize := max(dev.WriteBlockSize(), int64(m.maxPacketSize)) if cap(m.blockCache) != int(bufSize) { m.blockCache = make([]byte, bufSize) diff --git a/src/machine/usb/msc/scsi.go b/src/machine/usb/msc/scsi.go index 5439832b68..1b5fc63d83 100644 --- a/src/machine/usb/msc/scsi.go +++ b/src/machine/usb/msc/scsi.go @@ -267,7 +267,8 @@ func (m *msc) scsiQueueTask(cmdType scsi.CmdType, b []byte) bool { 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)) { + // Or if we've received all the data we're expecting. + if m.queuedBytes == uint32(cap(m.blockCache)) || m.sentBytes+m.queuedBytes >= m.totalBytes { m.taskQueued = true } case scsi.CmdUnmap: