[Revised]Getting Started with STM32F103: SPI Transmit using DMA

In this revised guide on SPI with DMA on STM32F103, we shall transmit packet of data using DMA without CPU intervention.

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;

Also, enable SPI1 interrupt in NVIC, this will be needed later as following:

	/*Enable SPI interrupt handler in NVIC*/
	NVIC_EnableIRQ(SPI1_IRQn);

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;

	/*Enable SPI interrupt handler in NVIC*/
	NVIC_EnableIRQ(SPI1_IRQn);

	//SPI Enable
	SPI1->CR1 |= SPI_CR1_SPE;
	//Clear initial flags
	(void)SPI1->SR;

. 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;

ow, 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 enable Tx buffer empty of SPI as following:

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

	if((DMA1->ISR & DMA_ISR_TCIF3) == DMA_ISR_TCIF3)
	{
		/*Enable SPI Tx buffer empty interrupt*/
		SPI1->CR2|=SPI_CR2_TXEIE;

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

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

For SPI interrupt handler, we shall check if the tx buffer is empty and the SPI is not busy as following:

If those two condition met, call SPI1_TX_Complete function which is defined as weak and allow the user to overwrite this function is his firmware and disable SPI TX buffer is empty interrupt as following:

__WEAK void SPI1_TX_Complete(void)
{

}

void SPI1_IRQHandler(void)
{

	if ((SPI1->SR & SPI_SR_TXE) && ((SPI1->SR & SPI_SR_BSY)==0))
	{
		/*Set finished to 1*/
		SPI1_TX_Complete();

		/*Disable SPI Tx Buffer empty interrupt*/
		SPI1->CR2&=~SPI_CR2_TXEIE;
	}

}

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;


}

Hence, the source code 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);

void cs_low();
void cs_high();

In main.c:

#include "spi.h"

uint8_t data[9]={0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09};


volatile uint8_t SPI_Transfer_Finished=0;

int main(void)
{
	spi_DMA_Init();

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

void SPI1_TX_Complete(void)
{
	SPI_Transfer_Finished=1;
}

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 😉

Add Comment

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