Skip to content

SPI slave with DMA and hardware NSS #535

@hacknus

Description

@hacknus

Hello,

Thanks to all the devs for this amazing crate!
I've been using it for the last couple of months and I'm very happy. I have noticed that when setting up SPI as a slave with DMA:

    // SPI1
    let sck = gpioa.pa5;
    let miso = gpioa.pa6.into_alternate_push_pull(&mut gpioa.crl);
    let mosi = gpioa.pa7;
    let spi = Spi::spi1_slave(dp.SPI1, (sck, miso, mosi), &mut afio.mapr, MODE);

     let dma_channels = dp.DMA1.split();

    // Prepare DMA
    let spi_dma = spi.with_rx_tx_dma(dma_channels.2, dma_channels.3);

   let slave_transfer = spi_dma.read_write(&mut RX_BUF_A, &mut TX_BUF_A);


unsafe {
        NVIC::unmask(Interrupt::DMA1_CHANNEL2);
        NVIC::unmask(Interrupt::DMA1_CHANNEL3);
    }

es explained in the example, this is not enough. I need to add some unsafe stuff to actually get the DMA1_CHANNEL2 and DMA1_CHANNEL3 interrupts to fire:

 unsafe {
        // Enable DMA transfer complete interrupts
        let dma = &(*stm32f1xx_hal::pac::DMA1::ptr());

        // Configure channel 2 (RX)
        dma.ch2.cr.modify(|_, w| w.tcie().set_bit()); // Enable transfer complete interrupt

        // Configure channel 3 (TX)
        dma.ch3.cr.modify(|_, w| w.tcie().set_bit()); // Enable transfer complete interrupt

        // Start the first DMA transfer
        USING_BUFFER_A.store(true, Ordering::Relaxed);
        let slave_transfer = spi_dma.read_write(&mut RX_BUF_A, &mut TX_BUF_A);

        // Store the transfer in the global
        cortex_m::interrupt::free(|cs| {
            G_SPI1_DMA_TRANSFER.borrow(cs).replace(Some(slave_transfer));
        });
    }

and then the interrupts are:

// Correct type definition for SPI1 with DMA
// Modify your type definitions to match what the HAL expects
type Spi1RxTxDma = stm32f1xx_hal::spi::SpiRxTxDma<
    stm32f1xx_hal::pac::SPI1,
    stm32f1xx_hal::spi::Spi1NoRemap,
    (
        stm32f1xx_hal::gpio::PA5<stm32f1xx_hal::gpio::Input<stm32f1xx_hal::gpio::Floating>>,
        stm32f1xx_hal::gpio::PA6<stm32f1xx_hal::gpio::Alternate<stm32f1xx_hal::gpio::PushPull>>,
        stm32f1xx_hal::gpio::PA7<stm32f1xx_hal::gpio::Input<stm32f1xx_hal::gpio::Floating>>,
    ),
    stm32f1xx_hal::spi::Slave,
    stm32f1xx_hal::dma::dma1::C2,
    stm32f1xx_hal::dma::dma1::C3,
>;

// DMA transfer type
type Spi1DmaTransfer = stm32f1xx_hal::dma::Transfer<
    stm32f1xx_hal::dma::W,
    (&'static mut [u8; 8], &'static mut [u8; 8]), // References instead of owned buffers
    Spi1RxTxDma,
>;

static mut RX_BUF_A: [u8; 8] = [0; 8];
static mut RX_BUF_B: [u8; 8] = [0; 8];
static mut TX_BUF_A: [u8; 8] = [0xDE, 0xAD, 0xBE, 0xEF, 1, 2, 3, 4];
static mut TX_BUF_B: [u8; 8] = [0xDE, 0xAD, 0xBE, 0xEF, 1, 2, 3, 4];
 // Add these global variables at the top of your file where the other globals are defined
static USING_BUFFER_A: AtomicBool = AtomicBool::new(true);
static G_SPI1_DMA_TRANSFER: Mutex<RefCell<Option<Spi1DmaTransfer>>> =
    Mutex::new(RefCell::new(None));

