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 the alternate mode drivers */ |
249 | |
250 | /** |
251 | * typec_altmode_get_plug - Find cable plug alternate mode |
252 | * @adev: Handle to partner alternate mode |
253 | * @index: Cable plug index |
254 | * |
255 | * Increment reference count for cable plug alternate mode device. Returns |
256 | * handle to the cable plug alternate mode, or NULL if none is found. |
257 | */ |
258 | struct typec_altmode *typec_altmode_get_plug(struct typec_altmode *adev, |
259 | enum typec_plug_index index) |
260 | { |
261 | struct altmode *port = to_altmode(adev)->partner; |
262 | |
263 | if (port->plug[index]) { |
264 | get_device(dev: &port->plug[index]->adev.dev); |
265 | return &port->plug[index]->adev; |
266 | } |
267 | |
268 | return NULL; |
269 | } |
270 | EXPORT_SYMBOL_GPL(typec_altmode_get_plug); |
271 | |
272 | /** |
273 | * typec_altmode_put_plug - Decrement cable plug alternate mode reference count |
274 | * @plug: Handle to the cable plug alternate mode |
275 | */ |
276 | void typec_altmode_put_plug(struct typec_altmode *plug) |
277 | { |
278 | if (plug) |
279 | put_device(dev: &plug->dev); |
280 | } |
281 | EXPORT_SYMBOL_GPL(typec_altmode_put_plug); |
282 | |
283 | int __typec_altmode_register_driver(struct typec_altmode_driver *drv, |
284 | struct module *module) |
285 | { |
286 | if (!drv->probe) |
287 | return -EINVAL; |
288 | |
289 | drv->driver.owner = module; |
290 | drv->driver.bus = &typec_bus; |
291 | |
292 | return driver_register(drv: &drv->driver); |
293 | } |
294 | EXPORT_SYMBOL_GPL(__typec_altmode_register_driver); |
295 | |
296 | void typec_altmode_unregister_driver(struct typec_altmode_driver *drv) |
297 | { |
298 | driver_unregister(drv: &drv->driver); |
299 | } |
300 | EXPORT_SYMBOL_GPL(typec_altmode_unregister_driver); |
301 | |
302 | /* -------------------------------------------------------------------------- */ |
303 | /* API for the port drivers */ |
304 | |
305 | /** |
306 | * typec_match_altmode - Match SVID and mode to an array of alternate modes |
307 | * @altmodes: Array of alternate modes |
308 | * @n: Number of elements in the array, or -1 for NULL terminated arrays |
309 | * @svid: Standard or Vendor ID to match with |
310 | * @mode: Mode to match with |
311 | * |
312 | * Return pointer to an alternate mode with SVID matching @svid, or NULL when no |
313 | * match is found. |
314 | */ |
315 | struct typec_altmode *typec_match_altmode(struct typec_altmode **altmodes, |
316 | size_t n, u16 svid, u8 mode) |
317 | { |
318 | int i; |
319 | |
320 | for (i = 0; i < n; i++) { |
321 | if (!altmodes[i]) |
322 | break; |
323 | if (altmodes[i]->svid == svid && altmodes[i]->mode == mode) |
324 | return altmodes[i]; |
325 | } |
326 | |
327 | return NULL; |
328 | } |
329 | EXPORT_SYMBOL_GPL(typec_match_altmode); |
330 | |
331 | /* -------------------------------------------------------------------------- */ |
332 | |
333 | static ssize_t |
334 | description_show(struct device *dev, struct device_attribute *attr, char *buf) |
335 | { |
336 | struct typec_altmode *alt = to_typec_altmode(dev); |
337 | |
338 | return sprintf(buf, fmt: "%s\n" , alt->desc ? alt->desc : "" ); |
339 | } |
340 | static DEVICE_ATTR_RO(description); |
341 | |
342 | static struct attribute *typec_attrs[] = { |
343 | &dev_attr_description.attr, |
344 | NULL |
345 | }; |
346 | ATTRIBUTE_GROUPS(typec); |
347 | |
348 | static int typec_match(struct device *dev, struct device_driver *driver) |
349 | { |
350 | struct typec_altmode_driver *drv = to_altmode_driver(driver); |
351 | struct typec_altmode *altmode = to_typec_altmode(dev); |
352 | const struct typec_device_id *id; |
353 | |
354 | for (id = drv->id_table; id->svid; id++) |
355 | if (id->svid == altmode->svid && |
356 | (id->mode == TYPEC_ANY_MODE || id->mode == altmode->mode)) |
357 | return 1; |
358 | return 0; |
359 | } |
360 | |
361 | static int typec_uevent(const struct device *dev, struct kobj_uevent_env *env) |
362 | { |
363 | const struct typec_altmode *altmode = to_typec_altmode(dev); |
364 | |
365 | if (add_uevent_var(env, format: "SVID=%04X" , altmode->svid)) |
366 | return -ENOMEM; |
367 | |
368 | if (add_uevent_var(env, format: "MODE=%u" , altmode->mode)) |
369 | return -ENOMEM; |
370 | |
371 | return add_uevent_var(env, format: "MODALIAS=typec:id%04Xm%02X" , |
372 | altmode->svid, altmode->mode); |
373 | } |
374 | |
375 | static int typec_altmode_create_links(struct altmode *alt) |
376 | { |
377 | struct device *port_dev = &alt->partner->adev.dev; |
378 | struct device *dev = &alt->adev.dev; |
379 | int err; |
380 | |
381 | err = sysfs_create_link(kobj: &dev->kobj, target: &port_dev->kobj, name: "port" ); |
382 | if (err) |
383 | return err; |
384 | |
385 | err = sysfs_create_link(kobj: &port_dev->kobj, target: &dev->kobj, name: "partner" ); |
386 | if (err) |
387 | sysfs_remove_link(kobj: &dev->kobj, name: "port" ); |
388 | |
389 | return err; |
390 | } |
391 | |
392 | static void typec_altmode_remove_links(struct altmode *alt) |
393 | { |
394 | sysfs_remove_link(kobj: &alt->partner->adev.dev.kobj, name: "partner" ); |
395 | sysfs_remove_link(kobj: &alt->adev.dev.kobj, name: "port" ); |
396 | } |
397 | |
398 | static int typec_probe(struct device *dev) |
399 | { |
400 | struct typec_altmode_driver *drv = to_altmode_driver(dev->driver); |
401 | struct typec_altmode *adev = to_typec_altmode(dev); |
402 | struct altmode *altmode = to_altmode(adev); |
403 | int ret; |
404 | |
405 | /* Fail if the port does not support the alternate mode */ |
406 | if (!altmode->partner) |
407 | return -ENODEV; |
408 | |
409 | ret = typec_altmode_create_links(alt: altmode); |
410 | if (ret) { |
411 | dev_warn(dev, "failed to create symlinks\n" ); |
412 | return ret; |
413 | } |
414 | |
415 | ret = drv->probe(adev); |
416 | if (ret) |
417 | typec_altmode_remove_links(alt: altmode); |
418 | |
419 | return ret; |
420 | } |
421 | |
422 | static void typec_remove(struct device *dev) |
423 | { |
424 | struct typec_altmode_driver *drv = to_altmode_driver(dev->driver); |
425 | struct typec_altmode *adev = to_typec_altmode(dev); |
426 | struct altmode *altmode = to_altmode(adev); |
427 | |
428 | typec_altmode_remove_links(alt: altmode); |
429 | |
430 | if (drv->remove) |
431 | drv->remove(to_typec_altmode(dev)); |
432 | |
433 | if (adev->active) { |
434 | WARN_ON(typec_altmode_set_state(adev, TYPEC_STATE_SAFE, NULL)); |
435 | typec_altmode_update_active(alt: adev, active: false); |
436 | } |
437 | |
438 | adev->desc = NULL; |
439 | adev->ops = NULL; |
440 | } |
441 | |
442 | const struct bus_type typec_bus = { |
443 | .name = "typec" , |
444 | .dev_groups = typec_groups, |
445 | .match = typec_match, |
446 | .uevent = typec_uevent, |
447 | .probe = typec_probe, |
448 | .remove = typec_remove, |
449 | }; |
450 | |