xref: /linux/drivers/power/supply/cros_charge-control.c (revision d2c9a99135da931377240942d44f3dea104cedb8)
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 <linux@weissschuh.net>
6  */
7 #include <acpi/battery.h>
8 #include <linux/container_of.h>
9 #include <linux/dmi.h>
10 #include <linux/lockdep.h>
11 #include <linux/module.h>
12 #include <linux/mutex.h>
13 #include <linux/platform_data/cros_ec_commands.h>
14 #include <linux/platform_data/cros_ec_proto.h>
15 #include <linux/platform_device.h>
16 #include <linux/types.h>
17 
18 #define EC_CHARGE_CONTROL_BEHAVIOURS	(BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO)             | \
19 					 BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE)   | \
20 					 BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE))
21 
22 /*
23  * Semantics of data *returned* from the EC API and Linux sysfs differ
24  * slightly, also the v1 API can not return any data.
25  * To match the expected sysfs API, data is never read back from the EC but
26  * cached in the driver.
27  *
28  * Changes to the EC bypassing the driver will not be reflected in sysfs.
29  * Any change to "charge_behaviour" will synchronize the EC with the driver state.
30  */
31 
32 struct cros_chctl_priv {
33 	struct device *dev;
34 	struct cros_ec_device *cros_ec;
35 	struct acpi_battery_hook battery_hook;
36 	struct power_supply *hooked_battery;
37 	u8 cmd_version;
38 
39 	const struct power_supply_ext *psy_ext;
40 
41 	struct mutex lock; /* protects fields below and cros_ec */
42 	enum power_supply_charge_behaviour current_behaviour;
43 	u8 current_start_threshold, current_end_threshold;
44 };
45 
cros_chctl_send_charge_control_cmd(struct cros_ec_device * cros_ec,u8 cmd_version,struct ec_params_charge_control * req)46 static int cros_chctl_send_charge_control_cmd(struct cros_ec_device *cros_ec,
47 					      u8 cmd_version, struct ec_params_charge_control *req)
48 {
49 	int ret;
50 	static const u8 outsizes[] = {
51 		[1] = offsetof(struct ec_params_charge_control, cmd),
52 		[2] = sizeof(struct ec_params_charge_control),
53 		[3] = sizeof(struct ec_params_charge_control),
54 	};
55 
56 	ret = cros_ec_cmd(cros_ec, cmd_version, EC_CMD_CHARGE_CONTROL, req,
57 			  outsizes[cmd_version], NULL, 0);
58 
59 	if (ret < 0)
60 		return ret;
61 
62 	return 0;
63 }
64 
cros_chctl_configure_ec(struct cros_chctl_priv * priv)65 static int cros_chctl_configure_ec(struct cros_chctl_priv *priv)
66 {
67 	struct ec_params_charge_control req = {};
68 
69 	lockdep_assert_held(&priv->lock);
70 
71 	req.cmd = EC_CHARGE_CONTROL_CMD_SET;
72 
73 	switch (priv->current_behaviour) {
74 	case POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO:
75 		req.mode = CHARGE_CONTROL_NORMAL;
76 		break;
77 	case POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE:
78 		req.mode = CHARGE_CONTROL_IDLE;
79 		break;
80 	case POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE:
81 		req.mode = CHARGE_CONTROL_DISCHARGE;
82 		break;
83 	default:
84 		return -EINVAL;
85 	}
86 
87 	if (priv->current_behaviour == POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO &&
88 	    !(priv->current_start_threshold == 0 && priv->current_end_threshold == 100)) {
89 		req.sustain_soc.lower = priv->current_start_threshold;
90 		req.sustain_soc.upper = priv->current_end_threshold;
91 	} else {
92 		/* Disable charging limits */
93 		req.sustain_soc.lower = -1;
94 		req.sustain_soc.upper = -1;
95 	}
96 
97 	return cros_chctl_send_charge_control_cmd(priv->cros_ec, priv->cmd_version, &req);
98 }
99 
cros_chctl_psy_ext_get_prop(struct power_supply * psy,const struct power_supply_ext * ext,void * data,enum power_supply_property psp,union power_supply_propval * val)100 static int cros_chctl_psy_ext_get_prop(struct power_supply *psy,
101 				       const struct power_supply_ext *ext,
102 				       void *data,
103 				       enum power_supply_property psp,
104 				       union power_supply_propval *val)
105 {
106 	struct cros_chctl_priv *priv = data;
107 
108 	switch (psp) {
109 	case POWER_SUPPLY_PROP_CHARGE_CONTROL_START_THRESHOLD:
110 		val->intval = priv->current_start_threshold;
111 		return 0;
112 	case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD:
113 		val->intval = priv->current_end_threshold;
114 		return 0;
115 	case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR:
116 		val->intval = priv->current_behaviour;
117 		return 0;
118 	default:
119 		return -EINVAL;
120 	}
121 }
122 
cros_chctl_psy_ext_set_threshold(struct cros_chctl_priv * priv,enum power_supply_property psp,int val)123 static int cros_chctl_psy_ext_set_threshold(struct cros_chctl_priv *priv,
124 					    enum power_supply_property psp,
125 					    int val)
126 {
127 	int ret;
128 
129 	if (val < 0 || val > 100)
130 		return -EINVAL;
131 
132 	if (psp == POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD) {
133 		/* Start threshold is not exposed, use fixed value */
134 		if (priv->cmd_version == 2)
135 			priv->current_start_threshold = val == 100 ? 0 : val;
136 
137 		if (val < priv->current_start_threshold)
138 			return -EINVAL;
139 		priv->current_end_threshold = val;
140 	} else {
141 		if (val > priv->current_end_threshold)
142 			return -EINVAL;
143 		priv->current_start_threshold = val;
144 	}
145 
146 	if (priv->current_behaviour == POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO) {
147 		ret = cros_chctl_configure_ec(priv);
148 		if (ret < 0)
149 			return ret;
150 	}
151 
152 	return 0;
153 }
154 
155 
cros_chctl_psy_ext_set_prop(struct power_supply * psy,const struct power_supply_ext * ext,void * data,enum power_supply_property psp,const union power_supply_propval * val)156 static int cros_chctl_psy_ext_set_prop(struct power_supply *psy,
157 				       const struct power_supply_ext *ext,
158 				       void *data,
159 				       enum power_supply_property psp,
160 				       const union power_supply_propval *val)
161 {
162 	struct cros_chctl_priv *priv = data;
163 	int ret;
164 
165 	guard(mutex)(&priv->lock);
166 
167 	switch (psp) {
168 	case POWER_SUPPLY_PROP_CHARGE_CONTROL_START_THRESHOLD:
169 	case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD:
170 		return cros_chctl_psy_ext_set_threshold(priv, psp, val->intval);
171 	case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR:
172 		priv->current_behaviour = val->intval;
173 		ret = cros_chctl_configure_ec(priv);
174 		if (ret < 0)
175 			return ret;
176 		return 0;
177 	default:
178 		return -EINVAL;
179 	}
180 }
181 
cros_chctl_psy_prop_is_writeable(struct power_supply * psy,const struct power_supply_ext * ext,void * data,enum power_supply_property psp)182 static int cros_chctl_psy_prop_is_writeable(struct power_supply *psy,
183 					    const struct power_supply_ext *ext,
184 					    void *data,
185 					    enum power_supply_property psp)
186 {
187 	return true;
188 }
189 
190 #define DEFINE_CROS_CHCTL_POWER_SUPPLY_EXTENSION(_name, ...)			\
191 	static const enum power_supply_property _name ## _props[] = {		\
192 		__VA_ARGS__,							\
193 	};									\
194 										\
195 	static const struct power_supply_ext _name = {				\
196 		.name			= "cros-charge-control",		\
197 		.properties		= _name ## _props,			\
198 		.num_properties		= ARRAY_SIZE(_name ## _props),		\
199 		.charge_behaviours	= EC_CHARGE_CONTROL_BEHAVIOURS,		\
200 		.get_property		= cros_chctl_psy_ext_get_prop,		\
201 		.set_property		= cros_chctl_psy_ext_set_prop,		\
202 		.property_is_writeable	= cros_chctl_psy_prop_is_writeable,	\
203 	}
204 
205 DEFINE_CROS_CHCTL_POWER_SUPPLY_EXTENSION(cros_chctl_psy_ext_v1,
206 	POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR
207 );
208 
209 DEFINE_CROS_CHCTL_POWER_SUPPLY_EXTENSION(cros_chctl_psy_ext_v2,
210 	POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR,
211 	POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD
212 );
213 
214 DEFINE_CROS_CHCTL_POWER_SUPPLY_EXTENSION(cros_chctl_psy_ext_v3,
215 	POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR,
216 	POWER_SUPPLY_PROP_CHARGE_CONTROL_START_THRESHOLD,
217 	POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD
218 );
219 
cros_chctl_add_battery(struct power_supply * battery,struct acpi_battery_hook * hook)220 static int cros_chctl_add_battery(struct power_supply *battery, struct acpi_battery_hook *hook)
221 {
222 	struct cros_chctl_priv *priv = container_of(hook, struct cros_chctl_priv, battery_hook);
223 
224 	if (priv->hooked_battery)
225 		return 0;
226 
227 	priv->hooked_battery = battery;
228 	return power_supply_register_extension(battery, priv->psy_ext, priv->dev, priv);
229 }
230 
cros_chctl_remove_battery(struct power_supply * battery,struct acpi_battery_hook * hook)231 static int cros_chctl_remove_battery(struct power_supply *battery, struct acpi_battery_hook *hook)
232 {
233 	struct cros_chctl_priv *priv = container_of(hook, struct cros_chctl_priv, battery_hook);
234 
235 	if (priv->hooked_battery == battery) {
236 		power_supply_unregister_extension(battery, priv->psy_ext);
237 		priv->hooked_battery = NULL;
238 	}
239 
240 	return 0;
241 }
242 
243 static bool probe_with_fwk_charge_control;
244 module_param(probe_with_fwk_charge_control, bool, 0644);
245 MODULE_PARM_DESC(probe_with_fwk_charge_control,
246 		 "Probe the driver in the presence of the custom Framework EC charge control");
247 
cros_chctl_fwk_charge_control_versions(struct cros_ec_device * cros_ec)248 static int cros_chctl_fwk_charge_control_versions(struct cros_ec_device *cros_ec)
249 {
250 	if (!dmi_match(DMI_SYS_VENDOR, "Framework"))
251 		return 0;
252 
253 	return cros_ec_get_cmd_versions(cros_ec, 0x3E03 /* FW_EC_CMD_CHARGE_LIMIT */);
254 }
255 
cros_chctl_probe(struct platform_device * pdev)256 static int cros_chctl_probe(struct platform_device *pdev)
257 {
258 	struct device *dev = &pdev->dev;
259 	struct cros_ec_dev *ec_dev = dev_get_drvdata(dev->parent);
260 	struct cros_ec_device *cros_ec = ec_dev->ec_dev;
261 	struct cros_chctl_priv *priv;
262 	int ret;
263 
264 	ret = cros_chctl_fwk_charge_control_versions(cros_ec);
265 	if (ret < 0)
266 		return ret;
267 	if (ret > 0 && !probe_with_fwk_charge_control) {
268 		dev_info(dev, "Framework charge control detected, preventing load\n");
269 		return -ENODEV;
270 	}
271 
272 	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
273 	if (!priv)
274 		return -ENOMEM;
275 
276 	ret = devm_mutex_init(dev, &priv->lock);
277 	if (ret)
278 		return ret;
279 
280 	ret = cros_ec_get_cmd_versions(cros_ec, EC_CMD_CHARGE_CONTROL);
281 	if (ret < 0)
282 		return ret;
283 	else if (ret & EC_VER_MASK(3))
284 		priv->cmd_version = 3;
285 	else if (ret & EC_VER_MASK(2))
286 		priv->cmd_version = 2;
287 	else if (ret & EC_VER_MASK(1))
288 		priv->cmd_version = 1;
289 	else
290 		return -ENODEV;
291 
292 	dev_dbg(dev, "Command version: %u\n", (unsigned int)priv->cmd_version);
293 
294 	priv->dev = dev;
295 	priv->cros_ec = cros_ec;
296 
297 	if (priv->cmd_version == 1)
298 		priv->psy_ext = &cros_chctl_psy_ext_v1;
299 	else if (priv->cmd_version == 2)
300 		priv->psy_ext = &cros_chctl_psy_ext_v2;
301 	else
302 		priv->psy_ext = &cros_chctl_psy_ext_v3;
303 
304 	priv->battery_hook.name = dev_name(dev);
305 	priv->battery_hook.add_battery = cros_chctl_add_battery;
306 	priv->battery_hook.remove_battery = cros_chctl_remove_battery;
307 
308 	priv->current_behaviour = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO;
309 	priv->current_start_threshold = 0;
310 	priv->current_end_threshold = 100;
311 
312 	/* Bring EC into well-known state */
313 	scoped_guard(mutex, &priv->lock)
314 		ret = cros_chctl_configure_ec(priv);
315 	if (ret < 0)
316 		return ret;
317 
318 	return devm_battery_hook_register(dev, &priv->battery_hook);
319 }
320 
321 static const struct platform_device_id cros_chctl_id[] = {
322 	{ .name = "cros-charge-control" },
323 	{ }
324 };
325 MODULE_DEVICE_TABLE(platform, cros_chctl_id);
326 
327 static struct platform_driver cros_chctl_driver = {
328 	.driver.name	= "cros-charge-control",
329 	.probe		= cros_chctl_probe,
330 	.id_table	= cros_chctl_id,
331 };
332 module_platform_driver(cros_chctl_driver);
333 
334 MODULE_DESCRIPTION("ChromeOS EC charge control");
335 MODULE_AUTHOR("Thomas Weißschuh <linux@weissschuh.net>");
336 MODULE_LICENSE("GPL");
337