Battery Voltage Measurement sample giving weird values

I'm trying to use the sample here: github.com/.../battery but I had some questions around how the voltage divider logic is handled. I'm getting weird readings.

In my final design, I'll have a battery hooked up like this:

But, until that's ready, I've been putting 400 mV into this pin using an ESP32. I've confirmed this with a multimeter.

Here is the relevant section of my .dts file:

```
// Voltage Divider Resistors:
// 2M Ohm
// 845k Ohm
vbatt {
compatible = "voltage-divider";
io-channels = <&adc 5>;
output-ohms = <845000>;
full-ohms = <(845000 + 2000000)>;
};
```

But, the output looks like this:

```
[00:03:08.021,392] <inf> app_name: raw 16380 ~ 599 mV => 2016 mV
[00:03:11.021,606] <inf> app_name: raw 16380 ~ 599 mV => 2016 mV
[00:03:14.021,820] <inf> app_name: raw 16380 ~ 599 mV => 2016 mV
[00:03:17.022,033] <inf> app_name: raw 16380 ~ 599 mV => 2016 mV
```

I've tweaked the gain and the resolution but the values are always wildly off what I would expect. When I change the voltage I'm supplying, the values change, but they never seem to be accurate. I would expect to see ~420 mV here.

I am using a BMD-300 Dev Kit that's based on the nRF52832. I'm using nRF Connect v2.1.2.

Apologies if this is a basic question - I'm very new to this!

Here is the full code below. It's very close to the sample:

#include "cx_battery.h"
#include <zephyr/kernel.h>
#include <zephyr/init.h>
#include <zephyr/drivers/adc.h>
#include <zephyr/drivers/sensor.h>
#include <drivers/gpio.h>
#include <logging/log.h>
#include <stdio.h>

#define LOG_MODULE_NAME app_name_battery
LOG_MODULE_REGISTER(LOG_MODULE_NAME);

#define VBATT DT_PATH(vbatt)
#define ZEPHYR_USER DT_PATH(zephyr_user)
#define BATTERY_ADC_GAIN ADC_GAIN_1

#define STACKSIZE 1024
#define THREAD0_PRIORITY 7

// Battery Voltage pin is the voltage level of the battery

static bool battery_ok;

int battery_measure_enable(bool enable);

static const struct battery_level_point levels[] = {
	/* "Curve" here eyeballed from captured data for the [Adafruit
	 * 3.7v 2000 mAh](https://www.adafruit.com/product/2011) LIPO
	 * under full load that started with a charge of 3.96 V and
	 * dropped about linearly to 3.58 V over 15 hours.  It then
	 * dropped rapidly to 3.10 V over one hour, at which point it
	 * stopped transmitting.
	 *
	 * Based on eyeball comparisons we'll say that 15/16 of life
	 * goes between 3.95 and 3.55 V, and 1/16 goes between 3.55 V
	 * and 3.1 V.
	 */

	{ 10000, 3950 },
	{ 625, 3550 },
	{ 0, 3100 },
};

static const char *now_str(void)
{
	static char buf[16]; /* ...HH:MM:SS.MMM */
	uint32_t now = k_uptime_get_32();
	unsigned int ms = now % MSEC_PER_SEC;
	unsigned int s;
	unsigned int min;
	unsigned int h;

	now /= MSEC_PER_SEC;
	s = now % 60U;
	now /= 60U;
	min = now % 60U;
	now /= 60U;
	h = now;

	snprintf(buf, sizeof(buf), "%u:%02u:%02u.%03u",
		 h, min, s, ms);
	return buf;
}

struct io_channel_config {
	uint8_t channel;
};

struct divider_config {
	struct io_channel_config io_channel;
	struct gpio_dt_spec power_gpios;
	uint32_t output_ohm;
	uint32_t full_ohm;
};

static const struct divider_config divider_config = {
	.io_channel = {
		DT_IO_CHANNELS_INPUT(VBATT),
	},
	.output_ohm = DT_PROP(VBATT, output_ohms),
	.full_ohm = DT_PROP(VBATT, full_ohms),
};

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(VBATT, okay)
	.adc = DEVICE_DT_GET(DT_IO_CHANNELS_CTLR(VBATT)),
#else
	.adc = DEVICE_DT_GET(DT_IO_CHANNELS_CTLR(ZEPHYR_USER)),
#endif
};

int cx_battery_init(void) {
	int rc = battery_measure_enable(true);

	if (rc != 0) {
		LOG_ERR("Failed initialize battery measurement: %d\n", rc);
		// TODO: Call a global function to halt initialization?
		return -1;
	}

    LOG_INF("Finished initializing Battery module successfully");
    return 0;
}

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,
	};

	*accp = (struct adc_channel_cfg){
		.gain = BATTERY_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;

	LOG_INF("Reference voltage: %d", ADC_REF_INTERNAL);

	rc = adc_channel_setup(ddp->adc, accp);
	// LOG_INF("Setup AIN%u got %d", iocp->channel, rc);

	return rc;
}

