From f6040979a41f26cafd5bf8d5037b9d7954dbbc58 Mon Sep 17 00:00:00 2001 From: sago35 Date: Tue, 28 Jul 2026 21:29:37 +0900 Subject: [PATCH 1/4] machine: add USBDevice.Attach and USBDevice.Detach The USB device is attached to the bus automatically during startup, before user code has a chance to finish its USB configuration (device identifiers, extra HID interfaces, ...). Composite devices such as keyboards may therefore be enumerated by the host with an incomplete configuration. Attach and Detach expose the soft-connect control (DP pull-up) so that an application or library can detach in an init function, complete its configuration, and attach again to let the host enumerate the finished device. They can also be used to force re-enumeration without replugging the cable. Implemented for atsamd21, atsamd51, nrf52840, rp2040 and rp2350. --- src/machine/machine_atsamd21_usb.go | 14 ++++++++++++++ src/machine/machine_atsamd51_usb.go | 14 ++++++++++++++ src/machine/machine_nrf52840_usb.go | 15 +++++++++++++++ src/machine/machine_rp2040_usb.go | 15 +++++++++++++++ src/machine/machine_rp2350_usb.go | 15 +++++++++++++++ 5 files changed, 73 insertions(+) diff --git a/src/machine/machine_atsamd21_usb.go b/src/machine/machine_atsamd21_usb.go index 45ba18d454..51f71f9d4f 100644 --- a/src/machine/machine_atsamd21_usb.go +++ b/src/machine/machine_atsamd21_usb.go @@ -68,6 +68,20 @@ func (dev *USBDevice) Configure(config UARTConfig) { dev.initcomplete = true } +// Attach connects the device to the USB bus, allowing the host to detect and +// enumerate it. It can be used together with Detach to delay enumeration +// until the USB configuration (device identifiers, classes, ...) is complete. +func (dev *USBDevice) Attach() { + sam.USB_DEVICE.CTRLB.ClearBits(sam.USB_DEVICE_CTRLB_DETACH) +} + +// Detach disconnects the device from the USB bus. To the host this appears +// as if the device was unplugged. A subsequent Attach makes the host +// enumerate the device again. +func (dev *USBDevice) Detach() { + sam.USB_DEVICE.CTRLB.SetBits(sam.USB_DEVICE_CTRLB_DETACH) +} + func handlePadCalibration() { // Load Pad Calibration data from non-volatile memory // This requires registers that are not included in the SVD file. diff --git a/src/machine/machine_atsamd51_usb.go b/src/machine/machine_atsamd51_usb.go index 186ba4ce01..925cfef5ea 100644 --- a/src/machine/machine_atsamd51_usb.go +++ b/src/machine/machine_atsamd51_usb.go @@ -71,6 +71,20 @@ func (dev *USBDevice) Configure(config UARTConfig) { dev.initcomplete = true } +// Attach connects the device to the USB bus, allowing the host to detect and +// enumerate it. It can be used together with Detach to delay enumeration +// until the USB configuration (device identifiers, classes, ...) is complete. +func (dev *USBDevice) Attach() { + sam.USB_DEVICE.CTRLB.ClearBits(sam.USB_DEVICE_CTRLB_DETACH) +} + +// Detach disconnects the device from the USB bus. To the host this appears +// as if the device was unplugged. A subsequent Attach makes the host +// enumerate the device again. +func (dev *USBDevice) Detach() { + sam.USB_DEVICE.CTRLB.SetBits(sam.USB_DEVICE_CTRLB_DETACH) +} + func handlePadCalibration() { // Load Pad Calibration data from non-volatile memory // This requires registers that are not included in the SVD file. diff --git a/src/machine/machine_nrf52840_usb.go b/src/machine/machine_nrf52840_usb.go index 0dc222b372..263d120ff9 100644 --- a/src/machine/machine_nrf52840_usb.go +++ b/src/machine/machine_nrf52840_usb.go @@ -89,6 +89,21 @@ func (dev *USBDevice) Configure(config UARTConfig) { dev.initcomplete = true } +// Attach connects the device to the USB bus by enabling the DP pull-up, +// allowing the host to detect and enumerate it. It can be used together with +// Detach to delay enumeration until the USB configuration (device +// identifiers, classes, ...) is complete. +func (dev *USBDevice) Attach() { + nrf.USBD.USBPULLUP.Set(1) +} + +// Detach disconnects the device from the USB bus by disabling the DP pull-up. +// To the host this appears as if the device was unplugged. A subsequent +// Attach makes the host enumerate the device again. +func (dev *USBDevice) Detach() { + nrf.USBD.USBPULLUP.Set(0) +} + func handleUSBIRQ(interrupt.Interrupt) { if nrf.USBD.EVENTS_SOF.Get() == 1 { nrf.USBD.EVENTS_SOF.Set(0) diff --git a/src/machine/machine_rp2040_usb.go b/src/machine/machine_rp2040_usb.go index efa6440428..fb3c4c3a42 100644 --- a/src/machine/machine_rp2040_usb.go +++ b/src/machine/machine_rp2040_usb.go @@ -46,6 +46,21 @@ func (dev *USBDevice) Configure(config UARTConfig) { rp.USBCTRL_REGS.SIE_CTRL.SetBits(rp.USBCTRL_REGS_SIE_CTRL_PULLUP_EN) } +// Attach connects the device to the USB bus by enabling the DP pull-up, +// allowing the host to detect and enumerate it. It can be used together with +// Detach to delay enumeration until the USB configuration (device +// identifiers, classes, ...) is complete. +func (dev *USBDevice) Attach() { + rp.USBCTRL_REGS.SIE_CTRL.SetBits(rp.USBCTRL_REGS_SIE_CTRL_PULLUP_EN) +} + +// Detach disconnects the device from the USB bus by disabling the DP pull-up. +// To the host this appears as if the device was unplugged. A subsequent +// Attach makes the host enumerate the device again. +func (dev *USBDevice) Detach() { + rp.USBCTRL_REGS.SIE_CTRL.ClearBits(rp.USBCTRL_REGS_SIE_CTRL_PULLUP_EN) +} + func handleUSBIRQ(intr interrupt.Interrupt) { status := rp.USBCTRL_REGS.INTS.Get() diff --git a/src/machine/machine_rp2350_usb.go b/src/machine/machine_rp2350_usb.go index ca565738a5..3d35f2d122 100644 --- a/src/machine/machine_rp2350_usb.go +++ b/src/machine/machine_rp2350_usb.go @@ -49,6 +49,21 @@ func (dev *USBDevice) Configure(config UARTConfig) { rp.USB.SetMAIN_CTRL_PHY_ISO(0x0) } +// Attach connects the device to the USB bus by enabling the DP pull-up, +// allowing the host to detect and enumerate it. It can be used together with +// Detach to delay enumeration until the USB configuration (device +// identifiers, classes, ...) is complete. +func (dev *USBDevice) Attach() { + rp.USB.SIE_CTRL.SetBits(rp.USB_SIE_CTRL_PULLUP_EN) +} + +// Detach disconnects the device from the USB bus by disabling the DP pull-up. +// To the host this appears as if the device was unplugged. A subsequent +// Attach makes the host enumerate the device again. +func (dev *USBDevice) Detach() { + rp.USB.SIE_CTRL.ClearBits(rp.USB_SIE_CTRL_PULLUP_EN) +} + func handleUSBIRQ(intr interrupt.Interrupt) { status := rp.USB.INTS.Get() From c16ff7b1dddd53cb9148e832e418de652059261a Mon Sep 17 00:00:00 2001 From: sago35 Date: Thu, 20 Aug 2026 22:18:39 +0900 Subject: [PATCH 2/4] machine: make USB Detach sticky on nrf52840 The USB IRQ handler re-enables the DP pull-up on every power-ready event, silently undoing an earlier Detach. Guard the pull-up write with a detached flag so the device stays off the bus until Attach is called. --- src/machine/machine_nrf52840_usb.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/machine/machine_nrf52840_usb.go b/src/machine/machine_nrf52840_usb.go index 263d120ff9..90904d0dcb 100644 --- a/src/machine/machine_nrf52840_usb.go +++ b/src/machine/machine_nrf52840_usb.go @@ -22,6 +22,11 @@ var ( epinen uint32 epouten uint32 easyDMABusy volatile.Register8 + + // usbDetached keeps the device detached from the bus after Detach: the + // USB IRQ handler re-enables the DP pull-up on every power-ready event, + // which would otherwise silently undo a Detach. + usbDetached bool ) // enterCriticalSection is used to protect access to easyDMA - only one thing @@ -94,6 +99,7 @@ func (dev *USBDevice) Configure(config UARTConfig) { // Detach to delay enumeration until the USB configuration (device // identifiers, classes, ...) is complete. func (dev *USBDevice) Attach() { + usbDetached = false nrf.USBD.USBPULLUP.Set(1) } @@ -101,6 +107,7 @@ func (dev *USBDevice) Attach() { // To the host this appears as if the device was unplugged. A subsequent // Attach makes the host enumerate the device again. func (dev *USBDevice) Detach() { + usbDetached = true nrf.USBD.USBPULLUP.Set(0) } @@ -118,7 +125,9 @@ func handleUSBIRQ(interrupt.Interrupt) { // Configure control endpoint initEndpoint(0, usb.ENDPOINT_TYPE_CONTROL) - nrf.USBD.USBPULLUP.Set(1) + if !usbDetached { + nrf.USBD.USBPULLUP.Set(1) + } usbConfiguration = 0 } From 858a56fa9fac5264351d1064d13c8b722abcb932 Mon Sep 17 00:00:00 2001 From: sago35 Date: Thu, 20 Aug 2026 22:18:58 +0900 Subject: [PATCH 3/4] machine: add USB Attach and Detach to stm32 and esp32 targets - stm32f4, stm32f7, stm32h7: implement Attach and Detach using the DCTL soft-disconnect bit that Configure already toggles. - esp32c3, esp32c6, esp32s3: add no-op stubs to keep user code portable; the fixed-function USB Serial/JTAG controller has no software-controlled soft-connect. --- src/machine/machine_esp32c3_usb.go | 5 +++++ src/machine/machine_esp32c6_usb.go | 5 +++++ src/machine/machine_esp32xx_usb.go | 5 +++++ src/machine/machine_stm32_otgfs_usb.go | 15 +++++++++++++++ src/machine/machine_stm32h7_usb.go | 15 +++++++++++++++ 5 files changed, 45 insertions(+) diff --git a/src/machine/machine_esp32c3_usb.go b/src/machine/machine_esp32c3_usb.go index 5d58c35ee2..4af895a52a 100644 --- a/src/machine/machine_esp32c3_usb.go +++ b/src/machine/machine_esp32c3_usb.go @@ -67,6 +67,11 @@ func (dev *USBDevice) SetStallEPOut(ep uint32) {} func (dev *USBDevice) ClearStallEPIn(ep uint32) {} func (dev *USBDevice) ClearStallEPOut(ep uint32) {} +// Attach and Detach are no-ops: the USB Serial/JTAG controller has no +// software-controlled soft-connect, it is always attached to the bus. +func (dev *USBDevice) Attach() {} +func (dev *USBDevice) Detach() {} + // initUSB is intentionally empty — the interp phase evaluates init() // functions at compile time and cannot access hardware registers. // Actual hardware setup is deferred to the first Configure() call. diff --git a/src/machine/machine_esp32c6_usb.go b/src/machine/machine_esp32c6_usb.go index aa9bc988bd..f05d54c5ae 100644 --- a/src/machine/machine_esp32c6_usb.go +++ b/src/machine/machine_esp32c6_usb.go @@ -66,6 +66,11 @@ func (dev *USBDevice) SetStallEPOut(ep uint32) {} func (dev *USBDevice) ClearStallEPIn(ep uint32) {} func (dev *USBDevice) ClearStallEPOut(ep uint32) {} +// Attach and Detach are no-ops: the USB Serial/JTAG controller has no +// software-controlled soft-connect, it is always attached to the bus. +func (dev *USBDevice) Attach() {} +func (dev *USBDevice) Detach() {} + // initUSB is intentionally empty — the interp phase evaluates init() // functions at compile time and cannot access hardware registers. // Actual hardware setup is deferred to the first Configure() call. diff --git a/src/machine/machine_esp32xx_usb.go b/src/machine/machine_esp32xx_usb.go index e9d7275d9e..7679f436cb 100644 --- a/src/machine/machine_esp32xx_usb.go +++ b/src/machine/machine_esp32xx_usb.go @@ -67,6 +67,11 @@ func (dev *USBDevice) SetStallEPOut(ep uint32) {} func (dev *USBDevice) ClearStallEPIn(ep uint32) {} func (dev *USBDevice) ClearStallEPOut(ep uint32) {} +// Attach and Detach are no-ops: the USB Serial/JTAG controller has no +// software-controlled soft-connect, it is always attached to the bus. +func (dev *USBDevice) Attach() {} +func (dev *USBDevice) Detach() {} + // initUSB is intentionally empty — the interp phase evaluates init() // functions at compile time and cannot access hardware registers. // Actual hardware setup is deferred to the first Configure() call. diff --git a/src/machine/machine_stm32_otgfs_usb.go b/src/machine/machine_stm32_otgfs_usb.go index 47f70936d5..28c9f83e7f 100644 --- a/src/machine/machine_stm32_otgfs_usb.go +++ b/src/machine/machine_stm32_otgfs_usb.go @@ -316,6 +316,21 @@ func (dev *USBDevice) Configure(config UARTConfig) { dev.initcomplete = true } +// Attach connects the device to the USB bus by releasing soft disconnect, +// allowing the host to detect and enumerate it. It can be used together with +// Detach to delay enumeration until the USB configuration (device +// identifiers, classes, ...) is complete. +func (dev *USBDevice) Attach() { + otgDevice.DCTL.ClearBits(dctlSDIS) +} + +// Detach disconnects the device from the USB bus by asserting soft +// disconnect. To the host this appears as if the device was unplugged. A +// subsequent Attach makes the host enumerate the device again. +func (dev *USBDevice) Detach() { + otgDevice.DCTL.SetBits(dctlSDIS) +} + // handleUSBIRQ is the OTG FS interrupt handler, dispatching on GINTSTS bits. func handleUSBIRQ(intr interrupt.Interrupt) { status := stm32.OTG_FS_GLOBAL.GINTSTS.Get() & diff --git a/src/machine/machine_stm32h7_usb.go b/src/machine/machine_stm32h7_usb.go index 7673819b01..044d3b3c3f 100644 --- a/src/machine/machine_stm32h7_usb.go +++ b/src/machine/machine_stm32h7_usb.go @@ -268,6 +268,21 @@ func (dev *USBDevice) Configure(config UARTConfig) { usbOTG.DCTL.ClearBits(DCTL_SDIS) } +// Attach connects the device to the USB bus by releasing soft disconnect, +// allowing the host to detect and enumerate it. It can be used together with +// Detach to delay enumeration until the USB configuration (device +// identifiers, classes, ...) is complete. +func (dev *USBDevice) Attach() { + usbOTG.DCTL.ClearBits(DCTL_SDIS) +} + +// Detach disconnects the device from the USB bus by asserting soft +// disconnect. To the host this appears as if the device was unplugged. A +// subsequent Attach makes the host enumerate the device again. +func (dev *USBDevice) Detach() { + usbOTG.DCTL.SetBits(DCTL_SDIS) +} + func initEndpoint(ep, config uint32) { if ep == 0 { // Control endpoint From e23373efa09ad16b328c204eb4d7fafa3b8d61af Mon Sep 17 00:00:00 2001 From: sago35 Date: Thu, 20 Aug 2026 22:13:35 +0900 Subject: [PATCH 4/4] machine: use Attach and Detach in USB Configure Replace the direct soft-connect register writes in Configure with the equivalent Attach and Detach calls on atsamd21, atsamd51, rp2040, rp2350, stm32f4, stm32f7 and stm32h7. --- src/machine/machine_atsamd21_usb.go | 2 +- src/machine/machine_atsamd51_usb.go | 2 +- src/machine/machine_rp2040_usb.go | 2 +- src/machine/machine_rp2350_usb.go | 2 +- src/machine/machine_stm32_otgfs_usb.go | 4 ++-- src/machine/machine_stm32h7_usb.go | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/machine/machine_atsamd21_usb.go b/src/machine/machine_atsamd21_usb.go index 51f71f9d4f..45b6ba8241 100644 --- a/src/machine/machine_atsamd21_usb.go +++ b/src/machine/machine_atsamd21_usb.go @@ -51,7 +51,7 @@ func (dev *USBDevice) Configure(config UARTConfig) { sam.USB_DEVICE.CTRLB.SetBits(sam.USB_DEVICE_CTRLB_SPDCONF_FS << sam.USB_DEVICE_CTRLB_SPDCONF_Pos) // attach - sam.USB_DEVICE.CTRLB.ClearBits(sam.USB_DEVICE_CTRLB_DETACH) + dev.Attach() // enable interrupt for end of reset sam.USB_DEVICE.INTENSET.SetBits(sam.USB_DEVICE_INTENSET_EORST) diff --git a/src/machine/machine_atsamd51_usb.go b/src/machine/machine_atsamd51_usb.go index 925cfef5ea..d10755db9c 100644 --- a/src/machine/machine_atsamd51_usb.go +++ b/src/machine/machine_atsamd51_usb.go @@ -51,7 +51,7 @@ func (dev *USBDevice) Configure(config UARTConfig) { sam.USB_DEVICE.CTRLB.SetBits(sam.USB_DEVICE_CTRLB_SPDCONF_FS << sam.USB_DEVICE_CTRLB_SPDCONF_Pos) // attach - sam.USB_DEVICE.CTRLB.ClearBits(sam.USB_DEVICE_CTRLB_DETACH) + dev.Attach() // enable interrupt for end of reset sam.USB_DEVICE.INTENSET.SetBits(sam.USB_DEVICE_INTENSET_EORST) diff --git a/src/machine/machine_rp2040_usb.go b/src/machine/machine_rp2040_usb.go index fb3c4c3a42..961f9bbd2f 100644 --- a/src/machine/machine_rp2040_usb.go +++ b/src/machine/machine_rp2040_usb.go @@ -43,7 +43,7 @@ func (dev *USBDevice) Configure(config UARTConfig) { rp.USBCTRL_REGS_INTE_SETUP_REQ) // Present full speed device by enabling pull up on DP - rp.USBCTRL_REGS.SIE_CTRL.SetBits(rp.USBCTRL_REGS_SIE_CTRL_PULLUP_EN) + dev.Attach() } // Attach connects the device to the USB bus by enabling the DP pull-up, diff --git a/src/machine/machine_rp2350_usb.go b/src/machine/machine_rp2350_usb.go index 3d35f2d122..1c4d1588ad 100644 --- a/src/machine/machine_rp2350_usb.go +++ b/src/machine/machine_rp2350_usb.go @@ -43,7 +43,7 @@ func (dev *USBDevice) Configure(config UARTConfig) { rp.USB_INTE_SETUP_REQ) // Present full speed device by enabling pull up on DP - rp.USB.SIE_CTRL.SetBits(rp.USB_SIE_CTRL_PULLUP_EN) + dev.Attach() // 12.7.2 Disable phy isolation rp.USB.SetMAIN_CTRL_PHY_ISO(0x0) diff --git a/src/machine/machine_stm32_otgfs_usb.go b/src/machine/machine_stm32_otgfs_usb.go index 28c9f83e7f..639a301ee7 100644 --- a/src/machine/machine_stm32_otgfs_usb.go +++ b/src/machine/machine_stm32_otgfs_usb.go @@ -254,7 +254,7 @@ func (dev *USBDevice) Configure(config UARTConfig) { otgPower.PCGCCTL.Set(0) // Soft-disconnect now (after CSRST reset DCTL to its default connected state). - otgDevice.DCTL.SetBits(dctlSDIS) + dev.Detach() // ---- 7. Configure data FIFOs -------------------------------------------- @@ -311,7 +311,7 @@ func (dev *USBDevice) Configure(config UARTConfig) { // ---- 12. Connect to host (clear soft-disconnect) ----------------------- - otgDevice.DCTL.ClearBits(dctlSDIS) + dev.Attach() dev.initcomplete = true } diff --git a/src/machine/machine_stm32h7_usb.go b/src/machine/machine_stm32h7_usb.go index 044d3b3c3f..e895600294 100644 --- a/src/machine/machine_stm32h7_usb.go +++ b/src/machine/machine_stm32h7_usb.go @@ -206,7 +206,7 @@ func (dev *USBDevice) Configure(config UARTConfig) { // Stay soft-disconnected until configuration is complete; CSRST left // DCTL at its default "connected" state. - usbOTG.DCTL.SetBits(DCTL_SDIS) + dev.Detach() // 5. Force device mode now that the core is out of reset. The mode // change takes effect only after up to 25 ms (RM0433); poll GINTSTS.CMOD @@ -265,7 +265,7 @@ func (dev *USBDevice) Configure(config UARTConfig) { dev.initcomplete = true // Release soft-disconnect: pulls D+ high, making device visible to host. - usbOTG.DCTL.ClearBits(DCTL_SDIS) + dev.Attach() } // Attach connects the device to the USB bus by releasing soft disconnect,