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

Sometimes nrf52840 aadc calibration is wrong

Using the nrf52840 for analog sensor measurements we wanted to increase the accuracy by doing a regular calibration. Doing so we sometimes noticed spurious changes in measurement offset error when the calibration was done. After investigating this behaviour on a devboard it looks like the calibration is just sometimes off!

The following can be used to reproduce the problem on a devboard using AIN0 with an open-input.

/**
This file shows that the adc calibration make the measurements fluctuate
*/

#include "aadc_errors.h"


#include <nrf_delay.h>
#include <nrfx_saadc.h>

#include <stdio.h>
#include <stdlib.h>

#define ADC_RESOLUTION 4096u /// 12 bit adc reolution (14 bit is only accomplished with oversampling)

#define CHANNEL_CONFIG(PIN, RESISTOR_P, GAIN) (nrf_saadc_channel_config_t)\
{\
    .resistor_p = RESISTOR_P,      \
    .resistor_n = NRF_SAADC_RESISTOR_DISABLED,      \
    .gain       = GAIN,                \
    .reference  = NRF_SAADC_REFERENCE_INTERNAL,     \
    .acq_time   = NRF_SAADC_ACQTIME_10US,           \
    .mode       = NRF_SAADC_MODE_SINGLE_ENDED,      \
    .burst      = NRF_SAADC_BURST_DISABLED,         \
    .pin_p      = PIN,       \
    .pin_n      = NRF_SAADC_INPUT_DISABLED         \
}

static nrfx_saadc_config_t saadc_config = NRFX_SAADC_DEFAULT_CONFIG;

static void saadc_callback(nrfx_saadc_evt_t const * p_event)
{
    UNUSED_PARAMETER(p_event);
}

// sample 1 channel, already configured
static void channel_convert(uint8_t channel, unsigned oversample, nrf_saadc_value_t *adc)
{
    int adc_sum = 0;
    for( unsigned i = 0; i<oversample+1; i++ )
    {
        ret_code_t rr = nrfx_saadc_sample_convert(channel, adc);
        ASSERT( rr == NRF_SUCCESS );
        //printf("    part_adc=%hd\n", *adc);
        adc_sum += *adc;
    }
    
    int adc_i = adc_sum / (int)(oversample+1);

    *adc = adc_i;
}

static void mcu_adc_calibrate(void)
{
    ret_code_t r;
    
    printf("Calibrate\n");

    r = nrfx_saadc_calibrate_offset();
    ASSERT(r == NRF_SUCCESS);
    
    while (nrfx_saadc_is_busy())
    {
    }

    // Nordic errata 86 workaround...
    nrfx_saadc_uninit();
    r = nrfx_saadc_init(&saadc_config, saadc_callback);
    ASSERT(r == NRF_SUCCESS);
        
    // Note: According documentation, the errata is also handled in    nrfx_saadc_abort()
}

static void mcu_adc_read_gnd(nrf_saadc_gain_t gain, nrf_saadc_value_t *adc)
{
    nrf_saadc_channel_config_t chcfg = CHANNEL_CONFIG(NRF_SAADC_INPUT_AIN0, NRF_SAADC_RESISTOR_PULLDOWN, gain);
    
    nrfx_saadc_channel_init(0, &chcfg);
    // get rid of noise by oversampling a lot
    channel_convert(0, 1000, adc);
    nrfx_saadc_channel_uninit(0);
    nrfx_saadc_abort();
    
    // FIXME this is a patch for the fact the that ADC does not stop (reason unknonw).
    nrf_saadc_task_trigger(NRF_SAADC_TASK_STOP);
}

void show_calibration_error(void)
{
    ret_code_t err_code = nrfx_saadc_init(&saadc_config, saadc_callback);
    ASSERT(err_code == NRF_SUCCESS);
    
    for(unsigned j=0; j<10; j++)
    {
        mcu_adc_calibrate();
        for(unsigned i=0; i<2; i++)
        {
            nrf_saadc_value_t adc = 0;
            const char *sep = "   ";
            
            for( nrf_saadc_gain_t gain = NRF_SAADC_GAIN1_6; gain <=NRF_SAADC_GAIN4; gain++ )
            {
                mcu_adc_read_gnd( gain, &adc );
                printf("%sadc:{gain:%d, v:%hd}", sep, gain, adc);
                sep = ", ";
            }

            printf("\n");
        }
    }
    
    nrfx_saadc_uninit();
}




