Skip to content

Commit 40ce203

Browse files
author
rdon
committed
usb/cdc: document txActive ownership and lost-wakeup recheck
1 parent c2184cc commit 40ce203

1 file changed

Lines changed: 32 additions & 42 deletions

File tree

src/machine/usb/cdc/usbcdc.go

Lines changed: 32 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,13 @@ type USBCDC struct {
3232
// inflight is the number of bytes currently submitted to the USB IN endpoint.
3333
inflight atomic.Uint32
3434

35-
// txActive serializes the USB CDC TX pump between Write and the TX
36-
// completion handler. This matters on multicore targets where they can run
37-
// concurrently.
35+
// txActive is the TX-pump ownership flag: 0 = idle, 1 = a pump owns the TX
36+
// path. Claimed once (kickTx, CAS 0->1), held across every in-flight packet
37+
// and the TX-complete IRQ, and released only when the ring drains. While it
38+
// is set, kickTx's CAS fails and no second pump starts, which serializes the
39+
// pump against Write across cores. Same model as Linux NAPI_STATE_SCHED:
40+
// held across completion, dropped only with a recheck
41+
// (Documentation/networking/napi.rst).
3842
txActive atomic.Uint32
3943

4044
rbuf [1]byte
@@ -113,7 +117,9 @@ func (usbcdc *USBCDC) Write(data []byte) (n int, err error) {
113117
return n, nil
114118
}
115119

116-
// kickTx starts the TX pump if it is idle.
120+
// kickTx claims the TX pump for a producer. This CAS is the only start-from-idle
121+
// edge; if it fails, a pump already owns the path and will drain what we just
122+
// enqueued -- see the recheck in sendFromRing.
117123
func (usbcdc *USBCDC) kickTx() {
118124
if !usbcdc.txActive.CompareAndSwap(0, 1) {
119125
return
@@ -122,6 +128,9 @@ func (usbcdc *USBCDC) kickTx() {
122128
}
123129

124130
func (usbcdc *USBCDC) txhandler() {
131+
// TX-complete IRQ. The pump is still owned here (txActive stayed 1 across the
132+
// in-flight packet), so continue WITHOUT re-claiming -- pairs with the CAS in
133+
// kickTx. A CAS here would see the flag already set, bail, and stall the chain.
125134
inflight := usbcdc.inflight.Load()
126135
if inflight == 0 {
127136
return
@@ -131,54 +140,35 @@ func (usbcdc *USBCDC) txhandler() {
131140
usbcdc.sendFromRing()
132141
}
133142

134-
// sendFromRing submits one USB IN packet from the TX ring, or shuts down
135-
// the TX pump if the ring is empty.
136-
//
137-
// The caller must own txActive (either having just acquired it via CAS,
138-
// or continuing ownership from a previous packet submission).
139-
// For kickTx: newly acquired via CAS.
140-
// For txhandler: inherited from the previous packet submission.
141-
//
142-
// While the caller owns txActive:
143-
// - If data is available, one packet is sent and txActive remains set
144-
// for the in-flight packet (ownership continues to txhandler).
145-
// - If the ring is empty, txActive is cleared and the pump shuts down
146-
// (with a final re-check to avoid a missed wakeup from Write).
143+
// sendFromRing runs one step of the TX pump: submit one IN packet, or release the
144+
// pump if the ring is empty. Precondition: txActive == 1 (from kickTx's CAS, or
145+
// still held from the previous packet when entered via txhandler).
147146
func (usbcdc *USBCDC) sendFromRing() {
148-
// This loop sends at most one USB IN packet per entry.
149-
//
150-
// If the TX ring has data, one packet is handed to the USB hardware and
151-
// txActive remains set until txhandler continues the pump after
152-
// completion.
153-
//
154-
// If the TX ring appears empty, this is the shutdown path. Clear
155-
// txActive, then re-check the ring to avoid missing a Write that added
156-
// data while txActive was still set.
157147
for {
158148
d1, _ := usbcdc.tx.Peek()
159149
if len(d1) == 0 {
150+
// Release the pump, then re-scan the ring: closes the missed-wakeup
151+
// race where Write Put()s data and kickTx's CAS then fails (txActive
152+
// still set), leaving the data for this pump to drain. The Store(0)
153+
// is ordered before the Used() load -- and, in the producer, Put()
154+
// before its CAS -- by the sequential consistency of Go's atomics, so
155+
// neither side misses the other (assumes the ring's accesses are
156+
// atomic too). cf. napi_complete_done() clearing NAPI_STATE_SCHED
157+
// then rechecking.
160158
usbcdc.txActive.Store(0)
161-
// Avoid a missed wakeup.
162-
//
163-
// A concurrent Write may have appended data while txActive was
164-
// still set. In that case kickTx would see an active pump and
165-
// return without starting another transfer. After clearing
166-
// txActive, re-check the ring and reclaim txActive if this pump
167-
// must continue.
168-
switch {
169-
case usbcdc.tx.Used() == 0:
170-
return
171-
case !usbcdc.txActive.CompareAndSwap(0, 1):
172-
return
159+
if usbcdc.tx.Used() == 0 {
160+
return // ring empty and pump released; done
173161
}
174-
// New data appeared during shutdown, and this caller reclaimed
175-
// txActive. Continue and re-peek the ring.
176-
continue
162+
if !usbcdc.txActive.CompareAndSwap(0, 1) {
163+
return // another producer re-claimed the pump; let it run
164+
}
165+
continue // re-claimed; re-peek and keep pumping
177166
}
167+
178168
chunk := d1[:min(usb.EndpointPacketSize, len(d1))]
179169
usbcdc.inflight.Store(uint32(len(chunk)))
180170
machine.SendUSBInPacket(cdcEndpointIn, chunk)
181-
return
171+
return // in flight; txActive stays set, txhandler continues
182172
}
183173
}
184174

0 commit comments

Comments
 (0)