1 /*
2 * Copyright (c) 2018-2019 Peter Bigot Consulting, LLC
3 * Copyright (c) 2019-2020 Nordic Semiconductor ASA
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 */
7
8 #include <math.h>
9 #include <stdio.h>
10 #include <stdlib.h>
11
12 #include <zephyr/kernel.h>
13 #include <zephyr/init.h>
14 #include <zephyr/drivers/gpio.h>
15 #include <zephyr/drivers/adc.h>
16 #include <zephyr/drivers/sensor.h>
17 #include <zephyr/logging/log.h>
18
19 #include "battery.h"
20
21 LOG_MODULE_REGISTER(BATTERY, CONFIG_ADC_LOG_LEVEL);
22
23 #define VBATT DT_PATH(vbatt)
24 #define ZEPHYR_USER DT_PATH(zephyr_user)
25
26 #ifdef CONFIG_BOARD_THINGY52_NRF52832
27 /* This board uses a divider that reduces max voltage to
28 * reference voltage (600 mV).
29 */
30 #define BATTERY_ADC_GAIN ADC_GAIN_1
31 #else
32 /* Other boards may use dividers that only reduce battery voltage to
33 * the maximum supported by the hardware (3.6 V)
34 */
35 #define BATTERY_ADC_GAIN ADC_GAIN_1_6
36 #endif
37
38 struct io_channel_config {
39 uint8_t channel;
40 };
41
42 struct divider_config {
43 struct io_channel_config io_channel;
44 struct gpio_dt_spec power_gpios;
45 /* output_ohm is used as a flag value: if it is nonzero then
46 * the battery is measured through a voltage divider;
47 * otherwise it is assumed to be directly connected to Vdd.
48 */
49 uint32_t output_ohm;
50 uint32_t full_ohm;
51 };
52
53 static const struct divider_config divider_config = {
54 #if DT_NODE_HAS_STATUS_OKAY(VBATT)
55 .io_channel = {
56 DT_IO_CHANNELS_INPUT(VBATT),
57 },
58 .power_gpios = GPIO_DT_SPEC_GET_OR(VBATT, power_gpios, {}),
59 .output_ohm = DT_PROP(VBATT, output_ohms),
60 .full_ohm = DT_PROP(VBATT, full_ohms),
61 #else /* /vbatt exists */
62 .io_channel = {
63 DT_IO_CHANNELS_INPUT(ZEPHYR_USER),
64 },
65 #endif /* /vbatt exists */
66 };
67
68 struct divider_data {
69 const struct device *adc;
70 struct adc_channel_cfg adc_cfg;
71 struct adc_sequence adc_seq;
72 int16_t raw;
73 };
74 static struct divider_data divider_data = {
75 #if DT_NODE_HAS_STATUS_OKAY(VBATT)
76 .adc = DEVICE_DT_GET(DT_IO_CHANNELS_CTLR(VBATT)),
77 #else
78 .adc = DEVICE_DT_GET(DT_IO_CHANNELS_CTLR(ZEPHYR_USER)),
79 #endif
80 };
81
divider_setup(void)82 static int divider_setup(void)
83 {
84 const struct divider_config *cfg = ÷r_config;
85 const struct io_channel_config *iocp = &cfg->io_channel;
86 const struct gpio_dt_spec *gcp = &cfg->power_gpios;
87 struct divider_data *ddp = ÷r_data;
88 struct adc_sequence *asp = &ddp->adc_seq;
89 struct adc_channel_cfg *accp = &ddp->adc_cfg;
90 int rc;
91
92 if (!device_is_ready(ddp->adc)) {
93 LOG_ERR("ADC device is not ready %s", ddp->adc->name);
94 return -ENOENT;
95 }
96
97 if (gcp->port) {
98 if (!device_is_ready(gcp->port)) {
99 LOG_ERR("%s: device not ready", gcp->port->name);
100 return -ENOENT;
101 }
102 rc = gpio_pin_configure_dt(gcp, GPIO_OUTPUT_INACTIVE);
103 if (rc != 0) {
104 LOG_ERR("Failed to control feed %s.%u: %d",
105 gcp->port->name, gcp->pin, rc);
106 return rc;
107 }
108 }
109
110 *asp = (struct adc_sequence){
111 .channels = BIT(0),
112 .buffer = &ddp->raw,
113 .buffer_size = sizeof(ddp->raw),
114 .oversampling = 4,
115 .calibrate = true,
116 };
117
118 #ifdef CONFIG_ADC_NRFX_SAADC
119 *accp = (struct adc_channel_cfg){
120 .gain = BATTERY_ADC_GAIN,
121 .reference = ADC_REF_INTERNAL,
122 .acquisition_time = ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 40),
123 };
124
125 if (cfg->output_ohm != 0) {
126 accp->input_positive = SAADC_CH_PSELP_PSELP_AnalogInput0
127 + iocp->channel;
128 } else {
129 accp->input_positive = SAADC_CH_PSELP_PSELP_VDD;
130 }
131
132 asp->resolution = 14;
133 #else /* CONFIG_ADC_var */
134 #error Unsupported ADC
135 #endif /* CONFIG_ADC_var */
136
137 rc = adc_channel_setup(ddp->adc, accp);
138 LOG_INF("Setup AIN%u got %d", iocp->channel, rc);
139
140 return rc;
141 }
142
143 static bool battery_ok;
144
battery_setup(void)145 static int battery_setup(void)
146 {
147 int rc = divider_setup();
148
149 battery_ok = (rc == 0);
150 LOG_INF("Battery setup: %d %d", rc, battery_ok);
151 return rc;
152 }
153
154 SYS_INIT(battery_setup, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);
155
battery_measure_enable(bool enable)156 int battery_measure_enable(bool enable)
157 {
158 int rc = -ENOENT;
159
160 if (battery_ok) {
161 const struct gpio_dt_spec *gcp = ÷r_config.power_gpios;
162
163 rc = 0;
164 if (gcp->port) {
165 rc = gpio_pin_set_dt(gcp, enable);
166 }
167 }
168 return rc;
169 }
170
battery_sample(void)171 int battery_sample(void)
172 {
173 int rc = -ENOENT;
174
175 if (battery_ok) {
176 struct divider_data *ddp = ÷r_data;
177 const struct divider_config *dcp = ÷r_config;
178 struct adc_sequence *sp = &ddp->adc_seq;
179
180 rc = adc_read(ddp->adc, sp);
181 sp->calibrate = false;
182 if (rc == 0) {
183 int32_t val = ddp->raw;
184
185 adc_raw_to_millivolts(adc_ref_internal(ddp->adc),
186 ddp->adc_cfg.gain,
187 sp->resolution,
188 &val);
189
190 if (dcp->output_ohm != 0) {
191 rc = val * (uint64_t)dcp->full_ohm
192 / dcp->output_ohm;
193 LOG_INF("raw %u ~ %u mV => %d mV\n",
194 ddp->raw, val, rc);
195 } else {
196 rc = val;
197 LOG_INF("raw %u ~ %u mV\n", ddp->raw, val);
198 }
199 }
200 }
201
202 return rc;
203 }
204
battery_level_pptt(unsigned int batt_mV,const struct battery_level_point * curve)205 unsigned int battery_level_pptt(unsigned int batt_mV,
206 const struct battery_level_point *curve)
207 {
208 const struct battery_level_point *pb = curve;
209
210 if (batt_mV >= pb->lvl_mV) {
211 /* Measured voltage above highest point, cap at maximum. */
212 return pb->lvl_pptt;
213 }
214 /* Go down to the last point at or below the measured voltage. */
215 while ((pb->lvl_pptt > 0)
216 && (batt_mV < pb->lvl_mV)) {
217 ++pb;
218 }
219 if (batt_mV < pb->lvl_mV) {
220 /* Below lowest point, cap at minimum */
221 return pb->lvl_pptt;
222 }
223
224 /* Linear interpolation between below and above points. */
225 const struct battery_level_point *pa = pb - 1;
226
227 return pb->lvl_pptt
228 + ((pa->lvl_pptt - pb->lvl_pptt)
229 * (batt_mV - pb->lvl_mV)
230 / (pa->lvl_mV - pb->lvl_mV));
231 }
232