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

SAADC Multi Channel High Current

Hello,

I have an application that requires an always running ADC to sample three channels at 4096 Hz. I have successfully adapted my application from the many examples posted by Jørgen, especially this one that uses the RTC to trigger ADC sample acquisition through PPI. I have the low power mode, oversample, and boost disabled, as that only works for a single channel. If I have one channel enabled I am seeing 1.1mA, and if I enable 3 channels I see 2.9mA. I guess the product specification does indicate that sampling at 16ksps should take 1.24mA, but this seems insanely high! By contrast, the STM32L4 can do 10ksps for 0.6uA!

Is there any way to reduce current draw by the ADC while maintaining a constant 4096 Hz sampling rate? Why is Nordic's ADC implementation such high power? For my application I can expect around 1mA constant input, so an ADC draw of 3mA is unacceptable.

I've attached my main.c.

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include "nrf.h"
#include "nrf_gpio.h"
#include "nrf_delay.h"
#include "nrf_twi_mngr.h"
#include "nrf_drv_spi.h"
#include "app_util_platform.h"
#include "nordic_common.h"
#include "app_timer.h"
#include "mem_manager.h"
#include "nrf_drv_clock.h"
#include "nrf_drv_gpiote.h"
#include "nrf_drv_saadc.h"
#include "nrf_drv_rtc.h"
#include "nrf_drv_ppi.h"
#include "nrf_power.h"
#include "nrf_log.h"
#include "nrf_log_ctrl.h"
#include "nrf_log_default_backends.h"
#include "nrf_dfu_utils.h"
#include "nrf_dfu_settings.h"
#include "coap_dfu.h"
#include "coap_api.h"
#include "background_dfu_state.h"
#include "fds.h"

#include "permamote.h"

#include <openthread/message.h>

//#define SAADC_SAMPLE_RATE 488 // every 1/2048th of a second
#define SAADC_SAMPLE_FREQ 4096 // every 1/4096th of a second
#define SAADC_SAMPLES_IN_BUFFER 3
#define SAADC_OVERSAMPLE NRF_SAADC_OVERSAMPLE_DISABLED
#define SAADC_BURST_MODE 0

static const nrf_drv_rtc_t  rtc = NRF_DRV_RTC_INSTANCE(0);
static nrf_saadc_value_t    m_buffer_pool[2][SAADC_SAMPLES_IN_BUFFER];
static nrf_ppi_channel_t    m_ppi_channel;
static uint32_t             m_adc_evt_counter;

static uint16_t sample_count = 0;
static float dcurrent[SAADC_SAMPLE_FREQ];
static float voltage[SAADC_SAMPLE_FREQ];

uint8_t enables[7] = {
   MAX44009_EN,
   ISL29125_EN,
   MS5637_EN,
   SI7021_EN,
   PIR_EN,
   I2C_SDA,
   I2C_SCL
};

static void lfclk_config(void)
{
    ret_code_t err_code = nrf_drv_clock_init();
    APP_ERROR_CHECK(err_code);
    nrf_drv_clock_lfclk_request(NULL);
}

void saadc_handler(nrf_drv_saadc_evt_t const * p_event)
{
    if (p_event->type == NRF_DRV_SAADC_EVT_DONE)
    {
        ret_code_t err_code;
        uint8_t value[SAADC_SAMPLES_IN_BUFFER*2];
        int16_t* buffer = p_event->data.done.p_buffer;

        // set buffers
        err_code = nrf_drv_saadc_buffer_convert(p_event->data.done.p_buffer, SAADC_SAMPLES_IN_BUFFER);
        APP_ERROR_CHECK(err_code);

        printf("%d, %d, %d\n", buffer[0], buffer[1], buffer[2]);
        //printf("%d\n", buffer[0]);

        // print samples on hardware UART and parse data for BLE transmission
        //printf("ADC event number: %d\r\n",(int)m_adc_evt_counter);
        //for (int i = 0; i < SAADC_SAMPLES_IN_BUFFER; i++)
        //{
        //    printf("%d\r\n", p_event->data.done.p_buffer[i]);

        //    adc_value = p_event->data.done.p_buffer[i];
        //    value[i*2] = adc_value;
        //    value[(i*2)+1] = adc_value >> 8;
        //}

        //// Send data over BLE via NUS service. Makes sure not to send more than 20 bytes.
        //if((SAADC_SAMPLES_IN_BUFFER*2) <= 20)
        //{
        //    bytes_to_send = (SAADC_SAMPLES_IN_BUFFER*2);
        //}
        //else
        //{
        //    bytes_to_send = 20;
        //}
        //err_code = ble_nus_string_send(&m_nus, value, bytes_to_send);
        //if (err_code != NRF_ERROR_INVALID_STATE)
        //{
        //    APP_ERROR_CHECK(err_code);
        //}

        m_adc_evt_counter++;
    }
}

