@@ -26,11 +26,23 @@ type cdcLineInfo struct {
2626
2727// USBCDC is the USB CDC aka serial over USB interface.
2828type USBCDC struct {
29- tx ring512
30- rx ring512
29+ tx ring512
30+ rx ring512
31+
32+ // inflight is the number of bytes currently submitted to the USB IN endpoint.
3133 inflight atomic.Uint32
32- rbuf [1 ]byte
33- wbuf [1 ]byte
34+
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).
42+ txActive atomic.Uint32
43+
44+ rbuf [1 ]byte
45+ wbuf [1 ]byte
3446}
3547
3648var (
@@ -81,7 +93,7 @@ func (usbcdc *USBCDC) Configure(config machine.UARTConfig) error {
8193
8294// Flush flushes buffered data.
8395func (usbcdc * USBCDC ) Flush () {
84- for usbcdc .tx .Used () > 0 {
96+ for usbcdc .tx .Used () > 0 || usbcdc . txActive . Load () != 0 {
8597 gosched ()
8698 }
8799}
@@ -105,33 +117,59 @@ func (usbcdc *USBCDC) Write(data []byte) (n int, err error) {
105117 return n , nil
106118}
107119
108- // kickTx starts a transfer if none is in flight. Called from main context only.
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.
109123func (usbcdc * USBCDC ) kickTx () {
110- if usbcdc .inflight . Load () > 0 {
111- return // txhandler will chain the next packet.
124+ if ! usbcdc .txActive . CompareAndSwap ( 0 , 1 ) {
125+ return
112126 }
113127 usbcdc .sendFromRing ()
114128}
115129
116130func (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.
117134 inflight := usbcdc .inflight .Load ()
118- usbcdc .inflight .Store (0 )
135+ if inflight == 0 {
136+ return
137+ }
119138 usbcdc .tx .Discard (inflight )
139+ usbcdc .inflight .Store (0 )
120140 usbcdc .sendFromRing ()
121141}
122142
123- // sendFromRing sends one USB packet from the ring and sets inflight.
124- // Called from kickTx (main) or txhandler (ISR), but never concurrently
125- // because kickTx only runs when inflight==0 and txhandler only runs
126- // when inflight>0.
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).
127146func (usbcdc * USBCDC ) sendFromRing () {
128- d1 , _ := usbcdc .tx .Peek ()
129- if len (d1 ) == 0 {
130- return
147+ for {
148+ d1 , _ := usbcdc .tx .Peek ()
149+ 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.
158+ usbcdc .txActive .Store (0 )
159+ if usbcdc .tx .Used () == 0 {
160+ return // ring empty and pump released; done
161+ }
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
166+ }
167+
168+ chunk := d1 [:min (usb .EndpointPacketSize , len (d1 ))]
169+ usbcdc .inflight .Store (uint32 (len (chunk )))
170+ machine .SendUSBInPacket (cdcEndpointIn , chunk )
171+ return // in flight; txActive stays set, txhandler continues
131172 }
132- chunk := d1 [:min (usb .EndpointPacketSize , len (d1 ))]
133- usbcdc .inflight .Store (uint32 (len (chunk )))
134- machine .SendUSBInPacket (cdcEndpointIn , chunk )
135173}
136174
137175// WriteByte writes a byte of data to the USB CDC interface.
0 commit comments