This discussion has been locked.
You can no longer post new replies to this discussion. If you have a question you can start a new discussion

nRF52840 SDK16 - SAADC + RTC + PPI (power optimized solution)

Hi everyone,

There is an open ticket that I need support with because the responsible engineer will be out of the office until the new year.

In a nutshell:

I design a low-power application and I want to sample 8 pressure sensors that are connected to a MUX. I want to sample with a rate of 100Hz the group of eight sensors. That means every 10ms I need to sample all of the 8 sensors as fast as possible (given of course the hardware delays such as the MUX's switching speed and ADC conversion and acquisition time). So far, the optimized approach is to use:

RTC because uses the low-frequency clock

PPI because sampling does not require CPU intervention. However, PPI channels require that the HFCLK is always kept running (so I still thinking about it)

My purpose to go through the MUX's truth table using PPI as well (but in a future step)

Now let's say that I set an RTC CC to 10ms to trigger the SAMPLE task using the COMPARE event. Then, I have to keep sapling until the 8th sensor. I came across with two possible solutions using PPI:

1. To set up a second RTC CC let's say to 50us, to trigger the sample task using the compare event until to sample all the sensors

2. To enable the DONE interrupt, to set up a second PPI channel and trigger the SAMPLE task with the DONE event (I tried to do this but still it hasn't work) until the 8th sensor

Here Karl says that I can achieve the same functionality with just one PPI channel. I do not understand how can I do it with one channel or I didn't explain correctly to Karl what I want to achieve.

Given the aforementioned, what is the most power-optimized approach?

Thanks

Nick

Parents
  • Hi,

    The suggestion you already got makes sense I would say. Though I would simplify even further and simply us an extra repeated app_timer, which gives you all you need out of the box. This way simply do the SAADC handling from SW, and use a normal system on low power sleep in between.

    The down-side of this approach is that as you do this in SW, a higher priority interrupt would delay your sampling. As you sample every 10 ms (not very often) that is perhaps not a big problem? Also, depending on interrupt priorities and durations of the high priority interrupts you have in the rest of your code, the actual worst case delay will probably be very short compared to the 10 ms interval.

  • Hi Einar,

    So you suggest not using the PPI but just application timers because my sampling frequency is low? This will keep down the power consumption since I won't use PPI I guess?

  • Yes, it will. Not on its own, though. You also need to disable the SAADC (nrf_saadc_int_disable()) when done and re-enable it before using it in order to obtain low power consumption (if not, the SAADC will always be enabled and so the high frequency clock will also always run).

  • Thank Einar,

    So I need two app timers? The first one will expire every 10ms to start sampling with the first sensor and then I need the second app timer to keep sampling the rest 7 sensors right (probably 80-100us it would be fine)? But now I am thinking that 80-100us is fast enough and could be a problem with the higher priority interrupts.

    The requirement is to start sampling every 10ms BUT the sampling between the sensors (8 in total) to be done as fast as possible.

  • No, you should not need two app timers. Just a single one where you do the following:

    1. Enable the SAADC
    2. Sample all sensors back to back
    3. Disable SAADC

    Note that the above would take a bit of time to complete, so you should either ensure that the interrupt priority of the app_timer is lower than other interrupts (by setting APP_TIMER_CONFIG_IRQ_PRIORITY in sdk_config.h accordingly), or defer the actual processing to the main loop (either manually or using the app_scheduler).

  • No, you should not need two app timers. Just a single one where you do the following:

    1. Enable the SAADC
    2. Sample all sensors back to back
    3. Disable SAADC

    Something like that (I used RTC for now instead of app timer)?

    Definitions

    #define RTC_FREQUENCY 32                               // Determines the RTC frequency and prescaler
    #define SAADC_SAMPLE_INTERVAL_MS 1000                  // Interval in milliseconds at which RTC times out and triggers SAADC sample task (
    #define SAADC_SAMPLES_IN_BUFFER 1                      // Number of SAADC samples in RAM before returning a SAADC event. For low power SAADC set this constant to 1. Otherwise the EasyDMA will be enabled for an extended time which consumes high current.
    #define SAADC_OVERSAMPLE NRF_SAADC_OVERSAMPLE_DISABLED // Oversampling setting for the SAADC. Setting oversample to 4x This will make the SAADC output a single averaged value when the SAMPLE task is triggered 4 times. Enable BURST mode to make the SAADC sample 4 times when triggering SAMPLE task once.
    #define SAADC_BURST_MODE 0                             // Set to 1 to enable BURST mode, otherwise set to 0.
    #define SENSORS 8
    
    const nrf_drv_rtc_t rtc = NRF_DRV_RTC_INSTANCE(2); /**< Declaring an instance of nrf_drv_rtc for RTC2. */
    static uint32_t rtc_ticks = RTC_US_TO_TICKS(SAADC_SAMPLE_INTERVAL_MS * 1000, RTC_FREQUENCY);
    static nrf_saadc_value_t m_buffer_pool[2][SAADC_SAMPLES_IN_BUFFER];
    static uint32_t m_adc_evt_counter = 0;

    RTC handler

    static void rtc_handler(nrf_drv_rtc_int_type_t int_type) {
      uint32_t err_code;
    
      if (int_type == NRF_DRV_RTC_INT_COMPARE0) {
        nrf_drv_saadc_sample(); // Trigger the SAADC SAMPLE task
    
        err_code = nrf_drv_rtc_cc_set(&rtc, 0, rtc_ticks, true); // Set RTC compare value and enable the interrupt
        APP_ERROR_CHECK(err_code);
        m_adc_evt_counter = 0;
        nrf_drv_rtc_counter_clear(&rtc); // Clear the RTC counter to start count from zero
      }
    }

    ADC handler

    void saadc_callback(nrf_drv_saadc_evt_t const *p_event) {
      ret_code_t err_code;
      if (p_event->type == NRF_DRV_SAADC_EVT_DONE) // Capture offset calibration complete event
      {
        err_code = nrf_drv_saadc_buffer_convert(p_event->data.done.p_buffer, SAADC_SAMPLES_IN_BUFFER); // Set buffer so the SAADC can write to it again.
        APP_ERROR_CHECK(err_code);
    
        NRF_LOG_INFO("ADC event number: %d\r\n", (int)m_adc_evt_counter); // Print the event number on UART
    
        for (int i = 0; i < p_event->data.done.size; i++) {
          NRF_LOG_INFO("SAADC result: %d\r\n", p_event->data.done.p_buffer[i]); // Print the SAADC result on UART
        }
    
        m_adc_evt_counter++;
        if ((m_adc_evt_counter % SENSORS) != 0) { // Sample all the sensors
    
          nrf_drv_saadc_sample(); // Trigger the SAADC SAMPLE task
          //add code to switch the MUX
        }
      }
    }

    The RTC IRQ priority is set to 6. Should it be 7? I do not have experience with differing processing. Is this something essential that I should include?

  • Hi,

    Nikosant03 said:
    Something like that (I used RTC for now instead of app timer)?

    Something like that, but not exactly like here.

    I really recommend you use the app_timer instead, as that implements some features for you which you need. And prevents you from ending up in a few pitfalls, which you have done here. Looking at your RTC handler, you first call nrf_drv_saadc_sample(), and after that has completed you configure the RTC for the next compare event. But a bit of time has elapsed here, so you will have more ticks than you had planned in between here.

    Nikosant03 said:
    The RTC IRQ priority is set to 6. Should it be 7?

    The interrupt priorities does not matter much in itself, but matters relative to others in the system, so this is application specific. How important is this timing compared to other events in the system, and can other events handle being blocked for too long? If you can live with a bit of jitter in the sampling interval, then using a low priority (like 7) makes sense, or perhaps even moving it to the main loop (no interrupt at all).

Reply
  • Hi,

    Nikosant03 said:
    Something like that (I used RTC for now instead of app timer)?

    Something like that, but not exactly like here.

    I really recommend you use the app_timer instead, as that implements some features for you which you need. And prevents you from ending up in a few pitfalls, which you have done here. Looking at your RTC handler, you first call nrf_drv_saadc_sample(), and after that has completed you configure the RTC for the next compare event. But a bit of time has elapsed here, so you will have more ticks than you had planned in between here.

    Nikosant03 said:
    The RTC IRQ priority is set to 6. Should it be 7?

    The interrupt priorities does not matter much in itself, but matters relative to others in the system, so this is application specific. How important is this timing compared to other events in the system, and can other events handle being blocked for too long? If you can live with a bit of jitter in the sampling interval, then using a low priority (like 7) makes sense, or perhaps even moving it to the main loop (no interrupt at all).

Children
  • Hi Einar, sorry for the late reply.


    Well, it seems that the best approach is using the app timer, however I learned a lot of things by experimenting with alternative solutions. In my case, the sampling interval is not super critical, but the power consumption is (and the PPI increases the power consumption significantly).


    Now my task is to read the IMU (I2C protocol) and 8 analog pressure sensors (connected on a MUX) with a frequency of 100Hz and send the payload through BLE. I want to start sampling when the user enables the notifications


    So I start an app timer when the notifications are enabled

    static void on_cus_evt(ble_cus_t *p_cus_service,
      ble_cus_evt_t *p_evt, int gatt_handler) {
      ret_code_t err_code;
    
      if (gatt_handler == CCCD_HANDLE) {
        switch (p_evt->evt_type) {
        case BLE_CUS_EVT_NOTIFICATION_ENABLED:
          NRF_LOG_INFO("Notification Enabled");
          notification = ENABLE;
    
          err_code = app_timer_start(m_notification_timer_id, samplingRate, NULL);
          APP_ERROR_CHECK(err_code);
          
          
          ........
          ....
          ..
          }

    When the timer expires (every 10ms), it calls the read_sensor() where I want initially read the IMU, then the pressure sensors, and then transmit the data.

    void read_sensors(void *p_context) {
    
      ret_code_t err_code;
      UNUSED_PARAMETER(p_context);
    
      // Read the IMU
      IMU_read(&sampling);
    
      // Read the pressure sensors
      for (uint8_t i = 0; i < NO_SENSORS - 1; i++) {
        pressure_sensors_read(&sampling);
      }
    
      sampling.index = 0;
    
      // Increment the packet counter
      packet_increment(&sampling);
    
      logger(&sampling);
    
      // Buffer that payload
      my_sensors_buffer(&sampling);
    
      // if the buffer is not empty Tx data
      if (!nrf_queue_is_empty(&m_sensor_data_queue)) {
        data_tx(&sampling);
      }
    }

    However when I run the for loop to read all the sensors, I read just the first sensor and not the other seven

    To read the pressure sensors I used an additional app timer and I set the SAADC_INERVAL to 50us just to read the sensors fast

    void pressure_sensors_read(struct s_sampling *sampling) {
      ret_code_t err_code;
      err_code = app_timer_start(m_SAADC_timer_id, SAADC_INTERVAL, NULL);
      APP_ERROR_CHECK(err_code);
    }
    
    void saadc_sample() {
      ret_code_t err_code;
    
      err_code = app_timer_stop(m_SAADC_timer_id);
      APP_ERROR_CHECK(err_code);
    
      err_code = nrf_drv_saadc_sample(); // na - trigger sampling
      APP_ERROR_CHECK(err_code);
    }

    By the way this is my saadc callback function

    void saadc_callback(nrf_drv_saadc_evt_t const *p_event) {
    
      ret_code_t err_code;
    
      if (p_event->type == NRF_DRV_SAADC_EVT_DONE) { 
    
        err_code = nrfx_saadc_buffer_convert(p_event->data.done.p_buffer, SAMPLES_IN_BUFFER); // convert two's complement to a proper value.
        APP_ERROR_CHECK(err_code);
    
        NRF_LOG_INFO("FF[%d]: %d", sampling.index, p_event->data.done.p_buffer[0]);
    
        sampling.flexi_force[sampling.index++] = p_event->data.done.p_buffer[0];
    
        selectMuxPin(sampling.index); // na - change MUX channel to read the next sensor
      }
    }

    The NRF_LOG_INFO("FF[%d]:.... prints just once

    So, how could I read the pressure sensors before transmitting payload? Any example?

    Thanks!

  • Apart from timing and power consumption, what analogue mux are you using? It is not too difficult to make external sample/hold circuits for each analogue pressure sensor to effectively give synchronous ADC sampling if you are able to modify the hardware. A single sample pin would then capture all 8 values simultaneously which would be read out with less importance on SAADC sampling speed as the serial box-car waveform

  • Thank you for your answer but I cannot modify the hardware at the moment. However I do not understand why I cannot sampling in a loop like this

    for(uint_8 i=0; i<no_of_sensors; i++){
    nrf_drv_saadc_sample();
    }

  • Hi,

    That will not work. Calling nrfx_saadc_sample() in the end calls nrfx_saadc_sample(), which looks like this:

    nrfx_err_t nrfx_saadc_sample()
    {
        NRFX_ASSERT(m_cb.state != NRFX_DRV_STATE_UNINITIALIZED);
    
        nrfx_err_t err_code = NRFX_SUCCESS;
        if (m_cb.adc_state != NRF_SAADC_STATE_BUSY)
        {
            err_code = NRFX_ERROR_INVALID_STATE;
        }
        else if (m_cb.low_power_mode)
        {
            nrf_saadc_task_trigger(NRF_SAADC_TASK_START);
        }
        else
        {
            nrf_saadc_task_trigger(NRF_SAADC_TASK_SAMPLE);
        }
    
        NRFX_LOG_INFO("Function: %s, error code: %s.", __func__, NRFX_LOG_ERROR_STRING_GET(err_code));
        return err_code;
    }

    So you need to have set everything up for sampling, and not for instance, be in the middle of another sampling. If you want to do thing like this where you juts call a function and everything is done for you and you are ready again once it returns you need to use the blocking API instead, which is nrfx_saadc_sample_convert() in this case.

Related