cancel
Showing results for 
Search instead for 
Did you mean: 

STM32F429: half-duplex SPI master with DMA stalls in the receive part

Todor Todorov
Associate II

Hello,

In my STM32F429 design I am using SP4 in master mode to read data from a slave with DMA in half-duplex mode. The setup is DMA2/Stream0 for RX and DMA2/Stream1 for TX, normal mode and no FIFO for both. Communication works without fail without DMA, but using DMA only TX works, while RX never completes (not even a single byte transferred as seen in the DMA2 registers in debug mode). The EN, TEIE, and TCIE bits in the DMA2->S0CR register are all set to 1, but no transfer happens and the RX DMA interrupt handler is never called. The DMA2->LISR bits FEIF0, DMEIF0, TEIF0, HTIF0, and TCIF0 all remain 0.

Tried for several days to figure out what I missed, but I am unable to spot it. Please help!

Here is my SPI init code:

void MX_SPI4_Init(void)
{
  /* USER CODE BEGIN SPI4_Init 0 */
  /* USER CODE END SPI4_Init 0 */
  LL_SPI_InitTypeDef SPI_InitStruct = {0};
  LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
 
  /* Peripheral clock enable */
  LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_SPI4);
  LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOE);
  /**SPI4 GPIO Configuration
  PE2   ------> SPI4_SCK
  PE5   ------> SPI4_MISO
  PE6   ------> SPI4_MOSI
  */
  GPIO_InitStruct.Pin = ADE7880_SPI4_SCK_Pin|ADE7880_SPI4_MISO_Pin|ADE7880_SPI4_MOSI_Pin;
  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_5;
  LL_GPIO_Init(GPIOE, &GPIO_InitStruct);
 
  /* SPI4 DMA Init */
  /* SPI4_RX Init */
  LL_DMA_SetChannelSelection(DMA2, LL_DMA_STREAM_0, LL_DMA_CHANNEL_4);
  LL_DMA_SetDataTransferDirection(DMA2, LL_DMA_STREAM_0, LL_DMA_DIRECTION_PERIPH_TO_MEMORY);
  LL_DMA_SetStreamPriorityLevel(DMA2, LL_DMA_STREAM_0, LL_DMA_PRIORITY_HIGH);
  LL_DMA_SetMode(DMA2, LL_DMA_STREAM_0, LL_DMA_MODE_NORMAL);
  LL_DMA_SetPeriphIncMode(DMA2, LL_DMA_STREAM_0, LL_DMA_PERIPH_NOINCREMENT);
  LL_DMA_SetMemoryIncMode(DMA2, LL_DMA_STREAM_0, LL_DMA_MEMORY_INCREMENT);
  LL_DMA_SetPeriphSize(DMA2, LL_DMA_STREAM_0, LL_DMA_PDATAALIGN_BYTE);
  LL_DMA_SetMemorySize(DMA2, LL_DMA_STREAM_0, LL_DMA_MDATAALIGN_BYTE);
  LL_DMA_DisableFifoMode(DMA2, LL_DMA_STREAM_0);
  /* SPI4_TX Init */
  LL_DMA_SetChannelSelection(DMA2, LL_DMA_STREAM_1, LL_DMA_CHANNEL_4);
  LL_DMA_SetDataTransferDirection(DMA2, LL_DMA_STREAM_1, 
 LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
  LL_DMA_SetStreamPriorityLevel(DMA2, LL_DMA_STREAM_1, LL_DMA_PRIORITY_HIGH);
  LL_DMA_SetMode(DMA2, LL_DMA_STREAM_1, LL_DMA_MODE_NORMAL);
  LL_DMA_SetPeriphIncMode(DMA2, LL_DMA_STREAM_1, LL_DMA_PERIPH_NOINCREMENT);
  LL_DMA_SetMemoryIncMode(DMA2, LL_DMA_STREAM_1, LL_DMA_MEMORY_INCREMENT);
  LL_DMA_SetPeriphSize(DMA2, LL_DMA_STREAM_1, LL_DMA_PDATAALIGN_BYTE);
  LL_DMA_SetMemorySize(DMA2, LL_DMA_STREAM_1, LL_DMA_MDATAALIGN_BYTE);
  LL_DMA_DisableFifoMode(DMA2, LL_DMA_STREAM_1);
 
  /* USER CODE BEGIN SPI4_Init 1 */
  LL_DMA_EnableIT_TC(DMA2, LL_DMA_STREAM_0);
  LL_DMA_EnableIT_TE(DMA2, LL_DMA_STREAM_0);
  LL_DMA_EnableIT_TC(DMA2, LL_DMA_STREAM_1);
  LL_DMA_EnableIT_TE(DMA2, LL_DMA_STREAM_1);
  /* USER CODE END SPI4_Init 1 */
  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_DIV32;
  SPI_InitStruct.BitOrder = LL_SPI_MSB_FIRST;
  SPI_InitStruct.CRCCalculation = LL_SPI_CRCCALCULATION_DISABLE;
  SPI_InitStruct.CRCPoly = 10;
  LL_SPI_Init(SPI4, &SPI_InitStruct);
  LL_SPI_SetStandard(SPI4, LL_SPI_PROTOCOL_MOTOROLA);
  /* USER CODE BEGIN SPI4_Init 2 */
#if defined(ADE7880_USE_DMA) && (ADE7880_USE_DMA == 1)
  LL_SPI_EnableDMAReq_RX(SPI4);
  LL_SPI_EnableDMAReq_TX(SPI4);
#endif
  LL_SPI_Enable(SPI4);
  /* USER CODE END SPI4_Init 2 */
}

