Reading samples from external ADC using SPIM with EasyDMA arrayList

Hi,

I am trying to read samples from an external ADC at 100kHz sampling rate using the SPIM peripheral. I want to take a window of 2000 samples then can pause for processing. It is important that the sampling rate is always 100kHz and that I don't miss any samples.

The ADC samples are 2 bytes and are read after toggling the ADC's conversion start pin (CNVST) low. I am switching the CNVST pin low every 10us using PPI, Timer and GPIOTE. This works reliably and I am able to get samples periodically by reading the data in the SPI buffer in the SPI interrupt handler like below. After I reach 2000 samples (ADC_QUE_SIZE) I stop and process the data.

void spi_event_handler(nrf_drv_spi_evt_t const * p_event,
                       void *                    p_context)
{
    if(count==ADC_QUE_SIZE)
    {
      spi_sampling_event_disable();
      spi_xfer_done = true;
    }
    else
    {
      adc[count++] = ((uint16_t)(m_rx_buf[0]<<8) | m_rx_buf[1]);
    }
}

The method above requires CPU intervention in the interrupt handler, the problem is when I have an active Bluetooth connection this interrupt handler can get delayed and I miss samples by the time the SPI reads can resume.

I am trying to implement a sampling method using an array list as discussed in this this post. My idea is to have 2000* 2-byte buffers that will be sequentially filled without requiring the CPU but I am not getting any data. It is my understanding that that the max SPI buffer is 155 bytes, but can I have more than this number of buffers in the arrayList for EasyDMA?

Here is my code trying to do this, adapted from this answer

#define ADC_QUE_SIZE    2000

typedef struct ArrayList
{  
    uint8_t rx_buffer[2];
}ArrayList_type;

static ArrayList_type p_rx_buffer[ADC_QUE_SIZE];

void spi_config(void)
{
    NRF_SPIM0->RXD.MAXCNT = 2;
    NRF_SPIM0->RXD.PTR = (int32_t) &p_rx_buffer;
    NRF_SPIM0->RXD.LIST = SPIM_RXD_LIST_LIST_ArrayList;

    nrf_drv_spi_config_t spi_config = NRF_DRV_SPI_DEFAULT_CONFIG;
    spi_config.mode     = NRF_DRV_SPI_MODE_1;
    spi_config.frequency = NRF_DRV_SPI_FREQ_8M;
    
    spi_config.ss_pin   = NRF_DRV_SPI_PIN_NOT_USED;
    spi_config.miso_pin = SPI_MISO_PIN;
    spi_config.mosi_pin = NRF_DRV_SPI_PIN_NOT_USED;
    spi_config.sck_pin  = SPI_SCK_PIN;
    APP_ERROR_CHECK(nrf_drv_spi_init(&spi, &spi_config, NULL, NULL)); 
      
    memset(p_rx_buffer, 0, sizeof(p_rx_buffer));

    nrf_drv_spi_xfer_desc_t xfer = NRF_DRV_SPI_XFER_TRX(m_tx_buf, m_length, (uint8_t*)p_rx_buffer, 2);
    
    uint32_t flags = NRF_DRV_SPI_FLAG_HOLD_XFER    |
                     NRF_DRV_SPI_FLAG_REPEATED_XFER|
                     NRF_DRV_SPI_FLAG_RX_POSTINC   |
                     NRF_DRV_SPI_FLAG_NO_XFER_EVT_HANDLER;
    
    ret_code_t ret = nrf_drv_spi_xfer(&spi, &xfer, flags); 
}

void timer_count_handler(nrf_timer_event_t event_type, void * p_context)
{
    switch (event_type)
    {
        case NRF_TIMER_EVENT_COMPARE0:
            spi_sampling_event_disable();
            spi_xfer_done = true;
            break;

        default:
            //Do nothing.
            break;
    }
}

