Skip to content

Commit 0e84235

Browse files
committed
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.
1 parent 8948394 commit 0e84235

4 files changed

Lines changed: 124 additions & 42 deletions

File tree

src/machine/machine_esp32c3_usb.go

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,11 @@ import (
1919

2020
const cpuInterruptFromUSB = 10
2121

22-
// flushTimeout is the maximum number of busy-wait iterations in flush().
23-
// Must be long enough for 2-3 USB frames (~3ms at 160MHz) so data gets
24-
// through when a host is connected, but short enough that println doesn't
25-
// freeze the application when no host is reading.
26-
const flushTimeout = 50000
27-
2822
type USB_DEVICE struct {
29-
Bus *esp.USB_DEVICE_Type
30-
Buffer *RingBuffer
23+
Bus *esp.USB_DEVICE_Type
24+
Buffer *RingBuffer
25+
txPending bool // unflushed data in the EP1 TX FIFO
26+
txStalled bool // set when flushAndWait fails (no host reading); cleared when FIFO becomes writable
3127
}
3228

3329
var (
@@ -147,19 +143,36 @@ func (usbdev *USB_DEVICE) handleInterrupt() {
147143
func (usbdev *USB_DEVICE) WriteByte(c byte) error {
148144
usbdev.ensureConfigured()
149145
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
150-
// FIFO not writable — try a short flush to nudge the hardware
151-
// (e.g. after reset the FIFO may need WR_DONE to transition).
152-
usbdev.flush()
153-
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
146+
// FIFO locked by a pending USB transfer.
147+
if usbdev.txStalled {
148+
// Previously failed — skip the expensive spin and drop
149+
// the byte. When a host reconnects SERIAL_IN_EP_DATA_FREE
150+
// goes back to 1, clearing the stall on the next call.
151+
return errUSBCouldNotWriteAllData
152+
}
153+
// First time the FIFO is full: wait briefly for the host to
154+
// read the previous packet.
155+
if !usbdev.flushAndWait() {
156+
usbdev.txStalled = true
154157
return errUSBCouldNotWriteAllData
155158
}
156159
}
160+
usbdev.txStalled = false
157161

158162
// Use EP1.Set() (direct store) instead of SetEP1_RDWR_BYTE which
159163
// does a read-modify-write — the read side-effect pops a byte from
160164
// the RX FIFO.
161165
usbdev.Bus.EP1.Set(uint32(c))
162-
usbdev.flush()
166+
167+
// Only signal WR_DONE on newline to batch bytes into a single USB
168+
// packet. The FIFO-full path above also flushes when the 64-byte
169+
// FIFO fills up.
170+
if c == '\n' {
171+
usbdev.flush()
172+
usbdev.txPending = false
173+
} else {
174+
usbdev.txPending = true
175+
}
163176

164177
return nil
165178
}
@@ -172,23 +185,32 @@ func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) {
172185

173186
for i, c := range data {
174187
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
175-
if i > 0 {
176-
usbdev.flush()
188+
if usbdev.txStalled {
189+
return i, errUSBCouldNotWriteAllData
177190
}
178-
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
191+
if !usbdev.flushAndWait() {
192+
usbdev.txStalled = true
179193
return i, errUSBCouldNotWriteAllData
180194
}
181195
}
196+
usbdev.txStalled = false
182197
usbdev.Bus.EP1.Set(uint32(c))
183198
}
184199

185200
usbdev.flush()
201+
usbdev.txPending = false
186202
return len(data), nil
187203
}
188204

189205
// Buffered returns the number of bytes waiting in the receive ring buffer.
190206
func (usbdev *USB_DEVICE) Buffered() int {
191207
usbdev.ensureConfigured()
208+
// Flush any pending TX data so callers like echo loops don't
209+
// need to explicitly flush after WriteByte.
210+
if usbdev.txPending {
211+
usbdev.flush()
212+
usbdev.txPending = false
213+
}
192214
return int(usbdev.Buffer.Used())
193215
}
194216

@@ -209,16 +231,34 @@ func (usbdev *USB_DEVICE) RTS() bool {
209231
return false
210232
}
211233

212-
// flush signals WR_DONE and briefly waits for the hardware to accept more
213-
// data. The timeout is intentionally short so that serial output never
214-
// stalls the application when no USB host is reading.
234+
// flush signals WR_DONE to tell the hardware to send the data that has
235+
// been written to the EP1 FIFO. Returns immediately without waiting.
215236
func (usbdev *USB_DEVICE) flush() {
216237
usbdev.Bus.SetEP1_CONF_WR_DONE(1)
217-
for i := 0; i < flushTimeout; i++ {
238+
}
239+
240+
// FlushSerial flushes any pending USB serial TX data. Called from the
241+
// runtime (e.g. before sleeping) to ensure data from print() without
242+
// a trailing newline gets sent promptly.
243+
func FlushSerial() {
244+
if _USBCDC.txPending {
245+
_USBCDC.flush()
246+
_USBCDC.txPending = false
247+
}
248+
}
249+
250+
// flushAndWait signals WR_DONE and waits for the EP1 FIFO to become
251+
// writable again. The timeout covers a few USB frames so that data gets
252+
// through when a host is connected. Returns false if the FIFO is still
253+
// locked after the timeout (no host reading).
254+
func (usbdev *USB_DEVICE) flushAndWait() bool {
255+
usbdev.Bus.SetEP1_CONF_WR_DONE(1)
256+
for i := 0; i < 50000; i++ {
218257
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() != 0 {
219-
return
258+
return true
220259
}
221260
}
261+
return false
222262
}
223263

224264
// The ESP32-C3 USB Serial/JTAG controller is fixed-function hardware.

src/machine/machine_esp32xx_usb.go

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,11 @@ import (
1919

2020
const cpuInterruptFromUSB = 8
2121

22-
// flushTimeout is the maximum number of busy-wait iterations in flush().
23-
// Must be long enough for 2-3 USB frames (~3ms at 240MHz) so data gets
24-
// through when a host is connected, but short enough that println doesn't
25-
// freeze the application when no host is reading.
26-
const flushTimeout = 50000
27-
2822
type USB_DEVICE struct {
29-
Bus *esp.USB_DEVICE_Type
30-
Buffer *RingBuffer
23+
Bus *esp.USB_DEVICE_Type
24+
Buffer *RingBuffer
25+
txPending bool // unflushed data in the EP1 TX FIFO
26+
txStalled bool // set when flushAndWait fails (no host reading); cleared when FIFO becomes writable
3127
}
3228

3329
var (
@@ -125,19 +121,36 @@ func (usbdev *USB_DEVICE) handleInterrupt() {
125121
func (usbdev *USB_DEVICE) WriteByte(c byte) error {
126122
usbdev.ensureConfigured()
127123
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
128-
// FIFO not writable — try a short flush to nudge the hardware
129-
// (e.g. after reset the FIFO may need WR_DONE to transition).
130-
usbdev.flush()
131-
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
124+
// FIFO locked by a pending USB transfer.
125+
if usbdev.txStalled {
126+
// Previously failed — skip the expensive spin and drop
127+
// the byte. When a host reconnects SERIAL_IN_EP_DATA_FREE
128+
// goes back to 1, clearing the stall on the next call.
129+
return errUSBCouldNotWriteAllData
130+
}
131+
// First time the FIFO is full: wait briefly for the host to
132+
// read the previous packet.
133+
if !usbdev.flushAndWait() {
134+
usbdev.txStalled = true
132135
return errUSBCouldNotWriteAllData
133136
}
134137
}
138+
usbdev.txStalled = false
135139

136140
// Use EP1.Set() (direct store) instead of SetEP1_RDWR_BYTE which
137141
// does a read-modify-write — the read side-effect pops a byte from
138142
// the RX FIFO.
139143
usbdev.Bus.EP1.Set(uint32(c))
140-
usbdev.flush()
144+
145+
// Only signal WR_DONE on newline to batch bytes into a single USB
146+
// packet. The FIFO-full path above also flushes when the 64-byte
147+
// FIFO fills up.
148+
if c == '\n' {
149+
usbdev.flush()
150+
usbdev.txPending = false
151+
} else {
152+
usbdev.txPending = true
153+
}
141154

142155
return nil
143156
}
@@ -150,17 +163,20 @@ func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) {
150163

151164
for i, c := range data {
152165
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
153-
if i > 0 {
154-
usbdev.flush()
166+
if usbdev.txStalled {
167+
return i, errUSBCouldNotWriteAllData
155168
}
156-
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
169+
if !usbdev.flushAndWait() {
170+
usbdev.txStalled = true
157171
return i, errUSBCouldNotWriteAllData
158172
}
159173
}
174+
usbdev.txStalled = false
160175
usbdev.Bus.EP1.Set(uint32(c))
161176
}
162177

163178
usbdev.flush()
179+
usbdev.txPending = false
164180
return len(data), nil
165181
}
166182

@@ -170,6 +186,12 @@ func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) {
170186
// level-triggered interrupt storm).
171187
func (usbdev *USB_DEVICE) Buffered() int {
172188
usbdev.ensureConfigured()
189+
// Flush any pending TX data so callers like echo loops don't
190+
// need to explicitly flush after WriteByte.
191+
if usbdev.txPending {
192+
usbdev.flush()
193+
usbdev.txPending = false
194+
}
173195
// Drain the hardware FIFO into the ring buffer.
174196
for usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL() != 0 {
175197
b := byte(usbdev.Bus.EP1.Get())
@@ -198,16 +220,34 @@ func (usbdev *USB_DEVICE) RTS() bool {
198220
return false
199221
}
200222

201-
// flush signals WR_DONE and briefly waits for the hardware to accept more
202-
// data. The timeout is intentionally short so that serial output never
203-
// stalls the application when no USB host is reading.
223+
// flush signals WR_DONE to tell the hardware to send the data that has
224+
// been written to the EP1 FIFO. Returns immediately without waiting.
204225
func (usbdev *USB_DEVICE) flush() {
205226
usbdev.Bus.SetEP1_CONF_WR_DONE(1)
206-
for i := 0; i < flushTimeout; i++ {
227+
}
228+
229+
// FlushSerial flushes any pending USB serial TX data. Called from the
230+
// runtime (e.g. before sleeping) to ensure data from print() without
231+
// a trailing newline gets sent promptly.
232+
func FlushSerial() {
233+
if _USBCDC.txPending {
234+
_USBCDC.flush()
235+
_USBCDC.txPending = false
236+
}
237+
}
238+
239+
// flushAndWait signals WR_DONE and waits for the EP1 FIFO to become
240+
// writable again. The timeout covers a few USB frames so that data gets
241+
// through when a host is connected. Returns false if the FIFO is still
242+
// locked after the timeout (no host reading).
243+
func (usbdev *USB_DEVICE) flushAndWait() bool {
244+
usbdev.Bus.SetEP1_CONF_WR_DONE(1)
245+
for i := 0; i < 50000; i++ {
207246
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() != 0 {
208-
return
247+
return true
209248
}
210249
}
250+
return false
211251
}
212252

213253
// The ESP32-S3 USB Serial/JTAG controller is fixed-function hardware.

src/runtime/runtime_esp32c3.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ func initTimerInterrupt() {
151151
// sleepTicks spins until the given number of ticks have elapsed, using the
152152
// TIMG0 alarm interrupt to avoid busy-waiting for the entire duration.
153153
func sleepTicks(d timeUnit) {
154+
machine.FlushSerial()
154155
target := ticks() + d
155156
for ticks() < target {
156157
// Set the alarm to fire at the target tick count (or as close

src/runtime/runtime_esp32sx.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ func initTimerInterrupt() {
110110
// sleepTicks spins until the given number of ticks have elapsed, using the
111111
// TIMG0 alarm interrupt to avoid busy-waiting for the entire duration.
112112
func sleepTicks(d timeUnit) {
113+
machine.FlushSerial()
113114
target := ticks() + d
114115
for ticks() < target {
115116
// Set the alarm to fire at the target tick count.

0 commit comments

Comments
 (0)