The DMA interrupts are enabled inside the MX_DMA_Init function as follows:

void MX_DMA_Init(void)
{
  /* Init with LL driver */
  /* DMA controller clock enable */
  LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA2);
 
  /* DMA interrupt init */
  /* DMA2_Stream0_IRQn interrupt configuration */
  NVIC_SetPriority(DMA2_Stream0_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),7, 0));
  NVIC_EnableIRQ(DMA2_Stream0_IRQn);
  /* DMA2_Stream1_IRQn interrupt configuration */
  NVIC_SetPriority(DMA2_Stream1_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),7, 0));
  NVIC_EnableIRQ(DMA2_Stream1_IRQn);
}

The top-level SPI write function is:

static void ADE7880_Write(uint16_t writeAddr, uint8_t* buffer, uint16_t numBytesToWrite)
{
  /* Encode command and write address, pld stands for payload */
  uint8_t     pld[3] = { FIRST_WRITE_COMMAND_BYTE, (uint8_t)((writeAddr >> 8) & 0x00ff), (uint8_t)(writeAddr & 0x00ff) };
  ErrorStatus status = SUCCESS;
 
  /* Set chip select Low at the start of the transmission */
  ADE7880_CS_LOW();
 
  /* Send the write command and the write address first ... */
#if defined(ADE7880_USE_DMA) && (ADE7880_USE_DMA == 1)
  /* setup DMA transmit */
  LL_DMA_ConfigAddresses(DMA2, LL_DMA_STREAM_1, (uint32_t) pld, LL_SPI_DMA_GetRegAddr(ADE7880_SPI), LL_DMA_GetDataTransferDirection(DMA2, LL_DMA_STREAM_1));
  LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_1, sizeof(pld));
  LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_1);
 
  /* wait for DMA transmit to finish */
  osSemaphoreWait(Ade7880EventTx, osWaitForever);
#else
  status = SPIx_WriteBlock(ADE7880_SPI, pld, sizeof(pld));
#endif
  if (status) {
    dbg_out("[SPI] set write op & address failed\n");
    return;
  }
 
  /* ... and then send the data to the device (MSB First) */
