Commit | Line | Data |
---|---|---|
56d629af DN |
1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* | |
3 | * Power supply driver for ChromeOS EC based Peripheral Device Charger. | |
4 | * | |
5 | * Copyright 2020 Google LLC. | |
6 | */ | |
7 | ||
d6486a13 | 8 | #include <linux/mod_devicetable.h> |
56d629af DN |
9 | #include <linux/module.h> |
10 | #include <linux/notifier.h> | |
11 | #include <linux/platform_data/cros_ec_commands.h> | |
12 | #include <linux/platform_data/cros_ec_proto.h> | |
13 | #include <linux/platform_device.h> | |
14 | #include <linux/power_supply.h> | |
15 | #include <linux/slab.h> | |
16 | #include <linux/stringify.h> | |
17 | #include <linux/types.h> | |
84530100 | 18 | #include <asm/unaligned.h> |
56d629af DN |
19 | |
20 | #define DRV_NAME "cros-ec-pchg" | |
21 | #define PCHG_DIR_PREFIX "peripheral" | |
22 | #define PCHG_DIR_NAME PCHG_DIR_PREFIX "%d" | |
23 | #define PCHG_DIR_NAME_LENGTH \ | |
24 | sizeof(PCHG_DIR_PREFIX __stringify(EC_PCHG_MAX_PORTS)) | |
25 | #define PCHG_CACHE_UPDATE_DELAY msecs_to_jiffies(500) | |
26 | ||
27 | struct port_data { | |
28 | int port_number; | |
29 | char name[PCHG_DIR_NAME_LENGTH]; | |
30 | struct power_supply *psy; | |
31 | struct power_supply_desc psy_desc; | |
32 | int psy_status; | |
33 | int battery_percentage; | |
34 | int charge_type; | |
35 | struct charger_data *charger; | |
36 | unsigned long last_update; | |
37 | }; | |
38 | ||
39 | struct charger_data { | |
40 | struct device *dev; | |
41 | struct cros_ec_dev *ec_dev; | |
42 | struct cros_ec_device *ec_device; | |
43 | int num_registered_psy; | |
44 | struct port_data *ports[EC_PCHG_MAX_PORTS]; | |
45 | struct notifier_block notifier; | |
46 | }; | |
47 | ||
48 | static enum power_supply_property cros_pchg_props[] = { | |
49 | POWER_SUPPLY_PROP_STATUS, | |
50 | POWER_SUPPLY_PROP_CHARGE_TYPE, | |
51 | POWER_SUPPLY_PROP_CAPACITY, | |
52 | POWER_SUPPLY_PROP_SCOPE, | |
53 | }; | |
54 | ||
55 | static int cros_pchg_ec_command(const struct charger_data *charger, | |
56 | unsigned int version, | |
57 | unsigned int command, | |
58 | const void *outdata, | |
59 | unsigned int outsize, | |
60 | void *indata, | |
61 | unsigned int insize) | |
62 | { | |
63 | struct cros_ec_dev *ec_dev = charger->ec_dev; | |
64 | struct cros_ec_command *msg; | |
65 | int ret; | |
66 | ||
23c46bab | 67 | msg = kzalloc(struct_size(msg, data, max(outsize, insize)), GFP_KERNEL); |
56d629af DN |
68 | if (!msg) |
69 | return -ENOMEM; | |
70 | ||
71 | msg->version = version; | |
72 | msg->command = ec_dev->cmd_offset + command; | |
73 | msg->outsize = outsize; | |
74 | msg->insize = insize; | |
75 | ||
76 | if (outsize) | |
77 | memcpy(msg->data, outdata, outsize); | |
78 | ||
79 | ret = cros_ec_cmd_xfer_status(charger->ec_device, msg); | |
80 | if (ret >= 0 && insize) | |
81 | memcpy(indata, msg->data, insize); | |
82 | ||
83 | kfree(msg); | |
84 | return ret; | |
85 | } | |
86 | ||
87 | static const unsigned int pchg_cmd_version = 1; | |
88 | ||
89 | static bool cros_pchg_cmd_ver_check(const struct charger_data *charger) | |
90 | { | |
91 | struct ec_params_get_cmd_versions_v1 req; | |
92 | struct ec_response_get_cmd_versions rsp; | |
93 | int ret; | |
94 | ||
95 | req.cmd = EC_CMD_PCHG; | |
96 | ret = cros_pchg_ec_command(charger, 1, EC_CMD_GET_CMD_VERSIONS, | |
97 | &req, sizeof(req), &rsp, sizeof(rsp)); | |
98 | if (ret < 0) { | |
99 | dev_warn(charger->dev, | |
100 | "Unable to get versions of EC_CMD_PCHG (err:%d)\n", | |
101 | ret); | |
102 | return false; | |
103 | } | |
104 | ||
105 | return !!(rsp.version_mask & BIT(pchg_cmd_version)); | |
106 | } | |
107 | ||
108 | static int cros_pchg_port_count(const struct charger_data *charger) | |
109 | { | |
110 | struct ec_response_pchg_count rsp; | |
111 | int ret; | |
112 | ||
113 | ret = cros_pchg_ec_command(charger, 0, EC_CMD_PCHG_COUNT, | |
114 | NULL, 0, &rsp, sizeof(rsp)); | |
115 | if (ret < 0) { | |
116 | dev_warn(charger->dev, | |
117 | "Unable to get number or ports (err:%d)\n", ret); | |
118 | return ret; | |
119 | } | |
120 | ||
121 | return rsp.port_count; | |
122 | } | |
123 | ||
124 | static int cros_pchg_get_status(struct port_data *port) | |
125 | { | |
126 | struct charger_data *charger = port->charger; | |
127 | struct ec_params_pchg req; | |
128 | struct ec_response_pchg rsp; | |
129 | struct device *dev = charger->dev; | |
130 | int old_status = port->psy_status; | |
131 | int old_percentage = port->battery_percentage; | |
132 | int ret; | |
133 | ||
134 | req.port = port->port_number; | |
135 | ret = cros_pchg_ec_command(charger, pchg_cmd_version, EC_CMD_PCHG, | |
136 | &req, sizeof(req), &rsp, sizeof(rsp)); | |
137 | if (ret < 0) { | |
138 | dev_err(dev, "Unable to get port.%d status (err:%d)\n", | |
139 | port->port_number, ret); | |
140 | return ret; | |
141 | } | |
142 | ||
143 | switch (rsp.state) { | |
144 | case PCHG_STATE_RESET: | |
145 | case PCHG_STATE_INITIALIZED: | |
146 | case PCHG_STATE_ENABLED: | |
147 | default: | |
148 | port->psy_status = POWER_SUPPLY_STATUS_UNKNOWN; | |
149 | port->charge_type = POWER_SUPPLY_CHARGE_TYPE_NONE; | |
150 | break; | |
151 | case PCHG_STATE_DETECTED: | |
152 | port->psy_status = POWER_SUPPLY_STATUS_CHARGING; | |
153 | port->charge_type = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; | |
154 | break; | |
155 | case PCHG_STATE_CHARGING: | |
156 | port->psy_status = POWER_SUPPLY_STATUS_CHARGING; | |
157 | port->charge_type = POWER_SUPPLY_CHARGE_TYPE_STANDARD; | |
158 | break; | |
159 | case PCHG_STATE_FULL: | |
160 | port->psy_status = POWER_SUPPLY_STATUS_FULL; | |
161 | port->charge_type = POWER_SUPPLY_CHARGE_TYPE_NONE; | |
162 | break; | |
163 | } | |
164 | ||
165 | port->battery_percentage = rsp.battery_percentage; | |
166 | ||
167 | if (port->psy_status != old_status || | |
168 | port->battery_percentage != old_percentage) | |
169 | power_supply_changed(port->psy); | |
170 | ||
171 | dev_dbg(dev, | |
172 | "Port %d: state=%d battery=%d%%\n", | |
173 | port->port_number, rsp.state, rsp.battery_percentage); | |
174 | ||
175 | return 0; | |
176 | } | |
177 | ||
178 | static int cros_pchg_get_port_status(struct port_data *port, bool ratelimit) | |
179 | { | |
180 | int ret; | |
181 | ||
182 | if (ratelimit && | |
183 | time_is_after_jiffies(port->last_update + PCHG_CACHE_UPDATE_DELAY)) | |
184 | return 0; | |
185 | ||
186 | ret = cros_pchg_get_status(port); | |
187 | if (ret < 0) | |
188 | return ret; | |
189 | ||
190 | port->last_update = jiffies; | |
191 | ||
192 | return ret; | |
193 | } | |
194 | ||
195 | static int cros_pchg_get_prop(struct power_supply *psy, | |
196 | enum power_supply_property psp, | |
197 | union power_supply_propval *val) | |
198 | { | |
199 | struct port_data *port = power_supply_get_drvdata(psy); | |
200 | ||
201 | switch (psp) { | |
202 | case POWER_SUPPLY_PROP_STATUS: | |
203 | case POWER_SUPPLY_PROP_CAPACITY: | |
204 | case POWER_SUPPLY_PROP_CHARGE_TYPE: | |
205 | cros_pchg_get_port_status(port, true); | |
206 | break; | |
207 | default: | |
208 | break; | |
209 | } | |
210 | ||
211 | switch (psp) { | |
212 | case POWER_SUPPLY_PROP_STATUS: | |
213 | val->intval = port->psy_status; | |
214 | break; | |
215 | case POWER_SUPPLY_PROP_CAPACITY: | |
216 | val->intval = port->battery_percentage; | |
217 | break; | |
218 | case POWER_SUPPLY_PROP_CHARGE_TYPE: | |
219 | val->intval = port->charge_type; | |
220 | break; | |
221 | case POWER_SUPPLY_PROP_SCOPE: | |
222 | val->intval = POWER_SUPPLY_SCOPE_DEVICE; | |
223 | break; | |
224 | default: | |
225 | return -EINVAL; | |
226 | } | |
227 | ||
228 | return 0; | |
229 | } | |
230 | ||
97dd69b1 | 231 | static int cros_pchg_event(const struct charger_data *charger) |
56d629af DN |
232 | { |
233 | int i; | |
234 | ||
235 | for (i = 0; i < charger->num_registered_psy; i++) | |
236 | cros_pchg_get_port_status(charger->ports[i], false); | |
237 | ||
238 | return NOTIFY_OK; | |
239 | } | |
240 | ||
56d629af DN |
241 | static int cros_ec_notify(struct notifier_block *nb, |
242 | unsigned long queued_during_suspend, | |
243 | void *data) | |
244 | { | |
84530100 | 245 | struct cros_ec_device *ec_dev = data; |
56d629af DN |
246 | struct charger_data *charger = |
247 | container_of(nb, struct charger_data, notifier); | |
84530100 | 248 | u32 host_event; |
56d629af | 249 | |
84530100 DN |
250 | if (ec_dev->event_data.event_type != EC_MKBP_EVENT_PCHG || |
251 | ec_dev->event_size != sizeof(host_event)) | |
56d629af DN |
252 | return NOTIFY_DONE; |
253 | ||
84530100 | 254 | host_event = get_unaligned_le32(&ec_dev->event_data.data.host_event); |
56d629af | 255 | |
84530100 | 256 | if (!(host_event & EC_MKBP_PCHG_DEVICE_EVENT)) |
56d629af DN |
257 | return NOTIFY_DONE; |
258 | ||
97dd69b1 | 259 | return cros_pchg_event(charger); |
56d629af DN |
260 | } |
261 | ||
262 | static int cros_pchg_probe(struct platform_device *pdev) | |
263 | { | |
264 | struct device *dev = &pdev->dev; | |
265 | struct cros_ec_dev *ec_dev = dev_get_drvdata(dev->parent); | |
266 | struct cros_ec_device *ec_device = ec_dev->ec_dev; | |
267 | struct power_supply_desc *psy_desc; | |
268 | struct charger_data *charger; | |
269 | struct power_supply *psy; | |
270 | struct port_data *port; | |
271 | struct notifier_block *nb; | |
272 | int num_ports; | |
273 | int ret; | |
274 | int i; | |
275 | ||
276 | charger = devm_kzalloc(dev, sizeof(*charger), GFP_KERNEL); | |
277 | if (!charger) | |
278 | return -ENOMEM; | |
279 | ||
280 | charger->dev = dev; | |
281 | charger->ec_dev = ec_dev; | |
282 | charger->ec_device = ec_device; | |
283 | ||
97dd69b1 DN |
284 | platform_set_drvdata(pdev, charger); |
285 | ||
56d629af DN |
286 | ret = cros_pchg_port_count(charger); |
287 | if (ret <= 0) { | |
288 | /* | |
289 | * This feature is enabled by the EC and the kernel driver is | |
290 | * included by default for CrOS devices. Don't need to be loud | |
291 | * since this error can be normal. | |
292 | */ | |
293 | dev_info(dev, "No peripheral charge ports (err:%d)\n", ret); | |
294 | return -ENODEV; | |
295 | } | |
296 | ||
297 | if (!cros_pchg_cmd_ver_check(charger)) { | |
298 | dev_err(dev, "EC_CMD_PCHG version %d isn't available.\n", | |
299 | pchg_cmd_version); | |
300 | return -EOPNOTSUPP; | |
301 | } | |
302 | ||
303 | num_ports = ret; | |
304 | if (num_ports > EC_PCHG_MAX_PORTS) { | |
305 | dev_err(dev, "Too many peripheral charge ports (%d)\n", | |
306 | num_ports); | |
307 | return -ENOBUFS; | |
308 | } | |
309 | ||
310 | dev_info(dev, "%d peripheral charge ports found\n", num_ports); | |
311 | ||
312 | for (i = 0; i < num_ports; i++) { | |
313 | struct power_supply_config psy_cfg = {}; | |
314 | ||
315 | port = devm_kzalloc(dev, sizeof(*port), GFP_KERNEL); | |
316 | if (!port) | |
317 | return -ENOMEM; | |
318 | ||
319 | port->charger = charger; | |
320 | port->port_number = i; | |
321 | snprintf(port->name, sizeof(port->name), PCHG_DIR_NAME, i); | |
322 | ||
323 | psy_desc = &port->psy_desc; | |
324 | psy_desc->name = port->name; | |
325 | psy_desc->type = POWER_SUPPLY_TYPE_BATTERY; | |
326 | psy_desc->get_property = cros_pchg_get_prop; | |
327 | psy_desc->external_power_changed = NULL; | |
328 | psy_desc->properties = cros_pchg_props; | |
329 | psy_desc->num_properties = ARRAY_SIZE(cros_pchg_props); | |
330 | psy_cfg.drv_data = port; | |
331 | ||
332 | psy = devm_power_supply_register(dev, psy_desc, &psy_cfg); | |
333 | if (IS_ERR(psy)) | |
334 | return dev_err_probe(dev, PTR_ERR(psy), | |
335 | "Failed to register power supply\n"); | |
336 | port->psy = psy; | |
337 | ||
338 | charger->ports[charger->num_registered_psy++] = port; | |
339 | } | |
340 | ||
341 | if (!charger->num_registered_psy) | |
342 | return -ENODEV; | |
343 | ||
344 | nb = &charger->notifier; | |
345 | nb->notifier_call = cros_ec_notify; | |
346 | ret = blocking_notifier_chain_register(&ec_dev->ec_dev->event_notifier, | |
347 | nb); | |
348 | if (ret < 0) | |
349 | dev_err(dev, "Failed to register notifier (err:%d)\n", ret); | |
350 | ||
351 | return 0; | |
352 | } | |
353 | ||
97dd69b1 DN |
354 | #ifdef CONFIG_PM_SLEEP |
355 | static int __maybe_unused cros_pchg_resume(struct device *dev) | |
356 | { | |
357 | struct charger_data *charger = dev_get_drvdata(dev); | |
358 | ||
359 | /* | |
360 | * Sync all ports on resume in case reports from EC are lost during | |
361 | * the last suspend. | |
362 | */ | |
363 | cros_pchg_event(charger); | |
364 | ||
365 | return 0; | |
366 | } | |
367 | #endif | |
368 | ||
369 | static SIMPLE_DEV_PM_OPS(cros_pchg_pm_ops, NULL, cros_pchg_resume); | |
370 | ||
d6486a13 TBS |
371 | static const struct platform_device_id cros_pchg_id[] = { |
372 | { DRV_NAME, 0 }, | |
373 | {} | |
374 | }; | |
375 | MODULE_DEVICE_TABLE(platform, cros_pchg_id); | |
376 | ||
56d629af DN |
377 | static struct platform_driver cros_pchg_driver = { |
378 | .driver = { | |
379 | .name = DRV_NAME, | |
97dd69b1 | 380 | .pm = &cros_pchg_pm_ops, |
56d629af | 381 | }, |
d6486a13 TBS |
382 | .probe = cros_pchg_probe, |
383 | .id_table = cros_pchg_id, | |
56d629af DN |
384 | }; |
385 | ||
386 | module_platform_driver(cros_pchg_driver); | |
387 | ||
388 | MODULE_LICENSE("GPL"); | |
389 | MODULE_DESCRIPTION("ChromeOS EC peripheral device charger"); |