mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-16 18:53:29 +00:00
esp32s3: switch USB implementation to use interrupts instead of polling
The previous implementation for USB was using polling instead of using interrupts. This changes that. Signed-off-by: deadprogram <ron@hybridgroup.com>
This commit is contained in:
@@ -5,18 +5,33 @@ package machine
|
|||||||
import (
|
import (
|
||||||
"device/esp"
|
"device/esp"
|
||||||
"errors"
|
"errors"
|
||||||
|
"machine/usb"
|
||||||
|
"machine/usb/descriptor"
|
||||||
|
"runtime/interrupt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// USB Serial/JTAG Controller
|
// USB Serial/JTAG Controller
|
||||||
// See esp32-c3_technical_reference_manual_en.pdf
|
// See esp32-s3_technical_reference_manual_en.pdf
|
||||||
// pg. 736
|
//
|
||||||
|
// 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 {
|
type USB_DEVICE struct {
|
||||||
Bus *esp.USB_DEVICE_Type
|
Bus *esp.USB_DEVICE_Type
|
||||||
|
Buffer *RingBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_USBCDC = &USB_DEVICE{
|
_USBCDC = &USB_DEVICE{
|
||||||
Bus: esp.USB_DEVICE,
|
Bus: esp.USB_DEVICE,
|
||||||
|
Buffer: NewRingBuffer(),
|
||||||
}
|
}
|
||||||
|
|
||||||
USBCDC Serialer = _USBCDC
|
USBCDC Serialer = _USBCDC
|
||||||
@@ -25,7 +40,6 @@ var (
|
|||||||
var (
|
var (
|
||||||
errUSBWrongSize = errors.New("USB: invalid write size")
|
errUSBWrongSize = errors.New("USB: invalid write size")
|
||||||
errUSBCouldNotWriteAllData = errors.New("USB: could not write all data")
|
errUSBCouldNotWriteAllData = errors.New("USB: could not write all data")
|
||||||
errUSBBufferEmpty = errors.New("USB: read buffer empty")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Serialer interface {
|
type Serialer interface {
|
||||||
@@ -38,26 +52,96 @@ type Serialer interface {
|
|||||||
RTS() bool
|
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() {}
|
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 {
|
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
|
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. For now, just clear the interrupt flag.
|
||||||
|
// The actual data drain happens in Buffered() via polling — once the ISR
|
||||||
|
// mechanism is proven, we can move the drain here.
|
||||||
|
func (usbdev *USB_DEVICE) handleInterrupt() {
|
||||||
|
usbdev.Bus.SetINT_CLR_SERIAL_OUT_RECV_PKT_INT_CLR(1)
|
||||||
|
}
|
||||||
|
|
||||||
func (usbdev *USB_DEVICE) WriteByte(c byte) error {
|
func (usbdev *USB_DEVICE) WriteByte(c byte) error {
|
||||||
|
usbdev.ensureConfigured()
|
||||||
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
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()
|
usbdev.flush()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) {
|
func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) {
|
||||||
if len(data) == 0 || len(data) > 64 {
|
usbdev.ensureConfigured()
|
||||||
return 0, errUSBWrongSize
|
if len(data) == 0 {
|
||||||
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, c := range data {
|
for i, c := range data {
|
||||||
@@ -65,26 +149,43 @@ func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) {
|
|||||||
if i > 0 {
|
if i > 0 {
|
||||||
usbdev.flush()
|
usbdev.flush()
|
||||||
}
|
}
|
||||||
|
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
||||||
return i, errUSBCouldNotWriteAllData
|
return i, errUSBCouldNotWriteAllData
|
||||||
|
}
|
||||||
}
|
}
|
||||||
usbdev.Bus.SetEP1_RDWR_BYTE(uint32(c))
|
usbdev.Bus.EP1.Set(uint32(c))
|
||||||
}
|
}
|
||||||
|
|
||||||
usbdev.flush()
|
usbdev.flush()
|
||||||
return len(data), nil
|
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
|
||||||
|
// USB interrupt (which the ISR disables via INTENABLE to prevent a
|
||||||
|
// level-triggered interrupt storm).
|
||||||
func (usbdev *USB_DEVICE) Buffered() int {
|
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.
|
||||||
|
usbdev.Bus.INT_CLR.Set(0xFFFFFFFF)
|
||||||
|
usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(1)
|
||||||
|
// Re-enable CPU interrupt 8 in INTENABLE (the ISR clears all bits).
|
||||||
|
interrupt.New(cpuInterruptFromUSB, usbHandleInterrupt).Enable()
|
||||||
|
return int(usbdev.Buffer.Used())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadByte returns a byte from the receive ring buffer.
|
||||||
func (usbdev *USB_DEVICE) ReadByte() (byte, error) {
|
func (usbdev *USB_DEVICE) ReadByte() (byte, error) {
|
||||||
if usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL() != 0 {
|
b, ok := usbdev.Buffer.Get()
|
||||||
return byte(usbdev.Bus.GetEP1_RDWR_BYTE()), nil
|
if !ok {
|
||||||
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
return b, nil
|
||||||
return 0, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (usbdev *USB_DEVICE) DTR() bool {
|
func (usbdev *USB_DEVICE) DTR() bool {
|
||||||
@@ -95,8 +196,32 @@ func (usbdev *USB_DEVICE) RTS() bool {
|
|||||||
return false
|
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() {
|
func (usbdev *USB_DEVICE) flush() {
|
||||||
usbdev.Bus.SetEP1_CONF_WR_DONE(1)
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user