I want to be able to receive 2 bytes (2x 8 bits) without managing each byte, I want to DMA to do that stuff for me and trigger an interruption when the two bytes are transferred. I also need the transfer to be full-duplex, the slave must respond to the master (in this example master should receive: 123 and 87).
With the code below nothing happens when I transfer 2 bytes, the interruption does not fire. The master receives a response where all bits are zeroes. I’m sure that the cables are correctly connected because a simple SPI slave like viewtopic.php?f=18&t=3525 works.
#include <SPI.h>
#include <cstdint>
#include <libmaple/dma.h>
#define DATA_BUFFER_SIZE 2
// Tx / Rx buffer
volatile uint8_t data_buffer[DATA_BUFFER_SIZE];
// If true a transfer is complete: data_bufer contains new and valid data
volatile bool dma_transfer_complete(false);
// DMA interrupt handler
void rxDMAirq(void)
{
if (dma_get_irq_cause(DMA1, DMA_CH2) == DMA_TRANSFER_COMPLETE)
dma_transfer_complete = true;
}
void setupDMA(void)
{
dma_init(DMA1);
// DMA tube configuration for SPI1 Rx - channel 2
dma_tube_config tube_cfg =
{
&SPI1->regs->DR, // Source of data
DMA_SIZE_8BITS, // Source transfer size
&data_buffer, // Destination of data
DMA_SIZE_8BITS, // Destination transfer size
DATA_BUFFER_SIZE, // Number of data to transfer
// Flags: auto increment destination address, circular buffer, set tube full IRQ, very high priority
(DMA_CFG_DST_INC | DMA_CFG_CIRC | DMA_CFG_CMPLT_IE), // FIXME Do I need DMA_CCR_PL_VERY_HIGH?
0, // Un-used
DMA_REQ_SRC_SPI1_RX, // Hardware DMA request source
};
// Configure DMA channel
// SPI1 Rx channel is nr. 2
const int ret(dma_tube_cfg(DMA1, DMA_CH2, &tube_cfg));
if (ret != DMA_TUBE_CFG_SUCCESS)
{
while (1)
{
Serial.print("DMA configuration error: ");
Serial.println(ret, HEX);
Serial.println("Reset is needed!");
delay(100);
}
}
dma_attach_interrupt(DMA1, DMA_CH2, rxDMAirq);
dma_enable(DMA1, DMA_CH2);
}
void setupSPI(void)
{
// MOSI, MISO, SCK PINs are set by the library
pinMode(BOARD_SPI_DEFAULT_SS, INPUT); // SS
// The clock value is not used
// SPI1 is selected by default
SPI.beginTransactionSlave(SPISettings(18000000, MSBFIRST, SPI_MODE0, DATA_SIZE_8BIT));
}
void setup()
{
Serial.begin(115200);
delay(100);
// Data that will be sent
data_buffer[0] = 123;
data_buffer[1] = 87;
setupSPI();
setupDMA();
}
unsigned count_1(0);
unsigned count_2(0);
void loop()
{
if (dma_transfer_complete)
{
dma_transfer_complete = false;
Serial.println("DMA transfer complete");
Serial.print("data_buffer[0] = ");
Serial.println((unsigned)data_buffer[0]);
Serial.print("data_buffer[1] = ");
Serial.println((unsigned)data_buffer[1]);
// Data that will be sent (for next transfer)
// Assumes that a transfer has not yet been initiated
data_buffer[0] = 123;
data_buffer[1] = 87;
}
delayMicroseconds(10);
if (++count_1 > 50000)
{
count_1 = 0;
Serial.print("Waiting... ");
Serial.println(++count_2);
}
}
I only modified the length of the buffer (RECORDS_PER_BUFFER = 2, RECORDS_PER_BLOCK = 8) and the SPI transfer data size from 16 bits to 8 bits, the transfer never seem to complete, do you have an idea why? The master sent 8 times a 8 bit SPI frame.
/*
Storage of multiple 8bit data received over SPI1 to SD card over SPI2.
*/
#include <libmaple/dma.h>
#include <SPI.h>
// SD card chip select pin
#define CHIP_SELECT PB12
#define TOTAL_BLOCKS 10 // number of blocks to record
// size of data block which will be written in one shot to card
#define BLOCK_SIZE 512 // bytes
// size of data buffer where the ADC sampled data will be transferred over DMA
#define DATA_BUFFER_SIZE (2*BLOCK_SIZE) // bytes
#define RECORD_SIZE 2 // 8 bits
#define RECORDS_PER_BLOCK 8 // uint8_t
#define RECORDS_PER_BUFFER 2 // uint8_t
// the data buffer to store values
uint8_t data_buffer[RECORDS_PER_BUFFER];
uint32_t total_blocks;
uint32_t total_records;
uint8_t * pCache;
// set by HW when a complete DMA transfer was finished.
volatile uint8_t dma_irq_full_complete;
// set by HW when a DMA transfer is at its half.
volatile uint8_t dma_irq_half_complete;
// set by SW when overrun occurs
volatile uint8_t overrun;
// signalling for lower and upper buffer status
volatile uint8_t buff0_stored, buff1_stored, buff_index;
void setupParameters(void)
{
// init some relevant values
total_blocks = TOTAL_BLOCKS;
// the SW can only record an even number of blocks, so we need to adjust it
if ( (total_blocks&1) ) total_blocks++;
total_records = (total_blocks*RECORDS_PER_BLOCK);
}
void DMA_Init(void)
{
dma_irq_full_complete = 0;
dma_irq_half_complete = 0;
overrun = 0;
buff0_stored = buff1_stored = 1; // avoid overrun detection
buff_index = 0;
dma_clear_isr_bits(DMA1, DMA_CH2);
// for test only: fill ADC buffer with dummy data
for (int i = 0; i<RECORDS_PER_BUFFER; ) data_buffer[i++] = 0x2222;
}
// This is our DMA interrupt handler.
void DMA_Rx_irq(void)
{
// Used to store DMA interrupt status register (ISR) bits. This helps explain what's going on
uint32_t dma_isr = dma_get_isr_bits(DMA1, DMA_CH2);
if (dma_isr & DMA_ISR_HTIF1) {
dma_irq_half_complete = 1;
buff0_stored = 0; // reset storage flag to detect overrun
if ( buff1_stored==0 ) overrun++; // upper buffer half being written before was stored
}
if (dma_isr & DMA_ISR_TCIF1) {
dma_irq_full_complete = 1;
buff1_stored = 0; // reset storage flag to detect overrun
if ( buff0_stored==0 ) overrun++; // lower buffer half being written before was stored
}
dma_clear_isr_bits(DMA1, DMA_CH2);
}
void DMA_Setup(void)
{
DMA_Init();
dma_init(DMA1); // init DMA clock domain
dma_disable(DMA1, DMA_CH2); // Disable the DMA tube.
// DMA tube configuration for SPI1 Rx - channel 2
dma_tube_config my_tube_cfg = {
&SPI1->regs->DR, // data source address
DMA_SIZE_8BITS, // source transfer size
&data_buffer, // data destination address
DMA_SIZE_8BITS, // destination transfer size
RECORDS_PER_BUFFER, // nr. of data to transfer
// tube flags: auto increment dest addr, circular buffer, set tube full IRQ, very high prio:
( DMA_CFG_DST_INC | DMA_CFG_CIRC | DMA_CFG_HALF_CMPLT_IE | DMA_CFG_CMPLT_IE | DMA_CCR_PL_VERY_HIGH ),
0, // unused
DMA_REQ_SRC_SPI1_RX, // Hardware DMA request source
};
// configure DMA channel
int ret = dma_tube_cfg(DMA1, DMA_CH2, &my_tube_cfg); // SPI1 Rx channel is nr. 2
if (ret > 0)
{
Serial1.print("DMA configuration error: ");
Serial1.println(ret,HEX);
Serial1.println("Stopped, reset is needed!");
while (1);
}
dma_attach_interrupt(DMA1, DMA_CH2, DMA_Rx_irq); // attach an interrupt handler.
dma_enable(DMA1, DMA_CH2); // Enable the DMA tube. It will now begin serving requests.
}
/*****************************************************************************/
SPIClass SPI_2(2);
uint32_t app_cr1;
/*****************************************************************************/
void SPI_setup(void)
{
// use SPI 1 for recording, SPI 2 is used by SD card
SPI.setModule(1);
SPI.beginTransactionSlave(SPISettings(18000000, MSBFIRST, SPI_MODE0, DATA_SIZE_8BIT));
SPI.setModule(2); // FIXME Remove?
}
void setup()
{
Serial.begin(115200);
Serial.println("8bit data received over SPI1 in slave mode is stored to SD-card over SPI2.");
delay (10);
}
void setupModules()
{
// Set-up involved hardware modules
SPI_setup();
DMA_Setup();
}
void loop()
{
setupParameters();
setupModules();
Serial1.print("Total_blocks = ");
Serial1.println(total_blocks);
uint32_t t_out; // time-out
uint32_t bl, val = 0;
for( bl = 0; bl < total_blocks; ++bl)
{
// time-out to avoid hangup if something goes wrong
t_out = 1000000;
// each ADC sequence is sampled triggered by TIMER3 event.
while ( (--t_out)>0 ) {
// check DMA recording status and push data to card if either half buffer is full
if ( dma_irq_half_complete ) {
dma_irq_half_complete = 0;
buff0_stored = 1;
break;
}
if ( dma_irq_full_complete ) {
dma_irq_full_complete = 0;
buff1_stored = 1;
break;
}
//if ( spi_is_rx_nonempty(SPI1) ) { spi_rx_reg(SPI1); val++; }
}
// only for debug purposes:
if ( t_out==0 ) {
Serial1.println(F("\n!!!! TIME_OUT !!!"));
break;
}
}
}
Besides configuring the DMA controller, you need to configure the SPI peripheral to generate DMA requests.
If you check the SPI library, the function name to do that is there, I forgot the name.
[victor_pv – Tue Apr 24, 2018 9:29 pm] –
I could not see in the code that you enable the SPI peripheral DMA requests anywhere.
dma_init(DMA1);
// DMA tube configuration for SPI1 Rx - channel 2
dma_tube_config tube_cfg =
{
&SPI1->regs->DR, // Source of data
DMA_SIZE_8BITS, // Source transfer size
&data_buffer, // Destination of data
DMA_SIZE_8BITS, // Destination transfer size
DATA_BUFFER_SIZE, // Number of data to transfer
// Flags: auto increment destination address, circular buffer, set tube full IRQ, very high priority
(DMA_CFG_DST_INC | DMA_CFG_CIRC | DMA_CFG_CMPLT_IE), // FIXME Do I need DMA_CCR_PL_VERY_HIGH?
0, // Un-used
DMA_REQ_SRC_SPI1_RX, // Hardware DMA request source
};
// Configure DMA channel
// SPI1 Rx channel is nr. 2
const int ret(dma_tube_cfg(DMA1, DMA_CH2, &tube_cfg));
if (ret != DMA_TUBE_CFG_SUCCESS)
{
while (1)
{
Serial.print("DMA configuration error: ");
Serial.println(ret, HEX);
Serial.println("Reset is needed!");
delay(100);
}
}
The Rx part should work in slave mode with the mentioned lines, of course, with adapted DMA and SPI channels and devices.

