Skip to main content
Associate III
June 18, 2024
Solved

SPI MISO shifted

  • June 18, 2024
  • 24 replies
  • 11545 views

Hello ST Community, 

I have setup a SPI master and a SPI slave, both use DMA in Normal mode. The word size is 8-bits and the data size is 8-bytes. The problem is that the slave transmits messages which contains old data. 

When it should send:
MISO: { 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 }
it sends:
MISO: { 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01 }

When it should send:
MISO: { 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02 }
it sends:
MISO: { 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02 }

Nikolaj_TL_0-1718698066796.png

OBS: The debug gpio is toggled when the ISR finishes and when theTxBuffer is edited in main.



This pattern continues for every transfer it contains 3 old bytes.

Implementation (slave):
The slave updates the response in the main function.

 

int main(void)
{
 HAL_Init();
 SystemClock_Config();
 MX_GPIO_Init();
 MX_DMA_Init();
 MX_SPI1_Init();

 HAL_SPI_TransmitReceive_DMA(&hspi1, TxBuffer, RxBuffer, 8);

 while (1)
 {
	if(msgReceived == 1) {
		memset(TxBuffer, SPI_Counter, BUFFERSIZE);
		HAL_SPI_TransmitReceive_DMA(&hspi1, TxBuffer, RxBuffer, BUFFERSIZE); // update SPI transfer
		
		msgReceived = 0;
		HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_1);		// DEBUG
	}
 }
}

 

 

void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
	msgReceived = 1;
	SPI_Counter++;
	HAL_SPI_TransmitReceive_DMA(&hspi1, TxBuffer, RxBuffer, 8); // setup a new transfer
	HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_1);		// DEBUG
}

 


Setup:
SetupSetup

How can it be that the MISO transfer contains old data?
 

This topic has been closed for replies.
Best answer by PPopo.1

If the TxBuffer is modified while a DMA transfer is still ongoing, you may end up with partial or stale data being sent. This often happens if the buffer update timing overlaps with DMA operations.

You are starting new DMA transfers from both the main loop and the HAL_SPI_TxRxCpltCallback. This can cause overlapping DMA requests, resulting in inconsistent buffer states.

 

You start the first DMA with a length of 8 but subsequently use BUFFERSIZE for the next transactions. Ensure consistency in buffer sizes to avoid partial data transmissions.

 

Updating TxBuffer while DMA might be using it can cause race conditions, leading to old or corrupted data being sent.

 

Something like this might help you with double buffering:

 

#include "stm32f4xx_hal.h"

#define BUFFERSIZE 8

uint8_t TxBuffer1[BUFFERSIZE];
uint8_t TxBuffer2[BUFFERSIZE];
uint8_t RxBuffer[BUFFERSIZE];
uint8_t *currentTxBuffer;
uint8_t *nextTxBuffer;
volatile uint8_t msgReceived = 0;
volatile uint8_t bufferReady = 0;
volatile uint8_t SPI_Counter = 0;
SPI_HandleTypeDef hspi1;

void SystemClock_Config(void);
void MX_GPIO_Init(void);
void MX_DMA_Init(void);
void MX_SPI1_Init(void);
void Error_Handler(void);

int main(void)
{
 HAL_Init();
 SystemClock_Config();
 MX_GPIO_Init();
 MX_DMA_Init();
 MX_SPI1_Init();

 // Initialize buffers
 currentTxBuffer = TxBuffer1;
 nextTxBuffer = TxBuffer2;
 memset(currentTxBuffer, 0x00, BUFFERSIZE); // Initial data pattern

 // Start the first DMA transfer
 if (HAL_SPI_TransmitReceive_DMA(&hspi1, currentTxBuffer, RxBuffer, BUFFERSIZE) != HAL_OK)
 {
 Error_Handler(); // Handle error
 }

 while (1)
 {
 if (msgReceived == 1)
 {
 // Prepare the next buffer
 memset(nextTxBuffer, SPI_Counter, BUFFERSIZE);

 // Indicate that the next buffer is ready for transmission
 bufferReady = 1;

 // Clear the flag to avoid reprocessing
 msgReceived = 0;

 HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_1); // DEBUG
 }
 }
}

void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
 if (hspi->Instance == SPI1)
 {
 SPI_Counter++; // Update the counter for the next data pattern

 // Swap buffers if the new buffer is ready
 if (bufferReady)
 {
 uint8_t *temp = currentTxBuffer;
 currentTxBuffer = nextTxBuffer;
 nextTxBuffer = temp;
 bufferReady = 0; // Clear buffer ready flag
 }

 // Start the next DMA transfer with the updated buffer
 if (HAL_SPI_TransmitReceive_DMA(&hspi1, currentTxBuffer, RxBuffer, BUFFERSIZE) != HAL_OK)
 {
 Error_Handler(); // Handle error
 }

 // Indicate transfer completion
 msgReceived = 1;

 HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_1); // DEBUG
 }
}

 

24 replies

PPopo.1
Senior
June 18, 2024

The problem with your SPI communication, where old data is being partially transmitted, likely arises due to the timing and synchronization between your data preparation and the SPI transmit/receive operations. Here’s a detailed analysis and how you might fix the issue:

Issue Explanation

When using DMA for SPI communication in a non-blocking mode, data preparation and transfer initiation must be carefully synchronized. Your main loop and the HAL_SPI_TxRxCpltCallback are both modifying the TxBuffer and restarting the DMA transfer. If the buffer is updated while a DMA transfer is in progress, it may lead to partial transmission of the old data. Specifically, the issue is:

  1. Buffer Update Timing: Your TxBuffer is updated in the main loop and also in the callback function, potentially leading to race conditions. The buffer might be modified before the previous transfer is completed, resulting in mixed or partial data being transmitted.
