This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

nRF52840 SPI EasyDMA Double Buffering

Hello,

I am trying to determine whether or not the SPI EasyDMA functionality of the nrf52840 supports double buffering in the sense that the DMA controller will switch buffers without intervention from the processor. In other words, can you start one DMA transfer and then immediately queue the next DMA transfer so that the following scenario happens:

Example:

255 Byte DMA buffer x 2

1. Start DMA transfer from buffer1 and immediately queue DMA transfer from buffer2.

2. Receive DMA transfer complete interrupt from buffer1. The DMA controller has already automatically switched to buffer2. Load buffer1 with new data and queue this transfer.

3. Receive DMA transfer complete interrupt from buffer2. The DMA controller has already automatically switched to buffer1. Load buffer2 with new data and queue this transfer.

4. etc...

I ask because I am using the SoftDevice while receiving ADC data over SPI sampled at 250 Hz and I want to make sure that if the DMA interrupt is delayed by any BLE activity that I do not miss a sample from my ADC's. If my understanding of this is correct, is there an example I can reference?

SoftDevice: S140

SDK: 15.0.0

Thanks,

Derek

Parents
  • I believe you've understood it correctly. 

    In essence it's just the RXD and TXD registers that are double buffered, which means that they can be updated immediately after an EVENTS_STARTED has fired. You can use as many buffers as you like. 

    You should check out the EasyDMA array list feature, as it allows you to queue multiple buffers at once without having to manually set the pointer registers each time after an EVENTS_STARTED. 

  • Thanks for the response!

    Let me preface this with what I am trying to now that I have gotten my hands on my DAC hardware. The DAC requires 2 bytes to be written while toggling chip select in-between transfers. I am trying to store an entire waveform in RAM and kick off a DMA SPI transfer to loop over this data indefinitely or until stopped by the application without intervention from the processor because the DAC writes cannot be interrupted. At the same time, the SoftDevice will be "listening" for a stop command to halt the DAC updates. My concern is that the SoftDevice interrupts will corrupt the output waveform.

    After looking at this thread: https://devzone.nordicsemi.com/f/nordic-q-a/18638/easydma-array-list, it looks like this can be done using Array Lists, hardware timers, and PPI. However, does this method also toggle the chip select line between transfers of each buffer in the Array List?

    My idea was to store each 2-byte value into a very large ArrayList as shown below and loop over this using SPI DMA, hardware timers, and PPI with no processor usage. Is this possible? If not, how can I achieve this? Are there any examples for SPI?

    Thanks!

    typedef struct ArrayList
    {
    uint8_t buffer[2]; // 2 DAC bytes
    } ArrayList_type;
    
    ArrayList_type MyArrayList[SIZEOF_WAVEFORM];

  • The SPIM0-2 peripherals does not have HW Chip Select, but the new SPIM3/QSPI peripheral does.

    If you need the QSPI peripheral for other devices I suggest you used SPIM0-2 and control the CS pin via PPI and GPIOTE. SPI slaves will usually have timing requirements for when the CS pin is pulled low and high, therefore I suggest that you control both the CS and the SPIM's TASKS_START with a TIMER and PPI. 

    You'll need to connect the TIMER's Compare0 event to the a GPIOTE TASKS_OUT, where the GPIOTE task is set up to pull the CS low. Then you'll need to connect TIMER's Compare1 event to the SPIM's TASKS_START, and the SPIM's EVENTS_END event to the GPIOTE TASKS_OUT[1], where the GPIOTE task is set up to pull the CS line high between each transfer. You will also need to fork the SPIM's EVENTS_END event to the TIMER's TASKS_CLEAR to restart the cycle. 

  • From what I have read on the Devzone and the datasheet, what you describe makes sense. Is there a working SPI driver example that implements this or similar functionality that I can reference? Specifically in regards to setting up the timers, tasks, forking, and PPI? I greatly appreciate your help.

    Thanks!

