| /* |
| * Copyright (c) 2018-2019 Peter Bigot Consulting, LLC |
| * Copyright (c) 2019-2020 Nordic Semiconductor ASA |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <math.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| |
| #include <zephyr/kernel.h> |
| #include <zephyr/init.h> |
| #include <zephyr/drivers/gpio.h> |
| #include <zephyr/drivers/adc.h> |
| #include <zephyr/drivers/sensor.h> |
| #include <zephyr/logging/log.h> |
| |
| #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(VBATT, okay) |
| .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(VBATT, okay) |
| .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)); |
| } |