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

Compiler optimization breaks SAADC in SDK 15.3.0

Hello!

It seems to me that GCC compiler optimization (-O3 -g3) somehow break the SAADC.

When enabling said optimizations the SAADC no longer gives me valid measurements (e.g. I have an expected ADC value of 1200 but when enabling optimizations I get a value of 3).

Here you can find my code that I am using to measure the SAADC

// defines
#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 ADC_GAIN NRF_SAADC_GAIN1                                // ADC gain.
#define ADC_REFERENCE_VOLTAGE (0.6f)                            // The standard internal ADC reference voltage.
#define ADC_RESOLUTION_BITS (8 + (SAADC_CONFIG_RESOLUTION * 2)) //ADC resolution [bits].

// variables
// The target Pins as defined by the PCB
static nrf_saadc_input_t saadc_channel_pins[2] = {NRF_SAADC_INPUT_AIN5, NRF_SAADC_INPUT_AIN4};
// Flag if the saadc has already been initialized
static bool is_saadc_initialized;
// a flag to determine if a calibration is in progress
static volatile bool is_calibrating;

// functions
static float adc_gain_enum_to_real_gain(nrf_saadc_gain_t gain_reg)
{
    switch (gain_reg)
    {
        case NRF_SAADC_GAIN1_6:
            return 1 / (float)6;
        case NRF_SAADC_GAIN1_5:
            return 1 / (float)5;
        case NRF_SAADC_GAIN1_4:
            return 1 / (float)4;
        case NRF_SAADC_GAIN1_3:
            return 1 / (float)3;
        case NRF_SAADC_GAIN1_2:
            return 1 / (float)2;
        case NRF_SAADC_GAIN1:
            return 1;
        case NRF_SAADC_GAIN2:
            return 2;
        case NRF_SAADC_GAIN4:
            return 3;
        default:
            return -1;
    };
}
static float adc_to_batt_voltage(uint32_t adc_val)
{
    float adc_gain = adc_gain_enum_to_real_gain(ADC_GAIN);
    return adc_val / ((adc_gain / ADC_REFERENCE_VOLTAGE) * pow(2, ADC_RESOLUTION_BITS));
}
static void saadc_callback(nrfx_saadc_evt_t const *p_event)
{
    NRF_LOG_DEBUG("%d received", p_event->type);
    if (p_event->type == NRFX_SAADC_EVT_CALIBRATEDONE)
    {
        NRF_LOG_INFO("Calibration complete");
        is_calibrating = false;
    }
}
/**
 * @brief Calibrates the SAADC and waits for the calibration to finish
 * 
 * @return ret_code_t Status code if the SAADC was calibrated successfully
 */
static ret_code_t calibrate_saadc()
{
    NRF_LOG_INFO("Calibration starting");
    nrfx_saadc_abort();

    is_calibrating = true;
    ret_code_t err_code = nrfx_saadc_calibrate_offset(); // trigger calibration request. if it fails the SAADC probably was busy so we'll try again next time
    RETURN_IF_ERROR(err_code);

    while (is_calibrating)
    {
    };
    nrfx_saadc_abort();

    return NRF_SUCCESS;
}

/**
 * @brief Initializes the SAADC
 * @return ret_code_t Status code if the SAADC was successfully initialized
 * 
 */
ret_code_t saadc_init(SAADC_TARGET target)
{
    ret_code_t err_code;

    // Configure and initialize SAADC (if not yet initialized)
    // this method can be called multiple times as different modules rely on the SAADC. this if ensures no erros while initializing the SAADC twice
    if (!is_saadc_initialized)
    {
        nrfx_saadc_config_t saadc_config = NRFX_SAADC_DEFAULT_CONFIG;
        err_code = nrfx_saadc_init(&saadc_config, saadc_callback); //Initialize the SAADC with configuration and callback function. The application must then implement the saadc_callback function, which will be called when SAADC interrupt is triggered
        RETURN_IF_ERROR(err_code);

        is_saadc_initialized = true;
        NRF_LOG_INFO("Initialized");
    }

    return NRF_SUCCESS;
}

