nRF52840 - SDK16 - SAADC Driver - Non-blocking sampling does not work inside a loop

Hi everyone,

I want to sample eight analog sensors connected on P0.30 through a multiplexer. I created an app_timer that expires every 10ms. When the timer expires I start sampling in non-blocking mode calling the nrf_drv_saadc_sample() in a for loop. The problem is that the event handler (saadc_callback() in my case) I assigned during the SAADC initialization is called just once, during the first iteration of the loop, meaning that just one of the eight sensors is sampled.

  // Create SAADC timer.
  err_code = app_timer_create(&m_SAADC_timer_id, APP_TIMER_MODE_REPEATED, pressure_sensors_read);
  APP_ERROR_CHECK(err_code);
 

void pressure_sensors_read(struct s_sampling *sampling) {
  ret_code_t err_code;

  for (uint8_t i = 0; i < NUM_FLEXIFORCE_SENSORS; i++) {
    err_code = nrf_drv_saadc_sample();
    APP_ERROR_CHECK(err_code);
  }
  sampling->index = 0;
}

SAADC initialization

- I configured the ADC converter to default settings by just reconfiguring the resolution

- I used the default channel configuration settings by reconfiguring the gain and the reference voltage

void saadc_init(void) {
  ret_code_t err_code;
  uint8_t saadc_input;

  nrfx_saadc_config_t adc_config = NRFX_SAADC_DEFAULT_CONFIG; // set default settings
  adc_config.resolution = NRF_SAADC_RESOLUTION_12BIT;         // configure the resolution

  // GPIO assignments - https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.nrf52832.ps.v1.1%2Fpin.html
  // By default the aquitition time is 10us. You can change the aquitition time from NRFX_SAADC_DEFAULT_CHANNEL_CONFIG_SE at nrfx_saadc.h
  // Configure the SAADC input based on the EVB version
  if (evb == EVB_V2) {
    saadc_input = NRF_SAADC_INPUT_AIN1; // This is the analog input (P0.03) used for EVB.v2
  } else if (evb == EVB_V3) {
    saadc_input = NRF_SAADC_INPUT_AIN6; // This is the analog input (P0.30) used for EVB.v3
  }

  // Configure the channel
  nrf_saadc_channel_config_t channel_config = NRF_DRV_SAADC_DEFAULT_CHANNEL_CONFIG_SE(saadc_input); // use default configuration settings
  channel_config.gain = NRF_SAADC_GAIN1_4;                                                          // configure the gain
  channel_config.reference = NRF_SAADC_REFERENCE_VDD4;                                              // configure the reference voltage

  // adc initialize with custom configuratiom
  err_code = nrf_drv_saadc_init(&adc_config, saadc_callback);
  APP_ERROR_CHECK(err_code);

  err_code = nrf_drv_saadc_channel_init(0, &channel_config);
  APP_ERROR_CHECK(err_code);

  err_code = nrf_drv_saadc_buffer_convert(m_buffer_pool[0], SAMPLES_IN_BUFFER); // na - designating the buffer in which to fill the samples produces by nrf_drv_saadc_sample or NRF_SAADC_TASK_SAMPLE.
  APP_ERROR_CHECK(err_code);

  err_code = nrf_drv_saadc_buffer_convert(m_buffer_pool[1], SAMPLES_IN_BUFFER);
  APP_ERROR_CHECK(err_code);
}

Event 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) {

    err_code = nrf_drv_saadc_buffer_convert(p_event->data.done.p_buffer, SAMPLES_IN_BUFFER);
    APP_ERROR_CHECK(err_code);

    sampling.flexi_force[sampling.index++] = p_event->data.done.p_buffer[0]; // na - Buffer the ADC values from flexi force (8 x sensors)

    selectMuxPin(sampling.index); // na - change MUX channel to read the next sensor
  }
}

sdk_config.h configuration

// <e> SAADC_ENABLED - nrf_drv_saadc - SAADC peripheral driver - legacy layer
//==========================================================
#ifndef SAADC_ENABLED
#define SAADC_ENABLED 1
#endif
// <o> SAADC_CONFIG_RESOLUTION  - Resolution
 
// <0=> 8 bit 
// <1=> 10 bit 
// <2=> 12 bit 
// <3=> 14 bit 

