diff --git a/src/examples/serial-stress/main.go b/src/examples/serial-stress/main.go new file mode 100644 index 0000000000..bd77bf1c96 --- /dev/null +++ b/src/examples/serial-stress/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "machine" + "strconv" + "time" +) + +func main() { + time.Sleep(2 * time.Second) // connect via serial + buf1 := makeBuffer('|', 600) + buf2 := makeBuffer('/', 600) + println("start") + serialWrite(buf1) + serialWrite(buf2) + + // machine.Serial. +} + +func makeBuffer(sep byte, size int) []byte { + buf := make([]byte, size) + for i := 0; i < size-5; i += 5 { + buf[i] = sep + strconv.AppendInt(buf[i+1:i+1:i+5], int64(i), 10) + } + return buf +} + +func serialWrite(b []byte) { + machine.Serial.Write(b) +} diff --git a/src/machine/machine_atsamd51.go b/src/machine/machine_atsamd51.go index 20dc41b8de..158be4911b 100644 --- a/src/machine/machine_atsamd51.go +++ b/src/machine/machine_atsamd51.go @@ -11,6 +11,7 @@ import ( "device/sam" "errors" "internal/binary" + "runtime/interrupt" "unsafe" ) diff --git a/src/machine/usb/cdc/buffer.go b/src/machine/usb/cdc/buffer.go deleted file mode 100644 index ad5eb3645b..0000000000 --- a/src/machine/usb/cdc/buffer.go +++ /dev/null @@ -1,119 +0,0 @@ -package cdc - -import ( - "runtime/volatile" -) - -const rxRingBufferSize = 128 - -// rxRingBuffer is ring buffer implementation inspired by post at -// https://www.embeddedrelated.com/showthread/comp.arch.embedded/77084-1.php -type rxRingBuffer struct { - buffer [rxRingBufferSize]volatile.Register8 - head volatile.Register8 - tail volatile.Register8 -} - -// NewRxRingBuffer returns a new ring buffer. -func NewRxRingBuffer() *rxRingBuffer { - return &rxRingBuffer{} -} - -// Used returns how many bytes in buffer have been used. -func (rb *rxRingBuffer) Used() uint8 { - return uint8(rb.head.Get() - rb.tail.Get()) -} - -// Put stores a byte in the buffer. If the buffer is already -// full, the method will return false. -func (rb *rxRingBuffer) Put(val byte) bool { - if rb.Used() != rxRingBufferSize { - rb.head.Set(rb.head.Get() + 1) - rb.buffer[rb.head.Get()%rxRingBufferSize].Set(val) - return true - } - return false -} - -// Get returns a byte from the buffer. If the buffer is empty, -// the method will return a false as the second value. -func (rb *rxRingBuffer) Get() (byte, bool) { - if rb.Used() != 0 { - rb.tail.Set(rb.tail.Get() + 1) - return rb.buffer[rb.tail.Get()%rxRingBufferSize].Get(), true - } - return 0, false -} - -// Clear resets the head and tail pointer to zero. -func (rb *rxRingBuffer) Clear() { - rb.head.Set(0) - rb.tail.Set(0) -} - -const txRingBufferSize = 8 - -// txRingBuffer is ring buffer implementation inspired by post at -// https://www.embeddedrelated.com/showthread/comp.arch.embedded/77084-1.php -type txRingBuffer struct { - buffer [txRingBufferSize]struct { - buf [64]byte - size int - } - head volatile.Register8 - tail volatile.Register8 -} - -// NewTxRingBuffer returns a new ring buffer. -func NewTxRingBuffer() *txRingBuffer { - return &txRingBuffer{} -} - -// Used returns how many bytes in buffer have been used. -func (rb *txRingBuffer) Used() uint8 { - return uint8(rb.head.Get() - rb.tail.Get()) -} - -// Put stores a byte in the buffer. If the buffer is already -// full, the method will return false. -func (rb *txRingBuffer) Put(val []byte) bool { - if rb.Used() == txRingBufferSize { - return false - } - - if rb.Used() == 0 { - rb.head.Set(rb.head.Get() + 1) - rb.buffer[rb.head.Get()%txRingBufferSize].size = 0 - } - buf := &rb.buffer[rb.head.Get()%txRingBufferSize] - - for i := 0; i < len(val); i++ { - if buf.size == 64 { - // next - // TODO: Make sure that data is not corrupted even when the buffer is full - rb.head.Set(rb.head.Get() + 1) - buf = &rb.buffer[rb.head.Get()%txRingBufferSize] - rb.buffer[rb.head.Get()%txRingBufferSize].size = 0 - } - buf.buf[buf.size] = val[i] - buf.size++ - } - return true -} - -// Get returns a byte from the buffer. If the buffer is empty, -// the method will return a false as the second value. -func (rb *txRingBuffer) Get() ([]byte, bool) { - if rb.Used() != 0 { - rb.tail.Set(rb.tail.Get() + 1) - size := rb.buffer[rb.tail.Get()%txRingBufferSize].size - return rb.buffer[rb.tail.Get()%txRingBufferSize].buf[:size], true - } - return nil, false -} - -// Clear resets the head and tail pointer to zero. -func (rb *txRingBuffer) Clear() { - rb.head.Set(0) - rb.tail.Set(0) -} diff --git a/src/machine/usb/cdc/cdc.go b/src/machine/usb/cdc/cdc.go index f180535df1..44bd7f7e0e 100644 --- a/src/machine/usb/cdc/cdc.go +++ b/src/machine/usb/cdc/cdc.go @@ -1,3 +1,5 @@ +//go:build baremetal + package cdc const ( @@ -9,10 +11,7 @@ const ( // New returns USBCDC struct. func New() *USBCDC { if USB == nil { - USB = &USBCDC{ - rxBuffer: NewRxRingBuffer(), - txBuffer: NewTxRingBuffer(), - } + USB = &USBCDC{} } return USB } diff --git a/src/machine/usb/cdc/ring.go b/src/machine/usb/cdc/ring.go new file mode 100644 index 0000000000..9d7396e60b --- /dev/null +++ b/src/machine/usb/cdc/ring.go @@ -0,0 +1,99 @@ +package cdc + +import "sync/atomic" + +// ring512 is an interrupt/concurrent-safe ring buffer for a single-producer, +// single-consumer (SPSC) pair. The writer calls Put, the reader calls +// Peek/Discard. Reset may only be called when neither side is active. +// +// Implementation uses monotonic counters (head/tail) instead of bounded +// offsets. This eliminates full/empty ambiguity and removes all CAS +// recovery paths. Unsigned subtraction (head - tail) always yields +// correct used count regardless of uint32 overflow. +type ring512 struct { + buf [ringBufLen]byte // power of 2 so compiler can optimize & mask. + // head counts total bytes written. Only the writer stores to head. + head atomic.Uint32 + // tail counts total bytes read. Only the reader stores to tail. + tail atomic.Uint32 +} + +const ( + ringBufLen = 512 + ringMask = ringBufLen - 1 // 0x1FF +) + +// Reset empties the ring buffer. Must not be called concurrently with +// Put, Peek, or Discard. +func (r *ring512) Reset() { + r.head.Store(0) + r.tail.Store(0) +} + +// Free returns number of bytes that can be written via Put. +func (r *ring512) Free() uint32 { + return ringBufLen - r.Used() +} + +// Used returns number of bytes ready to be peeked/discarded. +func (r *ring512) Used() uint32 { + return r.head.Load() - r.tail.Load() +} + +// Peek returns a contiguous view into the readable portions of the buffer +// without advancing the read position. When data wraps around the end of +// the internal buffer, two segments are returned. Second data2 is nil on fully contiguous buffer. +// Returns nil,nil when empty. +func (r *ring512) Peek() (data1, data2 []byte) { + head, tail := r.lims() + used := head - tail + if used == 0 { + return nil, nil + } + pos := tail & ringMask + contig := ringBufLen - pos + if contig >= used { + return r.buf[pos : pos+used], nil + } + return r.buf[pos:], r.buf[:used-contig] +} + +// Discard marks numBytes as read, advancing the read position. +// Panics if numBytes exceeds Used (indicates a race violating SPSC) +func (r *ring512) Discard(numBytes uint32) { + if numBytes == 0 { + return + } + head, tail := r.lims() + used := head - tail + if numBytes > used { + panic("ring: discard exceeds used") + } + r.tail.Store(tail + numBytes) +} + +// Put writes data into the ring buffer. Returns true if all data was +// written, false if insufficient free space (no partial writes). +func (r *ring512) Put(data []byte) bool { + wlen := uint32(len(data)) + if wlen == 0 { + return true + } + head, tail := r.lims() + used := head - tail + free := uint32(ringBufLen) - used + if wlen > free { + return false + } + pos := head & ringMask + n := uint32(copy(r.buf[pos:], data)) + if n < wlen { + copy(r.buf[:], data[n:]) + } + r.head.Store(head + wlen) + return true +} + +func (r *ring512) lims() (head, tail uint32) { + return r.head.Load(), r.tail.Load() +} diff --git a/src/machine/usb/cdc/ring_test.go b/src/machine/usb/cdc/ring_test.go new file mode 100644 index 0000000000..e79581a5b4 --- /dev/null +++ b/src/machine/usb/cdc/ring_test.go @@ -0,0 +1,658 @@ +package cdc + +import ( + "bytes" + "fmt" + "math/rand" + "sync" + "testing" +) + +// peekAll returns all readable data by concatenating both Peek segments. +func peekAll(r *ring512) []byte { + d1, d2 := r.Peek() + if len(d2) == 0 { + return d1 + } + out := make([]byte, len(d1)+len(d2)) + copy(out, d1) + copy(out[len(d1):], d2) + return out +} + +// drain reads all data from the ring, verifying Peek length matches Used. +func drain(t *testing.T, r *ring512) []byte { + t.Helper() + d1, d2 := r.Peek() + n := uint32(len(d1) + len(d2)) + if n != r.Used() { + t.Fatalf("Peek returned %d bytes but Used()=%d", n, r.Used()) + } + var out []byte + out = append(out, d1...) + out = append(out, d2...) + r.Discard(n) + return out +} + +// --- Basic Functionality --- + +func TestRing512_PutPeekDiscard(t *testing.T) { + var r ring512 + data := []byte("hello world") + if !r.Put(data) { + t.Fatal("Put failed on empty buffer") + } + got := peekAll(&r) + if !bytes.Equal(got, data) { + t.Fatalf("Peek = %q, want %q", got, data) + } + if r.Used() != uint32(len(data)) { + t.Fatalf("Used = %d, want %d", r.Used(), len(data)) + } + r.Discard(uint32(len(data))) + if r.Used() != 0 { + t.Fatalf("Used after full discard = %d, want 0", r.Used()) + } + d1, d2 := r.Peek() + if d1 != nil || d2 != nil { + t.Fatalf("Peek after full discard = (%v, %v), want (nil, nil)", d1, d2) + } +} + +func TestRing512_Reset(t *testing.T) { + var r ring512 + r.Put([]byte("data")) + r.Reset() + if r.Used() != 0 { + t.Fatalf("Used after Reset = %d", r.Used()) + } + if r.Free() != 512 { + t.Fatalf("Free after Reset = %d", r.Free()) + } +} + +func TestRing512_PutEmpty(t *testing.T) { + var r ring512 + if !r.Put(nil) { + t.Fatal("Put nil should succeed") + } + if !r.Put([]byte{}) { + t.Fatal("Put empty slice should succeed") + } + if r.Used() != 0 { + t.Fatalf("Used = %d after empty puts", r.Used()) + } +} + +func TestRing512_PutFull(t *testing.T) { + var r ring512 + data := make([]byte, 512) + for i := range data { + data[i] = byte(i) + } + if !r.Put(data) { + t.Fatal("Put 512 bytes failed on empty buffer") + } + if r.Free() != 0 { + t.Fatalf("Free after filling = %d", r.Free()) + } + if r.Put([]byte{0x42}) { + t.Fatal("Put on full buffer should fail") + } + got := peekAll(&r) + if !bytes.Equal(got, data) { + t.Fatalf("Peek full buffer: got len %d, want 512", len(got)) + } +} + +func TestRing512_PutExactFit(t *testing.T) { + var r ring512 + data := make([]byte, 512) + for i := range data { + data[i] = byte(i) + } + if !r.Put(data) { + t.Fatal("Put exact fit failed") + } + if r.Used() != 512 { + t.Fatalf("Used = %d, want 512", r.Used()) + } + r.Discard(512) + if r.Used() != 0 { + t.Fatal("buffer not empty after discard all") + } +} + +// --- Full buffer with wrapped position --- + +func TestRing512_FullBufferWrapped(t *testing.T) { + var r ring512 + + r.Put(make([]byte, 200)) + r.Discard(100) // tail=100, head=200, used=100 + + free := r.Free() + if free != 412 { + t.Fatalf("Free = %d, want 412", free) + } + fill := make([]byte, free) + for i := range fill { + fill[i] = byte(i) + } + if !r.Put(fill) { + t.Fatalf("Put(%d) into %d free space failed", free, free) + } + if r.Used() != 512 { + t.Fatalf("Used = %d, want 512 (full)", r.Used()) + } + if r.Free() != 0 { + t.Fatalf("Free = %d, want 0 (full)", r.Free()) + } + drained := drain(t, &r) + if len(drained) != 512 { + t.Fatalf("drained %d bytes, want 512", len(drained)) + } +} + +// --- Wrapping Tests --- + +func TestRing512_Wrap(t *testing.T) { + var r ring512 + r.Put(make([]byte, 500)) + r.Discard(490) // tail=490, head=500, used=10 + + wrapData := make([]byte, 30) + for i := range wrapData { + wrapData[i] = byte(i + 100) + } + if !r.Put(wrapData) { + t.Fatal("wrapped Put failed") + } + if r.Used() != 40 { + t.Fatalf("Used = %d, want 40", r.Used()) + } + + d1, d2 := r.Peek() + if len(d1)+len(d2) != 40 { + t.Fatalf("Peek total = %d, want 40", len(d1)+len(d2)) + } + if d2 == nil { + t.Fatal("expected wrapped data in d2") + } + drained := drain(t, &r) + if len(drained) != 40 { + t.Fatalf("drained %d bytes, want 40", len(drained)) + } +} + +func TestRing512_WrapDataIntegrity(t *testing.T) { + var r ring512 + r.Put(make([]byte, 500)) + r.Discard(500) + + data := make([]byte, 100) + for i := range data { + data[i] = byte(i) + } + if !r.Put(data) { + t.Fatal("wrapped put failed") + } + got := drain(t, &r) + if !bytes.Equal(got, data) { + t.Fatal("data integrity failure across wrap") + } +} + +// --- Edge Cases --- + +func TestRing512_DiscardPartial(t *testing.T) { + var r ring512 + r.Put([]byte("abcdefgh")) + r.Discard(3) + got := peekAll(&r) + if !bytes.Equal(got, []byte("defgh")) { + t.Fatalf("after partial discard, Peek = %q, want %q", got, "defgh") + } +} + +func TestRing512_DiscardZero(t *testing.T) { + var r ring512 + r.Discard(0) + r.Put([]byte("hi")) + r.Discard(0) + if r.Used() != 2 { + t.Fatalf("Used = %d after zero discard", r.Used()) + } +} + +func TestRing512_DiscardPanicOnOverread(t *testing.T) { + var r ring512 + r.Put([]byte("hi")) + defer func() { + if rec := recover(); rec == nil { + t.Fatal("expected panic on over-discard, got none") + } + }() + r.Discard(100) +} + +func TestRing512_FreeUsedInvariant(t *testing.T) { + var r ring512 + check := func(label string) { + if r.Free()+r.Used() != 512 { + t.Fatalf("%s: Free(%d) + Used(%d) != 512", label, r.Free(), r.Used()) + } + } + check("empty") + r.Put(make([]byte, 200)) + check("after put 200") + r.Discard(50) + check("after discard 50") + r.Put(make([]byte, 362)) + check("after fill to full") + r.Discard(512) + check("after drain") +} + +func TestRing512_PutOversize(t *testing.T) { + var r ring512 + if r.Put(make([]byte, 513)) { + t.Fatal("Put(513) should fail on empty 512 buffer") + } + r.Put(make([]byte, 1)) + if r.Put(make([]byte, 512)) { + t.Fatal("Put(512) should fail with 1 byte used") + } +} + +func TestRing512_MultiplePutPeekDiscard(t *testing.T) { + var r ring512 + for i := 0; i < 2000; i++ { + msg := []byte(fmt.Sprintf("msg%04d", i)) + if !r.Put(msg) { + t.Fatalf("Put failed at iteration %d, Free=%d, Used=%d", i, r.Free(), r.Used()) + } + got := drain(t, &r) + if !bytes.Equal(got, msg) { + t.Fatalf("iter %d: got %q, want %q", i, got, msg) + } + } +} + +func TestRing512_HeadTailOverflow(t *testing.T) { + var r ring512 + near := uint32(0xFFFFFFFF - 100) + r.head.Store(near) + r.tail.Store(near) + + if r.Used() != 0 || r.Free() != 512 { + t.Fatalf("Used=%d Free=%d, want 0/512", r.Used(), r.Free()) + } + + for i := 0; i < 300; i++ { + data := []byte{byte(i), byte(i + 1), byte(i + 2)} + if !r.Put(data) { + t.Fatalf("Put failed at iter %d (head=%d tail=%d)", i, r.head.Load(), r.tail.Load()) + } + got := drain(t, &r) + if !bytes.Equal(got, data) { + t.Fatalf("iter %d: data mismatch", i) + } + } +} + +// --- Peek two-segment tests --- + +func TestRing512_PeekNoWrap(t *testing.T) { + var r ring512 + r.Put([]byte("hello")) + d1, d2 := r.Peek() + if !bytes.Equal(d1, []byte("hello")) { + t.Fatalf("d1 = %q, want %q", d1, "hello") + } + if d2 != nil { + t.Fatalf("d2 = %v, want nil", d2) + } +} + +func TestRing512_PeekWrapped(t *testing.T) { + var r ring512 + r.Put(make([]byte, 508)) + r.Discard(508) // tail=508, head=508 + + data := []byte("abcdefghij") // 10 bytes: 4 at end, 6 at start + if !r.Put(data) { + t.Fatal("put failed") + } + d1, d2 := r.Peek() + if len(d1) != 4 { + t.Fatalf("d1 len = %d, want 4", len(d1)) + } + if len(d2) != 6 { + t.Fatalf("d2 len = %d, want 6", len(d2)) + } + var got []byte + got = append(got, d1...) + got = append(got, d2...) + if !bytes.Equal(got, data) { + t.Fatalf("got %q, want %q", got, data) + } +} + +func TestRing512_PeekEmpty(t *testing.T) { + var r ring512 + d1, d2 := r.Peek() + if d1 != nil || d2 != nil { + t.Fatalf("Peek on empty = (%v, %v), want (nil, nil)", d1, d2) + } +} + +func TestRing512_PeekTotalEqualsUsed(t *testing.T) { + var r ring512 + // Test at many wrap positions. + for offset := 0; offset < 512; offset += 37 { + r.Reset() + if offset > 0 { + r.Put(make([]byte, offset)) + r.Discard(uint32(offset)) + } + sz := 200 + r.Put(make([]byte, sz)) + d1, d2 := r.Peek() + total := len(d1) + len(d2) + if total != sz { + t.Fatalf("offset=%d: Peek total=%d, want %d", offset, total, sz) + } + } +} + +// --- Concurrent SPSC Test --- + +func TestRing512_SPSC(t *testing.T) { + for trial := 0; trial < 20; trial++ { + var r ring512 + const totalBytes = 1 << 18 + produced := make([]byte, totalBytes) + for i := range produced { + produced[i] = byte(i + trial) + } + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + sent := 0 + for sent < totalBytes { + chunkSize := 1 + rand.Intn(128) + if sent+chunkSize > totalBytes { + chunkSize = totalBytes - sent + } + if r.Put(produced[sent : sent+chunkSize]) { + sent += chunkSize + } + } + }() + + consumed := make([]byte, 0, totalBytes) + go func() { + defer wg.Done() + for len(consumed) < totalBytes { + d1, d2 := r.Peek() + n := len(d1) + len(d2) + if n == 0 { + continue + } + consumed = append(consumed, d1...) + consumed = append(consumed, d2...) + r.Discard(uint32(n)) + } + }() + + wg.Wait() + if !bytes.Equal(consumed, produced) { + for i := range consumed { + if i >= len(produced) || consumed[i] != produced[i] { + t.Fatalf("trial %d: mismatch at byte %d", trial, i) + } + } + t.Fatalf("trial %d: length mismatch: got %d want %d", trial, len(consumed), len(produced)) + } + } +} + +func TestRing512_SPSCSmallChunks(t *testing.T) { + var r ring512 + const totalBytes = 1 << 16 + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + for i := 0; i < totalBytes; i++ { + for !r.Put([]byte{byte(i)}) { + } + } + }() + + consumed := make([]byte, 0, totalBytes) + go func() { + defer wg.Done() + for len(consumed) < totalBytes { + d1, d2 := r.Peek() + n := len(d1) + len(d2) + if n == 0 { + continue + } + consumed = append(consumed, d1...) + consumed = append(consumed, d2...) + r.Discard(uint32(n)) + } + }() + + wg.Wait() + for i, b := range consumed { + if b != byte(i) { + t.Fatalf("mismatch at %d: got %d want %d", i, b, byte(i)) + } + } +} + +// --- Fuzz Testing --- + +type refRing struct{ data []byte } + +func (r *refRing) Put(d []byte) bool { + if len(r.data)+len(d) > 512 { + return false + } + r.data = append(r.data, d...) + return true +} +func (r *refRing) Discard(n uint32) { r.data = r.data[n:] } +func (r *refRing) Used() uint32 { return uint32(len(r.data)) } + +// FuzzRing512 compares ring512 against a trivially correct reference. +func FuzzRing512(f *testing.F) { + f.Add([]byte{0, 10, 1, 5, 2, 0, 10, 1, 10}) + f.Add([]byte{0, 0}) + f.Add([]byte{0, 255, 0, 255, 1, 255, 1, 255}) + f.Add(bytes.Repeat([]byte{0, 64, 1, 64}, 50)) + f.Add([]byte{0, 200, 1, 100, 0, 156, 0, 156}) + + f.Fuzz(func(t *testing.T, ops []byte) { + var ring ring512 + var ref refRing + + i := 0 + for i+1 < len(ops) { + op := ops[i] % 4 + arg := ops[i+1] + i += 2 + + switch op { + case 0: // Put + size := int(arg) + if size > 512 { + size = 512 + } + data := make([]byte, size) + for j := range data { + data[j] = byte(j) + } + gotOK := ring.Put(data) + refOK := ref.Put(data) + if gotOK != refOK { + t.Fatalf("Put(%d): ring=%v ref=%v", size, gotOK, refOK) + } + + case 1: // Discard + used := ring.Used() + if used != ref.Used() { + t.Fatalf("Used mismatch: ring=%d ref=%d", used, ref.Used()) + } + if used == 0 { + continue + } + n := uint32(arg) % (used + 1) + ring.Discard(n) + ref.Discard(n) + + case 2: // Peek + verify + rUsed := ring.Used() + if rUsed != ref.Used() { + t.Fatalf("Used mismatch: ring=%d ref=%d", rUsed, ref.Used()) + } + if rUsed == 0 { + d1, d2 := ring.Peek() + if d1 != nil || d2 != nil { + t.Fatal("Peek non-nil on empty ring") + } + continue + } + got := peekAll(&ring) + if uint32(len(got)) != rUsed { + t.Fatalf("Peek returned %d bytes, Used=%d", len(got), rUsed) + } + if !bytes.Equal(got, ref.data) { + t.Fatal("Peek data mismatch") + } + + case 3: // Invariant + if ring.Free()+ring.Used() != 512 { + t.Fatalf("invariant: Free(%d)+Used(%d)!=512", ring.Free(), ring.Used()) + } + } + } + + if ring.Used() != ref.Used() { + t.Fatalf("final Used mismatch: ring=%d ref=%d", ring.Used(), ref.Used()) + } + }) +} + +// FuzzRing512_Op2 uses raw fuzz bytes as an operation stream with +// data integrity tracking. +func FuzzRing512_Op2(f *testing.F) { + f.Add([]byte{0, 255, 0, 255, 0, 2, 1, 255}) + f.Add([]byte{0, 200, 1, 100, 0, 255, 0, 157, 1, 255, 1, 255}) + seed := make([]byte, 40) + for i := range seed { + if i%4 < 2 { + seed[i] = 0 + } else { + seed[i] = 1 + } + if i%2 == 1 { + seed[i] = byte(3 + i%13) + } + } + f.Add(seed) + + f.Fuzz(func(t *testing.T, ops []byte) { + const buflen = 512 + const maxOps = 128 + var ring ring512 + var written []byte + totalRead := 0 + + i := 0 + nops := 0 + for i+1 < len(ops) && nops < maxOps { + op := ops[i] % 3 + sz := int(ops[i+1]) + i += 2 + nops++ + + switch op { + case 0: // Put + if sz == 0 { + continue + } + free := int(ring.Free()) + if sz > free { + sz = free + } + if sz == 0 { + continue + } + data := make([]byte, sz) + for j := range data { + data[j] = byte(len(written) + j) + } + if !ring.Put(data) { + t.Fatalf("Put(%d) failed with Free()=%d", sz, free) + } + written = append(written, data...) + + case 1: // Read + used := int(ring.Used()) + if used == 0 || sz == 0 { + continue + } + if sz > used { + sz = used + } + d1, d2 := ring.Peek() + // Concatenate and take sz bytes. + var got []byte + if sz <= len(d1) { + got = d1[:sz] + } else { + got = make([]byte, sz) + copy(got, d1) + copy(got[len(d1):], d2) + } + expect := written[totalRead : totalRead+sz] + if !bytes.Equal(got, expect) { + t.Fatalf("data mismatch at read offset %d", totalRead) + } + ring.Discard(uint32(sz)) + totalRead += sz + + case 2: // Reset + ring.Reset() + totalRead = len(written) + } + + if ring.Free()+ring.Used() != buflen { + t.Fatalf("invariant: Free(%d)+Used(%d)!=%d", ring.Free(), ring.Used(), buflen) + } + if int(ring.Used()) != len(written)-totalRead { + t.Fatalf("Used()=%d expected %d", ring.Used(), len(written)-totalRead) + } + } + + // Final drain. + d1, d2 := ring.Peek() + var remaining []byte + remaining = append(remaining, d1...) + remaining = append(remaining, d2...) + expect := written[totalRead:] + if !bytes.Equal(remaining, expect) { + t.Fatalf("final drain mismatch: got %d bytes, want %d", len(remaining), len(expect)) + } + }) +} diff --git a/src/machine/usb/cdc/usbcdc.go b/src/machine/usb/cdc/usbcdc.go index 5b5ffbf7c4..b9e68369a2 100644 --- a/src/machine/usb/cdc/usbcdc.go +++ b/src/machine/usb/cdc/usbcdc.go @@ -1,10 +1,13 @@ +//go:build baremetal + package cdc import ( "errors" "machine" "machine/usb" - "runtime/interrupt" + "sync/atomic" + _ "unsafe" ) var ( @@ -21,64 +24,56 @@ type cdcLineInfo struct { lineState uint8 } -// Read from the RX buffer. -func (usbcdc *USBCDC) Read(data []byte) (n int, err error) { - // check if RX buffer is empty - size := usbcdc.Buffered() - if size == 0 { - return 0, nil - } +// USBCDC is the USB CDC aka serial over USB interface. +type USBCDC struct { + tx ring512 + rx ring512 + inflight atomic.Uint32 + rbuf [1]byte + wbuf [1]byte +} - // Make sure we do not read more from buffer than the data slice can hold. - if len(data) < size { - size = len(data) - } +var ( + // USB is a USB CDC interface. + USB *USBCDC - // only read number of bytes used from buffer - for i := 0; i < size; i++ { - v, _ := usbcdc.ReadByte() - data[i] = v - } + usbLineInfo = cdcLineInfo{115200, 0x00, 0x00, 0x08, 0x00} +) - return size, nil +// Read from the RX buffer. +func (usbcdc *USBCDC) Read(data []byte) (n int, err error) { + data1, data2 := usbcdc.rx.Peek() + n += copy(data, data1) + n += copy(data[n:], data2) + usbcdc.rx.Discard(uint32(n)) + return n, nil } // ReadByte reads a single byte from the RX buffer. // If there is no data in the buffer, returns an error. func (usbcdc *USBCDC) ReadByte() (byte, error) { // check if RX buffer is empty - buf, ok := usbcdc.rxBuffer.Get() - if !ok { - return 0, ErrBufferEmpty + b, _ := usbcdc.rx.Peek() + if len(b) > 0 { + c := b[0] + usbcdc.rx.Discard(1) + return c, nil } - return buf, nil + return 0, ErrBufferEmpty } // Buffered returns the number of bytes currently stored in the RX buffer. func (usbcdc *USBCDC) Buffered() int { - return int(usbcdc.rxBuffer.Used()) + return int(usbcdc.rx.Used()) } // Receive handles adding data to the UART's data buffer. // Usually called by the IRQ handler for a machine. func (usbcdc *USBCDC) Receive(data byte) { - usbcdc.rxBuffer.Put(data) + usbcdc.rbuf[0] = data + usbcdc.rx.Put(usbcdc.rbuf[:]) } -// USBCDC is the USB CDC aka serial over USB interface. -type USBCDC struct { - rxBuffer *rxRingBuffer - txBuffer *txRingBuffer - waitTxc bool -} - -var ( - // USB is a USB CDC interface. - USB *USBCDC - - usbLineInfo = cdcLineInfo{115200, 0x00, 0x00, 0x08, 0x00} -) - // Configure the USB CDC interface. The config is here for compatibility with the UART interface. func (usbcdc *USBCDC) Configure(config machine.UARTConfig) error { return nil @@ -86,32 +81,63 @@ func (usbcdc *USBCDC) Configure(config machine.UARTConfig) error { // Flush flushes buffered data. func (usbcdc *USBCDC) Flush() { - mask := interrupt.Disable() - if b, ok := usbcdc.txBuffer.Get(); ok { - machine.SendUSBInPacket(cdcEndpointIn, b) - } else { - usbcdc.waitTxc = false + for usbcdc.tx.Used() > 0 { + gosched() } - interrupt.Restore(mask) } // Write data to the USBCDC. func (usbcdc *USBCDC) Write(data []byte) (n int, err error) { - if usbLineInfo.lineState > 0 { - mask := interrupt.Disable() - usbcdc.txBuffer.Put(data) - if !usbcdc.waitTxc { - usbcdc.waitTxc = true - usbcdc.Flush() + n = len(data) + if usbLineInfo.lineState <= 0 { + return n, nil + } + for len(data) > 0 { + tosend := min(len(data), int(usbcdc.tx.Free())) + if tosend == 0 { + gosched() + continue } - interrupt.Restore(mask) + usbcdc.tx.Put(data[:tosend]) + data = data[tosend:] + usbcdc.kickTx() } - return len(data), nil + return n, nil +} + +// kickTx starts a transfer if none is in flight. Called from main context only. +func (usbcdc *USBCDC) kickTx() { + if usbcdc.inflight.Load() > 0 { + return // txhandler will chain the next packet. + } + usbcdc.sendFromRing() +} + +func (usbcdc *USBCDC) txhandler() { + inflight := usbcdc.inflight.Load() + usbcdc.inflight.Store(0) + usbcdc.tx.Discard(inflight) + usbcdc.sendFromRing() +} + +// 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. +func (usbcdc *USBCDC) sendFromRing() { + d1, _ := usbcdc.tx.Peek() + if len(d1) == 0 { + 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. func (usbcdc *USBCDC) WriteByte(c byte) error { - usbcdc.Write([]byte{c}) + usbcdc.wbuf[0] = c + usbcdc.Write(usbcdc.wbuf[:]) return nil } @@ -124,9 +150,8 @@ func (usbcdc *USBCDC) RTS() bool { } func cdcCallbackRx(b []byte) { - for i := range b { - USB.Receive(b[i]) - } + free := USB.rx.Free() + USB.rx.Put(b[:min(len(b), int(free))]) } var cdcSetupBuff [cdcLineInfoSize]byte @@ -187,5 +212,8 @@ func cdcSetup(setup usb.Setup) bool { func EnableUSBCDC() { machine.USBCDC = New() - machine.EnableCDC(USB.Flush, cdcCallbackRx, cdcSetup) + machine.EnableCDC(USB.txhandler, cdcCallbackRx, cdcSetup) } + +//go:linkname gosched runtime.Gosched +func gosched()