Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 61 additions & 21 deletions src/machine/machine_esp32c3_usb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
}
Expand All @@ -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())
}

Expand All @@ -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.
Expand Down
82 changes: 61 additions & 21 deletions src/machine/machine_esp32xx_usb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}

Expand All @@ -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())
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/runtime/runtime_esp32c3.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/runtime/runtime_esp32sx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading