jamesmunns / bbqueue

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

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Usage with circular DMA mode

romixlab opened this issue · comments

After setting up circular DMA on STM32F4 for receiving lots of data from USART, I thought, why not use a bbqueue and make the code a little bit safer.

I've come up with the following idea - create a buffer of let's say 32 bytes, obtain a write grant to the first half (16 bytes) and start the DMA from wgr.as_ptr() but with a transfer size of 32 bytes. When half transfer IRQ occurs, commit the first half, obtain a write grant for the second half and notify consumer after that. On transfer complete IRQ commit the second half, obtain the grant for the first half again, notify the consumer.

In theory this should work, if consumer will be fast enough to eat half of the buffer and release it. If it is not fast enough, write grant will fail, DMA could be stopped right away (I think this can even be done fast enough, so that DMA will not overwrite non processed data). Then in USART Idle interrupt DMA will be restarted, if a write grant to the beginning of the buffer will succeed.

So I went ahead and put together this structure (for storing it in RTIC resources for the DMA task):

pub struct DmaRxContext<N: generic_array::ArrayLength<u8>>
{
    producer: Producer<'static, N>,
    first_half_wgr: Option<GrantW<'static, N>>,
    second_half_wgr: Option<GrantW<'static, N>>,
    // ... pointers to dma, usart, etc
}

I've omitted non-relevant parts of the code for clarity.

On DMA interrupt, this code get's executed.

impl<N: generic_array::ArrayLength<u8>> DmaRxContext<N> {
    pub fn handle_dma_rx_irq(&mut self) {
        let (half_complete, complete) = self.dma_status();
        if half_complete {
            if self.first_half_wgr.is_some() {
                let wgr = self.first_half_wgr.take().unwrap();
                wgr.commit(N::USIZE / 2);
                self.first_half_wgr = None;
            }
            match self.producer.grant_exact(N::USIZE / 2) {
                Ok(wgr) => {
                    self.second_half_wgr = Some(wgr);
                    rprintln!(=>5, "DMA:F->S\n");
                },
                Err(_) => {
                    rprintln!(=>5, "DMA:ErrorA\n");
                }
            }
        } else if complete {
            if self.second_half_wgr.is_some() {
                let wgr = self.second_half_wgr.take().unwrap();
                wgr.commit(N::USIZE / 2);
                self.second_half_wgr = None;
            }
            match self.producer.grant_exact(N::USIZE / 2) {
                Ok(wgr) => {
                    self.first_half_wgr = Some(wgr);
                    rprintln!(=>5, "DMA:S->F\n");
                },
                Err(_) => {
                    rprintln!(=>5, "DMA:ErrorB\n");
                }
            }
        }
    }
}

Now the tricky part. I'm sending bytes one by one to better see what's going on. The first 16 bytes arrives, first half write grant get's committed, write grant for the second halt is successfully obtained and stored. Consumer eats 16 bytes and releases the read grant. Another 16 bytes arrive, second half write grant is committed, BUT grant_exact() gives an error this time. Even though the first 16 bytes is clearly released from the consumer.

After wasting a lot of time trying to understand wha't going on, I've noticed that grant_max_remaining(16) actually can succeed, and gives out 15 bytes instead. But why 15? I'm clearly missing something here.. Separately from all this, bbqueue works just fine, so most likely this is my mistake somewhere, or some pretty intricate bug.

Another possible solution is to use DMA in regular mode, but then one will have to obtain new write grants and restart a DMA pretty fast, or some data might get lost. Approx 8us for 1Mbps, assuming DMA priority is the highest one and not a lot of streams are in use this is quite a bit of time. Although this seem's like a strange solution, when hardware is perfectly fine on it's own.

} else {
// Not inverted, but need to go inverted
// NOTE: We check read > 1, NOT read >= 1, because
// write must never == read in an inverted condition, since
// we will then not be able to tell if we are inverted or not
if read > 1 {
sz = min(read - 1, sz);
0
} else {
// Not invertible, no space
inner.write_in_progress.store(false, Release);
return Err(Error::InsufficientSize);
}

pub struct ConstBBBuffer<A> {
buf: UnsafeCell<MaybeUninit<A>>,
/// Where the next byte will be written
write: AtomicUsize,
/// Where the next byte will be read from
read: AtomicUsize,
/// Used in the inverted case to mark the end of the
/// readable streak. Otherwise will == sizeof::<self.buf>().
/// Writer is responsible for placing this at the correct
/// place when entering an inverted condition, and Reader
/// is responsible for moving it back to sizeof::<self.buf>()
/// when exiting the inverted condition
last: AtomicUsize,
/// Used by the Writer to remember what bytes are currently
/// allowed to be written to, but are not yet ready to be
/// read from
reserve: AtomicUsize,
/// Is there an active read grant?
read_in_progress: AtomicBool,
/// Is there an active write grant?
write_in_progress: AtomicBool,
/// Have we already split?
already_split: AtomicBool,
}

Data is available between the read and write indexes. When read == write, the buffer is empty. The buffer can't actually ever be "full" because then read == write, so the maximum capacity is actually capacity - 1.

So DMA length * 2 + 1 should work just fine in this configuration?

Yes, as far as I know.

One other thing to note @romixlab, you need to be careful if you have a wraparound case in between your halves, you will walk off the end of the ring.

If you are strictly always doing half-and-half, you should be fine, but in that case (with a fixed size), you might be better off just using a regular double/ping-pong buffer.

Thank you for the clarification!