/* * Copyright (c) 2018-2019 Peter Bigot Consulting, LLC * Copyright (c) 2019-2020 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); #define VBATT DT_PATH(vbatt) #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 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 { 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 battery 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_OKAY(VBATT) .io_channel = { DT_IO_CHANNELS_INPUT(VBATT), }, .power_gpios = GPIO_DT_SPEC_GET_OR(VBATT, power_gpios, {}), .output_ohm = DT_PROP(VBATT, output_ohms), .full_ohm = DT_PROP(VBATT, full_ohms), #else /* /vbatt exists */ .io_channel = { DT_IO_CHANNELS_INPUT(ZEPHYR_USER), }, #endif /* /vbatt 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_OKAY(VBATT) .adc = DEVICE_DT_GET(DT_IO_CHANNELS_CTLR(VBATT)), #else .adc = DEVICE_DT_GET(DT_IO_CHANNELS_CTLR(ZEPHYR_USER)), #endif }; static int divider_setup(void) { const struct divider_config *cfg = ÷r_config; const struct io_channel_config *iocp = &cfg->io_channel; const struct gpio_dt_spec *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; 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 = 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; #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(void) { 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 gpio_dt_spec *gcp = ÷r_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 = ÷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) { 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 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)); }