From 3aaf8e5ec424a01b58be1e0837e38c4427efb77c Mon Sep 17 00:00:00 2001 From: Konstantin Sharlaimov Date: Fri, 15 May 2026 22:44:54 +0200 Subject: [PATCH] machine/stm32: add OTG FS USB driver for F4/F7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- builder/sizes_test.go | 2 +- src/examples/hid-keyboard/main.go | 1 + src/examples/usb-storage/main.go | 79 +- src/machine/flash.go | 2 +- src/machine/machine_atsamd21_usb.go | 8 + src/machine/machine_atsamd51_usb.go | 8 + src/machine/machine_nrf52840_usb.go | 16 + src/machine/machine_stm32.go | 5 + src/machine/machine_stm32_otgfs_usb.go | 858 ++++++++++++++++++++ src/machine/machine_stm32f4_otgfs_novbus.go | 12 + src/machine/machine_stm32f4_otgfs_vbus.go | 15 + src/machine/machine_stm32f7_flash.go | 10 + src/machine/machine_stm32f7_otgfs_vbus.go | 34 + src/machine/usb.go | 14 +- src/machine/usb/descriptor/endpoint.go | 42 + src/machine/usb/descriptor/hid_stm32.go | 29 + src/machine/usb/descriptor/msc_stm32.go | 45 + src/machine/usb/msc/msc.go | 1 + src/machine/usb/msc/setup.go | 13 +- src/machine/usb_physep.go | 7 + src/runtime/runtime_stm32_usb.go | 5 + src/runtime/runtime_stm32f7x2.go | 24 +- 22 files changed, 1211 insertions(+), 19 deletions(-) create mode 100644 src/machine/machine_stm32_otgfs_usb.go create mode 100644 src/machine/machine_stm32f4_otgfs_novbus.go create mode 100644 src/machine/machine_stm32f4_otgfs_vbus.go create mode 100644 src/machine/machine_stm32f7_flash.go create mode 100644 src/machine/machine_stm32f7_otgfs_vbus.go create mode 100644 src/machine/usb/descriptor/hid_stm32.go create mode 100644 src/machine/usb/descriptor/msc_stm32.go create mode 100644 src/machine/usb_physep.go create mode 100644 src/runtime/runtime_stm32_usb.go diff --git a/builder/sizes_test.go b/builder/sizes_test.go index fd985a8977..58bf313502 100644 --- a/builder/sizes_test.go +++ b/builder/sizes_test.go @@ -44,7 +44,7 @@ func TestBinarySize(t *testing.T) { // microcontrollers {"hifive1b", "examples/echo", 3680, 280, 0, 2252}, {"microbit", "examples/serial", 2694, 342, 8, 2248}, - {"wioterminal", "examples/pininterrupt", 7074, 1510, 120, 7248}, + {"wioterminal", "examples/pininterrupt", 7090, 1510, 120, 7248}, // TODO: also check wasm. Right now this is difficult, because // wasm binaries are run through wasm-opt and therefore the diff --git a/src/examples/hid-keyboard/main.go b/src/examples/hid-keyboard/main.go index dfa666d378..269970882a 100644 --- a/src/examples/hid-keyboard/main.go +++ b/src/examples/hid-keyboard/main.go @@ -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() { diff --git a/src/examples/usb-storage/main.go b/src/examples/usb-storage/main.go index 10aa3612d1..1f1379fafc 100644 --- a/src/examples/usb-storage/main.go +++ b/src/examples/usb-storage/main.go @@ -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) diff --git a/src/machine/flash.go b/src/machine/flash.go index bcbc64e12e..bb3d335f83 100644 --- a/src/machine/flash.go +++ b/src/machine/flash.go @@ -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 diff --git a/src/machine/machine_atsamd21_usb.go b/src/machine/machine_atsamd21_usb.go index e7faec8ecb..fc341af6b8 100644 --- a/src/machine/machine_atsamd21_usb.go +++ b/src/machine/machine_atsamd21_usb.go @@ -663,3 +663,11 @@ func setEPINTENSET(ep uint32, val uint8) { return } } + +func (dev *USBDevice) SetStallEPIn(ep uint32) { + setEPSTATUSSET(ep, sam.USB_DEVICE_EPSTATUSSET_STALLRQ1) +} + +func (dev *USBDevice) SetStallEPOut(ep uint32) { + setEPSTATUSSET(ep, sam.USB_DEVICE_EPSTATUSSET_STALLRQ0) +} diff --git a/src/machine/machine_atsamd51_usb.go b/src/machine/machine_atsamd51_usb.go index 49b44739a3..019e07449a 100644 --- a/src/machine/machine_atsamd51_usb.go +++ b/src/machine/machine_atsamd51_usb.go @@ -494,3 +494,11 @@ func setEPINTENCLR(ep uint32, val uint8) { func setEPINTENSET(ep uint32, val uint8) { sam.USB_DEVICE.DEVICE_ENDPOINT[ep].EPINTENSET.Set(val) } + +func (dev *USBDevice) SetStallEPIn(ep uint32) { + setEPSTATUSSET(ep, sam.USB_DEVICE_ENDPOINT_EPSTATUSSET_STALLRQ1) +} + +func (dev *USBDevice) SetStallEPOut(ep uint32) { + setEPSTATUSSET(ep, sam.USB_DEVICE_ENDPOINT_EPSTATUSSET_STALLRQ0) +} diff --git a/src/machine/machine_nrf52840_usb.go b/src/machine/machine_nrf52840_usb.go index cacafd47b4..2a85e1786e 100644 --- a/src/machine/machine_nrf52840_usb.go +++ b/src/machine/machine_nrf52840_usb.go @@ -378,3 +378,19 @@ func ReceiveUSBControlPacket() ([cdcLineInfoSize]byte, error) { return b, nil } + +func (dev *USBDevice) SetStallEPIn(ep uint32) { + if ep == 0 { + nrf.USBD.TASKS_EP0STALL.Set(1) + return + } + nrf.USBD.EPSTALL.Set(uint32(ep) | nrf.USBD_EPSTALL_IO_In<<7 | nrf.USBD_EPSTALL_STALL_Stall<<8) +} + +func (dev *USBDevice) SetStallEPOut(ep uint32) { + if ep == 0 { + nrf.USBD.TASKS_EP0STALL.Set(1) + return + } + nrf.USBD.EPSTALL.Set(uint32(ep) | nrf.USBD_EPSTALL_IO_Out<<7 | nrf.USBD_EPSTALL_STALL_Stall<<8) +} diff --git a/src/machine/machine_stm32.go b/src/machine/machine_stm32.go index 1edaa2cd06..d33ec5e300 100644 --- a/src/machine/machine_stm32.go +++ b/src/machine/machine_stm32.go @@ -86,6 +86,11 @@ 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. +func EnterBootloader() { +} + var deviceID [12]byte // DeviceID returns an identifier that is unique within diff --git a/src/machine/machine_stm32_otgfs_usb.go b/src/machine/machine_stm32_otgfs_usb.go new file mode 100644 index 0000000000..67b79d0912 --- /dev/null +++ b/src/machine/machine_stm32_otgfs_usb.go @@ -0,0 +1,858 @@ +//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 (0–3). Virtual indices 4–7 +// are mapped to physical 1–3 by physEP(). +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)) +} + +// physEP maps a TinyGo virtual endpoint index (0–7) to a physical OTG FS endpoint (0–3). +func physEP(ep uint32) uint32 { + switch ep { + case 0, 1, 2, 3: + return ep + case 4: // HID IN -> physical 2 + return 2 + case 5: // HID OUT -> physical 1 + return 1 + case 6: // MIDI IN -> physical 2 (collides with HID IN if both active) + return 2 + case 7: // MIDI OUT -> physical 3 + return 3 + default: + return ep % 4 + } +} + +// 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 ( + endPoints = []uint32{ + usb.CONTROL_ENDPOINT: usb.ENDPOINT_TYPE_CONTROL, + usb.CDC_ENDPOINT_ACM: usb.ENDPOINT_TYPE_DISABLE, + usb.CDC_ENDPOINT_OUT: usb.ENDPOINT_TYPE_DISABLE, + usb.CDC_ENDPOINT_IN: usb.ENDPOINT_TYPE_DISABLE, + usb.HID_ENDPOINT_IN: usb.ENDPOINT_TYPE_DISABLE, + usb.HID_ENDPOINT_OUT: usb.ENDPOINT_TYPE_DISABLE, + usb.MIDI_ENDPOINT_IN: usb.ENDPOINT_TYPE_DISABLE, + usb.MIDI_ENDPOINT_OUT: usb.ENDPOINT_TYPE_DISABLE, + } + + // 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 0–3). + 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, + ) + + // EP1–3 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 (0–3) + + 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< 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 physEP(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< 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 physEP(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 := physEP(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 := physEP(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 := physEP(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 := physEP(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 := physEP(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 := physEP(ep) + otgInEP(pep).CTL.SetBits(depctlSTALL) +} + +// ClearStallEPIn clears the stall condition on an IN endpoint. +func (dev *USBDevice) ClearStallEPIn(ep uint32) { + pep := physEP(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 := physEP(ep) + otgOutEP(pep).CTL.SetBits(depctlSTALL) +} + +// ClearStallEPOut clears the stall condition on an OUT endpoint. +func (dev *USBDevice) ClearStallEPOut(ep uint32) { + pep := physEP(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: 0–3 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) { + } +} diff --git a/src/machine/machine_stm32f4_otgfs_novbus.go b/src/machine/machine_stm32f4_otgfs_novbus.go new file mode 100644 index 0000000000..19b35d5e3e --- /dev/null +++ b/src/machine/machine_stm32f4_otgfs_novbus.go @@ -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) +} diff --git a/src/machine/machine_stm32f4_otgfs_vbus.go b/src/machine/machine_stm32f4_otgfs_vbus.go new file mode 100644 index 0000000000..e9830d9969 --- /dev/null +++ b/src/machine/machine_stm32f4_otgfs_vbus.go @@ -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 + ) +} diff --git a/src/machine/machine_stm32f7_flash.go b/src/machine/machine_stm32f7_flash.go new file mode 100644 index 0000000000..98323d30e9 --- /dev/null +++ b/src/machine/machine_stm32f7_flash.go @@ -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 } diff --git a/src/machine/machine_stm32f7_otgfs_vbus.go b/src/machine/machine_stm32f7_otgfs_vbus.go new file mode 100644 index 0000000000..97638084c2 --- /dev/null +++ b/src/machine/machine_stm32f7_otgfs_vbus.go @@ -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 + ) +} diff --git a/src/machine/usb.go b/src/machine/usb.go index 3833c78136..f7e70f286d 100644 --- a/src/machine/usb.go +++ b/src/machine/usb.go @@ -1,4 +1,4 @@ -//go:build sam || nrf52840 || rp2040 || rp2350 +//go:build sam || nrf52840 || rp2040 || rp2350 || stm32f4 || stm32f7 package machine @@ -172,7 +172,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: } @@ -353,6 +355,11 @@ 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. On most platforms this is an identity mapping; STM32 OTG FS +// overrides physEP() to fold virtual EPs 4-7 onto physical EPs 0-3. +func PhysicalEndpoint(ep uint32) uint32 { return physEP(ep) } + func ConfigureUSBEndpoint(desc descriptor.Descriptor, epSettings []usb.EndpointConfig, setup []usb.SetupConfig) { usbDescriptor = desc @@ -375,6 +382,9 @@ func ConfigureUSBEndpoint(desc descriptor.Descriptor, epSettings []usb.EndpointC } if ep.StallHandler != nil { usbStallHandler[ep.Index] = ep.StallHandler + if pep := physEP(uint32(ep.Index)); pep != uint32(ep.Index) && pep < uint32(len(usbStallHandler)) { + usbStallHandler[pep] = ep.StallHandler + } } } diff --git a/src/machine/usb/descriptor/endpoint.go b/src/machine/usb/descriptor/endpoint.go index 57a17060c4..44458fa158 100644 --- a/src/machine/usb/descriptor/endpoint.go +++ b/src/machine/usb/descriptor/endpoint.go @@ -85,6 +85,48 @@ var EndpointEP5OUT = EndpointType{ data: endpointEP5OUT[:], } +// EndpointEP2IN is an interrupt IN endpoint at address 0x82 (physical EP2). +// Used by targets with limited endpoint counts (e.g. STM32 OTG FS, max EP3). +var endpointEP2IN = [endpointTypeLen]byte{ + endpointTypeLen, + TypeEndpoint, + 0x82, // EndpointAddress: EP2 IN + 0x03, // Attributes: interrupt + 0x40, // MaxPacketSizeL + 0x00, // MaxPacketSizeH + 0x01, // Interval +} + +var EndpointEP2IN = EndpointType{data: endpointEP2IN[:]} + +// EndpointEP1OUT is an interrupt OUT endpoint at address 0x01 (physical EP1). +// Used by targets with limited endpoint counts (e.g. STM32 OTG FS, max EP3). +var endpointEP1OUT = [endpointTypeLen]byte{ + endpointTypeLen, + TypeEndpoint, + 0x01, // EndpointAddress: EP1 OUT + 0x03, // Attributes: interrupt + 0x40, // MaxPacketSizeL + 0x00, // MaxPacketSizeH + 0x01, // Interval +} + +var EndpointEP1OUT = EndpointType{data: endpointEP1OUT[:]} + +// EndpointEP3OUT is a bulk OUT endpoint at address 0x03 (physical EP3). +// Used by targets with limited endpoint counts (e.g. STM32 OTG FS, max EP3). +var endpointEP3OUT = [endpointTypeLen]byte{ + endpointTypeLen, + TypeEndpoint, + 0x03, // EndpointAddress: EP3 OUT + TransferTypeBulk, // Attributes: bulk + 0x40, // MaxPacketSizeL + 0x00, // MaxPacketSizeH + 0x00, // Interval +} + +var EndpointEP3OUT = EndpointType{data: endpointEP3OUT[:]} + // Mass Storage Class bulk in endpoint var endpointMSCIN = [endpointTypeLen]byte{ endpointTypeLen, diff --git a/src/machine/usb/descriptor/hid_stm32.go b/src/machine/usb/descriptor/hid_stm32.go new file mode 100644 index 0000000000..9f9c58fe39 --- /dev/null +++ b/src/machine/usb/descriptor/hid_stm32.go @@ -0,0 +1,29 @@ +//go:build stm32f4 || stm32f7 + +package descriptor + +// STM32 OTG FS has only 4 physical endpoints (0–3). Default CDCHID descriptor +// advertises EP4/EP5 which the hardware cannot service. Remap to free directions +// on physical EPs already used by CDC: +// physEP(4)=2 → EP2 IN (0x82, interrupt) for HID IN +// physEP(5)=1 → EP1 OUT (0x01, interrupt) for HID OUT + +func init() { + CDCHID.Configuration = Append([][]byte{ + ConfigurationCDCHID.Bytes(), + InterfaceAssociationCDC.Bytes(), + InterfaceCDCControl.Bytes(), + ClassSpecificCDCHeader.Bytes(), + ClassSpecificCDCACM.Bytes(), + ClassSpecificCDCUnion.Bytes(), + ClassSpecificCDCCallManagement.Bytes(), + EndpointEP1IN.Bytes(), + InterfaceCDCData.Bytes(), + EndpointEP2OUT.Bytes(), + EndpointEP3IN.Bytes(), + InterfaceHID.Bytes(), + ClassHID.Bytes(), + EndpointEP2IN.Bytes(), // HID IN on physical EP2 (address 0x82) + EndpointEP1OUT.Bytes(), // HID OUT on physical EP1 (address 0x01) + }) +} diff --git a/src/machine/usb/descriptor/msc_stm32.go b/src/machine/usb/descriptor/msc_stm32.go new file mode 100644 index 0000000000..925c149f5a --- /dev/null +++ b/src/machine/usb/descriptor/msc_stm32.go @@ -0,0 +1,45 @@ +//go:build stm32f4 || stm32f7 + +package descriptor + +// STM32 OTG FS has only 4 physical endpoints (0–3). Default MSC descriptor +// advertises EP6/EP7 which the hardware cannot service. Remap to physical EPs: +// physEP(6)=2 → EP2 IN (0x82, bulk) for MSC IN +// physEP(7)=3 → EP3 OUT (0x03, bulk) for MSC OUT + +var stm32EndpointMSCIN = [endpointTypeLen]byte{ + endpointTypeLen, TypeEndpoint, + 0x82, // EP2 IN + TransferTypeBulk, + 0x40, 0x00, 0x00, +} + +var stm32EndpointMSCOUT = [endpointTypeLen]byte{ + endpointTypeLen, TypeEndpoint, + 0x03, // EP3 OUT + TransferTypeBulk, + 0x40, 0x00, 0x00, +} + +var stm32MSCIN = EndpointType{data: stm32EndpointMSCIN[:]} +var stm32MSCOUT = EndpointType{data: stm32EndpointMSCOUT[:]} + +func init() { + MSC.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(), + stm32MSCIN.Bytes(), // MSC IN on physical EP2 (address 0x82, bulk) + stm32MSCOUT.Bytes(), // MSC OUT on physical EP3 (address 0x03, bulk) + }) +} diff --git a/src/machine/usb/msc/msc.go b/src/machine/usb/msc/msc.go index 420a06ed98..43a069c998 100644 --- a/src/machine/usb/msc/msc.go +++ b/src/machine/usb/msc/msc.go @@ -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.sendUSBPacket(m.cswBuf) } diff --git a/src/machine/usb/msc/setup.go b/src/machine/usb/msc/setup.go index 00507aac69..8fa445bfa1 100644 --- a/src/machine/usb/msc/setup.go +++ b/src/machine/usb/msc/setup.go @@ -51,11 +51,14 @@ 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 + physIN := uint16(machine.PhysicalEndpoint(uint32(usb.MSC_ENDPOINT_IN))) + physOUT := uint16(machine.PhysicalEndpoint(uint32(usb.MSC_ENDPOINT_OUT))) + if m.state == mscStateNeedReset { wIndex := setup.WIndex & 0x7F // Clear the direction bit from the endpoint address for comparison - if wIndex == usb.MSC_ENDPOINT_IN { + if wIndex == physIN { m.stallEndpoint(usb.MSC_ENDPOINT_IN) - } else if wIndex == usb.MSC_ENDPOINT_OUT { + } else if wIndex == physOUT { m.stallEndpoint(usb.MSC_ENDPOINT_OUT) } machine.SendZlp() @@ -66,16 +69,16 @@ func (m *msc) handleClearFeature(setup usb.Setup, wValue uint16) bool { wIndex := setup.WIndex & 0x7F // Clear the IN/OUT stalls if addressed to the endpoint - if wIndex == usb.MSC_ENDPOINT_IN { + if wIndex == physIN { m.clearStallEndpoint(usb.MSC_ENDPOINT_IN) ok = true } - if wIndex == usb.MSC_ENDPOINT_OUT { + if wIndex == physOUT { 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 { + if m.state == mscStateStatus && wIndex == physIN { m.sendCSW(m.respStatus) ok = true } diff --git a/src/machine/usb_physep.go b/src/machine/usb_physep.go new file mode 100644 index 0000000000..fb76547b4b --- /dev/null +++ b/src/machine/usb_physep.go @@ -0,0 +1,7 @@ +//go:build sam || nrf52840 || rp2040 || rp2350 + +package machine + +// physEP is the identity mapping for platforms with direct endpoint addressing. +// STM32 OTG FS overrides this in machine_stm32_otgfs_usb.go. +func physEP(ep uint32) uint32 { return ep } diff --git a/src/runtime/runtime_stm32_usb.go b/src/runtime/runtime_stm32_usb.go new file mode 100644 index 0000000000..36e27fc5a6 --- /dev/null +++ b/src/runtime/runtime_stm32_usb.go @@ -0,0 +1,5 @@ +//go:build stm32 && serial.usb + +package runtime + +import _ "machine/usb/cdc" diff --git a/src/runtime/runtime_stm32f7x2.go b/src/runtime/runtime_stm32f7x2.go index a15c692b1e..af78d70b7b 100644 --- a/src/runtime/runtime_stm32f7x2.go +++ b/src/runtime/runtime_stm32f7x2.go @@ -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<> 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)