SAMD SPI slave with DMA

Hi,
I've been trying to set up an SPI slave to use DMA. I have already implemented an SPI slave using interrupts. This works but handling the interrupts requires a few microseconds for every transmitted byte which essentially limits the communication speed/bandwidth. I would like to solve this by preparing read and write buffer (allocated in the SRAM) at the beginning of the transmission and then using DMA to copy data into/from buffers and the SPI data register. I'm using the Adafruit_ZeroDMA library to do this but I'm not afraid to work with the registers directly if needed.

The issue I'm getting is that the DMA only copies a single byte from the SPI data register into the read buffer in the SRAM and it doesn't continue with the rest of the data. Specifically, if I set the DMA descriptor transfer count to 1, I can get a single byte transferred when the SERCOM0_DMAC_ID_RX trigger fires. I get no data if the transfer count is larger, nor if I set the DMA channel to loop, nor can I transfer any data using the SERCOM0_DMAC_ID_TX regardless of of the transfer count I use.

I have verified that SPI works when using interrupts to read/transmit data to/from the slave. This should mean that the cause is not wiring, SPI mode, rate, or other SPI related configurations being mismatched between master and the slave, and that SERCOM0 is configured correctly. I have also verified that the buffers have been set up and filled with correct data.

I'm suspecting that the RX/TX triggers do not fire or that I use a faulty DMA configuration. I'm adding a few relevant pieces of code below. Any help with figuring out how to debug this or what configurations to try would be appreciated.

My setup are two SAMD boards (Seeduino XIAO), one set up as a SPI master, the other as a SPI slave. I print the send/receive buffers on both ends and I'm expecting to see the same data.

The setup on the master drives SPI using this code:

SPI.beginTransaction(SPISettings(500000, MSBFIRST, SPI_MODE0));
digitalWrite(cs_pins_[slave_id], LOW);
delayMicroseconds(100);
for (int i = 0; i < size; ++i)
    read_buffer[i] = transfer(write_buffer[i]);
digitalWrite(cs_pins_[slave_id], HIGH);

For testing, the transfer rate deliberately low (0.5MHz) and the delay after pulling CS pin low is high 100us (only ~20us are needed here).

On the slave device, I use the following settings to set up SPI:

  //Set up SPI Control A Register
  SERCOM0->SPI.CTRLA.bit.DORD = 0;        // MSB first
  SERCOM0->SPI.CTRLA.bit.CPOL = 0;        // SCK is low when idle, leading edge is rising edge
  SERCOM0->SPI.CTRLA.bit.CPHA = 0;        // Sample data on leading sck edge
  SERCOM0->SPI.CTRLA.bit.FORM = 0x0;      // Frame format = SPI
  SERCOM0->SPI.CTRLA.bit.DIPO = 2;        // DATA PAD0 MOSI is used as input (slave mode)
  SERCOM0->SPI.CTRLA.bit.DOPO = 3;        // DATA PAD3 MISO is used as output
  SERCOM0->SPI.CTRLA.bit.MODE = 0x2;      // SPI in Slave mode
  SERCOM0->SPI.CTRLA.bit.IBON = 0x0;      // Buffer Overflow notification
  SERCOM0->SPI.CTRLA.bit.RUNSTDBY = 0x1;  // Wake on receiver complete
  SERCOM0->SPI.CTRLB.bit.SSDE = 0x1;      // Slave Select Detection Enabled
  SERCOM0->SPI.CTRLB.bit.CHSIZE = 0;      // Character size 8 Bit
  SERCOM0->SPI.CTRLB.bit.PLOADEN = 0x1;   // Enable Preload Data Register

  // Set up SPI interrupts
  SERCOM0->SPI.INTENSET.bit.SSL = 0x1;    // Enable Slave Select low interrupt
  SERCOM0->SPI.INTENSET.bit.RXC = 0x0;    // Receive complete interrupt
  SERCOM0->SPI.INTENSET.bit.TXC = 0x1;    // Transmit complete interrupt
  SERCOM0->SPI.INTENSET.bit.ERROR = 0x0;  // Error interrupt
  SERCOM0->SPI.INTENSET.bit.DRE = 0x0;    // Data Register Empty interrupt

The version of the code with interrupts also enables RCX and DRE bits. When using DMA I switch these off to let DMA handle the data copying. The rest of the settings are the same for both implementations.

I then use the following settings to for the DMA:

