2
\$\begingroup\$

I am simulating a project where two STM32s communicate with each other through SPI (HAL drivers), where the SPI Master is an STM32F4 and the SPI Slave is an STM32F3. Basically, the master will send 5 bytes of data (1 command byte followed by 4 dummy bytes) and the slave will respond back with 4 bytes of data to the master depending on the first command byte received.

How the system should operate: Pressing the user button on the master will send SPI data (in non-interrupt mode) to the slave (and the slave will receive the data in interrupt mode). The slave then sends data back to the master depending on the first received byte. There is a blue LED indicator on the slave device whether an SPI data has been received, and a green LED indicator on the master that indicates whether it receives the correct sequence of data from the slave. Below is how the clocked data should be sent/received by the master:

Clocked SPI Data Exchange

Slave device issue: For the first few button presses, the slave successfully receives data from the master. After a few cycles, from observing the LEDs and setting breakpoints in Debug mode, I noticed that the slave starts to receive data after every 2 button presses on the master. In the slave device, the data received is correct. I also have to note that the interrupt service routine on the master executes at every button press, so SPI data should be sent at every button press.

Master device issue: The master never receives the correct sequence of data. If, for example, the slave sends the data: 0x01, 0x02, 0x03, 0x04 in that order, the master would receive the data in this order: 0x02, 0x03, 0x04, 0x01. Furthermore, this also happens every 2 button presses.

This is what the event looks like:

  1. n button press:
    • Slave: Data is correctly received by slave (and sends data back to the master)
    • Master: Garbage value received
  2. (n+1) button press:
    • Slave: Data is not received by slave (and does not send data back because slave sends data based on received data)
    • Master: Relevant values received but in incorrect order
  3. (n+2) button press (same as what happened on the n-th button press):
    • Slave: Data is correctly received by slave (and sends data back to the master)
    • Master: Garbage value received and so on.

Here is the SPI configuration code for the Master:

static void MX_SPI1_Init (void) {

   /* SPI Configuration for the STM32F4 (Master) */
   /* APB2 Clock set to 8MHz to match APB2 clock of Slave*/

   hspi1.Instance = SPI1;
   hspi1.Init.Mode = SPI_MODE_MASTER;
   hspi1.Init.Direction = SPI_DIRECTION_2LINES;
   hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
   hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
   hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
   hspi1.Init.NSS = SPI_NSS_SOFT;
   hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
   hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
   hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
   hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
   hspi1.Init.CRCPolynomial = 7;

   if (HAL_SPI_Init(&hspi1) != HAL_OK) {
      Error_Handler();
   }
}

The SPI configuration for the Slave:

static void MX_SPI1_Init (void) {

   /* SPI Configuration for the STM32F3 (Slave) */
   /* APB2 Clock is 8MHz */

   hspi1.Instance = SPI1;
   hspi1.Init.Mode = SPI_MODE_SLAVE;
   hspi1.Init.Direction = SPI_DIRECTION_2LINES;
   hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
   hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
   hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
   hspi1.Init.NSS = SPI_NSS_SOFT;
   hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
   hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
   hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
   hspi1.Init.CRCPolynomial = 7;
   hspi1.Init.CRCLength = SPI_CRC_LENGTH_DATASIZE;
   hspi1.Init.NSSPMode = SPI_NSS_PULSE_DISABLE;

   if (HAL_SPI_Init(&hspi1) != HAL_OK) {
      Error_Handler();
   }
   
   /* Set interrupt priority and enable interrupts for SPI1 */
   HAL_NVIC_SetPriority(SPI1_IRQn, 1, 0);
   HAL_NVIC_EnableIRQ(SPI1_IRQn);
}

Here is the relevant code for the Master (the Push Button interrupt segment):