/**
 * @brief Starts a measurement for the given target
 * @param target The target to measure
 * @param value Where to save the measured value to
 * 
 * @return ret_code_t Status code if the SAADC measurement was done successfully
 */
ret_code_t saadc_measure(SAADC_TARGET target, float *value)
{
    // prepare fields and set flags
    ret_code_t err_code;

    // Configure and initialize SAADC channel
    nrf_saadc_channel_config_t channel_config = NRFX_SAADC_DEFAULT_CHANNEL_CONFIG_SE(saadc_channel_pins[target]);
    channel_config.gain = ADC_GAIN;
    channel_config.burst = NRF_SAADC_BURST_ENABLED;
    channel_config.acq_time = NRF_SAADC_ACQTIME_40US;
    err_code = nrfx_saadc_channel_init(0, &channel_config); //Initialize SAADC channel 0 with the channel configuration
    RETURN_IF_ERROR(err_code);

    // calibrate
    err_code = calibrate_saadc();
    RETURN_IF_ERROR(err_code);

    //nrf_delay_ms(50);

    // ... and start measurement
    NRF_LOG_INFO("Measurement starting");
    nrf_saadc_value_t adc_value;
    err_code = nrfx_saadc_sample_convert(0, &adc_value);
    RETURN_IF_ERROR(err_code);

    *value = adc_to_batt_voltage(adc_value);
    NRF_LOG_INFO("Measured ADC value %d, voltage: " NRF_LOG_FLOAT_MARKER " V", adc_value, NRF_LOG_FLOAT(*value));

    // uninit channel so that it can be reinitialized for the next measurement
    err_code = nrfx_saadc_channel_uninit(0);
    RETURN_IF_ERROR(err_code);

    return NRF_SUCCESS;
}

This code is responsible for measuring the analog inputs 4 and 5.

The issue occours on both inputs. The numerical example I have given above is for Input 5.

