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);

Related