Skip to main content
Visitor II
October 26, 2018
Question

HAL_SPI_Transmit_IT blocks

  • October 26, 2018
  • 13 replies
  • 6674 views

Hello,

I've a problem with HAL_SPI_Transmit_IT() function as it blocks similar to HAL_SPI_Transmit.

Microcontroller: STM32F205VCT6

Compiler: arm-none-eabi-gcc, gcc version 7.3.1 20180622 (release)

[ARM/embedded-7-branch revision 261907] (15:7-2018-q2-4)

Code is generated by STM32CubeMX, Version 4.27.0

Activated peripherals:

SPI3 (Transmit Only Master), SPI3 global interrupt enabled

GPIOA.9 as output

My own code:

uint8_t buffer[3] = { 1, 2, 3 };
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9, GPIO_PIN_RESET);
HAL_SPI_Transmit_IT(&hspi3, buffer, 1);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9, GPIO_PIN_SET);

If I hand over one single byte to HAL_SPI_Transmit_IT() it does not work non-blocking as you can see at the following scope.

0690X000006CGEWQA4.png

If I hand over more than one byte (e.g. 3 bytes), HAL_SPI_Transmit_IT() works correctly (non-blocking).

0690X000006CGEbQAO.png

What could be the reason? Why can't I send one single byte in non-blocking mode?

    This topic has been closed for replies.

    13 replies

    Super User
    October 26, 2018

    Look at the HAL SPI source code, starting with SPI_Transmit_IT(). It enables the Tx interrupt, which fires immediately because the TX data reg is empty. SPI_TxISR_8BIT() writes 1 byte to the TX data reg. So far so good. But, since your are only sending 1 byte, the SPI_TxISR_8BIT() sees that it is done sending data and calls SPI_CloseTx_ISR() to start shutting down the SPI Tx function. SPI_CloseTx_ISR() calls SPI_EndRxTxTransaction(), which waits for the SPI to be idle before returning. So when sending 1 byte, your call to SPI_Transmit_IT() ends up not returning until the byte has been sent (shifted out), as you see in your scope traces.

    Marc1Author
    Visitor II
    October 27, 2018

    Hi Bob,

    Thank you for the explanation.

    Now let's send 3 bytes.

    HAL_SPI_Transmit_IT(&hspi3, buffer, 3);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9, GPIO_PIN_RESET);
     /* Check Busy flag */
     if(SPI_CheckFlag_BSY(hspi, SPI_DEFAULT_TIMEOUT, tickstart) != HAL_OK)
     {
     SET_BIT(hspi->ErrorCode, HAL_SPI_ERROR_FLAG);
     }
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9, GPIO_PIN_SET);

    Please have a look at the scope.

    0690X000006CGeWQAW.png

    It seems that every last byte is sent in blocking mode.

    I can't believe it.

    Marc1Author
    Visitor II
    October 27, 2018

    Another test:

    HAL_SPI_Transmit_IT(&hspi3, buffer, 3);
    while(true) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_9); }

    0690X000006CGebQAG.png

    This is called "non-blocking transmission". Horrible!

    Super User
    October 27, 2018

    The reason is simple: the "library" wants to ensure that by the time the "finished" callback is called, data are all out. Otherwise, relying only upon the Tx interrupt only it would signal "finished" one frame earlier, when the last frame leaves the holding register and is transferred to the shift register.

    There's of course a workaround, using the Rx interrupt, but even there with one of the CPHA settings you can get the interrupt a tad bit earlier than the last clock edge rolls out, so you may need to take some measures for that case anyway.

    By using a "library" you decided to live with whatever design decisions the "library" authors chose. The chips have an order of magnitude more usage modes, but "libraries" inevitably implement only a limited subset, presumably of the most often used ones.

    In other words: Are you dissatisfied with other's code? Feel free to write your own.

    JW

    Marc1Author
    Visitor II
    November 2, 2018

    Hello Jan,

    thank you for the detailed explanation and the hint with the workaround.

    > There's of course a workaround, using the Rx interrupt, but even there with one of the CPHA settings you can get the interrupt a tad bit earlier than the last clock edge rolls out, so you may need to take some measures for that case anyway.

    My first workaround was using a timer to predict the end of transmission. But using the RX interrupt is a smarter option. Even with CPHA = 1 timing is good.

    > By using a "library" you decided to live with whatever design decisions the "library" authors chose.

    It was not my intention to criticize the HAL libraries. My first thought was that "blocking" must be a bug or missconfiguration. But even though I use my own code now.

    Marc

    Super User
    October 29, 2018

    At the risk of paraphrasing an ex president, it depends on what your definition of "blocking" is. HAL_SPI_Transmit_IT() function is indeed written so that it (tries to) return as soon as it has configured everything to transmit (EDIT: "but the interrupt routines get in the way"). It turns out that the return is interrupted by the transmit ISR, which writes the first byte to the SPI port. If there are more bytes to send, the ISR exits. BUT, the SPI data register has a 4-byte (32-bit) FIFO. So before HAL_SPI_Transmit_IT() can return, the ISR is called a second time** to write the 2nd data byte. The ISR will be called up to 4 times, one right after the other, until the FIFO is full and the non-iterrupt code in HAL_SPI_Transmit_IT() gets a chance to finish and return. And when the ISR writes the byte it ends up waiting for the data to finish shifting out as I mentioned above.

    If you really need to be doing something else while ALL of the SPI data is sent, use DMA. Though you will need to experiment and/or look at the HAL code to see what DMA does after it writes the last byte to the SPI data register (i.e. how it handles "Tx complete").

    ** The first ISR may actually never return, since the STM32 has logic that chains interrupt handling if an interrupt is pending when a "return from interrupt" instruction is encountered.

    Marc1Author
    Visitor II
    November 2, 2018

    Hello Bob,

    thank you for your reply. Meanwhile I fixed the problem by creating my own code and following the idea of @Bob S​ (using RX interrupt).

    > If you really need to be doing something else while ALL of the SPI data is sent, use DMA. Though you will need to experiment and/or look at the HAL code to see what DMA does after it writes the last byte to the SPI data register (i.e. how it handles "Tx complete").

    I think there is no benefit of using DMA if you need an interrupt for each byte to detect the end of transmission correctly. I might be wrong.

    Marc

    Super User
    November 2, 2018

    If you don't want an RX interrupt on every byte, use a combination of DMA and RX IRQ. Configure the SPI port for master tx and rx mode (the rx data is "don't care", you never actually use it and don't even need to enable the pin asociated with it). With DMA and SPI idle, start transmitting using HAL_SPI_Transmit_DMA(). In the "DMA complete" callback, enable the SPI RXNE interrupt. When the SPI RXNE interrupt function is called, the SPI port is done transmitting your data (if you care when it is done). At least according to the diagram RXNE goes high the same time BUSY goes low.

    So, no RX interrupt on every byte, and only 2 interrupts: DMA Tx complete and SPI RXNE.

    Marc1Author
    Visitor II
    November 5, 2018

    Thank you for the further suggestion. I may test it later. Currently I'm happy whith my interrupts. Btw. I don't use the HAL libraries any more.

    Visitor II
    March 19, 2019

    @Marc​ , what other library do you use then? or do you make changes at a register level?

    Marc1Author
    Visitor II
    March 19, 2019

    I only use the CMSIS libraries.

    Visitor II
    March 19, 2019

    For SPI master, transmission complete comes from RXNE. For generating the clocks, transmission is responsible.

    Visitor II
    November 22, 2019

    I have a similar problem but, in my case, I use DMA on a slave SPI using Keil RTX OS.

    In my scenario the SPI_DMATransmitCplt function is called inside ISR but, since systick is used for timeout, in case of error (when timeout could happen) systick is not incremented because the systick interrupt has lower priority than DMA interrupt; final result is a complete freeze into an endless loop iside DMA ISR.

    I don't want to increase systick interrupt priority to avoid unpredictable behaviour in case of task switch inside ISR so I soved the problem creating a new DMA ISR function to set a thread flag to unblock a dedicated task that will execute the SPI_DMATransmitCplt. In this way ISR function will always exit and timeout is handled on a dedicated task leaving RTOS run, increment systick and making timeout expire.

    I don't like to modify standard HAL libraries and this was the only solution I found.

    Has anyone had the same issue and maybe solved it in a smarter way?

    Thanks,

    Davide.

    Super User
    November 22, 2019

    You've been told several times in this thread already - Rx completion indicates Tx completion too.

    JW