STM32 stuck (freeze) after using SPI transfer in interrupt mode - Page 1
Forum section: electronics / general

author: nishantnidaria
author_rank: Contributor
timestamp: November 24, 2021, 08:56:42 am
content: Greetings!I am using an STM32G030C8T6 on a custom PCB everything seems to be working fine but "sometimes" randomly my MCU gets stuck after i send (using SPI) battery voltage to the Master device, it happens randomly there's no pattern i could follow.Note: my STM32 is a slave device and the master device is a Raspberry PiSTM32 receives data from raspberry pi in while loop using the following function/code:Code:void SPI_Receive_Commands(void){while(HAL_GPIO_ReadPin(SPI_SS_GPIO_Port, SPI_SS_Pin) == GPIO_PIN_RESET);{HAL_SPI_Receive(&hspi1, (uint8_t *)spi_buf, 10, 100);if(spi_buf == 'v') // Battery voltage function{memset(spi_buf,'*',10);Battery_Voltage();HAL_GPIO_WritePin(GPIOA, Voltage_Ready_Pin, GPIO_PIN_SET); // Indicating Raspberry pi that the transmission is startingHAL_SPI_Transmit_IT(&hspi1, (uint8_t *)batteryVoltage, 6); // Sending voltage (e.g., 13.54)HAL_SPI_Transmit(&hspi1, (uint8_t *)"v", 1, 100); // Sending the last character 'v' to finish transmissionHAL_GPIO_WritePin(GPIOA, Voltage_Ready_Pin, GPIO_PIN_RESET); // Indicating Raspberry pi that the transmission has finishedprintmsg("Sending battery voltage\r\n");}}}Thank you in advance for your help and guidance!

author: nishantnidaria
author_rank: Contributor
timestamp: November 24, 2021, 10:13:47 am
content: Yes when i say interrupt mode i only meant for the transmission part,I don't have a mechanism to make sure first call has finished transmitting before using hspi1 again. The sending part only stops after sending the character 'v' and then pulling the GPIO low again.I used _IT function because i am sending withing the receive loop.Yes SS pin going to the peripheral is actually toggling as expected.

author: nishantnidaria
author_rank: Contributor
timestamp: November 24, 2021, 11:03:33 am
content: Adding a HAL_Delay(1000) between these two will help MCU not get stuck anymore ?something like this:Code:HAL_SPI_Transmit_IT(&hspi1, (uint8_t *)batteryVoltage, 6); // Sending voltage (e.g., 13.54)HAL_Delay(1000);HAL_SPI_Transmit(&hspi1, (uint8_t *)"v", 1, 100); // Sending the last character 'v' to finish transmission

author: Siwastaja
author_rank: Super Contributor
timestamp: November 24, 2021, 11:25:10 am
content: It doesn't help that ST's documentation,https://www.disca.upv.es/aperles/arm_cortex_m3/llibre/st/STM32F439xx_User_Manual/group__spi__exported__functions__group2.html#gafbb309aa738bb3296934fb1a39ffbf40- title says "user manual", does not actually document anything."Transmit an amount of data in non-blocking mode with Interrupt." is the full description of the function, no instructions let alone an example how to use it - go figure. So we can only guess.Construct the full packet first, do not try to append using two transmit function calls; just use HAL_SPI_Transmit once since apparently you are fine with the blocking call. If you call transmit function twice, you probably "should" wait for the the first to finish before the second, but since this is undocumented, I don't know.Assuming HAL_SPI_Transmit returns only after the transmission is fully completed (full description in the manual: "Transmit an amount of data in blocking mode. "), that one works as it should, but for the interrupt version, you should write an interrupt handler that waits for the completion. There probably is some HAL function you should call in the ISR, but since the official documentation does not document this, it's beyond anyone's guess.I would hazard a guess there is a separate document, the actual HAL user manual, titled something different from user manual. You should try to find this documentation and read it to understand how to use the HAL functions.My recommendation is to use SPI bare metal, bypassing these extra layers of complications, replacing them with other complications.

author: AndyC_772
author_rank: Super Contributor
timestamp: November 24, 2021, 12:20:33 pm
content: It's timing, again.You're:- indicating to the Rpi that dataisavailable- then, bearing in mind the Rpi is free to act upon that indication at any time, preparing a response and entering a loop that will send it to the SPI portThis can't work reliably, because:- the Rpi CPU is much faster than the STM32, so it can quite reasonably be expected to initiate an SPI read before the first byte is actually in the Tx shift register, and- your loop must guarantee to keep up with SPI reads, which means it needs to be tight, fast, and have interrupts disabledYouneedDMA for this. Prepare the response, store it in memory, set up a DMA channel to transmit it via SPI. Then, and only then, raise the flag to the Rpi.I agree that the HAL isn't really beneficial here - it's just a layer of obfuscation over and above the underlying hardware, which you need to know all about anyway.

