Lock-free SPSC ring buffer (C)
A single-producer, single-consumer FIFO is the cleanest way to hand bytes from an ISR (producer) to the main loop (consumer) without disabling interrupts or taking a lock. The trick: with one writer and one reader, and a power-of-two size, the write and read indices never need a mutex — each side only ever writes its own index.
#include <stdint.h>
#include <stdbool.h>
#define FIFO_SIZE 256u /* power of two; holds SIZE-1 bytes */
#define FIFO_MASK (FIFO_SIZE - 1u)
typedef struct
{
volatile uint16_t wr;
volatile uint16_t rd;
uint8_t data[FIFO_SIZE];
} fifo_t;
/*******************************************************************************
* Function Name : fifoPut
* Description : Push one byte from the producer (e.g. the UART RX ISR).
* Input : f - pointer to the FIFO; b - byte to store.
* Output : None.
* Return : true if stored, false if the FIFO is full.
*******************************************************************************/
static inline bool fifoPut(fifo_t *f, uint8_t b)
{
uint16_t w = f->wr;
uint16_t next = (w + 1u) & FIFO_MASK;
if (next == f->rd)
{
/* full */
return false;
}
/* publish only after the byte is in */
f->data[w] = b;
f->wr = next;
return true;
}
/*******************************************************************************
* Function Name : fifoGet
* Description : Pop one byte on the consumer side (the main loop).
* Input : f - pointer to the FIFO.
* Output : b - receives the byte when one is available.
* Return : true if a byte was read, false if the FIFO is empty.
*******************************************************************************/
static inline bool fifoGet(fifo_t *f, uint8_t *b)
{
uint16_t r = f->rd;
if (r == f->wr)
{
/* empty */
return false;
}
*b = f->data[r];
f->rd = (r + 1u) & FIFO_MASK;
return true;
}
/*******************************************************************************
* Function Name : fifoUsed
* Description : Number of bytes currently held in the FIFO.
* Input : f - pointer to the FIFO.
* Output : None.
* Return : Count of stored bytes.
*******************************************************************************/
static inline uint16_t fifoUsed(const fifo_t *f)
{
return (f->wr - f->rd) & FIFO_MASK;
}
Why it’s safe without a lock
The producer only writes wr; the consumer only writes rd. Each reads the other’s
index to test full/empty. Because the byte is stored before wr advances (and read before
rd advances), a torn read can at worst make the FIFO look momentarily emptier than it is
— never corrupt. The indices are volatile so the compiler always re-reads them; on a
multi-core or weakly-ordered part add a release/acquire barrier at the two publish points
(see volatile & memory barriers).
Usage
- Keep
FIFO_SIZEa power of two so the wrap is a single AND. - One producer, one consumer only — multiple writers need real atomics.
- For UART RX call
fifoPutin the ISR and drain withfifoGet; at high rates prefer DMA into a circular buffer instead.