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

Contiguous output from SPIM with EasyDMA on the nRF52840

Hi,

As background, what I am trying to implement is continuous uninterrupted SPI master output from the nRF52840, compatible with the MCP4822 two-channel 12-bit DAC.
The MCP4822 requires programming waveforms from the nRF52840 as shown in this photo.


i.e. it requires two bytes of data to be sent via SPI while the CSN signal is low (to program one channel), followed by another two bytes of data to be sent via SPI while the CS signal is low (to program one channel), and then finally, it needs the LDAC signal to go low to cause the two analog outputs to change simultaneously.

The desired output frequency for this entire sequence is 16KHz, and I want too involve the CPU as little as possible in the process (as it will be busy doing other things.)

My understanding so far is that that the SPIM3 device is capable of controlling the CSN line, and that it can be used with EasyDMA in ArrayList mode.
I gather that the EasyDMA is limited to outputting a MAXCNT of 256 bytes, but that the ArrayList mode allows a list of pointers to contiguously allocated blocks of 256 bytes to be given to the EasyDMA, which allows it to transmit a longer burst of data.

It is not clear to me from the documentation how the timings of that data transmission are controlled.
For example:
What controls the SCK clock rate of the SPIM?
What controls how many bits/bytes of data are written out of the SPIM before the CSN line is brought high again?
What controls the time interval between successive SPIM writes (of 2-bytes each) from the ArrayList?

In order to allow contiguous transmission from the SPIM, I will need to have two memory "swing" buffers, wherein the CPU fills one buffer while the contents of the other is being transmitted by the SPIM. Then, when the transmission is complete, the buffers swap over and the process continues seamlessly.
I have several questions related to how to implement this.
Firstly I don't know how to trigger an interrupt routine when the SPIM's steadily incrementing TXD.PTR has exceeded a particular value within the ArrayList. (which will be longer than 256 bytes long)
Secondly, as there will typically be a delay between the interrupt triggering and the interrupt service routine (ISR) actually being executed, we need to allow the SPIM module to continue transmitting extra data from the buffer after the interrupt triggers, then, within the ISR read the TXD.PTR to see where the SPIM has actually go to, and use that value as the starting offset when the swing buffers are swapped over. The trouble is, I have no idea how to configure the SPIM and EasyDMA ArrayLists to behave this way, as I am lacking any detailed usage documentation.

For Example:
We could create two "swing buffer" EasyDMA ArrayLists, each with the capacity to transmit data for 30ms each. This could contiguously transmit a series of 20ms audio "frames".
If we fill the first 30ms capacity transmit buffer with 20ms of audio from frame 0, and 10ms from frame 1. The Easy DMA transmission is then started from the first buffer, address 0.
We then start filling a second 30ms buffer with 20ms of audio from frame 1 and 10ms from frame 2.
The interrupt is triggered when the TXD.PTR has reached the 20ms mark, but the SPIM continues to transmit subsequent data from the buffer until told otherwise.
The actual interrupt service routine is later entered and serviced at (say) 24ms. The EasyDMA transmission is then switched to start transmitting from the second buffer, at address 4ms.
The transfer between playing audio from the first buffer and playing audio from the second buffer is then seamless.

None of the above complexity would be required if the SPIM and EasyDMA supported ring-buffers, but oh well.

Finally, we have the issue of generating the LDAC pulses, which (ideally) need to be one SCK clock long, and synchronised with the end of each pair of SPIM transmissions.
I'm not sure how to implement this. Would something to do with the GPIOTE and PPI systems do the trick, perhaps if the timers were regularly re-synchronised within the ISR?

There are a number of errata involving the SPIM3 module, so answers that take those "gotchas" into account would be appreciated.

