1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * Bus for USB Type-C Alternate Modes |
4 | * |
5 | * Copyright (C) 2018 Intel Corporation |
6 | * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com> |
7 | */ |
8 | |
9 | #include <linux/usb/pd_vdo.h> |
10 | |
11 | #include "bus.h" |
12 | #include "class.h" |
13 | #include "mux.h" |
14 | #include "retimer.h" |
15 | |
16 | static inline int |
17 | typec_altmode_set_retimer(struct altmode *alt, unsigned long conf, void *data) |
18 | { |
19 | struct typec_retimer_state state; |
20 | |
21 | if (!alt->retimer) |
22 | return 0; |
23 | |
24 | state.alt = &alt->adev; |
25 | state.mode = conf; |
26 | state.data = data; |
27 | |
28 | return typec_retimer_set(retimer: alt->retimer, state: &state); |
29 | } |
30 | |
31 | static inline int |
32 | typec_altmode_set_mux(struct altmode *alt, unsigned long conf, void *data) |
33 | { |
34 | struct typec_mux_state state; |
35 | |
36 | if (!alt->mux) |
37 | return 0; |
38 | |
39 | state.alt = &alt->adev; |
40 | state.mode = conf; |
41 | state.data = data; |
42 | |
43 | return typec_mux_set(mux: alt->mux, state: &state); |
44 | } |
45 | |
46 | /* Wrapper to set various Type-C port switches together. */ |
47 | static inline int |
48 | typec_altmode_set_switches(struct altmode *alt, unsigned long conf, void *data) |
49 | { |
50 | int ret; |
51 | |
52 | ret = typec_altmode_set_retimer(alt, conf, data); |
53 | if (ret) |
54 | return ret; |
55 | |
56 | return typec_altmode_set_mux(alt, conf, data); |
57 | } |
58 | |
59 | static int typec_altmode_set_state(struct typec_altmode *adev, |
60 | unsigned long conf, void *data) |
61 | { |
62 | bool is_port = is_typec_port(adev->dev.parent); |
63 | struct altmode *port_altmode; |
64 | |
65 | port_altmode = is_port ? to_altmode(adev) : to_altmode(adev)->partner; |
66 | |
67 | return typec_altmode_set_switches(alt: port_altmode, conf, data); |
68 | } |
69 | |
70 | /* -------------------------------------------------------------------------- */ |
71 | /* Common API */ |
72 | |
73 | /** |
74 | * typec_altmode_notify - Communication between the OS and alternate mode driver |
75 | * @adev: Handle to the alternate mode |
76 | * @conf: Alternate mode specific configuration value |
77 | * @data: Alternate mode specific data |
78 | * |
79 | * The primary purpose for this function is to allow the alternate mode drivers |
80 | * to tell which pin configuration has been negotiated with the partner. That |
81 | * information will then be used for example to configure the muxes. |
82 | * Communication to the other direction is also possible, and low level device |
83 | * drivers can also send notifications to the alternate mode drivers. The actual |
84 | * communication will be specific for every SVID. |
85 | */ |
86 | int typec_altmode_notify(struct typec_altmode *adev, |
87 | unsigned long conf, void *data) |
88 | { |
89 | bool is_port; |
90 | struct altmode *altmode; |
91 | struct altmode *partner; |
92 | int ret; |
93 | |
94 | if (!adev) |
95 | return 0; |
96 | |
97 | altmode = to_altmode(adev); |
98 | |
99 | if (!altmode->partner) |
100 | return -ENODEV; |
101 | |
102 | is_port = is_typec_port(adev->dev.parent); |
103 | partner = altmode->partner; |
104 | |
105 | ret = typec_altmode_set_switches(alt: is_port ? altmode : partner, conf, data); |
106 | if (ret) |
107 | return ret; |
108 | |
109 | if (partner->adev.ops && partner->adev.ops->notify) |
110 | return partner->adev.ops->notify(&partner->adev, conf, data); |
111 | |
112 | return 0; |
113 | } |
114 | EXPORT_SYMBOL_GPL(typec_altmode_notify); |
115 | |
116 | /** |
117 | * typec_altmode_enter - Enter Mode |
118 | * @adev: The alternate mode |
119 | * @vdo: VDO for the Enter Mode command |
120 | * |
121 | * The alternate mode drivers use this function to enter mode. The port drivers |
122 | * use this to inform the alternate mode drivers that the partner has initiated |
123 | * Enter Mode command. If the alternate mode does not require VDO, @vdo must be |
124 | * NULL. |
125 | */ |
126 | int typec_altmode_enter(struct typec_altmode *adev, u32 *vdo) |
127 | { |
128 | struct altmode *partner = to_altmode(adev)->partner; |
129 | struct typec_altmode *pdev = &partner->adev; |
130 | int ret; |
131 | |
132 | if (!adev || adev->active) |
133 | return 0; |
134 | |
135 | if (!pdev->ops || !pdev->ops->enter) |
136 | return -EOPNOTSUPP; |
137 | |
138 | if (is_typec_port(pdev->dev.parent) && !pdev->active) |
139 | return -EPERM; |
140 | |
141 | /* Moving to USB Safe State */ |
142 | ret = typec_altmode_set_state(adev, conf: TYPEC_STATE_SAFE, NULL); |
143 | if (ret) |
144 | return ret; |
145 | |
146 | /* Enter Mode */ |
147 | return pdev->ops->enter(pdev, vdo); |
148 | } |
149 | EXPORT_SYMBOL_GPL(typec_altmode_enter); |
150 | |
151 | /** |
152 | * typec_altmode_exit - Exit Mode |
153 | * @adev: The alternate mode |
154 | * |
155 | * The partner of @adev has initiated Exit Mode command. |
156 | */ |
157 | int typec_altmode_exit(struct typec_altmode *adev) |
158 | { |
159 | struct altmode *partner = to_altmode(adev)->partner; |
160 | struct typec_altmode *pdev = &partner->adev; |
161 | int ret; |
162 | |
163 | if (!adev || !adev->active) |
164 | return 0; |
165 | |
166 | if (!pdev->ops || !pdev->ops->exit) |
167 | return -EOPNOTSUPP; |
168 | |
169 | /* Moving to USB Safe State */ |
170 | ret = typec_altmode_set_state(adev, conf: TYPEC_STATE_SAFE, NULL); |
171 | if (ret) |
172 | return ret; |
173 | |
174 | /* Exit Mode command */ |
175 | return pdev->ops->exit(pdev); |
176 | } |
177 | EXPORT_SYMBOL_GPL(typec_altmode_exit); |
178 | |
179 | /** |
180 | * typec_altmode_attention - Attention command |
181 | * @adev: The alternate mode |
182 | * @vdo: VDO for the Attention command |
183 | * |
184 | * Notifies the partner of @adev about Attention command. |
185 | */ |
186 | int typec_altmode_attention(struct typec_altmode *adev, u32 vdo) |
187 | { |
188 | struct altmode *partner = to_altmode(adev)->partner; |
189 | struct typec_altmode *pdev; |
190 | |
191 | if (!partner) |
192 | return -ENODEV; |
193 | |
194 | pdev = &partner->adev; |
195 | |
196 | if (pdev->ops && pdev->ops->attention) |
197 | pdev->ops->attention(pdev, vdo); |
198 | |
199 | return 0; |
200 | } |
201 | EXPORT_SYMBOL_GPL(typec_altmode_attention); |
202 | |
203 | /** |
204 | * typec_altmode_vdm - Send Vendor Defined Messages (VDM) to the partner |
205 | * @adev: Alternate mode handle |
206 | * @header: VDM Header |
207 | * @vdo: Array of Vendor Defined Data Objects |
208 | * @count: Number of Data Objects |
209 | * |
210 | * The alternate mode drivers use this function for SVID specific communication |
211 | * with the partner. The port drivers use it to deliver the Structured VDMs |
212 | * received from the partners to the alternate mode drivers. |
213 | */ |
214 | int typec_altmode_vdm(struct typec_altmode *adev, |
215 | const u32 , const u32 *vdo, int count) |
216 | { |
217 | struct typec_altmode *pdev; |
218 | struct altmode *altmode; |
219 | |
220 | if (!adev) |
221 | return 0; |
222 | |
223 | altmode = to_altmode(adev); |
224 | |
225 | if (!altmode->partner) |
226 | return -ENODEV; |
227 | |
228 | pdev = &altmode->partner->adev; |
229 | |
230 | if (!pdev->ops || !pdev->ops->vdm) |
231 | return -EOPNOTSUPP; |
232 | |
233 | return pdev->ops->vdm(pdev, header, vdo, count); |
234 | } |
235 | EXPORT_SYMBOL_GPL(typec_altmode_vdm); |
236 | |
237 | const struct typec_altmode * |
238 | typec_altmode_get_partner(struct typec_altmode *adev) |
239 | { |
240 | if (!adev || !to_altmode(adev)->partner) |
241 | return NULL; |
242 | |
243 | return &to_altmode(adev)->partner->adev; |
244 | } |
245 | EXPORT_SYMBOL_GPL(typec_altmode_get_partner); |
246 | |
247 | /* -------------------------------------------------------------------------- */ |
248 | /* API for cable alternate modes */ |
249 | |
250 | /** |
251 | * typec_cable_altmode_enter - Enter Mode |
252 | * @adev: The alternate mode |
253 | * @sop: Cable plug target for Enter Mode command |
254 | * @vdo: VDO for the Enter Mode command |
255 | * |
256 | * Alternate mode drivers use this function to enter mode on the cable plug. |
257 | * If the alternate mode does not require VDO, @vdo must be NULL. |
258 | */ |
259 | int typec_cable_altmode_enter(struct typec_altmode *adev, enum typec_plug_index sop, u32 *vdo) |
260 | { |
261 | struct altmode *partner = to_altmode(adev)->partner; |
262 | struct typec_altmode *pdev; |
263 | |
264 | if (!adev || adev->active) |
265 | return 0; |
266 | |
267 | if (!partner) |
268 | return -ENODEV; |
269 | |
270 | pdev = &partner->adev; |
271 | |
272 | if (!pdev->active) |
273 | return -EPERM; |
274 | |
275 | if (!pdev->cable_ops || !pdev->cable_ops->enter) |
276 | return -EOPNOTSUPP; |
277 | |
278 | return pdev->cable_ops->enter(pdev, sop, vdo); |
279 | } |
280 | EXPORT_SYMBOL_GPL(typec_cable_altmode_enter); |
281 | |
282 | /** |
283 | * typec_cable_altmode_exit - Exit Mode |
284 | * @adev: The alternate mode |
285 | * @sop: Cable plug target for Exit Mode command |
286 | * |
287 | * The alternate mode drivers use this function to exit mode on the cable plug. |
288 | */ |
289 | int typec_cable_altmode_exit(struct typec_altmode *adev, enum typec_plug_index sop) |
290 | { |
291 | struct altmode *partner = to_altmode(adev)->partner; |
292 | struct typec_altmode *pdev; |
293 | |
294 | if (!adev || !adev->active) |
295 | return 0; |
296 | |
297 | if (!partner) |
298 | return -ENODEV; |
299 | |
300 | pdev = &partner->adev; |
301 | |
302 | if (!pdev->cable_ops || !pdev->cable_ops->exit) |
303 | return -EOPNOTSUPP; |
304 | |
305 | return pdev->cable_ops->exit(pdev, sop); |
306 | } |
307 | EXPORT_SYMBOL_GPL(typec_cable_altmode_exit); |
308 | |
309 | /** |
310 | * typec_cable_altmode_vdm - Send Vendor Defined Messages (VDM) between the cable plug and port. |
311 | * @adev: Alternate mode handle |
312 | * @sop: Cable plug target for VDM |
313 | * @header: VDM Header |
314 | * @vdo: Array of Vendor Defined Data Objects |
315 | * @count: Number of Data Objects |
316 | * |
317 | * The alternate mode drivers use this function for SVID specific communication |
318 | * with the cable plugs. The port drivers use it to deliver the Structured VDMs |
319 | * received from the cable plugs to the alternate mode drivers. |
320 | */ |
321 | int typec_cable_altmode_vdm(struct typec_altmode *adev, enum typec_plug_index sop, |
322 | const u32 , const u32 *vdo, int count) |
323 | { |
324 | struct altmode *altmode; |
325 | struct typec_altmode *pdev; |
326 | |
327 | if (!adev) |
328 | return 0; |
329 | |
330 | altmode = to_altmode(adev); |
331 | |
332 | if (is_typec_plug(adev->dev.parent)) { |
333 | if (!altmode->partner) |
334 | return -ENODEV; |
335 | pdev = &altmode->partner->adev; |
336 | } else { |
337 | if (!altmode->plug[sop]) |
338 | return -ENODEV; |
339 | pdev = &altmode->plug[sop]->adev; |
340 | } |
341 | |
342 | if (!pdev->cable_ops || !pdev->cable_ops->vdm) |
343 | return -EOPNOTSUPP; |
344 | |
345 | return pdev->cable_ops->vdm(pdev, sop, header, vdo, count); |
346 | } |
347 | EXPORT_SYMBOL_GPL(typec_cable_altmode_vdm); |
348 | |
349 | /* -------------------------------------------------------------------------- */ |
350 | /* API for the alternate mode drivers */ |
351 | |
352 | /** |
353 | * typec_altmode_get_plug - Find cable plug alternate mode |
354 | * @adev: Handle to partner alternate mode |
355 | * @index: Cable plug index |
356 | * |
357 | * Increment reference count for cable plug alternate mode device. Returns |
358 | * handle to the cable plug alternate mode, or NULL if none is found. |
359 | */ |
360 | struct typec_altmode *typec_altmode_get_plug(struct typec_altmode *adev, |
361 | enum typec_plug_index index) |
362 | { |
363 | struct altmode *port = to_altmode(adev)->partner; |
364 | |
365 | if (port->plug[index]) { |
366 | get_device(dev: &port->plug[index]->adev.dev); |
367 | return &port->plug[index]->adev; |
368 | } |
369 | |
370 | return NULL; |
371 | } |
372 | EXPORT_SYMBOL_GPL(typec_altmode_get_plug); |
373 | |
374 | /** |
375 | * typec_altmode_put_plug - Decrement cable plug alternate mode reference count |
376 | * @plug: Handle to the cable plug alternate mode |
377 | */ |
378 | void typec_altmode_put_plug(struct typec_altmode *plug) |
379 | { |
380 | if (plug) |
381 | put_device(dev: &plug->dev); |
382 | } |
383 | EXPORT_SYMBOL_GPL(typec_altmode_put_plug); |
384 | |
385 | int __typec_altmode_register_driver(struct typec_altmode_driver *drv, |
386 | struct module *module) |
387 | { |
388 | if (!drv->probe) |
389 | return -EINVAL; |
390 | |
391 | drv->driver.owner = module; |
392 | drv->driver.bus = &typec_bus; |
393 | |
394 | return driver_register(drv: &drv->driver); |
395 | } |
396 | EXPORT_SYMBOL_GPL(__typec_altmode_register_driver); |
397 | |
398 | void typec_altmode_unregister_driver(struct typec_altmode_driver *drv) |
399 | { |
400 | driver_unregister(drv: &drv->driver); |
401 | } |
402 | EXPORT_SYMBOL_GPL(typec_altmode_unregister_driver); |
403 | |
404 | /* -------------------------------------------------------------------------- */ |
405 | /* API for the port drivers */ |
406 | |
407 | /** |
408 | * typec_match_altmode - Match SVID and mode to an array of alternate modes |
409 | * @altmodes: Array of alternate modes |
410 | * @n: Number of elements in the array, or -1 for NULL terminated arrays |
411 | * @svid: Standard or Vendor ID to match with |
412 | * @mode: Mode to match with |
413 | * |
414 | * Return pointer to an alternate mode with SVID matching @svid, or NULL when no |
415 | * match is found. |
416 | */ |
417 | struct typec_altmode *typec_match_altmode(struct typec_altmode **altmodes, |
418 | size_t n, u16 svid, u8 mode) |
419 | { |
420 | int i; |
421 | |
422 | for (i = 0; i < n; i++) { |
423 | if (!altmodes[i]) |
424 | break; |
425 | if (altmodes[i]->svid == svid && altmodes[i]->mode == mode) |
426 | return altmodes[i]; |
427 | } |
428 | |
429 | return NULL; |
430 | } |
431 | EXPORT_SYMBOL_GPL(typec_match_altmode); |
432 | |
433 | /* -------------------------------------------------------------------------- */ |
434 | |
435 | static ssize_t |
436 | description_show(struct device *dev, struct device_attribute *attr, char *buf) |
437 | { |
438 | struct typec_altmode *alt = to_typec_altmode(dev); |
439 | |
440 | return sprintf(buf, fmt: "%s\n" , alt->desc ? alt->desc : "" ); |
441 | } |
442 | static DEVICE_ATTR_RO(description); |
443 | |
444 | static struct attribute *typec_attrs[] = { |
445 | &dev_attr_description.attr, |
446 | NULL |
447 | }; |
448 | ATTRIBUTE_GROUPS(typec); |
449 | |
450 | static int typec_match(struct device *dev, struct device_driver *driver) |
451 | { |
452 | struct typec_altmode_driver *drv = to_altmode_driver(driver); |
453 | struct typec_altmode *altmode = to_typec_altmode(dev); |
454 | const struct typec_device_id *id; |
455 | |
456 | for (id = drv->id_table; id->svid; id++) |
457 | if (id->svid == altmode->svid && |
458 | (id->mode == TYPEC_ANY_MODE || id->mode == altmode->mode)) |
459 | return 1; |
460 | return 0; |
461 | } |
462 | |
463 | static int typec_uevent(const struct device *dev, struct kobj_uevent_env *env) |
464 | { |
465 | const struct typec_altmode *altmode = to_typec_altmode(dev); |
466 | |
467 | if (add_uevent_var(env, format: "SVID=%04X" , altmode->svid)) |
468 | return -ENOMEM; |
469 | |
470 | if (add_uevent_var(env, format: "MODE=%u" , altmode->mode)) |
471 | return -ENOMEM; |
472 | |
473 | return add_uevent_var(env, format: "MODALIAS=typec:id%04Xm%02X" , |
474 | altmode->svid, altmode->mode); |
475 | } |
476 | |
477 | static int typec_altmode_create_links(struct altmode *alt) |
478 | { |
479 | struct device *port_dev = &alt->partner->adev.dev; |
480 | struct device *dev = &alt->adev.dev; |
481 | int err; |
482 | |
483 | err = sysfs_create_link(kobj: &dev->kobj, target: &port_dev->kobj, name: "port" ); |
484 | if (err) |
485 | return err; |
486 | |
487 | err = sysfs_create_link(kobj: &port_dev->kobj, target: &dev->kobj, name: "partner" ); |
488 | if (err) |
489 | sysfs_remove_link(kobj: &dev->kobj, name: "port" ); |
490 | |
491 | return err; |
492 | } |
493 | |
494 | static void typec_altmode_remove_links(struct altmode *alt) |
495 | { |
496 | sysfs_remove_link(kobj: &alt->partner->adev.dev.kobj, name: "partner" ); |
497 | sysfs_remove_link(kobj: &alt->adev.dev.kobj, name: "port" ); |
498 | } |
499 | |
500 | static int typec_probe(struct device *dev) |
501 | { |
502 | struct typec_altmode_driver *drv = to_altmode_driver(dev->driver); |
503 | struct typec_altmode *adev = to_typec_altmode(dev); |
504 | struct altmode *altmode = to_altmode(adev); |
505 | int ret; |
506 | |
507 | /* Fail if the port does not support the alternate mode */ |
508 | if (!altmode->partner) |
509 | return -ENODEV; |
510 | |
511 | ret = typec_altmode_create_links(alt: altmode); |
512 | if (ret) { |
513 | dev_warn(dev, "failed to create symlinks\n" ); |
514 | return ret; |
515 | } |
516 | |
517 | ret = drv->probe(adev); |
518 | if (ret) |
519 | typec_altmode_remove_links(alt: altmode); |
520 | |
521 | return ret; |
522 | } |
523 | |
524 | static void typec_remove(struct device *dev) |
525 | { |
526 | struct typec_altmode_driver *drv = to_altmode_driver(dev->driver); |
527 | struct typec_altmode *adev = to_typec_altmode(dev); |
528 | struct altmode *altmode = to_altmode(adev); |
529 | |
530 | typec_altmode_remove_links(alt: altmode); |
531 | |
532 | if (drv->remove) |
533 | drv->remove(to_typec_altmode(dev)); |
534 | |
535 | if (adev->active) { |
536 | WARN_ON(typec_altmode_set_state(adev, TYPEC_STATE_SAFE, NULL)); |
537 | typec_altmode_update_active(alt: adev, active: false); |
538 | } |
539 | |
540 | adev->desc = NULL; |
541 | adev->ops = NULL; |
542 | } |
543 | |
544 | const struct bus_type typec_bus = { |
545 | .name = "typec" , |
546 | .dev_groups = typec_groups, |
547 | .match = typec_match, |
548 | .uevent = typec_uevent, |
549 | .probe = typec_probe, |
550 | .remove = typec_remove, |
551 | }; |
552 | |