void spi_sampling_event_init(void)
{
    ret_code_t err_code;
    nrf_drv_gpiote_out_config_t config = GPIOTE_CONFIG_OUT_TASK_TOGGLE(true);  
    err_code = nrf_drv_gpiote_out_init(GPIO_OUTPUT_PIN_NUMBER, &config);
    
    // Fs = 100kHz: cc_value = 159. With interrupt disabled (false)
    nrf_drv_timer_extended_compare(&timer_adc, NRF_TIMER_CC_CHANNEL0, 159, NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, false);

    // Compare event after 2000 samples with interrupt enabled (true)
    nrf_drv_timer_extended_compare(&timer_count, NRF_TIMER_CC_CHANNEL0, 2000, NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, true);

    uint32_t gpiote_task_addr = nrf_drv_gpiote_out_task_addr_get(GPIO_OUTPUT_PIN_NUMBER);
        
    uint32_t spi_start_task_addr   = nrf_drv_spi_start_task_get(&spi);
    uint32_t spi_end_evt_addr = nrf_drv_spi_end_event_get(&spi);

    uint32_t timer_compare_event_addr = nrf_drv_timer_compare_event_address_get(&timer_adc, NRF_TIMER_CC_CHANNEL0);

    uint32_t timer_count_task = nrf_drv_timer_task_address_get(&timer_count, NRF_TIMER_TASK_COUNT);
    uint32_t timer_cc_event = nrf_drv_timer_event_address_get(&timer_count,  NRF_TIMER_EVENT_COMPARE0);

    err_code = nrf_drv_ppi_channel_alloc(&ppi_channel_1);
    err_code = nrf_drv_ppi_channel_assign(ppi_channel_1, timer_compare_event_addr, gpiote_task_addr);
    err_code = nrf_drv_ppi_channel_fork_assign(ppi_channel_1, spi_start_task_addr);	
    																			 																		 
    err_code = nrf_drv_ppi_channel_alloc(&ppi_channel_2);
    err_code = nrf_drv_ppi_channel_assign(ppi_channel_2, spi_end_evt_addr, gpiote_task_addr);
    err_code = nrf_drv_ppi_channel_fork_assign(ppi_channel_2, timer_count_task);
}

void spi_sampling_event_enable(void)
{
    ret_code_t err_code;
    nrf_drv_gpiote_out_task_enable(GPIO_OUTPUT_PIN_NUMBER);
    err_code = nrf_drv_ppi_channel_enable(ppi_channel_1);
    err_code = nrf_drv_ppi_channel_enable(ppi_channel_2);
    nrf_drv_timer_enable(&timer_adc);
    nrf_drv_timer_enable(&timer_count);

    // Configure short between spi end event and spi start task
    nrf_spim_shorts_enable(spi.u.spim.p_reg, NRF_SPIM_SHORT_END_START_MASK);
    nrf_spim_task_trigger(spi.u.spim.p_reg, NRF_SPIM_TASK_START);
}

void spi_sampling_event_disable(void)
{
    ret_code_t err_code;
    nrf_drv_gpiote_out_task_disable(GPIO_OUTPUT_PIN_NUMBER);
    err_code = nrf_drv_ppi_channel_disable(ppi_channel_1);
    err_code = nrf_drv_ppi_channel_disable(ppi_channel_2);
    nrf_drv_timer_disable(&timer_adc);
    nrf_drv_timer_disable(&timer_count);

    // Configure short between spi end event and spi start task
    nrf_spim_shorts_disable(spi.u.spim.p_reg, NRF_SPIM_SHORT_END_START_MASK);
}

The data in rx_buffer is never written to and stays 0, from the memset to 0 in the spi_config function.

It would be great if someone could point me toward some similar examples as I find the documentation on EasyDMA array lists quite cryptic.

Many thanks,

Daragh

Specs:

nRF52832QFAA on custom PCB interfacing with MAX11198 ADC (16bit)

