Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to manage lifetimes with DMA #905

Open
Ralim opened this issue Mar 2, 2025 · 1 comment
Open

How to manage lifetimes with DMA #905

Ralim opened this issue Mar 2, 2025 · 1 comment

Comments

@Ralim
Copy link

Ralim commented Mar 2, 2025

Hia,

This is probably a misunderstanding; but I'm really not sure how to correctly handle the buffers used for DMA given their 'static requirement that I seem to run into.

For example, given a struct that contains the buffer to be used with DMA

#[derive(Debug, Format)]
pub struct DMAExample{
    buffer: [u32; 16], 
}
impl DMAExample {

  pub fn send<TO, CH>(&mut self, channel: CH, sm_tx: TO) -> (CH, TO)
    where
        CH: SingleChannel,
        TO: WriteTarget<TransmittedWord = u32>,
    {
        let tx_transfer = rp2040_hal::dma::single_buffer::Config::new(channel, &self.buffer, sm_tx).start();
        let (ch, _b, to) = tx_transfer.wait();
        (ch, to)
    }
}

The only way to get this to really work is to make self 'static which isn't really viable with re-use.
This is because I run into rp2040_hal::dma::single_buffer::Config::new requiring a 'static lifetime on the ReadTarget.

Am I missing something that will allow any other lifetime around this? Trying to encapsulate the ownership nicely? Given that the transfer.wait() call returns the buffer, my original plan was to persist the transfer item into the object to retrieve it but can't get that far.

I assume I'm missing something obvious, but also can't seem to find any online examples that deal with this (i.e. dont just use the dma in the main entry function; but actually have the buffer stored elsewhere).

@jannic
Copy link
Member

jannic commented Mar 3, 2025

I'm really not an expert on DMA, so I waited a while to give others a chance to answer first. However, as nobody did yet, let me try.

As far as I know there's no great answer to your question. It's a hard problem if you want to be strict about rust's safety guarantees:

  • You must not reuse the memory or even just read a DMA buffer from rust while DMA is still running
  • The DMA engine keeps running independently from your rust code (well, that's the idea...)
  • In rust, leaking an object (forgetting it without dropping) is considered safe, so you can't reliably execute some code to stop the DMA transfer before an object becomes invalid
  • If the object was allocated on the stack, that memory can be reused, which is catastrophic if it's still in use by DMA
  • This can be solved by allocating the DMA buffer on the heap. That way, while the object still leaks, the memory leaks as well and won't get reused by rust.
  • But on embedded devices, you often don't want to use alloc to avoid out-of-memory situations (either do to actual lack of memory, or due to memory fragmentation)

Depending on your use case, the simplest solution may be to use an allocator, eg. https://crates.io/crates/embedded-alloc, and be careful to avoid OOM situations.
You probably still need to (unsafely) implement ReadBuffer (or WriteBuffer) for your object and implement an appropriate drop method to stop the DMA when the object is dropped. So it's not trivial.

Of course, if you are not writing a library that should be safe even if users of that library call the API in unexpected ways, but instead you are the only user of your DMA buffer, you can also "just be careful" not to forget an object, and make sure you always stop the DMA transfer. Then you can do something like this:

struct DMAExample {
    buffer: [u32; 16],
}
impl DMAExample {
    pub fn send<TO, CH>(&mut self, channel: CH, sm_tx: TO) -> (CH, TO)
    where
        CH: SingleChannel,
        TO: WriteTarget<TransmittedWord = u32>,
    {
        let tx_transfer =
            rp2040_hal::dma::single_buffer::Config::new(channel, &*self, sm_tx).start();
        let (ch, _b, to) = tx_transfer.wait();
        (ch, to)
    }
}

// Safety: As send() always waits for the transfer to finish before it returns, the implementation above is fine.
// But don't make this type `pub`: Otherwise, the type may be used in situations where it's not safe!
unsafe impl ReadBuffer for &DMAExample {
    type Word = u32;

    unsafe fn read_buffer(&self) -> (*const u32, usize) {
        (
            core::ptr::addr_of!(self.buffer) as *const _,
            self.buffer.len(),
        )
    }
}

Sorry, this is not a complete solution, and I'm not even sure that everything I wrote is correct.
If you find better examples, best practices etc., I'd be glad to hear about them!
(Also, I don't think this is rp-hal specific. You may have more luck if you search for examples for embedded-dma ).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants