Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 241 additions & 0 deletions src/machine/machine_esp32c3_usb.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion src/machine/machine_esp32xx_usb.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build esp32s3 || esp32c3
//go:build esp32s3

package machine

Expand Down
Loading