mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-05 19:43:44 +00:00
0e84235f9a
When no USB host is reading, flushAndWait() spins 50K iterations per FIFO-full event. With putchar calling WriteByte per byte, the cumulative delay starves I2C and other peripherals, freezing displays. Add a txStalled flag: the first FIFO-full triggers one flushAndWait attempt. If it fails (no host), txStalled is set and all subsequent writes return immediately with no spin — just a register read and a bool check. When a host reconnects, SERIAL_IN_EP_DATA_FREE goes back to 1, bypassing the stall path and clearing the flag automatically.
286 lines
8.2 KiB
Go
286 lines
8.2 KiB
Go
//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 = 10
|
|
|
|
type USB_DEVICE struct {
|
|
Bus *esp.USB_DEVICE_Type
|
|
Buffer *RingBuffer
|
|
txPending bool // unflushed data in the EP1 TX FIFO
|
|
txStalled bool // set when flushAndWait fails (no host reading); cleared when FIFO becomes writable
|
|
}
|
|
|
|
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 locked by a pending USB transfer.
|
|
if usbdev.txStalled {
|
|
// Previously failed — skip the expensive spin and drop
|
|
// the byte. When a host reconnects SERIAL_IN_EP_DATA_FREE
|
|
// goes back to 1, clearing the stall on the next call.
|
|
return errUSBCouldNotWriteAllData
|
|
}
|
|
// First time the FIFO is full: wait briefly for the host to
|
|
// read the previous packet.
|
|
if !usbdev.flushAndWait() {
|
|
usbdev.txStalled = true
|
|
return errUSBCouldNotWriteAllData
|
|
}
|
|
}
|
|
usbdev.txStalled = false
|
|
|
|
// 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))
|
|
|
|
// Only signal WR_DONE on newline to batch bytes into a single USB
|
|
// packet. The FIFO-full path above also flushes when the 64-byte
|
|
// FIFO fills up.
|
|
if c == '\n' {
|
|
usbdev.flush()
|
|
usbdev.txPending = false
|
|
} else {
|
|
usbdev.txPending = true
|
|
}
|
|
|
|
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 usbdev.txStalled {
|
|
return i, errUSBCouldNotWriteAllData
|
|
}
|
|
if !usbdev.flushAndWait() {
|
|
usbdev.txStalled = true
|
|
return i, errUSBCouldNotWriteAllData
|
|
}
|
|
}
|
|
usbdev.txStalled = false
|
|
usbdev.Bus.EP1.Set(uint32(c))
|
|
}
|
|
|
|
usbdev.flush()
|
|
usbdev.txPending = false
|
|
return len(data), nil
|
|
}
|
|
|
|
// Buffered returns the number of bytes waiting in the receive ring buffer.
|
|
func (usbdev *USB_DEVICE) Buffered() int {
|
|
usbdev.ensureConfigured()
|
|
// Flush any pending TX data so callers like echo loops don't
|
|
// need to explicitly flush after WriteByte.
|
|
if usbdev.txPending {
|
|
usbdev.flush()
|
|
usbdev.txPending = false
|
|
}
|
|
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 to tell the hardware to send the data that has
|
|
// been written to the EP1 FIFO. Returns immediately without waiting.
|
|
func (usbdev *USB_DEVICE) flush() {
|
|
usbdev.Bus.SetEP1_CONF_WR_DONE(1)
|
|
}
|
|
|
|
// FlushSerial flushes any pending USB serial TX data. Called from the
|
|
// runtime (e.g. before sleeping) to ensure data from print() without
|
|
// a trailing newline gets sent promptly.
|
|
func FlushSerial() {
|
|
if _USBCDC.txPending {
|
|
_USBCDC.flush()
|
|
_USBCDC.txPending = false
|
|
}
|
|
}
|
|
|
|
// flushAndWait signals WR_DONE and waits for the EP1 FIFO to become
|
|
// writable again. The timeout covers a few USB frames so that data gets
|
|
// through when a host is connected. Returns false if the FIFO is still
|
|
// locked after the timeout (no host reading).
|
|
func (usbdev *USB_DEVICE) flushAndWait() bool {
|
|
usbdev.Bus.SetEP1_CONF_WR_DONE(1)
|
|
for i := 0; i < 50000; i++ {
|
|
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() != 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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
|
|
}
|