2025-02-05 07:24 AM - edited 2025-02-05 08:15 AM
I have an optimized SPI Slave DMA setup which is driven by the chip select interrupt. It looks something like:
int main(void)
{
// Initialize SPI SLAVE, SPI_DATASIZE_8BIT, SPI_DIRECTION_2LINES
HAL_SPI_TransmitReceive_DMA(hspi, (uint8_t *)&spi->txBuffer[spi->sendFrameIdx], (uint8_t *)&spi->rxBuffer, SPI_FRAME_SIZE);
while (1)
{
// possibly generate data
if (new_data)
{
spinlock(spi->txLock);
// Append data to the fill buffer
unlock();
}
}
}
Then the interrupt code looks something like:
static void handleTransferStart(SPI_HandleTypeDef *hspi)
{
if (spinTryLock(&spi->txLock))
{
if (sendBufferIsEmpty && fillBufferHasData)
{
// Swap send and fill buffers. There is data to send but it's not in the current buffer
const uint8_t *txBuffer = (uint8_t *)&spi->txBuffer[spi->sendFrameIdx];
const DMA_HandleTypeDef *hdma = hspi->hdmatx;
__HAL_DMA_DISABLE(hdma);
// Write the first three bytes directly to the SPI TX FIFO
*(__IO uint16_t *)&hspi->Instance->DR = txBuffer[0] << 8;
*(__IO uint16_t *)&hspi->Instance->DR = txBuffer[2] << 8 | txBuffer[1];
// Update the TX DMA address and length for the remainder of the data
hdma->Instance->CM0AR = (uint32_t)txBuffer + 3;
hdma->Instance->CNDTR = SPI_FRAME_SIZE - 3;
__HAL_DMA_ENABLE(hdma);
}
}
}
static void handleTransferEnd(SPI_HandleTypeDef *hspi)
{
processRxBuffer();
// The RX buffer is re-used, just reset the count
__HAL_DMA_DISABLE(hspi->hdmarx);
hspi->hdmarx->Instance->CNDTR = SPI_FRAME_SIZE;
__HAL_DMA_ENABLE(hspi->hdmarx);
processTxBuffer();
if (spinTryLock(&spi->txLock))
{
// Swap send and fill buffers
// Reset the TX DMA
__HAL_DMA_DISABLE(hspi->hdmatx);
hspi->hdmatx->Instance->CNDTR = SPI_FRAME_SIZE;
hspi->hdmatx->Instance->CM0AR = (uint32_t)&spi->txBuffer[spi->sendFrameIdx];
__HAL_DMA_ENABLE(hspi->hdmatx);
}
}
void chip_select_interrupt(SPI_HandleTypeDef *hspi)
{
if (cs_gpio_is_low)
{
handleTransferStart(hspi);
}
else
{
handleTransferEnd(hspi);
}
}
This works 99.9% of the time, but occasionally `handleTransferStart` gets delayed by a couple microseconds.
This is okay because the master can detect the invalid data and re-try the transfer. My issue is that something gets out of sync in the TX DMA / FIFO. Here is my attempt to fix it:
static void handleTransferEnd(SPI_HandleTypeDef *hspi)
{
processRxBuffer();
// The RX buffer is re-used, just reset the count
__HAL_DMA_DISABLE(hspi->hdmarx);
hspi->hdmarx->Instance->CNDTR = SPI_FRAME_SIZE;
__HAL_DMA_ENABLE(hspi->hdmarx);
if (hspi->hdmatx->Instance->CNDTR || (hspi->Instance->SR & SPI_SR_FTLVL))
{
// Transfer failed, don't swap buffers and retry the transfer
// Reset the TX DMA / FIFO
const uint8_t *txBuffer = (uint8_t *)&spi->txBuffer[spi->sendFrameIdx];
__HAL_DMA_DISABLE(hspi->hdmatx);
*(__IO uint16_t *)&hspi->Instance->DR = txBuffer[0] << 8;
*(__IO uint16_t *)&hspi->Instance->DR = txBuffer[2] << 8 | txBuffer[1];
hspi->hdmatx->Instance->CM0AR = (uint32_t)txBuffer + 3;
hspi->hdmatx->Instance->CNDTR = SPI_FRAME_SIZE - 3;
__HAL_DMA_ENABLE(hspi->hdmatx);
}
else
{
processTxBuffer();
if (spinTryLock(&spi->txLock))
{
// Swap send and fill buffers
// Reset the TX DMA
__HAL_DMA_DISABLE(hspi->hdmatx);
hspi->hdmatx->Instance->CNDTR = SPI_FRAME_SIZE;
hspi->hdmatx->Instance->CM0AR = (uint32_t)&spi->txBuffer[spi->sendFrameIdx];
__HAL_DMA_ENABLE(hspi->hdmatx);
}
}
Oddly this works for the first failure, but fails all subsequent attempts:
EDIT: I changed my code to write 3 * 0xFF to the txFIFO and I still see the same thing so it's more than just the first 2 bytes missing...
I also tried writing different lengths to the CM0AR in the retry:
if (hspi->hdmatx->Instance->CNDTR != 0 || (hspi->Instance->SR & SPI_SR_FTLVL))
{
// Transfer failed, don't swap buffers and retry the transfer
// Reset the TX DMA / FIFO
const uint8_t *txBuffer = (uint8_t *)&spi->txBuffer[spi->sendFrameIdx];
__HAL_DMA_DISABLE(hspi->hdmatx);
*(__IO uint16_t *)&hspi->Instance->DR = txBuffer[0] << 8;
*(__IO uint16_t *)&hspi->Instance->DR = txBuffer[2] << 8 | txBuffer[1];
hspi->hdmatx->Instance->CM0AR = (uint32_t)txBuffer + 3;
hspi->hdmatx->Instance->CNDTR = SPI_FRAME_SIZE; // no `-3`
__HAL_DMA_ENABLE(hspi->hdmatx);
}
In this case the MISO line looked good every time, but as expected there were always 3 bytes remaining in the FIFO so my "resend / recovery" condition triggered indefinitely. Not checking the FIFO level put things out of sync again for some reason.
I've played around with several other things like not writing directly to the FIFO, using CNDTR sizes well below SPI_FRAME_SIZE but it always seems to end up out of sync by 1-2 bytes.
My current solution is to remove `handleTransferStart`, but this requires the master to perform 2 transfers to get a response to and data it writes due to the double buffering.