author: DavidAlfa
author_rank: Super Contributor
timestamp: November 26, 2021, 09:18:02 pm
content: Don't blame HAL here, most of the code is badly implemented.You might need to add some check or wait for the SS pin to go high after the transmission.If the Rpi doesn't set SS pin high fast enough, the function might loop and stall waiting for clocks that will never come.Anyways, this seems wrong, the ";" will make the while to loop over itself, instead doing the following code loop.SS is active low, right? And you want to receive data. But this will do absolutely nothing while SS pin is low!Code:while(HAL_GPIO_ReadPin(SPI_SS_GPIO_Port, SPI_SS_Pin) == GPIO_PIN_RESET); // Do nothing while SS=0. Remove ";" !{The rest of the code will will mainly fail because of this:Code:HAL_SPI_Transmit_IT(&hspi1, (uint8_t *)batteryVoltage, 6);// IT Transmission starts. This is a NON BLOCKING function!HAL_SPI_Transmit(&hspi1, (uint8_t *)"v", 1, 100);// But you inmediately start another transmission, without checking if the previous one was finished, this will likely fail and you're not checking the resultHAL_GPIO_WritePin(GPIOA, Voltage_Ready_Pin, GPIO_PIN_RESET);// And now you're resetting the pin, which will break the ongoing communication with the RpiInterrupts are ok for slow transmissions like serial and such, or transferring blocks using dma, but for single bytes in a fast spi, it can actually perform slower, as in will add the interrupt overhead for every byte.SPI is fast enough to do it without interrupts, and DMA is not needed for this, but you must add a small delay in the Rpi after setting low SS pin (100us should be more than enough, but you must test that), to let the stm32 notice and start the SPI receive.Also add that delay after detecting "Voltage_Ready_Pin", before sending the clocks to receive the data from the stm32.Ensure the spi clock isn't running crazy fast, like 50MHz. Try first with something lower, ex. 1MHz.I see you only check the first received byte to detect the answer type.Unless you're sending exactly 10 bytes from the Rpi, you'll be always losing 100ms here:Code:HAL_SPI_Receive(&hspi1, (uint8_t *)spi_buf, 10, 100);// Adjust the SPI receiving size to the real expected size, or it will always wait the whole timeout.Don't waste time clearing the buffer before sending data. Do it after that, when you're free!Code:memset(spi_buf,'*',10);You could also clear only the first byte to make it faster:Code:spi_buf=0;Also, build a single packet, don't break it in 2 separate transmissions, you will lose spi clocks between! Append 'V' in BatteryVoltage();Code:HAL_SPI_Transmit_IT(&hspi1, (uint8_t *)batteryVoltage, 6);// Sending voltage (e.g., 13.54)HAL_SPI_Transmit(&hspi1, (uint8_t *)"v", 1, 100);// Sending the last character 'v' to finish transmissionSimplify the code and check the spi transfer result so you can debug it:Code:void SPI_Receive_Commands(void){if(HAL_GPIO_ReadPin(SPI_SS_GPIO_Port, SPI_SS_Pin) == GPIO_PIN_RESET)// Check if SS is low (Transmission begins from Rpi){HAL_SPI_Receive(&hspi1, (uint8_t *)spi_buf, 10, 100);if(spi_buf == 'v') // Battery voltage function{Battery_Voltage();// Generate voltage string, appending 'V' and string null termination, ex. "13.54V\0"HAL_GPIO_WritePin(GPIOA, Voltage_Ready_Pin, GPIO_PIN_SET);// Indicating Raspberry pi that the transmission is startinguint8_t res = HAL_SPI_Transmit(&hspi1, (uint8_t *)batteryVoltage, 7, 100);// Send data, 100ms timeout, save resultswitch (res)// Check result{case HAL_OK:// Everything OKbreak;case HAL_ERROR:// Interface errorbreak;case HAL_BUSY:// Interface busybreak;case HAL_TIMEOUT:// Interface timeoutbreak;}HAL_GPIO_WritePin(GPIOA, Voltage_Ready_Pin, GPIO_PIN_RESET);// Indicating Raspberry pi that the transmission has finishedprintmsg("Sending battery voltage\r\n");// This can be slow if sending over serial in blocking mode!memset(spi_buf,'*',10);while(HAL_GPIO_ReadPin(SPI_SS_GPIO_Port, SPI_SS_Pin) == GPIO_PIN_RESET);// Wait for SS pin to go high, so we don't re-enter within the same transmission}}}

author: AndyC_772
author_rank: Super Contributor
timestamp: November 27, 2021, 11:54:37 am
content: Quote from: DavidAlfa on November 26, 2021, 09:18:02 pmWhy would you deliberately write code in such a way as to make it timing sensitive, when there's no need to?A signal which means "there will probably be data available in about 100us or so, provided you don't try to read it too quickly" is a great basis for a system which is flaky and unreliable, especially as it gets more complex and the various CPUs in the system become more heavily loaded.Compose the response, point a DMA channel at the SPI interface, then send the 'ready' signal.It's that simple. The RPi can read the data at any time once that 'ready' signal is active, the SPI clock can be as fast as the hardware physically supports, and the STM can go off and do other things rather than sitting in a tight loop waiting for the RPi.Creating a robust system is all about identifying the order in which things can happen, and ensuring there are no 'vulnerable' time windows during which things can break. If there's a 1 in a million chance of something happening at precisely the wrong time, but that particular event does happen a million times per second, then the system will fail once per second.

