Spark Logic

Non blocking SPI DMA – Added callback to the SPI DMA functions (dmaSend, dmaTransfer…)

victor_pv Tue Feb 21, 2017 8:05 pm

UPDATE:
Working code in this post, and an explanation how to modify sdFat to use it in the next post after that:
http://www.stm32duino.com/viewtopic.php … =60#p30306

======================================================

This is something Roger has suggested several times, and it seems to only make sense.

Currently the dma TX/RX functions we added to the SPI library block until the DMA transfer is completed or has timed out.
This change would add 2 functions to the SPI library, similar to the Arduino official I2S library:

onTransmit(handler);
onReceive(handler);

Replies

racemaniac Thu Feb 23, 2017 8:51 am

stevestrong wrote:In my code I initialized the DMA once (dmaSendInit()), then I just updated the number of data to transfer and the pointer in dmaSendBuffer() and enabled the DMA, so that the overhead could be kept minimal.
I will post my version here in the evening.

victor_pv Thu Feb 23, 2017 3:47 pm

racemaniac wrote:victor_pv wrote:racemaniac wrote:
indeed :). It would still be nice to be able to configure that you won’t reuse the dma channel for something else and get the cheaper dma setup (likely the most often usecase since so far dma is hardly used anyway ^^)

racemaniac Thu Feb 23, 2017 3:55 pm

ugh, that eternal trade off between performance and usability of a framework ^^’
there indeed is no silver bullet or “best” solution >_<. i’d still like the cheap setup functions for when using DMA directly, but for usage in frameworks, the full setup is indeed the safer choice.

stevestrong Thu Feb 23, 2017 6:09 pm

I think we could reserve the DMA usage of channels 2 to 5 for SPI1 and 2 (both Rx and Tx), user configurable. The other channels would be then always free for other purposes.
I am attaching my local version of SPI.cpp, where I started to implement a dual-buffered DMA transfer: while sending one buffer, the other buffer gets filled. When the first is sent, in the ISR is checked whether the other buffer contains data to send or not. If yes, the DMA will be setup automatically again. So far the plan, but the ISR part is not yet coded.
The interesting functions start from line 479, conditioned by the SPI_USE_DMA define.
Hopefully you can get some ideas from it.

Ollie Fri Feb 24, 2017 4:15 pm

An additional reference can be found at http://www.emblocks.org/wiki/tutorials: … _disco:spi. I have included the Logic Analyzer screenshots to show how 3 SPI devices are communicating at the same time using DMA for the bulk processing.

This is based on the principle that the SPI transfer requests are non-blocking. That means that the requests are put into a queue. When taking a request from the queue the following actions are done
– the device address is sent using polling
– the register number is sent using polling
– one or two DMAs are setup depending if it is read, write, or simultaneous read and write
– interrupt is set for DMA completion
– once the interrupt is received, the “callback” is executed and the next transfer is started

This code demonstrates how the speed and other SPI settings can be changed for different devices and how the SPI configuration registers are modified only when required.

This code demonstrates how the LCD driver command/data line is driven synchronized with the SPI transfers.

Cheers, Ollie

stevestrong Fri Feb 24, 2017 4:48 pm

Ollie wrote:This code demonstrates how the LCD driver command/data line is driven synchronized with the SPI transfers.
Cheers, Ollie

mrburnette Sat Feb 25, 2017 3:56 pm

…. thinking out loud…. no response expected ….

Anyone looked into what Paul Stoffregen is doing around Teensy 3.1/3.2 ?

RogerClark Sat Feb 25, 2017 8:24 pm

mrburnette wrote:…. thinking out loud…. no response expected ….

Anyone looked into what Paul Stoffregen is doing around Teensy 3.1/3.2 ?

victor_pv Sat Feb 25, 2017 10:51 pm

From what I have found, they have not integrated any DMA functions in the normal SPI library.
There is another library, DmaSpi, which is the one that has DMA capabilities:

https://github.com/crteensy/DmaSpi

That one works similar to what Steve was working on, it queues transfers with a series of properties (buffer pointer, size, and a pin object to control CS), then services those transfers in order.

Not sure if all that is worth the effort, as Steve tested, for small transfers doesn’t improve performance due to all the overhead, and the libraries using it have to be heavily modified.