I am aware of Anomaly 86 (https://infocenter.nordicsemi.com/index.jsp?topic=%2Ferrata_nRF52832_Rev2%2FERR%2FnRF52832%2FRev2%2Flatest%2Fanomaly_832_86.html&cp=3_1_1_0_1_23)

To avoid this issue I have slightly modified the nrfx_saadc_abort function to remove as seen below.

This change allows me to use this function to stop the SAADC as described in the anomaly without having to write my own code and it works just as expected if I have optimizations disabled.

void nrfx_saadc_abort(void)
{
    // miko, 20190827:
    // Removed this if-statement to allow the firmware to abort an ongoing calibration
    // This is a workaround for Anoamly 86 (https://infocenter.nordicsemi.com/index.jsp?topic=%2Ferrata_nRF52832_Rev2%2FERR%2FnRF52832%2FRev2%2Flatest%2Fanomaly_832_86.html&cp=3_1_1_0_1_23)
    //if (nrfx_saadc_is_busy())
    //{
    nrf_saadc_event_clear(NRF_SAADC_EVENT_STOPPED);
    nrf_saadc_int_enable(NRF_SAADC_INT_STOPPED);
    nrf_saadc_task_trigger(NRF_SAADC_TASK_STOP);

    if (m_cb.adc_state == NRF_SAADC_STATE_CALIBRATION)
    {
        m_cb.adc_state = NRF_SAADC_STATE_IDLE;
    }
    else
    {
        // Wait for ADC being stopped.
        bool result;
        NRFX_WAIT_FOR((m_cb.adc_state == NRF_SAADC_STATE_IDLE), HW_TIMEOUT, 0, result);
        NRFX_ASSERT(result);
    }

    nrf_saadc_int_disable(NRF_SAADC_INT_STOPPED);

    m_cb.p_buffer = 0;
    m_cb.p_secondary_buffer = 0;
    NRFX_LOG_INFO("Conversion aborted.");
    //}
}

I have been able to disable optimizations for the nrfx_saadc.c file by adding "#pragma GCC optimize("O0")" to the start of the file. This makes the SAADC measurement work again but it just doesn't feel right.

Are you aware of this issue and do you have any suggestions on what I should do to fix it?

Regards

Michael

Parents
  • Hi,

     

    What version of GCC are you using? LTO used?

    I have been able to disable optimizations for the nrfx_saadc.c file by adding "#pragma GCC optimize("O0")" to the start of the file. This makes the SAADC measurement work again but it just doesn't feel right.

    You're right to be worried.

    However, where does the corruption occur? Is it the float operation that fails, or is the nrfx call returning the incorrect value? It seems that you print only after the float conversion from the code snippets that you posted.

     

    Kind regards,

    Håkon

  • Hi and thanks for your reply!

    It's the nrfx call which fails. In the first code snippet above I am calling the `nrfx_saadc_sample_convert` function. That's where I already get the wrong data.

    When printing after the float conversion I am printing the previously received ADC value, as well as the newly calculated voltage value, if you look closely.

    For example I'd expect an ADC value of 1200, which I get if I have disabled the compiler optimizations. When enabling the optimizations I only get a value of 3.

    I am using GCC Version 7-2018-q2. Also, I have LTO disabled.

    Here you can see the part of my Makefile responsible for setting these flags, depending on the given CONFIGURATION

    # Optimization flags
    ifeq ($(CONFIGURATION), debug)
        OPT = -O0 -g3
        CFLAGS += -DDEBUG
        ASMFLAGS += -DDEBUG
    else
        OPT = -O3 -g3
    endif
    # Uncomment the line below to enable link time optimization
    #OPT += -flto

  • I am not 100 % sure that the workaround you have for implementing the calibration routine is water tight.

    Could you try this?

    NRF_SAADC->TASKS_STOP = 1;
    while(NRF_SAADC->EVENTS_STOPPED == 0);
    NRF_SAADC->EVENTS_STOPPED = 0;
    
    NRF_SAADC->TASKS_CALIBRATEOFFSET = 1;
    while(NRF_SAADC->EVENTS_CALIBRATEDONE == 0);
    NRF_SAADC->EVENTS_CALIBRATEDONE = 0;
    
    NRF_SAADC->TASKS_STOP = 1;
    while(NRF_SAADC->EVENTS_STOPPED == 0);
    NRF_SAADC->EVENTS_STOPPED = 0;
    
    NRF_SAADC->TASKS_START = 1;

     

    PS: Note that you do not need to calibrate every time you do a conversion, unless there's a temperature change for instance or any other external change.

    Normally its OK if you calibrate on boot.

     

    Kind regards,

    Håkon

Reply
  • I am not 100 % sure that the workaround you have for implementing the calibration routine is water tight.

    Could you try this?

    NRF_SAADC->TASKS_STOP = 1;
    while(NRF_SAADC->EVENTS_STOPPED == 0);
    NRF_SAADC->EVENTS_STOPPED = 0;
    
    NRF_SAADC->TASKS_CALIBRATEOFFSET = 1;
    while(NRF_SAADC->EVENTS_CALIBRATEDONE == 0);
    NRF_SAADC->EVENTS_CALIBRATEDONE = 0;
    
    NRF_SAADC->TASKS_STOP = 1;
    while(NRF_SAADC->EVENTS_STOPPED == 0);
    NRF_SAADC->EVENTS_STOPPED = 0;
    
    NRF_SAADC->TASKS_START = 1;

     

    PS: Note that you do not need to calibrate every time you do a conversion, unless there's a temperature change for instance or any other external change.

    Normally its OK if you calibrate on boot.

     

    Kind regards,

    Håkon

Children
No Data
Related