From 3fecf15ec449a259736bb4b31993895292f4db37 Mon Sep 17 00:00:00 2001 From: Richard Jones Date: Thu, 27 Aug 2020 21:23:59 +0100 Subject: [PATCH] Rip out zephyr battery example and hack into ZMK --- app/CMakeLists.txt | 1 + app/Kconfig | 5 + app/include/zmk/battery.h | 60 +++++++ app/src/battery.c | 331 ++++++++++++++++++++++++++++++++++++++ app/src/main.c | 11 ++ 5 files changed, 408 insertions(+) create mode 100644 app/include/zmk/battery.h create mode 100644 app/src/battery.c diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 9fce3b64..de858170 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -29,6 +29,7 @@ target_sources(app PRIVATE src/keymap.c) target_sources(app PRIVATE src/hid.c) target_sources(app PRIVATE src/sensors.c) target_sources_ifdef(CONFIG_ZMK_DISPLAY app PRIVATE src/display.c) +target_sources_ifdef(CONFIG_ZMK_BATTERY app PRIVATE src/battery.c) target_sources(app PRIVATE src/event_manager.c) target_sources(app PRIVATE src/events/position_state_changed.c) target_sources(app PRIVATE src/events/keycode_state_changed.c) diff --git a/app/Kconfig b/app/Kconfig index 877fce43..f12e05fe 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -76,6 +76,11 @@ config ZMK_DISPLAY select LVGL_THEME_MONO select LVGL_OBJ_LABEL +config ZMK_BATTERY + bool "ZMK battery level logging" + default n + select BATTERY + menu "Split Support" config ZMK_SPLIT diff --git a/app/include/zmk/battery.h b/app/include/zmk/battery.h new file mode 100644 index 00000000..f4083301 --- /dev/null +++ b/app/include/zmk/battery.h @@ -0,0 +1,60 @@ +/* + * 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); + + +int zmk_log_battery_enable(void); +void zmk_log_battery_disable(void); +void zmk_log_battery_sample(void); + + +#endif /* APPLICATION_BATTERY_H_ */ + diff --git a/app/src/battery.c b/app/src/battery.c new file mode 100644 index 00000000..b5972980 --- /dev/null +++ b/app/src/battery.c @@ -0,0 +1,331 @@ +/* + * 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 + +LOG_MODULE_REGISTER(BATTERY, CONFIG_ADC_LOG_LEVEL); + +#define VBATT DT_PATH(vbatt) + +#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 { + const char *label; + u8_t channel; +}; + +struct gpio_channel_config { + const char *label; + u8_t pin; + u8_t flags; +}; + +struct divider_config { + struct io_channel_config io_channel; + struct gpio_channel_config 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. + */ + u32_t output_ohm; + u32_t full_ohm; +}; + +static const struct divider_config divider_config = { +#if DT_NODE_HAS_STATUS(VBATT, okay) + .io_channel = { + DT_IO_CHANNELS_LABEL(VBATT), + DT_IO_CHANNELS_INPUT(VBATT), + }, +#if DT_NODE_HAS_PROP(VBATT, power_gpios) + .power_gpios = { + DT_GPIO_LABEL(VBATT, power_gpios), + DT_GPIO_PIN(VBATT, power_gpios), + DT_GPIO_FLAGS(VBATT, power_gpios), + }, +#endif + .output_ohm = DT_PROP(VBATT, output_ohms), + .full_ohm = DT_PROP(VBATT, full_ohms), +#else /* /vbatt exists */ + .io_channel = { + DT_LABEL(DT_ALIAS(adc_0)), + }, +#endif /* /vbatt exists */ +}; + +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; + + if (iocp->label == NULL) { + return -ENOTSUP; + } + + 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_configure(ddp->gpio, gcp->pin, + GPIO_OUTPUT_INACTIVE | gcp->flags); + 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), + }; + + 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(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_set(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); + + if (dcp->output_ohm != 0) { + rc = val * (u64_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)); +} + +/* was main.c */ +/** A discharge curve specific to the power source. */ +static const struct battery_level_point levels[] = { +#if DT_NODE_HAS_PROP(DT_INST(0, voltage_divider), io_channels) + /* "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 }, +#else + /* Linear from maximum voltage to minimum voltage. */ + { 10000, 3600 }, + { 0, 1700 }, +#endif +}; + +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; +} + +int zmk_log_battery_enable(void) +{ + int rc = battery_measure_enable(true); + + if (rc != 0) { + printk("Failed initialize battery measurement: %d\n", rc); + return -1; + } + + return 0; +} + +void zmk_log_battery_disable(void) +{ + printk("Disable: %d\n", battery_measure_enable(false)); +} + + +void zmk_log_battery_sample(void) +{ + int batt_mV = battery_sample(); + + if (batt_mV < 0) { + printk("Failed to read battery voltage: %d\n", + batt_mV); + return; + } + + unsigned int batt_pptt = battery_level_pptt(batt_mV, levels); + + 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); +} diff --git a/app/src/main.c b/app/src/main.c index 5a678ee0..edc935d2 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -15,6 +15,7 @@ LOG_MODULE_REGISTER(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#include #define ZMK_KSCAN_DEV DT_LABEL(ZMK_MATRIX_NODE_ID) @@ -34,4 +35,14 @@ void main(void) zmk_display_task_handler(); } #endif /* CONFIG_ZMK_DISPLAY */ + +#ifdef CONFIG_ZMK_BATTERY + if (zmk_log_battery_enable() != 0) + { + LOG_ERR("Could not enable battery logging\n"); + return; + } + zmk_log_battery_sample(); + zmk_log_battery_disable(); +#endif /* CONFIG_ZMK_BATTERY */ }