Usage with circular DMA mode
romixlab opened this issue · 5 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.
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!