#[interrupt]
fn DMA1_CHANNEL2() {
    cortex_m::interrupt::free(|_cs| {
        // Get DMA registers
        let dma = unsafe { &(*stm32f1xx_hal::pac::DMA1::ptr()) };

        // Check if transfer complete interrupt flag is set
        if dma.isr.read().tcif2().is_complete() {
            // Clear the interrupt flag
            dma.ifcr.write(|w| w.ctcif2().clear());

            usb::usb_print_bytes_ln(b"DMA1_CH2 interrupt fired");

            // With hardware NSS, we only get data when CS is asserted
            // Process the received data
            let rx_data = unsafe {
                if USING_BUFFER_A.load(Ordering::Relaxed) {
                    &RX_BUF_A
                } else {
                    &RX_BUF_B
                }
            };

            usb::usb_print_bytes_ln(b"SPI data received:");
            for &byte in rx_data.iter() {
                let mut hex_buf = [0u8; 4];
                hex_buf[0] = b'0';
                hex_buf[1] = b'x';
                let hi = byte >> 4;
                let lo = byte & 0x0F;
                hex_buf[2] = if hi < 10 { b'0' + hi } else { b'A' + (hi - 10) };
                hex_buf[3] = if lo < 10 { b'0' + lo } else { b'A' + (lo - 10) };
                usb::usb_print_bytes_ln(&hex_buf);
            }

            // Toggle buffer for next transfer
            USING_BUFFER_A.store(!USING_BUFFER_A.load(Ordering::Relaxed), Ordering::Relaxed);

            // Restart DMA
            let spi = unsafe { &(*stm32f1xx_hal::pac::SPI1::ptr()) };
            let dma1 = unsafe { &(*stm32f1xx_hal::pac::DMA1::ptr()) };

            unsafe {
                // Configure DMA channels
                // First disable channels
                dma1.ch2.cr.modify(|_, w| w.en().clear_bit());
                dma1.ch3.cr.modify(|_, w| w.en().clear_bit());

                // Wait for channels to be disabled
                while dma1.ch2.cr.read().en().bit_is_set() {}
                while dma1.ch3.cr.read().en().bit_is_set() {}

                // Clear all interrupt flags for these channels
                dma1.ifcr.write(|w| {
                    w.cgif2()
                        .set_bit()
                        .ctcif2()
                        .set_bit()
                        .chtif2()
                        .set_bit()
                        .cteif2()
                        .set_bit()
                        .cgif3()
                        .set_bit()
                        .ctcif3()
                        .set_bit()
                        .chtif3()
                        .set_bit()
                        .cteif3()
                        .set_bit()
                });

                // Set buffer addresses
                let (rx_buf, tx_buf) = if USING_BUFFER_A.load(Ordering::Relaxed) {
                    (
                        &mut RX_BUF_A as *mut _ as u32,
                        &mut TX_BUF_A as *mut _ as u32,
                    )
                } else {
                    (
                        &mut RX_BUF_B as *mut _ as u32,
                        &mut TX_BUF_B as *mut _ as u32,
                    )
                };

                // Configure RX DMA (Channel 2)
                dma1.ch2
                    .par
                    .write(|w| w.pa().bits(&spi.dr as *const _ as u32));
                dma1.ch2.mar.write(|w| w.ma().bits(rx_buf));
                dma1.ch2.ndtr.write(|w| w.ndt().bits(8));

                // Configure TX DMA (Channel 3)
                dma1.ch3
                    .par
                    .write(|w| w.pa().bits(&spi.dr as *const _ as u32));
                dma1.ch3.mar.write(|w| w.ma().bits(tx_buf));
                dma1.ch3.ndtr.write(|w| w.ndt().bits(8));

                // Configure RX DMA control register
                dma1.ch2.cr.write(|w| {
                    w.mem2mem()
                        .disabled()
                        .pl()
                        .high()
                        .msize()
                        .bits8()
                        .psize()
                        .bits8()
                        .minc()
                        .enabled()
                        .pinc()
                        .disabled()
                        .circ()
                        .disabled()
                        .dir()
                        .from_peripheral()
                        .teie()
                        .enabled()
                        .tcie()
                        .enabled()
                        .en()
                        .enabled()
                });

                // Configure TX DMA control register
                dma1.ch3.cr.write(|w| {
                    w.mem2mem()
                        .disabled()
                        .pl()
                        .high()
                        .msize()
                        .bits8()
                        .psize()
                        .bits8()
                        .minc()
                        .enabled()
                        .pinc()
                        .disabled()
                        .circ()
                        .disabled()
                        .dir()
                        .from_memory()
                        .teie()
                        .enabled()
                        .tcie()
                        .enabled()
                        .en()
                        .enabled()
                });

                // Enable SPI DMA requests
                spi.cr2
                    .modify(|_, w| w.rxdmaen().enabled().txdmaen().enabled());
            }
        }
    });
}

#[interrupt]
fn DMA1_CHANNEL3() {
    cortex_m::interrupt::free(|_cs| {
        // Get DMA registers
        let dma = unsafe { &(*stm32f1xx_hal::pac::DMA1::ptr()) };

        // Check if transfer complete interrupt flag is set
        if dma.isr.read().tcif3().is_complete() {
            // Clear the interrupt flag
            dma.ifcr.write(|w| w.ctcif3().clear());

            // Log interrupt
            usb::usb_print_bytes_ln(b"DMA1_CH3 interrupt fired");

            // No need to restart the transfer here - it's done in CHANNEL2 handler
        }
    });
}

And I also noticed that to enable hardware NSS (standard PA4 pin for SPI1), since other devices are also connected to the same SPI bus, I also required this unsafe block:

    unsafe {
        let spi_reg = &(*stm32f1xx_hal::pac::SPI1::ptr());
        spi_reg.cr1.modify(|_, w| w.ssm().clear_bit()); // Hardware NSS management
        spi_reg.cr2.modify(|_, w| w.ssoe().clear_bit()); // NSS input for slave mode
    }

The code above is definitely not pretty, I had AI help me a lot to get this running, since I have not that much experience with stuff that goes beyond using standard HAL functions.

Expected behaviour:

In my opinion, these unsafe calls should be minimized and mostly safely abstracted by the HAL.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions