machine/esp32s3,esp32c3: make USB Serial/JTAG writes non-blocking when FIFO is full

When no USB host is reading (e.g. board not connected to a serial
monitor), WriteByte and Write would spin for up to 200k iterations
per byte waiting for the FIFO to drain. This stalled the entire
application, freezing unrelated peripherals like I2C displays.

Reduce flushTimeout from 200,000 to 50,000 iterations (~3ms) which
is enough for 2-3 USB frames when a host is connected, but short
enough that serial output won't freeze the application when no host
is reading. Serial output is best-effort; callers like putchar
already ignore write errors.

Applies to both ESP32-S3 and ESP32-C3 which share the same USB
Serial/JTAG controller design.
This commit is contained in:
deadprogram
2026-04-12 19:18:15 +02:00
committed by Ron Evans
parent f4a6e0373b
commit eba6e7a428
2 changed files with 18 additions and 10 deletions
+9 -5
View File
@@ -20,8 +20,10 @@ import (
const cpuInterruptFromUSB = 10
// flushTimeout is the maximum number of busy-wait iterations in flush().
// Prevents hanging when no USB host is connected.
const flushTimeout = 200000
// 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
@@ -145,7 +147,8 @@ 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 full — try flushing first, then recheck.
// 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 {
return errUSBCouldNotWriteAllData
@@ -206,8 +209,9 @@ 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.
// 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.
func (usbdev *USB_DEVICE) flush() {
usbdev.Bus.SetEP1_CONF_WR_DONE(1)
for i := 0; i < flushTimeout; i++ {
+9 -5
View File
@@ -20,8 +20,10 @@ import (
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
// 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
@@ -123,7 +125,8 @@ 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 full — try flushing first, then recheck.
// 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 {
return errUSBCouldNotWriteAllData
@@ -195,8 +198,9 @@ 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.
// 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.
func (usbdev *USB_DEVICE) flush() {
usbdev.Bus.SetEP1_CONF_WR_DONE(1)
for i := 0; i < flushTimeout; i++ {