RogerClark Sun Feb 26, 2017 12:40 am

I Totally agree.

Its not worth adding that level of complexity as hardly any one will use it.

victor_pv Sun Feb 26, 2017 1:23 am

First shot at separating the setting up of the DMA transfer, and the firing of the transfer.
Basically I just separated the code that was previously in dmaSend in two parts, dmaSensdSet configured the transfer, except for the length, and enabling the channel. Next you call dmaSendRepeat (I dont really like that name, but can’t think on other appropriate one that I like) with the length of the transfer, and that sets the DMACNDTR register, which needs to be reloaded for a new transfer, and enables the channel. If a callback function has been set previously, doesn’t block and return 0 for success. If a callback function was not set, it will block.
SPI.dmaSend has the exact same functionality as before, so any code using it should not break in any way, but now it uses the other 2 functions to do the work.

This way if you want to send a number of bytes from the same buffer pointer, you only call SPI.dmaSendRepeat with 1 parameter, the number of bytes to send, everything else stays the same as the last transfer, so you only need to fill the buffer and fire the transfer.

What do you guys think? This still doesn’t implement the internal buffer Steven had been testing, but code that repeat transmissions from the same buffer should get less overhead.

EDIT: I realize it may be good to add checks to confirm whether there is a transmission already in progress before changing settings. It depends if we prefer that little overhead for safety.

uint8 SPIClass::dmaSendRepeat(uint16 length)
{
if (length == 0) return 1;
dma_set_num_transfers(_currentSetting->spiDmaDev, _currentSetting->spiTxDmaChannel, length);
dma_enable(_currentSetting->spiDmaDev, _currentSetting->spiTxDmaChannel);// enable transmit
if (_currentSetting->TXcallback){
return 0;
}
uint32_t m = millis();
uint8 b = 0;
while ((dma_get_isr_bits(_currentSetting->spiDmaDev, _currentSetting->spiTxDmaChannel) & DMA_ISR_TCIF1)==0) {//Avoid interrupts and just loop waiting for the flag to be set.
if ((millis() - m) > DMA_TIMEOUT) { b = 2; break; }
}
dma_clear_isr_bits(_currentSetting->spiDmaDev, _currentSetting->spiTxDmaChannel);

while (spi_is_tx_empty(_currentSetting->spi_d) == 0); // "5. Wait until TXE=1 ..."
while (spi_is_busy(_currentSetting->spi_d) != 0); // "... and then wait until BSY=0 before disabling the SPI."
dma_disable(_currentSetting->spiDmaDev, _currentSetting->spiTxDmaChannel);
spi_tx_dma_disable(_currentSetting->spi_d);
//uint16 x = spi_rx_reg(_currentSetting->spi_d); // dummy read, needed, don't remove!
return b;
}

void SPIClass::dmaSendSet(void * transmitBuf, bool minc)
{
uint32 flags = ( (DMA_MINC_MODE*minc) | DMA_FROM_MEM | DMA_TRNS_CMPLT);
dma_init(_currentSetting->spiDmaDev);
// TX
spi_tx_dma_enable(_currentSetting->spi_d);
dma_xfer_size dma_bit_size = (_currentSetting->dataSize==SPI_DATA_SIZE_16BIT) ? DMA_SIZE_16BITS : DMA_SIZE_8BITS;
dma_setup_transfer(_currentSetting->spiDmaDev, _currentSetting->spiTxDmaChannel, &_currentSetting->spi_d->regs->DR, dma_bit_size,
transmitBuf, dma_bit_size, flags);// Transmit buffer DMA
}

uint8 SPIClass::dmaSend(void * transmitBuf, uint16 length, bool minc)
{
dmaSendSet(transmitBuf, minc);
return dmaRepeat(length);
}

RogerClark Sun Feb 26, 2017 2:34 am

Thanks Victor

The blocking / non blocking selection via the callback argument being non null is what Nordic semi do in their SDK / API, so if its good enough for them, I think its good enough for us.

Also

dmaSendRepeat() seems a prefectly good name to me, as its descriptive and concise

stevestrong Sun Feb 26, 2017 7:04 am

