cstm32spidmalow-level-api

STM32 SPI LL DMA Transmit


I have been trying to get SPI master transmit to work using DMA and STM32 LL drivers, on STM32G030C8.

I did get the SPI to work with LL drivers without DMA, so I believe that at least my wiring is correct.

What I have done:

  1. Set SPI to use DMA in cubeMX by setting SPI1_TX Request to DMA1 channel 1

  2. Setup the transmit in code:

main.c

#include "main.h"
#include "dma.h"
#include "gpio.h"
#include "spi.h"

uint8_t test_data[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

void SystemClock_Config(void);

int main(void) {
       
        HAL_Init();

        SystemClock_Config();

        MX_GPIO_Init();
        MX_SPI1_Init();
        MX_DMA_Init();
        while (1) {
                LL_DMA_ConfigAddresses(DMA1, LL_DMA_CHANNEL_1, (uint32_t)(&test_data[0]),
                                       (uint32_t)LL_SPI_DMA_GetRegAddr(SPI1),
                                       LL_DMA_GetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1));
                LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, 8);
                LL_SPI_EnableDMAReq_TX(SPI1);
                LL_SPI_Enable(SPI1);

                LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1);

                HAL_Delay(1000);
                HAL_GPIO_TogglePin(STATUS_LED_GPIO_Port, STATUS_LED_Pin);
        }
}

spi.c:

#include "spi.h"

void MX_SPI1_Init(void)
{

  LL_SPI_InitTypeDef SPI_InitStruct = {0};

  LL_GPIO_InitTypeDef GPIO_InitStruct = {0};

  GPIO_InitStruct.Pin = LL_GPIO_PIN_1;
  GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
  GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_LOW;
  GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
  GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
  GPIO_InitStruct.Alternate = LL_GPIO_AF_0;
  LL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = LL_GPIO_PIN_2;
  GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
  GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_LOW;
  GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
  GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
  GPIO_InitStruct.Alternate = LL_GPIO_AF_0;
  LL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  LL_DMA_SetPeriphRequest(DMA1, LL_DMA_CHANNEL_1, LL_DMAMUX_REQ_SPI1_TX);

  LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);

  LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PRIORITY_LOW);

  LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MODE_NORMAL);

  LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PERIPH_NOINCREMENT);

  LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MEMORY_INCREMENT);

  LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PDATAALIGN_BYTE);

  LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MDATAALIGN_BYTE);

  NVIC_SetPriority(SPI1_IRQn, 0);
  NVIC_EnableIRQ(SPI1_IRQn);

  SPI_InitStruct.TransferDirection = LL_SPI_FULL_DUPLEX;
  SPI_InitStruct.Mode = LL_SPI_MODE_MASTER;
  SPI_InitStruct.DataWidth = LL_SPI_DATAWIDTH_8BIT;
  SPI_InitStruct.ClockPolarity = LL_SPI_POLARITY_LOW;
  SPI_InitStruct.ClockPhase = LL_SPI_PHASE_1EDGE;
  SPI_InitStruct.NSS = LL_SPI_NSS_SOFT;
  SPI_InitStruct.BaudRate = LL_SPI_BAUDRATEPRESCALER_DIV4;
  SPI_InitStruct.BitOrder = LL_SPI_MSB_FIRST;
  SPI_InitStruct.CRCCalculation = LL_SPI_CRCCALCULATION_DISABLE;
  SPI_InitStruct.CRCPoly = 7;
  LL_SPI_Init(SPI1, &SPI_InitStruct);
  LL_SPI_SetStandard(SPI1, LL_SPI_PROTOCOL_MOTOROLA);
  LL_SPI_DisableNSSPulseMgt(SPI1);
}

dma.c:

#include "dma.h"

void MX_DMA_Init(void)
{
  LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);

  NVIC_SetPriority(DMA1_Channel1_IRQn, 0);
  NVIC_EnableIRQ(DMA1_Channel1_IRQn);

}

from the reference manual I found the following steps for DMA configuration:

