summaryrefslogtreecommitdiff
path: root/drivers/power/supply/adc-battery-helper.c
blob: 6e0f5b6d73d7c11c3d9fa265edda1c9c7a0133a5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
// SPDX-License-Identifier: GPL-2.0+
/*
 * Helper for batteries with accurate current and voltage measurement, but
 * without temperature measurement or without a "resistance-temp-table".
 *
 * Some fuel-gauges are not full-featured autonomous fuel-gauges.
 * These fuel-gauges offer accurate current and voltage measurements but
 * their coulomb-counters are intended to work together with an always on
 * micro-controller monitoring the fuel-gauge.
 *
 * This adc-battery-helper code offers open-circuit-voltage (ocv) and through
 * that capacity estimation for devices where such limited functionality
 * fuel-gauges are exposed directly to Linux.
 *
 * This helper requires the hw to provide accurate battery current_now and
 * voltage_now measurement and this helper the provides the following properties
 * based on top of those readings:
 *
 *	POWER_SUPPLY_PROP_STATUS
 *	POWER_SUPPLY_PROP_VOLTAGE_OCV
 *	POWER_SUPPLY_PROP_VOLTAGE_NOW
 *	POWER_SUPPLY_PROP_CURRENT_NOW
 *	POWER_SUPPLY_PROP_CAPACITY
 *
 * As well as optional the following properties assuming an always present
 * system-scope battery, allowing direct use of adc_battery_helper_get_prop()
 * in this common case:
 *	POWER_SUPPLY_PROP_PRESENT
 *	POWER_SUPPLY_PROP_SCOPE
 *
 * Using this helper is as simple as:
 *
 * 1. Embed a struct adc_battery_helper this MUST be the first member of
 *    the battery driver's data struct.
 * 2. Use adc_battery_helper_props[] or add the above properties to
 *    the list of properties in power_supply_desc
 * 3. Call adc_battery_helper_init() after registering the power_supply and
 *    before returning from the probe() function
 * 4. Use adc_battery_helper_get_prop() as the power-supply's get_property()
 *    method, or call it for the above properties.
 * 5. Use adc_battery_helper_external_power_changed() as the power-supply's
 *    external_power_changed() method or call it from that method.
 * 6. Use adc_battery_helper_[suspend|resume]() as suspend-resume methods or
 *    call them from the driver's suspend-resume methods.
 *
 * The provided get_voltage_and_current_now() method will be called by this
 * helper at adc_battery_helper_init() time and later.
 *
 * Copyright (c) 2021-2025 Hans de Goede <hansg@kernel.org>
 */

#include <linux/cleanup.h>
#include <linux/devm-helpers.h>
#include <linux/gpio/consumer.h>
#include <linux/mutex.h>
#include <linux/power_supply.h>
#include <linux/workqueue.h>

#include "adc-battery-helper.h"

#define MOV_AVG_WINDOW_SIZE			ADC_BAT_HELPER_MOV_AVG_WINDOW_SIZE
#define INIT_POLL_TIME				(5 * HZ)
#define POLL_TIME				(30 * HZ)
#define SETTLE_TIME				(1 * HZ)

#define INIT_POLL_COUNT				30

#define CURR_HYST_UA				65000

#define LOW_BAT_UV				3700000
#define FULL_BAT_HYST_UV			38000

#define AMBIENT_TEMP_CELSIUS			25

static int adc_battery_helper_get_status(struct adc_battery_helper *help)
{
	int full_uv =
		help->psy->battery_info->constant_charge_voltage_max_uv - FULL_BAT_HYST_UV;

	if (help->curr_ua > CURR_HYST_UA)
		return POWER_SUPPLY_STATUS_CHARGING;

	if (help->curr_ua < -CURR_HYST_UA)
		return POWER_SUPPLY_STATUS_DISCHARGING;

	if (help->supplied) {
		bool full;

		if (help->charge_finished)
			full = gpiod_get_value_cansleep(help->charge_finished);
		else
			full = help->ocv_avg_uv > full_uv;

		if (full)
			return POWER_SUPPLY_STATUS_FULL;
	}

	return POWER_SUPPLY_STATUS_NOT_CHARGING;
}

