ADC Reading from Multiple Voltage Dividers/Multiple Channels on nRF9160 w/NCS

Trying to read the voltage of two ADC inputs, each with different voltage divider circuits. 

Using zephyr/samples/boards/nrf/battery, I can get the correct voltage if I pick one input or the other. However, if I copy battery.c to battery2.c changing the ADC input, I get incorrect values from both inputs. 

If I use zephyr/samples/drivers/adc, the wrong voltages are displayed and changing one input affects both values. (Should be 15V and 3.9V)

ADC reading:
- adc@e000, channel 4: 8779 = 1928 mV
- adc@e000, channel 6: 8745 = 1921 mV
ADC reading:
- adc@e000, channel 4: 8790 = 1931 mV
- adc@e000, channel 6: 8740 = 1920 mV
ADC reading:
- adc@e000, channel 4: 8774 = 1927 mV
- adc@e000, channel 6: 8738 = 1919 mV

DTS:

/ {
	zephyr,user {
		io-channels = <&adc 4>, <&adc 6>;
	};
};

&adc {
	status = "okay";
   #address-cells = <1>;
	#size-cells = <0>;
   channel@4 {
		reg = <4>;
		zephyr,gain = "ADC_GAIN_1_6";
		zephyr,reference = "ADC_REF_INTERNAL";
		zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
		zephyr,input-positive = <NRF_SAADC_VDD>;
		zephyr,resolution = <14>;
		zephyr,oversampling = <8>;
	};
   channel@6 {
		reg = <6>;
		zephyr,gain = "ADC_GAIN_1_6";
		zephyr,reference = "ADC_REF_INTERNAL";
		zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
		zephyr,input-positive = <NRF_SAADC_VDD>;
		zephyr,resolution = <14>;
		zephyr,oversampling = <8>;
	};
};

/ {
	vbatt {
		compatible = "voltage-divider";
		io-channels = <&adc 4>;
		output-ohms = <100000>;
		full-ohms = <(100000 + 100000)>;
		power-gpios = <&gpio0 18 GPIO_ACTIVE_HIGH>;
	};
	vin {
		compatible = "voltage-divider";
		io-channels = <&adc 6>;
		output-ohms = <100000>;
		full-ohms = <(100000 + 12000)>;
	};
};

