Project 12.01 Section 12 ⚡ Embedded Relevance: Critical ArrayQueue Ring Buffer Modulo Arithmetic Bounded Memory ISR Safety

12.01 Implementing Bounded FIFO Queues with Modulo Arithmetic for Non-Blocking ISRs

Executive Summary: Deep dive into circular array queue data structures. We examine index wrapping using modulo arithmetic, full/empty state disambiguation, and why array-backed ring buffers are the foundational communication pipeline for embedded UART, CAN, and SPI drivers.

💻 1. Annotated Source Code

#ifndef QUEUE_H
#define QUEUE_H

class Queue {
	virtual void enqueue(int newEntry) = 0;
	virtual int dequeue() = 0;
	virtual int peekFront() const = 0;
	virtual bool isEmpty() const = 0;
	virtual void makeEmpty() = 0;
};

#endif 
#ifndef ARRAY_QUEUE_H
#define ARRAY_QUEUE_H

#include <iostream>
#include "Queue.h"
using namespace std;

class ArrayQueue : public Queue {
	public:

		ArrayQueue(int s = 16) : MAX_SIZE(s) {
			front = 0;
			back = 0;
			numElements = 0;
			mArray = new int[MAX_SIZE];
		}//end ctor

		virtual ~ArrayQueue() {
			delete[] mArray;
		}//end dtor

		void enqueue(int newEntry) override {
			if (numElements < MAX_SIZE - 1) {
				mArray[back] = newEntry;
				back = (back + 1) % MAX_SIZE;
				numElements++;
			}
			else {
				cout << "You cannot enqueue onto a full queue" << endl;
			}
		}//end enqueue

		int dequeue() override {
			if (!isEmpty()) {
				int data = mArray[front];
				front = (front + 1) % MAX_SIZE;
				numElements--;
				return data;
			}
			else {
				cout << "You cannot dequeueon an empty queue." << endl;
				return 0;
			}
		}//end dequeue

		int peekFront() const override {
			if (!isEmpty()) {
				return mArray[front];
			}
			else {
				cout << "Queue is empty.  You cannot peek the front." << endl;
				return 0;
			}
		}//end peekFront


		bool isEmpty() const override {
			return numElements == 0;
		}//end isEmpty

		void makeEmpty() override {
			front = 0;
			back = 0;
			numElements = 0;
		}//end makeEmpty


	private:
		int* mArray;
		const int MAX_SIZE;
		int front;
		int back;
		int numElements;
};

#endif 
#include <iostream>
#include "ArrayQueue.h"
using namespace std;

int main() {
	ArrayQueue queue;

	for (int i = 1; i <= 16; i++) {
		queue.enqueue(i * 100);
	}//end for

	queue.enqueue(1234);  //should trigger an error

	while (!queue.isEmpty()) {
		cout << queue.dequeue() << endl;
	}//end while

	queue.dequeue();  //should trigger an error

	for (int i = 0; i < 20; i++) {
		cout << "Just enqueued " << (i * 10) << endl;
		queue.enqueue(i * 10);

		if (i % 3 == 0) {
			cout << "Just dequeued " << queue.dequeue() << endl;
		}
	}

	queue.enqueue(123);
	queue.enqueue(234);
	queue.enqueue(345);

	return 0;
}

📐 2. Architecture & UML Class Model

📐 ArrayQueue Fixed Circular Ring Buffer Architecture
+ Public - Private # Protected
<<interface>> Queue<T> Queue Interface Contract
(none / stateless)
+enqueue(item: const T&) : bool[pure virtual =0]
+dequeue() : bool[pure virtual =0]
+peekFront() : T const[pure virtual =0]
+isEmpty() : bool const[pure virtual =0]
+~Queue()[virtual]
<<template class>> ArrayQueue<T> Circular Ring Buffer
-items[CAPACITY] : T (Contiguous Array)
-front : int32_t = 0
-back : int32_t = CAPACITY - 1
-count : size_t = 0
+DEFAULT_CAPACITY : constexpr size_t = 5
+ArrayQueue()
+enqueue(newEntry: const T&) : bool[override, O(1)]
+dequeue() : bool[override, O(1)]
+peekFront() : T const[override, O(1)]
+isEmpty() : bool const[override, O(1)]
+isFull() : bool const[O(1)]
+size() : size_t const[O(1)]
🔗 Architectural Relationships & Hierarchy
ArrayQueue<T> - - ▷ implements interface - - ▷ Queue<T>

📚 3. Core C++ Concepts Deep-Dive

1. Abstract Interface & Array Implementation

The Queue<T> interface defines enqueue, dequeue, peek, and isEmpty. ArrayQueue<T> implements these operations in fixed contiguous memory using circular indexing.

🔄 Circular Ring Buffer FIFO Architecture

Slot 0 Slot 1 Slot 2 Slot 3 Empty Empty TAIL (Dequeue) HEAD (Enqueue Next)

2. Modulo Arithmetic Index Wrapping

Instead of shifting elements on dequeue ($O(N)$), the queue simply advances its front and rear indices using modulo arithmetic: (rear + 1) % capacity, achieving constant $O(1)$ enqueue and dequeue.

⚡ 4. Embedded Systems & Hardware Reality

1. Power-of-Two Bitmask Optimization

In high-frequency ISRs, the hardware division instruction (or software division routine on Cortex-M0) required for % capacity takes multiple clock cycles. Embedded engineers dimension ring buffers to powers of two (e.g. 64, 128, 256), replacing expensive modulo with a single-cycle bitwise AND: (index + 1) & (CAPACITY - 1)!

⚡ Hard Real-Time Advantage

Zero dynamic memory allocation, zero pointer chasing, and bounded memory consumption ensure predictable WCET (Worst-Case Execution Time).

💡 5. Production-Ready Embedded Refactoring

💡 Production-Ready Refactor
#include <array>
#include <cstdint>

template <typename T, size_t PowerOfTwoCap = 64>
class FastRingBuffer {
    static_assert((PowerOfTwoCap & (PowerOfTwoCap - 1)) == 0, "Capacity must be power of 2!");
public:
    bool enqueue(T val) {
        size_t nextHead = (head_ + 1) & MASK;
        if (nextHead == tail_) return false; // Full
        buffer_[head_] = val;
        head_ = nextHead;
        return true;
    }
    bool dequeue(T& out) {
        if (head_ == tail_) return false; // Empty
        out = buffer_[tail_];
        tail_ = (tail_ + 1) & MASK;
        return true;
    }
private:
    static constexpr size_t MASK = PowerOfTwoCap - 1;
    std::array<T, PowerOfTwoCap> buffer_;
    size_t head_ = 0, tail_ = 0;
};

📝 Knowledge Verification Quiz

Test your understanding of the C++ concepts and embedded microcontroller trade-offs covered in this guide. Click any option for instant feedback.

Q1. Why is the modulo operation `% capacity` often replaced with `& (capacity - 1)` in high-speed embedded ring buffers?
A Bitwise AND executes in a single clock cycle, whereas integer division/modulo takes significantly more CPU cycles.
B Bitwise AND automatically detects hardware parity errors.
C Modulo cannot be compiled on ARM Cortex microcontrollers.
D Bitwise AND converts numbers to floating point.
Detailed Explanation: When capacity is a power of 2, index & (capacity - 1) produces identical results to index % capacity in a single fast clock cycle.
Q2. What is the algorithmic time complexity of enqueue and dequeue in an ArrayQueue?
A O(1) constant time
B O(N) linear time
C O(log N)
D O(N^2)
Detailed Explanation: Circular array queues achieve true $O(1)$ constant time for both enqueue and dequeue because no element shifting is performed.