Page 1 of 1

STM32 Programming. Part 10: SPI + DMA

Posted: 16 Oct 2023, 04:57
by Oleg
In this part we will move on to the practice of working with DMA on the example of SPI interface, namely we will consider the transfer and reception of data via SPI in Master mode with the help of DMA controller. All examples, as always, are for the stm32f103c8 microcontroller.

General information about DMA requests

First of all, let's understand to which DMA channel SPI requests are connected. Open Reference manual, in the section about DMA we find this picture:
image.png
image.png (97.34 KiB)
Viewed 3553 times
The figure shows that each DMA channel can handle requests from a large number of peripheral modules. Let's take channel 3 as an example. It can receive 5 different requests: USART3_RX, TIM1_CH2, TIM3_CH4, TIM3_UP and SPI1_TX. All of these requests are input to the inputs of the OR logic element. As soon as one of the queries becomes active, the output of this element will show a log. 1. This signal is then fed to another OR element, which can only pass a logical signal through it if a special enable signal (Channel 3 EN bit) is set to one. The following thing happens here: a DMA request can be generated either from the peripherals connected to this channel or by the MEM2MEM bit. MEM2MEM is used when we don't need to wait for any request from peripherals to transfer data, for example when copying one memory location to another. I think we are clear about this. There is also this table, which contains all the same things, but in a different format:
image.png
image.png (38.81 KiB)
Viewed 3553 times
Now let's go to the SPI section. There are two interesting bits in the SPI_CR2 register: TXDMAEN and RXDMAEN:
image.png
image.png (14.93 KiB)
Viewed 3553 times
If the TXDMAEN bit is set, the SPI sends a SPIx_TX request to the DMA when the TXE flag is set (transmitter buffer is empty), and if the RXDMAEN bit is set, the SPI sends a SPIx_RX request when the RXNE flag is set (receiver buffer is not empty). For SPI1, these will be SPI1_TX and SPI1_RX requests.

Sending data via SPI in Master mode via DMA
In order to send a data array via SPI using DMA, you need to do the following:
  • Enable SPI and DMA clocking
  • Configure SPI as required
  • Set the TXDMAEN bit in the SPI_CR2 register.
And in DMA:
  • Write to the peripheral address register DMA_CPARx the address of the SPI_DR register
  • Write to the memory address register DMA_CMARx the address of the array to be sent to SPI
  • Write to the DMA_CNDTRx register the number of elements to be transferred
  • Configure DMA channel
  • Enable DMA channel
Let's go coding!

To begin with, there is the SPI initialization. Here is the full code of the function:

Code: Select all

void SPIInit(void)
{
  RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; //Enable SPI1 clocking.
  RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; //enable GPIOA port clocking
  RCC->AHBENR |= RCC_AHBENR_DMA1EN; //Enable DMA1 clocking
  
  
  //Customize GPIO
  
  //PA7 - MOSI
  //PA6 - MISO
  //PA5 - SCK
  //First reset all configuration bits to zeros
  GPIOA->CRL &= ~(GPIO_CRL_CNF5_Msk | GPIO_CRL_MODE5_Msk
                | GPIO_CRL_CNF6_Msk | GPIO_CRL_MODE6_Msk
                | GPIO_CRL_CNF7_Msk | GPIO_CRL_MODE7_Msk);
  
  //Customize
  //SCK: MODE5 = 0x03 (11b); CNF5 = 0x02 (10b)
  GPIOA->CRL |= (0x02<<GPIO_CRL_CNF5_Pos) | (0x03<<GPIO_CRL_MODE5_Pos);
  
  //MISO: MODE6 = 0x00 (00b); CNF6 = 0x01 (01b)
  GPIOA->CRL |= (0x01<<GPIO_CRL_CNF6_Pos) | (0x00<<GPIO_CRL_MODE6_Pos);
  
  //MOSI: MODE7 = 0x03 (11b); CNF7 = 0x02 (10b)
  GPIOA->CRL |= (0x02<<GPIO_CRL_CNF7_Pos) | (0x03<<GPIO_CRL_MODE7_Pos);
  
  
  //Set SPI
  SPI1->CR1 = 0<<SPI_CR1_DFF_Pos //Frame size 8 bits
    | 0<<SPI_CR1_LSBFIRST_Pos //MSB first
    | 1<<SPI_CR1_SSM_Pos //SSS program control
    | 1<<SPI_CR1_SSI_Pos //SS in high state
    | 0x04<<SPI_CR1_BR_Pos //Baud rate: F_PCLK/32
    | 1<<SPI_CR1_MSTR_Pos //Master mode (master)
    | 0<<SPI_CR1_CPOL_Pos | 0<<SPI_CR1_CPHA_Pos; // SPI operating mode: 0
  
  
  SPI1->CR2 |= 1<<SPI_CR2_TXDMAEN_Pos; //Allow DMA request.
  SPI1->CR1 |= 1<<SPI_CR1_SPE_Pos; //Enable SPI
}

