C · 2026-06-16 · embedded · buffer · ISR · DMA · lock-free

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_SIZE a power of two so the wrap is a single AND.
  • One producer, one consumer only — multiple writers need real atomics.
  • For UART RX call fifoPut in the ISR and drain with fifoGet; at high rates prefer DMA into a circular buffer instead.