Skip to content
Open
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
2 changes: 1 addition & 1 deletion builder/sizes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestBinarySize(t *testing.T) {
// microcontrollers
{"hifive1b", "examples/echo", 3680, 280, 0, 2252},
{"microbit", "examples/serial", 2694, 342, 8, 2248},
{"wioterminal", "examples/pininterrupt", 7074, 1510, 120, 7248},
{"wioterminal", "examples/pininterrupt", 7184, 1508, 120, 7256},

// TODO: also check wasm. Right now this is difficult, because
// wasm binaries are run through wasm-opt and therefore the
Expand Down
68 changes: 51 additions & 17 deletions src/machine/usb/cdc/usbcdc.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,19 @@ type cdcLineInfo struct {

// USBCDC is the USB CDC aka serial over USB interface.
type USBCDC struct {
tx ring512
rx ring512
tx ring512
rx ring512

// inflight is the number of bytes currently submitted to the USB IN endpoint.
inflight atomic.Uint32
rbuf [1]byte
wbuf [1]byte

// txActive serializes the USB CDC TX pump between Write and the TX
// completion handler. This matters on multicore targets where they can run
// concurrently.
txActive atomic.Uint32

rbuf [1]byte
wbuf [1]byte
}

var (
Expand Down Expand Up @@ -81,7 +89,7 @@ func (usbcdc *USBCDC) Configure(config machine.UARTConfig) error {

// Flush flushes buffered data.
func (usbcdc *USBCDC) Flush() {
for usbcdc.tx.Used() > 0 {
for usbcdc.tx.Used() > 0 || usbcdc.txActive.Load() != 0 {
gosched()
}
}
Expand All @@ -105,33 +113,59 @@ func (usbcdc *USBCDC) Write(data []byte) (n int, err error) {
return n, nil
}

// kickTx starts a transfer if none is in flight. Called from main context only.
// kickTx starts the TX pump if it is idle.
func (usbcdc *USBCDC) kickTx() {
if usbcdc.inflight.Load() > 0 {
return // txhandler will chain the next packet.
if !usbcdc.txActive.CompareAndSwap(0, 1) {
return
}
usbcdc.sendFromRing()
}

func (usbcdc *USBCDC) txhandler() {
inflight := usbcdc.inflight.Load()
usbcdc.inflight.Store(0)
if inflight == 0 {
return
}
usbcdc.tx.Discard(inflight)
usbcdc.inflight.Store(0)
usbcdc.sendFromRing()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is odd to me this is not guarded with a txActive compare-swap. I think my issue with this PR as it stands is it is not clear to me what txActive is guarding. From the looks of it sendFromRing can be called concurrently from kickTx and txhandler and we've added a bunch of logic in sendFromRing to deal with this. I feel like we can maybe keep sendFromRing as a simple function that does what it says and guard it from executing concurrently as I suspect that concurrent execution is not necessary for this to work correctly.

maybe we can even delete kickTx and simply add the guard at the start of sendFromRing and do a simple Store(0) immediately when sendFromRing ends it's process?

}

// sendFromRing sends one USB packet from the ring and sets inflight.
// Called from kickTx (main) or txhandler (ISR), but never concurrently
// because kickTx only runs when inflight==0 and txhandler only runs
// when inflight>0.
//
// The caller must own txActive. Ownership starts in kickTx and is kept across
// TX completion handling until the TX ring is empty.
func (usbcdc *USBCDC) sendFromRing() {
d1, _ := usbcdc.tx.Peek()
if len(d1) == 0 {
// This is the main TX pump loop. While txActive is held, each iteration
// peeks the TX ring and sends one packet if data is available.
//
// The empty-ring case below is the shutdown path: release txActive, then
// re-check the ring to avoid missing a Write that appended data while
// txActive was still set.
for {
d1, _ := usbcdc.tx.Peek()
if len(d1) == 0 {
usbcdc.txActive.Store(0)

// Avoid a missed wakeup: Write may append data while txActive is
// still set, causing kickTx to return without starting a transfer.
switch {
case usbcdc.tx.Used() == 0:
return
case !usbcdc.txActive.CompareAndSwap(0, 1):
return
}

// New data appeared while txActive was set, and this TX pump owner
// re-acquired ownership. Continue the pump and re-peek the ring.
continue
Comment thread
soypat marked this conversation as resolved.
}

chunk := d1[:min(usb.EndpointPacketSize, len(d1))]
usbcdc.inflight.Store(uint32(len(chunk)))
machine.SendUSBInPacket(cdcEndpointIn, chunk)
return
}
chunk := d1[:min(usb.EndpointPacketSize, len(d1))]
usbcdc.inflight.Store(uint32(len(chunk)))
machine.SendUSBInPacket(cdcEndpointIn, chunk)
}

// WriteByte writes a byte of data to the USB CDC interface.
Expand Down
Loading