Beware that this post is related to an SDK in maintenance mode
More Info: Consider nRF Connect SDK for new designs
This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

ADC - how to consume fast samples while avoiding race conditions?

Hi everyone,

I have a fairly simple use for ADC: sample 4 channels (scan mode) every 10ms and store in RAM.

Every now and then (asynchronously) there will be a function or a task (not in ISR context) that will want to read the latest values of the 4 channels.

I've been struggling to implement that without a potential for getting a race condition (where the task might read the values as they are written), and I wanted to ask for your advice on that.

My (simplified) code for configuring the ADC is as follows:

#define NUM_SAADC_CHANNELS 4
#define ADC_NUM_SAMPLES_IN_BUFFER NUM_SAADC_CHANNELS
static nrf_saadc_value_t m_buffer_pool[2][ADC_NUM_SAMPLES_IN_BUFFER];
static volatile uint32_t m_buffer_index = 0;  // current buffer idx the DMA will be using to copy the ADC samples to (alternates between 0 and 1)

static void saadc_callback(nrfx_saadc_evt_t const * p_event)
{
    if (p_event->type == NRFX_SAADC_EVT_DONE)
    {
        // p_buffer is a pointer to the buffer with converted samples.
        /* set buffers for the next ADC event */
        APP_ERROR_CHECK(nrfx_saadc_buffer_convert(p_event->data.done.p_buffer, ADC_NUM_SAMPLES_IN_BUFFER));
        m_buffer_index ^= 1; // 0 becomes 1, 1 becomes 0
    }
}

ret_code_t adc_init(void)
{
    ret_code_t err_code = NRF_SUCCESS;

    // Initialize SAADC
    nrfx_saadc_init(...);

    // Initialize ADC channels
    for (int i=0; i<NUM_SAADC_CHANNELS; i++) {
        nrf_saadc_channel_config_t channel_config = NRFX_SAADC_DEFAULT_CHANNEL_CONFIG_SE(...);
        err_code = nrfx_saadc_channel_init(...); 
        APP_ERROR_CHECK(err_code);
    }

    // Configure buffers to ensure double buffering of samples, to avoid data loss when the sampling frequency is high
    err_code = nrfx_saadc_buffer_convert(m_buffer_pool[0],ADC_NUM_SAMPLES_IN_BUFFER);
    APP_ERROR_CHECK(err_code);   
    err_code = nrfx_saadc_buffer_convert(m_buffer_pool[1],ADC_NUM_SAMPLES_IN_BUFFER);
    APP_ERROR_CHECK(err_code);

    // Initialize PPI and Timer, as in the ADC example
    // ...

    return err_code;
}

In a nutshell it's identical to the example ADC code, where we have a double buffer, and PPI+Timer to sample every 10ms.

As for the asynchronous function run from a task, it will occasionally need to read the latest values stored by the ADC. I was thinking about this code:

void adc_get_latest_values(int16_t* p_array)
{
    // grab the values from the pool that is not currently being updated
    memcpy(p_array, &m_buffer_pool[m_buffer_index^1], ADC_NUM_SAMPLES_IN_BUFFER*sizeof(m_buffer_pool[0][0]));
}

My idea is that the ADC will update the buffer index m_buffer_index (0 or 1) and the task will read from m_buffer_index^1 (1 or 0).

My concern here is that there is a potential for a race condition, where m_buffer_index would get updated by the ISR while the function above is being run.

Your advice would be greatly appreciated!

Related