rp-rs / rp-hal

A Rust Embedded-HAL for the rp series microcontrollers
https://crates.io/crates/rp2040-hal
Apache License 2.0
1.45k stars 237 forks source link

Unsure how to enable DMA interrupts #852

Open FayCarsons opened 2 months ago

FayCarsons commented 2 months ago

I'm working on a library (my first serious embedded project) which needs to write to a PIO FIFO continuously. I'm trying to do this with DMA, using an interrupt to re-enqueue the buffers whenever a write finishes, but I'm unsure how to enable an interrupt for a double-buffered DMA transfer in the way I would with, say, a, ADC or PWM. The documentation is a bit confusing and from what I understand some features it mentions are maybe not actually implemented?

I'm initializing DMA like this:

let transfer = unsafe {
    double_buffer::Config::new((dma.ch0, dma.ch1), &mut BITSTREAM, tx)
        .start()
        .read_next(&mut TX_BUF)
};

unsafe {
    TRANSFER_HANDLER = Some(transfer);
    BUFFERS = Some((bitstream, tx_buf));
    rp_pico::pac::NVIC::unmask(rp_pico::pac::Interrupt::DMA_IRQ_0);
}

/* unrelated code ... */

#[hal::pac::interrupt]
unsafe fn DMA_IRQ_0() {
  if let Some((transfer, bufs)) = TRANSFER_HANDLER.take().zip(BUFFERS.take()) {
      loop {
          if transfer.is_done() {
              let (tx_buf, next) = transfer.wait();
              let transfer = next.read_next(tx_buf);
              let bufs = (bufs.1, bufs.0);
              TRANSFER_HANDLER = Some(transfer);
              BUFFERS = Some(bufs);
              break;
          }
      }
  }
}

With the buffers (BITSTREAM and TX_BUF) being [u32; N] and 'tx' being the PIO FIFO.

Do you think this should work as is? Is there something I'm missing? Here's the repo for context: https://github.com/FayCarsons/cosmic-unicorn-rs, the file in question is src/cosmic_unicorn.rs

Any help at all would be deeply appreciated!!!!!

jannic commented 2 months ago

I see two issues with your code:

FayCarsons commented 2 months ago

@jannic thank you so much for taking a look at this!

So, if the interrupt is triggered the transfer is done? The busy loop was an attempt at being sure of that. In general, it's confusing what's necessary to re-enqueue the buffers once the transfer is complete, the doc comments of is_done and wait lead me to believe either they aren't actually necessary inside the interrupt, or the interrupt being triggered does not necessarily mean the transfer is complete.

Do both channels need interrupts enabled? With the way the configuration works, my assumption was that the transfer would be the thing to enable an interrupt for, not the channels. Should I maybe be using the lower-level DMA API here? It feels like this higher-level API is maybe not what I want

jannic commented 2 months ago

Sorry, I don't have a complete example available, and don't know the DMA interrupts well enough to quickly write one. As a rough sketch, I'd try something like this (pseudocode):

fn interrupt_handler() {
  if check_irq0() && transfer.is_done() {
     [your processing here]
  }
}

check_irq0() checks if the dma interrupt is pending, and if so, clears it. Then, transfer.is_done() is checked. If the transfer is not yet done (which would mean that the interrupt was triggered by something else - should not happen, but who knows?) the handler just returns. Note that there is no loop involved: If the DMA finishes after check_irq0() was called, the interrupt is triggered again and the handler will be called once more.

And yes, both channels need interrupts enabled, because they will be used alternately by the double buffering transfer.