This results in something like:


Calibrate
   adc:{gain:0, v:0}, adc:{gain:1, v:0}, adc:{gain:2, v:0}, adc:{gain:3, v:0}, adc:{gain:4, v:3}, adc:{gain:5, v:9}, adc:{gain:6, v:24}, adc:{gain:7, v:51}
   adc:{gain:0, v:0}, adc:{gain:1, v:0}, adc:{gain:2, v:1}, adc:{gain:3, v:0}, adc:{gain:4, v:3}, adc:{gain:5, v:9}, adc:{gain:6, v:24}, adc:{gain:7, v:53}
Calibrate
   adc:{gain:0, v:-12}, adc:{gain:1, v:-11}, adc:{gain:2, v:-10}, adc:{gain:3, v:-11}, adc:{gain:4, v:-8}, adc:{gain:5, v:-1}, adc:{gain:6, v:13}, adc:{gain:7, v:42}
   adc:{gain:0, v:-12}, adc:{gain:1, v:-11}, adc:{gain:2, v:-11}, adc:{gain:3, v:-11}, adc:{gain:4, v:-8}, adc:{gain:5, v:-2}, adc:{gain:6, v:14}, adc:{gain:7, v:41}
Calibrate
   adc:{gain:0, v:-1}, adc:{gain:1, v:0}, adc:{gain:2, v:0}, adc:{gain:3, v:0}, adc:{gain:4, v:3}, adc:{gain:5, v:9}, adc:{gain:6, v:24}, adc:{gain:7, v:51}
   adc:{gain:0, v:0}, adc:{gain:1, v:0}, adc:{gain:2, v:1}, adc:{gain:3, v:0}, adc:{gain:4, v:3}, adc:{gain:5, v:10}, adc:{gain:6, v:25}, adc:{gain:7, v:52}
Calibrate
   adc:{gain:0, v:-12}, adc:{gain:1, v:-11}, adc:{gain:2, v:-10}, adc:{gain:3, v:-10}, adc:{gain:4, v:-8}, adc:{gain:5, v:-2}, adc:{gain:6, v:14}, adc:{gain:7, v:42}
   adc:{gain:0, v:-12}, adc:{gain:1, v:-11}, adc:{gain:2, v:-10}, adc:{gain:3, v:-11}, adc:{gain:4, v:-8}, adc:{gain:5, v:-1}, adc:{gain:6, v:13}, adc:{gain:7, v:42}
Calibrate
   adc:{gain:0, v:-11}, adc:{gain:1, v:-11}, adc:{gain:2, v:-10}, adc:{gain:3, v:-11}, adc:{gain:4, v:-7}, adc:{gain:5, v:-1}, adc:{gain:6, v:14}, adc:{gain:7, v:42}
   adc:{gain:0, v:-11}, adc:{gain:1, v:-11}, adc:{gain:2, v:-10}, adc:{gain:3, v:-10}, adc:{gain:4, v:-8}, adc:{gain:5, v:-1}, adc:{gain:6, v:13}, adc:{gain:7, v:42}
Calibrate
   adc:{gain:0, v:-1}, adc:{gain:1, v:0}, adc:{gain:2, v:0}, adc:{gain:3, v:0}, adc:{gain:4, v:4}, adc:{gain:5, v:9}, adc:{gain:6, v:24}, adc:{gain:7, v:53}
   adc:{gain:0, v:0}, adc:{gain:1, v:0}, adc:{gain:2, v:1}, adc:{gain:3, v:1}, adc:{gain:4, v:3}, adc:{gain:5, v:10}, adc:{gain:6, v:24}, adc:{gain:7, v:52}
Calibrate
   adc:{gain:0, v:0}, adc:{gain:1, v:0}, adc:{gain:2, v:0}, adc:{gain:3, v:1}, adc:{gain:4, v:4}, adc:{gain:5, v:9}, adc:{gain:6, v:24}, adc:{gain:7, v:53}
   adc:{gain:0, v:0}, adc:{gain:1, v:0}, adc:{gain:2, v:0}, adc:{gain:3, v:0}, adc:{gain:4, v:3}, adc:{gain:5, v:9}, adc:{gain:6, v:24}, adc:{gain:7, v:52}