#ifndef SAADC_CONFIG_RESOLUTION
#define SAADC_CONFIG_RESOLUTION 1
#endif

// <o> SAADC_CONFIG_OVERSAMPLE  - Sample period
 
// <0=> Disabled 
// <1=> 2x 
// <2=> 4x 
// <3=> 8x 
// <4=> 16x 
// <5=> 32x 
// <6=> 64x 
// <7=> 128x 
// <8=> 256x 

#ifndef SAADC_CONFIG_OVERSAMPLE
#define SAADC_CONFIG_OVERSAMPLE 0
#endif

// <q> SAADC_CONFIG_LP_MODE  - Enabling low power mode
 

#ifndef SAADC_CONFIG_LP_MODE
#define SAADC_CONFIG_LP_MODE 0
#endif

// <o> SAADC_CONFIG_IRQ_PRIORITY  - Interrupt priority
 

// <i> Priorities 0,2 (nRF51) and 0,1,4,5 (nRF52) are reserved for SoftDevice
// <0=> 0 (highest) 
// <1=> 1 
// <2=> 2 
// <3=> 3 
// <4=> 4 
// <5=> 5 
// <6=> 6 
// <7=> 7 

#ifndef SAADC_CONFIG_IRQ_PRIORITY
#define SAADC_CONFIG_IRQ_PRIORITY 6
#endif

What I am I doing wrong? Should I avoid placing the nrf_drv_saadc_sample() inside the for loop? Or should I tweak the configurations?