Segger Embedded Studio v6.30 with nRF5_SDK_17.1.0

  • Hi Daragh

    I made a prototype example and shared it here:
    https://github.com/too1/ncs-spi-arraylist-example

    It uses arraylist to trigger SPI transactions automatically based on a timer, and uses a second timer module to count the number of transactions. 

    I didn't have as much time as I'd like to work on the example, but I will share what I have for now if you want to have a look at it:
    https://github.com/too1/ncs-spi-arraylist-example

    I will be out in vacation next week, but will be back over Easter if you have comments or questions. 

    Best regards
    Torbjørn

  • Hi Torbjørn,

    Thanks for the example code, I have just gotten back to this now. I was able to implement a very similar method based on your example and it is nearly working. Unfortunately I'm still missing some samples between the end of the buffer and the start of the next one. The application requires very precise coherent sampling so missing data for any time will cause issues.

    I didn't try to add some margin as I think the issue is in the extra time it takes to start sampling again rather than extra samples being taken at the end, I might be wrong and will try this if you have no other suggestions. I am manually setting the EasyDMA RXD pointer back to the start of the buffer at the end of the 2nd half in the timer counter interrupt handler like this:

    void timer_count_handler(nrf_timer_event_t event_type, void * p_context)
    {
        //ret_code_t err_code;
        //spi_sampling_disable();
        spi_xfer_done = true;
    
        switch (event_type)
        {
            case NRF_TIMER_EVENT_COMPARE0:
                // halfway through buffer (1 window sampled)
                current_sample_num = 0;
                buffer_half = 1;
                //printf("NRF_TIMER_EVENT_COMPARE0\n");
                break;
    
            case NRF_TIMER_EVENT_COMPARE1:
                // at end of buffer (2 windows sampled)
                m_spi.u.spim.p_reg->RXD.PTR = (uint32_t)rx_buf_array;
                m_spi.u.spim.p_reg->TXD.PTR = (uint32_t)m_tx_buf;
                nrf_drv_timer_clear(&timer_counter);
                current_sample_num = NUM_SAMPLES;
                buffer_half = 2;
                //printf("NRF_TIMER_EVENT_COMPARE1\n");
                break;
            //TODO check if this interrupt is getting delayed and missing samples
            default:
                // Do nothing
                break;
        }
    }

    I'm curious to know if there is a way to do this using PPI or similar so that interrupt won't be required for resetting the buffer address.

    For an example here is a plot of the ADC values when sampling a single tone sine wave. I save the last 100 samples from the buffer and then the first 100 samples from the next buffer. You can see a slight discontinuity where the first buffer ends and the next buffer starts (in reality there is a single buffer that is being written over).

    Thanks,

    Daragh

  • Hi Daragh

    As I remember it from working on the example some months ago the issue is that the arraylist feature won't work with double buffering in the same way that other peripherals do. As an example, if you were to run several long SPI transactions not using array list you would have the entirety of the first transaction to prepare the buffers for the second transaction. 

    The way arraylist works by simply updating the TXD.PTR and RXD.PTR automatically you lose this capability.  

    D_Crow said:
    I didn't try to add some margin as I think the issue is in the extra time it takes to start sampling again

    Why would it take extra time to start sampling again? Isn't the sampling being run over PPI automatically?

    I think we should start by verifying whether or not buffer overrun is the issue. 

    Testing this is quite simple. Just add some margin to your buffer, initialize the entire buffer to some fixed value that is unlikely to show up in your data (0xFFFF or 0 is probably sufficient), and run the code until the issue occurs. Stop the execution in the debugger and see how much of the buffer has been overwritten with sensor data, whether it is just til the end (not including margin), or whether any of the margin elements have been overwritten as well. 

    Best regards
    Torbjørn

  • I tried this and you're completely right, that interrupt handler is delayed by up to 10 samples (usually only 1 or 2 samples) at the end of the buffer (10 samples is 0.1ms). So that explains my missing data. I'll be able to add a check for this but will have to figure out how to handle it efficiently as it will quickly add up over time.

    Thanks again for the help, I'll mark your reply with the prototype code as the answer to this. It would be great to see double buffering with array lists in the future.

  • Hi Daragh

    I am glad I could help out Slight smile

    I am not sure what the best way is to process the samples. Either you have to process the samples in unevenly sized chunks (the half point chunk would be the right size, while the second half would be longer because of the overflow), or you would have to schedule the next interrupt earlier to compensate for the added samples at the end. Essentially save the 'margin' samples for the next processing cycle, and set up the next interrupt to occur earlier. 

    Best regards
    Torbjørn

Related