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