static void adc_battery_helper_work(struct work_struct *work)
{
	struct adc_battery_helper *help = container_of(work, struct adc_battery_helper,
						       work.work);
	int i, curr_diff_ua, volt_diff_uv, res_mohm, ret, win_size;
	struct device *dev = help->psy->dev.parent;
	int volt_uv, prev_volt_uv = help->volt_uv;
	int curr_ua, prev_curr_ua = help->curr_ua;
	bool prev_supplied = help->supplied;
	int prev_status = help->status;

	guard(mutex)(&help->lock);

	ret = help->get_voltage_and_current_now(help->psy, &volt_uv, &curr_ua);
	if (ret)
		goto out;

	help->volt_uv = volt_uv;
	help->curr_ua = curr_ua;

	help->ocv_uv[help->ocv_avg_index] =
		help->volt_uv - help->curr_ua * help->intern_res_avg_mohm / 1000;
	dev_dbg(dev, "volt-now: %d, curr-now: %d, volt-ocv: %d\n",
		help->volt_uv, help->curr_ua, help->ocv_uv[help->ocv_avg_index]);
	help->ocv_avg_index = (help->ocv_avg_index + 1) % MOV_AVG_WINDOW_SIZE;
	help->poll_count++;

	help->ocv_avg_uv = 0;
	win_size = min(help->poll_count, MOV_AVG_WINDOW_SIZE);
	for (i = 0; i < win_size; i++)
		help->ocv_avg_uv += help->ocv_uv[i];
	help->ocv_avg_uv /= win_size;

	help->supplied = power_supply_am_i_supplied(help->psy);
	help->status = adc_battery_helper_get_status(help);
	if (help->status == POWER_SUPPLY_STATUS_FULL)
		help->capacity = 100;
	else
		help->capacity = power_supply_batinfo_ocv2cap(help->psy->battery_info,
							      help->ocv_avg_uv,
							      AMBIENT_TEMP_CELSIUS);

	/*
	 * Skip internal resistance calc on charger [un]plug and
	 * when the battery is almost empty (voltage low).
	 */
	if (help->supplied != prev_supplied ||
	    help->volt_uv < LOW_BAT_UV ||
	    help->poll_count < 2)
		goto out;

	/*
	 * Assuming that the OCV voltage does not change significantly
	 * between 2 polls, then we can calculate the internal resistance
	 * on a significant current change by attributing all voltage
	 * change between the 2 readings to the internal resistance.
	 */
	curr_diff_ua = abs(help->curr_ua - prev_curr_ua);
	if (curr_diff_ua < CURR_HYST_UA)
		goto out;

	volt_diff_uv = abs(help->volt_uv - prev_volt_uv);
	res_mohm = volt_diff_uv * 1000 / curr_diff_ua;

	if ((res_mohm < (help->intern_res_avg_mohm * 2 / 3)) ||
	    (res_mohm > (help->intern_res_avg_mohm * 4 / 3))) {
		dev_dbg(dev, "Ignoring outlier internal resistance %d mOhm\n", res_mohm);
		goto out;
	}

	dev_dbg(dev, "Internal resistance %d mOhm\n", res_mohm);

	help->intern_res_mohm[help->intern_res_avg_index] = res_mohm;
	help->intern_res_avg_index = (help->intern_res_avg_index + 1) % MOV_AVG_WINDOW_SIZE;
	help->intern_res_poll_count++;

	help->intern_res_avg_mohm = 0;
	win_size = min(help->intern_res_poll_count, MOV_AVG_WINDOW_SIZE);
	for (i = 0; i < win_size; i++)
		help->intern_res_avg_mohm += help->intern_res_mohm[i];
	help->intern_res_avg_mohm /= win_size;

out:
	queue_delayed_work(system_percpu_wq, &help->work,
			   (help->poll_count <= INIT_POLL_COUNT) ?
					INIT_POLL_TIME : POLL_TIME);

	if (help->status != prev_status)
		power_supply_changed(help->psy);
}

const enum power_supply_property adc_battery_helper_properties[] = {
	POWER_SUPPLY_PROP_STATUS,
	POWER_SUPPLY_PROP_VOLTAGE_NOW,
	POWER_SUPPLY_PROP_VOLTAGE_OCV,
	POWER_SUPPLY_PROP_CURRENT_NOW,
	POWER_SUPPLY_PROP_CAPACITY,
	POWER_SUPPLY_PROP_PRESENT,
	POWER_SUPPLY_PROP_SCOPE,
};
EXPORT_SYMBOL_GPL(adc_battery_helper_properties);

static_assert(ARRAY_SIZE(adc_battery_helper_properties) ==
	      ADC_HELPER_NUM_PROPERTIES);