Here is the simplest example I could get to work:
#include <SPI.h>
#include <cstdint>
#include <libmaple/dma.h>
#define DATA_BUFFER_SIZE 2
// Rx and Tx buffers
volatile uint8_t rx_buffer[DATA_BUFFER_SIZE];
volatile uint8_t tx_buffer[DATA_BUFFER_SIZE];
// If true a transfer is complete: rx_bufer contains new and valid data
// tx_buffer has been completely sent
volatile bool dma_transfer_complete(false);
// DMA interrupt handler
void rxDMAirq(void)
{
if (dma_get_irq_cause(DMA1, DMA_CH2) == DMA_TRANSFER_COMPLETE)
dma_transfer_complete = true;
}
void setupDMA(void)
{
// SPI is on DMA1
dma_init (DMA1);
// DMA tube configuration for SPI1 Rx
dma_tube_config rx_tube_cfg =
{
&SPI1->regs->DR, // Source of data
DMA_SIZE_8BITS, // Source transfer size
&rx_buffer, // Destination of data
DMA_SIZE_8BITS, // Destination transfer size
DATA_BUFFER_SIZE, // Number of data to transfer
// Flags: auto increment destination address, circular buffer, set tube full IRQ, very high priority
(DMA_CFG_DST_INC | DMA_CFG_CIRC | DMA_CFG_CMPLT_IE | DMA_CCR_PL_VERY_HIGH),
0, // Un-used
DMA_REQ_SRC_SPI1_RX, // Hardware DMA request source
};
// SPI1 Rx is channel 2
const int ret_rx(dma_tube_cfg(DMA1, DMA_CH2, &rx_tube_cfg));
if (ret_rx != DMA_TUBE_CFG_SUCCESS)
{
while (1)
{
Serial.print("Rx DMA configuration error: ");
Serial.println(ret_rx, HEX);
Serial.println("Reset is needed!");
delay(100);
}
}
// DMA tube configuration for SPI1 Tx
dma_tube_config tx_tube_cfg =
{
&tx_buffer, // Source of data
DMA_SIZE_8BITS, // Source transfer size
&SPI1->regs->DR, // Destination of data
DMA_SIZE_8BITS, // Destination transfer size
DATA_BUFFER_SIZE, // Number of data to transfer
// Flags: auto increment source address, circular buffer, set tube full IRQ, low priority
(DMA_CFG_SRC_INC | DMA_CFG_CIRC | DMA_CFG_CMPLT_IE | DMA_CCR_PL_LOW),
0, // Un-used
DMA_REQ_SRC_SPI1_TX, // Hardware DMA request source
};
// SPI1 Tx is channel 3
const int ret_tx(dma_tube_cfg(DMA1, DMA_CH3, &tx_tube_cfg));
if (ret_tx != DMA_TUBE_CFG_SUCCESS)
{
while (1)
{
Serial.print("Tx DMA configuration error: ");
Serial.println(ret_tx, HEX);
Serial.println("Reset is needed!");
delay(100);
}
}
// Clear RX register in case we already received SPI data
spi_rx_reg(SPI.dev());
// SPI DMA requests for Rx and Tx
spi_rx_dma_enable(SPI.dev());
spi_tx_dma_enable(SPI.dev());
// Attach interrupt to catch end of DMA transfer
dma_attach_interrupt(DMA1, DMA_CH2, rxDMAirq);
// Enable DMA configurations
dma_enable(DMA1, DMA_CH2); // Rx
dma_enable(DMA1, DMA_CH3); // Tx
}
void setupSPI(void)
{
// MOSI, MISO, SCK PINs are set by the library
pinMode(BOARD_SPI_DEFAULT_SS, INPUT); // SS
// The clock value is not used
// SPI1 is selected by default
SPI.beginTransactionSlave(SPISettings(0, MSBFIRST, SPI_MODE0, DATA_SIZE_8BIT));
}
void setup()
{
Serial.begin(115200);
delay(100);
// Data that will be sent
tx_buffer[0] = 20;
tx_buffer[1] = 18;
setupSPI();
setupDMA();
}
unsigned count_1(0);
unsigned count_2(0);
void loop()
{
if (dma_transfer_complete)
{
dma_transfer_complete = false;
Serial.println("DMA transfer complete");
Serial.print("data_buffer[0] = ");
Serial.println((unsigned)rx_buffer[0]);
Serial.print("data_buffer[1] = ");
Serial.println((unsigned)rx_buffer[1]);
// Data that will be sent (for next transfer)
// Assumes that a transfer has not yet been initiated
tx_buffer[0] = 20;
tx_buffer[1] = 18;
}
delayMicroseconds(10);
if (++count_1 > 50000)
{
count_1 = 0;
Serial.print("Waiting... ");
Serial.println(++count_2);
}
}
If a few people can test this, think it should be included in the examples…
I think it would be good if the dmaTransfer function already in the core can be tested and made compatible with Slave mode if it’s not, that would further simplify the use.

