RTIC and using DMA in Interrupt Handler

Hi there, I am writing an app that communicates with a serial device. I am using RTIC and would like to leverage DMA for the writing and reading of values to/from the serial device. My program worked until I introduced DMA. I have initialized the stm32f3xx_hal::dma::dma1::Channels structure in the init function and would like to assign it to the LateResources structure so that it is available in my USART1_EXTI25 interrupt task. When I compile, I get these errors:

error[E0507]: cannot move out of `_dma1.ch4` which is behind a mutable reference
   --> hanger-buddy/src/main.rs:124:41
    |
124 |         let (tx_channel, rx_channel) = (_dma1.ch4, _dma1.ch5); 
    |                                         ^^^^^^^^^ move occurs because `_dma1.ch4` has type `stm32f3xx_hal::dma::dma1::C4`, which does not implement the `Copy` trait

error[E0507]: cannot move out of `_dma1.ch5` which is behind a mutable reference
   --> hanger-buddy/src/main.rs:124:52
    |
124 |         let (tx_channel, rx_channel) = (_dma1.ch4, _dma1.ch5); 
    |                                                    ^^^^^^^^^ move occurs because `_dma1.ch5` has type `stm32f3xx_hal::dma::dma1::C5`, which does not implement the `Copy` trait

error[E0507]: cannot move out of `*tx` which is behind a mutable reference
   --> hanger-buddy/src/main.rs:128:23
    |
128 |         let sending = tx.write_all(tx_buf, tx_channel); 
    |                       ^^ move occurs because `*tx` has type `Tx<USART1>`, which does not implement the `Copy` trait

error[E0507]: cannot move out of `*rx` which is behind a mutable reference
   --> hanger-buddy/src/main.rs:131:25
    |
131 |         let receiving = rx.read_exact(rx_buf, rx_channel); 
    |                         ^^ move occurs because `*rx` has type `Rx<USART1>`, which does not implement the `Copy` trait

My code (still in proof of concept mode if you want to call it that):

//! main.rs

#![deny(unsafe_code)]
#![no_main]
#![no_std]
#[allow(unused_imports)]

use cortex_m_semihosting::{debug, hprintln};
use cortex_m::singleton;
use core::panic::PanicInfo;
use stm32f3xx_hal::prelude::*;
use stm32f3xx_hal::stm32;
use stm32f3xx_hal::interrupt;
use stm32f3xx_hal::serial;
use stm32f3xx_hal::serial::Event;
use stm32f3xx_hal::pac::DMA1;
use panic_semihosting as _;

