machine/stm32: add OTG FS USB driver for F4/F7

STM32F4 and F7 share the same OTG FS IP but have no USB driver in
TinyGo. This adds a full device-mode driver covering CDC, HID, and
MSC with working examples.

- OTG FS has 4 physical EPs (0–3); virtual indices 4–7 fold onto
  them via physEP() so the existing machine/usb API is unchanged
- F4 bypasses VBUS sensing via GCCFG.NOVBUSSENS; F7 uses the USB
  voltage regulator + GOTGCTL B-valid override instead
- STM32F7 PLL_Q changed 2→9 to produce the 48 MHz clock required
  by USB/RNG/SDMMC; CK48MSEL cleared to select main PLL as source
- HID and MSC descriptors remapped at init() to physical endpoint
  addresses (EP2/EP1 for HID, EP2/EP3 for MSC)
- usb-storage example replaced machine.Flash with a FAT12 RAM disk
  so the host mounts without reformatting
- MSC sendCSW sets queuedBytes before state transition to fix a
  missed byte-count on the status phase
This commit is contained in:
Konstantin Sharlaimov
2026-05-15 22:44:54 +02:00
committed by Ron Evans
parent 140c82e012
commit c1a4ed1489
15 changed files with 1016 additions and 14 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ func TestBinarySize(t *testing.T) {
// microcontrollers
{"hifive1b", "examples/echo", 3771, 309, 0, 2260},
{"microbit", "examples/serial", 2832, 368, 8, 2256},
{"wioterminal", "examples/pininterrupt", 8053, 1663, 132, 7488},
{"wioterminal", "examples/pininterrupt", 8065, 1663, 132, 7488},
// TODO: also check wasm. Right now this is difficult, because
// wasm binaries are run through wasm-opt and therefore the
+1
View File
@@ -22,6 +22,7 @@ func main() {
button.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
kb := keyboard.Port()
machine.USBDev.Configure(machine.UARTConfig{}) // no-op if already init'd by serial.usb
for {
if !button.Get() {
+78 -1
View File
@@ -6,8 +6,85 @@ import (
"time"
)
// Disk geometry.
const (
sectorSize = 512
diskSectors = 128 // 64 KB total
)
var diskData [diskSectors * sectorSize]byte
type ramDisk struct{}
func (r *ramDisk) ReadAt(p []byte, off int64) (int, error) {
return copy(p, diskData[off:]), nil
}
func (r *ramDisk) WriteAt(p []byte, off int64) (int, error) {
return copy(diskData[off:], p), nil
}
func (r *ramDisk) Size() int64 { return int64(diskSectors * sectorSize) }
func (r *ramDisk) WriteBlockSize() int64 { return sectorSize }
func (r *ramDisk) EraseBlockSize() int64 { return sectorSize }
func (r *ramDisk) EraseBlocks(start, len int64) error { return nil }
func init() {
formatFAT12(diskData[:])
}
// formatFAT12 writes a minimal FAT12 volume boot record and FAT tables so the
// host OS can mount the disk without reformatting.
func formatFAT12(d []byte) {
// --- Sector 0: Volume Boot Record ---
s := d[0:]
s[0] = 0xEB
s[1] = 0x3C
s[2] = 0x90 // short JMP + NOP
copy(s[3:11], "MSDOS5.0")
// BPB fields (little-endian)
s[11] = 0x00
s[12] = 0x02 // bytesPerSector = 512
s[13] = 0x01 // sectorsPerCluster = 1
s[14] = 0x01
s[15] = 0x00 // reservedSectors = 1
s[16] = 0x02 // numFATs = 2
s[17] = 0x20
s[18] = 0x00 // rootEntryCount = 32
s[19] = 0x80
s[20] = 0x00 // totalSectors16 = 128
s[21] = 0xF8 // mediaType = fixed disk
s[22] = 0x01
s[23] = 0x00 // sectorsPerFAT = 1
s[24] = 0x80
s[25] = 0x00 // sectorsPerTrack = 128
s[26] = 0x01
s[27] = 0x00 // numHeads = 1
// hiddenSectors[28:32] = 0
// totalSectors32[32:36] = 0
s[38] = 0x29 // extBootSig
s[39] = 0x47
s[40] = 0x4F
s[41] = 0x30
s[42] = 0x31 // volumeID "GO01"
copy(s[43:54], "TINYGO ") // volumeLabel (11 bytes)
copy(s[54:62], "FAT12 ") // fsType
s[510] = 0x55
s[511] = 0xAA // boot sector signature
// --- Sector 1: FAT1 ---
// Entry 0 = 0xFF8 (media byte), entry 1 = 0xFFF (EOC); all others = free.
d[512] = 0xF8
d[513] = 0xFF
d[514] = 0xFF
// --- Sector 2: FAT2 (identical copy) ---
copy(d[1024:1027], d[512:515])
}
func main() {
msc.Port(machine.Flash)
msc.Port(&ramDisk{})
machine.USBDev.Configure(machine.UARTConfig{})
for {
time.Sleep(2 * time.Second)
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build esp32c3 || nrf || nrf51 || nrf52 || nrf528xx || stm32f4 || stm32l0 || stm32l4 || stm32wlx || atsamd21 || atsamd51 || atsame5x || rp2040 || rp2350
//go:build esp32c3 || nrf || nrf51 || nrf52 || nrf528xx || stm32f4 || stm32f7 || stm32l0 || stm32l4 || stm32wlx || atsamd21 || atsamd51 || atsame5x || rp2040 || rp2350
package machine
+6
View File
@@ -86,6 +86,12 @@ func (p Pin) PortMaskClear() (*uint32, uint32) {
return &port.BSRR.Reg, 1 << (pin + 16)
}
// EnterBootloader resets the chip into the bootloader.
// This is currently a stub for STM32, required to satisfy machine.EnterBootloader
// called by machine/usb/cdc.
func EnterBootloader() {
}
var deviceID [12]byte
// DeviceID returns an identifier that is unique within
+828
View File
@@ -0,0 +1,828 @@
//go:build stm32f4 || stm32f7
package machine
import (
"device/stm32"
"machine/usb"
"runtime/interrupt"
"runtime/volatile"
"unsafe"
)
// NumberOfUSBEndpoints is sized to cover TinyGo's full endpoint index space
// (0=control, 1=CDC ACM, 2=CDC OUT, 3=CDC IN, 4=HID IN, 5=HID OUT, 6=MIDI IN, 7=MIDI OUT).
// Physical OTG FS hardware has 4 IN + 4 OUT endpoints (03).
const NumberOfUSBEndpoints = 8
// Default USB identifiers; board files with USB support should override these.
const (
usb_VID = uint16(0x239A)
usb_PID = uint16(0x0001)
usb_STRING_MANUFACTURER = "TinyGo"
usb_STRING_PRODUCT = "STM32 USB Device"
)
// OTG FS register blocks.
var (
otgDevice = (*usbDeviceRegs)(unsafe.Pointer(stm32.OTG_FS_DEVICE))
otgPower = (*usbPowerRegs)(unsafe.Pointer(stm32.OTG_FS_PWRCLK))
)
// usbDeviceRegs represents the USB device-mode control block at base+0x800.
type usbDeviceRegs struct {
DCFG volatile.Register32 // 0x800
DCTL volatile.Register32 // 0x804
DSTS volatile.Register32 // 0x808
_ uint32 // 0x80C
DIEPMSK volatile.Register32 // 0x810
DOEPMSK volatile.Register32 // 0x814
DAINT volatile.Register32 // 0x818
DAINTMSK volatile.Register32 // 0x81C
_ [5]uint32 // 0x820 - 0x830
DIEPEMPMSK volatile.Register32 // 0x834
}
// usbInEndpointRegs represents the registers for a single IN endpoint at 0x900 + ep*0x20.
type usbInEndpointRegs struct {
CTL volatile.Register32 // 0x00
_ uint32 // 0x04
INT volatile.Register32 // 0x08
_ uint32 // 0x0C
TSIZ volatile.Register32 // 0x10
_ uint32 // 0x14
TXFST volatile.Register32 // 0x18
_ uint32 // 0x1C
}
// usbOutEndpointRegs represents the registers for a single OUT endpoint at 0xB00 + ep*0x20.
type usbOutEndpointRegs struct {
CTL volatile.Register32 // 0x00
_ uint32 // 0x04
INT volatile.Register32 // 0x08
_ uint32 // 0x0C
TSIZ volatile.Register32 // 0x10
_ [3]uint32 // 0x14 - 0x1C
}
// usbPowerRegs represents the power and clock gating block at base+0xE00.
type usbPowerRegs struct {
PCGCCTL volatile.Register32 // 0xE00
}
// otgInEP returns the IN endpoint registers for physical endpoint ep.
func otgInEP(ep uint32) *usbInEndpointRegs {
return (*usbInEndpointRegs)(unsafe.Pointer(uintptr(unsafe.Pointer(stm32.OTG_FS_GLOBAL)) + 0x900 + uintptr(ep)*0x20))
}
// otgOutEP returns the OUT endpoint registers for physical endpoint ep.
func otgOutEP(ep uint32) *usbOutEndpointRegs {
return (*usbOutEndpointRegs)(unsafe.Pointer(uintptr(unsafe.Pointer(stm32.OTG_FS_GLOBAL)) + 0xB00 + uintptr(ep)*0x20))
}
// otgDFIFO returns a volatile pointer to the data FIFO for physical endpoint ep.
// DFIFO[ep] is located at 0x50001000 + ep*0x1000.
func otgDFIFO(ep uint32) *volatile.Register32 {
return (*volatile.Register32)(unsafe.Pointer(uintptr(unsafe.Pointer(stm32.OTG_FS_GLOBAL)) + 0x1000 + uintptr(ep)*0x1000))
}
// DCFG bit positions and masks.
const (
dcfgDSPD = uint32(0x3) // FS PHY speed (bits [1:0] = 0b11)
dcfgDAD_Pos = uint32(4) // device address field start bit
dcfgDAD_Msk = uint32(0x7F << 4)
)
// DCTL bits.
const (
dctlSDIS = uint32(1 << 1) // soft disconnect
dctlSGINAK = uint32(1 << 7) // set global IN NAK
dctlCGINAK = uint32(1 << 8) // clear global IN NAK
dctlSGONAK = uint32(1 << 9) // set global OUT NAK
dctlCGONAK = uint32(1 << 10) // clear global OUT NAK
)
// DSTS bits.
const (
dstsSUSPSTS = uint32(1 << 0)
dstsENUMSPD_Pos = uint32(1)
dstsENUMSPD_Msk = uint32(0x3 << 1)
)
// DIEPCTLn / DOEPCTLn bits (shared between IN and OUT endpoint control registers).
const (
depctlEPENA = uint32(1 << 31) // endpoint enable
depctlEPDIS = uint32(1 << 30) // endpoint disable
depctlSD0PID = uint32(1 << 28) // set DATA0 PID
depctlSNAK = uint32(1 << 27) // set NAK
depctlCNAK = uint32(1 << 26) // clear NAK
depctlSTALL = uint32(1 << 21) // STALL handshake
depctlETYP_Pos = uint32(18) // endpoint type field start
depctlUSBAEP = uint32(1 << 15) // USB active endpoint
depctlTXFNUM_Pos = uint32(22) // TX FIFO number field start (IN eps only)
depctlMPSIZ_Pos = uint32(0) // max packet size field start
)
// EP0 max packet size encoding in DIEPCTL0 / DOEPCTL0 bits [1:0].
const ep0Mps64 = uint32(0x0) // 64 bytes (FS default)
// DIEPINTn / DOEPINTn bits.
const (
depintXFRC = uint32(1 << 0) // transfer complete
depintSTUP = uint32(1 << 3) // SETUP phase done (DOEPINTn)
)
// GRXSTSP_Device PKTSTS field values.
const (
rxPktstsGNAK = uint32(0x1) // global OUT NAK
rxPktstsOUTData = uint32(0x2) // OUT data packet received
rxPktstsOUTDone = uint32(0x3) // OUT transfer complete
rxPktstsSetupDone = uint32(0x4) // SETUP transaction complete
rxPktstsSetupData = uint32(0x6) // SETUP data packet received (always 8 bytes)
)
// GINTSTS / GINTMSK bits (values taken from stm32f405 SVD constants).
const (
gintRXFLVL = uint32(stm32.USB_OTG_FS_GINTSTS_RXFLVL) // 0x10
gintUSBRST = uint32(stm32.USB_OTG_FS_GINTSTS_USBRST) // 0x1000
gintENUMDNE = uint32(stm32.USB_OTG_FS_GINTSTS_ENUMDNE) // 0x2000
gintUSBSUSP = uint32(stm32.USB_OTG_FS_GINTSTS_USBSUSP) // 0x800
gintWKUPINT = uint32(stm32.USB_OTG_FS_GINTSTS_WKUPINT) // 0x80000000
gintIEPINT = uint32(stm32.USB_OTG_FS_GINTSTS_IEPINT) // 0x40000
gintOEPINT = uint32(stm32.USB_OTG_FS_GINTSTS_OEPINT) // 0x80000
)
// GRSTCTL bits.
const (
grstCSRST = uint32(stm32.USB_OTG_FS_GRSTCTL_CSRST) // core soft reset
grstRXFFLSH = uint32(stm32.USB_OTG_FS_GRSTCTL_RXFFLSH) // RX FIFO flush
grstTXFFLSH = uint32(stm32.USB_OTG_FS_GRSTCTL_TXFFLSH) // TX FIFO flush
grstTXFNUM_Pos = uint32(stm32.USB_OTG_FS_GRSTCTL_TXFNUM_Pos)
grstAHBIDL = uint32(stm32.USB_OTG_FS_GRSTCTL_AHBIDL) // AHB master idle
)
// FIFO size layout in 32-bit words (total budget = 320 words).
const (
rxFIFODepth = uint32(128) // shared RX FIFO
ep0TxFIFODepth = uint32(16) // EP0 TX FIFO
ep1TxFIFODepth = uint32(64) // EP1 TX FIFO
ep2TxFIFODepth = uint32(64) // EP2 TX FIFO
ep3TxFIFODepth = uint32(48) // EP3 TX FIFO
ep0TxFIFOStart = rxFIFODepth
ep1TxFIFOStart = ep0TxFIFOStart + ep0TxFIFODepth
ep2TxFIFOStart = ep1TxFIFOStart + ep1TxFIFODepth
ep3TxFIFOStart = ep2TxFIFOStart + ep2TxFIFODepth
)
// Driver state.
var (
// sendOnEP0DATADONE tracks multi-chunk EP0 IN transfers.
sendOnEP0DATADONE struct {
ptr *byte
count int
offset int
}
// usbSetupBuf holds the 8-byte SETUP packet from the RX FIFO.
usbSetupBuf [8]byte
// usbRxBufLen tracks the byte count of the most recently received OUT packet
// per physical endpoint (index 03).
usbRxBufLen [4]uint32
)
// Configure initialises the OTG FS USB peripheral in device mode.
// The config parameter is unused (present for interface compatibility).
func (dev *USBDevice) Configure(config UARTConfig) {
if dev.initcomplete {
return
}
// ---- 1. Enable peripheral clocks ----------------------------------------
// GPIOA clock (PA11 = D-, PA12 = D+)
stm32.RCC.AHB1ENR.SetBits(stm32.RCC_AHB1ENR_GPIOAEN)
// OTG FS peripheral clock
stm32.RCC.AHB2ENR.SetBits(stm32.RCC_AHB2ENR_OTGFSEN)
// ---- 2. Configure GPIO pins (PA11 D-, PA12 D+) as AF, very high speed ----
for _, pin := range [2]Pin{PA11, PA12} {
pos := uint8(pin%16) * 2
port := pin.getPort()
port.MODER.ReplaceBits(gpioModeAlternate, gpioModeMask, pos)
port.OSPEEDR.ReplaceBits(gpioOutputSpeedVeryHigh, gpioOutputSpeedMask, pos)
port.PUPDR.ReplaceBits(gpioPullFloating, gpioPullMask, pos)
// OTYPER remains 0 (push-pull)
pin.SetAltFunc(10) // AF10 = OTG FS on both F4 and F7
}
// ---- 3. OTG core reset --------------------------------------------------
// Wait for AHB master idle before core reset.
for stm32.OTG_FS_GLOBAL.GRSTCTL.Get()&grstAHBIDL == 0 {
}
// Core soft reset
stm32.OTG_FS_GLOBAL.GRSTCTL.SetBits(grstCSRST)
for stm32.OTG_FS_GLOBAL.GRSTCTL.HasBits(grstCSRST) {
}
// Wait for AHB idle again after reset
for stm32.OTG_FS_GLOBAL.GRSTCTL.Get()&grstAHBIDL == 0 {
}
// ---- 4. Force device mode, set turnaround time --------------------------
gusbcfg := stm32.OTG_FS_GLOBAL.GUSBCFG.Get()
gusbcfg &^= stm32.USB_OTG_FS_GUSBCFG_FHMOD |
stm32.USB_OTG_FS_GUSBCFG_FDMOD |
stm32.USB_OTG_FS_GUSBCFG_TRDT_Msk
gusbcfg |= stm32.USB_OTG_FS_GUSBCFG_FDMOD |
(9 << stm32.USB_OTG_FS_GUSBCFG_TRDT_Pos) // turnaround time = 9 for 216MHz HCLK
stm32.OTG_FS_GLOBAL.GUSBCFG.Set(gusbcfg)
// ---- 5. PHY / VBUS configuration (platform-specific) --------------------
initOTGFSPHY()
// ---- 6. Enable PHY clock, soft-disconnect before further init -----------
// Clear stop-clock / stop-phy-clock bits so the PHY clock runs.
// If a bootloader left these set, USB would hang silently.
otgPower.PCGCCTL.Set(0)
// Soft-disconnect now (after CSRST reset DCTL to its default connected state).
otgDevice.DCTL.SetBits(dctlSDIS)
// ---- 7. Configure data FIFOs --------------------------------------------
// RX FIFO (shared for all OUT + SETUP packets)
stm32.OTG_FS_GLOBAL.GRXFSIZ.Set(rxFIFODepth)
// EP0 TX FIFO: start = rxFIFODepth, depth = ep0TxFIFODepth
stm32.OTG_FS_GLOBAL.DIEPTXF0.Set(
(ep0TxFIFODepth << 16) | ep0TxFIFOStart,
)
// EP13 TX FIFOs
stm32.OTG_FS_GLOBAL.DIEPTXF1.Set(
(ep1TxFIFODepth << 16) | ep1TxFIFOStart,
)
stm32.OTG_FS_GLOBAL.DIEPTXF2.Set(
(ep2TxFIFODepth << 16) | ep2TxFIFOStart,
)
stm32.OTG_FS_GLOBAL.DIEPTXF3.Set(
(ep3TxFIFODepth << 16) | ep3TxFIFOStart,
)
// ---- 8. Flush FIFOs ----------------------------------------------------
flushRxFIFO()
flushTxFIFO(0x10) // flush all TX FIFOs (TXFNUM = 0x10 = all)
// ---- 9. Configure device: full-speed, no SOF output --------------------
otgDevice.DCFG.Set(dcfgDSPD) // FS PHY speed
// Clear any stale interrupts
stm32.OTG_FS_GLOBAL.GINTSTS.Set(0xFFFFFFFF)
// ---- 10. Enable interrupts ----------------------------------------------
stm32.OTG_FS_GLOBAL.GINTMSK.Set(
gintUSBRST | gintENUMDNE | gintRXFLVL | gintIEPINT | gintOEPINT |
gintUSBSUSP | gintWKUPINT,
)
// Enable device-level IN and OUT endpoint interrupt masks
otgDevice.DIEPMSK.Set(depintXFRC)
otgDevice.DOEPMSK.Set(depintXFRC | depintSTUP)
// Enable global interrupt
stm32.OTG_FS_GLOBAL.GAHBCFG.SetBits(stm32.USB_OTG_FS_GAHBCFG_GINT)
// ---- 11. Register and enable NVIC interrupt -----------------------------
intr := interrupt.New(stm32.IRQ_OTG_FS, handleUSBIRQ)
intr.SetPriority(0) // Highest priority
intr.Enable()
// ---- 12. Connect to host (clear soft-disconnect) -----------------------
otgDevice.DCTL.ClearBits(dctlSDIS)
dev.initcomplete = true
}
// handleUSBIRQ is the OTG FS interrupt handler, dispatching on GINTSTS bits.
func handleUSBIRQ(intr interrupt.Interrupt) {
status := stm32.OTG_FS_GLOBAL.GINTSTS.Get() &
stm32.OTG_FS_GLOBAL.GINTMSK.Get()
if status&gintUSBSUSP != 0 {
stm32.OTG_FS_GLOBAL.GINTSTS.Set(gintUSBSUSP)
// Stop PHY clock during suspend. Only STPPCLK (bit 0); do NOT set
// GATEHCLK (bit 1) — that gates the AHB bus, which prevents the ISR
// from reading GINTSTS when WKUPINT fires.
otgPower.PCGCCTL.SetBits(1) // STPPCLK
}
if status&gintWKUPINT != 0 {
stm32.OTG_FS_GLOBAL.GINTSTS.Set(gintWKUPINT)
// Restart clocks before any endpoint activity can resume.
otgPower.PCGCCTL.ClearBits(1 | 2)
}
if status&gintUSBRST != 0 {
stm32.OTG_FS_GLOBAL.GINTSTS.Set(gintUSBRST)
otgPower.PCGCCTL.ClearBits(1 | 2) // ensure clocks running after reset-from-suspend
handleUSBReset()
}
if status&gintRXFLVL != 0 {
// RXFLVL is level-triggered; drain entire FIFO in a loop.
stm32.OTG_FS_GLOBAL.GINTMSK.ClearBits(gintRXFLVL)
handleRxFIFO()
stm32.OTG_FS_GLOBAL.GINTMSK.SetBits(gintRXFLVL)
}
if status&gintENUMDNE != 0 {
stm32.OTG_FS_GLOBAL.GINTSTS.Set(gintENUMDNE)
handleEnumDone()
}
if status&gintIEPINT != 0 {
handleInEndpoints()
}
if status&gintOEPINT != 0 {
handleOutEndpoints()
}
}
// handleUSBReset is called on USB bus reset (USBRST interrupt).
func handleUSBReset() {
// Set NAK on all OUT endpoints.
for ep := uint32(0); ep < 4; ep++ {
otgOutEP(ep).CTL.SetBits(depctlSNAK)
}
// Flush RX and all TX FIFOs.
flushRxFIFO()
flushTxFIFO(0x10)
// Clear all endpoint interrupts.
otgDevice.DAINT.Set(0xFFFFFFFF)
otgDevice.DAINTMSK.Set(0)
// Enable EP0 IN and OUT interrupt sources.
otgDevice.DAINTMSK.Set((1 << 0) | (1 << 16)) // DIEP0 + DOEP0
// Re-arm EP0 OUT for up to 3 back-to-back SETUP packets.
armEP0Out()
// Signal upper layer: device is no longer configured.
usbConfiguration = 0
USBDev.InitEndpointComplete = false
}
// handleEnumDone is called after USB enumeration speed is detected (ENUMDNE).
func handleEnumDone() {
// Activate EP0 (max packet 64, type control, TX FIFO 0).
ep0Ctl := ep0Mps64 | depctlUSBAEP | (0 << depctlETYP_Pos) // control type
otgInEP(0).CTL.SetBits(ep0Ctl)
otgOutEP(0).CTL.SetBits(ep0Mps64 | depctlUSBAEP)
// Clear global IN NAK so EP0 IN can send.
otgDevice.DCTL.SetBits(dctlCGINAK)
}
// handleRxFIFO drains the RX FIFO completely, processing each pop via GRXSTSP.
// RXFLVL is level-triggered, so this must loop until the FIFO is empty.
func handleRxFIFO() {
for stm32.OTG_FS_GLOBAL.GINTSTS.HasBits(gintRXFLVL) {
status := stm32.OTG_FS_GLOBAL.GRXSTSP_Device.Get()
ep := status & stm32.USB_OTG_FS_GRXSTSP_Device_EPNUM_Msk
bcnt := (status & stm32.USB_OTG_FS_GRXSTSP_Device_BCNT_Msk) >>
stm32.USB_OTG_FS_GRXSTSP_Device_BCNT_Pos
pktsts := (status >> stm32.USB_OTG_FS_GRXSTSP_Device_PKTSTS_Pos) & 0xF
pep := ep // GRXSTSP.EPNUM is already a physical endpoint (03)
switch pktsts {
case rxPktstsSetupData:
// 8-byte SETUP packet: read exactly 2 words from DFIFO[0].
w0 := otgDFIFO(0).Get()
w1 := otgDFIFO(0).Get()
usbSetupBuf[0] = byte(w0)
usbSetupBuf[1] = byte(w0 >> 8)
usbSetupBuf[2] = byte(w0 >> 16)
usbSetupBuf[3] = byte(w0 >> 24)
usbSetupBuf[4] = byte(w1)
usbSetupBuf[5] = byte(w1 >> 8)
usbSetupBuf[6] = byte(w1 >> 16)
usbSetupBuf[7] = byte(w1 >> 24)
case rxPktstsSetupDone:
// SETUP transaction complete: process the buffered SETUP packet.
setup := usb.Setup{
BmRequestType: usbSetupBuf[0],
BRequest: usbSetupBuf[1],
WValueL: usbSetupBuf[2],
WValueH: usbSetupBuf[3],
WIndex: uint16(usbSetupBuf[4]) | (uint16(usbSetupBuf[5]) << 8),
WLength: uint16(usbSetupBuf[6]) | (uint16(usbSetupBuf[7]) << 8),
}
ok := false
if setup.BmRequestType&usb.REQUEST_TYPE == usb.REQUEST_STANDARD {
ok = handleStandardSetup(setup)
} else {
if setup.WIndex < uint16(len(usbSetupHandler)) &&
usbSetupHandler[setup.WIndex] != nil {
ok = usbSetupHandler[setup.WIndex](setup)
}
}
if !ok {
// Stall EP0 IN and OUT on unrecognised requests.
otgInEP(0).CTL.SetBits(depctlSTALL)
otgOutEP(0).CTL.SetBits(depctlSTALL)
}
// Re-arm EP0 OUT for the next SETUP.
armEP0Out()
case rxPktstsOUTData:
// OUT data: read bcnt bytes from DFIFO[pep] into cache buffer.
if bcnt > 0 && pep < 4 {
readFIFO(pep, bcnt)
usbRxBufLen[pep] = bcnt
}
case rxPktstsOUTDone:
// OUT transfer complete: nothing to do here; handled in handleOutEndpoints.
}
}
}
// readFIFO reads bcnt bytes from the shared RX FIFO (DFIFO 0) into udd_ep_out_cache_buffer[ep].
func readFIFO(ep, bcnt uint32) {
buf := udd_ep_out_cache_buffer[ep][:]
words := (bcnt + 3) / 4
for i := uint32(0); i < words; i++ {
w := otgDFIFO(0).Get() // Always read from FIFO 0
b := i * 4
buf[b] = byte(w)
if b+1 < bcnt {
buf[b+1] = byte(w >> 8)
}
if b+2 < bcnt {
buf[b+2] = byte(w >> 16)
}
if b+3 < bcnt {
buf[b+3] = byte(w >> 24)
}
}
}
// handleInEndpoints handles IEPINT: checks each active IN endpoint for XFRC.
func handleInEndpoints() {
daint := otgDevice.DAINT.Get() & 0x0000FFFF // lower 16 bits = IN EPs
daintmsk := otgDevice.DAINTMSK.Get() & 0x0000FFFF
active := daint & daintmsk
for ep := uint32(0); ep < 4; ep++ {
if active&(1<<ep) == 0 {
continue
}
diep := otgInEP(ep)
diepint := diep.INT.Get()
diepintmsk := otgDevice.DIEPMSK.Get()
fired := diepint & diepintmsk
if fired&depintXFRC != 0 {
// Clear XFRC.
diep.INT.Set(depintXFRC)
if ep == 0 {
// EP0 IN transfer complete.
if sendOnEP0DATADONE.ptr != nil {
// More data to send.
ptr := sendOnEP0DATADONE.ptr
count := sendOnEP0DATADONE.count
if count > usb.EndpointPacketSize {
sendOnEP0DATADONE.offset += usb.EndpointPacketSize
sendOnEP0DATADONE.ptr = &udd_ep_control_cache_buffer[sendOnEP0DATADONE.offset]
count = usb.EndpointPacketSize
}
sendOnEP0DATADONE.count -= count
sendViaEPIn(0, ptr, count)
if sendOnEP0DATADONE.count == 0 {
sendOnEP0DATADONE.ptr = nil
sendOnEP0DATADONE.offset = 0
}
} else {
// All EP0 IN data sent; arm EP0 OUT for the status ZLP from host.
armEP0Out()
}
} else {
// Non-EP0 IN: find the virtual endpoint(s) mapped to this physical EP
// and call the registered TX handler. Multiple virtual EPs may share
// a physical EP (e.g., HID_IN=4 and CDC_IN=3 both → physical 1 or 3).
for vep := uint32(0); vep < NumberOfUSBEndpoints; vep++ {
if vep == ep && usbTxHandler[vep] != nil {
usbTxHandler[vep]()
}
}
}
}
}
}
// handleOutEndpoints handles OEPINT: checks each active OUT endpoint for STUP / XFRC.
func handleOutEndpoints() {
daint := otgDevice.DAINT.Get() >> 16 // upper 16 bits = OUT EPs
daintmsk := otgDevice.DAINTMSK.Get() >> 16
active := daint & daintmsk
for ep := uint32(0); ep < 4; ep++ {
if active&(1<<ep) == 0 {
continue
}
doep := otgOutEP(ep)
doepint := doep.INT.Get()
doepintmsk := otgDevice.DOEPMSK.Get()
fired := doepint & doepintmsk
if fired&depintSTUP != 0 {
// EP0 SETUP phase done (already processed in handleRxFIFO).
doep.INT.Set(depintSTUP)
}
if fired&depintXFRC != 0 {
doep.INT.Set(depintXFRC)
if ep > 0 {
buf := handleEndpointRx(ep)
// Find the virtual endpoint(s) mapped to this physical EP and call the RX handler.
for vep := uint32(0); vep < NumberOfUSBEndpoints; vep++ {
if vep == ep && usbRxHandler[vep] != nil {
if usbRxHandler[vep](buf) {
AckUsbOutTransfer(ep)
}
break
}
}
}
}
}
}
// initEndpoint configures a USB endpoint for the given type and direction.
// MPS is hardcoded to 64 bytes; the caller (usb.go) does not pass a descriptor.
func initEndpoint(ep, config uint32) {
pep := ep
if pep == 0 {
return // EP0 is always active; configured in handleEnumDone
}
txFIFONum := pep // TX FIFO number matches physical EP
switch config {
case usb.ENDPOINT_TYPE_INTERRUPT | usb.EndpointIn:
ctl := (64 << depctlMPSIZ_Pos) | depctlUSBAEP |
(txFIFONum << depctlTXFNUM_Pos) |
(3 << depctlETYP_Pos) | // interrupt type
depctlSD0PID
otgInEP(pep).CTL.Set(ctl)
otgDevice.DAINTMSK.SetBits(1 << pep)
case usb.ENDPOINT_TYPE_BULK | usb.EndpointIn:
ctl := (64 << depctlMPSIZ_Pos) | depctlUSBAEP |
(txFIFONum << depctlTXFNUM_Pos) |
(2 << depctlETYP_Pos) | // bulk type
depctlSD0PID
otgInEP(pep).CTL.Set(ctl)
otgDevice.DAINTMSK.SetBits(1 << pep)
case usb.ENDPOINT_TYPE_INTERRUPT | usb.EndpointOut:
ctl := uint32(64) | depctlUSBAEP | depctlSD0PID |
(3 << depctlETYP_Pos) // interrupt type
otgOutEP(pep).CTL.Set(ctl)
otgOutEP(pep).TSIZ.Set((1 << 19) | 64)
otgOutEP(pep).CTL.SetBits(depctlEPENA | depctlCNAK)
otgDevice.DAINTMSK.SetBits(1 << (pep + 16))
case usb.ENDPOINT_TYPE_BULK | usb.EndpointOut:
ctl := uint32(64) | depctlUSBAEP | depctlSD0PID |
(2 << depctlETYP_Pos) // bulk type
otgOutEP(pep).CTL.Set(ctl)
otgOutEP(pep).TSIZ.Set((1 << 19) | 64)
otgOutEP(pep).CTL.SetBits(depctlEPENA | depctlCNAK)
otgDevice.DAINTMSK.SetBits(1 << (pep + 16))
case usb.ENDPOINT_TYPE_CONTROL:
// EP0 activated in handleEnumDone.
}
}
// SendUSBInPacket sends data on a USB IN endpoint (interrupt or bulk).
func SendUSBInPacket(ep uint32, data []byte) bool {
sendUSBPacket(ep, data)
return true
}
// sendUSBPacket copies data into the endpoint cache buffer then initiates the transfer.
//
//go:noinline
func sendUSBPacket(ep uint32, data []byte) {
count := len(data)
var buf []byte
if ep == 0 {
buf = udd_ep_control_cache_buffer[:]
if count > usb.EndpointPacketSize {
// Large response: queue continuation via sendOnEP0DATADONE.
sendOnEP0DATADONE.offset = usb.EndpointPacketSize
sendOnEP0DATADONE.ptr = &udd_ep_control_cache_buffer[usb.EndpointPacketSize]
sendOnEP0DATADONE.count = count - usb.EndpointPacketSize
count = usb.EndpointPacketSize
}
} else {
pep := ep
buf = udd_ep_in_cache_buffer[pep][:]
}
copy(buf[:len(data)], data)
sendViaEPIn(ep, &buf[0], count)
}
// sendViaEPIn arms the IN endpoint and writes count bytes from ptr into the TX FIFO.
func sendViaEPIn(ep uint32, ptr *byte, count int) {
pep := ep
diep := otgInEP(pep)
// Verify TX FIFO has enough space before writing.
// DTXFSTS[15:0] = INEPTFSAV: available words. Stall if insufficient.
if count > 0 {
need := uint32((count + 3) / 4)
avail := diep.TXFST.Get() & 0xFFFF
if avail < need {
return
}
}
// Program transfer size: 1 packet, count bytes.
diep.TSIZ.Set(
uint32(count) | (1 << 19), // XFRSIZ = count, PKTCNT = 1
)
// Enable endpoint and clear NAK (starts transfer).
diep.CTL.SetBits(depctlEPENA | depctlCNAK)
// Write bytes to FIFO in 32-bit words (last word padded if needed).
fifo := otgDFIFO(pep)
data := unsafe.Slice(ptr, count)
words := (count + 3) / 4
for i := 0; i < words; i++ {
b := i * 4
var w uint32
w = uint32(data[b])
if b+1 < count {
w |= uint32(data[b+1]) << 8
}
if b+2 < count {
w |= uint32(data[b+2]) << 16
}
if b+3 < count {
w |= uint32(data[b+3]) << 24
}
fifo.Set(w)
}
}
// SendZlp sends a zero-length packet on EP0 IN (status stage for OUT control transfers).
func SendZlp() {
// PKTCNT=1, XFRSIZ=0
otgInEP(0).TSIZ.Set(1 << 19)
otgInEP(0).CTL.SetBits(depctlEPENA | depctlCNAK)
}
// handleEndpointRx returns the bytes received on the given physical endpoint.
func handleEndpointRx(ep uint32) []byte {
pep := ep
return udd_ep_out_cache_buffer[pep][:usbRxBufLen[pep]]
}
// AckUsbOutTransfer re-arms the OUT endpoint to receive the next packet.
func AckUsbOutTransfer(ep uint32) {
pep := ep
usbRxBufLen[pep] = 0
doep := otgOutEP(pep)
doep.TSIZ.Set(
(1 << 19) | 64, // PKTCNT=1, XFRSIZ=64
)
doep.CTL.SetBits(depctlEPENA | depctlCNAK)
}
// handleUSBSetAddress applies the new device address from a SET_ADDRESS request
// and sends the status ZLP. The address is written to DCFG before the ZLP is
// enqueued: the OTG FS core has already committed the current IN token to
// address 0, so the ZLP goes out at the old address while the new address is
// already in DCFG and ready for the host's next transaction.
func handleUSBSetAddress(setup usb.Setup) bool {
addr := uint8(setup.WValueL) & 0x7F
dcfg := otgDevice.DCFG.Get()
dcfg &^= dcfgDAD_Msk
dcfg |= uint32(addr) << dcfgDAD_Pos
otgDevice.DCFG.Set(dcfg)
SendZlp()
return true
}
// ReceiveUSBControlPacket synchronously receives a CDC control OUT packet on EP0.
func ReceiveUSBControlPacket() ([cdcLineInfoSize]byte, error) {
var b [cdcLineInfoSize]byte
// Arm EP0 OUT for up to 64 bytes.
armEP0Out()
// Busy-wait for data to arrive. We call handleRxFIFO() manually to drain
// the shared RX FIFO because we are currently in an interrupt context
// (this is called from the setup handler) and the hardware-triggered
// handleRxFIFO loop is blocked waiting for us to return.
const timeout = 300000
for i := 0; i < timeout; i++ {
if stm32.OTG_FS_GLOBAL.GINTSTS.HasBits(gintRXFLVL) {
handleRxFIFO()
}
if usbRxBufLen[0] > 0 {
n := usbRxBufLen[0]
if n > cdcLineInfoSize {
n = cdcLineInfoSize
}
copy(b[:n], udd_ep_out_cache_buffer[0][:n])
usbRxBufLen[0] = 0
SendZlp()
return b, nil
}
}
return b, ErrUSBReadTimeout
}
// SetStallEPIn stalls an IN endpoint.
func (dev *USBDevice) SetStallEPIn(ep uint32) {
pep := ep
otgInEP(pep).CTL.SetBits(depctlSTALL)
}
// ClearStallEPIn clears the stall condition on an IN endpoint.
func (dev *USBDevice) ClearStallEPIn(ep uint32) {
pep := ep
// Clear STALL and reset DATA0 PID.
ctl := &otgInEP(pep).CTL
ctl.ClearBits(depctlSTALL)
ctl.SetBits(depctlSD0PID)
}
// SetStallEPOut stalls an OUT endpoint.
func (dev *USBDevice) SetStallEPOut(ep uint32) {
pep := ep
otgOutEP(pep).CTL.SetBits(depctlSTALL)
}
// ClearStallEPOut clears the stall condition on an OUT endpoint.
func (dev *USBDevice) ClearStallEPOut(ep uint32) {
pep := ep
ctl := &otgOutEP(pep).CTL
ctl.ClearBits(depctlSTALL)
ctl.SetBits(depctlSD0PID)
}
// armEP0Out re-arms EP0 OUT to receive the next SETUP or status ZLP from the host.
func armEP0Out() {
// STUPCNT=3 (bits[30:29]=11): accept up to 3 back-to-back SETUPs.
// PKTCNT=1 (bit[19]): one packet.
// XFRSIZ=64 (bits[6:0]): max 64 bytes.
otgOutEP(0).TSIZ.Set((3 << 29) | (1 << 19) | 64)
otgOutEP(0).CTL.SetBits(depctlEPENA | depctlCNAK)
}
// flushTxFIFO flushes the selected TX FIFO(s).
// txfnum: 03 for a specific FIFO, 0x10 to flush all TX FIFOs.
func flushTxFIFO(txfnum uint32) {
stm32.OTG_FS_GLOBAL.GRSTCTL.Set(
grstTXFFLSH | (txfnum << grstTXFNUM_Pos),
)
for stm32.OTG_FS_GLOBAL.GRSTCTL.HasBits(grstTXFFLSH) {
}
}
// flushRxFIFO flushes the shared RX FIFO.
func flushRxFIFO() {
stm32.OTG_FS_GLOBAL.GRSTCTL.Set(grstRXFFLSH)
for stm32.OTG_FS_GLOBAL.GRSTCTL.HasBits(grstRXFFLSH) {
}
}
@@ -0,0 +1,12 @@
//go:build stm32f4 && !(stm32f429 || stm32f427 || stm32f411 || stm32f407 || stm32f405 || stm32f401)
package machine
import "device/stm32"
// initOTGFSPHY enables the STM32F4 OTG FS PHY and bypasses VBUS sensing.
// GCCFG.PWRDWN deactivates the PHY power-down; NOVBUSSENS skips the VBUS pin
// check so boards without PA9 connected to VBUS still enumerate.
func initOTGFSPHY() {
stm32.OTG_FS_GLOBAL.GCCFG.Set(stm32.USB_OTG_FS_GCCFG_PWRDWN)
}
+15
View File
@@ -0,0 +1,15 @@
//go:build stm32f4 && (stm32f429 || stm32f427 || stm32f411 || stm32f407 || stm32f405 || stm32f401)
package machine
import "device/stm32"
// initOTGFSPHY enables the STM32F4 OTG FS PHY and bypasses VBUS sensing.
// GCCFG.PWRDWN deactivates the PHY power-down; NOVBUSSENS skips the VBUS pin
// check so boards without PA9 connected to VBUS still enumerate.
func initOTGFSPHY() {
stm32.OTG_FS_GLOBAL.GCCFG.Set(
stm32.USB_OTG_FS_GCCFG_PWRDWN | // enable FS PHY
stm32.USB_OTG_FS_GCCFG_NOVBUSSENS, // bypass VBUS sensing
)
}
+10
View File
@@ -0,0 +1,10 @@
//go:build stm32f7
package machine
// eraseBlockSize returns the smallest erasable unit for the STM32F7 internal
// flash. The first sectors are 32 KB; return that as the nominal page size.
// Flash write/erase via machine.Flash is not implemented for STM32F7; this
// stub satisfies the flash.go interface so that BlockDevice (needed by MSC)
// compiles on this target.
func eraseBlockSize() int64 { return 32768 }
+34
View File
@@ -0,0 +1,34 @@
//go:build stm32f7
package machine
import (
"device/arm"
"device/stm32"
)
// initOTGFSPHY enables the STM32F7 OTG FS PHY and overrides B-session VBUS
// detection via GOTGCTL so boards without VBUS sensing still enumerate.
func initOTGFSPHY() {
// Enable USB voltage regulator (specific to F72x/F73x).
// Bit 14 in PWR_CR2 is USBREGEN.
stm32.PWR.CR2.SetBits(1 << 14)
// Stabilization delay for the regulator (~100us is plenty).
for i := 0; i < 10000; i++ {
arm.Asm("nop")
}
// Enable FS PHY.
stm32.OTG_FS_GLOBAL.GCCFG.SetBits(stm32.USB_OTG_FS_GCCFG_PWRDWN)
// Disable hardware VBUS detection (F7 uses VBDEN, opposite polarity to F4's NOVBUSSENS).
// Clearing this prevents the peripheral from gating enumeration on PA9 VBUS level.
stm32.OTG_FS_GLOBAL.GCCFG.ClearBits(stm32.USB_OTG_FS_GCCFG_VBDEN)
// Override B-session valid so GOTGCTL-based detection reports device connected.
stm32.OTG_FS_GLOBAL.GOTGCTL.SetBits(
stm32.USB_OTG_FS_GOTGCTL_BVALOEN | // enable B-valid override
stm32.USB_OTG_FS_GOTGCTL_BVALOVAL, // set B-valid = 1
)
}
+8 -2
View File
@@ -1,4 +1,4 @@
//go:build sam || nrf52840 || rp2040 || rp2350
//go:build sam || nrf52840 || rp2040 || rp2350 || stm32f4 || stm32f7
package machine
@@ -184,7 +184,9 @@ func sendDescriptor(setup usb.Setup) {
return
}
case descriptor.TypeDeviceQualifier:
// skip
// Full-speed-only device: STALL to signal no high-speed capability (USB 2.0 §9.6.2).
USBDev.SetStallEPIn(0)
return
default:
}
@@ -369,6 +371,10 @@ func EnableCDC(txHandler func(), rxHandler func([]byte), setupHandler func(usb.S
})
}
// PhysicalEndpoint maps a virtual endpoint index to the physical endpoint number
// used by the hardware. This is an identity mapping on all currently supported platforms.
func PhysicalEndpoint(ep uint32) uint32 { return ep }
func ConfigureUSBEndpoint(desc descriptor.Descriptor, epSettings []usb.EndpointConfig, setup []usb.SetupConfig) {
usbDescriptor = desc
+1
View File
@@ -165,6 +165,7 @@ func (m *msc) sendCSW(status csw.Status) {
residue = expected - m.sentBytes
}
m.cbw.CSW(status, residue, m.cswBuf)
m.queuedBytes = csw.MsgLen
m.state = mscStateStatusSent
m.queuedBytes = csw.MsgLen
m.sendUSBPacket(m.cswBuf)
+1
View File
@@ -51,6 +51,7 @@ func (m *msc) handleClearFeature(setup usb.Setup, wValue uint16) bool {
// (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 := uint8(setup.WIndex & 0x7F)
if wIndex == usb.MSC_ENDPOINT_IN {
+5
View File
@@ -0,0 +1,5 @@
//go:build stm32 && serial.usb
package runtime
import _ "machine/usb/cdc"
+15 -9
View File
@@ -23,7 +23,7 @@ const (
PLL_M = 4
PLL_N = 216
PLL_P = 2
PLL_Q = 2
PLL_Q = 9
)
func init() {
@@ -55,7 +55,7 @@ func initCLK() {
stm32.RCC.APB1ENR.SetBits(stm32.RCC_APB1ENR_PWREN)
_ = stm32.RCC.APB1ENR.Get()
// PWR_VOLTAGESCALING_CONFIG
// PWR_VOLTAGESCALING_CONFIG: Set VOS to Scale 1 (max performance)
stm32.PWR.CR1.ReplaceBits(0x3<<stm32.PWR_CR1_VOS_Pos, stm32.PWR_CR1_VOS_Msk, 0)
_ = stm32.PWR.CR1.Get()
@@ -67,6 +67,9 @@ func initCLK() {
stm32.FLASH.ACR.ReplaceBits(7, stm32.FLASH_ACR_LATENCY_Msk, 0)
}
// Enable ART Accelerator and Prefetch (ARTEN also enables I-Cache on F7)
stm32.FLASH.ACR.SetBits(stm32.FLASH_ACR_ARTEN | stm32.FLASH_ACR_PRFTEN)
// HCLK (0x1C00 = DIV_16, 0x0 = RCC_SYSCLK_DIV1) - ensure timers remain
// within spec as the SYSCLK source changes.
stm32.RCC.CFGR.ReplaceBits(0x00001C00, stm32.RCC_CFGR_PPRE1_Msk, 0)
@@ -87,6 +90,9 @@ func initCLK() {
// Set APB1 and APB2 clocks (0x1800 = DIV8, 0x1000 = DIV2)
stm32.RCC.CFGR.ReplaceBits(0x1800, stm32.RCC_CFGR_PPRE1_Msk, 0)
stm32.RCC.CFGR.ReplaceBits(0x1000<<3, stm32.RCC_CFGR_PPRE2_Msk, 0)
// Select Main PLL as the 48MHz clock source for USB/RNG/SDMMC (CK48MSEL = 0).
stm32.RCC.DCKCFGR2.ClearBits(stm32.RCC_DCKCFGR2_CK48MSEL)
}
func initOsc() {
@@ -100,13 +106,13 @@ func initOsc() {
for stm32.RCC.CR.HasBits(stm32.RCC_CR_PLLRDY) {
}
// Configure the PLL
stm32.RCC.PLLCFGR.Set(0x20000000 |
(1 << stm32.RCC_PLLCFGR_PLLSRC_Pos) | // 1 = HSE
PLL_M |
(PLL_N << stm32.RCC_PLLCFGR_PLLN_Pos) |
(((PLL_P >> 1) - 1) << stm32.RCC_PLLCFGR_PLLP_Pos) |
(PLL_Q << stm32.RCC_PLLCFGR_PLLQ_Pos))
// Configure the PLL: HSE as source, use SVD constants for positions.
stm32.RCC.PLLCFGR.Set(
(stm32.RCC_PLLCFGR_PLLSRC_HSE << stm32.RCC_PLLCFGR_PLLSRC_Pos) |
(PLL_M << stm32.RCC_PLLCFGR_PLLM_Pos) |
(PLL_N << stm32.RCC_PLLCFGR_PLLN_Pos) |
(((PLL_P >> 1) - 1) << stm32.RCC_PLLCFGR_PLLP_Pos) |
(PLL_Q << stm32.RCC_PLLCFGR_PLLQ_Pos))
// Enable the PLL, wait until ready
stm32.RCC.CR.SetBits(stm32.RCC_CR_PLLON)