0
0
Power-electronicsHow-ToIntermediate · 4 min read

How to Use SPI with DMA in Embedded C: Simple Guide

To use SPI with DMA in embedded C, configure the SPI peripheral and DMA controller to handle data transfers automatically. Initialize DMA channels for SPI TX and RX, then start the SPI communication; DMA moves data in the background, freeing the CPU.
📐

Syntax

Using SPI with DMA involves setting up the SPI peripheral and DMA channels. You typically configure:

  • SPI initialization: Set SPI mode, clock, and data size.
  • DMA setup: Configure source and destination addresses, data length, and transfer direction.
  • Start transfer: Enable DMA streams and SPI to begin communication.
c
/* SPI and DMA initialization pseudocode */
void SPI_DMA_Init(void) {
    // Initialize SPI peripheral
    SPI_InitTypeDef SPI_InitStruct = {0};
    SPI_InitStruct.Mode = SPI_MODE_MASTER;
    SPI_InitStruct.Direction = SPI_DIRECTION_2LINES;
    SPI_InitStruct.DataSize = SPI_DATASIZE_8BIT;
    SPI_InitStruct.CLKPolarity = SPI_POLARITY_LOW;
    SPI_InitStruct.CLKPhase = SPI_PHASE_1EDGE;
    SPI_InitStruct.NSS = SPI_NSS_SOFT;
    SPI_InitStruct.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;
    SPI_InitStruct.FirstBit = SPI_FIRSTBIT_MSB;
    SPI_InitStruct.TIMode = SPI_TIMODE_DISABLE;
    SPI_InitStruct.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    SPI_InitStruct.CRCPolynomial = 7;
    SPI_Init(&SPI_InitStruct);

    // Configure DMA for SPI TX
    DMA_InitTypeDef DMA_TX_InitStruct = {0};
    DMA_TX_InitStruct.Channel = DMA_CHANNEL_3;
    DMA_TX_InitStruct.Direction = DMA_MEMORY_TO_PERIPH;
    DMA_TX_InitStruct.PeriphInc = DMA_PINC_DISABLE;
    DMA_TX_InitStruct.MemInc = DMA_MINC_ENABLE;
    DMA_TX_InitStruct.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    DMA_TX_InitStruct.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    DMA_TX_InitStruct.Mode = DMA_NORMAL;
    DMA_TX_InitStruct.Priority = DMA_PRIORITY_LOW;
    DMA_Init(DMA1_Stream3, &DMA_TX_InitStruct);

    // Configure DMA for SPI RX
    DMA_InitTypeDef DMA_RX_InitStruct = {0};
    DMA_RX_InitStruct.Channel = DMA_CHANNEL_3;
    DMA_RX_InitStruct.Direction = DMA_PERIPH_TO_MEMORY;
    DMA_RX_InitStruct.PeriphInc = DMA_PINC_DISABLE;
    DMA_RX_InitStruct.MemInc = DMA_MINC_ENABLE;
    DMA_RX_InitStruct.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    DMA_RX_InitStruct.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    DMA_RX_InitStruct.Mode = DMA_NORMAL;
    DMA_RX_InitStruct.Priority = DMA_PRIORITY_HIGH;
    DMA_Init(DMA1_Stream0, &DMA_RX_InitStruct);
}
💻

Example

This example shows how to send and receive data over SPI using DMA on an STM32 microcontroller. It initializes SPI and DMA, then starts a transfer of 10 bytes.

c
#include "stm32f4xx.h"
#include <string.h>

uint8_t txBuffer[10] = {0,1,2,3,4,5,6,7,8,9};
uint8_t rxBuffer[10] = {0};

void SPI_DMA_Init(void);
void SPI_TransmitReceive_DMA(uint8_t *txData, uint8_t *rxData, uint16_t size);

int main(void) {
    SPI_DMA_Init();

    SPI_TransmitReceive_DMA(txBuffer, rxBuffer, 10);

    // Wait for transfer complete (polling DMA flags for simplicity)
    while(!(DMA1->HISR & DMA_HISR_TCIF0)) {}
    DMA1->HIFCR |= DMA_HIFCR_CTCIF0; // Clear flag

    // Now rxBuffer contains received data
    while(1) {}
}

void SPI_DMA_Init(void) {
    // Enable clocks
    RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;
    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;

    // Configure SPI1 as master, 8-bit, mode 0
    SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_SPE;

    // Configure DMA streams
    // DMA1 Stream3 Channel3 for SPI1_TX
    DMA1_Stream3->CR = 0;
    DMA1_Stream3->PAR = (uint32_t)&SPI1->DR;
    DMA1_Stream3->M0AR = (uint32_t)txBuffer;
    DMA1_Stream3->NDTR = 10;
    DMA1_Stream3->CR = DMA_SxCR_CHSEL_0 | DMA_SxCR_MINC | DMA_SxCR_DIR_0 | DMA_SxCR_TCIE;

    // DMA1 Stream0 Channel3 for SPI1_RX
    DMA1_Stream0->CR = 0;
    DMA1_Stream0->PAR = (uint32_t)&SPI1->DR;
    DMA1_Stream0->M0AR = (uint32_t)rxBuffer;
    DMA1_Stream0->NDTR = 10;
    DMA1_Stream0->CR = DMA_SxCR_CHSEL_0 | DMA_SxCR_MINC | DMA_SxCR_TCIE;
}

void SPI_TransmitReceive_DMA(uint8_t *txData, uint8_t *rxData, uint16_t size) {
    // Update DMA memory addresses and size
    DMA1_Stream3->M0AR = (uint32_t)txData;
    DMA1_Stream3->NDTR = size;
    DMA1_Stream0->M0AR = (uint32_t)rxData;
    DMA1_Stream0->NDTR = size;

    // Enable DMA streams
    DMA1_Stream0->CR |= DMA_SxCR_EN;
    DMA1_Stream3->CR |= DMA_SxCR_EN;

    // Enable SPI DMA requests
    SPI1->CR2 |= SPI_CR2_RXDMAEN | SPI_CR2_TXDMAEN;
}
⚠️

Common Pitfalls

Common mistakes when using SPI with DMA include:

  • Not enabling the DMA streams before starting SPI communication.
  • Forgetting to enable SPI DMA requests with SPI_CR2_RXDMAEN and SPI_CR2_TXDMAEN.
  • Incorrectly configuring DMA memory increment mode, causing data overwrite.
  • Not handling DMA transfer complete interrupts or flags, leading to premature data use.
  • Misconfiguring SPI clock polarity and phase, causing communication errors.
c
/* Wrong: Not enabling DMA streams */
// DMA1_Stream3->CR |= DMA_SxCR_EN; // Missing
// DMA1_Stream0->CR |= DMA_SxCR_EN; // Missing

/* Right: Enable DMA streams before SPI transfer */
DMA1_Stream0->CR |= DMA_SxCR_EN;
DMA1_Stream3->CR |= DMA_SxCR_EN;
📊

Quick Reference

Remember these key steps for SPI with DMA:

  • Initialize SPI peripheral with correct mode and settings.
  • Configure DMA channels for TX and RX with proper addresses and data size.
  • Enable DMA streams and SPI DMA requests before starting transfer.
  • Wait for DMA transfer complete flags or interrupts before using data.

Key Takeaways

Configure SPI and DMA peripherals carefully before starting transfers.
Enable DMA streams and SPI DMA requests to allow automatic data movement.
Use DMA transfer complete flags or interrupts to know when data is ready.
Set DMA memory increment mode to avoid data overwrite.
Match SPI clock polarity and phase with the connected device.