That’s nice so far.
It should be non-blocking if a non-null CB function is passed as parameter.
I would really tend to reserve the DMA channels for SPI, if configured by the user, in order to remove the overhead calling dmaSend/Set each time if only the buffer pointer and its length have changed. This way one could quickly switch between two buffers, one being filled while the previous one being sent.
I think it is very unlikely to use same DMA channel for other purposes if one use the DMA for SPI, which means the SPI is constantly working, and then with DMA, like display-related applications.

Ollie Sun Feb 26, 2017 4:07 pm

If/when you are preparing to port the F1xx architecture to F4xx/F7xx/H7xx, the DMA allocation management has to be addressed. With multiple alternative DMA streams available for the peripherals and more peripherals requiring DMA, the visibility of this mapping matrix is important. In long run, it will not work if the peripheral libraries are assuming that certain DMA streams are available for them.

We either need to have on demand DMA stream request/release or static allocations with conflict detection.

stevestrong Sun Feb 26, 2017 4:25 pm

Ollie wrote:If/when you are preparing to port the F1xx architecture to F4xx/F7xx/H7xx, the DMA allocation management has to be addressed. With multiple alternative DMA streams available for the peripherals and more peripherals requiring DMA, the visibility of this mapping matrix is important. In long run, it will not work if the peripheral libraries are assuming that certain DMA streams are available for them.

We either need to have on demand DMA stream request/release or static allocations with conflict detection.

victor_pv Sun Feb 26, 2017 4:30 pm

How do you guys suggest to manage the DMA channels allocation?
As far as I know currently the core doesn’t keep any kind of data on what channels are enabled and for what peripheral, I guess that would require an additional library, that keeps that data, and manage allocation and releasing channels.

About the F4 porting, I think the first effort we should probably try is changing to the “tubes” API that leaflabs added to manage the F4 DMA streams. I haven’t used it in the F1, but I have seen code using it, so probably works.

Now, I am of the opinion of changing things one at a time when they are already working, so not to break anything.
Personally I will finish with what I started, adding the part to manage a callback, and repeated transfers. I will add another function to cover changing the source address and length at once as Steve suggested, that will help manage doublebuffering.

If someone can start working on code that manage DMA channels reservations, we can work in parallel. Once I’m finished with the extra functions and they work, I’ll try to see if I can get it working with tubes so it works for the F4.

EDIT: One way could be try to build some table like the PIN_MAP one, that maps the DMA channels with the peripherals that can use them, and add a bool indicating if that peripheral DMA requests have been enabled in that channel.

RogerClark Sun Feb 26, 2017 9:00 pm

Is it going to take much space to store the reservations ? e.g. do we need to just store something for each DMA channel e.g. whether its reserved or do we need to store a struct per channel ?

Is there just a way read back the information from the hardware. From what I recall it was possible to effectively parse the DMA registers to figure out quite a lot of information about their setup.

victor_pv Mon Feb 27, 2017 1:36 am

RogerClark wrote:Is it going to take much space to store the reservations ? e.g. do we need to just store something for each DMA channel e.g. whether its reserved or do we need to store a struct per channel ?

Is there just a way read back the information from the hardware. From what I recall it was possible to effectively parse the DMA registers to figure out quite a lot of information about their setup.

Ollie Mon Feb 27, 2017 4:41 am

I agree, that we will have many moons before we will use Arduino IDE for serious or semiserious F4/F7/H7 programming. In that frontier, the developers need to be aware of the capabilities and restrictions of different peripheral buses and the DMA channels in them.

I do propose that we are using STM terminology for these concepts. In that sense we need to identify that there are only few DMA channels and each of them have multiple streams. The streams in different channels can be active at the same time, but for the streams in a channel only one can be active at any point of time.

victor_pv Mon Feb 27, 2017 5:14 pm

Ollie wrote:I agree, that we will have many moons before we will use Arduino IDE for serious or semiserious F4/F7/H7 programming. In that frontier, the developers need to be aware of the capabilities and restrictions of different peripheral buses and the DMA channels in them.

I do propose that we are using STM terminology for these concepts. In that sense we need to identify that there are only few DMA channels and each of them have multiple streams. The streams in different channels can be active at the same time, but for the streams in a channel only one can be active at any point of time.

Sign in to reply