mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-16 10:43:29 +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
|
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 {
|
type USB_DEVICE struct {
|
||||||
Bus *esp.USB_DEVICE_Type
|
Bus *esp.USB_DEVICE_Type
|
||||||
Buffer *RingBuffer
|
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 (
|
var (
|
||||||
@@ -147,19 +143,36 @@ func (usbdev *USB_DEVICE) handleInterrupt() {
|
|||||||
func (usbdev *USB_DEVICE) WriteByte(c byte) error {
|
func (usbdev *USB_DEVICE) WriteByte(c byte) error {
|
||||||
usbdev.ensureConfigured()
|
usbdev.ensureConfigured()
|
||||||
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
||||||
// FIFO not writable — try a short flush to nudge the hardware
|
// FIFO locked by a pending USB transfer.
|
||||||
// (e.g. after reset the FIFO may need WR_DONE to transition).
|
if usbdev.txStalled {
|
||||||
usbdev.flush()
|
// Previously failed — skip the expensive spin and drop
|
||||||
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
// 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
|
return errUSBCouldNotWriteAllData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
usbdev.txStalled = false
|
||||||
|
|
||||||
// Use EP1.Set() (direct store) instead of SetEP1_RDWR_BYTE which
|
// Use EP1.Set() (direct store) instead of SetEP1_RDWR_BYTE which
|
||||||
// does a read-modify-write — the read side-effect pops a byte from
|
// does a read-modify-write — the read side-effect pops a byte from
|
||||||
// the RX FIFO.
|
// the RX FIFO.
|
||||||
usbdev.Bus.EP1.Set(uint32(c))
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -172,23 +185,32 @@ func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) {
|
|||||||
|
|
||||||
for i, c := range data {
|
for i, c := range data {
|
||||||
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
||||||
if i > 0 {
|
if usbdev.txStalled {
|
||||||
usbdev.flush()
|
return i, errUSBCouldNotWriteAllData
|
||||||
}
|
}
|
||||||
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
if !usbdev.flushAndWait() {
|
||||||
|
usbdev.txStalled = true
|
||||||
return i, errUSBCouldNotWriteAllData
|
return i, errUSBCouldNotWriteAllData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
usbdev.txStalled = false
|
||||||
usbdev.Bus.EP1.Set(uint32(c))
|
usbdev.Bus.EP1.Set(uint32(c))
|
||||||
}
|
}
|
||||||
|
|
||||||
usbdev.flush()
|
usbdev.flush()
|
||||||
|
usbdev.txPending = false
|
||||||
return len(data), nil
|
return len(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffered returns the number of bytes waiting in the receive ring buffer.
|
// Buffered returns the number of bytes waiting in the receive ring buffer.
|
||||||
func (usbdev *USB_DEVICE) Buffered() int {
|
func (usbdev *USB_DEVICE) Buffered() int {
|
||||||
usbdev.ensureConfigured()
|
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())
|
return int(usbdev.Buffer.Used())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,16 +231,34 @@ func (usbdev *USB_DEVICE) RTS() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// flush signals WR_DONE and briefly waits for the hardware to accept more
|
// flush signals WR_DONE to tell the hardware to send the data that has
|
||||||
// data. The timeout is intentionally short so that serial output never
|
// been written to the EP1 FIFO. Returns immediately without waiting.
|
||||||
// stalls the application when no USB host is reading.
|
|
||||||
func (usbdev *USB_DEVICE) flush() {
|
func (usbdev *USB_DEVICE) flush() {
|
||||||
usbdev.Bus.SetEP1_CONF_WR_DONE(1)
|
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 {
|
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.
|
// The ESP32-C3 USB Serial/JTAG controller is fixed-function hardware.
|
||||||
|
|||||||
@@ -19,15 +19,11 @@ import (
|
|||||||
|
|
||||||
const cpuInterruptFromUSB = 8
|
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 {
|
type USB_DEVICE struct {
|
||||||
Bus *esp.USB_DEVICE_Type
|
Bus *esp.USB_DEVICE_Type
|
||||||
Buffer *RingBuffer
|
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 (
|
var (
|
||||||
@@ -125,19 +121,36 @@ func (usbdev *USB_DEVICE) handleInterrupt() {
|
|||||||
func (usbdev *USB_DEVICE) WriteByte(c byte) error {
|
func (usbdev *USB_DEVICE) WriteByte(c byte) error {
|
||||||
usbdev.ensureConfigured()
|
usbdev.ensureConfigured()
|
||||||
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
||||||
// FIFO not writable — try a short flush to nudge the hardware
|
// FIFO locked by a pending USB transfer.
|
||||||
// (e.g. after reset the FIFO may need WR_DONE to transition).
|
if usbdev.txStalled {
|
||||||
usbdev.flush()
|
// Previously failed — skip the expensive spin and drop
|
||||||
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
// 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
|
return errUSBCouldNotWriteAllData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
usbdev.txStalled = false
|
||||||
|
|
||||||
// Use EP1.Set() (direct store) instead of SetEP1_RDWR_BYTE which
|
// Use EP1.Set() (direct store) instead of SetEP1_RDWR_BYTE which
|
||||||
// does a read-modify-write — the read side-effect pops a byte from
|
// does a read-modify-write — the read side-effect pops a byte from
|
||||||
// the RX FIFO.
|
// the RX FIFO.
|
||||||
usbdev.Bus.EP1.Set(uint32(c))
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -150,17 +163,20 @@ func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) {
|
|||||||
|
|
||||||
for i, c := range data {
|
for i, c := range data {
|
||||||
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
||||||
if i > 0 {
|
if usbdev.txStalled {
|
||||||
usbdev.flush()
|
return i, errUSBCouldNotWriteAllData
|
||||||
}
|
}
|
||||||
if usbdev.Bus.GetEP1_CONF_SERIAL_IN_EP_DATA_FREE() == 0 {
|
if !usbdev.flushAndWait() {
|
||||||
|
usbdev.txStalled = true
|
||||||
return i, errUSBCouldNotWriteAllData
|
return i, errUSBCouldNotWriteAllData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
usbdev.txStalled = false
|
||||||
usbdev.Bus.EP1.Set(uint32(c))
|
usbdev.Bus.EP1.Set(uint32(c))
|
||||||
}
|
}
|
||||||
|
|
||||||
usbdev.flush()
|
usbdev.flush()
|
||||||
|
usbdev.txPending = false
|
||||||
return len(data), nil
|
return len(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +186,12 @@ func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) {
|
|||||||
// level-triggered interrupt storm).
|
// level-triggered interrupt storm).
|
||||||
func (usbdev *USB_DEVICE) Buffered() int {
|
func (usbdev *USB_DEVICE) Buffered() int {
|
||||||
usbdev.ensureConfigured()
|
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.
|
// Drain the hardware FIFO into the ring buffer.
|
||||||
for usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL() != 0 {
|
for usbdev.Bus.GetEP1_CONF_SERIAL_OUT_EP_DATA_AVAIL() != 0 {
|
||||||
b := byte(usbdev.Bus.EP1.Get())
|
b := byte(usbdev.Bus.EP1.Get())
|
||||||
@@ -198,16 +220,34 @@ func (usbdev *USB_DEVICE) RTS() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// flush signals WR_DONE and briefly waits for the hardware to accept more
|
// flush signals WR_DONE to tell the hardware to send the data that has
|
||||||
// data. The timeout is intentionally short so that serial output never
|
// been written to the EP1 FIFO. Returns immediately without waiting.
|
||||||
// stalls the application when no USB host is reading.
|
|
||||||
func (usbdev *USB_DEVICE) flush() {
|
func (usbdev *USB_DEVICE) flush() {
|
||||||
usbdev.Bus.SetEP1_CONF_WR_DONE(1)
|
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 {
|
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.
|
// 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
|
// sleepTicks spins until the given number of ticks have elapsed, using the
|
||||||
// TIMG0 alarm interrupt to avoid busy-waiting for the entire duration.
|
// TIMG0 alarm interrupt to avoid busy-waiting for the entire duration.
|
||||||
func sleepTicks(d timeUnit) {
|
func sleepTicks(d timeUnit) {
|
||||||
|
machine.FlushSerial()
|
||||||
target := ticks() + d
|
target := ticks() + d
|
||||||
for ticks() < target {
|
for ticks() < target {
|
||||||
// Set the alarm to fire at the target tick count (or as close
|
// 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
|
// sleepTicks spins until the given number of ticks have elapsed, using the
|
||||||
// TIMG0 alarm interrupt to avoid busy-waiting for the entire duration.
|
// TIMG0 alarm interrupt to avoid busy-waiting for the entire duration.
|
||||||
func sleepTicks(d timeUnit) {
|
func sleepTicks(d timeUnit) {
|
||||||
|
machine.FlushSerial()
|
||||||
target := ticks() + d
|
target := ticks() + d
|
||||||
for ticks() < target {
|
for ticks() < target {
|
||||||
// Set the alarm to fire at the target tick count.
|
// Set the alarm to fire at the target tick count.
|
||||||
|
|||||||
Reference in New Issue
Block a user