#define DATA_LENGTH 1024
Adafruit_ZeroDMA dma_rx_;
Adafruit_ZeroDMA dma_tx_;
DmacDescriptor *rx_desc_;
DmacDescriptor *tx_desc_;
uint8_t read_buffer_[DATA_LENGTH];
uint8_t write_buffer_[DATA_LENGTH];

dma_rx_.setTrigger(SERCOM0_DMAC_ID_RX);
dma_tx_.setTrigger(SERCOM0_DMAC_ID_TX);
dma_rx_.setAction(DMA_TRIGGER_ACTON_BEAT);
dma_tx_.setAction(DMA_TRIGGER_ACTON_BEAT);
dma_rx_.allocate();
dma_tx_.allocate();
rx_desc_ = dma_rx_.addDescriptor(
    (void *)(spi_sercom->SPI.DATA.reg), // move data from here
    read_buffer_,   // to here
    DATA_LENGTH,                        // this many...
    DMA_BEAT_SIZE_BYTE,                 // bytes/hword/words
    false,                               // increment source addr?
    true);                             // increment dest addr?
tx_desc_ = dma_tx_.addDescriptor(
    write_buffer_, // move data from here
    (void *)(spi_sercom->SPI.DATA.reg),   // to here
    DATA_LENGTH,                        // this many...
    DMA_BEAT_SIZE_BYTE,                 // bytes/hword/words
    true,                               // increment source addr?
    false);                             // increment dest addr?

Changing the DATA_LENGTH to 1 for rx_desc_ if the only configuration I found to work. However, I would like to receive the whole buffer and also send a the whole write buffer.
I don't use the DMA callback to avoid firing additional interrupts with every byte transfer.

Inside the SPI interrupt handler, I start/enable the DMA when the CS line is low (SERCOM_SPI_INTFLAG_SSL) and I handle the received data when CS line gets pulled high:

void SERCOM0_Handler() {
    uint8_t interrupts_reg = SERCOM0->SPI.INTFLAG.reg;
    if (interrupts_reg & SERCOM_SPI_INTFLAG_SSL) {
        dma_rx_.startJob();
        dma_tx_.startJob();
    }
    SERCOM0->SPI.INTFLAG.bit.SSL = 1;

    if (interrupts_reg & SERCOM_SPI_INTFLAG_TXC) {
        handle_data();
    }
    SERCOM0->SPI.INTFLAG.bit.TXC = 1
}

Based on my understanding of the DMA feature this setup should start two DMA channels, each of which will handle copying data either from the write buffer or into the read buffer. A byte should be transferred on each beat, which is triggered by SPI TX/RX triggers respectively. The master should receive the full content of the slave's write buffer, and the slave should receive a full read buffer. Please let me know if you can spot any issues here. I'm happy to dig into how Adafruit_ZeroDMA configures the DMA registers if that helps. In case the bug is there.

Similar topic went unanswered here.

Thank you for rubber ducking this with me.

I managed to fix the issue. I had a typo in the source and destination addresses in the descriptor. I forgot the & operator. I then changed the size of the block from DATA_LENGTH to 1 and set each channel to loop. This now works and I can get it to run up to 8MHz rates.
The working DMA config is below:

#define DATA_LENGTH 1024
Adafruit_ZeroDMA dma_rx_;
Adafruit_ZeroDMA dma_tx_;
DmacDescriptor *rx_desc_;
DmacDescriptor *tx_desc_;
uint8_t read_buffer_[DATA_LENGTH];
uint8_t write_buffer_[DATA_LENGTH];

dma_rx_.setTrigger(SERCOM0_DMAC_ID_RX);
dma_tx_.setTrigger(SERCOM0_DMAC_ID_TX);
dma_rx_.setAction(DMA_TRIGGER_ACTON_BEAT);
dma_tx_.setAction(DMA_TRIGGER_ACTON_BEAT);
dma_rx_.loop(true);
dma_tx_.loop(true);
dma_rx_.allocate();
dma_tx_.allocate();
rx_desc_ = dma_rx_.addDescriptor(
    (void *)(&spi_sercom->SPI.DATA.reg), // move data from here
    &read_buffer_,   // to here
    1,                        // this many...
    DMA_BEAT_SIZE_BYTE,                 // bytes/hword/words
    false,                               // increment source addr?
    true);                             // increment dest addr?
tx_desc_ = dma_tx_.addDescriptor(
    &write_buffer_, // move data from here
    (void *)(&spi_sercom->SPI.DATA.reg),   // to here
    1,                        // this many...
    DMA_BEAT_SIZE_BYTE,                 // bytes/hword/words
    true,                               // increment source addr?
    false);                             // increment dest addr?
1 Like