Getting Started with STM32F103: SPI Transmit using DMA

In the previous guide (here), we took a look how to configure the SPI to transmit data using polling mode. In this guide, we shall use DMA to transfer data over SPI bus.

In this guide, we shall cover the following:

  • Enable TXDMA for SPI.
  • Configure the DMA.
  • Send data over DMA.
  • Code.
  • Results.

1. Enable TXDMA:

In order to enable DMA transmission for SPI, we need to set TXDMA bit in CR2 register as following:

	/*Set TXDMA bit in CR2*/
	SPI1->CR2|=SPI_CR2_TXDMAEN;

The rest on the initialization is the same as polling mode.

//Enable SPI Clock
	RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;

	//Mode: Master
	SPI1->CR1 |= SPI_CR1_MSTR;
	//Baud rate to (8MHz / 2 = 4MHz)
	SPI1->CR1 &= ~SPI_CR1_BR;
	//MSB first
	SPI1->CR1 &= ~(SPI_CR1_LSBFIRST);
	//Full duplex (Transmit/Receive)
	SPI1->CR1 &= ~(SPI_CR1_RXONLY);
	//Data format 8-bit
	SPI1->CR1 &= ~(SPI_CR1_DFF);
	//Software Slave select
	SPI1->CR1 |= SPI_CR1_SSI;
	SPI1->CR1 |= SPI_CR1_SSM;

	/*Set TXDMA bit in CR2*/
	SPI1->CR2|=SPI_CR2_TXDMAEN;
	//SPI Enable
	SPI1->CR1 |= SPI_CR1_SPE;
	//Clear initial flags
	(void)SPI1->SR;

2. Configure the DMA:

Before we start configuring the DMA, we need to enable clock access to it. To find which bus the DMA is connected to, we need to check the block diagram of STM32F103 in the datasheet which states that DMA is connected to AHB bus:

Hence, we can enable it as following:

	/*DMA Configuration*/

	RCC->AHBENR|=RCC_AHBENR_DMA1EN;

Now, we need to know which DMA channel is related to SPI1_TX:

From the table, we can find that Channel 3 is for SPI1_TX. Hence, we can configure the channel as following:

  • Memory increment mode.
  • Direction read from memory.
  • Transfer complete interrupt.
	DMA1_Channel3->CCR|=DMA_CCR_MINC|DMA_CCR_DIR|DMA_CCR_TCIE;

Set the peripheral address to be SPI1->DR:

	/*Set Peripheral Address to be SPI1->DR*/
	DMA1_Channel3->CPAR=(uint32_t)& SPI1->DR;

Enable the interrupt in NVIC:

	/*Enable DMA1_Channel3 interrupt in NVIC*/

	NVIC_EnableIRQ(DMA1_Channel3_IRQn);

Within the interrupt:

Check the interrupt source if it is transfer complete:

If it is: clear pending flag and set SPI_Transfer_Finished to 1.

void DMA1_Channel3_IRQHandler (void)
{
	/*Check if the source is transfer complete*/

	if((DMA1->ISR & DMA_ISR_TCIF3) == DMA_ISR_TCIF3)
	{
		/*Set flag to 1*/
		SPI_Transfer_Finished=1;

		/*Disable DMA */
		DMA1_Channel3->CCR &= ~DMA_CCR_EN;

		/*Clear pending flag*/
		DMA1->IFCR = DMA_IFCR_CTCIF3;
	}
}

3. Send data over DMA:

To send data over DMA, the following steps are required:

  • Clear any pending flags.
  • Set the peripheral address.
  • Set the memory address.
  • set number of transfer.
  • Finally launch the transfer.
void SPI_DMA_Transmit(uint8_t *data,uint32_t size)
{
	/*Clear pending flag*/
	DMA1->IFCR = DMA_IFCR_CTCIF3;

	/*Set Peripheral Address to be SPI1->DR*/
	DMA1_Channel3->CPAR=(uint32_t)& SPI1->DR;

	DMA1_Channel3->CMAR=(uint32_t)data;

	DMA1_Channel3->CNDTR=size;

	/*Launch DMA*/
	DMA1_Channel3->CCR |= DMA_CCR_EN;


}

Check if transfer is completed function:

uint8_t finished_transfer()
{
	return SPI_Transfer_Finished;
}

Reset the flag function:

void reset_finished()
{
	SPI_Transfer_Finished=0;
}

Hence, the entire spi.c as following:

#include "spi.h"
#include "stm32f1xx.h"

volatile uint8_t SPI_Transfer_Finished=0;

