Also known as: ring buffer, circular buffer, circular queue
A ring buffer (circular buffer) is a fixed-size array used as a queue, where a write index and a read index each advance and wrap back to the start when they reach the end, so the storage is reused endlessly without allocation.1 In software-defined radio it is the standard structure for handing a live IQ sample stream from the code that fills it to the code that drains it, giving each side a stable place to work while the other runs at its own pace.
How it works
A ring buffer is a plain array of length N plus two positions. The producer writes a sample at the write index and advances it; the consumer reads at the read index and advances it. Both indices are taken modulo N, so when either reaches the end it wraps to slot 0 and keeps going — the memory is a logical circle. The buffer is empty when the two indices are equal and full when the writer has caught up to one slot behind the reader, so a capacity-N array usually stores N−1 items to keep those two states distinguishable.
The reason this structure dominates streaming is that it needs no allocation and no copying of the backlog in steady state. The array is allocated once; every push and pop is a couple of index arithmetic operations. Because the producer only ever touches the write index and the consumer only the read index, the common single-producer / single-consumer (SPSC) case can be made lock-free: with the indices published through atomic loads and stores (and the right memory ordering) each side sees the other’s progress without a mutex, so the DSP thread never blocks waiting for a lock held by the USB thread.2
In practice
- Sizing. Capacity is a latency-versus-safety trade. A larger ring rides out longer scheduling stalls before it overflows, but every buffered sample is added latency. Radios are typically sized to hold tens of milliseconds of IQ.
- Contiguity. DSP kernels like to process a run of samples in one call, but a run near the end of the array wraps. Implementations either hand out the two pieces separately, or use a mirrored/virtual-memory mapping so the wrap looks contiguous — a common enabler of zero-copy reads.
- False sharing. For lock-free rings the read and write indices are padded onto separate cache lines so the two threads’ atomic updates don’t ping-pong the same line.
Relevance to SDR
An SDR front end delivers samples on the schedule of the USB or network transport — in bursty chunks, from a callback or a driver thread you do not control. The DSP downstream wants a smooth, arbitrary-sized supply. A ring buffer sits between them as the classic sample buffer: the driver’s delivery thread is the producer, the demodulator is the consumer, and the ring absorbs the timing mismatch. When the consumer falls behind and the writer laps the reader, that is an overrun; when the consumer outruns a slow producer, an underrun. Sizing and monitoring the ring is therefore how a real-time SDR keeps up with its sample rate.
GopherTrunk is a pure-Go SDR application and leans on exactly this pattern. Its device drivers stream IQ into buffers that the scanner and decoders drain independently, and the drivers expose a dropped-sample counter so a live overrun is visible rather than silent — the airspy path, for example, tracks discarded IQ chunks explicitly. Go’s channels are a higher-level bounded queue with similar decoupling semantics, but for the tight, high-rate sample path a direct ring over a slice avoids per-item overhead. The back-pressure a full ring applies is what ultimately protects the pipeline from unbounded memory growth.
Sources
-
Circular buffer — Wikipedia, on the circular-buffer data structure, wrap-around indexing, and full/empty conditions. ↩
-
Correctly implementing a lock-free SPSC queue — Erik Rigtorp, on a cache-friendly, lock-free single-producer/single-consumer ring buffer. ↩