mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-07-26 06:38:42 +00:00
machine/esp32s3,esp32c3: add txStalled flag to skip USB serial spin when no host
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.
This commit is contained in:
@@ -19,15 +19,11 @@ import (
|
||||
|
||||
const cpuInterruptFromUSB = 10
|
||||
|
||||
// flushTimeout is the maximum number of busy-wait iterations in flush().
|
||||
// Must be long enough for 2-3 USB frames (~3ms at 160MHz) so data gets
|
||||
// through when a host is connected, but short enough that println doesn't
|
||||
// freeze the application when no host is reading.
|
||||
const flushTimeout = 50000
|
||||
|
||||
type USB_DEVICE struct {
|
||||
Bus *esp.USB_DEVICE_Type
|
||||
Buffer *RingBuffer
|
||||
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 (
|
||||
@@ -147,19 +143,36 @@ func (usbdev *USB_DEVICE) handleInterrupt() {
|
||||
func (usbdev *USB_DEVICE) WriteByte(c byte) error {
|
||||
usbdev.ensureConfigured()
|
||||
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
||||
// FIFO not writable — try a short flush to nudge the hardware
|
||||
// (e.g. after reset the FIFO may need WR_DONE to transition).
|
||||
usbdev.flush()
|
||||
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))
|
||||
usbdev.flush()
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -172,23 +185,32 @@ func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) {
|
||||
|
||||
for i, c := range data {
|
||||
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
||||
if i > 0 {
|
||||
usbdev.flush()
|
||||
if usbdev.txStalled {
|
||||
return i, errUSBCouldNotWriteAllData
|
||||
}
|
||||
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -209,16 +231,34 @@ func (usbdev *USB_DEVICE) RTS() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// flush signals WR_DONE and briefly waits for the hardware to accept more
|
||||
// data. The timeout is intentionally short so that serial output never
|
||||
// stalls the application when no USB host is reading.
|
||||
// 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)
|
||||
for i := 0; i < flushTimeout; i++ {
|
||||
}
|
||||
|
||||
// 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
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// The ESP32-C3 USB Serial/JTAG controller is fixed-function hardware.
|
||||
|
||||
@@ -19,15 +19,11 @@ import (
|
||||
|
||||
const cpuInterruptFromUSB = 8
|
||||
|
||||
// flushTimeout is the maximum number of busy-wait iterations in flush().
|
||||
// Must be long enough for 2-3 USB frames (~3ms at 240MHz) so data gets
|
||||
// through when a host is connected, but short enough that println doesn't
|
||||
// freeze the application when no host is reading.
|
||||
const flushTimeout = 50000
|
||||
|
||||
type USB_DEVICE struct {
|
||||
Bus *esp.USB_DEVICE_Type
|
||||
Buffer *RingBuffer
|
||||
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 (
|
||||
@@ -125,19 +121,36 @@ func (usbdev *USB_DEVICE) handleInterrupt() {
|
||||
func (usbdev *USB_DEVICE) WriteByte(c byte) error {
|
||||
usbdev.ensureConfigured()
|
||||
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
||||
// FIFO not writable — try a short flush to nudge the hardware
|
||||
// (e.g. after reset the FIFO may need WR_DONE to transition).
|
||||
usbdev.flush()
|
||||
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))
|
||||
usbdev.flush()
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -150,17 +163,20 @@ func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) {
|
||||
|
||||
for i, c := range data {
|
||||
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
||||
if i > 0 {
|
||||
usbdev.flush()
|
||||
if usbdev.txStalled {
|
||||
return i, errUSBCouldNotWriteAllData
|
||||
}
|
||||
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -170,6 +186,12 @@ func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) {
|
||||
// level-triggered interrupt storm).
|
||||
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
|
||||
}
|
||||
// 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())
|
||||
@@ -198,16 +220,34 @@ func (usbdev *USB_DEVICE) RTS() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// flush signals WR_DONE and briefly waits for the hardware to accept more
|
||||
// data. The timeout is intentionally short so that serial output never
|
||||
// stalls the application when no USB host is reading.
|
||||
// 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)
|
||||
for i := 0; i < flushTimeout; i++ {
|
||||
}
|
||||
|
||||
// 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
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// The ESP32-S3 USB Serial/JTAG controller is fixed-function hardware.
|
||||
|
||||
@@ -151,6 +151,7 @@ func initTimerInterrupt() {
|
||||
// sleepTicks spins until the given number of ticks have elapsed, using the
|
||||
// TIMG0 alarm interrupt to avoid busy-waiting for the entire duration.
|
||||
func sleepTicks(d timeUnit) {
|
||||
machine.FlushSerial()
|
||||
target := ticks() + d
|
||||
for ticks() < target {
|
||||
// Set the alarm to fire at the target tick count (or as close
|
||||
|
||||
@@ -110,6 +110,7 @@ func initTimerInterrupt() {
|
||||
// sleepTicks spins until the given number of ticks have elapsed, using the
|
||||
// TIMG0 alarm interrupt to avoid busy-waiting for the entire duration.
|
||||
func sleepTicks(d timeUnit) {
|
||||
machine.FlushSerial()
|
||||
target := ticks() + d
|
||||
for ticks() < target {
|
||||
// Set the alarm to fire at the target tick count.
|
||||
|
||||
Reference in New Issue
Block a user