Thanks, Nick

  • Hello Nick,

    What I am I doing wrong? Should I avoid placing the nrf_drv_saadc_sample() inside the for loop? Or should I tweak the configurations?

    You mentioned that the sensors are multiplexed on the same pin - but I do not see how this multiplexing is implemented, how does the target of the measurements change inbetween each for-loop iteration?
    Furthermore, calling the nrf_drv_saadc_sample in a loop is not best practice, since this might not leave enough time for the conversions to actually finish before the next one is called - which might be causing the trouble you are seeing. With the default acquisition time of 10 µs each conversion will take at least 12 µs.

    How long does your multiplexing implementation require for each switching? How important is it for your application that the samplings happen at an accurate period of 10 ms? If an accurate samplerate is important I would recommend triggering sampling using the PPI peripheral rather than using the CPU, since the CPU might be busy in a higher-priority interrupt at any time.

    The problem is that the event handler (saadc_callback() in my case) I assigned during the SAADC initialization is called just once, during the first iteration of the loop, meaning that just one of the eight sensors is sampled.

    You mention that the event handler is only called once - do you by this mean that the DONE event is generated only once?
    The DONE event will be generated when the provided buffer is filled - what is the value of SAMPLES_IN_BUFFER?

    sdk_config.h configuration

    Lastly, I see that you are using the legacy SAADC definitions. You should rather use the NRFX_SAADC definitions directly - since the nrfx_saadc driver is what is being used behind the scenes anyways - and remove all the legacy defines from your sdk_config to avoid that the NRFX_SAADC configuration is overwritten by the legacy configuration by the apply_old_config file. This will happen if the legacy define's are defined in your sdk_config, so they should be removed or commented out entirely to avoid this.

    Best regards,
    Karl

  • Thank you for your answer Karl,

    You mentioned that the sensors are multiplexed on the same pin - but I do not see how this multiplexing is implemented, how does the target of the measurements change inbetween each for-loop iteration?

    After each sampling, I call this function to switch the channel of the multiplexer

    void selectMuxPin(uint8_t index) {
      NRF_LOG_INFO("Index value: %d", index);
      for (int i = 0; i < MUX_CTRL_CHA; i++) {
    
        if (index & (1 << i)) {
          nrf_gpio_pin_set(ctrlPins[i]);
        } else {
          nrf_gpio_pin_clear(ctrlPins[i]);
        }
      }
    }

    How long does your multiplexing implementation require for each switching?

    I didn't measure the switching transition of the MUX but according to the datasheet it should be around 50ns

    Furthermore, calling the nrf_drv_saadc_sample in a loop is not best practice, since this might not leave enough time for the conversions to actually finish before the next one is called - which might be causing the trouble you are seeing

    So, what is the suggested solution for sampling the sensors?

    You mention that the event handler is only called once - do you by this mean that the DONE event is generated only once?

    The event handler is called every time an event is generated by the SAADC module, right? In my case, I call the nrf_drv_saadc_sample in a loop of eight iterations (because I have eight sensors) but only after the first nrf_drv_saadc_sample call the event handler is called, so probably the DONE event is generated just once?

    How important is it for your application that the samplings happen at an accurate period of 10 ms? If an accurate samplerate is important I would recommend triggering sampling using the PPI peripheral rather than using the CPU, since the CPU might be busy in a higher-priority interrupt at any time.

     

    Our application requires a 100Hz sampling frequency, that's why I set the sampling period to 10ms. The application does not require a super-duper accurate sampling rate but I will consider using the PPI peripheral after solving the current issue Slight smile

    You should rather use the NRFX_SAADC definitions directly - since the nrfx_saadc driver is what is being used behind the scenes anyways

    Ok I will replace the legacy with nrfx

    and remove all the legacy defines from your sdk_config to avoid that the NRFX_SAADC configuration is overwritten by the legacy configuration by the apply_old_config file.

    Do you mean, to comment out these definitions from the sdk_config?

    // <e> SAADC_ENABLED - nrf_drv_saadc - SAADC peripheral driver - legacy layer
    //==========================================================
    
    // <o> SAADC_CONFIG_RESOLUTION  - Resolution
     
    // <0=> 8 bit 
    // <1=> 10 bit 
    // <2=> 12 bit 
    // <3=> 14 bit 
    
    #ifndef SAADC_CONFIG_RESOLUTION
    #define SAADC_CONFIG_RESOLUTION 1
    #endif
    
    // <o> SAADC_CONFIG_OVERSAMPLE  - Sample period
     
    // <0=> Disabled 
    // <1=> 2x 
    // <2=> 4x 
    // <3=> 8x 
    // <4=> 16x 
    // <5=> 32x 
    // <6=> 64x 
    // <7=> 128x 
    // <8=> 256x 
    
    #ifndef SAADC_CONFIG_OVERSAMPLE
    #define SAADC_CONFIG_OVERSAMPLE 0
    #endif
    
    // <q> SAADC_CONFIG_LP_MODE  - Enabling low power mode
     
    
    #ifndef SAADC_CONFIG_LP_MODE
    #define SAADC_CONFIG_LP_MODE 0
    #endif
    
    // <o> SAADC_CONFIG_IRQ_PRIORITY  - Interrupt priority
     
    
    // <i> Priorities 0,2 (nRF51) and 0,1,4,5 (nRF52) are reserved for SoftDevice
    // <0=> 0 (highest) 
    // <1=> 1 
    // <2=> 2 
    // <3=> 3 
    // <4=> 4 
    // <5=> 5 
    // <6=> 6 
    // <7=> 7 
    
    #ifndef SAADC_CONFIG_IRQ_PRIORITY
    #define SAADC_CONFIG_IRQ_PRIORITY 6
    #endif
    
    // </e>

  • Hello,

    Nikosant03 said:
    Thank you for your answer Karl,

    No problem at all, Nick - I am happy to help!

    Nikosant03 said:
    So, what is the suggested solution for sampling the sensors?

    This depends on the timing requirements for the sampling. Do you want them to happen as quickly as possible, or at a specific interval?
    Either way the recommended approach for keeping a precise sampling interval is to trigger the sampling using the PPI peripheral, in which you may connect an event (such as a TIMER CC event) to a task (such as the SAMPLE task) to perform the samplings. This way each sampling does not require CPU intervention, and so they will happen whenever they need to, rather than whenever the CPU has time to trigger them. You could see this demonstrated in the SAADC example from the nRF5 SDK.

    Nikosant03 said:
    The event handler is called every time an event is generated by the SAADC module, right? In my case, I call the nrf_drv_saadc_sample in a loop of eight iterations (because I have eight sensors) but only after the first nrf_drv_saadc_sample call the event handler is called, so probably the DONE event is generated just once?

    This depends on which events you have enabled interrupts for, and other factors such as the SAADC configuration - the DONE event for example is only generated when a buffer is filled up, which in turn depends on the size of the provided buffer and the frequency of sampling.
    Could it for example be an option to increase the buffer size to 8, and then trigger the samplings through PPI using a TIMER, and handle the filled buffer in the following DONE event? We might also have to look into using the PPI for the switching the GPIO's if possible, since the mux switch will not happen on time if the CPU is busy elsewhere, unless the switching is run from the highest priority (which it cant be if the SoftDevice is present).

    Nikosant03 said:

    Our application requires a 100Hz sampling frequency, that's why I set the sampling period to 10ms. The application does not require a super-duper accurate sampling rate but I will consider using the PPI peripheral after solving the current issue

    Thank you for clarifying. I think the issue here might be with the way the samples are initiated, so switching to the PPI approach should solve this.

    Nikosant03 said:
    Do you mean, to comment out these definitions from the sdk_config?

    Yes, along with SAADC_ENABLE especially, if it is present.

    Best regards,
    Karl

  • Hi Karl,

    Do you want them to happen as quickly as possible, or at a specific interval?

    To happen a quickly as possible

    This depends on which events you have enabled interrupts for

    How do I enable interrupts based on specific events? I do not see any function in SAADC driver. The available events are these? Also looking at the saadc example of SDK16, I cannot find any code line that enables any event interrupt (let's say the NRFX_SAADC_EVT_DONE event), but the example works fine.

    Yes, along with SAADC_ENABLE especially, if it is present.

    I modified the code using the SAADC Driver (nrfx) and commented out all the legacy definitions.

    void saadc_init(void) {
      ret_code_t err_code;
    
      nrf_saadc_channel_config_t channel_config = NRF_DRV_SAADC_DEFAULT_CHANNEL_CONFIG_SE(NRF_SAADC_INPUT_AIN6); // AIN6 is the pin P0.30 - https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.nrf52832.ps.v1.1%2Fpin.html
    
      err_code = nrfx_saadc_init(NULL, saadc_callback);
      APP_ERROR_CHECK(err_code);
    
      err_code = nrfx_saadc_channel_init(0, &channel_config);
      APP_ERROR_CHECK(err_code);
    
      err_code = nrfx_saadc_buffer_convert(m_buffer_pool[0], SAMPLES_IN_BUFFER);
      APP_ERROR_CHECK(err_code);
    
      err_code = nrfx_saadc_buffer_convert(m_buffer_pool[1], SAMPLES_IN_BUFFER);
      APP_ERROR_CHECK(err_code);
    }
    

    However, if I do now set the SAADC_ENABLED to 1 the code doesn't compile and returns "undefined reference" error. 

    According to the Migration guide for nrfx driversSAADC_ENABLED must be set to 1? This is the same issue with this ticket. Sorry for asking the same thing but I have to ensure that I'm doing it the right way.

    Thanks

    Nick

  • Hello Nick,

    Nikosant03 said:
    How do I enable interrupts based on specific events? I do not see any function in SAADC driver. The available events are these?

    The HAL layer events are handled in the driver in the irq_handler, and then the appropriate NRFX event is passed to the application layer event handler through the call to m_cb.event_handler(&evt_data);
    The available NRFX events can be seen in the nrfx_saadc.h file - keep in mind that not everyone of these events are forwarded to the application layer, so you might have to modify the driver to make this happen if your require some other events.

    For SDK v16 you will have to check the nrfx v1.8 tagged release of the nrfx repo, since this is what is included with the nRF5 SDK v16.0.0.

    Nikosant03 said:
    However, if I do now set the SAADC_ENABLED to 1 the code doesn't compile and returns "undefined reference" error. 


    Yes, SAADC_ENABLED should not be left defined - i.e it needs to be removed or commented out of sdk_config.h all together. Only NRFX_SAADC_ENABLED should be defined in sdk_config, along with the other NRFX_SAADC_* configurations.

    Nikosant03 said:
    According to the Migration guide for nrfx driversSAADC_ENABLED must be set to 1? This is the same issue with this ticket. Sorry for asking the same thing but I have to ensure that I'm doing it the right way.

    No need to apologize - I would rather that you ask whatever question you have, in case anything is unclear. Please know that you are not the first, and likely not the last, to ask about the sdk_config.h and the confusion around the legacy definitions :) 

    The note you quote from is actually trying to say that you need to remove SAADC_ENABLED all together - it is not enough to leave it defined to 0, because then it is still defined, which will cause the apply_old_config to kick in and overwrite the particular peripheral's sdk_config.h NRFX configurations.

    Best regards,
    Karl

Related