Channel configuration procedure The following sequence is needed to configure a DMA channel x:

  1. Set the peripheral register address in the DMA_CPARx register. The data is moved from/to this address to/from the memory after the peripheral event, or after the channel is enabled in memory-to-memory mode.
  2. Set the memory address in the DMA_CMARx register. The data is written to/read from the memory after the peripheral event or after the channel is enabled in memory-to-memory mode.
  3. Configure the total number of data to transfer in the DMA_CNDTRx register. After each data transfer, this value is decremented.
  4. Configure the parameters listed below in the DMA_CCRx register: – the channel priority – the data transfer direction – the circular mode – the peripheral and memory incremented mode – the peripheral and memory data size – the interrupt enable at half and/or full transfer and/or transfer error
  5. Activate the channel by setting the EN bit in the DMA_CCRx register. A channel, as soon as enabled, may serve any DMA request from the peripheral connected to this channel, or may start a memory-to-memory block transfer.

To my understanding steps 1,2,3 and 5 are done in main.c, and the step 4 already in spi.c

Also in the reference manual about SPI and DMA:

A DMA access is requested when the TXE or RXNE enable bit in the SPIx_CR2 register is set. Separate requests must be issued to the Tx and Rx buffers.

-In transmission, a DMA request is issued each time TXE is set to 1. The DMA then writes to the SPIx_DR register

and

When starting communication using DMA, to prevent DMA channel management raising error events, these steps must be followed in order:

  1. Enable DMA Rx buffer in the RXDMAEN bit in the SPI_CR2 register, if DMA Rx is used.
  2. Enable DMA streams for Tx and Rx in DMA registers, if the streams are used.
  3. Enable DMA Tx buffer in the TXDMAEN bit in the SPI_CR2 register, if DMA Tx is used.
  4. Enable the SPI by setting the SPE bit.

In my understanding I have done all the steps, but I cannot see anything with my oscilloscope attached to SPI1 lines.

I must be missing something (or something is done in wrong order) but I cannot figure out what is wrong.

In some other questions the problem has been that the DMA channel was wrong and not supporting SPI, but in this MCU, if I understood correctly, the DMAMUX handles that, and any signal should be available in any DMA channel? (configured in spi.c)

EDIT:

Reading flags from SPI and DMA:

LL_SPI_IsActiveFlag_BSY(SPI1)                   returns 0
LL_SPI_IsEnabledDMAReq_TX(SPI1)                 returns 1
LL_SPI_IsEnabled(SPI1)                          returns 1
LL_DMA_IsEnabledChannel(DMA1, LL_DMA_CHANNEL_1) returns 1
LL_DMA_IsActiveFlag_TE1(DMA1)                   returns 0
LL_SPI_IsActiveFlag_TXE(SPI1)                   returns 1

So, everything seems to be enabled, no errors but no data is transferred!

Any help is appreciated! Thanks!


Solution

  • After some time debugging I found there is a bug in STM32cubeMX code generator. (this also seems to be already reported (https://community.st.com/s/question/0D53W00001HJ3EhSAL/wrong-initialization-sequence-with-cubemx-in-cubeide-when-using-i2s-with-dma?t=1641156995520&searchQuery)

    when selecting SPI and DMA, the generator first initializes SPI and then DMA with

    MX_SPIx_Init();
    MX_DMA_Init();
    

    The problem is that the SPI initialization tries to set DMA registers, but the DMA clock is not yet enabled so the values are not saved.

    This is also the reason I got it working with USART, the USART initialization came after the DMA Initialization.

    So the simple solution is to move the DMA initialization before the other peripherals. BUT since this code is generated automatically every time you make changes in cubeMX, the changes will be lost.

    To get around this, I used the cubeMX to disable the automatic initialization call in the project manager tab under advanced settings:

    enter image description here

    And then called these initialization functions manually in the following user code segment, and now it looks like this:

        /* Initialize all configured peripherals */
        MX_GPIO_Init();
        MX_DMA_Init();
        /* USER CODE BEGIN 2 */
        MX_SPI1_Init();
        MX_USART1_UART_Init();
        /* USER CODE END 2 */