void HAL_GPIO_EXTI_Callback (uint16_t GPIO_Pin) {

   /* Clear wake-up power flag */
   __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);

   if (GPIO_Pin == PUSH_BUTTON_PIN) {
      /* Transmit one command byte located in pTxBuff array of length cTxLen */
      HAL_SPI_Transmit(&hspi1, (uint8_t*)pTxBuff, cTxLen, spi_timeout);

      /* Wait for end of SPI transmission */
      while(HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY);

      /* Transmit dummy values located in pTempTxBuff and store retrieved relevant data in pRxBuff */
      HAL_SPI_TransmitReceive(&hspi1, (uint8_t*)pTempTxBuff, (uint8_t*)pRxBuff, cRxLen, spi_timeout);

      /* Flash LED if correct sequence stored in pRxBuff was received - cRxLen = 4 */
      if((pRxBuff[0] == DATA0) && (pRxBuff[1] == DATA1) && (pRxBuff[2] == DATA2) && (pRxBuff[3] == DATA3)) {
         /* Toggle dedicated green LED */
         ToggleLED();
      }

}

And the relevant code for the Slave:

void HAL_SPI_RxCpltCallback (SPI_HandleTypeDef *hspi) {
   /* Toggle blue LED when SPI receives data through interrupt */
   ToggleLED();

   /* Check the data received through SPI interrupt stored in pRxBuff[0] (array is of size 1) */
   if (pRxBuff[0] == COMMAND_DATA) {

      /**
       * pTxBuff := contains appropriate data to be sent to Master device based on this specific command
       * pTempRxBuff := array to store dummy variables, this array is of size 4
       */
      HAL_SPI_TransmitReceive(&hspi1, (uint8_t*)pTxBuff, (uint8_t*)pTempRxBuff, cTxLen, spi_timeout);
   }

   /* Set device in SPI Receive interrupt mode again and store received data in pRxBuff of size 1 */
   HAL_SPI_Receive_IT(&hspi1, (uint8_t*)pRxBuff, cRxCmdLen);
}

I do not understand where the error is here. If it is only the master sending the data to the slave device, everything works fine. I have also matched the SPI configuration and bus clock speeds. I also tried changing the direction line to SPI_DIRECTION_1LINE as suggested elsewhere but it did not work. I also attempted using HAL_SPI_Transmit() and HAL_SPI_Receive() separately instead of HAL_SPI_TransmitReceive(), but the same issues still appear. Also, I would like to use HAL_SPI_TransmitReceive() on both to verify on the Master and the Slave that the devices are communicating properly.Any help would be greatly appreciated.

Thank you!

\$\endgroup\$
9
  • 3
    \$\begingroup\$ Sounds like you may have a sync problem either between trials or between expectation and reality. Consider using a scope or getting a cheap USB logic analyzer to run with sigrok. \$\endgroup\$ Commented Jul 11, 2020 at 2:12
  • 2
    \$\begingroup\$ You should not be doing all those SPI calls inside HAL_GPIO_EXTI_Callback. That callback is called directly from the EXTI interrupt handler, so you're then doing those blocking SPI calls from within the EXTI interrupt. Rather just set a volatile flag from EXTI and then in your main loop, do the SPI operations when it see's the flag get set.. \$\endgroup\$
    – brhans
    Commented Jul 11, 2020 at 3:04
  • 2
    \$\begingroup\$ Same goes for HAL_SPI_RxCpltCallback in the slave. You shouldn't be doing the blocking HAL_SPI_TransmitReceive from inside a callback (which is actually still inside the interrupt). Either use another volatile flag there, or do HAL_SPI_TransmitReceive_IT instead and make your HAL_SPI_RxCpltCallback stateful so that it remembers whether it last did a HAL_SPI_TransmitReceive_IT for 4 bytes or a HAL_SPI_Receive_IT for 1 byte. \$\endgroup\$
    – brhans
    Commented Jul 11, 2020 at 3:08
  • 2
    \$\begingroup\$ Also make sure that your trigger switch is properly debounced so that you are not accidentally initiating the SPI trigger sequences on top of each other. This is the first thing I thought of as I read your question. \$\endgroup\$ Commented Jul 11, 2020 at 5:20
  • \$\begingroup\$ @ChrisStratton What would the solution be if there are sync problems? One of the sync problems I could think of is that the Master's second transmission would probably start earlier because it will take time for the Slave device to check the first command byte sent by the Master. Do we also have to make the system clocks on both devices the same? The Master is currently operating at 16MHz but the Slave is operating at 8MHz. My assumption is that it should be fine because the SPI transaction would be clocked by the SPI Clock. \$\endgroup\$
    – Cimory
    Commented Jul 11, 2020 at 23:58