void spi_DMA_Init(void)
{
	//Enable Port A clock
	RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
	RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
	/* PA5-SCK and PA7-MOSI */
	//Mode: Output, Speed: 10MHz
	GPIOA->CRL &= ~(GPIO_CRL_MODE5);
	GPIOA->CRL &= ~(GPIO_CRL_MODE7);
	GPIOA->CRL |= (GPIO_CRL_MODE5_0);
	GPIOA->CRL |= (GPIO_CRL_MODE7_0);
	//Alternate function push-pull
	GPIOA->CRL &= ~(GPIO_CRL_CNF5);
	GPIOA->CRL &= ~(GPIO_CRL_CNF7);
	GPIOA->CRL |= (GPIO_CRL_CNF5_1);
	GPIOA->CRL |= (GPIO_CRL_CNF7_1);

	/* PA5-MISO */
	//Mode input
	GPIOA->CRL &= ~(GPIO_CRL_MODE6);
	//Floating input
	GPIOA->CRL &= ~(GPIO_CRL_CNF6);
	GPIOA->CRL |= (GPIO_CRL_CNF6_0);

	//Remap 0: PA5, PA6, PA7
	AFIO->MAPR &= ~(1UL << 0);

	/*Configure PA0 as output CS*/
	GPIOA->CRL|=GPIO_CRL_MODE0;
	GPIOA->CRL&=~(GPIO_CRL_CNF0);


	//Enable SPI Clock
	RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;

	//Mode: Master
	SPI1->CR1 |= SPI_CR1_MSTR;
	//Baud rate to (8MHz / 2 = 4MHz)
	SPI1->CR1 &= ~SPI_CR1_BR;
	//MSB first
	SPI1->CR1 &= ~(SPI_CR1_LSBFIRST);
	//Full duplex (Transmit/Receive)
	SPI1->CR1 &= ~(SPI_CR1_RXONLY);
	//Data format 8-bit
	SPI1->CR1 &= ~(SPI_CR1_DFF);
	//Software Slave select
	SPI1->CR1 |= SPI_CR1_SSI;
	SPI1->CR1 |= SPI_CR1_SSM;

	/*Set TXDMA bit in CR2*/
	SPI1->CR2|=SPI_CR2_TXDMAEN;
	//SPI Enable
	SPI1->CR1 |= SPI_CR1_SPE;
	//Clear initial flags
	(void)SPI1->SR;


	/*DMA Configuration*/

	RCC->AHBENR|=RCC_AHBENR_DMA1EN;
	/*Set the DMA with the following parameters
	 *
	 * 1. Memory Increment mode
	 * 2. Direction Read from Memory
	 * 3. Transfer Complete Interrupt
	 * */

	DMA1_Channel3->CCR|=DMA_CCR_MINC|DMA_CCR_DIR|DMA_CCR_TCIE;

	/*Set Peripheral Address to be SPI1->DR*/
	DMA1_Channel3->CPAR=(uint32_t)& SPI1->DR;

	/*Enable DMA1_Channel3 interrupt in NVIC*/

	NVIC_EnableIRQ(DMA1_Channel3_IRQn);



}

uint8_t finished_transfer()
{
	return SPI_Transfer_Finished;
}

void reset_finished()
{
	SPI_Transfer_Finished=0;
}

void SPI_DMA_Transmit(uint8_t *data,uint32_t size)
{
	/*Clear pending flag*/
	DMA1->IFCR = DMA_IFCR_CTCIF3;

	/*Set Peripheral Address to be SPI1->DR*/
	DMA1_Channel3->CPAR=(uint32_t)& SPI1->DR;

	DMA1_Channel3->CMAR=(uint32_t)data;

	DMA1_Channel3->CNDTR=size;

	/*Launch DMA*/
	DMA1_Channel3->CCR |= DMA_CCR_EN;


}


void cs_low()
{
	GPIOA->BSRR=GPIO_BSRR_BR0;
}

void cs_high()
{
	GPIOA->BSRR=GPIO_BSRR_BS0;
}


void DMA1_Channel3_IRQHandler (void)
{
	/*Check if the source is transfer complete*/

	if((DMA1->ISR & DMA_ISR_TCIF3) == DMA_ISR_TCIF3)
	{
		/*Set flag to 1*/
		SPI_Transfer_Finished=1;

		/*Disable DMA */
		DMA1_Channel3->CCR &= ~DMA_CCR_EN;

		/*Clear pending flag*/
		DMA1->IFCR = DMA_IFCR_CTCIF3;
	}
}

The spi.h header file as following:

#ifndef SPI_H_
#define SPI_H_

#include "stdint.h"


void spi_DMA_Init(void);
void SPI_DMA_Transmit(uint8_t *data,uint32_t size);
uint8_t finished_transfer();
void reset_finished();

void cs_low();
void cs_high();



#endif /* SPI_H_ */

In mian.c:

#include "spi.h"

uint8_t data[5]={0x01,0x02,0x03,0x04,0x05};


int main(void)
{
	spi_DMA_Init();

	while(1)
	{	cs_low();
		SPI_DMA_Transmit(data, 5);
		while(finished_transfer()==0){;}
		reset_finished();
		cs_high();
		for (int i=0;i<10000;i++);
	}
}

4. Code:

You may download the source code from here:

5. Results:

Connect logic analyzer to PA5, PA7 and PA0 and you should get the following:

Happy coding 🙂

4 Comments

  • Ralf Koenig Posted December 23, 2023 9:13 am

    Dear Sir,
    Many thanks for your great tutorial “Getting Started with STM32F103: SPI Transmit using DMA”.
    I use an STM32F103 with 72MHz clock frequency. I set the baud rate for SPI to 281.25kHz.
    If I only check the DMA_ISR_TCIF3 bit in this case and then immediately set CS to high again, the SPI transfer is aborted too early and the last two bytes are lost.
    So that this does not happen in your code, I have added the check of the BSY bit in the SPI status register. The code blocks at this point but it is only for demonstration purposes.

    uint8_t finished_transfer() {
    if (SPI1->SR & SPI_SR_BSY)
    return 0;
    else
    return 1;
    }
    After that all bytes are transferred completely.

    Best regards
    Ralf Koenig

    • Husamuldeen Posted December 23, 2023 9:35 am

      Yes. You are correct.
      All SPI with DMA guides will be revised to use better version.

      • Ralf Koenig Posted December 23, 2023 10:04 am

        Il will suggest to modify the finished_transfer function like this:

        uint8_t finished_transfer()
        {
        if (SPI1->SR & SPI_SR_BSY)
        return 0;
        else
        return SPI_Transfer_Finished;
        }

        • Husamuldeen Posted December 23, 2023 11:57 am

          Wait for the revised version.
          I’ll start with F4.

Add Comment

Your email address will not be published. Required fields are marked *