jamesmunns / bbqueue

A SPSC, lockless, no_std, thread safe, queue, based on BipBuffers

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Ability to read_exact so that producer can fill up the rest of the buffer

ninjasource opened this issue · comments

Hi @jamesmunns,

Awesome project! I am using BBQueue to shuffle audio data from a producer loop that is temporally unstable (some audio frames take longer to process than others) into a buffer and then reading that data at very critically timed intervals (inside an I2S interrupt) to pass it to a digital to analog converter. I've tried to distill the essence of this setup in the code below.

   // runs forever
    fn producer_task(producer: &mut Producer<'_, 1000>) {
        loop {
            match producer.grant_exact(100) {
                Ok(mut grant) => {
                    grant.to_commit(100);
                    // get audio bytes and copy them into write grant
                }
                Err(_) => {
                    // Input queue full, this is normal
                    // the i2s interrupt firing should free up space
                    cortex_m::asm::wfi();
                }
            }
        }
    }

    // fired by interrupt every 10 ms
    fn consumer_task(previous_grant: GrantR<'_, 1000>, consumer: &mut Consumer<'_, 1000>) {
        previous_grant.release(100);

        match consumer.read() {
            Ok(grant) => {
                long_running_dma_copy(grant);
            }
            Err(_) => {
                // no audio to play - play silence
            }
        }
    }

The long running consumer read operation holds the read grant for all the data available to read when it only really needs to read a small chunk here (100 bytes). This seems to put undue back-pressure on the producer resulting in the whole thing operating more like a turnstyle than a buffer.

My current solution to this problem is to use a double buffer and copy the data out of the BBQueue read grant and release the grant immediately. The double buffer is needed because there is only really enough time to switch pointers in this I2S interrupt handler or the audio will glitch.

My question is this, would it be possible to hold a read grant for only a small window of the data (say 100 bytes out of 1000) so that the producer can continue to write data to the queue elsewhere. Like a read_exact() function on the consumer. This may not be possible due to how read and write pointers work in bip buffers. However, if you think it is possible and worthwhile adding then I would like to attempt to implement it.

PS, I took a look at the framed stuff but that pesky frame length header messes up my alignment!

Hey, not sure I totally understand, so feel free to correct any missed assumptions!

In general - holding a read grant does not affect the producer. The producer will block only when the buffer is full, though the existence of data will block overwriting, whether you have a grant or not.

Unlike some other ringbuffers, bbqueue does not allow you to overwrite the oldest data automatically - once it is full, it will remain full until you read + release the data it contains.

Could you maybe explain a bit more about what you want to happen, and why it isn't possible currently?

Thanks for your insight, that helped! I understand the read grant a little better now so the problem was between the chair and the keyboard. Feel free to close the issue. :)

I was confused by the fact that the read() function returns a GrantR<TOTAL_BUFFER_SIZE> (thinking that the entire buffer was locked) but didn't realize that the len() of that grant is only the number of continuous committed bytes until the end of the buffer which makes a lot more sense and is kind-of obvious come to think of it. It's in the documentation too I see. I read the docs but didn't really comprehend until I played around with the interface a little. The examples work with one byte at a time which is clean and neat but perhaps not what newcomers will use and maybe others will struggle with chunks of bytes like I did. Let's hope this post is useful for them. I have come to realize that when working with constant size chunks of bytes in my use case it is better to use split_read() instead of read() as I was mistaking circular buffer wrap-around for an empty queue and inserting periodic silence into my audio.

Here is a working example if anyone finds this useful: https://github.com/ninjasource/nrf52840-dk-i2s-demo

