Skip to main content
Graduate
June 18, 2024
Solved

SPI MISO shifted

  • June 18, 2024
  • 18 replies
  • 6437 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
     }
    }

     

    18 replies

    Graduate
    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.
    Graduate
    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

    Graduate
    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.
    Graduate
    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?

    Graduate
    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.

     

    Graduate II
    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. 

    Graduate II
    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.

    Graduate
    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. 

    Graduate
    June 19, 2024

    I personaly would do it with freeRTOS and queues. 

    Graduate II
    June 19, 2024

    What does "OBS" stand for?

    Graduate
    June 19, 2024

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

    Graduate
    June 19, 2024

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

    Graduate II
    June 19, 2024

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

    Graduate II
    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.

    Graduate II
    June 19, 2024

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