#if defined(ADE7880_USE_DMA) && (ADE7880_USE_DMA == 1)
  /* setup DMA transmit again */
  LL_DMA_ConfigAddresses(DMA2, LL_DMA_STREAM_1, (uint32_t) buffer, LL_SPI_DMA_GetRegAddr(ADE7880_SPI), LL_DMA_GetDataTransferDirection(DMA2, LL_DMA_STREAM_1));
  LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_1, numBytesToWrite);
  LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_1);
 
  /* wait for DMA transmit to finish */
  osSemaphoreWait(Ade7880EventTx, osWaitForever);
#else
  status = SPIx_WriteBlock(ADE7880_SPI, buffer, numBytesToWrite);
#endif
  if (status) {
    dbg_out("[SPI] write DMA payload failed\n");
    return;
  }
 
  /* Set chip select High at the end of the transmission */
  ADE7880_CS_HIGH();
}

The following contains the relevant DMA handlers and the corresponding callbacks:

void DMA2_Stream0_IRQHandler(void)
{
  if (LL_DMA_IsActiveFlag_TC0(DMA2)) {
    LL_DMA_ClearFlag_TC0(DMA2);
    ADE7880_RxCompleteCallback();
  } else if (LL_DMA_IsActiveFlag_TE0(DMA2)) {
    // todo: call ADE7880 rx error handler
    while (1) {
      __NOP(); // for debugging but breakpoints here are never hit
    }
  }
}
 
void ADE7880_RxCompleteCallback(void)
{
  LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_0);
  osSemaphoreRelease(Ade7880EventRx);
}
 
void DMA2_Stream1_IRQHandler(void)
{
  if (LL_DMA_IsActiveFlag_TC1(DMA2)) {
    LL_DMA_ClearFlag_TC1(DMA2);
    ADE7880_TxCompleteCallback();
  } else if (LL_DMA_IsActiveFlag_TE1(DMA2)) {
    // todo: call ADE7880 tx error handler
  }
}
 
void ADE7880_TxCompleteCallback(void)
{
  LL_DMA_DisableStream(DMA2, LL_DMA_STREAM_1);
  osSemaphoreRelease(Ade7880EventTx);
}

Here is the top-level SPI read function, where the read part never completes:

static void ADE7880_Read(uint16_t readAddr, uint8_t* buffer, uint16_t numBytesToRead)
{
  /* Encode command and read address */
  uint8_t     pld[3] = { FIRST_READ_COMMAND_BYTE, (uint8_t)((readAddr >> 8) & 0x00ff), (uint8_t)(readAddr & 0x00ff) };
  ErrorStatus status = SUCCESS;
 
  ADE7880_CS_LOW();
 
  /* Send the read command and address first ... */
#if defined(ADE7880_USE_DMA) && (ADE7880_USE_DMA == 1)
  /* setup DMA transmit */
  LL_DMA_ConfigAddresses(DMA2, LL_DMA_STREAM_1, (uint32_t) pld, LL_SPI_DMA_GetRegAddr(ADE7880_SPI), LL_DMA_GetDataTransferDirection(DMA2, LL_DMA_STREAM_1));
  LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_1, sizeof(pld));
  LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_1);
 
  /* wait for DMA transmit to finish */
  osSemaphoreWait(Ade7880EventTx, osWaitForever);
#else
  status = SPIx_WriteBlock(ADE7880_SPI, pld, sizeof(pld));
#endif
  if (status) {
    dbg_out("[SPI] set read op & address failed\n");
    return;
  }
 
  __NOP(); // start debugging from here and check registers
 
  /* ... and then receive the data that the device is sending (MSB First) */
#if defined(ADE7880_USE_DMA) && (ADE7880_USE_DMA == 1)
  /* clear flags */
  LL_SPI_ClearFlag_OVR(ADE7880_SPI); // this is because of half-duplex
  LL_DMA_ClearFlag_DME0(DMA2);
  LL_DMA_ClearFlag_FE0(DMA2);
  LL_DMA_ClearFlag_HT0(DMA2);
  LL_DMA_ClearFlag_TE0(DMA2);
  LL_DMA_ClearFlag_TC0(DMA2);
  /* setup DMA receive */
  LL_DMA_ConfigAddresses(DMA2, LL_DMA_STREAM_0, LL_SPI_DMA_GetRegAddr(ADE7880_SPI), (uint32_t) buffer, LL_DMA_GetDataTransferDirection(DMA2, LL_DMA_STREAM_0));
  LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_0, numBytesToRead);
  LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_0);
 
  /* wait for DMA receive to complete */
  osSemaphoreWait(Ade7880EventRx, osWaitForever);
