From eba6e7a4283b6d8604ea9dc3b52537567a29f72a Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 12 Apr 2026 19:18:15 +0200 Subject: [PATCH] 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. --- src/machine/machine_esp32c3_usb.go | 14 +++++++++----- src/machine/machine_esp32xx_usb.go | 14 +++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/machine/machine_esp32c3_usb.go b/src/machine/machine_esp32c3_usb.go index 648034824..756eb8bf7 100644 --- a/src/machine/machine_esp32c3_usb.go +++ b/src/machine/machine_esp32c3_usb.go @@ -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++ { diff --git a/src/machine/machine_esp32xx_usb.go b/src/machine/machine_esp32xx_usb.go index 5066fab67..f32869e72 100644 --- a/src/machine/machine_esp32xx_usb.go +++ b/src/machine/machine_esp32xx_usb.go @@ -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++ {