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