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.