Skip to Content
Course content

153: Implementing a Circular Buffer

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine a sushi conveyor belt. The chef places plates of sushi on the belt at one end, and customers pick them up at another. The belt is a loop; once a plate reaches the end of the track, it simply circles back to the start. If the chef keeps adding sushi faster than people eat it, the belt eventually fills up, and the chef has to stop or start replacing old plates. If the customers eat everything, they're just staring at an empty loop.

That's exactly how a circular buffer—or ring buffer—works in C. Instead of a physical belt, we have a fixed-size array. Instead of a chef and a customer, we have a "write pointer" (the head) and a "read pointer" (the tail). When either pointer reaches the end of the array, it doesn't crash the program or overflow into random memory; it just jumps back to index 0.

Tracking the Head and the Tail

To make this work, we can't just use a raw array. We need a structure to keep track of where we are. I usually bundle the array, the head, the tail, and the current count into a struct. This keeps things clean, especially if you end up needing multiple buffers in one project.

typedef struct {
    int buffer[8];     // Our fixed-size "belt"
    int head;          // Where the next item is written
    int tail;          // Where the next item is read from
    int count;         // How many items are currently in the buffer
} CircularBuffer;

In this setup, the head always moves forward as we add data, and the tail follows behind as we consume it. If head == tail and the count is 0, the buffer is empty. If head == tail but the count is 8, it's full.

Handling the Wrap-Around with Modulo

This is the part where most people overthink things by using if statements to check if the index has hit the limit. While that works, the "pro" way to do this is using the modulo operator (%).

When you increment the head, you do this: head = (head + 1) % SIZE;. If the size is 8 and the head is at index 7, (7 + 1) % 8 equals 0. It snaps right back to the start automatically. It's elegant, concise, and avoids a bunch of branching logic that could slow down your code.

Managing the Full-Buffer Dilemma

One of the first decisions you have to make when implementing this is: what happens when the buffer is full? You have two real options. You can either return an error (telling the producer to wait), or you can "overwrite" the oldest data. In high-speed logging or audio streaming, we usually choose the latter—we just advance the tail forward one spot to make room, effectively dropping the oldest piece of data to make way for the newest.

Here is how I'd implement a basic push and pop for this:

void push(CircularBuffer *cb, int value) {
    cb->buffer[cb->head] = value;
    
    // If we are full, the tail must move forward to "drop" the oldest item
    if (cb->count == 8) {
        cb->tail = (cb->tail + 1) % 8;
    } else {
        cb->count++;
    }
    
    cb->head = (cb->head + 1) % 8;
}

int pop(CircularBuffer *cb) {
    if (cb->count == 0) {
        return -1; // Buffer is empty
    }
    
    int value = cb->buffer[cb->tail];
    cb->tail = (cb->tail + 1) % 8;
    cb->count--;
    
    return value;
}

Notice that the pop function is much simpler. We just grab the value at the tail, nudge the tail forward, and decrement the count. I've used -1 as an error code here, but in a production environment, you'd likely use a boolean success flag or a pointer to return the value.




📋 Practical Task

Implementing a UART-style Log Buffer

In embedded systems, data often arrives from a UART (serial port) faster than the CPU can process it. To prevent data loss, we use a circular buffer to "stage" the incoming bytes.

Your task is to implement a circular buffer specifically for unsigned char (bytes). You must modify the provided template to include a function called flush_buffer. This function should not delete the data, but instead reset both the head and tail to 0 and set the count to 0, effectively clearing the buffer for a fresh start.

Requirements:

  • Create a struct named LogBuffer with a capacity of 16 bytes.
  • Implement push_byte: If the buffer is full, it should overwrite the oldest byte (advance the tail).
  • Implement pop_byte: Return the oldest byte and update the tail.
  • Implement flush_buffer: Reset the buffer state completely.
  • Write a main function that pushes 20 different bytes into the 16-byte buffer, then pops all remaining bytes to verify that only the last 16 were preserved.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.