← Back to Embedded C series
🔌
Applications
Applied · Prereq: lesson 21

22. Ring Buffer and Interrupt-Driven UART Receive

When UART data pours in, a receive interrupt fires per byte. Handling each fully inside the ISR makes it long, and a slow main lets the next byte overwrite the previous — data loss. The fix: the ISR only enqueues; the idle main does the processing. The key data structure is the ring buffer. If producer (ISR) and consumer (main) touch different indices, it's safe without disabling interrupts.

ring bufferFIFOUARTinterruptSPSCvolatile
Duration
~1.5–2 hours
Level
📊 Advanced
Prerequisite
🎯 Lesson 21
OUTCOME
Implement a head/tail FIFO ring buffer with power-of-two masking and combine it safely with a UART RX interrupt via single-producer/consumer (SPSC) + volatile.

What you'll learn

  • 1Explain why interrupt receive loses data
  • 2Understand the ring buffer's head/tail FIFO structure
  • 3Implement empty/full checks and power-of-two masking
  • 4Understand SPSC safety and volatile
  • 5Know how to combine the ring buffer with a UART RX interrupt

Introduction

The RX interrupt fires per byte. If the ISR is long or main doesn't fetch in time, the next byte overwrites the receive register and is lost. You need "receive fast, stash somewhere, process later".

Key concepts

1) Structure and empty/full checks

Use a fixed array circularly. head (write)·tail (read) wrap to 0 at the end (FIFO). Empty is head==tail; full is ((head+1)&MASK)==tail. Leaving one slot for the full check means capacity is N-1 for size N.

c
#define RB_SIZE 8u
#define RB_MASK (RB_SIZE - 1u)
next = (head + 1) & RB_MASK;   /* fast wrap with & if power-of-two */

2) SPSC safety and volatile

The producer (ISR) writes only head, the consumer (main) only tail. Since they update different variables, single-producer/single-consumer (SPSC) is safe without locks or disabling interrupts. But head/tail are shared, so declare them volatile.

3) Combining with the UART RX interrupt

c
void USART1_IRQHandler(void) {
    if (USART1->SR & USART_SR_RXNE) {
        uint8_t b = (uint8_t)USART1->DR;   /* reading clears RXNE */
        rb_push(&rx_rb, b);                /* producer: head only */
    }
}
/* main: if (rb_pop(&rx_rb, &c)) { ...process leisurely... }  (consumer: tail only) */
ℹ️

Enable USART_CR1_RXNEIE in CR1 and call NVIC_EnableIRQ(USART1_IRQn) for the RX interrupt. Main does the processing.

Core example

c
uint8_t rb_push(ringbuffer_t *rb, uint8_t data) {
    uint16_t next = (uint16_t)((rb->head + 1u) & RB_MASK);
    if (next == rb->tail) return 0u;          /* full — reject (avoid loss) */
    rb->buf[rb->head] = data; rb->head = next; return 1u;
}
uint8_t rb_pop(ringbuffer_t *rb, uint8_t *out) {
    if (rb->head == rb->tail) return 0u;       /* empty */
    *out = rb->buf[rb->tail];
    rb->tail = (uint16_t)((rb->tail + 1u) & RB_MASK); return 1u;
}

Verify: with size 8 (capacity 7), the 8th push is rejected as FULL, and popping returns in insertion order (FIFO). Build: gcc pc_test.c ringbuffer.c -o pc_test.

Common mistakes

Q. It works on PC without volatile on head/tail.

A. On a single-threaded PC the problem hides, but in real embedded where ISR·main share them, the compiler caches and misses updates. Always add volatile.

Q. Size 10 makes wrap behave oddly.

A. & (N-1) masking only works when N is a power of two. Use % 10, or pick 8·16.

Q. push overwrites when the buffer is full.

A. push must reject (return 0) when ((head+1)&MASK)==tail. Otherwise it overtakes tail and overwrites unread data.

Summary

  • Interrupt receive is safe when the ISR enqueues and main dequeues to process
  • A ring buffer is a head/tail FIFO; empty/full are decided by indices
  • A power-of-two size wraps fast with & MASK (capacity N-1)
  • SPSC is safe without locks via head/tail separation + volatile
  • The UART RX ISR only rb_push; main rb_pop then processes

Exercises

  1. Confirm rb_count() returns the correct count even in wrap cases
  2. Grow the buffer to size 16 and fix MASK accordingly
  3. Add line_assemble() that gathers bytes up to a newline (\n) into one line
Example code / lecture materials

All lecture materials and example code (with homework and answers) are openly available on GitHub.

View on GitHub ↗