int adc_battery_helper_get_property(struct power_supply *psy,
				    enum power_supply_property psp,
				    union power_supply_propval *val)
{
	struct adc_battery_helper *help = power_supply_get_drvdata(psy);
	int dummy, ret = 0;

	/*
	 * Avoid racing with adc_battery_helper_work() while it is updating
	 * variables and avoid calling get_voltage_and_current_now() reentrantly.
	 */
	guard(mutex)(&help->lock);

	switch (psp) {
	case POWER_SUPPLY_PROP_STATUS:
		val->intval = help->status;
		break;
	case POWER_SUPPLY_PROP_VOLTAGE_NOW:
		ret = help->get_voltage_and_current_now(psy, &val->intval, &dummy);
		break;
	case POWER_SUPPLY_PROP_VOLTAGE_OCV:
		val->intval = help->ocv_avg_uv;
		break;
	case POWER_SUPPLY_PROP_CURRENT_NOW:
		ret = help->get_voltage_and_current_now(psy, &dummy, &val->intval);
		break;
	case POWER_SUPPLY_PROP_CAPACITY:
		val->intval = help->capacity;
		break;
	case POWER_SUPPLY_PROP_PRESENT:
		val->intval = 1;
		break;
	case POWER_SUPPLY_PROP_SCOPE:
		val->intval = POWER_SUPPLY_SCOPE_SYSTEM;
		break;
	default:
		return -EINVAL;
	}

	return ret;
}
EXPORT_SYMBOL_GPL(adc_battery_helper_get_property);

void adc_battery_helper_external_power_changed(struct power_supply *psy)
{
	struct adc_battery_helper *help = power_supply_get_drvdata(psy);

	dev_dbg(help->psy->dev.parent, "external power changed\n");
	mod_delayed_work(system_percpu_wq, &help->work, SETTLE_TIME);
}
EXPORT_SYMBOL_GPL(adc_battery_helper_external_power_changed);

static void adc_battery_helper_start_work(struct adc_battery_helper *help)
{
	help->poll_count = 0;
	help->ocv_avg_index = 0;

	queue_delayed_work(system_percpu_wq, &help->work, 0);
	flush_delayed_work(&help->work);
}

int adc_battery_helper_init(struct adc_battery_helper *help, struct power_supply *psy,
			    adc_battery_helper_get_func get_voltage_and_current_now,
			    struct gpio_desc *charge_finished_gpio)
{
	struct device *dev = psy->dev.parent;
	int ret;

	help->psy = psy;
	help->get_voltage_and_current_now = get_voltage_and_current_now;
	help->charge_finished = charge_finished_gpio;

	ret = devm_mutex_init(dev, &help->lock);
	if (ret)
		return ret;

	ret = devm_delayed_work_autocancel(dev, &help->work, adc_battery_helper_work);
	if (ret)
		return ret;

	if (!help->psy->battery_info ||
	    help->psy->battery_info->factory_internal_resistance_uohm == -EINVAL ||
	    help->psy->battery_info->constant_charge_voltage_max_uv == -EINVAL ||
	    !psy->battery_info->ocv_table[0]) {
		dev_err(dev, "error required properties are missing\n");
		return -ENODEV;
	}

	/* Use provided internal resistance as start point (in milli-ohm) */
	help->intern_res_avg_mohm =
		help->psy->battery_info->factory_internal_resistance_uohm / 1000;
	/* Also add it to the internal resistance moving average window */
	help->intern_res_mohm[0] = help->intern_res_avg_mohm;
	help->intern_res_avg_index = 1;
	help->intern_res_poll_count = 1;

	adc_battery_helper_start_work(help);
	return 0;
}
EXPORT_SYMBOL_GPL(adc_battery_helper_init);

int adc_battery_helper_suspend(struct device *dev)
{
	struct adc_battery_helper *help = dev_get_drvdata(dev);

	cancel_delayed_work_sync(&help->work);
	return 0;
}
EXPORT_SYMBOL_GPL(adc_battery_helper_suspend);

int adc_battery_helper_resume(struct device *dev)
{
	struct adc_battery_helper *help = dev_get_drvdata(dev);

	adc_battery_helper_start_work(help);
	return 0;
}
EXPORT_SYMBOL_GPL(adc_battery_helper_resume);

MODULE_AUTHOR("Hans de Goede <hansg@kernel.org>");
MODULE_DESCRIPTION("ADC battery capacity estimation helper");
MODULE_LICENSE("GPL");