author: Siwastaja
author_rank: Super Contributor
timestamp: November 27, 2021, 01:46:58 pm
content: Adding a physical "data ready" signal for SPI slave sounds like overkill for intermittent battery voltage readings. The downside is now you are implementing both GPIO and SPI through the Raspberry, when you only really need SPI.Just design the SPI slave so that it can always react to the activating chip select signal in time, for example, by using interrupt on the falling nCS edge. Then do the magic in the interrupt handler in time; always post thelatestbattery voltage measurement.Now just poll the battery voltage from the Raspberry (master) side whenever you need that info. Problem solved.There are two usual patterns to do this, one is to "waste" the first byte (or two) of the SPI transaction, i.e., prepare the data during the frame. Another is to have master generate multiple frames, so that during the current frame, the response for thepreviousframe is being transmitted.Only when you actually need to haveeach and everybattery voltage sample, and every sample onlyonce, then you need to rethink this system, adding some kind of FIFO for example, where SPI access cycle pops one element, and the SPI response contains FIFO status (so that the master knows if it needs to read more packets).

author: DavidAlfa
author_rank: Super Contributor
timestamp: November 28, 2021, 08:48:58 pm
content: Are you really asking that?Should I explain why a 600MHz+ CPU shouldn't set SS pin and inmediately start sending spi clocks when at the other side you have a stm32 running at least 10x slower?Maybe 10us is enough, but you definitely need some time to start spi receive.Not flaky in any way, you need to adapt to the slower system, this is not an async, buffered transmission like ex. Uart, spi is much faster, and if you're the master you must then ensure you aren't going too fast for the slave devices.Or yes, you can use DMA, but it will need extra care and timeouts to ensure it doesn't stall in the middle of a transfer.For such simple messages, I would use uart, unless it's transfering other large chunks of data.

author: AndyC_772
author_rank: Super Contributor
timestamp: November 29, 2021, 01:46:45 pm
content: 1)BEready2)Thentell the other device you're readyFollow that simple rule and there's absolutely no issue about which device is faster.As a good rule of thumb, if you ever find yourself putting delays into your code without knowingfor certainhow long they need to be, and why they're needed, then you're doing something wrong that will come back to bite you (or whoever ends up taking over your project) later on.

author: DavidAlfa
author_rank: Super Contributor
timestamp: November 30, 2021, 08:17:07 am
content: I'm only saying you need to wait few clock cycles after you set SS low, the stm32 might be running some interrupt or whatever, also setting the spi peripheral takes some time.Just to ensure it catches the signal in time! Not about complicated delays needed when the programming flow is badly done.SPI always ready? Then you must use interrupts or dma, unless you main loop is fast enough to catch bytes as they come in real time, which I doubt...- Interrupt: Good luck receiving the first byte, entering the interrupt, reading the spi register and returning in time before the second byte arrives. Unless running at slow speeds, overflow coming!- DMA: Good idea, but must be very well done, regulary polling the dma to know if it has received the first byte and setting some kind of timeout.- In this modes, SS should be set to hardware mode. I hope this stm32 doesn't suffer of the silicon bugs affecting SS in slave mode!So, for only 10 bytes, I think you can skip all this drama and do it in the main loop.- Software SS mode makes life much easier. I'd make it open drain with a pullup resistor, so it can be used by master and slave.- Knowing the spi frequency and the largest transfer size, adjust the timeout accordingly, so you don't waste time waiting for nothing.- If the largest transfer takes 1ms, set 1.2, 1.5, 2ms timeout, but not 100ms!I'd probably do something like this:Rpi:SS is input when idling.To transfer, set SS pin low, wait few microseconds and start the spi.When SS reads low, it means the stm32 want to transfer (Ex. answer to our last message, or ask something). Again, wait few us before sending the clocks.Stm32:SS is input when idling.Check its state regularly from the main loop.Avoid dumb delays like DELAY_MS(100) or WHILE(WHATEVER TAKING LONG TIME). Use variables to hold the time and state machines.When read low (Active), quickly sets the spi in receiving mode.When stm32 wants to transfer, sets SS low and starts the spi slave transfer. Doesn't need to wait, as it depends on the clock from the master.Setting the spi peripheral takes some time,measure this time, it could be 1us or 10us, this is the time the Rpi should wait before sending clocks.Maybe it's only 1us, but ignoring it will break the whole spi transfer. Just one lost clock pulse will shift every received byte and stall because you'll never receive the last byte completely.

author: nishantnidaria
author_rank: Contributor
timestamp: November 30, 2021, 08:53:47 am
content: Thank you all for your helpful and thorough comments! much appreciated!!My STM32 is no longer getting stuck, My raspberry pi SPI clock is @1MHz and STM32 is @47MHz (i have checked for different clock values and it works perfectly without getting stuck). I have other problems now but i would make another post for that.