// enable SPI DMA requests for Rx and Tx
spi_rx_dma_enable(SPI.dev());
spi_tx_dma_enable(SPI.dev());
[stevestrong – Sat Apr 28, 2018 10:37 am] –
I have only one remark without having tested it: these lines
// enable SPI DMA requests for Rx and Tx
spi_rx_dma_enable(SPI.dev());
spi_tx_dma_enable(SPI.dev());
I suggest to not set the DMA in circular mode.
I normally use one single circular buffer, the size being double of a single buffer.
In this case there is no need to change the buffer address.
I check in main loop the half complete and transfer complete flags of the DMA and do process the completed half: when the DMA is transferring one half, the main level is processing the complementary half of the buffer.
[stevestrong – Mon Apr 30, 2018 6:23 pm] –
If you use two different buffers, you have to change the buffer address.
I suggest to not set the DMA in circular mode.I normally use one single circular buffer, the size being double of a single buffer.
In this case there is no need to change the buffer address.
I check in main loop the half complete and transfer complete flags of the DMA and do process the completed half: when the DMA is transferring one half, the main level is processing the complementary half of the buffer.
For that you should use the double buffer mode of the DMA, it uses 2 memory adresses and swaps the pointers each time a transfer is complete (see AN4031 page 13, Double-buffer mode).
I now face a new problem. Sometimes for an unknown reason (I guess electrical impedance and such) my peripherals receives one more word that it should. I can detect it because I am computing flechter’s sums on both sides (master/slave) so I know the message if ill-formed.
But how can I fix that automatically? One solution would be to access the DMA buffer index and if it does not change for a period of time (longer than one full transmission), reset the DMA.
Do you know how I could do that?
Here is a pseudo code of what I would like to do:
uint32_t last(0);
uint32_t dma_check_time(0);
bool dma_check(false);
unsigned dma_check_buffer_id(0);
void loop()
{
// Count time (usec) elapsed since last time
uint32_t right_now(micros());
uint32_t elapsed(right_now - last);
if (last == 0)
elapsed = 0;
last = right_now;
// In case DMA buffer index is not zero: reset DMA buffer index if the DMA buffer index
// do not change after 150 microseconds
//
// This is useful when the peripheral received one word more than expected for whatever reason and
// all messages gets shifted because of that.
if (dma_check)
{
if (right_now - dma_check_time > 150)
{
dma_check = false;
if (dma_check_buffer_id == dma_index)
{
// Reset DMA index
}
}
}
else if (dma_index != 0)
{
dma_check_buffer_id = dma_index;
dma_check_time = right_now;
dma_check = true;
}
}
Here, we are talking about blue pill (F1), right? F1 and F3 do not have that feature.
Without knowing your code it is hard to evaluate what can be wrong.
Btw, I posted here some working master/slave examples: http://stm32duino.com/viewtopic.php?f=9 … 878#p45041
EDIT
It is possible to check how many transfers has the DMA yet to complete:
dma_get_count(<dma_device>,<dma_channel>);
[stevestrong – Sat Jun 30, 2018 4:46 pm] –
Regarding the application note you pointed out, it is for F2, F4 and F7, as you probably noticed.
Here, we are talking about blue pill (F1), right? F1 and F3 do not have that feature.Without knowing your code it is hard to evaluate what can be wrong.
Btw, I posted here some working master/slave examples: http://stm32duino.com/viewtopic.php?f=9 … 878#p45041EDIT
It is possible to check how many transfers has the DMA yet to complete:
dma_get_count(<dma_device>,<dma_channel>);
You could import that part in your files.
I just made a PR: https://github.com/rogerclarkmelbourne/ … 2/pull/534

