Skip to content

Commit c6ed48e

Browse files
t-8chTzung-Bi Shih
authored andcommitted
power: supply: add ChromeOS EC based charge control driver
The ChromeOS Embedded Controller implements a command to control charge thresholds and behaviour. Use it to implement the standard Linux charge_control_start_threshold, charge_control_end_threshold and charge_behaviour sysfs UAPIs. The driver is designed to be probed via the cros_ec mfd device. Acked-by: Sebastian Reichel <[email protected]> Signed-off-by: Thomas Weißschuh <[email protected]> Link: https://lore.kernel.org/r/[email protected] Signed-off-by: Tzung-Bi Shih <[email protected]>
1 parent 69a1374 commit c6ed48e

File tree

4 files changed

+351
-0
lines changed

4 files changed

+351
-0
lines changed

MAINTAINERS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5135,6 +5135,11 @@ S: Maintained
51355135
F: Documentation/devicetree/bindings/sound/google,cros-ec-codec.yaml
51365136
F: sound/soc/codecs/cros_ec_codec.*
51375137

5138+
CHROMEOS EC CHARGE CONTROL
5139+
M: Thomas Weißschuh <[email protected]>
5140+
S: Maintained
5141+
F: drivers/power/supply/cros_charge-control.c
5142+
51385143
CHROMEOS EC HARDWARE MONITORING
51395144
M: Thomas Weißschuh <[email protected]>
51405145
@@ -5148,6 +5153,7 @@ M: Benson Leung <[email protected]>
51485153
R: Guenter Roeck <[email protected]>
51495154
51505155
S: Maintained
5156+
F: drivers/power/supply/cros_charge-control.c
51515157
F: drivers/power/supply/cros_usbpd-charger.c
51525158
N: cros_ec
51535159
N: cros-ec

drivers/power/supply/Kconfig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,18 @@ config CHARGER_CROS_PCHG
860860
the peripheral charge ports from the EC and converts that into
861861
power_supply properties.
862862

863+
config CHARGER_CROS_CONTROL
864+
tristate "ChromeOS EC based charge control"
865+
depends on MFD_CROS_EC_DEV
866+
depends on ACPI_BATTERY
867+
default MFD_CROS_EC_DEV
868+
help
869+
Say Y here to enable ChromeOS EC based battery charge control.
870+
This driver can manage charge thresholds and behaviour.
871+
872+
This driver can also be built as a module. If so, the module will be
873+
called cros_charge-control.
874+
863875
config CHARGER_SC2731
864876
tristate "Spreadtrum SC2731 charger driver"
865877
depends on MFD_SC27XX_PMIC || COMPILE_TEST

