Skip to main content
Visitor II
February 26, 2019
Question

HAL_SPI_TransmitReceive_DMA() transmit interrupt always triggered after receive interrupt

  • February 26, 2019
  • 5 replies
  • 7200 views

I have a HAL_SPI_TransmitReceive_DMA() transaction that I am performing. The data can be seen on the line to properly get clocked out over SPI.

The problem is that in the receive interrupt, we kick off the next transaction (a DMA SPI write). This write fails because the HAL erroneously still has a lock on the DMA tx handler. The transmit eventually comes through, but we fail to write a transaction due to the lock.

I cannot figure out why the transmit interrupt does not fire before the rx interrupt. I did see that the SW priority of the DMA streams was the same, and the natural priority in that case is to prefer the lower stream number (which in my case was the rx stream); however, when I bumped up the priority of the sw dma stream priority to be higher than the rx stream, that did nothing to change the outcome.

    This topic has been closed for replies.

    5 replies

    Super User
    February 26, 2019

    If you transmit and receive the same number of bytes (frames, whatever), from the point of view of DMA, transmit is finished at the moment when the last byte (frame) is moved into the holding buffer, i.e. two frames before the end (there is the holding buffer and then the transmit buffer, i.e. the shift register itself). Reception of course is finished when the last byte (frame) is safely in.

    I'm not going to comment on Cube's "architecture"; I don't use Cube.

    JW

    Visitor II
    February 26, 2019

    by your definition, wouldn't that guarantee that the dma stream for the tx would occur before the corresponding rx? what conditions would not make that an invariant?

    Visitor II
    February 26, 2019

    i guess if there was a higher priority stream interrupt that was higher than the tx but lower than the rx, it could happen? although if that were the case, i'd expect it to still properly service the interrupts

    Visitor II
    February 27, 2019

    Actually I never use DMA TX interrupt, only use RX DMA interrupt to manage data flow. When using HAL you have to use SPI callback, not the DMA. And remember that HAL will activate all possible interrupts to be able to plumb any user callbacks.

    Visitor II
    February 27, 2019

    Got it, so you set up the DMA Tx, but disable it's interrupt, and let the DMA Rx interrupt manage the data flow. That sounds like a good solution, although it may be tricky to do if using the HAL. Thanks for the input.

    Visitor II
    February 27, 2019

    It's much easier to use HAL SPI+DMA than USART+DMA (where LL seems a better fit)

    Let me try to grap extracts of my working code for SPI Master 4 wires with SW NSS:

    Maybe this will give you some hints.

    // STM32L4R5 SPI Master 16 bit words 4 wire transfer
     
    HAL_StatusTypeDef SPIP_MasterConfigureSpi(SPI_HandleTypeDef *hspi){
     hspi->Instance = SPI2;
     hspi->Init.Direction = SPI_DIRECTION_2LINES;
     hspi->Init.CLKPhase = SPI_PHASE_1EDGE;
     hspi->Init.CLKPolarity = SPI_POLARITY_LOW;
     hspi->Init.DataSize = SPI_DATASIZE_16BIT;
     hspi->Init.FirstBit = SPI_FIRSTBIT_MSB;
     hspi->Init.TIMode = SPI_TIMODE_DISABLE;
     hspi->Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
     hspi->Init.CRCPolynomial = 7;
     hspi->Init.CRCLength = SPI_CRC_LENGTH_8BIT;
     hspi->Init.NSS = SPI_NSS_SOFT;
     hspi->Init.NSSPMode = SPI_NSS_PULSE_DISABLE;
     hspi->Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;//16;
     hspi->Init.Mode = SPI_MODE_MASTER;
     return (HAL_SPI_Init(hspi));
    }
     
    void SPIP_MasterConfigureDma(SPI_HandleTypeDef *hspi){
     /* Configure the DMA handler for Transmission process */
     pSPIP->hdma_tx->Instance = DMA1_Channel2;//DMA1_Channel5;
     pSPIP->hdma_tx->Init.Request = DMA_REQUEST_SPI2_TX;//DMA_REQUEST_1;
     pSPIP->hdma_tx->Init.Direction = DMA_MEMORY_TO_PERIPH;
     pSPIP->hdma_tx->Init.PeriphInc = DMA_PINC_DISABLE;
     pSPIP->hdma_tx->Init.MemInc = DMA_MINC_ENABLE;
     pSPIP->hdma_tx->Init.PeriphDataAlignment = /*DMA_PDATAALIGN_BYTE;*/ DMA_PDATAALIGN_HALFWORD;
     pSPIP->hdma_tx->Init.MemDataAlignment = /*DMA_MDATAALIGN_BYTE;*/ DMA_MDATAALIGN_HALFWORD;
     pSPIP->hdma_tx->Init.Mode = DMA_NORMAL;
     pSPIP->hdma_tx->Init.Priority = DMA_PRIORITY_HIGH;
     HAL_DMA_Init(pSPIP->hdma_tx);
     /* Associate the initialized DMA handle to the the SPI handle */
     __HAL_LINKDMA(hspi, hdmatx, *(pSPIP->hdma_tx));
     
     /* Configure the DMA handler for Reception process */
     pSPIP->hdma_rx->Instance = DMA1_Channel1;//DMA1_Channel4;
     pSPIP->hdma_rx->Init.Request = DMA_REQUEST_SPI2_RX;//DMA_REQUEST_1;
     pSPIP->hdma_rx->Init.Direction = DMA_PERIPH_TO_MEMORY;
     pSPIP->hdma_rx->Init.PeriphInc = DMA_PINC_DISABLE;
     pSPIP->hdma_rx->Init.MemInc = DMA_MINC_ENABLE;
     pSPIP->hdma_rx->Init.PeriphDataAlignment = /*DMA_PDATAALIGN_BYTE;*/ DMA_PDATAALIGN_HALFWORD;
     pSPIP->hdma_rx->Init.MemDataAlignment = /*DMA_MDATAALIGN_BYTE;*/ DMA_MDATAALIGN_HALFWORD;
     pSPIP->hdma_rx->Init.Mode = DMA_NORMAL;
     pSPIP->hdma_rx->Init.Priority = DMA_PRIORITY_HIGH;
     HAL_DMA_Init(pSPIP->hdma_rx);
     /* Associate the initialized DMA handle to the the SPI handle */
     __HAL_LINKDMA(hspi, hdmarx, *(pSPIP->hdma_rx));
    }
     
    // this code from ISR state machine...
     
    HAL_StatusTypeDef SPIP_MasterSerialTransferStart_ISR(SPIP_t* pSPIP){
     if(HAL_SPI_TransmitReceive_DMA(pSPIP->hspi, pSPIP->pMasterSerialTxBuffer, pSPIP->pMasterSerialRxBuffer, SERIAL_BLOCK_SIZE_WORD) != HAL_OK)
     TrapError();
     return HAL_OK;
    }
     
    (...)
     
    //==== ISR Callbacks =====================================================
    /* @brief TxRx Transfer completed callback.
     * @param hspi: SPI handle
     * @retval None */
    void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi){
     // we only need this in master mode!
     if(pSPIP->Device == IS_SLAVE) return;
     SPIP_InterruptBasedStateMachine(0); // what's next to do? next block or rise NSS
    }

    Visitor II
    February 27, 2019

    @KIC8462852 EPIC204278916, thanks for the snippet, although since you are using the HAL_SPI_TransmitReceive_DMA(), I don't see how you are avoiding using the DMA tx interrupt. That function calls HAL_DMA_Start_IT() for both TX and RX, and that function sets the TC interrupt bit for both. The HAL then uses its broken "locking" mechanism to lock the DMA handlers until the respective interrupt comes through.

    From your code above, I don't see how you're avoiding that.

    The ideal way to do this would be to do everything the same, except for the transmit, do not enable the TC interrupt and to unlock both handlers when the rx completes...then we're only relying on rx for flow control.

    Visitor II
    February 28, 2019

    The HAL in today's implementation will enable all interrupts in case user overrides any of the weak declared callbacks. So to get the peripheral state machine not lock up, just let it handle. When optimisation is required, LL or direct register manipulation maybe necessary. Since I transfer kbytes, the loss of performance is minor.

    Super User
    February 27, 2019

    Which Cube, and which version of it are you using?

    JW

    Visitor II
    February 27, 2019

    HAL 1.9, Cube 4.27.0

    Visitor II
    December 24, 2023

    I see the same problem, only not when using HAL_SPI_TransmitReceive_DMA. Using this function, the callback that is executed is HAL_SPI_TX_RX_COMPLETE_CB_ID and not HAL_SPI_RX_COMPLETE_CB_ID. Using this callback I am able to restart the next SPI transfer from the callback without any issues. Where I do see the problem is when using HAL_SPI_Transmit_DMA. Then, when restarting the next transfer from the HAL_SPI_TX_COMPLETE_CB_ID the lockup described in this thread occurs for me too. 

    Based on the info in this thread I understand now where the problem comes from. Actually I would expect that starting a new transfer from any SPI callback would be possible but unfortunatelly it is not. I consider this an error in the HAL layer which should be fixed by ST.

    I see three possible solutions:

    1: Only use HAL_SPI_TransmitReceive_DMA. When sending data, the DMA receive stream can be changed from the memory pointer being incremented on each byte transfer to no-increment. The DMA receive stream can then be set to a single byte (dummy) buffer. Likewise when receiving data do the same for the transmit DMA channel. The downside is that this breaks compatibility over different STM32 types since they have different DMA architectures. The proposed change would have to be made for different DMA architectures.

    2: Like solution 1 but use a dummy array instead of a single byte. Again when sending, set the DMA receive stream to this dummy buffer and vice versa for receiving. The downside of this is that the data length would be limited to the size of this dummy buffer. Of course this can be fixed by splitting a transfer in multiple transfers when the size of the dummy buffer is exceeded.

    3: Use the suggestion proposed in this thread to schedule the restart to a deferred interrupt based on using PendSV. The downside of this is that such a mechanism (Deferred Interrupt Handling) would always have to run at the lowest interrupt priority, being the priority of PendSV. I have some situations where SPI is running at the highest priority for performance reasons and when using a Deferred Interrupt Handler, the SPI priority would no longer be leading and hurt performance.

    In the end I chose for solution 2 with a buffer of a few hundred bytes. But again, I consider this a bug in the HAL layer which should be fixed by ST.