From 71245845c8466b6f1c03d0ae6b5d586f59a8163a Mon Sep 17 00:00:00 2001 From: deadprogram Date: Thu, 5 Mar 2026 20:22:12 +0100 Subject: [PATCH] esp32c3: implement USBDevice using interrupts for more proper operation. This improves the ESP32-C3 USBDevice implementation by using interrupts to properly handle data RX/TX. It also stubs out USB interfaces for HID functions, since those cannot be implemented on ESP32-C3 due to using a JTAG-USB interface. Signed-off-by: deadprogram --- src/machine/machine_esp32c3_usb.go | 241 +++++++++++++++++++++++++++++ src/machine/machine_esp32xx_usb.go | 2 +- 2 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 src/machine/machine_esp32c3_usb.go diff --git a/src/machine/machine_esp32c3_usb.go b/src/machine/machine_esp32c3_usb.go new file mode 100644 index 000000000..06fdca913 --- /dev/null +++ b/src/machine/machine_esp32c3_usb.go @@ -0,0 +1,241 @@ +//go:build esp32c3 + +package machine + +import ( + "device/esp" + "errors" + "machine/usb" + "machine/usb/descriptor" + "runtime/interrupt" +) + +// USB Serial/JTAG Controller +// See esp32-c3_technical_reference_manual_en.pdf pg. 736 +// +// The ESP32-C3 has a built-in USB Serial/JTAG controller that provides a +// CDC-ACM serial port. The USB protocol and enumeration are handled entirely +// in hardware; software only reads/writes the EP1 FIFO. + +const cpuInterruptFromUSB = 8 + +// flushTimeout is the maximum number of busy-wait iterations in flush(). +// Prevents hanging when no USB host is connected. +const flushTimeout = 200000 + +type USB_DEVICE struct { + Bus *esp.USB_DEVICE_Type + Buffer *RingBuffer +} + +var ( + _USBCDC = &USB_DEVICE{ + Bus: esp.USB_DEVICE, + Buffer: NewRingBuffer(), + } + + USBCDC Serialer = _USBCDC +) + +var ( + errUSBWrongSize = errors.New("USB: invalid write size") + errUSBCouldNotWriteAllData = errors.New("USB: could not write all data") +) + +type Serialer interface { + WriteByte(c byte) error + Write(data []byte) (n int, err error) + Configure(config UARTConfig) error + Buffered() int + ReadByte() (byte, error) + DTR() bool + RTS() bool +} + +var usbConfigured bool + +// USBDevice provides a stub USB device for the ESP32-C3. The hardware +// only supports a fixed-function CDC-ACM serial port, so the programmable +// USB device features are no-ops. +type USBDevice struct { + initcomplete bool + InitEndpointComplete bool +} + +var USBDev = &USBDevice{} + +func (dev *USBDevice) SetStallEPIn(ep uint32) {} +func (dev *USBDevice) SetStallEPOut(ep uint32) {} +func (dev *USBDevice) ClearStallEPIn(ep uint32) {} +func (dev *USBDevice) ClearStallEPOut(ep uint32) {} + +// 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. +func initUSB() {} + +// Configure initialises the USB Serial/JTAG controller clock, pads, and +// interrupt so that received data is buffered automatically. +func (usbdev *USB_DEVICE) Configure(config UARTConfig) error { + if usbConfigured { + return nil + } + usbConfigured = true + + // Enable the USB_DEVICE peripheral clock. + // Do NOT reset the peripheral — the ROM bootloader has already + // configured the USB Serial/JTAG controller and the host may + // already be connected. Resetting would drop the USB link. + esp.SYSTEM.SetPERIP_CLK_EN0_USB_DEVICE_CLK_EN(1) + esp.SYSTEM.SetPERIP_RST_EN0_USB_DEVICE_RST(0) + + // Ensure internal PHY is selected and USB pads are enabled. + usbdev.Bus.SetCONF0_PHY_SEL(0) + usbdev.Bus.SetCONF0_USB_PAD_ENABLE(1) + usbdev.Bus.SetCONF0_DP_PULLUP(1) + + // Clear any pending interrupts. + usbdev.Bus.INT_CLR.Set(0xFFFFFFFF) + + // Enable the RX-packet-received interrupt. + usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(1) + + // Map the USB peripheral interrupt to CPU interrupt cpuInterruptFromUSB. + esp.INTERRUPT_CORE0.SetUSB_INTR_MAP(cpuInterruptFromUSB) + + _ = interrupt.New(cpuInterruptFromUSB, func(interrupt.Interrupt) { + _USBCDC.handleInterrupt() + }).Enable() + + return nil +} + +// ensureConfigured triggers lazy initialization on first use. +func (usbdev *USB_DEVICE) ensureConfigured() { + if !usbConfigured { + usbdev.Configure(UARTConfig{}) + } +} + +// handleInterrupt drains the hardware RX FIFO into the software ring buffer. +func (usbdev *USB_DEVICE) handleInterrupt() { + // Read INT_ST while INT_ENA is still set (INT_ST = INT_RAW & INT_ENA). + intStatus := usbdev.Bus.INT_ST.Get() + + // Disable the RX interrupt to prevent re-triggering while we drain. + usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(0) + + if intStatus&esp.USB_DEVICE_INT_ST_SERIAL_OUT_RECV_PKT_INT_ST != 0 { + // Drain all available bytes from the EP1 OUT FIFO. + // Use EP1.Get() directly — the generated GetEP1_RDWR_BYTE is + // functionally identical, but a direct load makes the FIFO-pop + // intent explicit. + for usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL() != 0 { + b := byte(usbdev.Bus.EP1.Get()) + usbdev.Buffer.Put(b) + } + // Clear the interrupt. + usbdev.Bus.SetINT_CLR_SERIAL_OUT_RECV_PKT_INT_CLR(1) + } + + // Re-enable the RX interrupt. + usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(1) +} + +func (usbdev *USB_DEVICE) WriteByte(c byte) error { + usbdev.ensureConfigured() + if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { + // FIFO full — try flushing first, then recheck. + usbdev.flush() + if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { + return errUSBCouldNotWriteAllData + } + } + + // Use EP1.Set() (direct store) instead of SetEP1_RDWR_BYTE which + // does a read-modify-write — the read side-effect pops a byte from + // the RX FIFO. + usbdev.Bus.EP1.Set(uint32(c)) + usbdev.flush() + + return nil +} + +func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) { + usbdev.ensureConfigured() + if len(data) == 0 { + return 0, nil + } + + for i, c := range data { + if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { + if i > 0 { + usbdev.flush() + } + if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 { + return i, errUSBCouldNotWriteAllData + } + } + usbdev.Bus.EP1.Set(uint32(c)) + } + + usbdev.flush() + return len(data), nil +} + +// Buffered returns the number of bytes waiting in the receive ring buffer. +func (usbdev *USB_DEVICE) Buffered() int { + usbdev.ensureConfigured() + return int(usbdev.Buffer.Used()) +} + +// ReadByte returns a byte from the receive ring buffer. +func (usbdev *USB_DEVICE) ReadByte() (byte, error) { + b, ok := usbdev.Buffer.Get() + if !ok { + return 0, nil + } + return b, nil +} + +func (usbdev *USB_DEVICE) DTR() bool { + return false +} + +func (usbdev *USB_DEVICE) RTS() bool { + return false +} + +// flush signals WR_DONE and waits (with timeout) for the hardware to +// consume the data. A timeout prevents hanging when no USB host is present. +func (usbdev *USB_DEVICE) flush() { + usbdev.Bus.SetEP1_CONF_WR_DONE(1) + for i := 0; i < flushTimeout; i++ { + if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() != 0 { + return + } + } +} + +// The ESP32-C3 USB Serial/JTAG controller is fixed-function hardware. +// It only provides a CDC-ACM serial port; the USB protocol and endpoint +// configuration are handled entirely in silicon. The functions below +// are no-op stubs so that higher-level USB packages (HID, MIDI, …) +// compile, but they cannot add real endpoints on this hardware. + +// ConfigureUSBEndpoint is a no-op on ESP32-C3 — the hardware does not +// support programmable USB endpoints. +func ConfigureUSBEndpoint(desc descriptor.Descriptor, epSettings []usb.EndpointConfig, setup []usb.SetupConfig) { +} + +// SendZlp is a no-op on ESP32-C3 — the hardware handles control +// transfers internally. +func SendZlp() { +} + +// SendUSBInPacket is a no-op on ESP32-C3 — the hardware does not +// support arbitrary IN endpoints. Returns false to indicate the +// packet was not sent. +func SendUSBInPacket(ep uint32, data []byte) bool { + return false +} diff --git a/src/machine/machine_esp32xx_usb.go b/src/machine/machine_esp32xx_usb.go index bdd1281a8..a2e79dc0c 100644 --- a/src/machine/machine_esp32xx_usb.go +++ b/src/machine/machine_esp32xx_usb.go @@ -1,4 +1,4 @@ -//go:build esp32s3 || esp32c3 +//go:build esp32s3 package machine