First of all, there is SPI initialization. Here is the full code of the function:
Everything here is as usual: initialization of GPIO pins to which SPI is connected, initialization of SPI itself. Only 2 lines are added: enabling DMA1 clocking (RCC->AHBENR |= RCC_AHBENR_DMA1EN) and allowing DMA request generation (SPI1->CR2 |= 1<<SPI_CR2_TXDMAEN_Pos).

Next, we move on to the data transfer function. Let's call it SPI_Send():

Code: Select all

void SPI_Send(uint8_t *data, uint16_t len)
{
 ...
}
It takes as input a pointer to the array to be transferred and the number of bytes to be transferred.

The request for data transfer from SPI1 hangs on the 3rd DMA channel (see the picture above). Before starting any manipulations with this channel, make sure that it is disabled:

Code: Select all

 //disable DMA channel after previous data transfer
  DMA1_Channel3->CCR &= ~(1 << DMA_CCR_EN_Pos);
Next, we specify to DMA what exactly we want to transfer, where to and in what quantity:

Code: Select all

  DMA1_Channel3->CPAR = (uint32_t)(&SPI1->DR); //get the DR register address into the CPAR
  DMA1_Channel3->CMAR = (uint32_t)data; //add the address of data to the CMAR register
  DMA1_Channel3->CNDTR = len; //number of transmitted data
After that we proceed to channel configuration:

Code: Select all

 //DMA channel configuration
  DMA1_Channel3->CCR = 0 << DMA_CCR_MEM2MEM_Pos // MEM2MEM mode disabled
    | 0x00 << DMA_CCR_PL_Pos //priority low
    | 0x00 << DMA_CCR_MSIZE_Pos //memory data size 8 bits
    | 0x01 << DMA_CCR_PSIZE_Pos //data register size 16 bits
    | 1 << DMA_CCR_MINC_Pos // Enable increment of memory address
    | 0 << DMA_CCR_PINC_Pos // Peripheral address increment disabled
    | 0 << DMA_CCR_CIRC_Pos //ring mode disabled
    | 1 << DMA_CCR_DIR_Pos; //1 - from memory to periphery.
There are a lot of letters, let's understand. We don't need the MEM2MEM mode, so we set it to zero. Priority is also not very important to us, also zero (low). Now it is important: memory and peripheral bit sizes. Since the transferred array is one byte each, MSIZE=0x00 (8 bits). And the SPI register is 16-bit, so PSIZE=0x01 (16 bits). Further, after each transaction would be good to increase by one pointer to the array in memory, we do not want 10 times in SPI to send an element with index 0? We set MINC=1. But we do not need to increment the address of the peripheral register, so we set PINC=0. Further, we don't need the ring mode now either, CIRC=0. And lastly, the direction of transfer: DIR=1, from memory to peripheral.

All we need to do now is to enable this DMA channel:

Code: Select all

 DMA1_Channel3->CCR |= 1 << DMA_CCR_EN_Pos; //enable data transfer
And the full code of the function:

Code: Select all

