usb/cdc: Better ring buffer implementation (#5209)

* begin adding ring512 implementation

* refactor to make operation driven fuzz test

* refactor USBCDC.Read to use ring512

* try txhandler separate to flush

* working USBCDC with large packets

* fix binary size

* remove comment

* documentation improvements
This commit is contained in:
Pat Whittingslow
2026-03-02 00:25:56 +01:00
committed by deadprogram
parent 6bb6ea2432
commit 86cc3b6c12
8 changed files with 886 additions and 192 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ func TestBinarySize(t *testing.T) {
// microcontrollers
{"hifive1b", "examples/echo", 3668, 280, 0, 2244},
{"microbit", "examples/serial", 2694, 342, 8, 2248},
{"wioterminal", "examples/pininterrupt", 6837, 1491, 120, 6888},
{"wioterminal", "examples/pininterrupt", 7074, 1510, 120, 7248},
// TODO: also check wasm. Right now this is difficult, because
// wasm binaries are run through wasm-opt and therefore the
+29
View File
@@ -0,0 +1,29 @@
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)
}
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)
}
+1
View File
@@ -11,6 +11,7 @@ import (
"device/sam"
"errors"
"internal/binary"
"runtime/interrupt"
"unsafe"
)
-119
View File
@@ -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)
}
+3 -4
View File
@@ -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
}
+98
View File
@@ -0,0 +1,98 @@
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. 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 contiguous views 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()
}
+658
View File
@@ -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))
}
})
}
+96 -68
View File
@@ -1,10 +1,13 @@
//go:build baremetal
package cdc
import (
"errors"
"machine"
"machine/usb"
"runtime/interrupt"
"sync/atomic"
_ "unsafe"
)
var (
@@ -21,55 +24,13 @@ 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
}
// Make sure we do not read more from buffer than the data slice can hold.
if len(data) < size {
size = len(data)
}
// only read number of bytes used from buffer
for i := 0; i < size; i++ {
v, _ := usbcdc.ReadByte()
data[i] = v
}
return size, 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
}
return buf, nil
}
// Buffered returns the number of bytes currently stored in the RX buffer.
func (usbcdc *USBCDC) Buffered() int {
return int(usbcdc.rxBuffer.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 is the USB CDC aka serial over USB interface.
type USBCDC struct {
rxBuffer *rxRingBuffer
txBuffer *txRingBuffer
waitTxc bool
tx ring512
rx ring512
inflight atomic.Uint32
rbuf [1]byte
wbuf [1]byte
}
var (
@@ -79,6 +40,40 @@ var (
usbLineInfo = cdcLineInfo{115200, 0x00, 0x00, 0x08, 0x00}
)
// 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
b, _ := usbcdc.rx.Peek()
if len(b) > 0 {
c := b[0]
usbcdc.rx.Discard(1)
return c, nil
}
return 0, ErrBufferEmpty
}
// Buffered returns the number of bytes currently stored in the RX buffer.
func (usbcdc *USBCDC) Buffered() int {
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.rbuf[0] = data
usbcdc.rx.Put(usbcdc.rbuf[:])
}
// 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()
}
interrupt.Restore(mask)
n = len(data)
if usbLineInfo.lineState <= 0 {
return n, nil
}
return len(data), nil
for len(data) > 0 {
tosend := min(len(data), int(usbcdc.tx.Free()))
if tosend == 0 {
gosched()
continue
}
usbcdc.tx.Put(data[:tosend])
data = data[tosend:]
usbcdc.kickTx()
}
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()