A ring buffer is also known as a queue or a circular buffer and is a common form of a queue. It's a popular, easy-to-implement standard, and although represented as a circle, it's linear in the underlying code. The ring queue exists as a fixed length array with two pointers, one representing the head of the queue and the other representing the tail. The disadvantage of the method is its fixed size. For queues where elements must be added and removed in the middle, and not just at the beginning and end of the buffer, a linked list implementation is the preferred approach.
Theoretical foundations of the buffer
It is easier for the user to make the choice of an efficient array structure after understanding the underlying theory. A circular buffer is a data structure where an array is processed and rendered in cycles, i.e. indexes return to 0 after reaching the length of the array. This is done with twoarray pointers: "head" and "tail". When data is added to the buffer, the header pointer moves up. Similarly, when they are removed, the tail also moves up. The definition of the head, tail, direction of their movement, places of writing and reading depend on the implementation of the scheme.
Circular buffers are overused to solve consumer problems. That is, one thread of execution is responsible for the production of data, and the other for the consumption. In very low to medium embedded devices, the producer is represented as ISR (sensor information) and the consumer is represented as the main event loop.
A feature of circular buffers is that they are implemented without the need for locks in the environment of one producer and one consumer. This makes them an ideal information structure for embedded programs. The next difference is that there is no exact way to differentiate a filled sector from an empty one. This is because in both cases the head merges with the tail. There are many ways and workarounds to deal with this, but most of them are confusing and hard to read.
Another question that comes up about the circular buffer. Do I need to flush new data or overwrite existing ones when it's full? Experts argue that there is no clear advantage of one over the other, and its implementation depends on the specific situation. If the latter are more relevant to the application, the overwrite method is used. On the other hand, if they are processed infirst-come-first-served mode, then discard new ones when the ring buffer is full.
Implementation of a circular queue
Starting implementation, define data types, and then methods: core, push and pop. The "push" and "pop" procedures calculate the "next" offset points for the location where the current write and read will occur. If the next location points to the tail, then the buffer is full and no more data is being written. Similarly, when "head" is equal to "tail", it is empty and nothing is read from it.
Standard use case
Helper procedure called by the application process to retrieve data from the Java ring buffer. It must be included in critical sections if more than one thread is reading the container. The tail moves to the next offset before the information is read because each block is one byte and the same amount is reserved in the buffer when the volume is fully loaded. But in more advanced circular storage implementations, single partitions do not have to be the same size. In such cases, they try to keep even the last byte by adding more checks and boundaries.
In such schemes, if the tail moves before being read, information that needs to be read can potentially be overwritten by newly pushed data. In general, it is recommended to read first and then move the tail pointer. First determine the length of the buffer, and then create an instance"circ_bbuf_t" and assign the pointer "maxlen". In this case, the container must be global or be on the stack. So, for example, if you need a 32-byte ring buffer, do the following in your application (see the figure below).
Functional Requirements Specification
The "ring_t" data type will be a data type that contains a pointer to the buffer, its size, header and tail index, data counter.
The "ring_init()" initialization function initializes a buffer based on receiving a pointer to a container structure created by the calling function, which has a predefined size.
The "ring_add()" call add function will add a byte to the next available space in the buffer.
The ring_remove() function will remove a byte from the oldest valid location in the container.
Ring peek in the "ring_peek()" function will read the number of bytes "uint8_t 'count'" from the ring buffer into the new one provided as a parameter without deleting any values read from the container. It will return the number of bytes actually read.
The "ring_clear()" function to clear the ring will set "Tail" to "Head" and load "0" into all buffer positions.
Creating a buffer in C/C ++
Due to the resource constraints of embedded systems, circular buffer data structures can be found in most fixed-size designs, which act as if memory were inherently contiguous and circular. The data does not need to be rearranged because the memoryis generated and used, and the head/tail pointers are adjusted. During the creation of a circular buffer library, you want users to work with the library APIs, and not change the structure directly. Therefore, the encapsulation of the ring buffer in "C" is used. This way the developer will keep the library implementation, modifying it as needed, without requiring end users to also update it.
Users cannot work with the "circular_but_t" pointer, a handle type is created that can be used instead. This will eliminate the need to cast the pointer in the implementation of the ".typedefcbuf_handle_t" function. Developers need to build an API for the library. They interact with the "C" ring buffer library using an opaque handle type that is created during initialization. Usually choose "uint8_t" as the base data type. But any particular type can be used, with care to properly handle the underlying buffer and number of bytes. Users interact with the container by following mandatory procedures:
- Initialize the container and its size.
- Reset the circular container.
- Add data to the ring buffer in "C".
- Get the next value from the container.
- Request information about the current number of items and maximum capacity.
Both "full" and "empty" cases look the same: "head" and "tail", pointers are equal. There are two approaches to distinguish between full and empty:
- Full state tail + 1==head.
- Empty state head==tail.
Implementation of library functions
To create a circular container, use its structure to manage the state. To preserve encapsulation, the structure is defined inside the library ".c" file, not in the header. When installing, you will need to track:
- Base data buffer.
- Max size.
- Current position of the head, increasing when added.
- Current tail that grows larger when removed.
- Flag indicating whether the container is full or not.
Now that the container is designed, the library functions are implemented. Each of the APIs requires an initialized buffer descriptor. Instead of littering your code with conditional statements, use assertions to enforce API requirements in style.
An implementation will not be thread-safe unless locks have been added to the underlying circular storage library. To initialize the container, the API has clients that provide the base buffer size, so they create it on the library side, for example, for simplicity, "malloc". Systems that cannot use dynamic memory should change the "init" function to use a different method, such as allocating from a static container pool.
Another approach is to break encapsulation, allowing users tostatically declare container structures. In this case, "circular_buf_init" needs to be updated to take a pointer or "init", create a stack structure, and return it. However, because the encapsulation is broken, users will be able to change it without library routines. After the container is created, the values are filled in and "reset" is called. Before returning from "init", the system ensures that the container is created in an empty state.
Adding and deleting data
Adding and deleting data from the buffer requires manipulating the "head" and "tail" pointers. When added to a container, insert the new value at the current "head" location and advance it. When removed, get the value of the current "tail" pointer and advance "tail". If you want to advance the "tail" pointer as well as "head", you need to check if inserting the value causes "full". When the buffer is already full, advance "tail" one step ahead of "head".
After the pointer has been advanced, fill in the "full" flag, checking if "head==tail" is equal. The modular use of the operator will cause "head" and "tail" to reset to "0" when the maximum size is reached. This ensures that "head" and "tail" will always be valid indexes of the underlying data container: "static void advance_pointer(cbuf_handle_t cbuf)". You can create a similar helper function thatcalled when a value is removed from the buffer.
Template class interface
In order for the C++ implementation to support any data types, execute the pattern:
- Flush the buffer to clear.
- Adding and deleting data.
- Checking full/empty state.
- Checking the current number of items.
- Checking the total container capacity.
- In order to leave no data behind after the buffer is destroyed, use C++ smart pointers to ensure that users can manipulate the data.
In this example, the C++ buffer mimics much of the C implementation logic, but the result is a much cleaner and reusable design. Also, the C++ container uses "std::mutex" to provide a thread-safe implementation. When creating a class, allocate data for the main buffer and set its size. This eliminates the overhead required with the C implementation. In contrast, the C++ constructor does not call "reset", since initial values for member variables are specified, the circular container is started in the correct state. The flush behavior returns the buffer to an empty state. In the C++ circular container implementation, "size" and "capacity" reports the number of items in the queue, not the size in bytes.
UART STM32 driver
After running the buffer, it must be integrated into the UART driver. First as a global element in the file, so you need to declare:
- "descriptor_rbd" and buffer memory "_rbmem: static rbd_t _rbd";
- "static char _rbmem".
Because this is a UART driver where each character must be 8-bit, creating a character array is allowed. If 9-bit or 10-bit mode is used, then each element must be "uint16_t". The container is calculated in such a way as to avoid data loss.
Queue modules often contain statistical information to keep track of peak usage. In the "uart_init" initialization function, the buffer must be initialized by calling "ring_buffer_init" and passing in an attribute structure with each member assigned the values in question. If it initializes successfully, the UART module is taken out of reset, the receive interrupt is enabled in IFG2.
The second function to be changed is "uart_getchar". Reading the received character from the UART peripheral is replaced by reading from the queue. If the queue is empty, the function should return -1. Next, you need to implement a UART to get the ISR. Open the header file "msp430g2553.h", scroll down to the interrupt vectors section, where you find a vector named USCIAB0RX. The naming implies that it is used by USCI modules A0 and B0. USCI A0 receive abort status can be read from IFG2. If it is set, the flag must be cleared and the data in the receive bay buffered using "ring_buffer_put".
UART data repository
Thisthe repository gives information on how to read data over UART using DMA when the number of bytes to receive is not known in advance. In the family of microcontrollers, the STM32 ring buffer can operate in different modes:
- Polling mode (no DMA, no IRQ) - the application must poll the status bits to check if a new character has been received and read it fast enough to get all the bytes. Very simple implementation, but no one uses it in real life. Cons - easy to miss received characters in data packets, only works for low baud rates.
- Interrupt Mode (No DMA) - The UART ring buffer triggers an interrupt, and the CPU jumps to the utility program to process the data received. The most common approach in all applications today, works well in the medium speed range. Cons - the interrupt routine is executed for each received character, can stop other tasks in high-performance microcontrollers with a large number of interrupts and simultaneously the operating system when a data packet is received.
- DMA mode is used to transfer data from the USART RX register to user memory in hardware. At this stage, no interaction with the application is required, except for the need to process the data received by the application. Can work with operating systems very easily. Optimized for high data rates > 1Mbps and low power applications, in case of large data packets, increasing the buffer size can improvefunctionality.
Implementation in ARDUINO
The Arduino ring buffer refers to both board design and the programming environment that is used to work. The Arduino core is an Atmel AVR series microcontroller. It is the AVR that does most of the work, and in many ways the Arduino board around the AVR represents functionality - easy-to-plug pins, USB-to-serial interface for programming and communication.
Many of the common Arduino boards currently use the ATmega 328 ring buffer, older boards used the ATmega168 and ATmega8. Boards like the Mega opt for more sophisticated options like the 1280 and similar. The faster Due and Zero, the better use ARM. There are about a dozen different named Arduino boards. They can have different amount of flash, RAM and I/O ports with AVR ring buffer.
The "roundBufferIndex" variable is used to store the current position, and when added to the buffer, the array will be limited.
These are the results of running the code. The numbers are stored in a buffer, and when they are full, they start to be overwritten. This way you can get the last N numbers.
In the previous example, an index is used to access the current buffer position because it is sufficient to explain the operation. But in general, it is normal that a pointer is used. This is the modified code to use a pointerinstead of an index. In essence, the operation is the same as the previous one, and the results obtained are similar.
High performance CAS operations
Disruptor is a high performance cross-thread messaging library developed and open sourced several years ago by LMAX Exchange. They created this software to handle huge traffic (over 6 million TPS) on their retail financial trading platform. In 2010, they surprised everyone with how fast their system could be by executing all the business logic in a single thread. While single thread was an important concept in their solution, Disruptor works in a multi-threaded environment and is based on a ring buffer - a thread in which stale data is no longer needed because fresher and more up-to-date data arrives.
In this case, either returning a false boolean value or blocking will work. If none of these solutions satisfies users, a resizable buffer can be implemented, but only when it fills up, not just when the producer reaches the end of the array. Resizing would require all elements to be moved to the newly allocated larger array if used as the underlying data structure, which is of course an expensive operation. There are many other things that make Disruptor fast, such as consuming messages in batches.
The ring buffer "qtserialport" (serial port) is inherited from QIODevice, can beused to obtain various serial information and includes all of the available serial devices. The serial port is always open in exclusive access, which means that other processes or threads cannot access the open serial port.
Ring buffers are very useful in C programming, for example, you can evaluate the byte stream coming through the UART.