From d715fbba63a74957e44de7bd5ebd591de7a4db6e Mon Sep 17 00:00:00 2001 From: "Peter A. Bigot" Date: Mon, 21 Oct 2019 13:34:04 -0500 Subject: [PATCH] samples: boards: nrf: add battery measurement sample Use the voltage divider devicetree binding to demonstrate measurement of battery voltage for two Nordic-based boards that have the necessary circuitry. Signed-off-by: Peter A. Bigot --- samples/boards/nrf/battery/CMakeLists.txt | 9 + samples/boards/nrf/battery/prj.conf | 1 + samples/boards/nrf/battery/sample.yaml | 7 + samples/boards/nrf/battery/src/battery.c | 214 ++++++++++++++++++++++ samples/boards/nrf/battery/src/battery.h | 53 ++++++ samples/boards/nrf/battery/src/main.c | 86 +++++++++ 6 files changed, 370 insertions(+) create mode 100644 samples/boards/nrf/battery/CMakeLists.txt create mode 100644 samples/boards/nrf/battery/prj.conf create mode 100644 samples/boards/nrf/battery/sample.yaml create mode 100644 samples/boards/nrf/battery/src/battery.c create mode 100644 samples/boards/nrf/battery/src/battery.h create mode 100644 samples/boards/nrf/battery/src/main.c diff --git a/samples/boards/nrf/battery/CMakeLists.txt b/samples/boards/nrf/battery/CMakeLists.txt new file mode 100644 index 00000000000..9fb519f8a8b --- /dev/null +++ b/samples/boards/nrf/battery/CMakeLists.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.13.1) + +include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) +project(battery) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/samples/boards/nrf/battery/prj.conf b/samples/boards/nrf/battery/prj.conf new file mode 100644 index 00000000000..488a81dca52 --- /dev/null +++ b/samples/boards/nrf/battery/prj.conf @@ -0,0 +1 @@ +CONFIG_ADC=y diff --git a/samples/boards/nrf/battery/sample.yaml b/samples/boards/nrf/battery/sample.yaml new file mode 100644 index 00000000000..502fab0dfba --- /dev/null +++ b/samples/boards/nrf/battery/sample.yaml @@ -0,0 +1,7 @@ +sample: + name: Measure battery voltage +tests: + sample.boards.nrf.battery: + build_only: true + platform_whitelist: particle_xenon nrf52_pca20020 + tags: battery diff --git a/samples/boards/nrf/battery/src/battery.c b/samples/boards/nrf/battery/src/battery.c new file mode 100644 index 00000000000..2cd15829a6e --- /dev/null +++ b/samples/boards/nrf/battery/src/battery.c @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2018-2019 Peter Bigot Consulting, LLC + * Copyright (c) 2019 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "battery.h" + +LOG_MODULE_REGISTER(BATTERY, CONFIG_ADC_LOG_LEVEL); + +#ifdef CONFIG_BOARD_NRF52_PCA20020 +/* This board uses a divider that reduces max voltage to + * reference voltage (600 mV). + */ +#define BATTERY_ADC_GAIN ADC_GAIN_1 +#else +/* Other boards may use dividers that only reduce battery voltage to + * the maximum supported by the hardware (3.6 V) + */ +#define BATTERY_ADC_GAIN ADC_GAIN_1_6 +#endif + +struct io_channel_config { + const char *label; + u8_t channel; +}; + +struct gpio_channel_config { + const char *label; + u8_t pin; + u8_t flags; +}; + +struct divider_config { + const struct io_channel_config io_channel; + const struct gpio_channel_config power_gpios; + const u32_t output_ohm; + const u32_t full_ohm; +}; + +static const struct divider_config divider_config = { + .io_channel = DT_VOLTAGE_DIVIDER_VBATT_IO_CHANNELS, +#ifdef DT_VOLTAGE_DIVIDER_VBATT_POWER_GPIOS + .power_gpios = DT_VOLTAGE_DIVIDER_VBATT_POWER_GPIOS, +#endif + .output_ohm = DT_VOLTAGE_DIVIDER_VBATT_OUTPUT_OHMS, + .full_ohm = DT_VOLTAGE_DIVIDER_VBATT_FULL_OHMS, +}; + +struct divider_data { + struct device *adc; + struct device *gpio; + struct adc_channel_cfg adc_cfg; + struct adc_sequence adc_seq; + s16_t raw; +}; +static struct divider_data divider_data; + +static int divider_setup(void) +{ + const struct divider_config *cfg = ÷r_config; + const struct io_channel_config *iocp = &cfg->io_channel; + const struct gpio_channel_config *gcp = &cfg->power_gpios; + struct divider_data *ddp = ÷r_data; + struct adc_sequence *asp = &ddp->adc_seq; + struct adc_channel_cfg *accp = &ddp->adc_cfg; + int rc; + + ddp->adc = device_get_binding(iocp->label); + if (ddp->adc == NULL) { + LOG_ERR("Failed to get ADC %s", iocp->label); + return -ENOENT; + } + + if (gcp->label) { + ddp->gpio = device_get_binding(gcp->label); + if (ddp->gpio == NULL) { + LOG_ERR("Failed to get GPIO %s", gcp->label); + return -ENOENT; + } + rc = gpio_pin_write(ddp->gpio, gcp->pin, 0); + if (rc == 0) { + rc = gpio_pin_configure(ddp->gpio, gcp->pin, + gcp->flags | GPIO_DIR_OUT); + } + if (rc != 0) { + LOG_ERR("Failed to control feed %s.%u: %d", + gcp->label, 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 = BATTERY_ADC_GAIN, + .reference = ADC_REF_INTERNAL, + .acquisition_time = ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 40), + .input_positive = SAADC_CH_PSELP_PSELP_AnalogInput0 + iocp->channel, + }; + + 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 battery_ok; + +static int battery_setup(struct device *arg) +{ + int rc = divider_setup(); + + battery_ok = (rc == 0); + LOG_INF("Battery setup: %d %d", rc, battery_ok); + 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 divider_data *ddp = ÷r_data; + const struct gpio_channel_config *gcp = ÷r_config.power_gpios; + + rc = 0; + if (ddp->gpio) { + rc = gpio_pin_write(ddp->gpio, gcp->pin, enable); + } + } + return rc; +} + +int battery_sample(void) +{ + int rc = -ENOENT; + + if (battery_ok) { + struct divider_data *ddp = ÷r_data; + const struct divider_config *dcp = ÷r_config; + struct adc_sequence *sp = &ddp->adc_seq; + + rc = adc_read(ddp->adc, sp); + sp->calibrate = false; + if (rc == 0) { + s32_t val = ddp->raw; + + adc_raw_to_millivolts(adc_ref_internal(ddp->adc), + ddp->adc_cfg.gain, + sp->resolution, + &val); + rc = val * (u64_t)dcp->full_ohm / dcp->output_ohm; + LOG_INF("raw %u ~ %u mV => %d mV\n", + ddp->raw, val, rc); + } + } + + return rc; +} + +unsigned int battery_level_pptt(unsigned int batt_mV, + const struct battery_level_point *curve) +{ + const struct battery_level_point *pb = curve; + + if (batt_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) + && (batt_mV < pb->lvl_mV)) { + ++pb; + } + if (batt_mV < pb->lvl_mV) { + /* Below lowest point, cap at minimum */ + return pb->lvl_pptt; + } + + /* Linear interpolation between below and above points. */ + const struct battery_level_point *pa = pb - 1; + + return pb->lvl_pptt + + ((pa->lvl_pptt - pb->lvl_pptt) + * (batt_mV - pb->lvl_mV) + / (pa->lvl_mV - pb->lvl_mV)); +} diff --git a/samples/boards/nrf/battery/src/battery.h b/samples/boards/nrf/battery/src/battery.h new file mode 100644 index 00000000000..0ffaae8d5d2 --- /dev/null +++ b/samples/boards/nrf/battery/src/battery.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2018-2019 Peter Bigot Consulting, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef APPLICATION_BATTERY_H_ +#define APPLICATION_BATTERY_H_ + +/** Enable or disable measurement of the battery voltage. + * + * @param enable true to enable, false to disable + * + * @return zero on success, or a negative error code. + */ +int battery_measure_enable(bool enable); + +/** Measure the battery voltage. + * + * @return the battery voltage in millivolts, or a negative error + * code. + */ +int battery_sample(void); + +/** A point in a battery discharge curve sequence. + * + * A discharge curve is defined as a sequence of these points, where + * the first point has #lvl_pptt set to 10000 and the last point has + * #lvl_pptt set to zero. Both #lvl_pptt and #lvl_mV should be + * monotonic decreasing within the sequence. + */ +struct battery_level_point { + /** Remaining life at #lvl_mV. */ + u16_t lvl_pptt; + + /** Battery voltage at #lvl_pptt remaining life. */ + u16_t lvl_mV; +}; + +/** Calculate the estimated battery level based on a measured voltage. + * + * @param batt_mV a measured battery voltage level. + * + * @param curve the discharge curve for the type of battery installed + * on the system. + * + * @return the estimated remaining capacity in parts per ten + * thousand. + */ +unsigned int battery_level_pptt(unsigned int batt_mV, + const struct battery_level_point *curve); + +#endif /* APPLICATION_BATTERY_H_ */ diff --git a/samples/boards/nrf/battery/src/main.c b/samples/boards/nrf/battery/src/main.c new file mode 100644 index 00000000000..e05001b9b14 --- /dev/null +++ b/samples/boards/nrf/battery/src/main.c @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2018-2019 Peter Bigot Consulting, LLC + * Copyright (c) 2019 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include "battery.h" + +/** A discharge curve calibrated from LiPo batteries. + * + * Specifically ones like [Adafruit 3.7v 2000 + * mAh](https://www.adafruit.com/product/2011) + */ +static const struct battery_level_point lipo[] = { + + /* "Curve" here eyeballed from captured data for a 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 */ + u32_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; +} + +void main(void) +{ + int rc = battery_measure_enable(true); + + if (rc != 0) { + printk("Failed initialize battery measurement: %d\n", rc); + return; + } + + while (true) { + int batt_mV = battery_sample(); + + if (batt_mV < 0) { + printk("Failed to read battery voltage: %d\n", + batt_mV); + break; + } + + unsigned int batt_pptt = battery_level_pptt(batt_mV, lipo); + + printk("[%s]: %d mV; %u pptt\n", now_str(), + batt_mV, batt_pptt); + + /* Burn battery so you can see that this works over time */ + k_busy_wait(5 * USEC_PER_SEC); + } + printk("Disable: %d\n", battery_measure_enable(false)); +}