Hey! I appreciate you following up! Even if you did find the docs when you knew what to look for, if you have any suggestions for how to make this more clear for the next person (when they DON'T know what to look for), I'm very open to ideas on improving the docs!

I find that docs are more useful when you are familiar with what the library is meant for in the first place. I found that the README was a good introduction but the reading and writing single bytes immediately confused me because I can't think of a scenario where I would ever do that so I assumed that this was just the library demonstrating that it was generic over the type, in this case a simple byte. However, because of alignment complications, the tool is meant for byte buffers only. Therefore I think the readme example could be changed to using a small buffer instead and talking about bytes instead of elements (which sounds like it is generic). Like so:

// Create a buffer with 12 bytes
let bb: BBBuffer<12> = BBBuffer::new();
let (mut prod, mut cons) = bb.try_split().unwrap();

// Request space for 4 bytes
let mut wgr = prod.grant_exact(4).unwrap();

// Set the data
wgr.copy_from_slice(&[1,1,1,1]);

// Make the data ready for consuming
wgr.commit(4);

// Get a read grant for all available bytes
let rgr = cons.read().unwrap();

assert_eq!(rgr.len(), 4);
assert_eq!(rgr.buf(), &[1,1,1,1]);

// Release the space for later writes
rgr.release(4);

I think that the docs and code comments themselves are great but, given that there are no examples, the tests will typically be used for inspiration on how to use the library as the author intended. Sometimes this can be tricky because testing code is not the same as usage code. I think there is an argument to be made to add a test with complexity somewhere between the single_thread and the ring_around_the_senders tests. Something that demonstrates the nuances of a wrapping ring buffer where the buffer size is a multiple of a fixed buffer length that you would typically (is it typical?) use for a DMA transfer over and over again:

// a 15 byte ring buffer
let bb: BBBuffer<15> = BBBuffer::new();
let (mut prod, mut cons) = bb.try_split().unwrap();
const CHUNK_LEN: usize = 5;

// produce chunk 0
let mut grant = prod.grant_exact(CHUNK_LEN).unwrap();
grant.copy_from_slice(&[0, 0, 0, 0, 0]);
grant.commit(CHUNK_LEN);

// produce chunk 1
let mut grant = prod.grant_exact(CHUNK_LEN).unwrap();
grant.copy_from_slice(&[1, 1, 1, 1, 1]);
grant.commit(CHUNK_LEN);

// consume chunk 0
let grant = cons.read().unwrap();
assert_eq!(&[0, 0, 0, 0, 0], &grant[..CHUNK_LEN]);
grant.release(CHUNK_LEN);

// produce chunk 2
let mut grant = prod.grant_exact(CHUNK_LEN).unwrap();
grant.copy_from_slice(&[2, 2, 2, 2, 2]);
grant.commit(CHUNK_LEN);

// NOTE: even though there appears to be space in the ring buffer, we cannot use it
// because in the wrap-around case the start and end pointers cannot be the same
// because the data structure cannot tell if the queue has wrapped around or if it is full
assert_eq!(
    prod.grant_exact(CHUNK_LEN),
    Err(bbqueue::Error::InsufficientSize)
);

// consume chunk 1
let grant = cons.read().unwrap();
assert_eq!(&[1, 1, 1, 1, 1], &grant[..CHUNK_LEN]);
grant.release(CHUNK_LEN);

// produce chunk 3 (wrapped around)
// NOTE: we have to at least consume chunk 1 by this point
// because the read and write pointer cannot be at the same location
let mut grant = prod.grant_exact(CHUNK_LEN).unwrap();
grant.copy_from_slice(&[3, 3, 3, 3, 3]);
grant.commit(CHUNK_LEN);

// consume chunk 2
let grant = cons.read().unwrap();
assert_eq!(&[2, 2, 2, 2, 2], &grant[..CHUNK_LEN]);
grant.release(CHUNK_LEN);

// consume chunk 3 (wrapped around)
let grant = cons.read().unwrap();
assert_eq!(&[3, 3, 3, 3, 3], &grant[..CHUNK_LEN]);
grant.release(CHUNK_LEN);

Feel free to use the code above without attribution but I won't be offended if you don't use it. I just wanted to use it to illustrate the perspective of a noob like me!

@ninjasource Thanks, I think your comment really helped!

I realized bbqueue's readme/docs are missing a "mission statement", or "tl;dr", that bbqueue is meant to help with cases where you need one or all of: contiguous, variably sized, chunks of bytes, with a stable location for writing/reading.

I'll see if I can integrate these examples as well, I think you hit some really important points. It's hard to reset my "view" to match that of someone who didn't write the crate, so I really appreciate your perspective.

Thanks for your kind words and the replies! :)