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

Parents
  • 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

  • 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

Reply
  • 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

Children
  • 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.

  • Hi Nicholas,

    Nicholas Lee said:
    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.

    I am very sorry, this is on me. As you send in the flags "NRF_DRV_SPI_FLAG_REPEATED_XFER | NRF_DRV_SPI_FLAG_TX_POSTINC", this will be set by the driver. I have edited my former post.

    Nicholas Lee said:
    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.

     The RAM is split into different sections, as shown here:

    https://www.nordicsemi.com/DocLib/Content/Product_Spec/nRF52840/latest/memory?17#ram

    Make sure that your buffers are located in a RAM region where the CPU does not normally read/write from.

    As mentioned, I would not set this specific requirement high on the list yet, but just keep it in mind for later.

     

    Nicholas Lee said:

    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.

     I agree, this is not documented very good for the easydma list feature, and given that there's no examples for this in the SDK, it makes it harder to implement. That being said, By looking at your former code, it looks like you have configured the SPIM list feature correctly, you are just lacking a way to trigger the NRF_SPIM->TASKS_START. For testing reasons, you can set your buffer in a incremental fashion, then trigger the start task from your main loop to see if the MOSI line shifts out the next buffer entry.

     

    Kind regards,

    Håkon

  • Dear Håkon,
    I am configuring the SPIM as follows, such that it doesn't start immediately, allowing it to later be triggered by a recurring timer event.

    // Configure the SPIM3, PPI and GPIOTE to enable regular writing to the stereo DAC
    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);
      NFRX_APP_ERROR_CHECK(err_code);
    
      // Enable the use of EasyDMA ArrayList
      spim3.p_reg->TXD.LIST = 1;
    
      // Function for initializing the SPI master driver instance.
      nrfx_spim_xfer_desc_t xfer_desc = NRFX_SPIM_XFER_TX(WriterList0, BUFFER_SIZE);
    
      // Function for starting the SPI data transfer
      // NRFX_SPIM_FLAG_REPEATED_XFER = Flag indicating that the transfer will be executed multiple times. 
      // NRFX_SPIM_FLAG_TX_POSTINC = TX buffer address incremented after transfer. 
      // NRFX_SPIM_FLAG_HOLD_XFER = Set up the transfer but do not start it. 
      err_code = nrfx_spim_xfer(&spim3, &xfer_desc, NRFX_SPIM_FLAG_TX_POSTINC|NRFX_SPIM_FLAG_REPEATED_XFER|NRFX_SPIM_FLAG_HOLD_XFER);
      NFRX_APP_ERROR_CHECK(err_code);
    }


    However, the call to nrfx_spim_xfer hangs.

    Tracing inside this library call, I can see that it hangs in file nrfx_spim.c, function spim_xfer, line 522, processing this while loop:

    while (!nrf_spim_event_check(p_spim, NRF_SPIM_EVENT_END)){}

    This appears to be exactly the same library "bug" that was reported 2 years ago in this posting:
    https://devzone.nordicsemi.com/f/nordic-q-a/19796/possible-spi-master-bug-when-using-dma

    It would be great if you could confirm this assumption, provide a precisely worded work-around, and then actually fix the bug in the distributed library.
    Thanks.

    Regards,
    Nicholas Lee

  • Dear Håkon,
    Having done a small modification to nrfx_spim.c, I can now get nice automated SPI output waveforms on the SS, SCK, MOSI, and LDAC lines, as shown below.



    i.e. it correctly transmits two sets of 16-bits SPI writes, followed by an LDAC pulse.

    The code is shown below...

    File    : main.c
    Purpose : Generic application start
    
    */
    
    #include <stdio.h>
    #include <stdlib.h>
    // #include "sdk_config.h"
    #include "nrfx_config.h"
    #include "nrfx_spim.h"
    #include "nrfx_timer.h"
    #include "nrf_gpio.h"
    #include "nrfx_gpiote.h"
    #include "nrfx_ppi.h"
    #include "opus.h"
    #include "nrfx_coredep.h" // for delay function
    #include "app_error.h"
    #include <math.h>
    
    // Define the pins used for testing on the nRF52840_DK board
    #include "pca10056.h"
    
    // Translation macro for error handling
    #define NFRX_APP_ERROR_CHECK(err_code) APP_ERROR_CHECK(err_code-NRFX_ERROR_BASE_NUM+NRF_SUCCESS)
    
    // Define the pins used on the Headphones board
    #define AIN0_PIN     NRF_GPIO_PIN_MAP(0,2)  // P0.02 = SIO_02
    #define AIN1_PIN     NRF_GPIO_PIN_MAP(0,3)  // P0.03 = SIO_03
    #define SPI_MISO_PIN NRF_GPIO_PIN_MAP(0,4)  // P0.04 = SIO_04
    #define UART_RTS_PIN NRF_GPIO_PIN_MAP(0,5)  // P0.05 = SIO_05
    #define UART_TX_PIN  NRF_GPIO_PIN_MAP(0,6)  // P0.06 = SIO_06
    #define UART_CTS_PIN NRF_GPIO_PIN_MAP(0,7)  // P0.07 = SIO_07
    #define UART_RX_PIN  NRF_GPIO_PIN_MAP(0,8)  // P0.08 = SIO_08
    #define GPOUT_PIN    NRF_GPIO_PIN_MAP(0,12) // P0.12 = SIO_12
    #define SW_PIN       NRF_GPIO_PIN_MAP(0,13) // P0.13 = SIO_13
    #define B_PIN        NRF_GPIO_PIN_MAP(0,14) // P0.14 = SIO_16
    #define A_PIN        NRF_GPIO_PIN_MAP(0,16) // P0.16 = SIO_14
    #define RESET_PIN    NRF_GPIO_PIN_MAP(0,18) // P0.18 = SIO_18
    #define POWER_EN_PIN NRF_GPIO_PIN_MAP(0,24) // P0.24 = SIO_24
    #define I2C_SDA_PIN  NRF_GPIO_PIN_MAP(0,26) // P0.26 = SIO_26
    #define I2C_SCL_PIN  NRF_GPIO_PIN_MAP(0,27) // P0.27 = SIO_27
    #define AIN4_PIN     NRF_GPIO_PIN_MAP(0,28) // P0.28 = SIO_28
    #define AIN5_PIN     NRF_GPIO_PIN_MAP(0,29) // P0.29 = SIO_29
    #define AIN6_PIN     NRF_GPIO_PIN_MAP(0,30) // P0.30 = SIO_30
    #define AIN7_PIN     NRF_GPIO_PIN_MAP(0,31) // P0.31 = SIO_31
    
    #define KG_PIN       NRF_GPIO_PIN_MAP(1,2)  // P1.02 = SIO_34
    #define KR_PIN       NRF_GPIO_PIN_MAP(1,4)  // P1.04 = SIO_36
    #define SW1_PIN      NRF_GPIO_PIN_MAP(1,5)  // P1.05 = SIO_37
    #define SPI_MOSI_PIN NRF_GPIO_PIN_MAP(1,8)  // P1.08 = SIO_40
    #define SPI_SCK_PIN  NRF_GPIO_PIN_MAP(1,9)  // P1.09 = SIO_41
    #define SW3_PIN      NRF_GPIO_PIN_MAP(1,10) // P1.10 = SIO_42
    #define SW2_PIN      NRF_GPIO_PIN_MAP(1,11) // P1.11 = SIO_43
    #define SPI_CSN_PIN  NRF_GPIO_PIN_MAP(1,12) // P1.12 = SIO_44
    #define SPI_LDAC_PIN NRF_GPIO_PIN_MAP(1,13) // P1.13 = SIO_45
    #define JACK_EN_PIN  NRF_GPIO_PIN_MAP(1,14) // P1.14 = SIO_46
    #define ISET2_PIN    NRF_GPIO_PIN_MAP(1,15) // P1.15 = SIO_47
    
    #define DAC_A    0x0000 // DAC Channel Selection (0=>A, 1=>B)
    #define DAC_B    0x8000 // DAC Channel Selection (0=>A, 1=>B)
    #define DAC_GAx1 0x2000 // DAC Gain Selection (0=>2x, 1=>1x)
    #define DAC_GAx2 0x0000 // DAC Gain Selection (0=>2x, 1=>1x)
    #define DAC_RUN  0x1000 // DAC Shutdown (0=>SHUTDOWN, 1=>RUNNING)
    #define DAC_SHDN 0x0000 // DAC Shutdown (0=>SHUTDOWN, 1=>RUNNING)
    
    #define AngleStep 0.0098174770424681038701957605727484
    
    // EasyDMA configuration for the SPI DAC
    #define SPI_INSTANCE 3 // SPI instance index.
    #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.
    static volatile bool WriterListTransmitting = 0;
      
    typedef struct ArrayList
    {
      volatile uint8_t buffer[BUFFER_SIZE];
    } ArrayList_type;
    static volatile ArrayList_type WriterList0[NUMBER_OF_BUFFERS]; // __at__ 0x200022B0; NB: __at__ is not supported in SEGGER due to GCC restrictions.
    static volatile ArrayList_type WriterList1[NUMBER_OF_BUFFERS]; // __at__ 0x200022B0; NB: __at__ is not supported in SEGGER due to GCC restrictions.
    
    // Declare the Timer instances (NB: Timer 0 is used by the soft device)
    static const nrfx_timer_t m_timer1 = NRFX_TIMER_INSTANCE(1);
    static const nrfx_timer_t m_timer2 = NRFX_TIMER_INSTANCE(2);
    
    // Declare the PPI instances
    nrf_ppi_channel_t ppi_channel0;
    nrf_ppi_channel_t ppi_channel1;
    nrf_ppi_channel_t ppi_channel2;
    nrf_ppi_channel_t ppi_channel3;
    nrf_ppi_channel_t ppi_channel4;
    
    void GPIO_init(void)
    {
      nrfx_err_t err_code = NRFX_SUCCESS;
      // Initialise the GPIOTE driver if it hasn't already been initilaised
      if(!nrfx_gpiote_is_init())
      {
        err_code = nrfx_gpiote_init();
        NFRX_APP_ERROR_CHECK(err_code);
      }
    
      // set the IO direction and IO type (digital/analog) for every used pin on the IC.
    
      // Digital Outputs
      nrf_gpio_cfg_output(SPI_SCK_PIN);
      nrf_gpio_cfg_output(SPI_MOSI_PIN);
      nrf_gpio_cfg_output(SPI_CSN_PIN);
      nrf_gpio_cfg_output(SPI_LDAC_PIN);
      nrf_gpio_cfg_output(ISET2_PIN);
      nrf_gpio_cfg_output(KR_PIN);
      nrf_gpio_cfg_output(KG_PIN);
      nrf_gpio_cfg_output(POWER_EN_PIN);
      nrf_gpio_cfg_output(GPOUT_PIN);
      nrf_gpio_cfg_output(UART_TX_PIN);
      nrf_gpio_cfg_output(I2C_SDA_PIN);
      nrf_gpio_cfg_output(I2C_SCL_PIN);
      nrf_gpio_cfg_output(UART_RTS_PIN);
      nrf_gpio_cfg_output(JACK_EN_PIN);
    
      // Digital Inputs
      nrf_gpio_cfg_input(SPI_MISO_PIN, NRF_GPIO_PIN_PULLDOWN);
      nrf_gpio_cfg_input(RESET_PIN, NRF_GPIO_PIN_PULLUP);
      nrf_gpio_cfg_input(A_PIN, NRF_GPIO_PIN_PULLDOWN);
      nrf_gpio_cfg_input(B_PIN, NRF_GPIO_PIN_PULLDOWN);
      nrf_gpio_cfg_input(SW_PIN, NRF_GPIO_PIN_PULLDOWN);
      nrf_gpio_cfg_input(GPOUT_PIN, NRF_GPIO_PIN_PULLUP);
      nrf_gpio_cfg_input(UART_RX_PIN, NRF_GPIO_PIN_PULLUP);
      nrf_gpio_cfg_input(UART_CTS_PIN, NRF_GPIO_PIN_PULLUP);
      nrf_gpio_cfg_input(AIN7_PIN, NRF_GPIO_PIN_NOPULL);
      nrf_gpio_cfg_input(AIN6_PIN, NRF_GPIO_PIN_NOPULL);
      nrf_gpio_cfg_input(AIN5_PIN, NRF_GPIO_PIN_NOPULL);
      nrf_gpio_cfg_input(AIN4_PIN, NRF_GPIO_PIN_NOPULL);
      nrf_gpio_cfg_input(AIN1_PIN, NRF_GPIO_PIN_NOPULL);
      nrf_gpio_cfg_input(AIN0_PIN, NRF_GPIO_PIN_NOPULL);
      nrf_gpio_cfg_input(SW1_PIN, NRF_GPIO_PIN_PULLDOWN);
      nrf_gpio_cfg_input(SW2_PIN, NRF_GPIO_PIN_PULLDOWN);
      nrf_gpio_cfg_input(SW3_PIN, NRF_GPIO_PIN_PULLDOWN);
    
      // configure gpiote with the task to toggle the bit. Takes the pin to toggle and the task config
      nrfx_gpiote_out_config_t config = NRFX_GPIOTE_CONFIG_OUT_TASK_TOGGLE(true);
      err_code = nrfx_gpiote_out_init(SPI_LDAC_PIN, &config);
      NFRX_APP_ERROR_CHECK(err_code);
    }
    
    void timer2_event_handler(nrf_timer_event_t event_type, void *p_context)
    {
      nrfx_err_t err_code = NRFX_SUCCESS;
      volatile uint32_t uiTXD_PTR;
      volatile uint32_t uiTXD_PTR_OFFSET;
    
      // Read the TXD.PTR value from the currently transmitting ArrayList to see 
      // where in the buffer the transmit process has currently reached
      uiTXD_PTR = spim3.p_reg->TXD.PTR;
    
      // Stop the SPIM3 transmission
      nrfx_spim_abort(&spim3);
    
      // NOTES: Consider connecting WriterListTransmitting to a GPIO line to test that it toggles evenly at 16KHz
      if (WriterListTransmitting==0)
      {
        uiTXD_PTR_OFFSET = ((uint32_t)WriterList1 - uiTXD_PTR) / 2;
    
        // Start the SPIM3 transmission from the other ArrayList,at a starting offset 
        // relative to the TXD.PTR
        WriterListTransmitting=1;
        // Enable the use of EasyDMA ArrayList
        spim3.p_reg->TXD.LIST = 1;
        // Function for initializing the SPI master driver instance.
        nrfx_spim_xfer_desc_t xfer_desc = NRFX_SPIM_XFER_TX(&WriterList1[uiTXD_PTR_OFFSET], BUFFER_SIZE);
        // Function for starting the SPI data transfer
        // NRFX_SPIM_FLAG_REPEATED_XFER = Flag indicating that the transfer will be executed multiple times. 
        // NRFX_SPIM_FLAG_TX_POSTINC = TX buffer address incremented after transfer. 
        // NRFX_SPIM_FLAG_HOLD_XFER = Set up the transfer but do not start it. 
        err_code = nrfx_spim_xfer(&spim3, &xfer_desc, NRFX_SPIM_FLAG_TX_POSTINC|NRFX_SPIM_FLAG_REPEATED_XFER|NRFX_SPIM_FLAG_HOLD_XFER);
        NFRX_APP_ERROR_CHECK(err_code);
        // TODO: Write new audio data to the relevant sections of the two 30ms swing-buffer(s)
      }
      else
      {
        uiTXD_PTR_OFFSET = ((uint32_t)WriterList1 - uiTXD_PTR) / 2;
        // Start the SPIM3 transmission from the other ArrayList,at a starting offset 
        // relative to the TXD.PTR
        WriterListTransmitting=0;
        // Enable the use of EasyDMA ArrayList
        spim3.p_reg->TXD.LIST = 1;
        // Function for initializing the SPI master driver instance.
        nrfx_spim_xfer_desc_t xfer_desc = NRFX_SPIM_XFER_TX(&WriterList0[uiTXD_PTR_OFFSET], BUFFER_SIZE);
        // Function for starting the SPI data transfer
        // NRFX_SPIM_FLAG_REPEATED_XFER = Flag indicating that the transfer will be executed multiple times. 
        // NRFX_SPIM_FLAG_TX_POSTINC = TX buffer address incremented after transfer. 
        // NRFX_SPIM_FLAG_HOLD_XFER = Set up the transfer but do not start it. 
        err_code = nrfx_spim_xfer(&spim3, &xfer_desc, NRFX_SPIM_FLAG_TX_POSTINC|NRFX_SPIM_FLAG_REPEATED_XFER|NRFX_SPIM_FLAG_HOLD_XFER);
        NFRX_APP_ERROR_CHECK(err_code);
        // TODO: Write new audio data to the relevant sections of the two 30ms swing-buffer(s)
      }
    }
    
    static void TIMERs_init(void)
    {
      // Configure and initialise Timer 0 as a 16-bit 4MHz counter.
      nrfx_err_t err_code = NRFX_SUCCESS;
      nrfx_timer_config_t timer_cfg = NRFX_TIMER_DEFAULT_CONFIG;
      timer_cfg.frequency = NRF_TIMER_FREQ_4MHz;
      timer_cfg.mode      = NRF_TIMER_MODE_TIMER;
      timer_cfg.bit_width = NRF_TIMER_BIT_WIDTH_16;
      err_code = nrfx_timer_init(&m_timer1, &timer_cfg, NULL);
      NFRX_APP_ERROR_CHECK(err_code);
    
      // Allocate the four PPI channels
      err_code = nrfx_ppi_channel_alloc(&ppi_channel0);
      NFRX_APP_ERROR_CHECK(err_code);
      err_code = nrfx_ppi_channel_alloc(&ppi_channel1);
      NFRX_APP_ERROR_CHECK(err_code);
      err_code = nrfx_ppi_channel_alloc(&ppi_channel2);
      NFRX_APP_ERROR_CHECK(err_code);
      err_code = nrfx_ppi_channel_alloc(&ppi_channel3);
      NFRX_APP_ERROR_CHECK(err_code);
    
      // set TIMER1 COMPARE[0] count to be 15 (so it generates an event every time the Timer starts or is reset)
      nrfx_timer_compare(&m_timer1,
                         NRF_TIMER_CC_CHANNEL0,
                         15,
                         false);
    
      // set TIMER1 COMPARE[1] count to be 140 (repeats at 32KHz = 31.25uS)
      nrfx_timer_compare(&m_timer1,
                         NRF_TIMER_CC_CHANNEL1,
                         140,
                         false);
    
      // Set TIMER1 COMPARE[2] count to be 246 (0.5uS before the COMPARE[2] event)
      nrfx_timer_compare(&m_timer1,
                         NRF_TIMER_CC_CHANNEL2,
                         244,
                         false);
    
      uint32_t compare0_timer_event_address = nrfx_timer_event_address_get(&m_timer1, NRF_TIMER_EVENT_COMPARE0);
      uint32_t compare1_timer_event_address = nrfx_timer_event_address_get(&m_timer1, NRF_TIMER_EVENT_COMPARE1);
      uint32_t compare2_timer_event_address = nrfx_timer_event_address_get(&m_timer1, NRF_TIMER_EVENT_COMPARE2);
      uint32_t compare3_timer_event_address = nrfx_timer_event_address_get(&m_timer1, NRF_TIMER_EVENT_COMPARE3);
    
      uint32_t spim_start_task = nrfx_spim_start_task_get(&spim3);
      uint32_t gpiote_clr_task = nrfx_gpiote_clr_task_addr_get(SPI_LDAC_PIN);
      uint32_t gpiote_set_task = nrfx_gpiote_set_task_addr_get(SPI_LDAC_PIN);
    
      // set TIMER1 COMPARE[3] count to be 250 (repeats at 16KHz = 62.5uS)
      // SHORT the COMPARE[3] event and CLEAR task.
      nrfx_timer_extended_compare(&m_timer1,
                                  NRF_TIMER_CC_CHANNEL3,
                                  252, // TODO Find out why this needs to be 252 to get real-world 16KHz
                                  NRF_TIMER_SHORT_COMPARE3_CLEAR_MASK,
                                  false);
    
      // Connect COMPARE[0] event to TASKS_START of SPIM3 to start transmission every time the event occurs
      err_code = nrfx_ppi_channel_assign(ppi_channel0, compare0_timer_event_address, spim_start_task);
      NFRX_APP_ERROR_CHECK(err_code);
    
      // Connect COMPARE[1] event to TASKS_START of SPIM3 to start transmission every time the event occurs
      err_code = nrfx_ppi_channel_assign(ppi_channel1, compare1_timer_event_address, spim_start_task);
      NFRX_APP_ERROR_CHECK(err_code);
    
      // Connect COMPARE[2] event to GPIOTE->TASKS_CLR for the /LDAC signal to make it go LOW
      err_code = nrfx_ppi_channel_assign(ppi_channel2, compare2_timer_event_address, gpiote_clr_task);
      NFRX_APP_ERROR_CHECK(err_code);
    
      // Connect COMPARE[3] event to GPIOTE->TASKS_CLR for the /LDAC signal to make it go HIGH
      err_code = nrfx_ppi_channel_assign(ppi_channel3, compare3_timer_event_address, gpiote_set_task);
      NFRX_APP_ERROR_CHECK(err_code);
    
      // Enable the GPIOTE task events for SPI_LDAC_PIN
      nrfx_gpiote_out_task_enable(SPI_LDAC_PIN);
    
      // Enable the PPI channels
      err_code = nrfx_ppi_channel_enable(ppi_channel0);
      NFRX_APP_ERROR_CHECK(err_code);
      err_code = nrfx_ppi_channel_enable(ppi_channel1);
      NFRX_APP_ERROR_CHECK(err_code);
      err_code = nrfx_ppi_channel_enable(ppi_channel2);
      NFRX_APP_ERROR_CHECK(err_code);
      err_code = nrfx_ppi_channel_enable(ppi_channel3);
      NFRX_APP_ERROR_CHECK(err_code);
    
    
      // Configure TIMER2 in COUNTER mode, 
      // PPI.FORK COMPARE[3] to COUNT input of TIMER2, to cause TIMER2 to increment on each COMPARE[2] event 
      // Set TIMER2 COMPARE[0] count to be 319
      // When the count reaches 320, we have sent 20ms of samples (2 lots of 2 byte tramsissions)
      // Connect TIMER2 COMPARE[0] event to an Interrupt
    
       // Configure and initialise Timer 2 in COUNTER mode
      timer_cfg.frequency = NRF_TIMER_FREQ_4MHz;
      timer_cfg.mode      = NRF_TIMER_MODE_COUNTER;
      timer_cfg.bit_width = NRF_TIMER_BIT_WIDTH_16;
      err_code = nrfx_timer_init(&m_timer2, &timer_cfg, timer2_event_handler);
      NFRX_APP_ERROR_CHECK(err_code);
    
      // set TIMER2 COMPARE[0] count to be 320 (20ms of 2x 16-bit at 16KHz)
      // SHORT the TIMER2 COMPARE[0] event and CLEAR task.
      nrfx_timer_extended_compare(&m_timer2,
                                  NRF_TIMER_CC_CHANNEL0,
                                  319,
                                  NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK,
                                  true);
    
      // Fork Timer 1's COMPARE[3] event to Timer 2's count input task
      uint32_t timer_count_task_addr = nrfx_timer_task_address_get(&m_timer2, NRF_TIMER_TASK_COUNT);
      err_code = nrfx_ppi_channel_fork_assign(ppi_channel3, timer_count_task_addr);
      NFRX_APP_ERROR_CHECK(err_code);
    
      // Clear Timer 2
      nrfx_timer_clear(&m_timer2);
      // Enable Timer 2
      nrfx_timer_enable(&m_timer2);
    
    
      // Clear Timer 1
      nrfx_timer_clear(&m_timer1);
      // Enable Timer 1
      nrfx_timer_enable(&m_timer1);
    }
    
    // Configure the SPIM3, PPI and GPIOTE to enable regular writing to the stereo DAC
    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_1M;
      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);
      NFRX_APP_ERROR_CHECK(err_code);
    
      // Enable the use of EasyDMA ArrayList
      spim3.p_reg->TXD.LIST = 1;
    
      // Function for initializing the SPI master driver instance.
      nrfx_spim_xfer_desc_t xfer_desc = NRFX_SPIM_XFER_TX(WriterList0, BUFFER_SIZE);
    
      // Function for starting the SPI data transfer
      // NRFX_SPIM_FLAG_REPEATED_XFER = Flag indicating that the transfer will be executed multiple times. 
      // NRFX_SPIM_FLAG_TX_POSTINC = TX buffer address incremented after transfer. 
      // NRFX_SPIM_FLAG_HOLD_XFER = Set up the transfer but do not start it. 
      err_code = nrfx_spim_xfer(&spim3, &xfer_desc, NRFX_SPIM_FLAG_TX_POSTINC|NRFX_SPIM_FLAG_REPEATED_XFER|NRFX_SPIM_FLAG_HOLD_XFER);
      NFRX_APP_ERROR_CHECK(err_code);
    }
    
    //********************************************************************
    //
    //       main()
    //
    //  Function description
    //   Application entry point.
    //
    
    void main(void)
    {
      unsigned short i;
      unsigned short s;
      float m_fSin;
      float m_fCos;
      float m_fAngle;
    
      // Read the HWID field of the CONFIGID register in FICR to determine the IC's version
      // number, and therefore apply the correct errata work-arounds.
    
      // Write some initial data to the SPI write buffers.
      // There are 480 pairs of 16-bit values
      for(i=0;i<480;i++)
      {
        m_fAngle = ((float)i) * AngleStep;    // 640 steps gives 2*PI radians
        m_fSin = (sinf(m_fAngle) + 1) * 2047; // Gain gives full 12-bit range
        m_fCos = (cosf(m_fAngle) + 1) * 2047; // Gain gives full 12-bit range
    
        // Channel A (Even addresses)
        s = DAC_A | DAC_GAx1 | DAC_RUN |0xAAA;
        WriterList0[i*2].buffer[0] = s >> 8;   // MSB
        WriterList0[i*2].buffer[1] = s & 0xFF; // LSB
        WriterList1[i*2].buffer[0] = s >> 8;   // MSB
        WriterList1[i*2].buffer[1] = s & 0xFF; // LSB
        // Channel B (Odd Addresses)
        s = DAC_B | DAC_GAx1 | DAC_RUN | 0x555;
        WriterList0[(i*2)+1].buffer[0] = s >> 8;   // MSB
        WriterList0[(i*2)+1].buffer[1] = s & 0xFF; // LSB
        WriterList1[(i*2)+1].buffer[0] = s >> 8;   // MSB
        WriterList1[(i*2)+1].buffer[1] = s & 0xFF; // LSB
    
        /*
        // Channel A (Even addresses)
        s = DAC_A | DAC_GAx1 | DAC_RUN | (unsigned short)lroundf(m_fSin);
        WriterList0[i*2].buffer[0] = s >> 8;   // MSB
        WriterList0[i*2].buffer[1] = s & 0xFF; // LSB
        WriterList1[i*2].buffer[0] = s >> 8;   // MSB
        WriterList1[i*2].buffer[1] = s & 0xFF; // LSB
        // Channel B (Odd Addresses)
        s = DAC_B | DAC_GAx1 | DAC_RUN | (unsigned short)lroundf(m_fCos);
        WriterList0[(i*2)+1].buffer[0] = s >> 8;   // MSB
        WriterList0[(i*2)+1].buffer[1] = s & 0xFF; // LSB
        WriterList1[(i*2)+1].buffer[0] = s >> 8;   // MSB
        WriterList1[(i*2)+1].buffer[1] = s & 0xFF; // LSB
        */
      }
    
      // Configure the IO pins and their directions
      GPIO_init();
      SPIM3_init();
      TIMERs_init();
    
      for (i = 0; i < 100; i++) {
        printf("Hello World %d!\n", i);
      }
      do {
        i++;
      } while (1);
    }
    
    /*************************** End of file ****************************/


    There is a problem though.
    There are two timers. Timer 1 is in Timer-Mode, and through the use of several compare values it controls the timing of the SPIM transmissions.
    The SPIM address auto-increments through the EasyDMA ArrayList.

    Around line 307 in the code, the Timer 1's final compare event is FORKed so it can be used to increment Timer 2 which is in Counter-Mode.
    When Timer 2 has received 320 incrementing events, it is supposed to trigger an interrupt, and call the function "timer2_event_handler".

    However, the interrupt event never calls timer2_event_handler, so the SPIM merrily auto-increments off the end of the allocated EasyDMA ArrayList, and keeps going forever.

    Can you tell me why the interrupt function never gets called?
    Thank you

    Regards,
    Nicholas Lee

  • Dear Nicholas,

     

    My apologies for the inconvenience! Regarding the issue in the driver, I have raised the issue internally.

    The fix should be like this:

    if (!(flags & NRF_DRV_SPI_FLAG_HOLD_XFER))
    {
        while (!nrf_spim_event_check(p_spim, NRF_SPIM_EVENT_END)){}
    }

     

    Nicholas Lee said:

    There is a problem though.
    There are two timers. Timer 1 is in Timer-Mode, and through the use of several compare values it controls the timing of the SPIM transmissions.
    The SPIM address auto-increments through the EasyDMA ArrayList.

    Around line 307 in the code, the Timer 1's final compare event is FORKed so it can be used to increment Timer 2 which is in Counter-Mode.
    When Timer 2 has received 320 incrementing events, it is supposed to trigger an interrupt, and call the function "timer2_event_handler".

    However, the interrupt event never calls timer2_event_handler, so the SPIM merrily auto-increments off the end of the allocated EasyDMA ArrayList, and keeps going forever.

    Can you tell me why the interrupt function never gets called?

     I tested your application, and the interrupt runs fine, but the logic for switching the buffers is not working properly.

    each buffer is 1920 bytes (0x780), but it looks like you want to switch the buffers after 320*4 bytes have been sent?

    Instead of setting up the transfer again, you can update the TX.PTR: spim3.p_reg->TXD.PTR = array[index];

    As you mention in the comments, I would recommend adding some GPIO signals to see how often it toggles.

      

    On a side note:

    The comment "TODO Find out why this needs to be 252 to get real-world 16KHz", I believe this is due to the HFCLK source.

    Try starting the external HFCLK instead of running on the internal 64M RC oscillator:

    NRF_CLOCK->TASKS_HFCLKSTART = 1;

    while (NRF_CLOCK->EVENTS_HFCLKSTARTED == 0);

     

    Kind regards,

    Håkon

Related