Reply Children
  • See Timer ExamplePPI ExampleGPIOTE Example, SPI Master ExampleGPIOTE Driver description, SPI master Driver description, GPIOTE Driver and HAL API, PPI Driver and HAL API, and TIMER Driver and HAL API.

    I'd start by playing with the TIMER and GPIOTE via HAL. Set up some GPIOTE tasks like pin toggles that are triggered by a TIMER's compare tasks. Use a digital analyzer to see how the GPIOs behave. This will teach you the basics of the PPI system (EVENT --> TASK) and how to set up the TIMER and GPIOTE. 

    The PPI system uses the register address of an EVENT and couples it to the register address of a TASK. All drivers or HALs should have a function for getting the address of an EVENT or TASK. Those addresses are also given in the Registers chapter of a peripheral's technical specification. 

    You can use the SPIM driver to initialize the SPIM peripheral and the HAL API to enable the linked list feature with a call tonrf_spim_tx_list_enable

  • I have spent a bit of time playing around with these examples and have gotten DMA transmit working with SPIM3 and HW chip select. Thanks for pointing that out, I didn't realize SPIM3 had this feature. However, I have run into a small problem and hopefully I am missing something here.

    My DAC SPI writes need to occur at specific intervals without CPU involvement indefinitely. So what I have done is set up a periodic timer to trigger each SPI transfer using PPI. I have set up a second timer to count the number of transfers using the SPI end event. This works great in that my SPI transmit ArrayList is iterated through as expected solely using PPI and timers. The problem arises when you want to reset the SPI transfer pointer back to the top of the ArrayList. How can this be done without CPU involvement?

    Unless I am missing something, this was my idea as a possible solution. Since the SPIM buffer pointers are double buffered, I was going to set up a third timer to count the number of SPI transfers -1 and interrupt the CPU. So while the last transfer is ongoing, the 3rd timer callback will set the SPI pointer back to the top, ie Channel.PTR = &MyArrayList;. Once the next SPI transfer begins, it will start back at the top. My only concern with this is that the system could stall due to other interrupts and not reset the pointer soon enough and overflow the buffer.

    Does this approach makes sense and seem feasible? Is there an easier way that I am missing to reset the pointer back to the top of the buffer? Just to reiterate, it is imperative that there is no noticeable delay in DAC writes when looping over this buffer.

    Edit: Not sure why I wasn't able to find this article before, but it appears that the ArrayList pointer cannot be reset without CPU involvement after-all: https://devzone.nordicsemi.com/f/nordic-q-a/23349/ringbuffer-spi-twi-tx-with-fixed-sample-rate . Hopefully my idea of using a third timer will work unless you have any other tricks or ideas?

    Thanks!

  • I think you have grasped the functionality of the SPIM peripherals and linked list EasyDMA, and I'm afraid that you cannot be 100% sure that you can update the buffer pointer in time since we need the CPU to write to the register and you only have the time it takes to send the last two bytes to do it. 

    There is one thing I am curious about and that is if the TXD.PTR itself is incremented by the EasyDMA or if it is copied at the start of the SPI transaction. If it's the latter then the TXD.PTR should contain the pointer to the beginning of you buffer and you can immidiately trigger another transfer to start the process again. To verify this I suggest you halt the CPU when you have finished transfering your buffer and read the content of SPIM3's TXD.PTR register. If it contains the address of your buffer, then you should have a 100% glitch free communication with your DAC. 

  • Just wanted to follow up on how I solved this issue. TXD.PTR is indeed incremented by the DMA controller. 

    What I did to get around the fact that you can't reset the pointer without the CPU is added a 10 msec buffer "overhead" to my DAC ArrayList buffer. In other words, I duplicate my DAC waveform for an additional 10 msec beyond where I expect the transmit buffer to end. Once my interrupt fires to tell me that I have transmitted the entire "expected" buffer, I then check TXD.PTR to see how far beyond the expected buffer it went into the extra 10 msec buffer. I then move the pointer back to the start of the buffer plus the offset of (TXD.PTR - end of expected buffer). Worst case scenario I duplicate a single data-point of the waveform on the DAC. Testing with BLE enabled yields excellent results and I haven't had any issues yet.

    Thanks again for all of your help. Without it I wouldn't have resolved this issue.

  • Hello, I have a follow up question to this: How do you make sure that between the moment you read TXD.PTR and the moment you overwrite it the DMA did not update it?

    I was thinking that one could do active wait inside the interrupt for an EVENTS_STARTED and then change it right after, but I wonder if there is a way to make sure that such active poll is not interrupted by a higher-priority interrupt.

    Thank you

Related