void SPI_Send(uint8_t *data, uint16_t len)
{
  // disable the DMA channel after the previous data transfer
  DMA1_Channel3->CCR &= ~(1 << DMA_CCR_EN_Pos);
  
  DMA1_Channel3->CPAR = (uint32_t)(&SPI1->DR); //add DR register address to CPAR
  DMA1_Channel3->CMAR = (uint32_t)data; //add data address to CMAR register
  DMA1_Channel3->CNDTR = len; //number of transmitted data
  
  //DMA channel setup
  DMA1_Channel3->CCR = 0 << DMA_CCR_MEM2MEM_Pos // MEM2MEM mode disabled
    | 0x00 << DMA_CCR_PL_Pos //priority low
    | 0x00 << DMA_CCR_MSIZE_Pos //memory data size 8 bits
    | 0x01 << DMA_CCR_PSIZE_Pos //data register size 16 bits
    | 1 << DMA_CCR_MINC_Pos // Enable increment of memory address
    | 0 << DMA_CCR_PINC_Pos // Peripheral address increment disabled
    | 0 << DMA_CCR_CIRC_Pos //ring mode disabled
    | 1 << DMA_CCR_DIR_Pos; //1 - from memory to periphery
  
  DMA1_Channel3->CCR |= 1 << DMA_CCR_EN_Pos; //enable data transfer
}
Let's build this main() for an example:

Code: Select all

uint8_t data[10];
void main()
{
  for(int i=0; i<sizeof(data); i++)
  {
    data[i] = i+1;
  }
  
  SPIInit();
  SPI_Send(data, sizeof(data));
  
  for(;;)
  {
  }
}
This code will send 10 bytes to SPI1 via DMA.

Data reception via SPI in Master mode via DMA

Data reception is a bit more fun. First of all, we add a line to the SPI initialization function where we allow DMA request for data reception:

SPI1->CR2 |= 1<<SPI_CR2_RXDMAEN_Pos;

And the initialization function will look like this:

Code: Select all

void SPIInit(void)
{
  RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; // Enable SPI1 clocking
  RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; //enable GPIOA port clocking
  RCC->AHBENR |= RCC_AHBENR_DMA1EN; //Enable DMA1 clocking
  
  //Customize GPIO
  
  //PA7 - MOSI
  //PA6 - MISO
  //PA5 - SCK
  //First reset all configuration bits to zeros
  GPIOA->CRL &= ~(GPIO_CRL_CNF5_Msk | GPIO_CRL_MODE5_Msk
                | GPIO_CRL_CNF6_Msk | GPIO_CRL_MODE6_Msk
                | GPIO_CRL_CNF7_Msk | GPIO_CRL_MODE7_Msk);
  
  //Customize
  //SCK: MODE5 = 0x03 (11b); CNF5 = 0x02 (10b)
  GPIOA->CRL |= (0x02<<GPIO_CRL_CNF5_Pos) | (0x03<<GPIO_CRL_MODE5_Pos);
  
  //MISO: MODE6 = 0x00 (00b); CNF6 = 0x01 (01b)
  GPIOA->CRL |= (0x01<<GPIO_CRL_CNF6_Pos) | (0x00<<GPIO_CRL_MODE6_Pos);
  
  //MOSI: MODE7 = 0x03 (11b); CNF7 = 0x02 (10b)
  GPIOA->CRL |= (0x02<<GPIO_CRL_CNF7_Pos) | (0x03<<GPIO_CRL_MODE7_Pos);
  
  
  //Set SPI
  SPI1->CR1 = 0<<SPI_CR1_DFF_Pos //Frame size 8 bits
    | 0<<SPI_CR1_LSBFIRST_Pos //MSB first
    | 1<<SPI_CR1_SSM_Pos //SSS program control
    | 1<<SPI_CR1_SSI_Pos //SS in high state
    | 0x04<<SPI_CR1_BR_Pos //Baud rate: F_PCLK/32
    | 1<<SPI_CR1_MSTR_Pos //Master mode (master)
    | 0<<SPI_CR1_CPOL_Pos | 0<<SPI_CR1_CPHA_Pos; // SPI operating mode: 0
  
  
  SPI1->CR2 |= 1<<SPI_CR2_TXDMAEN_Pos;
  SPI1->CR2 |= 1<<SPI_CR2_RXDMAEN_Pos;
  SPI1->CR1 |= 1<<SPI_CR1_SPE_Pos; //Enable SPI
}
DMA-request to receive data from SPI1 is connected to the 2nd DMA channel, so let's proceed to the configuration of this channel. As in the previous case, first disable the channel:

