In Chapter 18, we learned that interrupts should be kept "short." However, when data floods in via UART, a receive interrupt (RXNE) is triggered for every byte. If you try to handle all these bytes within the ISR, the ISR becomes long, and if the main loop is slow, the next byte arrives and overwrites the previous byte, causing loss. The solution is that the ISR should only put the received byte into a buffer, and the actual processing is handled by the idle main loop, dividing the work.
The key data structure for this division of labor is the ring buffer (circular buffer). If it is designed so that the producer (ISR) and consumer (main) only touch different indices, data can be safely exchanged without turning off interrupts. In this chapter, we implement a ring buffer as a hardware-independent module, verify it on a PC, and then look at how it is combined with the actual UART RX interrupt.
Learning Objectives
- Explain why data loss occurs in interrupt-based reception.
- Understand the head/tail FIFO structure of the ring buffer.
- Implement Empty/Full judgment and power-of-two masking.
- Understand the safety and
volatileof a single Producer/Consumer (SPSC) environment. - Learn the pattern of combining a ring buffer with UART RX interrupts.
Target Environment
- Compiler: PC + gcc or μVision (pure logic).
- Device: None (hardware-independent). On an actual board, the producer is the USART RX ISR.
- Verification: Check expected outputs using PC
gcc(pc_test.c).
Key Concepts
1) The Dilemma of Interrupt Reception
RX interrupt occurs for each byte. If the ISR is long or the main program doesn't fetch that byte in time, the next byte will overwrite the receive register, causing data loss. "Receive quickly and store it somewhere, process it later" is necessary.
2) Ring Buffer Structure
A fixed-size array is used in a circular manner. head (write position) and tail (read position) wrap back to 0 when they reach the end. The first data in is the first data out (FIFO).
[ . . A B C . . . ]
^tail ^head pop is at the tail, push is at the head
3) Empty/Full Determination
- Empty state:
head == tail. - Full state:
((head + 1) & MASK) == tail.
To determine fullness, one slot is left empty, so if the size is N, the actual capacity is N-1. If the size is a power of 2, it can wrap quickly using & (N-1) instead of % N.
#define RB_SIZE 8u
#define RB_MASK (RB_SIZE - 1u)
next = (head + 1) & RB_MASK; /*Automatic wrap from 8 → 0*/
4) SPSC Safety and volatile
The producer (ISR) writes only to head, and the consumer (main) writes only to tail. Since they update different variables, in a single consumer-single producer (SPSC) setup, it is safe without locks or disabling interrupts. However, since ISR and main share head/tail-is, it should be declared as volatile volatile to prevent compiler caching.
5) Combining with UART RX Interrupt (Concept)
/*USART1 receive interrupt: just put the received byte into the buffer (keep it short!)*/
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: only touches head*/
}
}
int main(void)
{
/*... USART1 + RXNEIE interrupt setup ...*/
uint8_t c;
while (1) {
if (rb_pop(&rx_rb, &c)) { /*Consumer: only touches tail*/
/*Process the received byte c leisurely*/
}
}
}
RX interrupt operates when
USART1->CR1is enabled withUSART_CR1_RXNEIE(receive interrupt enable) andNVIC_EnableIRQ(USART1_IRQn)is called. Processing is done by the main program.
Examples
Example 1 — ringbuffer.h / ringbuffer.c: Ring Buffer Module
#ifndef RINGBUFFER_H
#define RINGBUFFER_H
#include <stdint.h>
/*
*Circular buffer (ring buffer) — a FIFO connecting interrupt reception and main processing.
*The producer (ISR) only touches the head with push, and the consumer (main) only touches the tail with pop.
*Therefore, it is safe in a single producer/single consumer (SPSC) without turning off interrupts.
*
*The size must be a power of 2 so that the index can be wrapped with (& MASK).
*With the 'leave one slot empty' method, the actual capacity is RB_SIZE - 1.
*/
#define RB_SIZE 8u /*Power of 2*/
typedef struct {
uint8_t buf[RB_SIZE];
volatile uint16_t head; /*Next position to write (producer)*/
volatile uint16_t tail; /*Next position to read (consumer)*/
} ringbuffer_t;
void rb_init(ringbuffer_t *rb);
uint8_t rb_is_empty(const ringbuffer_t *rb);
uint8_t rb_is_full(const ringbuffer_t *rb);
uint8_t rb_push(ringbuffer_t *rb, uint8_t data); /*Success 1, full 0*/
uint8_t rb_pop(ringbuffer_t *rb, uint8_t *out); /*Success 1, empty 0*/
uint16_t rb_count(const ringbuffer_t *rb);
#endif /* RINGBUFFER_H */
#include "ringbuffer.h"
#define RB_MASK (RB_SIZE - 1u)
void rb_init(ringbuffer_t *rb)
{
rb->head = 0u;
rb->tail = 0u;
}
uint8_t rb_is_empty(const ringbuffer_t *rb)
{
return (rb->head == rb->tail) ? 1u : 0u;
}
uint8_t rb_is_full(const ringbuffer_t *rb)
{
return (((rb->head + 1u) & RB_MASK) == rb->tail) ? 1u : 0u;
}
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 — Rejected to prevent data 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;
}
uint16_t rb_count(const ringbuffer_t *rb)
{
return (uint16_t)((rb->head - rb->tail) & RB_MASK);
}
Example 2 — pc_test.c: FIFO / Fullness Verification
#include <stdio.h>
#include <stdint.h>
#include "ringbuffer.h"
/*
*Lesson 22 PC Verification — Ring Buffer FIFO Operation.
*Check insertion/removal, rejection when full (capacity RB_SIZE-1=7), and FIFO order.
*Build: gcc pc_test.c ringbuffer.c -o pc_test
*/
int main(void)
{
ringbuffer_t rb;
uint8_t c;
char x;
rb_init(&rb);
/*1) Insert 3 items*/
rb_push(&rb, (uint8_t)'A');
rb_push(&rb, (uint8_t)'B');
rb_push(&rb, (uint8_t)'C');
printf("after push A,B,C: count=%u empty=%u full=%u\n",
(unsigned)rb_count(&rb), (unsigned)rb_is_empty(&rb),
(unsigned)rb_is_full(&rb));
/*2) Remove one item (A comes first as per FIFO)*/
rb_pop(&rb, &c);
printf("pop -> %c, count=%u\n", c, (unsigned)rb_count(&rb));
/*3) Insert until full (capacity 7)*/
for (x = 'D'; x <= 'L'; x++) {
uint8_t ok = rb_push(&rb, (uint8_t)x);
printf("push %c -> %s (count=%u)\n",
x, ok ? "ok" : "FULL", (unsigned)rb_count(&rb));
}
/*4) Remove all — they come out in the order they went in*/
printf("drain: ");
while (rb_pop(&rb, &c)) {
putchar((int)c);
}
putchar('\n');
return 0;
}
Verification on a PC
gcc pc_test.c ringbuffer.c -o pc_test && ./pc_test
Expected output (stdout):
after push A,B,C: count=3 empty=0 full=0
pop -> A, count=2
push D -> ok (count=3)
push E -> ok (count=4)
push F -> ok (count=5)
push G -> ok (count=6)
push H -> ok (count=7)
push I -> FULL (count=7)
push J -> FULL (count=7)
push K -> FULL (count=7)
push L -> FULL (count=7)
drain: BCDEFGH
Check the FIFO operation where the capacity is 7 (size 8 − empty 1), and since A was removed first, the remaining data starts from B and comes out in the BCDEFGH order.
Common Mistakes
Q. It works fine on PC even without attaching at-the-head/tail volatile.
A. On a PC (single thread), it may appear fine, but on actual embedded systems where ISR and main share data, the compiler may cache the value and not see updates. Always attach volatile.
Q. When I set the size to 10, the wrap behaves strangely.
A. & (N-1) masking only works correctly when N is a power of 2. For 10, use % 10 or choose a power of 2 like 8 or 16.
Q. The buffer is full, but push overwrites.
A. With push using ((head+1)&MASK)==tail, it should reject (return 0). Otherwise, it may overtake the tail and overwrite data that has not been read yet.
Q. Can ISR pop while main push?
A. This SPSC safety holds only when "producer uses head only, consumer uses tail only." If roles are mixed or there are multiple producers, additional protection like interrupt blocking is needed.
Summary
- Receiving interrupts is safe if ISR puts data into the buffer, and the main retrieves and processes it.
- The ring buffer is a FIFO that operates with with-head/tail and is determined by the Empty/Full index.
- If the size is set to a power of 2, it wraps quickly with
& MASK(capacity is N−1). - In SPSC, separating head/tail +
volatileis safe without locks. - The UART RX ISR only uses
rb_push, and the main processes it afterrb_pop.
Try it yourself
Solve the assignment below. The reference solution is shown below.