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 <ron@hybridgroup.com>
This commit is contained in:
deadprogram
2026-03-05 20:22:12 +01:00
parent d9f6f3b81f
commit 71245845c8
2 changed files with 242 additions and 1 deletions
+241
View File
@@ -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
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build esp32s3 || esp32c3
//go:build esp32s3
package machine