drivers/power/supply/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ obj-$(CONFIG_CHARGER_TPS65090) += tps65090-charger.o
100100
obj-$(CONFIG_CHARGER_TPS65217) += tps65217_charger.o
101101
obj-$(CONFIG_AXP288_FUEL_GAUGE) += axp288_fuel_gauge.o
102102
obj-$(CONFIG_AXP288_CHARGER) += axp288_charger.o
103+
obj-$(CONFIG_CHARGER_CROS_CONTROL) += cros_charge-control.o
103104
obj-$(CONFIG_CHARGER_CROS_USBPD) += cros_usbpd-charger.o
104105
obj-$(CONFIG_CHARGER_CROS_PCHG) += cros_peripheral_charger.o
105106
obj-$(CONFIG_CHARGER_SC2731) += sc2731_charger.o
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
/*
3+
* ChromeOS EC driver for charge control
4+
*
5+
* Copyright (C) 2024 Thomas Weißschuh <[email protected]>
6+
*/
7+
#include <acpi/battery.h>
8+
#include <linux/container_of.h>
9+
#include <linux/mod_devicetable.h>
10+
#include <linux/module.h>
11+
#include <linux/platform_data/cros_ec_commands.h>
12+
#include <linux/platform_data/cros_ec_proto.h>
13+
#include <linux/platform_device.h>
14+
#include <linux/types.h>
15+
16+
#define EC_CHARGE_CONTROL_BEHAVIOURS (BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO) | \
17+
BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE) | \
18+
BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE))
19+
20+
enum CROS_CHCTL_ATTR {
21+
CROS_CHCTL_ATTR_START_THRESHOLD,
22+
CROS_CHCTL_ATTR_END_THRESHOLD,
23+
CROS_CHCTL_ATTR_CHARGE_BEHAVIOUR,
24+
_CROS_CHCTL_ATTR_COUNT
25+
};
26+
27+
/*
28+
* Semantics of data *returned* from the EC API and Linux sysfs differ
29+
* slightly, also the v1 API can not return any data.
30+
* To match the expected sysfs API, data is never read back from the EC but
31+
* cached in the driver.
32+
*
33+
* Changes to the EC bypassing the driver will not be reflected in sysfs.
34+
* Any change to "charge_behaviour" will synchronize the EC with the driver state.
35+
*/
36+
37+
struct cros_chctl_priv {
38+
struct cros_ec_device *cros_ec;
39+
struct acpi_battery_hook battery_hook;
40+
struct power_supply *hooked_battery;
41+
u8 cmd_version;
42+
43+
/* The callbacks need to access this priv structure.
44+
* As neither the struct device nor power_supply are under the drivers
45+
* control, embed the attributes within priv to use with container_of().
46+
*/
47+
struct device_attribute device_attrs[_CROS_CHCTL_ATTR_COUNT];
48+
struct attribute *attributes[_CROS_CHCTL_ATTR_COUNT];
49+
struct attribute_group group;
50+
51+
enum power_supply_charge_behaviour current_behaviour;
52+
u8 current_start_threshold, current_end_threshold;
53+
};
54+
55+
static int cros_chctl_send_charge_control_cmd(struct cros_ec_device *cros_ec,
56+
u8 cmd_version, struct ec_params_charge_control *req)
57+
{
58+
static const u8 outsizes[] = {
59+
[1] = offsetof(struct ec_params_charge_control, cmd),
60+
[2] = sizeof(struct ec_params_charge_control),
61+
[3] = sizeof(struct ec_params_charge_control),
62+
};
63+
64+
struct {
65+
struct cros_ec_command msg;
66+
union {
67+
struct ec_params_charge_control req;
68+
struct ec_response_charge_control resp;
69+
} __packed data;
70+
} __packed buf = {
71+
.msg = {
72+
.command = EC_CMD_CHARGE_CONTROL,
73+
.version = cmd_version,
74+
.insize = 0,
75+
.outsize = outsizes[cmd_version],
76+
},
77+
.data.req = *req,
78+
};
79+
80+
return cros_ec_cmd_xfer_status(cros_ec, &buf.msg);
81+
}
82+
83+
static int cros_chctl_configure_ec(struct cros_chctl_priv *priv)
84+
{
85+
struct ec_params_charge_control req = {};
86+
87+
req.cmd = EC_CHARGE_CONTROL_CMD_SET;
88+
89+
switch (priv->current_behaviour) {
90+
case POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO:
91+
req.mode = CHARGE_CONTROL_NORMAL;
92+
break;
93+
case POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE:
94+
req.mode = CHARGE_CONTROL_IDLE;
95+
break;
96+
case POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE:
97+
req.mode = CHARGE_CONTROL_DISCHARGE;
98+
break;
99+
default:
100+
return -EINVAL;
101+
}
102+
103+
if (priv->current_behaviour == POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO &&
104+
!(priv->current_start_threshold == 0 && priv->current_end_threshold == 100)) {
105+
req.sustain_soc.lower = priv->current_start_threshold;
106+
req.sustain_soc.upper = priv->current_end_threshold;
107+
} else {
108+
/* Disable charging limits */
109+
req.sustain_soc.lower = -1;
110+
req.sustain_soc.upper = -1;
111+
}
112+
113+
return cros_chctl_send_charge_control_cmd(priv->cros_ec, priv->cmd_version, &req);
114+
}
115+
116+
static struct cros_chctl_priv *cros_chctl_attr_to_priv(struct attribute *attr,
117+
enum CROS_CHCTL_ATTR idx)
118+
{
119+
struct device_attribute *dev_attr = container_of(attr, struct device_attribute, attr);
120+
121+
return container_of(dev_attr, struct cros_chctl_priv, device_attrs[idx]);
122+
}
123+
124+
static ssize_t cros_chctl_store_threshold(struct device *dev, struct cros_chctl_priv *priv,
125+
int is_end_threshold, const char *buf, size_t count)
126+
{
127+
int ret, val;
128+
129+
ret = kstrtoint(buf, 10, &val);
130+
if (ret < 0)
131+
return ret;
132+
if (val < 0 || val > 100)
133+
return -EINVAL;
134+
135+
if (is_end_threshold) {
136+
if (val <= priv->current_start_threshold)
137+
return -EINVAL;
138+
priv->current_end_threshold = val;
139+
} else {
140+
if (val >= priv->current_end_threshold)
141+
return -EINVAL;
142+
priv->current_start_threshold = val;
143+
}
144+
145+
if (priv->current_behaviour == POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO) {
146+
ret = cros_chctl_configure_ec(priv);
147+
if (ret < 0)
148+
return ret;
149+
}
150+
151+
return count;
152+
}
153+
154+
static ssize_t charge_control_start_threshold_show(struct device *dev,
155+
struct device_attribute *attr,
156+
char *buf)
157+
{
158+
struct cros_chctl_priv *priv = cros_chctl_attr_to_priv(&attr->attr,
159+
CROS_CHCTL_ATTR_START_THRESHOLD);
160+
161+
return sysfs_emit(buf, "%u\n", (unsigned int)priv->current_start_threshold);
162+
}
163+
164+
static ssize_t charge_control_start_threshold_store(struct device *dev,
165+
struct device_attribute *attr,
166+
const char *buf, size_t count)
167+
{
168+
struct cros_chctl_priv *priv = cros_chctl_attr_to_priv(&attr->attr,
169+
CROS_CHCTL_ATTR_START_THRESHOLD);
170+
171+
return cros_chctl_store_threshold(dev, priv, 0, buf, count);
172+
}
173+
174+
static ssize_t charge_control_end_threshold_show(struct device *dev, struct device_attribute *attr,
175+
char *buf)
176+
{
177+
struct cros_chctl_priv *priv = cros_chctl_attr_to_priv(&attr->attr,
178+
CROS_CHCTL_ATTR_END_THRESHOLD);
179+
180+
return sysfs_emit(buf, "%u\n", (unsigned int)priv->current_end_threshold);
181+
}
182+
183+
static ssize_t charge_control_end_threshold_store(struct device *dev, struct device_attribute *attr,
184+
const char *buf, size_t count)
185+
{
186+
struct cros_chctl_priv *priv = cros_chctl_attr_to_priv(&attr->attr,
187+
CROS_CHCTL_ATTR_END_THRESHOLD);
188+
189+
return cros_chctl_store_threshold(dev, priv, 1, buf, count);
190+
}
191+
192+
static ssize_t charge_behaviour_show(struct device *dev, struct device_attribute *attr, char *buf)
193+
{
194+
struct cros_chctl_priv *priv = cros_chctl_attr_to_priv(&attr->attr,
195+
CROS_CHCTL_ATTR_CHARGE_BEHAVIOUR);
196+
197+
return power_supply_charge_behaviour_show(dev, EC_CHARGE_CONTROL_BEHAVIOURS,
198+
priv->current_behaviour, buf);
199+
}
200+
201+
static ssize_t charge_behaviour_store(struct device *dev, struct device_attribute *attr,
202+
const char *buf, size_t count)
203+
{
204+
struct cros_chctl_priv *priv = cros_chctl_attr_to_priv(&attr->attr,
205+
CROS_CHCTL_ATTR_CHARGE_BEHAVIOUR);
206+
enum power_supply_charge_behaviour behaviour;
207+
int ret;
208+
209+
behaviour = power_supply_charge_behaviour_parse(EC_CHARGE_CONTROL_BEHAVIOURS, buf);
210+
if (behaviour < 0)
211+
return behaviour;
212+
213+
priv->current_behaviour = behaviour;
214+
215+
ret = cros_chctl_configure_ec(priv);
216+
if (ret < 0)
217+
return ret;
218+
219+
return count;
220+
}
221+
222+
static umode_t cros_chtl_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n)
223+
{
224+
struct cros_chctl_priv *priv = cros_chctl_attr_to_priv(attr, n);
225+
226+
if (priv->cmd_version < 2) {
227+
if (n == CROS_CHCTL_ATTR_START_THRESHOLD)
228+
return 0;
229+
if (n == CROS_CHCTL_ATTR_END_THRESHOLD)
230+
return 0;
231+
}
232+
233+
return attr->mode;
234+
}
235+
236+
static int cros_chctl_add_battery(struct power_supply *battery, struct acpi_battery_hook *hook)
237+
{
238+
struct cros_chctl_priv *priv = container_of(hook, struct cros_chctl_priv, battery_hook);
239+
240+
if (priv->hooked_battery)
241+
return 0;
242+
243+
priv->hooked_battery = battery;
244+
return device_add_group(&battery->dev, &priv->group);
245+
}
246+
247+
static int cros_chctl_remove_battery(struct power_supply *battery, struct acpi_battery_hook *hook)
248+
{
249+
struct cros_chctl_priv *priv = container_of(hook, struct cros_chctl_priv, battery_hook);
250+
251+
if (priv->hooked_battery == battery) {
252+
device_remove_group(&battery->dev, &priv->group);
253+
priv->hooked_battery = NULL;
254+
}
255+
256+
return 0;
257+
}
258+
259+
static int cros_chctl_probe(struct platform_device *pdev)
260+
{
261+
struct device *dev = &pdev->dev;
262+
struct cros_ec_dev *ec_dev = dev_get_drvdata(dev->parent);
263+
struct cros_ec_device *cros_ec = ec_dev->ec_dev;
264+
struct cros_chctl_priv *priv;
265+
size_t i;
266+
int ret;
267+
268+
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
269+
if (!priv)
270+
return -ENOMEM;
271+
272+
ret = cros_ec_get_cmd_versions(cros_ec, EC_CMD_CHARGE_CONTROL);
273+
if (ret < 0)
274+
return ret;
275+
else if (ret & EC_VER_MASK(3))
276+
priv->cmd_version = 3;
277+
else if (ret & EC_VER_MASK(2))
278+
priv->cmd_version = 2;
279+
else if (ret & EC_VER_MASK(1))
280+
priv->cmd_version = 1;
281+
else
282+
return -ENODEV;
283+
284+
dev_dbg(dev, "Command version: %u\n", (unsigned int)priv->cmd_version);
285+
286+
priv->cros_ec = cros_ec;
287+
priv->device_attrs[CROS_CHCTL_ATTR_START_THRESHOLD] =
288+
(struct device_attribute)__ATTR_RW(charge_control_start_threshold);
289+
priv->device_attrs[CROS_CHCTL_ATTR_END_THRESHOLD] =
290+
(struct device_attribute)__ATTR_RW(charge_control_end_threshold);
291+
priv->device_attrs[CROS_CHCTL_ATTR_CHARGE_BEHAVIOUR] =
292+
(struct device_attribute)__ATTR_RW(charge_behaviour);
293+
for (i = 0; i < _CROS_CHCTL_ATTR_COUNT; i++) {
294+
sysfs_attr_init(&priv->device_attrs[i].attr);
295+
priv->attributes[i] = &priv->device_attrs[i].attr;
296+
}
297+
priv->attributes[_CROS_CHCTL_ATTR_COUNT] = NULL;
298+
priv->group.is_visible = cros_chtl_attr_is_visible;
299+
priv->group.attrs = priv->attributes;
300+
301+
priv->battery_hook.name = dev_name(dev);
302+
priv->battery_hook.add_battery = cros_chctl_add_battery;
303+
priv->battery_hook.remove_battery = cros_chctl_remove_battery;
304+
305+
priv->current_behaviour = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO;
306+
priv->current_start_threshold = 0;
307+
priv->current_end_threshold = 100;
308+
309+
/* Bring EC into well-known state */
310+
ret = cros_chctl_configure_ec(priv);
311+
if (ret < 0)
312+
return ret;
313+
314+
return devm_battery_hook_register(dev, &priv->battery_hook);
315+
}
316+
317+
static const struct platform_device_id cros_chctl_id[] = {
318+
{ "cros-charge-control", 0 },
319+
{}
320+
};
321+
322+
static struct platform_driver cros_chctl_driver = {
323+
.driver.name = "cros-charge-control",
324+
.probe = cros_chctl_probe,
325+
.id_table = cros_chctl_id,
326+
};
327+
module_platform_driver(cros_chctl_driver);
328+
329+
MODULE_DEVICE_TABLE(platform, cros_chctl_id);
330+
MODULE_DESCRIPTION("ChromeOS EC charge control");
331+
MODULE_AUTHOR("Thomas Weißschuh <[email protected]>");
332+
MODULE_LICENSE("GPL");

0 commit comments

Comments
 (0)