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 Children
  • Thank you for providing the code.

    Could you confirm that you saw the same results with the added offset calibration at the beginning?
    Could you also let me know which nRF Connect SDK version that this code is made for?

    Best regards,
    Karl

  • > Could you confirm that you saw the same results with the added offset calibration at the beginning?

    I am not sure what you mean: before ADC (offset) calibration the offset error is different but the gain error is not.

    > Could you also let me know which nRF Connect SDK version that this code is made for?

    nrfcnonnect SDK version 2.0.0

  • maarten v said:
    nrfcnonnect SDK version 2.0.0

    Thank you for confirming this.

    maarten v said:
    I am not sure what you mean: before ADC (offset) calibration the offset error is different but the gain error is not.

    What do you mean by this, could you provide some examples of what you are seeing with and without the offset calibration at the beginning?

    Best regards,
    Karl

  • I am sorry to respond so slow: i am only working 2 days a week on this project.

    maarten v said:
    I am not sure what you mean: before ADC (offset) calibration the offset error is different but the gain error is not.

    What do you mean by this, could you provide some examples of what you are seeing with and without the offset calibration at the beginning?

    I don't a an exact output right now. The offset calibration is of no consequence for this mentioned problem: it only differs a few millivolts before and after calibration.

    The gain error i mention was that i assumed that the full ADC range would be from gnd to vdd_io (1.8V) with the chosen reference, but i observe something completely different (as mentioned earlier).

    So when measuring 0.5Vdd_io with 12 bit resolution, i would expect an ADC value of about 2048 but i get something like 1415 (as mentioned in the first post). ADC calibration does not change that significantly.

  • Hello again,

    Thank you for your extreme patience with this.

    Thank you for confirming regarding the calibration.
    This sounds very strange indeed - I too would expect your raw ADC value to come out at the top of the resolution range, in this case.

    Have you had a chance to scope the VDD, using an oscilloscope?
    Could you try to short the pin used for measuring to GND directly, and see what the sample reads then? For good measure, could you also check what the PSELP and CONFIG register of the SAADC is set to, right before the sampling is triggered?

    Best regards,
    Karl

Related