static void rtc_handler(nrf_drv_rtc_int_type_t int_type)
{

}



void saadc_sampling_event_init(void) {
    ret_code_t err_code;
    err_code = nrf_drv_ppi_init();
    APP_ERROR_CHECK(err_code);

    nrf_drv_rtc_config_t config = NRF_DRV_RTC_DEFAULT_CONFIG;
    config.reliable = 1;
    err_code = nrf_drv_rtc_init(&rtc, &config, rtc_handler);
    APP_ERROR_CHECK(err_code);

    //Set compare channel 0 to trigger at 4096 Hz
    err_code = nrf_drv_rtc_cc_set(&rtc,0,8,false);
    APP_ERROR_CHECK(err_code);

    //Power on RTC instance
    nrf_drv_rtc_enable(&rtc);

    uint32_t rtc_compare_event_addr = nrf_drv_rtc_event_address_get(&rtc, NRF_RTC_EVENT_COMPARE_0);
    uint32_t rtc_clear_task_addr = nrf_drv_rtc_task_address_get(&rtc, NRF_RTC_TASK_CLEAR);
    uint32_t saadc_sample_event_addr = nrf_drv_saadc_sample_task_get();

    /* setup ppi channel so that timer compare event is triggering sample task in SAADC */
    err_code = nrf_drv_ppi_channel_alloc(&m_ppi_channel);
    APP_ERROR_CHECK(err_code);

    err_code = nrf_drv_ppi_channel_assign(m_ppi_channel, rtc_compare_event_addr, saadc_sample_event_addr);
    APP_ERROR_CHECK(err_code);
    err_code = nrf_drv_ppi_channel_fork_assign(m_ppi_channel, rtc_clear_task_addr);
    APP_ERROR_CHECK(err_code);
}

void saadc_sampling_event_enable(void)
{
    ret_code_t err_code = nrf_drv_ppi_channel_enable(m_ppi_channel);
    APP_ERROR_CHECK(err_code);
}

void saadc_init(void) {

    ret_code_t err_code;

    nrf_drv_saadc_config_t saadc_config = {0};
    saadc_config.low_power_mode = 1;
    saadc_config.resolution = NRF_SAADC_RESOLUTION_12BIT;

    // set up voltage ADC
    nrf_saadc_channel_config_t channel_config0 =
        NRF_DRV_SAADC_DEFAULT_CHANNEL_CONFIG_SE(NRF_SAADC_INPUT_AIN5);

    nrf_saadc_channel_config_t channel_config1 =
        NRF_DRV_SAADC_DEFAULT_CHANNEL_CONFIG_SE(NRF_SAADC_INPUT_AIN6);

    nrf_saadc_channel_config_t channel_config2 =
        NRF_DRV_SAADC_DEFAULT_CHANNEL_CONFIG_SE(NRF_SAADC_INPUT_AIN7);

    err_code = nrf_drv_saadc_init(&saadc_config, saadc_handler);
    APP_ERROR_CHECK(err_code);

    err_code = nrf_drv_saadc_channel_init(0, &channel_config0);
    APP_ERROR_CHECK(err_code);
    err_code = nrf_drv_saadc_channel_init(1, &channel_config1);
    APP_ERROR_CHECK(err_code);
    err_code = nrf_drv_saadc_channel_init(2, &channel_config2);
    APP_ERROR_CHECK(err_code);

    // set up double buffers
    err_code = nrf_drv_saadc_buffer_convert(m_buffer_pool[0], SAADC_SAMPLES_IN_BUFFER);
    err_code = nrf_drv_saadc_buffer_convert(m_buffer_pool[1], SAADC_SAMPLES_IN_BUFFER);

    //nrf_drv_saadc_calibrate_offset();
}


void log_init(void)
{
    ret_code_t err_code = NRF_LOG_INIT(NULL);
    APP_ERROR_CHECK(err_code);

    NRF_LOG_DEFAULT_BACKENDS_INIT();
}