Calibrate
   adc:{gain:0, v:-11}, adc:{gain:1, v:-10}, adc:{gain:2, v:-11}, adc:{gain:3, v:-11}, adc:{gain:4, v:-8}, adc:{gain:5, v:-2}, adc:{gain:6, v:13}, adc:{gain:7, v:42}
   adc:{gain:0, v:-12}, adc:{gain:1, v:-11}, adc:{gain:2, v:-11}, adc:{gain:3, v:-10}, adc:{gain:4, v:-7}, adc:{gain:5, v:-1}, adc:{gain:6, v:13}, adc:{gain:7, v:41}
Calibrate
   adc:{gain:0, v:-12}, adc:{gain:1, v:-11}, adc:{gain:2, v:-10}, adc:{gain:3, v:-10}, adc:{gain:4, v:-7}, adc:{gain:5, v:-2}, adc:{gain:6, v:13}, adc:{gain:7, v:42}
   adc:{gain:0, v:-12}, adc:{gain:1, v:-11}, adc:{gain:2, v:-11}, adc:{gain:3, v:-11}, adc:{gain:4, v:-8}, adc:{gain:5, v:-1}, adc:{gain:6, v:13}, adc:{gain:7, v:42}
Calibrate
   adc:{gain:0, v:-12}, adc:{gain:1, v:-12}, adc:{gain:2, v:-10}, adc:{gain:3, v:-10}, adc:{gain:4, v:-8}, adc:{gain:5, v:-2}, adc:{gain:6, v:13}, adc:{gain:7, v:41}
   adc:{gain:0, v:-12}, adc:{gain:1, v:-11}, adc:{gain:2, v:-11}, adc:{gain:3, v:-11}, adc:{gain:4, v:-8}, adc:{gain:5, v:-2}, adc:{gain:6, v:13}, adc:{gain:7, v:41}



This makes it clear the the calibration jumps between 2 values (some runs it is even 4 different values values)

- Why does this happen?
- what can we do to fix this?

