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
  • Hello,

    That looks strange indeed - would you mind detailing how you are powering the nRF9160 DK?
    Could you also explicitly perform an offset calibration, to see if this has any influence on the measurements you're seeing?

    Best regards,
    Karl

  • The board power is just via the usb debug connection J4. 

    Measuring the vdd gives 1.8V and measuring then 5V gives indeed 5V.
    The board version: PCA10090 1.1.0.
    I tried this on two nrf9160dk boards with the same result.
    Edit: i use a resistor ladder of 2x30k to divide the vdd by 2 to measure the voltage with the board analog input..
  • Hello again,

    maarten v said:

    We have a simple work-around: just measure the vdd and don't expect it to be full-range. Then use that to calculate a correction for actual measured value.

    I'll come back later on this topic when i have more time.

    While this might be a pragmatic solution - and I completely understand the need to be pragmatic about things like this sometimes - I would want to understand the root cause of this, in order to guarantee that you wont run into any other unexpected issues in the future.

    Please let me know if you get a chance to take a look at the questions in my previous comment, or if you have any updates on this issue.

    Best regards,
    Karl

  • I finally got time to take a further look at this. It looks like the measement with the nrf9160dk is off, but only when the vdd_io is set to 1.8V.

    I modified the test program to show this better:

    #include <zephyr.h>
    #include <sys/printk.h>
    #include <drivers/gpio.h>
    #include <drivers/adc.h>
    #include <drivers/sensor.h>
    #include <nrfx_saadc.h>
    #include <string.h>
    
    #include <logging/log.h>
    
    
    LOG_MODULE_REGISTER(main,LOG_LEVEL_DBG);
    
    #define MY_ADC DT_NODELABEL(adc)
    #define ADC_PIN NRF_SAADC_INPUT_AIN3 // P0.16
    #define ADC_RESOLUTION 14
    
    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 char *tag, 
    		const struct device *device, 
    		uint8_t input,
    		enum adc_reference ref,
    		bool calibrate )
    {
    	struct adc_channel_cfg cfg = 
    	{
    		.gain = ADC_GAIN_1_6, // enough to measure VDD referenced to VDD_1_4 at vdd_io set to 3V
    		.reference = ref, 
    		.acquisition_time = ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 10),
    		.input_positive = input,
    
    		.input_negative = NRF_SAADC_INPUT_DISABLED,
    		.differential = 0,
    		.channel_id = 0, // just use the first logical channel
    	};
    
    	// 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, &cfg);
    	if( err != 0 )
    	{
    		LOG_ERR("ADC channel setup failed with %d: %s.\n", err, err_str(err));
    		return -1;
    	}
    
    	int16_t sample_buffer[1] = {0};
    	struct adc_sequence sequence = 
    	{
    		.options = NULL,
    		.channels = 0b00000001, // only measuring cfg.channel_id = 0
    		.buffer = sample_buffer,
    		.buffer_size = sizeof(sample_buffer),
    		.resolution = ADC_RESOLUTION,
    		.oversampling = NRF_SAADC_OVERSAMPLE_256X,
    		.calibrate = calibrate
    	};
    
    	if( calibrate )
    	{
    		LOG_DBG("calibrate ADC");
    	}
    
    	// Read sequence of channels (fails if not supported by MCU)
    	err = adc_read(device, &sequence);
    	if (err != 0) 
    	{
    		LOG_ERR("ADC reading failed with error %d: %s.", err, err_str(err));
    		return -2; 
    	}
    
    	volatile float part_from_range = sample_buffer[0] / (float)(1<<ADC_RESOLUTION);
    	#define VDD_IO_mV 3000
    	volatile int32_t ref_mv = ref == ADC_REF_VDD_1_4 ? VDD_IO_mV/4 : adc_ref_internal(device);
    
    	int v_meas_mv = sample_buffer[0];
    	__unused int result = adc_raw_to_millivolts(ref_mv, cfg.gain, ADC_RESOLUTION, &v_meas_mv);
    	__ASSERT( result == 0, "could not convert to millivolts" );
    
    	if (tag)
    	{
    		LOG_INF("%s:{adc:%hd, part_range_%%%%:%d, ref_mv:%d, measured_mv:%d}", 
    			tag, sample_buffer[0], 
    			(int)(1000*part_from_range), ref_mv, v_meas_mv);
    	}
    
    	return sample_buffer[0];
    }
    
    
    void main(void)
    {
    	char hw_rev[8];
    	memcpy( hw_rev, (char*)&NRF_FICR->INFO.VARIANT, 4);
    	hw_rev[4] = 0;
    	char hw_type[8];
    	memcpy( hw_type, (char*)&NRF_FICR->INFO.DEVICETYPE, 4);
    	hw_type[4] = 0;
    
    	LOG_INF("nrf91 FICR HW revision: %s, type: %s", hw_rev, hw_type);
    
    	// 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)) 
    	{
    		LOG_ERR("ADC device not found\n");
    		return;
    	}
    
    	// calibrate once:
    	read_adc(NULL, dev_adc, NRF_SAADC_INPUT_VDD, ADC_REF_INTERNAL, true);
    
    	while (1) 
    	{
    		LOG_INF("ADC_reading:");
    		LOG_INF("{");
    		LOG_INF("  full_range:%d,\n", 0x01<<ADC_RESOLUTION);
    		
    		volatile __unused int vdd_ref_vdd = read_adc("  vdd_ref_vdd", dev_adc, NRF_SAADC_INPUT_VDD, ADC_REF_VDD_1_4, false);
    		volatile __unused int vdd_ref_int = read_adc(", vdd_ref_int", dev_adc, NRF_SAADC_INPUT_VDD, ADC_REF_INTERNAL, false);
    
    		// measure 0.5 Vdd at pin p0.16 (using 2 same resistors in sweries between GND and VDD)
    		volatile __unused int p016_ref_vdd = read_adc(", p0_16_ref_vdd", dev_adc, ADC_PIN, ADC_REF_VDD_1_4, false);
    		volatile __unused int p016_ref_int = read_adc(", p0_16_ref_int", dev_adc, ADC_PIN, ADC_REF_INTERNAL, false);
    
    		LOG_INF("},");
    
    		k_sleep(K_MSEC(300));
    	}
    }
    

    With the board set to Vdd_io=3V this logs:

    *** Booting Zephyr OS build v3.1.99-ncs1  ***
    
    [00:00:03.074,737] <inf> main: nrf91 FICR HW revision: 0BAA, type: 
    [00:00:03.074,768] <dbg> main: read_adc: calibrate ADC
    [00:00:03.095,123] <inf> main: ADC_reading:
    [00:00:03.095,153] <inf> main: {
    [00:00:03.095,184] <inf> main:   full_range:16384,
    
    [00:00:03.098,144] <inf> main:   vdd_ref_vdd:{adc:10957, part_range_%%:668, ref_mv:750, measured_mv:3009}
    [00:00:03.101,104] <inf> main: , vdd_ref_int:{adc:13626, part_range_%%:831, ref_mv:600, measured_mv:2993}
    [00:00:03.104,095] <inf> main: , p0_16_ref_vdd:{adc:5451, part_range_%%:332, ref_mv:750, measured_mv:1497}
    [00:00:03.107,055] <inf> main: , p0_16_ref_int:{adc:6782, part_range_%%:413, ref_mv:600, measured_mv:1490}
    [00:00:03.107,086] <inf> main: },
    

    With the board set to Vdd_io=1.8V this logs:

    *** Booting Zephyr OS build v3.1.99-ncs1  ***
    
    [00:00:00.959,930] <inf> main: nrf91 FICR HW revision: 0BAA, type: 
    [00:00:00.959,930] <dbg> main: read_adc: calibrate ADC
    [00:00:00.980,316] <inf> main: ADC_reading:
    [00:00:00.980,346] <inf> main: {
    [00:00:00.980,346] <inf> main:   full_range:16384,
    
    [00:00:00.983,337] <inf> main:   vdd_ref_vdd:{adc:8885, part_range_%%:542, ref_mv:450, measured_mv:1464}
    [00:00:00.986,297] <inf> main: , vdd_ref_int:{adc:7597, part_range_%%:463, ref_mv:600, measured_mv:1669}
    [00:00:00.989,257] <inf> main: , p0_16_ref_vdd:{adc:4439, part_range_%%:270, ref_mv:450, measured_mv:731}
    [00:00:00.992,218] <inf> main: , p0_16_ref_int:{adc:3799, part_range_%%:231, ref_mv:600, measured_mv:834}
    [00:00:00.992,248] <inf> main: },
    

    To me it looks like something is wrong with the nrf9160DK.

    Our own PCBA works with 3 vdd_io (no defined input here) so executing the above gives:

    *** Booting Zephyr OS build v3.1.99-ncs1  ***
    
    [00:00:02.464,172] <inf> main: nrf91 FICR HW revision: 0BAA, type: 
    [00:00:02.464,172] <dbg> main: read_adc: calibrate ADC
    [00:00:02.484,008] <inf> main: ADC_reading:
    [00:00:02.484,039] <inf> main: {
    [00:00:02.484,069] <inf> main:   full_range:16384,
    
    [00:00:02.486,968] <inf> main:   vdd_ref_vdd:{adc:10924, part_range_%%:666, ref_mv:750, measured_mv:3000}
    [00:00:02.489,837] <inf> main: , vdd_ref_int:{adc:13865, part_range_%%:846, ref_mv:600, measured_mv:3046}
    

    Conclusion: no problem with the code but with the nrf9160DK PCBA working on vdd_io=1.8V

  • Hello,

    Karl asked if I could take over this ticket, because he will be out of office for a while.

    I have seen something like this before, but that DevZone ticket "died". I have reported this internally, and I will keep you posted when I receive a reply.

    Best regards,

    Edvin

  • Thanks, i am looking forward to know the actual cause of this.

  • Hi, Is there any update on the issue yet? We have a similar issue. ADC works fine on VDD_IO 3V, but not on VDD_IO 1,8V.

Reply Children
No Data
Related