SAADC feature request: RESULT.PTR auto increment for lowest power

SAADC feature request: RESULT.PTR auto increment


Right now, continuous sampling works well for a high sampling rate (up to about 200ksps), keeping DMA etc active to cope with the high rate, so it uses a lot of power. The low power option in the driver is great for tens of samples per second but relies on interrupts for each sample to setup RESULT.PTR for the next sample. The middle of the range needs some improvement, eg 1ksps-16ksps.

I'm trying to sample the ADC at a medium rate (eg 4ksps) but would like to keep DMA etc off between samples to reduce power consumption AND not rely on interrupts for each sample to set RESULT.PTR. The current low power option requires interrupts per sample just to set RESULT.PTR for each sample.

It would be very helpful if there was a mode where the buffer write position pointed to by RESULT.PTR could auto increment even after an end event is generated. Currently, the write position relative to RESULT.PTR is auto incremented after each sample task, when MAXCNT > 1 but DMA is kept active until you reach MAXCNT. So low power options use MAXCNT=1 so that an end event is generated for each sample which then releases DMA between samples. But after the end event, the write position goes back to RESULT.PTR for the next sample task and you need a software interrupt to update RESULT.PTR for each sample. If the write position just kept incrementing even after the end event, until something in software tells it to change, then we could achieve much lower power using 1 interrupt per buffer, not 1 per sample:

Its possible to get very close now : Set MAXCNT=1 and use PPI and an RTC to trigger a start task on saadc, then trigger a sample task (after getting a started event on a PPI channel). A counter is incremented when it gets the end event from saadc after every sample (since MAXCNT=1) and only sends an interrupt when the counter reaches enough samples that would fill the buffer. After the end event is fired for each sample, DMA etc is let go until the next RTC event which starts and samples again. So you get 1 interrupt per buffer and not 1 per sample, and very low power between samples. But since the write position goes back to RESULT.PTR after the end event, this approach just keeps overwriting the previous sample and does not fill the buffer. You need to route the end event to the CPU to set RESULT.PTR but I would want to avoid that interrupt per sample.

Its easy to see what could be achieved. I've implemented the rtc/counter approach (ie data is overwritten) and also using the interrupt per sample approach (data is not overwritten), and here are the numbers. If you run sampling continuously at the sample rate shown, these are the ppk numbers:


Samples per second  1024  2048  4096  8192
Auto Increment(uA)    65   100   160   285
Interrupts(uA)       190   350   750  1360


Besides the power benefits, the other benefit is knowing you have a precise sample rate, mostly to within your crystals rating, eg 20ppm. In some applications, it's necessary to know the sampling rate very precisely when analysing the data.

Here's some sample code using the RTC+Counter approach:

#define SAMPLES   128
nrf_saadc_value_t buffer[SAMPLES];
const nrfx_timer_t counter = NRFX_TIMER_INSTANCE(3);


void Start()
{
    // Initialize and setup Saadc
    SaadcInit();

    // Rtc
    NRF_RTC2->PRESCALER = 15 // 2048 sps
    NRF_RTC2->EVTEN     = RTC_EVTENCLR_TICK_Enabled; 

    // Counter
    nrfx_timer_config_t counterConfig = NRFX_TIMER_DEFAULT_CONFIG; 
    counterConfig.mode                = NRF_TIMER_MODE_LOW_POWER_COUNTER;
    counterConfig.bit_width           = NRF_TIMER_BIT_WIDTH_32;
    nrfx_timer_init(&counter, &counterConfig, CounterHandler);
	nrfx_timer_extended_compare(
	    &counter, NRF_TIMER_CC_CHANNEL1, 
	    SAMPLES,     // wait for this number of saadc end events before we interrupt
	    NRF_TIMER_SHORT_COMPARE1_CLEAR_MASK, true);

    // Rtc-Saadc channel : start saadc using the RTC tick event
    nrfx_ppi_channel_alloc(&ppiChannelCounterToSaadc);
    nrfx_ppi_channel_assign(ppiChannelCounterToSaadc, 
        (uint32_t)&NRF_RTC2->EVENTS_TICK,
        nrf_saadc_task_address_get(NRF_SAADC_TASK_START));
	nrfx_ppi_channel_enable(ppiChannelCounterToSaadc);

    // Saadc-Saadc channel : sample saadc once its actually started
    nrfx_ppi_channel_alloc(&ppiChannelSaadcToSaadc);
    nrfx_ppi_channel_assign(ppiChannelSaadcToSaadc, 
        nrf_saadc_event_address_get(NRF_SAADC_EVENT_STARTED),
        nrf_saadc_task_address_get(NRF_SAADC_TASK_SAMPLE));
	nrfx_ppi_channel_enable(ppiChannelSaadcToSaadc);

    // Saadc-Counter channel : count the number of end events
    nrfx_ppi_channel_alloc(&ppiChannelSaadcToCounter);
    nrfx_ppi_channel_assign(ppiChannelSaadcToCounter, 
        nrf_saadc_event_address_get(NRF_SAADC_EVENT_END),
        nrfx_timer_task_address_get(&counter, NRF_TIMER_TASK_COUNT));
	nrfx_ppi_channel_enable(ppiChannelSaadcToCounter);

    // buffer, use MAXCNT=1 so we get an end event for each sample and release DMA between samples
    nrf_saadc_buffer_init(buffer, 1);

    // Start
    NRF_RTC2->TASKS_START = 1;

}

void CounterHandler(nrf_timer_event_t event_type, void* p_context) 
{ 
    // Get here only when we've sampled enough times to fill the buffer. 
    // ie once per buffer not once per sample
    
    // But since noone (software or hardware) has updated RESULT.PTR between samples
    // all samples were written to buffer[0] and the rest of the buffer is empty
    
    // With an auto increment across end events, we could set RESULT.PTR only now in software to another buffer
}
Related