Please, before submitting a support request read carefully this README and check if an answer already exists among previously answered questions: do not abuse of the Github issue tracker.
The library itself has an implicit memory consumption of about 0.5Kb: 580 bytes (max) of code and 8 bytes of memory, according to my calculations. That does not consider the space used to store the items themselves, obviously.
When declaring your buffer you should specify the data type it must handle and the buffer capacity: those two parameters will influence the memory consumed by the buffer.
#include <CircularBuffer.hpp>
CircularBuffer<byte,100> bytes; // uses 538 bytes
CircularBuffer<int,100> ints; // uses 638 bytes
CircularBuffer<long,100> longs; // uses 838 bytes
CircularBuffer<float,100> floats; // uses 988 bytes
CircularBuffer<double,100> doubles; // uses 988 bytes
CircularBuffer<char,100> chars; // uses 538 bytes
CircularBuffer<void*,100> pointers; // uses 638 bytes
Please note: the memory usage reported above includes the program memory used by the library code, the heap memory is much less and is comparable to an array of the same size and type of the buffer.
Let's start making things clear: the library doesn't support inserting data in the middle of the buffer.
You can add data to the buffer either before the first element via an unshift()
operation or after the last element via a push()
operation.
You can keep adding data beyond the buffer maximum capacity, but you'll lose the least significant information:
- since
unshift()
adds to the head, adding beyond capacity causes the element at tail to be overwritten and lost - since
push()
adds to the tail, adding beyond capacity causes the element at head to be overwritten and lost
Both unshift()
and push()
return true
if the addition didn't cause any information loss, false
if an overwrite occurred:
CircularBuffer<int,5> buffer; // buffer capacity is 5
// all of the following return true
buffer.unshift(1); // [1]
buffer.unshift(2); // [2,1]
buffer.unshift(3); // [3,2,1]
buffer.push(0); // [3,2,1,0]
buffer.push(5); // [3,2,1,0,5]
// buffer is now at full capacity, from now on any addition returns false
buffer.unshift(2); // [2,3,2,1,0] returns false
buffer.unshift(10); // [10,2,3,2,1] returns false
buffer.push(-5); // [2,3,2,1,-5] returns false
Similarly to data addition, data retrieval can be performed at tail via a pop()
operation or from head via a shift()
operation: both cause the element being read to be removed from the buffer.
⚠ Reading data beyond the actual buffer size has an undefined behaviour and is user's responsibility to prevent such boundary violations using the additional operations listed in the next section. The library will behave differently depending on the data type and allocation method, but you can safely assume your program will crash if you don't watch your steps.
Non-destructive read operations are also available:
first()
returns the element at headlast()
returns the element at tail- an array-like indexed read operation is also available so you can read any element in the buffer using the
[]
operator
CircularBuffer<char, 50> buffer; // ['a','b','c','d','e','f','g']
buffer.first(); // ['a','b','c','d','e','f','g'] returns 'a'
buffer.last(); // ['a','b','c','d','e','f','g'] returns 'g'
buffer.pop(); // ['a','b','c','d','e','f'] returns 'g'
buffer.pop(); // ['a','b','c','d','e'] returns 'f'
buffer.shift(); // ['b','c','d','e'] returns 'a'
buffer.shift(); // ['c','d','e'] returns 'b'
buffer[0]; // ['c','d','e'] returns 'c'
buffer[1]; // ['c','d','e'] returns 'd'
buffer[2]; // ['c','d','e'] returns 'e'
buffer[10]; // ['c','d','e'] returned value is unpredictable
buffer[15]; // ['c','d','e'] returned value is unpredictable
isEmpty()
returnstrue
only if no data is stored in the bufferisFull()
returnstrue
if no data can be further added to the buffer without causing overwrites/data losssize()
returns the number of elements currently stored in the buffer; it should be used in conjunction with the[]
operator to avoid boundary violations: the first element index is always0
(if buffer is not empty), the last element index is alwayssize() - 1
available()
returns the number of elements that can be added before saturating the buffercapacity()
returns the number of elements the buffer can store, for completeness only as it's user-defined and never changes REMOVED from1.3.0
replaced by the read-only member variablecapacity
clear()
resets the whole buffer to its initial state (pay attention though, if you had dynamically allocated objects in your buffer, memory used by such object is not released: iterate over the buffer contents and release objects accordingly to their allocation method)copyToArray(array)
copies the contents of the buffer to a standard arrayarray
. The array must be large enough to hold all the elements currently in the buffer.copyToArray(conversionFn, array)
copies the contents of the buffer to a standard arrayarray
executing a function on each element, usually a type conversion. The array must be large enough to hold all the elements currently in the buffer.
Starting from version 1.3.0
the library is capable to automatically detecting which data type should be used for the index based on the buffer capacity:
- if you declare a buffer with a capacity greater than
65535
then your index is going to be anunsigned long
unsigned int
for buffers with a declared capacity greater than255
- otherwise, a
byte
is going to suffice
In addition, you can mix in the same code buffers with small index and buffers with normal index: previously this was not possible.
CircularBuffer<char,100> optimizedBuffer; // reduced memory footprint, index type is uint8_t (a.k.a. byte)
CircularBuffer<long,500> normalBuffer; // standard memory footprint, index type is unit16_t (a.k.a. unsigned int)
CircularBuffer<int,66000> hugeBuffer; // extended memory footprint, index type is unit32_t (a.k.a. unsigned long)
To obtain the maximum advantage of the optimization above, anytime you need to refer to the buffer index you should use the most appropriate type: this can be easily achieved using the decltype
specifier, like in the following example:
// the iterator variable i is of the correct type, even if
// we don't know what's the buffer declared capacity
for (decltype(buffer)::index_t i = 0; i < buffer.size(); i++) {
avg += buffer[i] / buffer.size();
}
If you prefer, you can alias the index type and refer to such alias:
using index_t = decltype(buffer)::index_t;
for (index_t i = 0; i < buffer.size(); i++) {
avg += buffer[i] / buffer.size();
}
The following applies to versions prior to 1.3.0
only.
By default the library uses unsigned int
indexes, allowing for a maximum of 65535
items, but you'll rarely need such a huge store.
You can switch the library indexes to byte
type defining the CIRCULAR_BUFFER_XS
macro BEFORE the #include
directive: this reduces the memory used by the library itself by only 36
bytes, but allows you to potentially squeeze out much more whenever you perform an indexed access, if you do any, by using the smaller data type.
#define CIRCULAR_BUFFER_XS
#include <CircularBuffer.h>
CircularBuffer<short,100> buffer;
void setup() { }
void loop() {
// here i should be declared of type byte rather than unsigned int
// in order to maximize the effects of the optimization
for (byte i = 0; i < buffer.size() - 1; i++) {
Serial.print(buffer[i]);
}
}
Please note: this macro switch forces the buffer to use an 8 bits data type as the internal index, as such all your buffers will be limited to a maximum capacity of 255
.
The library does help to work with interrupts defining the CIRCULAR_BUFFER_INT_SAFE
macro switch, which introduces the volatile
modifier to the count
variable, making the whole library more interrupt-friendly at the price of disabling some compiler optimizations. The #define
statement needs to be put somewhere before the #include
statement:
#define CIRCULAR_BUFFER_INT_SAFE
#include <CircularBuffer.h>
CircularBuffer<unsigned long, 10> timings;
void count() {
timings.push(millis());
}
void setup() {
attachInterrupt(digitalPinToInterrupt(2), count, RISING);
}
void loop() {
Serial.print("buffer size is "); Serial.println(timings.size());
delay(250);
}
Please note this does NOT make the library interrupt safe, but it does help its usage in interrupt driven firmwares.
Multiple examples are available in the examples
folder of the library:
- CircularBuffer.ino shows how you can use the library to create a continous averaging of the most recent readings
- EventLogging.ino focuses on dumping the buffer when it becomes full and printing the buffer contents periodically at the same time
- Object.ino is meant to demonstrate how to use the buffer to store dynamic structures
- Queue.ino is a classical example of a queue, or a FIFO data structure
- Stack.ino on the other end shows how to use the library to represent a LIFO data structure
- Struct.ino answers to the question can this library store structured data?
- Interrupts.ino demonstrates the use of the library in interrupt-driven code
- Arrays.ino demonstrates the use of the
copyToArray()
functions.
If you use this library to store dynamically allocated objects, refrain from using the clear()
method as that will not perform memory deallocation: you need to iterate over your buffer content and release memory accordingly to the allocation method used, either via delete
(if you had used new
) or free
(in case of malloc
):
while (!buffer.isEmpty()) {
// pick the correct one
delete buffer.pop();
free(buffer.pop());
}
The very same applies for the pop()
and shift()
operations as any dynamically allocated object is only detached from the buffer, but the memory it uses is not automagically released (see the Object.ino example)
Record* record = new Record(millis(), sample); // a dynamically allocated object
buffer.push(record);
// somewhere else
if (!buffer.isEmpty()) {
Record* current = buffer.pop();
Serial.println(current.value());
delete current; // if you don't do this the object memory is lost!!!
}
- Resolves #52 adding two new additional methods
copyToArray(array)
andcopyToArray(array, convertFn)
- Fixes #28 adding compatibility with Nano 33 BLE by switching for
.h
header extension to.hpp
- Fixes #27 compilation error
- Fixes #2 preventing
shift()
andpop()
operations misuse to mess up the buffer - Fixes #2 preventing out of boundary access using the
[]
operator
- Fixes #21 _call to
abort()
is AVR-specific
Most of the major improvements below have been contributed by Erlkoenig90: thank you Niklas!
- Slightly reduced both flash and heap footprint
- Introduced instance based control over index data type
- Replaced method
capacity()
in favor of the constant instance attributecapacity
- Added the
EventLogging
andInterrupts
examples - Dropped the
CIRCULAT_BUFFER_XS
macro switch in favor of automatic index type identification - Added support for very large buffers (capacity can go up to
UINT32_MAX
)
- Added interrupt-related macro switch
CIRCULAR_BUFFER_INT_SAFE
- Dropped unnecessary call to
memset
when clearing
- Added tests
- Fixed
clear()
function - Fixed
pop()
function
- Improved robustness against access outside the buffer boundaries
- Fixed
pop()
andshift()
implementations - Added test sketch
- Added
capacity()
function - Added
debug()
function, disabled by pre-processor by default
- Initial implementation