Code: Select all

  //disable the DMA channel after the previous data transfer
  DMA1_Channel2->CCR &= ~(1 << DMA_CCR_EN_Pos);
Then we configure where to receive from, where to store and in what amount:

Code: Select all

  DMA1_Channel2->CPAR = (uint32_t)(&SPI1->DR); //get the DR register address into CPAR
  DMA1_Channel2->CMAR = (uint32_t)data; //add the address of data to the CMAR register
  DMA1_Channel2->CNDTR = len; //number of transmitted data
And setting of the DMA channel:

Code: Select all

DMA1_Channel2->CCR = 0 << DMA_CCR_MEM2MEM_Pos // MEM2MEM mode disabled
    | 0x00 << DMA_CCR_PL_Pos //priority low
    | 0x00 << DMA_CCR_MSIZE_Pos //memory data size 8 bits
    | 0x01 << DMA_CCR_PSIZE_Pos //data register size 16 bits
    | 1 << DMA_CCR_MINC_Pos // Enable increment of memory address
    | 0 << DMA_CCR_PINC_Pos // Peripheral address increment disabled
    | 0 << DMA_CCR_CIRC_Pos //ring mode disabled
    | 0 << DMA_CCR_DIR_Pos; //0 - from periphery to memory
Here everything is the same as in data transfer, only the direction of transfer is from the periphery to memory (DIR = 0).

And we allow the DMA channel to work:

Code: Select all

DMA1_Channel2->CCR |= 1 << DMA_CCR_EN_Pos; //enable data reception
Well, the reception has been started. And this is where a little fun begins. The point is that in Master mode data reception will not work after switching on the DMA channel. To understand what can be wrong here, let's remember how SPI works.

SPI is a shift register, and in Master mode to receive something from a Slave device, something must be transferred to it, because receiving and transferring data in SPI occurs simultaneously. I hope that those who have worked with SPI at least in AVRs understand this. This is where the ambiguity lies. DMA-request SPI1_RX will occur only when something appears in the receiver buffer. And to make something appear in the receiver buffer, you need to "push" SPI by writing some value into the transmitter buffer, for example, 0xFF. And there is a way out of the situation! It is necessary to configure the 3rd DMA channel to transfer data from memory to SPI1, so that it pushes SPI1, thus receiving data.

And there is a little trick here. We can save on memory and not create a buffer for pushing SPI, the length of which is equal to the receive buffer. It is enough to create one single variable uint8_t, which we will send to SPI the required number of times. To pull this off, it is enough to disable memory address increment when transferring data from memory to SPI. Let's move on to the code. First, let's create a placeholder variable:

Code: Select all

static uint8_t _filler = 0xFF;
Then, as usual, we disable the DMA channel before we change anything in the registers:

Code: Select all

 DMA1_Channel3->CCR &= ~(1 << DMA_CCR_EN_Pos);
Next, initialize the memory address, peripheral, and transfer quantity registers:

Code: Select all

DMA1_Channel3->CPAR = (uint32_t)(&SPI1->DR); //get the DR register address into CPAR
  DMA1_Channel3->CMAR = (uint32_t)(&_filler); //add the data address to the CMAR register
  DMA1_Channel3->CNDTR = len; //number of transmitted data
Here we put the address of our _filler variable into the memory address register. And then we set up the DMA channel:

Code: Select all

 DMA1_Channel3->CCR = 0 << DMA_CCR_MEM2MEM_Pos // MEM2MEM mode disabled
    | 0x00 << DMA_CCR_PL_Pos //priority low
    | 0x00 << DMA_CCR_MSIZE_Pos //memory data size 8 bits
    | 0x01 << DMA_CCR_PSIZE_Pos //data register bit size 16 bits
    | 0 << DMA_CCR_MINC_Pos // Memory address increment disabled
    | 0 << DMA_CCR_PINC_Pos // Peripheral address increment disabled
    | 0 << DMA_CCR_CIRC_Pos //ring mode disabled
    | 1 << DMA_CCR_DIR_Pos; //1 - from memory to periphery
