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