Regards,
Nicholas Lee

  • Hi,

     

    As background, what I am trying to implement is continuous uninterrupted SPI master output from the nRF52840, compatible with the MCP4822 two-channel 12-bit DAC.
    The MCP4822 requires programming waveforms from the nRF52840 as shown in this photo.


    i.e. it requires two bytes of data to be sent via SPI while the CSN signal is low (to program one channel), followed by another two bytes of data to be sent via SPI while the CS signal is low (to program one channel), and then finally, it needs the LDAC signal to go low to cause the two analog outputs to change simultaneously.

    The desired output frequency for this entire sequence is 16KHz, and I want too involve the CPU as little as possible in the process (as it will be busy doing other things.)

    My understanding so far is that that the SPIM3 device is capable of controlling the CSN line, and that it can be used with EasyDMA in ArrayList mode.
    I gather that the EasyDMA is limited to outputting a MAXCNT of 256 bytes, but that the ArrayList mode allows a list of pointers to contiguously allocated blocks of 256 bytes to be given to the EasyDMA, which allows it to transmit a longer burst of data.

    It is not clear to me from the documentation how the timings of that data transmission are controlled.
    For example:
    What controls the SCK clock rate of the SPIM?

    The clock frequency itself is controlled by the .FREQUENCY register, a transaction frequency would be approx. FREQUENCY / 16 bits in your case, excluding the CSN rise/fall.

    The sensor itself seems to latch the DAC output based on the /LDAC signal, so it seems you can send data to the device at a faster frequency, then toggle this pin at a given frequency to set the desired output in a given frequency.

    What controls how many bits/bytes of data are written out of the SPIM before the CSN line is brought high again?

     This is controlled by the DMA and the .MAXCNT that sets the amount of bytes for each section. See this section for more detailed information:

    https://www.nordicsemi.com/DocLib/Content/Product_Spec/nRF52840/latest/spim?912#concept_lhv_fx2_wr

    What controls the time interval between successive SPIM writes (of 2-bytes each) from the ArrayList?

     For peripherals that support EasyDMA list, the sequence of them all are similar in the setup:

    https://www.nordicsemi.com/DocLib/Content/Product_Spec/nRF52840/latest/easydma?103#arraylist

    Once configured, you trigger a _START task to run the sequence. When you trigger this is up to your application.

    For instance: This can be tied together using the PPI, where you can have a timer running in the background that triggers this every X us.

     

    In order to allow contiguous transmission from the SPIM, I will need to have two memory "swing" buffers, wherein the CPU fills one buffer while the contents of the other is being transmitted by the SPIM. Then, when the transmission is complete, the buffers swap over and the process continues seamlessly.
    I have several questions related to how to implement this.
    Firstly I don't know how to trigger an interrupt routine when the SPIM's steadily incrementing TXD.PTR has exceeded a particular value within the ArrayList. (which will be longer than 256 bytes long)
    Secondly, as there will typically be a delay between the interrupt triggering and the interrupt service routine (ISR) actually being executed, we need to allow the SPIM module to continue transmitting extra data from the buffer after the interrupt triggers, then, within the ISR read the TXD.PTR to see where the SPIM has actually go to, and use that value as the starting offset when the swing buffers are swapped over. The trouble is, I have no idea how to configure the SPIM and EasyDMA ArrayLists to behave this way, as I am lacking any detailed usage documentation.

     There's support in the SPIM driver to send repeated transfers in this function:

    https://www.nordicsemi.com/DocLib/Content/SDK_Doc/nRF5_SDK/v15-3-0/group__nrfx__spim#gae4b5f522da698ed536ce915ede1216ac

    The "algorithm" for using ArrayList is better described here: https://devzone.nordicsemi.com/f/nordic-q-a/18638/easydma-array-list

    For Example:
    We could create two "swing buffer" EasyDMA ArrayLists, each with the capacity to transmit data for 30ms each. This could contiguously transmit a series of 20ms audio "frames".
    If we fill the first 30ms capacity transmit buffer with 20ms of audio from frame 0, and 10ms from frame 1. The Easy DMA transmission is then started from the first buffer, address 0.
    We then start filling a second 30ms buffer with 20ms of audio from frame 1 and 10ms from frame 2.
    The interrupt is triggered when the TXD.PTR has reached the 20ms mark, but the SPIM continues to transmit subsequent data from the buffer until told otherwise.
    The actual interrupt service routine is later entered and serviced at (say) 24ms. The EasyDMA transmission is then switched to start transmitting from the second buffer, at address 4ms.
    The transfer between playing audio from the first buffer and playing audio from the second buffer is then seamless.

    None of the above complexity would be required if the SPIM and EasyDMA supported ring-buffers, but oh well.

    Finally, we have the issue of generating the LDAC pulses, which (ideally) need to be one SCK clock long, and synchronised with the end of each pair of SPIM transmissions.
    I'm not sure how to implement this. Would something to do with the GPIOTE and PPI systems do the trick, perhaps if the timers were regularly re-synchronised within the ISR?

    There are a number of errata involving the SPIM3 module, so answers that take those "gotchas" into account would be appreciated.

     As briefly mentioned, the /LDAC pin looks to be the synchronization at the DAC output, so the frequency of the SPI transfer can be faster than the update frequency of the DAC, as long as your /LDAC signal is synched towards your wanted frequency. If you use PPI + TIMER + GPIOTE, you can synchronize this sequence based on the /LDAC signal.

    Here's a theoretical approach (steps like GPIOTE, PPI, and TIMER initial setup is omitted):

    PPI channel 0:

    TASK: GPIOTE -> TASKS_CLR

    Event: TIMER -> EVENTS_COMPARE[0]

    PPI Channel 1:

    TASK: GPIOTE -> TASKS_SET

    Event: TIMER -> EVENTS_COMPARE[1]

    PPI.FORK: SPIM3 -> TASKS_START

    Timer in this case needs to be set up with capture CC[0] smaller than CC[1], then you add SHORT to clear the timer on EVENTS_COMPARE[1].

     

    Kind regards,

    Håkon

  • Thank you, I will try implementing your suggestions, and come back to this thread if I encounter any problems.

  • Dear Håkon,
    I have been trying very hard to understand all this and I have done a lot of reading. However, I think I need a few points clarifying before I will be able to implement anything that works. I'm still new to developing with the nRF52840, so I don't understand all the jargon being referred to.

    What does your cryptic answer "PPI.FORK: SPIM3 -> TASKS_START" actually mean?
    What is triggering what, in what order?
    Is this a SPIM starting event triggering the TIMER, or the TIMER event triggering the SPIM to start?

    Regarding the EasyDMA list, I have found the documentation is unintelligible (to me) and the associated example code doesn't actually compile.
    Ref: https://www.nordicsemi.com/DocLib/Content/Product_Spec/nRF52840/latest/easydma?103#arraylist

    This page shows code allocating an array using

    ArrayList_type ReaderList[3]  __at__ 0x20000000;

    However __at__ doesn't work under SEGGER, as it isn't GCC compatible code. Does this matter or will I get a HARD FAULT if the array memory is allocated at a compiler chosen location?

    Also, the following example code doesn't work as MYPERIPHERAL isn't defined and the use needs to be psychic to read the developer's minds to know what code is used to create a  MYPERIPHERAL .

      MYPERIPHERAL->READER.MAXCNT = BUFFER_SIZE;
      MYPERIPHERAL->READER.PTR = &ReaderList;
      MYPERIPHERAL->READER.LIST = MYPERIPHERAL_READER_LIST_ArrayList;

    I'm not sure how this pseudo-code relates to anything, and I want to use the nfrx driver anyway.

    In my case, where each SPI write is 2-bytes long, would my BUFFERSIZE be 2?
    If I want to store an ArrayList of ( 30ms of these at 32KHz), i.e. 960 writes, would the code be as follows:

    #define BUFFER_SIZE  2
      
      typedef struct ArrayList
      {
        uint8_t buffer[BUFFER_SIZE];
      } ArrayList_type;
      
      ArrayList_type WriterList[960]

    I was also unable to work out how to use the nrfx_spim_xfer command as the "documentation" you pointed me to was just a list of function names with no explanation of how they work, how to use them or what they do.
    That might be a great resource if you are already an expert and need a reminder, but it is utterly useless for a beginner who wants to get started in a timely manner.

    So far, my initialisation code looks like this, but I'm not sure if I am missing any vital steps, or how to start or trigger a recurring sequence of writes from the EasyDMA ArrayList

    #define SPI_INSTANCE  3
    #define BUFFER_SIZE 2 // There are 2 bytes in each SPI transfer to the DAC
    #define NUMBER_OF_BUFFERS 960 // There are 960 2-byte transfers to the DAC every 30ms
    static const nrfx_spim_t spim3 = NRFX_SPIM_INSTANCE(SPI_INSTANCE);  /**< SPI instance. */

    typedef struct ArrayList
    {
      volatile uint8_t buffer[BUFFER_SIZE];
    } ArrayList_type;
    static volatile ArrayList_type WriterList0[NUMBER_OF_BUFFERS];
    static volatile ArrayList_type WriterList1[NUMBER_OF_BUFFERS];

    void SPIM3_init(void)
    {
      nrfx_err_t err_code = NRFX_SUCCESS;

      nrfx_spim_config_t spi_config = NRFX_SPIM_DEFAULT_CONFIG;
      spi_config.ss_pin         = SPI_CSN_PIN;
      spi_config.miso_pin       = NRFX_SPIM_PIN_NOT_USED;
      spi_config.mosi_pin       = SPI_MOSI_PIN;
      spi_config.sck_pin        = SPI_SCK_PIN;
      spi_config.dcx_pin        = NRFX_SPIM_PIN_NOT_USED;
      spi_config.use_hw_ss      = true;
      spi_config.ss_active_high = false;
      spi_config.frequency      = NRF_SPIM_FREQ_2M;
      spi_config.mode           = NRF_SPIM_MODE_0;
      spi_config.bit_order      = NRF_SPIM_BIT_ORDER_MSB_FIRST;

      err_code = nrfx_spim_init(&spim3, &spi_config, NULL, NULL);
      APP_ERROR_CHECK(err_code);

      nrfx_spim_xfer_desc_t xfer_desc = NRFX_SPIM_XFER_TRX(WriterList0, BUFFER_SIZE, NULL, 0);

      err_code = nrfx_spim_xfer(&spim3, &xfer_desc, NRFX_SPIM_FLAG_TX_POSTINC||NRFX_SPIM_FLAG_REPEATED_XFER);
      APP_ERROR_CHECK(err_code);
    }





  • Hi,

    Nicholas Lee said:
    What does your cryptic answer "PPI.FORK: SPIM3 -> TASKS_START" actually mean?
    What is triggering what, in what order?
    Is this a SPIM starting event triggering the TIMER, or the TIMER event triggering the SPIM to start?

    From the TIMER event, you trigger two tasks:

    NRF_GPIOTE->TASKS_SET

    NRF_SPIMx->TASKS_START (where x is the instance used)

     

    This means that when your pin is cleared again (via NRF_GPIOTE->TASKS_SET), you also start the SPI transaction.

    When using the driver/library, you'll need to fetch the start task register by calling "uint32_t spi_start_task_addr = nrf_drv_spi_start_task_get(my_spi_instance)"

     

    Nicholas Lee said:
    However __at__ doesn't work under SEGGER, as it isn't GCC compatible code. Does this matter or will I get a HARD FAULT if the array memory is allocated at a compiler chosen location?

    This is not strictly needed in the first process of getting things working, but it is recommended that you keep the list array in a memory section that is not touched by the CPU, to avoid race-conditions on the internal AHB bus. This is done using the flash_placement.xml file in SES: https://studio.segger.com/index.htm?https://studio.segger.com/ide_section_placement.htm

     

    We do section placement in the DFU, so that can be used as a reference when you get that far.

     

    Nicholas Lee said:

    I'm not sure how this pseudo-code relates to anything, and I want to use the nfrx driver anyway.

    In my case, where each SPI write is 2-bytes long, would my BUFFERSIZE be 2?
    If I want to store an ArrayList of ( 30ms of these at 32KHz), i.e. 960 writes, would the code be as follows:

    When you setup the list array like this:

    #define BUFFER_SIZE 2 // There are 2 bytes in each SPI transfer to the DAC
    #define NUMBER_OF_BUFFERS 960 // There are 960 2-byte transfers to the DAC every 30ms
    
    typedef struct ArrayList
    {
      volatile uint8_t buffer[BUFFER_SIZE];
    } ArrayList_type;
    static volatile ArrayList_type WriterList0[NUMBER_OF_BUFFERS];
    static volatile ArrayList_type WriterList1[NUMBER_OF_BUFFERS];

    You are setting up the actual SPI buffer, and with the flags NRF_DRV_SPI_FLAG_REPEATED_XFER and NRF_DRV_SPI_FLAG_TX_POSTINC, you state to the driver that its the same buffer that is going to be transferred every time. 

    Every time you call the TASKS_START, the buffer should be incremented. You can test this by scoping the MOSI line and trigger the task_start, to see if your buffer increments properly.

    Note: The LIST feature does not have a "auto stop", so that will keep on rolling through your memory. If you send every 30 ms, 960 transactions, I'd recommend that you set a timer to timeout after 960 transactions has occurred so that you can "reset" the process.

    Next up, you need to tie the SPI->TASKS_START in with your timing requirements and /LDAC pin (via GPIOTE). If you haven't used GPIOTE and PPI, I'd recommend that you look at the examples in the peripherals folder. PPI is a interconnect bus for events and tasks, where you can trigger tasks based on events without having the CPU involved.

     

    Kind regards,

    Håkon

  • Dear Håkon,
    Thanks.
    I need to use swing-arrays to write vales out to the DAC such that the CPU is filling one array while the other is being written out to the DAC using EasyDMA. When one array empties, the the arrays swap over, allowing for continuous transmission.
    If the arrays have to be placed in a region of memory that the CPU can't access, how can this ever work? I really don't understand what I am suppose to do to solve this.

    For reference, here is the flash_placement.xml file for the project.

    <!DOCTYPE Linker_Placement_File>
    <Root name="Flash Section Placement">
      <MemorySegment name="$(FLASH_NAME:FLASH)">
        <ProgramSection alignment="0x100" load="Yes" name=".vectors" start="$(FLASH_START:)" />
        <ProgramSection alignment="4" load="Yes" name=".init" />
        <ProgramSection alignment="4" load="Yes" name=".init_rodata" />
        <ProgramSection alignment="4" load="Yes" name=".text" />
        <ProgramSection alignment="4" load="Yes" name=".dtors" />
        <ProgramSection alignment="4" load="Yes" name=".ctors" />
        <ProgramSection alignment="4" load="Yes" name=".rodata" />
        <ProgramSection alignment="4" load="Yes" name=".ARM.exidx" address_symbol="__exidx_start" end_symbol="__exidx_end" />
        <ProgramSection alignment="4" load="Yes" runin=".fast_run" name=".fast" />
        <ProgramSection alignment="4" load="Yes" runin=".data_run" name=".data" />
        <ProgramSection alignment="4" load="Yes" runin=".tdata_run" name=".tdata" />
      </MemorySegment>
      <MemorySegment name="$(RAM_NAME:RAM);SRAM">
        <ProgramSection alignment="0x100" load="No" name=".vectors_ram" start="$(RAM_START:$(SRAM_START:))" />
        <ProgramSection alignment="4" load="No" name=".fast_run" />
        <ProgramSection alignment="4" load="No" name=".data_run" />
        <ProgramSection alignment="4" load="No" name=".bss" />
        <ProgramSection alignment="4" load="No" name=".tbss" />
        <ProgramSection alignment="4" load="No" name=".tdata_run" />
        <ProgramSection alignment="4" load="No" name=".non_init" />
    	<ProgramSection alignment="4" size="((__RAM_segment_end__ - __STACKSIZE__) - __heap_load_start__)" load="No" name=".heap" />
        <ProgramSection alignment="8" size="__STACKSIZE__" load="No" place_from_segment_end="Yes" name=".stack" />
        <ProgramSection alignment="8" size="__STACKSIZE_PROCESS__" load="No" name=".stack_process" />
      </MemorySegment>
      <MemorySegment name="$(FLASH2_NAME:FLASH2)">
        <ProgramSection alignment="4" load="Yes" name=".text2" />
        <ProgramSection alignment="4" load="Yes" name=".rodata2" />
        <ProgramSection alignment="4" load="Yes" runin=".data2_run" name=".data2" />
      </MemorySegment>
      <MemorySegment name="$(RAM2_NAME:RAM2)">
        <ProgramSection alignment="4" load="No" name=".data2_run" />
        <ProgramSection alignment="4" load="No" name=".bss2" />
      </MemorySegment>
    </Root>


    The API call "spim_list_enable_handle()" isn't a documented part of the API, and you didn't say in what code-context to apply this call, or what parameters to provide it with.
    After searching the driver source code, I have found it is an internal helper function inside nrfx_spim.c, that isn't listed in the header file, so it isn't even callable from my code.
    It appears to only ever be used from inside the function spim_xfer.

    As best as I can tell, spim_list_enable_handle would be called with the SPIM instance pointer, and the NRFX_SPIM_FLAG_TX_POSTINC flag. This would just cause it to call the (legacy?) spim  HAL function: nrf_spim_tx_list_enable(p_spim); This function has a meagre 1-line of documentation that says "Function for enabling the TX list feature."

    So, I still don't really understand the proper sequence of function calls required to use the EasyDMA ArrayList properly.

    Does spim_list_enable_handle() replace the call to nrfx_spim_xfer() , or complement it in some way?

    The EasyDMA ArrayList is such a fundamental feature for using the nRF52840, that a proper plain-English tutorial / blog post (that doesn't assume insider knowledge!!!) is badly needed.

Related