edit: formatting to improve readability

  • I restructured the program to improve showing the calibration problem.

    #include <stdio.h>
    #include <stdlib.h>
    
    #include <nrf_delay.h>
    #include <nrfx_gpiote.h>
    #include <nrfx_saadc.h>
    
    
    // uncomment this to show the calibration problem without oversampling.
    //#define ENABLE_CALIBRATION_BUG
    
    
    const float v_ref = 0.6;     // internal reference voltage
    STATIC_ASSERT(NRFX_SAADC_CONFIG_RESOLUTION == 2);
    #define ADC_RESOLUTION 4096u /// 12 bit adc reolution (14 bit is only accomplished with oversampling)
    
    #define CHANNEL_CONFIG(PIN_P, RESISTOR_P, GAIN) (nrf_saadc_channel_config_t)\
    {\
        .resistor_p = RESISTOR_P,      \
        .resistor_n = NRF_SAADC_RESISTOR_DISABLED,      \
    	.gain       = GAIN,                \
        .reference  = NRF_SAADC_REFERENCE_INTERNAL,     \
        .acq_time   = NRF_SAADC_ACQTIME_10US,           \
        .mode       = NRF_SAADC_MODE_SINGLE_ENDED,      \
        .burst      = NRF_SAADC_BURST_DISABLED,         \
        .pin_p      = (nrf_saadc_input_t)(PIN_P),       \
        .pin_n      = NRF_SAADC_INPUT_DISABLED         \
    }
    
    static nrfx_saadc_config_t saadc_config = NRFX_SAADC_DEFAULT_CONFIG;
    static volatile bool limit_event;
    
    static float get_gain_factor(nrf_saadc_gain_t gain)
    {
    	float factor ;
    	switch( gain )
    	{
    		case NRF_SAADC_GAIN1_6: factor = 1.0/6; break;
    		case NRF_SAADC_GAIN1_5: factor = 1.0/5; break;
    		case NRF_SAADC_GAIN1_4: factor = 1.0/4; break;
    		case NRF_SAADC_GAIN1_3: factor = 1.0/3; break;
    		case NRF_SAADC_GAIN1_2: factor = 1.0/2; break;
    		case NRF_SAADC_GAIN1: factor = 1.0; break;
    		case NRF_SAADC_GAIN2: factor = 2.0; break;
    		case NRF_SAADC_GAIN4: factor = 4.0; break;
    		default:
    			ASSERT(false);
    			break;
    	}
    	return factor;
    }
    
    static float adc_to_voltage( float adc, nrf_saadc_gain_t gain )
    {
    	float range = 0.6/get_gain_factor(gain);
    	return range * adc/ADC_RESOLUTION;
    }
    
    static void saadc_callback(nrfx_saadc_evt_t const * p_event)
    {
    	if (p_event->type == NRFX_SAADC_EVT_LIMIT)
    	{
    		limit_event = true;
    	}
    	else if (p_event->type == NRFX_SAADC_EVT_DONE)
    	{
    		limit_event = false;
    	}
    	else if (p_event->type == NRFX_SAADC_EVT_CALIBRATEDONE)
    	{
    		limit_event = false;
    	}
    }
    
    static void mcu_adc_init(void)
    {
    	ret_code_t err_code = nrfx_saadc_init(&saadc_config, saadc_callback);
    	ASSERT(err_code == NRF_SUCCESS);
    }
    
    static void mcu_adc_uninit(void)
    {
    	nrfx_saadc_uninit();
    }
    
    // sample 1 channel, already configured
    // @param *adc [out]: the average of the raw read adc samples.
    static void channel_convert(uint8_t channel, unsigned oversample, float *adc_avg)
    {
    	int adc_sum = 0;
    	for( unsigned i = 0; i<oversample+1; i++ )
    	{
    		nrf_saadc_value_t adc;
    		ret_code_t rr = nrfx_saadc_sample_convert(channel, &adc);
    		ASSERT( rr == NRF_SUCCESS );
    		//printf("    part_adc=%hd\n", *adc);
    		adc_sum += adc;
    	}
    	
    	*adc_avg = adc_sum / (float)(oversample+1);
    }
    
    
    void mcu_adc_calibrate(void)
    {
    	ret_code_t r;
    	
    	printf("Calibrate\n");
    	
    	// prepare
    	nrfx_saadc_uninit();
    	#ifndef ENABLE_CALIBRATION_BUG
    	saadc_config.oversample = 8;
    	#endif
    	r = nrfx_saadc_init(&saadc_config, saadc_callback);
    	
    	// calibrate
    	r = nrfx_saadc_calibrate_offset();
    	ASSERT(r == NRF_SUCCESS);
    	
    	while (nrfx_saadc_is_busy())
    	{
    	}
    
    	// finalize
    	
    	// Nordic errata 86 workaround...
    	nrfx_saadc_uninit();
    	saadc_config.oversample = 0;
    	r = nrfx_saadc_init(&saadc_config, saadc_callback);
    	ASSERT(r == NRF_SUCCESS);
    }
    
    void mcu_adc_read_gnd(nrf_saadc_gain_t gain, unsigned oversample, float *adc)
    {
    	nrf_saadc_channel_config_t chcfg = CHANNEL_CONFIG(NRF_SAADC_INPUT_DISABLED, NRF_SAADC_RESISTOR_PULLDOWN, gain);
    
    	nrf_saadc_channel_init(0, &chcfg);
    	channel_convert(0, oversample, adc);
    	nrf_saadc_channel_input_set(0, NRF_SAADC_INPUT_DISABLED, NRF_SAADC_INPUT_DISABLED);
    }
    
    void mcu_adc_read_1(nrf_saadc_input_t pin, nrf_saadc_gain_t gain, unsigned oversample, float *adc)
    {
    	nrf_saadc_channel_config_t chcfg = CHANNEL_CONFIG(pin, NRF_SAADC_RESISTOR_DISABLED, gain);
    	
    	nrfx_saadc_channel_init(0, &chcfg);
    	channel_convert(0, oversample, adc);
    	nrfx_saadc_channel_uninit(0);
    }
    
    int mcu_adc_read_test(void)
    {
    	mcu_adc_init();
    	nrf_gpio_cfg_input(NRF_GPIO_PIN_MAP(0, 2), NRF_GPIO_PIN_NOPULL);
    	
    	for(unsigned j=0; j<1000; j++)
    	{
    		mcu_adc_calibrate();
    		for(unsigned i=0; i<3; i++)
    		{
    			const char *sep = "   ";
    			const unsigned oversample = 1023;
    			
    			for( nrf_saadc_gain_t gain = NRF_SAADC_GAIN1_6; gain<=NRF_SAADC_GAIN1_5; gain++ )
    			{
    				printf("%s{gain:%0.2f, ", sep,  get_gain_factor(gain));
    					
    				// zero measurement
    				float adcgnd;
    				mcu_adc_read_gnd(gain, oversample, &adcgnd);
    				
    				if(nrfx_saadc_is_busy())
    				{
    					printf("still busy!\n");
    					nrfx_saadc_is_busy();
    					{}
    				}
    				
    				float v_adcgnd = adc_to_voltage(adcgnd, gain);
    				printf(", adcgnd:{adc:%0.1f, V::%0.3fV}", adcgnd, v_adcgnd);
    				
    				// ain0 measurement
    				float ain0;
    				mcu_adc_read_1(NRF_SAADC_INPUT_AIN0, gain, oversample, &ain0);
    				
    				if(nrfx_saadc_is_busy())
    				{
    					printf("still busy!\n");
    					nrfx_saadc_is_busy();
    					{}
    				}
    
    				float v_ain0 = adc_to_voltage(ain0, gain);
    
    				printf(", ain0:{adc:%0.1f, V::%0.3fV}", ain0, v_ain0);
    
    				printf(", ain0_corrected:{adc:%0.1f, V::%0.3fV}", ain0-adcgnd, v_ain0-v_adcgnd);
    
    				// FIXME this is a patch for the fact the that ADC does not stop (reason unknonw).
    				nrfx_saadc_abort();
    				nrf_saadc_task_trigger(NRF_SAADC_TASK_STOP);
    
    				sep = ", ";
    			}
    
    			printf("\n");
    		}
    	}
    	
    	mcu_adc_uninit();
    	
    	do
    	{} 
    	while (1);
    }

    Oversampling is disabled ENABLE_CALIBRATION_BUG is defined: then reading the gnd voltage differs often between calibrations.
    When oversampling is turned on during calibration, the ground reading is almost always stable.

    I now use the gnd reading for an extra compensation. This works quite well but i do not know if reading the gnd as  shown is defined behavior.

  • Hello again,

    maarten v said:
    I restructured the program to improve showing the calibration problem.

    Thank you for pinpointing this.
    Could you possibly share with me the entire project from which this main.c file is taken from?
    Then I will run this on my end and attempt to replicate the behavior - I would love to get a better look at what might cause this change in offset calibration.

    maarten v said:
    I now use the gnd reading for an extra compensation. This works quite well but i do not know if reading the gnd as  shown is defined behavior.

    If you are asking whether measuring gnd by setting the pin to pulldown is defined behavior then the answer would have to be that it indeed is undefined, but I do not immediately see any reason why this would not work.
    If this is not what you were asking, please elaborate.

    maarten v said:
    I want a solution for this offset problem.

    How big is the fluctuation you are seeing now, in terms of bits of saadc output / mV? And, how frequently is it happening?

    maarten v said:
    Is this possible by setting the input to NRF_SAADC_INPUT_DISABLED, while setting the internal resistor network to NRF_SAADC_RESISTOR_PULLDOWN?

    No, you should instead point it to a specific pin, and then change the configuration of that pin after the channel configuration is complete.
    Alternatively you can also just connect the chosen pin to GND with a jumper, for the same result.

    Best regards,
    Karl

  • Thank you for the answers.

    Attached is the whole project.

    aadc-precision.zip

    I did run it on a nRF52840DK.

    Looking forward to your findings.

  • Hello again,

    Thank you for providing me with the project files.
    I have just gotten back from speaking with the SAADC experts, which confirmed for me that setting oversampling for AIN0 does actually increase accuracy of the offset calibration since it too will use oversampling during the calibration. The oversampling will in this case suppress thermal noise - increasing accuracy -, but the calibration will not be more accurate than its internal DAC. The calibration will also take longer, since more samplings will be done.

    What accuracy do you require for your project?

    Best regards,
    Karl

  • Thank you for the answer, and my apologies that it took some time.
    This is the confirmation i was looking for. Time is not an issue since it only has to be done incidentally and can be scheduled in between other actions.

    fyi: The remaining offset is compensated with a firmware calculated offset from an average of 1024 samples. This does the trick in keeping the signal stable when the temperature is not changed, so i guess it will also work when it does change.

Related