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:
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:
Now let's go to the SPI section. There are two interesting bits in the SPI_CR2 register: TXDMAEN and RXDMAEN:
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.
- 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
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:
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)
{
...
}
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);
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
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.
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
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
}
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(;;)
{
}
}
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
}
Code: Select all
//disable the DMA channel after the previous data transfer
DMA1_Channel2->CCR &= ~(1 << DMA_CCR_EN_Pos);
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
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
And we allow the DMA channel to work:
Code: Select all
DMA1_Channel2->CCR |= 1 << DMA_CCR_EN_Pos; //enable data reception
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;
Code: Select all
DMA1_Channel3->CCR &= ~(1 << DMA_CCR_EN_Pos);
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
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
And the last line starts the process:
Code: Select all
DMA1_Channel3->CCR |= 1 << DMA_CCR_EN_Pos; //Start the process
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
}
Code: Select all
static uint8_t _filler = 0xFF;
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.