2 Answers 2

1
\$\begingroup\$

In SPI, MASTER and Slave are actually connected in a loop to the FIFO buffers. As shown in the picture.
Try flushing these data buffers and shift registers before each data frame transfer. On both master and slave controllers.

Diagram of SPI communication

(Image source: Electronics Hub - Basics of Serial Peripheral Interface (SPI))

\$\endgroup\$
2
  • \$\begingroup\$ Embedded Guy - Hi, As required by this site rule, when we include something in an answer (e.g. photo, image or text) which isn't our own original work, we need to properly reference (cite) it. That image has come from somewhere else, so in order to comply with that rule, it must be referenced. I found (what I believe to be) the original source and added that reference for you this time. Please can you do that in future? Thanks! (I recommend that you also read the tour and help center to see more site rules.) \$\endgroup\$
    – SamGibson
    Commented May 26, 2021 at 14:16
  • \$\begingroup\$ Embedded Guy - Dead on, STM32 has SPI fifo buffers that make it very easy to have data alignment errors. \$\endgroup\$
    – Ocanath
    Commented Jul 11, 2022 at 14:09
0
\$\begingroup\$

In general, I strongly recommend not using the HAL for communication code. For SPI, UART and I2C, it only really works in the 'happy path' scenario, and it does not generally fail elegantly (permanent data alignment issues, etc.). It's also very bloated and slow, which can create issues if you're trying to do things that require a lot of speed (like parse an SPI frame while you are actively receiving it, as is the case here). The STM32Cube generated code sets up the interrupt handler functions for you in the stm32xx_it.c file; you can comment out the generated HAL handlers and put your own in. Remember to clear the interrupt flags before the function returns!

Some thoughts on what is going wrong, and some advice as you continue working on this project:

  1. You're not using the HAL correctly. Actually, it is impossible to do what you want to do correctly using the HAL at all. To have the slave respond with new data based on the first byte received from the master, you need to have code to parse the first byte from within the SPI interrupt handler itself, not the callback. The HAL callback functions are called inside the SPI interrupt handler, after a complete frame has been received (i.e. frame of expected size with a rising nSS transition). In order to implement the system as described, you will have to write your own SPI interrupt handler.

  2. As Embedded Guy pointed out, STM32 SPI peripherals have a FIFO buffer which can be filled with junk due to software bugs (on the slave or master side), spurious clock edges caused by noise, etc. Unfortunately, there is no explicit 'clear fifo' command that can be issued from within the peripheral itself. The obvious next idea, of using the RX/TX data registers to flush the FIFO, is not 100% effective either (although it may be good enough in most cases). For instance, in a situation where you have received 5/8 bits of a word, reading the RX data register will not flush this incomplete byte out of the FIFO. The only consistently reliable way I have found to clear the SPI FIFO buffers is to use the RCC (reset clock controller) peripheral to fully reset the SPI peripheral, then rapidly reinitialize it. Fortunately this isn't as terrible as it seems, since the peripheral can be fully reinitialized with only a small handful of register writes and can be effectively done from within an interrupt handler.

Final advice: It may not be good system design to try and have your slave parse the first received byte while actively receiving an SPI frame. This is simply because SPI can run so fast, that you might not have finished parsing the first byte and preparing the response bytes by the time the master has finished clocking the first one (you have a very limited time frame to do so, only the width of one clock transition!). If your SPI clock is slow, and you have a very simple and fast parser running inside the interrupt handler, you can probably get away with this specification. However it might be better to send the 'command' byte in a complete SPI frame, then issue a separate multi-byte read frame. In this scenario the slave would have a much greater amount of time to parse the master command before having to prepare a reply message.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.