static int battery_setup(const struct device *arg)
{
	int rc = divider_setup();

	battery_ok = (rc == 0);
    if (battery_ok) {
        LOG_INF("Battery setup successfully");
    } else {
        LOG_ERR("Battery setup failed");
    }
	return rc;
}

SYS_INIT(battery_setup, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);

int battery_measure_enable(bool enable)
{
	int rc = -ENOENT;

	if (battery_ok) {
		const struct gpio_dt_spec *gcp = &divider_config.power_gpios;

		rc = 0;
		if (gcp->port) {
			rc = gpio_pin_set_dt(gcp, enable);
		}
	}
	return rc;
}

int battery_sample(void)
{
	int rc = -ENOENT;

	if (battery_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", ddp->raw, val, rc);
			} else {
				rc = val;
				LOG_INF("raw %u ~ %u mV", ddp->raw, val);
			}

		}
	}

	return rc;
}

void check_battery_life_indefinitely() {
	k_msleep(5000);
    while (!battery_ok) {
        k_msleep(1000);
    }

    LOG_INF("Finished waiting for battery setup");
	while (true) {
		int batt_mV = battery_sample();

		if (batt_mV < 0) {
			printk("Failed to read battery voltage: %d\n",
			       batt_mV);
			break;
		}

		k_msleep(3000);
	}
	printk("Disable: %d\n", battery_measure_enable(false));
}

K_THREAD_DEFINE(thread0_id, STACKSIZE, check_battery_life_indefinitely, NULL, NULL, NULL,
		THREAD0_PRIORITY, 0, 0);

  • Hello Mike,

    Thank you for contacting DevZone at NordicSemi.

    As per you provided schematic, you are connecting the voltage divider output to the P0.29. Voltage divider is powered by the VBAT and the output (voltage divider output) is obtained from VBAT_DIV node.

    However, in your dts, I can see that you are obtaining the output from across the 845K resistor, while the output as per your schematic and expectations should be coming from across 2 Mega Ohm resistor.

    Therefore, please update "output-ohms" value from 845k to 2M.

    Hope it will work. By the way, In your code, I have seen that a line is missing as compared with the provided sample:

    .power_gpios = GPIO_DT_SPEC_GET_OR(VBATT, power_gpios, {}),

    Was this removed on purpose?

    Regards,
    Naeem

  • Hi Naeem,

    Thank you for getting back to me. I changed the VBatt node to what you suggested:

    vbatt {
        compatible = "voltage-divider";
        io-channels = <&adc 5>;
        output-ohms = <2000000>; // TODO: Verify this value
        full-ohms = <(845000 + 2000000)>; // TODO: Verify these values
    };

    I removed the line about power-gpios because I can't find any documentation around what this does. The example has the following in the .dts file:

    power-gpios = <&sx1509b 4 0>;

    But this doesn't compile and the example doesn't provide guidance on how to supply a value here. Is this required? What value should I use for my setup?

    Unfortunately this didn't fix my issue: I am still getting the following output when supplying 420mV to the pin:

    ```
    [00:07:53.140,258] <inf> stylus_battery: raw 16380 ~ 599 mV => 852 mV
    ```

    If it's helpful: when I remove the 420 mV to the pin, I get output like this:

    ```
    [00:08:35.151,794] <inf> stylus_battery: raw 6551 ~ 239 mV => 339 mV
    ```

    Thank you for any help you can provide.

  • When I run the samples/boards/nrf/battery sample with no changes (and the /vbatt node I shared above), I get the following:

    ```

    [0:00:05.271]: 3907 mV; 8992 pptt
    [0:00:10.213]: 3850 mV; 7656 pptt
    [0:00:15.157]: 3889 mV; 8570 pptt
    [0:00:20.100]: 3874 mV; 8218 pptt
    [0:00:25.042]: 3864 mV; 7984 pptt
    [0:00:29.985]: 3873 mV; 8195 pptt

    ```

    The only difference I see is the sample is using ADC_GAIN_1_6. But, my multimeter is saying the pin is receiving 420 mV, so a reading of ~3850 mV doesn't make sense to me. Please let me know if I'm missing something.

  • When I disconnect pin 29 from 420 mV and connect it to ground, the readings change to:

    ```

    [0:04:52.068]: 355 mV; 0 pptt
    [0:04:57.013]: 352 mV; 0 pptt
    [0:05:01.957]: 379 mV; 0 pptt
    [0:05:06.902]: 368 mV; 0 pptt

    ```

    Which is all very confusing!

  • It turns out I had a loose wire issue. Fixing that fixed my problem. Thank you for the tip about the voltage divider - that solved an issue before it presented itself!

Related