ADC value 20% too low.

I am working on a project using the nrf9160DK for initial firmware development. Trying out the SAADC i encountered that the measured values were always about 20% lower then expected.

For instance, when the measured inout is connected to 50% vdd, then the next code

#include <zephyr.h>
#include <sys/printk.h>
#include <drivers/gpio.h>
#include <drivers/adc.h>
#include <drivers/sensor.h>
#include <nrfx_saadc.h>

#define MY_AIN1 DT_PATH(ain1)

#define MY_ADC DT_NODELABEL(adc)

#define ADC_PIN NRF_SAADC_INPUT_AIN3 // P0.16
#define ADC_RESOLUTION 12
#define ADC_OVERSAMPLE 0

static struct adc_channel_cfg adc_pin_ref_vdd_cfg = 
{
	.gain = ADC_GAIN_1_4,
	.reference = ADC_REF_VDD_1_4, // reference direct to ADC_REF_VDD_1 isnot supported by nrf9160
	.acquisition_time = ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 10),
	.input_positive = ADC_PIN
};

static struct adc_channel_cfg adc_vdd_ref_vdd_cfg = 
{
	.gain = ADC_GAIN_1_4,
	.reference = ADC_REF_VDD_1_4, // reference direct to ADC_REF_VDD_1 isnot supported by nrf9160
	.acquisition_time = ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 10),
	.input_positive = NRF_SAADC_INPUT_VDD
};

static struct adc_channel_cfg adc_pin_ref_internal_cfg = 
{
	.gain = ADC_GAIN_1_4,
	.reference = ADC_REF_INTERNAL,
	.acquisition_time = ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 10),
	.input_positive = ADC_PIN
};

const char* err_str(int err)
{
	switch (-err)
	{
		case 0: return "OK";
		case EINVAL: return "EINVAL";
		case ENOMEM: return "ENOMEM";
		case ENOTSUP: return "ENOTSUP";
		case EBUSY: return "EBUSY";
		default: return "?";
	}
}


int read_adc(const struct device *device, 
		const struct adc_channel_cfg *cfg, 
		uint8_t resolution, nrf_saadc_oversample_t oversample)
{
	int16_t sample_buffer[1];

	struct adc_channel_cfg my_cfg = *cfg;
	my_cfg.channel_id = 0; // just use the first logical channel
	my_cfg.differential = 0;
	my_cfg.input_negative = NRF_SAADC_INPUT_DISABLED;

	struct adc_sequence sequence = 
	{
		.options = NULL,
		.channels = 0b00000001, // only measuring my_cfg.channel_id = 0
		.buffer = sample_buffer,
		.buffer_size = sizeof(sample_buffer),
		.resolution = resolution,
		.oversampling = oversample, // hw can oversample up to 256
		.calibrate = false
	};

	static unsigned count=0;
	if( count++ % 10 == 0 )
	{
		sequence.calibrate = true;
		printk("DEBUG Calibrate before measure\n");
	}

	// choosing the correct resolution for the channel is important for accuracy!
	// but changing the resolution between readings can cause offset deviations!
	int32_t err = adc_channel_setup(device, &my_cfg);
	if( err != 0 )
	{
		printk("ADC channel setup failed with %d: %s.\n", err, err_str(err));
		return -1;
	}

	// Read sequence of channels (fails if not supported by MCU)
	err = adc_read(device, &sequence);
	if (err != 0) 
	{
		printk("ADC reading failed with error %d: %s.\n", err, err_str(err));
		return -2;
	}

	return sample_buffer[0];
}



void main(void)
{
	// get board attributes of this analog input voltage divider:
	// get io-channels attribute from ain1
	uint8_t input = DT_IO_CHANNELS_INPUT(MY_AIN1);
	uint32_t output_ohm = DT_PROP(MY_AIN1, output_ohms);
	uint32_t full_ohm = DT_PROP(MY_AIN1, full_ohms);

	char hw_rev[8];
	memcpy( hw_rev, &NRF_FICR->INFO.VARIANT, 4);
	hw_rev[4] = 0;
	char hw_type[8];
	memcpy( hw_type, &NRF_FICR->INFO.DEVICETYPE, 4);
	hw_type[4] = 0;

	printk("nrf91 FICR HW revision: %s, type: ", hw_rev, hw_type);
	printk("ain1 channel = %d, output_ohm=%u, full_ohm=%u, factor=%f\n", input, output_ohm, full_ohm, (float)full_ohm/output_ohm);

	// see: https://developer.nordicsemi.com/nRF_Connect_SDK/doc/2.0.0/zephyr/build/dts/howtos.html
	const struct device *dev_adc = DEVICE_DT_GET(MY_ADC);
	// or runtime
	//const struct device *dev_adc = device_get_binding("ADC_0");

	if (!device_is_ready(dev_adc)) 
	{
		printk("ADC device not found\n");
		return;
	}

	while (1) 
	{
		int vdd_ref_vdd = read_adc(dev_adc, &adc_vdd_ref_vdd_cfg, ADC_RESOLUTION, NRF_SAADC_OVERSAMPLE_DISABLED);
		int pin_ref_vdd = read_adc(dev_adc, &adc_pin_ref_vdd_cfg, ADC_RESOLUTION, NRF_SAADC_OVERSAMPLE_DISABLED);
		int pin_ref_int = read_adc(dev_adc, &adc_pin_ref_internal_cfg, ADC_RESOLUTION, NRF_SAADC_OVERSAMPLE_DISABLED);

		int32_t mv_value = pin_ref_int;
		adc_raw_to_millivolts(600, adc_pin_ref_internal_cfg.gain, ADC_RESOLUTION, &mv_value);


		printk("ADC reading:{range:%d, pin:{ref_int:{adc:%d, Vpin:%0.3f}, ref_vdd:{adc:%d, _%%:%0.1f}, vdd:{adc:%d}}}\n", 
			0x01<<ADC_RESOLUTION, 
			pin_ref_int, mv_value/1000.0,
			pin_ref_vdd, 100.0*pin_ref_vdd/(float)(0x01<<ADC_RESOLUTION),
			vdd_ref_vdd);


		k_sleep(K_MSEC(1000));
	}
}

outputs the following values:

ADC reading:{range:4096, pin:{ref_int:{adc:1415, Vpin:0.829}, ref_vdd:{adc:1658, _%:40.5}, vdd:{adc:3307}}}
ADC reading:{range:4096, pin:{ref_int:{adc:1420, Vpin:0.832}, ref_vdd:{adc:1631, _%:39.8}, vdd:{adc:3320}}}

However, i expected vdd:{adc:4096} and corresponding values for the ref_vdd:0.9V and ref_int:50%

What is going wrong here?

Parents Reply
  • The project is finished. It was decided to use an external ADC.

    For another project using an nrf52840 we experience the same issue. It looks like the nrf9160  has the same ADC issues as the nrf52840.

    Turned out that the ADC is just not that accurate, and:
    - the ADC calibration is only valid for the exact same settings (gain, resolution), which is not documented.

    - the gain, especially the high gain (6x) is not llnear in the high value of the range (also not documented).

    - even with 12 bit resultution, the actual accuracy is only 8 bit (this is documented).

    For the project with the nrf52840 we now do a 'calibration' by  measuring the 'zero' value configuring the disconnect for the input and a  pull-down in the resistor ladder for each required configuration.

    We also  avoid the high input values by configuring a lower gain than possible..

    Now we get about 0.5% to 1% accuracy for our measurements, which is good enough, and use oversampling to remove noise.

    Hope this helps. For me, the ticket can ble closed.

Children
No Data
Related