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