Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 33 additions & 10 deletions uartx/ringbuffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,44 @@

//go:build atmega || esp || nrf || sam || sifive || stm32 || k210 || nxp || rp2040 || rp2350

// An API-compatible replacement for machine.RingBuffer with an added Size() method.
// Methods and semantics match TinyGo's implementation.
// RingBuffer is the single-producer/single-consumer byte queue that sits
// between the UART interrupt handler and the foreground reader. It used to
// be an API-compatible replacement for TinyGo's machine.RingBuffer, whose
// default capacity is 128 bytes. That default is too small for interrupt-
// driven use at typical bauds (see the file-level comment on bufferSize);
// the size and index width here have been raised so the ring can absorb
// longer foreground stalls without silently dropping bytes.

package uartx

import "runtime/volatile"

// Choose a power-of-two size for efficient modulo.
const bufferSize uint8 = 128
// bufferSize is the per-instance RX/TX ring capacity in bytes.
//
// At 115200 baud a byte arrives every ~87 us, so 128 bytes is only about
// 11 ms of headroom between the ISR producing and the foreground consuming.
// Any stall longer than that (a flash page program, a GC pause, a heavy
// scheduled task) causes the ISR's Put to start failing and bytes are lost
// with no visible symptom other than downstream data corruption.
//
// 4096 bytes gives ~355 ms of headroom at 115200 (and scales linearly with
// baud), which is large enough to absorb any realistic stall on the MCUs
// this package currently targets. The cost is 2 * bufferSize bytes of
// static RAM per UART (one RX, one TX); on RP2040/RP2350 with hundreds of
// KB of SRAM this is negligible.
//
// bufferSize must remain a power of two so `(h+1) % bufferSize` compiles
// to a bitmask.
const bufferSize uint16 = 4096

// RingBuffer is a byte ring buffer compatible with TinyGo's machine.RingBuffer.
// RingBuffer is a byte ring buffer. The head/tail indices are uint16 so
// the capacity can exceed 255 bytes without wraparound ambiguity; the
// existing head-minus-tail arithmetic still works because uint16
// subtraction is modulo 2^16 which is a multiple of bufferSize.
type RingBuffer struct {
rxbuffer [bufferSize]volatile.Register8
head volatile.Register8
tail volatile.Register8
head volatile.Register16
tail volatile.Register16
}

// NewRingBuffer returns a new ring buffer.
Expand All @@ -25,13 +48,13 @@ func NewRingBuffer() *RingBuffer {
}

// Size returns the total capacity of the buffer in bytes.
func (rb *RingBuffer) Size() uint8 {
func (rb *RingBuffer) Size() uint16 {
return bufferSize
}

// Used returns how many bytes in buffer have been used.
func (rb *RingBuffer) Used() uint8 {
return uint8(rb.head.Get() - rb.tail.Get())
func (rb *RingBuffer) Used() uint16 {
return uint16(rb.head.Get() - rb.tail.Get())
}

// Put stores a byte in the buffer. If the buffer is already full, it returns false.
Expand Down
22 changes: 20 additions & 2 deletions uartx/rp2_uart.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"errors"
"machine"
"runtime/interrupt"
"runtime/volatile"
)

// UART represents a single PL011 instance on RP2040/RP2350.
Expand All @@ -38,6 +39,15 @@ type UART struct {
notify chan struct{} // coalesced RX readiness notifications

baud uint32 // last configured baud (for diagnostics, not used by HW)

// Drop counters incremented from the RX ISR and read from foreground
// context via RXDrops. Single writer (the ISR) plus 32-bit aligned
// reads means no locking is required on a single-core MCU. On a
// dual-core MCU these are still race-free for Cortex-M0+/M33 because
// aligned 32-bit loads and stores are naturally atomic; the counters
// may however read slightly stale in that case.
rxHwDrops volatile.Register32 // bytes with PL011 error bits set (OE/BE/PE/FE)
rxSwDrops volatile.Register32 // bytes the ISR could not enqueue because Buffer was full
}

// Configure sets up the PL011, its pins and interrupts. It leaves RXIM/RTIM
Expand Down Expand Up @@ -303,13 +313,21 @@ func (uart *UART) handleInterrupt(interrupt.Interrupt) {
r := uart.Bus.UARTDR.Get()
if (r & (rp.UART0_UARTDR_OE | rp.UART0_UARTDR_BE |
rp.UART0_UARTDR_PE | rp.UART0_UARTDR_FE)) != 0 {
// Drop errored byte; reading DR clears the per-byte error flags.
// Drop errored byte; reading DR clears the per-byte error
// flags. UARTDR_OE specifically means the hardware FIFO
// overflowed before this ISR got here, so this counts as
// an observable drop from the consumer's point of view.
uart.rxHwDrops.Set(uart.rxHwDrops.Get() + 1)
continue
}
if uart.Buffer.Put(byte(r & 0xFF)) {
enq++
} else {
// optional rxDrops++
// Software RX ring is full: the foreground hasn't drained
// it in time. Count the drop so the application can
// surface it; silently losing bytes here is what motivated
// the ring resize and these counters.
uart.rxSwDrops.Set(uart.rxSwDrops.Get() + 1)
}
}

Expand Down
16 changes: 16 additions & 0 deletions uartx/uartx.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,19 @@ func (u *UART) TxFree() int { return int(u.TxBuffer.Size() - u.TxBuffer.Used())
func (uart *UART) Receive(data byte) {
uart.Buffer.Put(data)
}

// RXDrops returns the cumulative number of RX bytes that have been dropped
// since boot. hw counts bytes that arrived from the PL011 with any of its
// per-byte error bits set (OE, BE, PE, FE); the most informative of these
// is OE, which means the hardware FIFO overflowed before the ISR serviced
// it. sw counts bytes the ISR could not enqueue because the software RX
// ring was full. Both counters are monotonic and wrap at 2^32.
//
// In a healthy steady state both counters should stay at zero at the
// package's supported bauds. A nonzero hw count indicates ISR latency
// exceeded one character time at the configured baud; a nonzero sw count
// indicates the application's foreground consumer is not draining the
// ring fast enough.
func (uart *UART) RXDrops() (hw, sw uint32) {
return uart.rxHwDrops.Get(), uart.rxSwDrops.Get()
}