Zephyr includes many built-in features like stacks for networking and BLE, Flash
storage APIs, and many kernel services. These components allow you to quickly
get up and running with a project and maintain less code! Taking advantage of
these is a huge win for small firmware teams and was a huge motivation in
bringing Zephyr to my teams.
This post covers Zephyr’s built-in ring buffer API, a component
commonly used in producer-consumer scenarios. We will cover how ring buffers
in Zephyr work, when to use them, and their strengths and weaknesses. This post
will close with an example of augmenting ring buffers with waiting capabilities.
Like Interrupt? Subscribe to get our latest posts straight to your mailbox.
Zephyr Ring Buffers
Ring buffers1 in Zephyr provide a way to pass data through a shared memory
buffer. Ring buffers are safely used in single-consumer, single-producer
scenarios. Their usage generally follows the diagram below:
The producer writes to the ring buffer via the ring_buf_put
functions, while
the reader reads from the ring buffer with the ring_buf_get
functions. Next,
we will explore the two interfaces provided: the bytes-based API and the
items-based API.
Bytes API
The ring buffer bytes API is often used in driver implementations, where a piece
of hardware needs to send chunks of data up to a higher layer for further
processing. We will look at the eswifi
driver2 as a guide for ring buffer
bytes API operation and walk through some of the driver code to understand its
use. This driver uses AT commands to interface with a WiFi module over a UART.
Let’s start with the initialization code:
struct eswifi_uart_data {
// ... rest of code
uint8_t iface_rb_buf[ESWIFI_RING_BUF_SIZE];
struct ring_buf rx_rb;
};
int eswifi_uart_init(struct eswifi_dev *eswifi)
{
// ... rest of code
ring_buf_init(&uart->rx_rb, sizeof(uart->iface_rb_buf),
uart->iface_rb_buf);
// ... rest of code
}
We can see from this initialization that we have two components to set up. The
structure containing the state of the ring buffer object,
eswifi_uart_data.rx_rb
, and the underlying shared buffer,
eswifi_uart_data.iface_rb_buf
. Next, let’s examine how the driver writes to
the ring buffer:
static void eswifi_iface_uart_isr(const struct device *uart_dev,
void *user_data)
{
struct eswifi_uart_data *uart = &eswifi_uart0; /* Static instance */
int rx = 0;
uint8_t *dst;
uint32_t partial_size = 0;
uint32_t total_size = 0;
ARG_UNUSED(user_data);
while (uart_irq_update(uart->dev) &&
uart_irq_rx_ready(uart->dev)) {
if (!partial_size) {
partial_size = ring_buf_put_claim(&uart->rx_rb, &dst,
UINT32_MAX);
}
if (!partial_size) {
LOG_ERR("Rx buffer doesn't have enough space");
eswifi_iface_uart_flush(uart);
break;
}
rx = uart_fifo_read(uart->dev, dst, partial_size);
if (rx <= 0) {
continue;
}
dst += rx;
total_size += rx;
partial_size -= rx;
}
ring_buf_put_finish(&uart->rx_rb, total_size);
}
When the MCU receives data over the UART from the WiFi module, the UART
peripheral runs the interrupt handler, eswifi_iface_uart_isr
. Next, the
interrupt handler repeatedly checks for available data. If data is available,
the driver calls ring_buf_put_claim
. The put_claim
operation reserves up to
the provided size in the ring buffer for writing and returns the number of bytes
claimed. A return of 0 indicates the ring buffer is full. The write operation
then becomes a simple copy into the shared buffer. After copying the data, the
handler calls ring_buf_put_finish
to signal completion.
If the ring buffer is full (i.e. the put_claim
returns 0), the driver drops
the current batch of data received because we cannot block to wait for new data
in this interrupt. Resolving this issue may require increasing the ring buffer
size to account for pressure on the ring buffer. Problems like these can be hard
to trace the specific instance when the buffer fills completely. One way to
determine the proper size is to add an assertion when the put_claim
fails to
reserve any data. Asserting in this manner is a technique of offensive
programming3, as this modification proactively looks for an issue and fails
purposefully.
The main driver work is done within the context of a dedicated driver workqueue.
The workqueue operates as a dedicated cooperative thread that sleeps until new
work is submitted. In our case, the work starts with requests sent to the WiFi
module and ends with responses sent back. The code below shows how the function,
eswifi_uart_get_resp
reads data from the ring buffer to parse responses from
the WiFi module:
static int eswifi_uart_get_resp(struct eswifi_uart_data *uart)
{
uint8_t c;
while (ring_buf_get(&uart->rx_rb, &c, 1) > 0) {
LOG_DBG("FSM: %c, RX: 0x%02x : %c",
get_fsm_char(uart->fsm), c, c);
if (uart->rx_buf_size > 0) {
uart->rx_buf[uart->rx_count++] = c;
if (uart->rx_count == uart->rx_buf_size) {
return -ENOMEM;
}
}
// ... rest of code
}
// ... rest of code
}
Two things stand out compared to the code that wrote data to the ring buffer.
The first is the usage of ring_buf_get
instead of the
=span>
Read More