Zephyr Ring Buffer: why is no wrap-around happening?

Hi, I have a BLE device in Observer role - it will constantly scan for Broadcasters broadcasting our unique UUID and put these scanned devices into a Ring Buffer.

The reason I thought of using a Ring Buffer is because newly scanned devices should replace old ones - which seemed appropriate because a Ring Buffer automatically does wrapping: so let's say I make it large enough to hold 10 scanned devices --- when the 11th device is found, it should replace the 1st device (because it is "obsolete") in the Ring Buffer.

I went through the documentation and thought of using the "claim" API which said that it did not support that kind of wrapping:

This boundary is invisible to the user using the normal put/get APIs, but becomes a barrier to the “claim” API, because obviously no contiguous region can be returned that crosses the end of the buffer. This can be surprising to application code, and produce performance artifacts when transfers need to happen close to the end of the buffer, as the number of calls to claim/finish needs to double for such transfers.

So I decided to try the normal put/get APIs as mentioned, but the ring_buf_put function does not cause a wrap around with some test code:

#include <string.h>

#include <zephyr/kernel.h>
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);

#define RING_BUFF_SIZE 10
RING_BUF_DECLARE(my_ring_buf, RING_BUFF_SIZE);

int main(void)
{
int nbytes = 0;
int tbytes = 0;

nbytes = ring_buf_put(&my_ring_buf, "Hello", 5);
tbytes += nbytes;
LOG_INF("%d bytes written to RB (%d total so far).", nbytes, tbytes);

nbytes = ring_buf_put(&my_ring_buf, "World", 5);
tbytes += nbytes;
LOG_INF("%d bytes written to RB (%d total so far).", nbytes, tbytes);

nbytes = ring_buf_put(&my_ring_buf, "FooBar", 6);
tbytes += nbytes;
LOG_INF("%d bytes written to RB (%d total so far).", nbytes, tbytes);

nbytes = ring_buf_put(&my_ring_buf, "FooBar", 6);
tbytes += nbytes;
LOG_INF("%d bytes written to RB (%d total so far).", nbytes, tbytes);

return 0;
}

Output:

SEGGER J-Link V7.96e - Real time terminal output
SEGGER J-Link V12.0, SN=822000525
Process: JLinkExe
*** Booting nRF Connect SDK v3.5.99-ncs1 ***
[00:00:00.417,175] <inf> main: 5 bytes written to RB (5 total so far).
[00:00:00.417,175] <inf> main: 5 bytes written to RB (10 total so far).
[00:00:00.417,175] <inf> main: 0 bytes written to RB (10 total so far).
[00:00:00.417,205] <inf> main: 0 bytes written to RB (10 total so far).
[00:00:00.417,236] <inf> main: Ring Buffer:
                               48 65 6c 6c 6f 57 6f 72  6c 64 00 00 00 00 00 00 |HelloWor ld.......

Here the RB is 10 bytes large and accepts the first 5 + 5 bytes ("Hello" + "World"), but then I also want to insert "FooBar" and make it wrap around to the first byte so that  "HelloWorld" becomes "FooBarorld". But that does not seem to work. Is this possible to do? if not, would you recommend some other data structure that would work better for this situation? Thanks.

  • The function `ring_buf_put()` returns the number of bytes written to the ringbuffer. In your case the ringbuffer gets full after the first 2 writes and therefore you will not be able to write anything more to it before you remove something from it.

    A side note: You can build and run the above example on your machine instead by building for the `native_sim` board. It is very powerful.

    Noe, I am not sure exactly what you want to accomplish but to me it sounds that a ringbuffer is not ideal for your use case.

    If you want to keep track of some devices, perhaps count number of packets from them or whatever, then just use a table. I shared a code snippet for that in this thread:

    Scan does not shows short/long names for HR peripheral(s) - Nordic Q&A - Nordic DevZone - Nordic DevZone

    The code snippet looks like this:

    #define NAME_LEN 30
    
    typedef struct {
      bool used;
      bt_addr_le_t address;
      char name[NAME_LEN];
      int64_t last_seen;
    } device_t;
    
    static device_t s_devices[50];
    
    device_t *find_device(const bt_addr_le_t *address)
    {
    	for (ssize_t i = 0; i < ARRAY_SIZE(s_devices); i++) {
    		if (s_devices[i].used && bt_addr_le_eq(&s_devices[i].address, address)) {
    			return &s_devices[i];
    		}
    	}
    	return NULL;
    }
    
    device_t *add_device(const bt_addr_le_t *address)
    {
    	for (ssize_t i = 0; i < ARRAY_SIZE(s_devices); i++) {
    		if (!s_devices[i].used) {
    			s_devices[i].used = true;
    			s_devices[i].address = *address;
    			s_devices[i].last_seen = k_uptime_get();
    			return &s_devices[i];
    		}
    	}
    	return NULL;
    }

    I leave as an exercise to you to write a function that goes through the table and sets `used` to `false` if a device has not been heard for a while.

    Good luck!

  • I think you misunderstood, or the documentation is not clear enough.

    The ring buffer supports wraparound but only if the space is free when wrapping around.

    At this line in ring_buf_item_put, there is an explicit check to see if the new item is going to a free space or not. In other words, overwriting is not allowed.

  • Hi, thanks for the clarification. I definitely misunderstood the intended use case of the ring buffer API. I studied the code you linked a little more and am now using a ringbuffer somewhere else in my code (for offloading UART data processing).

  • Hi, this  seems like a way more comprehensive approach than the simple scan + dump routine that I had in mind.  I ditched the ring buffer idea and am using a k_fifo to send the devices from the ble-scan-callback to the main thread. I'll try putting the table logic in main. Thanks a lot for taking the time to write such an informative response!

Related