int main(void) {
    nrf_power_dcdcen_set(1);
    lfclk_config();

    nrf_gpio_cfg_output(LED_1);
    nrf_gpio_cfg_output(LED_2);
    nrf_gpio_cfg_output(LED_3);
    nrf_gpio_pin_set(LED_1);
    nrf_gpio_pin_set(LED_2);
    nrf_gpio_pin_set(LED_3);
    for (int i = 0; i < 7; i++) {
      nrf_gpio_cfg_output(enables[i]);
      nrf_gpio_pin_set(enables[i]);
    }

    nrf_gpio_cfg_output(LI2D_CS);
    nrf_gpio_cfg_output(SPI_MISO);
    nrf_gpio_cfg_output(SPI_MOSI);
    nrf_gpio_pin_set(LI2D_CS);
    nrf_gpio_pin_set(SPI_MISO);
    nrf_gpio_pin_set(SPI_MOSI);

    // Init log
    log_init();
    printf("\nADC TEST\n");

    saadc_init();
    saadc_sampling_event_init();
    saadc_sampling_event_enable();

    while (1) {
        if (NRF_LOG_PROCESS() == false)
        {
            __WFI();
        }
    }
}

Parents
  • It's Actually EasyDMA that's causing most of the SAADC's run current of ~1.24mA (~1.1mA is DMA), what you're seeing above that is most likely increased CPU activity. 
     
    With '#define SAADC_SAMPLES_IN_BUFFER 3', and three channels enabled you will only get one sample per channel before the END event is fired, where you process the data and the driver initializes the buffer again. This causes a significant overhead at 16kHz, and there's zero chance the STM32L4 is anywhere near it's ADC's run current in that scenario. With a similar µA/Hz CPU, that overhead is just as big for that device, and they don't seem to specify the run current of their DMA controller. 

    You need to increase the buffer size as much as you can in order to reduce the CPU overhead. What latency requirements do you have for processing your samples?

  • Hi haakonsh, thanks for the answer!

    If I'm understanding this correctly, DMA and interrupts waking up the core are the primary contributors to current draw. DMA will be required regardless of buffer size and the number of samples the ADC collects before generating an interrupt. You're right that reducing the number of times the core is woken up will significantly reduce current draw. This is the next thing I want to try.

    For my application, I want to run the ADC at 4096 samples per second, and I'm fine with processing these samples every second. I am fine using double buffers of size 4096. I am unsure how to get the ADC to sample at this rate, however. I see that it has an internal clock that can drive a continuous sample capture, but this functionality is not exposed by the SAADC driver. Can you point me to the right resources to achieve this?

  • So I think I figured it out. I returned to my PPI implementation, and increased the buffer size to 3 (channels) * 4096 (samples). I kept my RTC configuration the same because I still want to sample at 4096 samples per second. I know get an interrupt every second with a full buffer of data that looks correct. I'm measuring 1.8mA still. Is this the lower bound I can expect for 3 channels @ 4096Hz?

  • The SAADC operation is specced at ~1.24mA in SAADC active (at 16kHz, at 3V, with DCDC), so you should expect around that for 4kHz as well. 

    I think the last ~500-600µA is CPU activity. 

    A current consumption scope is actually very useful in this scenario, as it should tell you how much time the CPU is active during this period. 

    Your RTC sampling timer is right on the money btw.

  • So there is no way to disable the ADC and DMA between samples without processor intervention to get lower power? How would you architect ADC sampling to achieve the lowest current if sampling at 4kHz?

    As far as I can tell, my processor is sleeping almost the entire second except for when the ADC generates an interrupt indicating sampling is finished, so I'm not quite sure I buy that 500-600uA is from CPU. I can create a time-series measurement of current in the next few days to verify this.

  • nealjack said:
    So there is no way to disable the ADC and DMA between samples without processor intervention to get lower power? How would you architect ADC sampling to achieve the lowest current if sampling at 4kHz?

    If you do not need to have the BLE stack running at the same time as you're sampling you can sample the SAADC "manually" by enabling and disabling the SAADC between each sampling event and only take one sample. This will remove the base ~1.1mA EasyDMA current, at the cost of having to wake up the CPU at precise timings intervals. It might reduce the current consumption by a fair bit. 

    How do you process the samples btw? 

Reply
  • nealjack said:
    So there is no way to disable the ADC and DMA between samples without processor intervention to get lower power? How would you architect ADC sampling to achieve the lowest current if sampling at 4kHz?

    If you do not need to have the BLE stack running at the same time as you're sampling you can sample the SAADC "manually" by enabling and disabling the SAADC between each sampling event and only take one sample. This will remove the base ~1.1mA EasyDMA current, at the cost of having to wake up the CPU at precise timings intervals. It might reduce the current consumption by a fair bit. 

    How do you process the samples btw? 

Children
No Data
Related