Note that memory address increment is disabled.

And the last line starts the process:

Code: Select all

  DMA1_Channel3->CCR |= 1 << DMA_CCR_EN_Pos; //Start the process
That's it! After that the value 0xFF will be written into SPI1 and data reception will start.

For clarity I will give the whole code of the receiving function:

Code: Select all

void SPI_Receive(uint8_t *data, uint16_t len)
{
  static uint8_t _filler = 0xFF;
  
  //disable the DMA channel after the previous data transfer
  DMA1_Channel2->CCR &= ~(1 << DMA_CCR_EN_Pos);
  
  DMA1_Channel2->CPAR = (uint32_t)(&SPI1->DR); //add DR register address to CPAR
  DMA1_Channel2->CMAR = (uint32_t)data; //add data address to CMAR register
  DMA1_Channel2->CNDTR = len; //number of transmitted data
  
  //DMA channel setup
  DMA1_Channel2->CCR = 0 << DMA_CCR_MEM2MEM_Pos //MEM2MEM mode disabled
    | 0x00 << DMA_CCR_PL_Pos //priority low
    | 0x00 << DMA_CCR_MSIZE_Pos //memory data size 8 bits
    | 0x01 << DMA_CCR_PSIZE_Pos //data register size 16 bits
    | 1 << DMA_CCR_MINC_Pos // Enable increment of memory address
    | 0 << DMA_CCR_PINC_Pos // Peripheral address increment disabled
    | 0 << DMA_CCR_CIRC_Pos //ring mode disabled
    | 0 << DMA_CCR_DIR_Pos; //0 - from periphery to memory
  
  DMA1_Channel2->CCR |= 1 << DMA_CCR_EN_Pos; //enable data reception
  
  
  //////////////////////////////////////////////////////////////////////////////
  
  //disable DMA channel after previous data transfer
  DMA1_Channel3->CCR &= ~(1 << DMA_CCR_EN_Pos);
  
  DMA1_Channel3->CPAR = (uint32_t)(&SPI1->DR); //add DR register address to CPAR
  DMA1_Channel3->CMAR = (uint32_t)(&_filler); //add the data address to the CMAR register
  DMA1_Channel3->CNDTR = len; //number of transmitted data
  
  //DMA channel setup
  DMA1_Channel3->CCR = 0 << DMA_CCR_MEM2MEM_Pos // MEM2MEM mode disabled
    | 0x00 << DMA_CCR_PL_Pos //priority low
    | 0x00 << DMA_CCR_MSIZE_Pos //memory data size 8 bits
    | 0x01 << DMA_CCR_PSIZE_Pos //data register size 16 bits
    | 0 << DMA_CCR_MINC_Pos // Memory address increment disabled
    | 0 << DMA_CCR_PINC_Pos // Increment of peripheral address disabled
    | 0 << DMA_CCR_CIRC_Pos //ring mode disabled
    | 1 << DMA_CCR_DIR_Pos; //1 - from memory to periphery
  
  DMA1_Channel3->CCR |= 1 << DMA_CCR_EN_Pos; //Start the process
}
So it turns out that to receive data via SPI, which is configured in Master mode, we have to use 2 DMA channels. For those who do not know or did not pay attention. _filler variable is declared as static, and this is not for nothing.

Code: Select all

static uint8_t _filler = 0xFF;
The point is that non-static variables declared inside a function are allocated on the stack and live only during the execution of this function. After exiting the function, the address provided to the variable may be occupied by another variable from another function. And if a variable inside a function is declared as static, it will be located in the area of global variables and its value will be saved after exiting the function. Its only difference from a global variable is that it can be accessed only from the function in which it was declared.

A small main() for demonstration:

uint8_t data[10];
void main()
{
SPIInit();
SPI_Receive(data, sizeof(data));


for(;;)
{
}
}


That's all, in the next article we will learn how to copy one memory area to another using DMA.