>// ...
> T data[LENGTH];
I'm not sure how important it is in practice, but I'm pretty sure you don't zero out the whole array for sizeof(T) > 1.
Anyhow, memsetting to 0 a complex type is... not something I'd recommend in most cases.
I haven't checked the code for correctness, but in a typical ring buffer implementation intended to be used in interrupts, you would make the read and write pos volatile.
To write, you put the value in the array, and then advance the write position. To read, you copy a value out, and then advance the read position. Volatile ensures that if a read is interrupted by a write or vice versa, the entire operation is still atomic. Without volatile, the compiler has more freedom to reorder memory access.
/* * @brief Retrieve a continuous block of * valid buffered data. * @param num_reads_requested How many reads are required. * @param skip Whether to increment the read position * automatically, (false for manual skip * control) * @return A block of items containing the maximum * number the buffer can provide at this time. / Block<T> Read(unsigned int num_reads_requested)
Where is skip?
> if (buffer_full) > { > /
> * Tried to append a value while the buffer is full. > / > overrun_flag = true; > } > else > { > / > * Buffer isn't full yet, write to the curr write position > * and increment it by 1. > / > overrun_flag = false; > data[write_position] = value; > write_position = (write_position + 1U) % LENGTH; > }You don't write in the case of an "overrun". Isn't that one of the most interesting property of a ringbuffer? It seems that your implementation is specific to your use case (buffering to sd cards?). I don't think your _current_ implementation is apropriate for a _general_ purpose ring buffer mostly because of api concerns. It may be interesting to emphasis this part in your doc.
> reads_to_end = LENGTH - read_position; > req_surpasses_buffer_end = num_reads_requested > reads_to_end; > > if (req_surpasses_buffer_end) > { > /
> * If the block requested exceeds the buffer end. Then > * return a block that reaches the end and no more. > / > block.SetStart(&(data[read_position])); > block.SetLength(reads_to_end); > } > else > { > / > * If the block requested does not exceed 0 > * then return a block that reaches the number of reads required. > */ > block.SetStart(&(data[read_position])); > block.SetLength(num_reads_requested); > }Maybe : > reads_to_end = LENGTH - read_position; > eff_reads = (num_reads_requested > reads_to_end) ? reads_to_end : num_reads_requested; > //or : eff_reads = std::min(num_reads_requested, reads_to_end) > block.SetStart(&(data[read_position])); > block.SetLength(eff_reads);
Still, I understand the need to be very explicit in an embedded context.
Same principle for ``if (!bridges_zero)`` (the ``else`` case)
Sample implementation in C++17 here:
https://github.com/stanford-stagecast/audio/blob/main/src/ut...
https://github.com/stanford-stagecast/audio/blob/main/src/ut...
You don't even need to double-map memory. Just make the ring buffer smaller than your actual buffer, and treat any overrun as if it had wrapped around, but really just write past the (official) end. You might waste a bit of space at the front when you do, just to keep the code simple, but if you didn't have some room to waste, you wouldn't be using a ring buffer.
Another way to simplify management is to give the ring buffer a power-of-two size, and make the head and tail counters 64 bits, masking off the high bits when actually looking at the buffer. They only ever increase.
volatile T member;
T access( void ) { return member; }
Use volatile on the functions instead: T member;
T access( void ) volatile { return member; }
This has causes all access to this-> within the function to be volatile, including read_position, write_position, overrun_flag and data[].When inlining these functions within other parts of the code, the compiler can hoist reads into registers from the non-volatile members unless you add the compiler barrier that @ghhhhhk8899jj mentioned.
[1] https://cpp4arduino.com/2018/10/26/why-cpp-programmers-dont-...
GCC used to use a magic builtin in pre C++11 to implement NULL, but IIRC they removed it as non-conforming.
> Are there any drawbacks to using nullptr instead of NULL? No, unless you target old compilers that don’t support C++11, which is very unlikely.
and OP is specifically targeting c++98.
But confining yourself to C++98 is just masochism. Just because we're embedded doesn't mean we can't have nice things.
>data[write_position] = value;
>COMPILER_BARRIER(); // __asm__ volatile("":::"memory");
>write_position = (write_position + 1U) % LENGTH;
and same in Skip()
A specular barrier is needed on the reader side.