Any idea why the Zephyr ADC is displaying the wrong values or how to read multiple inputs with the nRF example?

  • Hi,

     

    You should not have two root nodes in the same overlay.

    Normally, I would recommend that you combine them, but you are actually providing both ways of running the specific sample:

    https://github.com/nrfconnect/sdk-zephyr/blob/v3.2.99-ncs2/samples/boards/nrf/battery/README.rst

     

    You should choose to either use "vbatt" and "vin" nodes, or set it up using the zephyr,user defined section.

    The "zephyr,user" approach will not include any voltage divider information.

    However, if I copy battery.c to battery2.c changing the ADC input, I get incorrect values from both inputs. 

    Can you share how you interface the other voltage divider?

     

    Kind regards,

    håkon

  • The problem with using "vbatt" or "vin" is that we are trying to measure two different voltages. One is for measuring the voltage of the internal battery (3.7V) and the other is for measuring up to 30V. So we need two different voltage divider circuits. What is the recommended way to measure both voltages?

  • My point was that you should either declare the "voltage-divider" or the "zephyr,user" declaration, not both.

    However, if I copy battery.c to battery2.c changing the ADC input, I get incorrect values from both inputs. 

    Can you share the battery2.c file?

     

    Kind regards,

    Håkon

  • Battery2 is vin.c:

    /*
     * Copyright (c) 2018-2019 Peter Bigot Consulting, LLC
     * Copyright (c) 2019-2020 Nordic Semiconductor ASA
     *
     * SPDX-License-Identifier: Apache-2.0
     */
    
    #include <math.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    #include <zephyr/kernel.h>
    #include <zephyr/init.h>
    #include <zephyr/drivers/gpio.h>
    #include <zephyr/drivers/adc.h>
    #include <zephyr/drivers/sensor.h>
    #include <zephyr/logging/log.h>
    
    #include "vin.h"
    
    LOG_MODULE_REGISTER(VIN, LOG_LEVEL_INF);
    
    #define VIN DT_PATH(vin)
    #define ZEPHYR_USER DT_PATH(zephyr_user)
    
    #ifdef CONFIG_BOARD_THINGY52_NRF52832
    /* This board uses a divider that reduces max voltage to
     * reference voltage (600 mV).
     */
    #define VIN_ADC_GAIN ADC_GAIN_1
    #else
    /* Other boards may use dividers that only reduce vin voltage to
     * the maximum supported by the hardware (3.6 V)
     */
    #define VIN_ADC_GAIN ADC_GAIN_1_6
    #endif
    
    struct io_channel_config {
    	uint8_t channel;
    };
    
    struct divider_config {
    	struct io_channel_config io_channel;
    	struct gpio_dt_spec power_gpios;
    	/* output_ohm is used as a flag value: if it is nonzero then
    	 * the vin is measured through a voltage divider;
    	 * otherwise it is assumed to be directly connected to Vdd.
    	 */
    	uint32_t output_ohm;
    	uint32_t full_ohm;
    };
    
    static const struct divider_config divider_config = {
    #if DT_NODE_HAS_STATUS(VIN, okay)
    	.io_channel = {
    		DT_IO_CHANNELS_INPUT(VIN),
    	},
    	.power_gpios = GPIO_DT_SPEC_GET_OR(VIN, power_gpios, {}),
    	.output_ohm = DT_PROP(VIN, output_ohms),
    	.full_ohm = DT_PROP(VIN, full_ohms),
    #else /* /vin exists */
    	.io_channel = {
    		DT_IO_CHANNELS_INPUT(ZEPHYR_USER),
    	},
    #endif /* /vin exists */
    };
    
    struct divider_data {
    	const struct device *adc;
    	struct adc_channel_cfg adc_cfg;
    	struct adc_sequence adc_seq;
    	int16_t raw;
    };
    static struct divider_data divider_data = {
    #if DT_NODE_HAS_STATUS(VIN, okay)
    	.adc = DEVICE_DT_GET(DT_IO_CHANNELS_CTLR(VIN)),
    #else
    	.adc = DEVICE_DT_GET(DT_IO_CHANNELS_CTLR(ZEPHYR_USER)),
    #endif
    };
    
    static int divider_setup(void)
    {
    	const struct divider_config *cfg = &divider_config;
    	const struct io_channel_config *iocp = &cfg->io_channel;
    	const struct gpio_dt_spec *gcp = &cfg->power_gpios;
    	struct divider_data *ddp = &divider_data;
    	struct adc_sequence *asp = &ddp->adc_seq;
    	struct adc_channel_cfg *accp = &ddp->adc_cfg;
    	int rc;
    
    	if (!device_is_ready(ddp->adc)) {
    		LOG_ERR("ADC device is not ready %s", ddp->adc->name);
    		return -ENOENT;
    	}
    
    	if (gcp->port) {
    		if (!device_is_ready(gcp->port)) {
    			LOG_ERR("%s: device not ready", gcp->port->name);
    			return -ENOENT;
    		}
    		rc = gpio_pin_configure_dt(gcp, GPIO_OUTPUT_INACTIVE);
    		if (rc != 0) {
    			LOG_ERR("Failed to control feed %s.%u: %d",
    				gcp->port->name, gcp->pin, rc);
    			return rc;
    		}
    	}
    
    	*asp = (struct adc_sequence){
    		.channels = BIT(0),
    		.buffer = &ddp->raw,
    		.buffer_size = sizeof(ddp->raw),
    		.oversampling = 4,
    		.calibrate = true,
    	};
    
    #ifdef CONFIG_ADC_NRFX_SAADC
    	*accp = (struct adc_channel_cfg){
    		.gain = VIN_ADC_GAIN,
    		.reference = ADC_REF_INTERNAL,
    		.acquisition_time = ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 40),
    	};
    
    	if (cfg->output_ohm != 0) {
    		accp->input_positive = SAADC_CH_PSELP_PSELP_AnalogInput0
    			+ iocp->channel;
    	} else {
    		accp->input_positive = SAADC_CH_PSELP_PSELP_VDD;
    	}
    
    	asp->resolution = 14;
    #else /* CONFIG_ADC_var */
    #error Unsupported ADC
    #endif /* CONFIG_ADC_var */
    
    	rc = adc_channel_setup(ddp->adc, accp);
    	LOG_INF("Setup AIN%u got %d", iocp->channel, rc);
    
    	return rc;
    }
    
    static bool vin_ok = false;
    
    static int vin_setup(const struct device *arg)
    {
    	int rc = divider_setup();
    
    	vin_ok = (rc == 0);
    	LOG_INF("vin setup: %d %d", rc, vin_ok);
    	return rc;
    }
    
    SYS_INIT(vin_setup, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);
    
    int vin_sample(void)
    {
    	int rc = -ENOENT;
    
    	if (vin_ok) {
    		struct divider_data *ddp = &divider_data;
    		const struct divider_config *dcp = &divider_config;
    		struct adc_sequence *sp = &ddp->adc_seq;
    
    		rc = adc_read(ddp->adc, sp);
    		sp->calibrate = false;
    		if (rc == 0) {
    			int32_t val = ddp->raw;
    
    			adc_raw_to_millivolts(adc_ref_internal(ddp->adc),
    					      ddp->adc_cfg.gain,
    					      sp->resolution,
    					      &val);
    
    			if (dcp->output_ohm != 0) {
    				rc = val * (uint64_t)dcp->full_ohm
    					/ dcp->output_ohm;
    				//LOG_INF("raw %u ~ %u mV => %d mV\n",
    				//	ddp->raw, val, rc);
    			} else {
    				rc = val;
    				//LOG_INF("raw %u ~ %u mV\n", ddp->raw, val);
    			}
    		}
    	}
    
    	return rc;
    }
    
    unsigned int vin_level_pptt(unsigned int vin_mV,
    				const struct vin_level_point *curve)
    {
    	const struct vin_level_point *pb = curve;
    
    	if (vin_mV >= pb->lvl_mV) {
    		/* Measured voltage above highest point, cap at maximum. */
    		return pb->lvl_pptt;
    	}
    	/* Go down to the last point at or below the measured voltage. */
    	while ((pb->lvl_pptt > 0)
    	       && (vin_mV < pb->lvl_mV)) {
    		++pb;
    	}
    	if (vin_mV < pb->lvl_mV) {
    		/* Below lowest point, cap at minimum */
    		return pb->lvl_pptt;
    	}
    
    	/* Linear interpolation between below and above points. */
    	const struct vin_level_point *pa = pb - 1;
    
    	return pb->lvl_pptt
    	       + ((pa->lvl_pptt - pb->lvl_pptt)
    		  * (vin_mV - pb->lvl_mV)
    		  / (pa->lvl_mV - pb->lvl_mV));
    }
    
    unsigned int vin_mV() {
    
       //k_mutex_lock(&battMutex, K_FOREVER);
    
    	int vin_mV = vin_sample();
    
    	if (vin_mV < 0) {
    		LOG_INF("Failed to read vin voltage: %d\n", vin_mV);
    	} else {
    
    		//unsigned int vin_pptt = vin_level_pptt(vin_mV, levels);
    
    		//LOG_INF("%d mV; %u pptt\n", vin_mV, vin_pptt);
    		//LOG_INF("%d mV", vin_mV);
    
    		/* Burn vin so you can see that this works over time */
    		//k_busy_wait(5 * USEC_PER_SEC);
    
    	}
       //k_mutex_unlock(&battMutex);
    	return vin_mV;
    }

  • Hi,

     

    Thanks for sharing.

    I think I found your problem, which is that you need to re-initialize the channel setup for each time you sample "battery" and "vin", if not the former channel will be used.

     

    Can you please try to add this to the top of vin.c::vin_sample():

    int vin_sample(...)
    {
        int rc = divider_setup();
        ...
    }

     

    And similarly to the battery.c::battery_sample():

    int battery_sample(void)
    {
    	int rc = divider_setup();
    	...

     

    And see if this works on your end?

     

    PS: I think you got the resistors mixed up in your divider, looks like you want this configuration:

    / {
            vbatt {
                    compatible = "voltage-divider";
                    io-channels = <&adc 4>;
                    output-ohms = <100000>;
                    full-ohms = <(100000 + 100000)>;
            };
            vin {
                    compatible = "voltage-divider";
                    io-channels = <&adc 6>;
                    output-ohms = <12000>;
                    full-ohms = <(100000 + 12000)>;
            };
    };

     

    Kind regards,

    Håkon

Related