Associate III
June 18, 2024

The flag msgReceived should avoid the buffer being changed in the main function if not a new message is received. 

In the following I have set a debug gpio when the ISR finishes and reset the same gpio when the main function has processed the TxBuffer. From this it is clear that the new Txbuffer ready when a new transaction begins. 
DEBUGDEBUG

PPopo.1
Senior
June 18, 2024

Issue with partial transmission indicates there might still be a timing problem or insufficient synchronization between buffer updates and DMA transfers.

Even with msgReceived flag, there can be a problem if:

  • The buffer is being updated too soon after starting a DMA transfer.
  • The DMA transfer is re-triggered before the buffer update is fully complete.
Associate III
June 18, 2024

Adding a flag (answerPending) resolves the problem, but introduces an additional response delay.

Msg1: Master sends request
Msg2: ... (TxBuffer not updated jet)
Msg3: Slave response to master

 

int main(void) {
 ...

 while (1)
 {
	if(msgReceived == 1) {
		memset(tempTxBuffer, SPI_Counter, BUFFERSIZE);
		answerPending = 1;

		msgReceived = 0;
		HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_1);		// DEBUG
	}
 }
}
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
	msgReceived = 1;

	if( answerPending == 1 ) {
		memcpy(TxBuffer, tempTxBuffer, BUFFERSIZE);
		answerPending = 0;
	}

	SPI_Counter++;

	HAL_SPI_TransmitReceive_DMA(&hspi1, TxBuffer, RxBuffer, BUFFERSIZE);
	HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_1);		// DEBUG
}

 

 
How could the synchronization be implemented without introducing an additional delay?

PPopo.1
Senior
June 18, 2024

 

  • Double Buffering: Use two buffers to alternate between reading and writing. This ensures that one buffer is always available for DMA while the other is being prepared.In double buffering, you maintain two separate buffers. While the DMA is transmitting from one buffer, the other can be updated. After the DMA transfer completes, you switch the buffers.

  • Atomic Operations: Ensure buffer updates and DMA re-triggering are atomic, avoiding conflicts between the main loop and the DMA callback.

 

BarryWhit
Lead
June 18, 2024

This probably won't fix your problem, but it's logically wrong to set msgReceived=0 *after* calling HAL_SPI_TransmitReceive_DMA from main.

As soon as you call HAL_SPI_TransmitReceive_DMA, write "ownership" of msgReceived passes to the callback. 

"- If someone's post helped resolve your issue, please thank them by clicking ""Accept as Solution"".- Please post an update with details once you've solved your issue. Your experience may help others."
BarryWhit
Lead
June 18, 2024

Since they are modified by an interrupt handler, you should mark these synchronization variables `volatile`.  If you don't, the compiler might mistakenly optimize away code you need and ruin your day.

"- If someone's post helped resolve your issue, please thank them by clicking ""Accept as Solution"".- Please post an update with details once you've solved your issue. Your experience may help others."
Associate III
June 19, 2024

Thank you for the response @PPopo.1  and @BarryWhit

I have been working on implementing the double buffering setup, but haven't been able to achieve the desired functionality. How would you implement the double buffering or the atomic operations?

I have sketched a sequence diagram which illustrates the desired functionality, which I hope to achieve from solving the problem which I have stated. 
Sequence DiagramSequence Diagram

OBS: The two cases show how the slave should respond when the slave is busy and when it is done executing. 

PPopo.1
Senior
June 19, 2024

I personaly would do it with freeRTOS and queues. 

BarryWhit
Lead
June 19, 2024

What does "OBS" stand for?

"- If someone's post helped resolve your issue, please thank them by clicking ""Accept as Solution"".- Please post an update with details once you've solved your issue. Your experience may help others."
Associate III
June 19, 2024

OBS is short for 'Observation' used to draw attention to something.

Associate III
June 19, 2024

'OBS' used in this context is short for 'observe', used to draw attention to something.

BarryWhit
Lead
June 19, 2024

ah. I use "NOTE:" - it's less acronymical.

"- If someone's post helped resolve your issue, please thank them by clicking ""Accept as Solution"".- Please post an update with details once you've solved your issue. Your experience may help others."
BarryWhit
Lead
June 19, 2024

The first question you need to decide is whether your application can do I/O and computation sequentially (simpler, slower) or whether it needs to handle both concurrently (a bit more complex, faster).

 

Sequentially - "get some data, process it, get some more data, process it"

concurrently - " get some data and immediately start the next transfer, process the already received data while

the next transfer is ongoing".

 

If "sequential" operation is good enough for you, there's no need for double-buffering, nor an extra synchronization variable. All you need to do is to be disciplined about which code block "owns" the buffers at any given time. Only the code block that owns the buffers is allowed to access them (for read OR write), and ownership transfers back and forth between them, using the access-synchronizing variable. With the DMA start/complete locations in the code serving as the logical transition points.

"- If someone's post helped resolve your issue, please thank them by clicking ""Accept as Solution"".- Please post an update with details once you've solved your issue. Your experience may help others."
BarryWhit
Lead
June 19, 2024

If he already knows FreeRTOS, sure. that would work. If not, it's a large detour.

"- If someone's post helped resolve your issue, please thank them by clicking ""Accept as Solution"".- Please post an update with details once you've solved your issue. Your experience may help others."