I still don’t know why this happens but at least I can detect it and reset the SPI / DMA, then everything runs fine again.
The setupDMA and setupSPI functions:
void setupDMA(void)
{
// SPI is on DMA1
dma_init(DMA1);
// Disable in case already enabled
spi_tx_dma_disable(SPI.dev());
spi_rx_dma_disable(SPI.dev());
// Disable in case it was already enabled
dma_disable(DMA1, DMA_CH2);
dma_disable(DMA1, DMA_CH3);
dma_detach_interrupt(DMA1, DMA_CH2);
// DMA tube configuration for SPI1 Rx
dma_tube_config rx_tube_cfg =
{
&SPI1->regs->DR, // Source of data
DMA_SIZE_8BITS, // Source transfer size
&rx_buffer, // Destination of data
DMA_SIZE_8BITS, // Destination transfer size
BUFFER_SIZE, // Number of data to transfer
// Flags: auto increment destination address, circular buffer, set tube full IRQ, very high priority
(DMA_CFG_DST_INC | DMA_CFG_CIRC | DMA_CFG_CMPLT_IE | DMA_CCR_PL_VERY_HIGH),
0, // Un-used
DMA_REQ_SRC_SPI1_RX, // Hardware DMA request source
};
// SPI1 Rx is channel 2
const int ret_rx(dma_tube_cfg(DMA1, DMA_CH2, &rx_tube_cfg));
if (ret_rx != DMA_TUBE_CFG_SUCCESS)
{
while (1)
{
#ifdef SERIAL_DEBUG
Serial.print("Rx DMA configuration error: ");
Serial.println(ret_rx, HEX);
Serial.println("Reset is needed!");
#endif
delay(500);
}
}
// DMA tube configuration for SPI1 Tx
dma_tube_config tx_tube_cfg =
{
&tx_buffer, // Source of data
DMA_SIZE_8BITS, // Source transfer size
&SPI1->regs->DR, // Destination of data
DMA_SIZE_8BITS, // Destination transfer size
BUFFER_SIZE, // Number of data to transfer
// Flags: auto increment source address, circular buffer, set tube full IRQ, very high priority
(DMA_CFG_SRC_INC | DMA_CFG_CIRC | DMA_CFG_CMPLT_IE | DMA_CCR_PL_VERY_HIGH),
0, // Un-used
DMA_REQ_SRC_SPI1_TX, // Hardware DMA request source
};
// SPI1 Tx is channel 3
const int ret_tx(dma_tube_cfg(DMA1, DMA_CH3, &tx_tube_cfg));
if (ret_tx != DMA_TUBE_CFG_SUCCESS)
{
while (1)
{
#ifdef SERIAL_DEBUG
Serial.print("Tx DMA configuration error: ");
Serial.println(ret_tx, HEX);
Serial.println("Reset is needed!");
#endif
delay(500);
}
}
// Attach interrupt to catch end of DMA transfer
dma_attach_interrupt(DMA1, DMA_CH2, rxDMAirq);
// Enable DMA configurations
dma_enable(DMA1, DMA_CH2); // Rx
dma_enable(DMA1, DMA_CH3); // Tx
// SPI DMA requests for Rx and Tx
spi_rx_dma_enable(SPI.dev());
spi_tx_dma_enable(SPI.dev());
#ifdef SERIAL_DEBUG
Serial.print("setupDMA | Rx count = ");
Serial.print((unsigned) dma_get_count(DMA1, DMA_CH2));
Serial.print(" | Tx DMA count = ");
Serial.println((unsigned) dma_get_count(DMA1, DMA_CH3));
#endif
}
void setupSPI(void)
{
// MOSI, MISO, SCK PINs are set by the library
pinMode(BOARD_SPI_DEFAULT_SS, INPUT); // SS
// The clock value is not used
// SPI1 is selected by default
SPI.beginTransactionSlave(SPISettings(0, MSBFIRST, SPI_MODE0, DATA_SIZE_8BIT));
// Clear RX register in case we already received SPI data
spi_rx_reg(SPI.dev());
}
I am trying to modify the operation of a UHF FM radio ( Baofeng UV-82 ) to receive digital signals, and I need to intercept the SPI commands being sent to the chip, via SPI.
This code looks really useful for this project.
Actually I probably won’t use the DMA version, as I need to listen to each individual command, and respond immediately .
But SPI slave functionality is a really useful feature.