mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-07-26 06:38:42 +00:00
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:
committed by
deadprogram
parent
6bb6ea2432
commit
86cc3b6c12
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"device/sam"
|
||||
"errors"
|
||||
"internal/binary"
|
||||
|
||||
"runtime/interrupt"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user