#[rtic::app(device = stm32f3xx_hal::pac, peripherals = true)]
const APP: () = {
    struct Resources {
        usart1_tx: stm32f3xx_hal::serial::Tx<stm32f3xx_hal::pac::USART1>,
        usart1_rx: stm32f3xx_hal::serial::Rx<stm32f3xx_hal::pac::USART1>,
        dma1: stm32f3xx_hal::dma::dma1::Channels,
        shared: u32,
    }
    #[init]
    fn init(cx: init::Context) -> init::LateResources {
        // Pends the UART1 interrupt but its handler won't run until *after*
        // `init` returns because interrupts are disabled
        rtic::pend(stm32f3xx_hal::interrupt::USART1_EXTI25); // equivalent to NVIC::pend
        
        let peripherals: stm32f3xx_hal::pac::Peripherals = cx.device;

        let mut rcc = peripherals.RCC.constrain();
        let mut flash = peripherals.FLASH.constrain();
        let clocks = rcc.cfgr.use_hse(8.mhz()).freeze(&mut flash.acr);

        let mut gpioa = peripherals.GPIOA.split(&mut rcc.ahb);
        let mut gpioc = peripherals.GPIOC.split(&mut rcc.ahb);
        let dma1 = peripherals.DMA1.split(&mut rcc.ahb);

        let tx =  gpioc.pc4.into_af7(&mut gpioc.moder, &mut gpioc.afrl);
        let rx =  gpioc.pc5.into_af7(&mut gpioc.moder, &mut gpioc.afrl); 

        let mut usart1 = stm32f3xx_hal::serial::Serial::usart1(
            peripherals.USART1,
            (tx, rx),
            9_600.bps(),
            clocks,
            &mut rcc.apb2,
        );
        
        usart1.listen(Event::Rxne);
        
        let (usart1_tx, usart1_rx) = usart1.split();

        let shared = 0;

        init::LateResources {usart1_tx, usart1_rx, shared, dma1}
    }

    #[idle]
    fn idle(_: idle::Context) -> ! {
        static mut X: u32 = 0;

        // Safe access to local `static mut` variable
        let _x: &'static mut u32 = X;


        debug::exit(debug::EXIT_SUCCESS);

        loop {
            cortex_m::asm::nop();
        }
    }

    #[task(binds = USART1_EXTI25, resources = [usart1_tx, usart1_rx, dma1, shared])]
    fn usart1(cx: usart1::Context) {
        let tx: &mut stm32f3xx_hal::serial::Tx<stm32f3xx_hal::pac::USART1> = cx.resources.usart1_tx;
        let rx: &mut stm32f3xx_hal::serial::Rx<stm32f3xx_hal::pac::USART1> = cx.resources.usart1_rx;
        let _dma1: &mut stm32f3xx_hal::dma::dma1::Channels = cx.resources.dma1;
        let shared_val: &mut u32 = cx.resources.shared;
        

        // Safe access to local `static mut` variable, just testing
        *shared_val += 1;

        let tx_buf = singleton!(:[u8;8]=*b"hello321").unwrap(); 
        let rx_buf = singleton!(:[u8;8]=[0;8]).unwrap(); 
        
        let (tx_channel, rx_channel) = (_dma1.ch4, _dma1.ch5); 
        
        // tx, tx_buf, tx_channel will be moved here. 
        // The data will be sent here. 
        let sending = tx.write_all(tx_buf, tx_channel); 
        
        // The data will not be read here. 
        let receiving = rx.read_exact(rx_buf, rx_channel); 
        
        // tx, tx_buf, tx_channel are regenerated here. 
        // Method `wait` waits for the send operation complete. 
        let (_tx_buf, _tx_channel, _tx) = sending.wait(); 
        
        // Wait for data to be received. 
        let (rx_buf, _rx_channel, _rx) = receiving.wait(); 
       
    }
};

I'm still learning Rust which makes this probably more complex than it is. The good news is that I am able to communicate with the serial device and am feeling good momentum on my project.

Any ideas on how to get through this? Thanks in advance!

@mvanbergen, I don't know your HAL at all, since I only use atsamd-hal, but I can at least point out the major problem you have.

As the compiler says, it wants to move the DMA channels and the USART structs into the DMA transfer struct. But it can't do that, because you don't actually own those two types, you only have a temporary &mut given to you by RTIC.

If the DMA transfer didn't take ownership of the USART and the channel, it would be possible to accidentally clobber the transfer when using either of them later. Your HAL is using the type system to make sure the DMA transfer struct has exclusive access for the duration of the transfer.

One solution would be to wrap your resources in an Option<>. That way, you could .take() the object, leaving None, and move it into the DMA transfer. That's straightforward for the Tx and Rx structs, but it's a little trickier for the Channels struct, because you don't need to move all of the channels, only the two you're interested in. In that case, it might make sense to change your Resources struct to only hold the channels you need, rather than all of them.

Finally, I would like to point you to the Matrix channel, where you might find it easier to get help.

Good luck with the project.

3 Likes

Thank you very much for the reply, @bradleyharden. I follow what you are suggesting and will definitely give it a try this afternoon. Also, thank you too for the reference to the Matrix channel.

To leverage the non blockning powers of DMA you could wire it up a bit differently. If you instead store the Transfer objects returned from read_exact and write_all (ie your receiving and sending) in your resources (probably wrapped in Option) and instead use the DMA TransferComplete interrupts, you can initiate a transfer in init and when the task fires you call wait() on the resource (now returns immediately without blocking) to access your data etc and initiate a new transfer that you put back in the resource. On the tx side (or both) you might want to use an enum to store either the ongoing Transfer or the tx, dma channel and buffer in an idle state when not transmitting.
Here’s a quick example of what i mean (UNTESTED)
https://github.com/kalkyl/f303-rtic/blob/main/src/bin/serial.rs
(Note that this example uses the new RTIC 0.6alpha.5 syntax) The example also includes some ideas how you can utilize task priority and capacity to offload the ”data processing” to a lower priority task (queue) and make sure you don’t lose incoming data.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.