Skip to content
Draft
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
160 changes: 142 additions & 18 deletions src/machine/machine_esp32xx_usb.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,33 @@ 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
// See esp32-s3_technical_reference_manual_en.pdf
//
// The ESP32-S3 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
Bus *esp.USB_DEVICE_Type
Buffer *RingBuffer
}

var (
_USBCDC = &USB_DEVICE{
Bus: esp.USB_DEVICE,
Bus: esp.USB_DEVICE,
Buffer: NewRingBuffer(),
}

USBCDC Serialer = _USBCDC
Expand All @@ -25,7 +40,6 @@ var (
var (
errUSBWrongSize = errors.New("USB: invalid write size")
errUSBCouldNotWriteAllData = errors.New("USB: could not write all data")
errUSBBufferEmpty = errors.New("USB: read buffer empty")
)

type Serialer interface {
Expand All @@ -38,53 +52,139 @@ type Serialer interface {
RTS() bool
}

var usbConfigured bool

// USBDevice provides a stub USB device for the ESP32-S3. 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() {}

// usbHandleInterrupt is the top-level interrupt handler passed to
// interrupt.New. It must be a plain function (not a closure) because
// interrupt.New is a compiler intrinsic that does not support closures.
func usbHandleInterrupt(interrupt.Interrupt) {
_USBCDC.handleInterrupt()
}

// Configure initialises the USB Serial/JTAG controller.
func (usbdev *USB_DEVICE) Configure(config UARTConfig) error {
if usbConfigured {
return nil
}
usbConfigured = true

// Enable the USB_DEVICE peripheral clock.
esp.SYSTEM.SetPERIP_CLK_EN1_USB_DEVICE_CLK_EN(1)
esp.SYSTEM.SetPERIP_RST_EN1_USB_DEVICE_RST(0)

// Clear any pending interrupts, then enable the RX interrupt.
usbdev.Bus.INT_CLR.Set(0xFFFFFFFF)
usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(1)

// Map the USB_DEVICE peripheral interrupt to CPU interrupt line.
esp.INTERRUPT_CORE0.SetUSB_DEVICE_INT_MAP(cpuInterruptFromUSB)

_ = interrupt.New(cpuInterruptFromUSB, usbHandleInterrupt).Enable()

return nil
}

// ensureConfigured triggers lazy initialization on first use.
func (usbdev *USB_DEVICE) ensureConfigured() {
if !usbConfigured {
usbdev.Configure(UARTConfig{})
}
}

// handleInterrupt is called from the CPU interrupt vector when the USB
// peripheral raises an interrupt. Disable INT_ENA to prevent the
// level-triggered interrupt from re-asserting immediately (data may
// still be in the FIFO). Buffered() re-enables after draining.
func (usbdev *USB_DEVICE) handleInterrupt() {
usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(0)
usbdev.Bus.SetINT_CLR_SERIAL_OUT_RECV_PKT_INT_CLR(1)
}

func (usbdev *USB_DEVICE) WriteByte(c byte) error {
usbdev.ensureConfigured()
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
return errUSBCouldNotWriteAllData
// FIFO full — try flushing first, then recheck.
usbdev.flush()
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
return errUSBCouldNotWriteAllData
}
}

usbdev.Bus.SetEP1_RDWR_BYTE(uint32(c))
// 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) {
if len(data) == 0 || len(data) > 64 {
return 0, errUSBWrongSize
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()
}

return i, errUSBCouldNotWriteAllData
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
return i, errUSBCouldNotWriteAllData
}
}
usbdev.Bus.SetEP1_RDWR_BYTE(uint32(c))
usbdev.Bus.EP1.Set(uint32(c))
}

usbdev.flush()
return len(data), nil
}

// Buffered returns the number of bytes waiting in the receive ring buffer.
// It drains any data sitting in the hardware FIFO and re-enables the
// peripheral-level USB interrupt (which the ISR disables to prevent a
// level-triggered interrupt storm).
func (usbdev *USB_DEVICE) Buffered() int {
return int(usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL())
usbdev.ensureConfigured()
// Drain the hardware FIFO into the ring buffer.
for usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL() != 0 {
b := byte(usbdev.Bus.EP1.Get())
usbdev.Buffer.Put(b)
}
// Clear pending flags and re-enable the RX interrupt at the peripheral level.
usbdev.Bus.INT_CLR.Set(0xFFFFFFFF)
usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(1)
return int(usbdev.Buffer.Used())
}

// ReadByte returns a byte from the receive ring buffer.
func (usbdev *USB_DEVICE) ReadByte() (byte, error) {
if usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL() != 0 {
return byte(usbdev.Bus.GetEP1_RDWR_BYTE()), nil
b, ok := usbdev.Buffer.Get()
if !ok {
return 0, nil
}

return 0, nil
return b, nil
}

func (usbdev *USB_DEVICE) DTR() bool {
Expand All @@ -95,8 +195,32 @@ 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 usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
for i := 0; i < flushTimeout; i++ {
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() != 0 {
return
}
}
}

// The ESP32-S3 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-S3.
func ConfigureUSBEndpoint(desc descriptor.Descriptor, epSettings []usb.EndpointConfig, setup []usb.SetupConfig) {
}

// SendZlp is a no-op on ESP32-S3.
func SendZlp() {
}

// SendUSBInPacket is a no-op on ESP32-S3.
func SendUSBInPacket(ep uint32, data []byte) bool {
return false
}
Loading
Loading