#else
  status = SPIx_ReadBlock(ADE7880_SPI, buffer, numBytesToRead);
#endif
  if (status) {
    dbg_out("[SPI] read payload failed\n");
    return;
  }
 
  ADE7880_CS_HIGH();
}

Thanks!

6 REPLIES 6
Todor Todorov
Associate II

I believe I need to clarify my first post.

Apparently, SPI half-duplex means to use the MOSI line bidirectionally. That's not what I do. My communication requires that both MISO and MOSI are used, but SPI read and write from/to slave happen "asynchronously". To write data to the slave requires to SPI writes: the first write sends the write command with the start address where the data is written, and the second write contains the data itself. To read from the slave requires one SPI write to send the command and the address to read from, followed by the SPI read to receive the data.

Thanks for any help!

Do you see expected waveform on the SCK pin?

Polled communication works?

JW

Todor Todorov
Associate II

Hi and thanks for responding!

Yes, polled communication works 100%.

When using DMA, writing to the device works as expected. When reading from the slave, only the first part of the "transaction" works - when the master sends the read command and the register to read from. But the following spi read to actually receive the data does not happen.

As suggested, I hooked up a logic analyzer and decoded (part of) the SPI stream. You can see in the screenshot - every red frame is one SPI transaction. Every transaction consists of 2 SPI operations. Writing to the slave is 2 SPI writes (one command+address, followed by one with data). Reading from the slave is 1 SPI write (command+address) followed by 1 SPI read (receiving the data). The first 2 frames can be seen to be successful, while the last frame fails after the SPI write cmd+address. Because I don't see any waveform on the SCK pin after the end of the write, I expect the error is in my code, since polling works and the slave responds as expected.

0693W00000D18owQAB.pngIf you have a look in my code above, the functions ADE7880_Read and ADE7880_Write each implement a full SPI transaction - first DMA transfer is set up and started to write the command+address, then the code waits for the semaphore, then the second DMA transfer follows, and lastly the code waits for the corresponding semaphore before the CS is set high.

But I have no idea where my error is - did I forget to clear a flag? Did I set up the DMA incorrectly? I can't see it :loudly_crying_face:

Thank you for any further suggestions!

Follow the progress of your code by instrumenting its various parts - toggle one or more pins and observe them using the LA, write to volatile variable and observe in debugger, etc. Note, that

Read out and check/post content of DMA registers, including the status registers.

Note, that debugging may be intrusive so don't observe SPI registers in debugger while running the code.

JW

Petr Sladecek
ST Employee

Hello, I've notice, you've applied DMA stream 0 for reading phase only. That is fine that you are ready for reception but the SPI will never continue at any communication if there is no data to transmit at the IP full duplex mode configuration. You need to force output of some dummy data via DMA stream 1 at this phase , too, else you cannot move ahead or reconfigure the SPI into simplex receiver at second phase of the reading command.

Best regards,

Petr, the IP owner, STMicroelectronics

ATzou.1
Associate II

I wonder if this is related to my post. I'm using an STM32G474RE and my DMA TX and RX 16-bits show up on the logic analyzer but my SPI transaction is stuck in the while loop for 'TRANSFER_WAIT' and never gets to 'TRANSFER_COMPLETE'. Even though the SPI RX data is on the line as viewed with my Logic Analzyer/Decoder. SPI RX data is not getting into the MCU. I'm using the HAL libraries.

Post for my title is SPI DMA Master to SPI DMA Slave works in 8-bit but hangs on 16-bit mode