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 <pab@pabigot.com>
This commit is contained in:
Peter A. Bigot 2019-10-21 13:34:04 -05:00 committed by Carles Cufí
parent 4420c5ed40
commit d715fbba63
6 changed files with 370 additions and 0 deletions

View file

@ -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})

View file

@ -0,0 +1 @@
CONFIG_ADC=y

View file

@ -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

View file

@ -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 <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <zephyr.h>
#include <init.h>
#include <drivers/gpio.h>
#include <drivers/adc.h>
#include <drivers/sensor.h>
#include <logging/log.h>
#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 = &divider_config;
const struct io_channel_config *iocp = &cfg->io_channel;
const struct gpio_channel_config *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;
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 = &divider_data;
const struct gpio_channel_config *gcp = &divider_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 = &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) {
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));
}

View file

@ -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_ */

View file

@ -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 <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <zephyr.h>
#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));
}