1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * xhci-plat.c - xHCI host controller driver platform Bus Glue. |
4 | * |
5 | * Copyright (C) 2012 Texas Instruments Incorporated - https://www.ti.com |
6 | * Author: Sebastian Andrzej Siewior <bigeasy@linutronix.de> |
7 | * |
8 | * A lot of code borrowed from the Linux xHCI driver. |
9 | */ |
10 | |
11 | #include <linux/clk.h> |
12 | #include <linux/dma-mapping.h> |
13 | #include <linux/module.h> |
14 | #include <linux/pci.h> |
15 | #include <linux/of.h> |
16 | #include <linux/of_device.h> |
17 | #include <linux/platform_device.h> |
18 | #include <linux/usb/phy.h> |
19 | #include <linux/slab.h> |
20 | #include <linux/acpi.h> |
21 | #include <linux/usb/of.h> |
22 | #include <linux/reset.h> |
23 | |
24 | #include "xhci.h" |
25 | #include "xhci-plat.h" |
26 | #include "xhci-mvebu.h" |
27 | |
28 | static struct hc_driver __read_mostly xhci_plat_hc_driver; |
29 | |
30 | static int xhci_plat_setup(struct usb_hcd *hcd); |
31 | static int xhci_plat_start(struct usb_hcd *hcd); |
32 | |
33 | static const struct xhci_driver_overrides xhci_plat_overrides __initconst = { |
34 | .extra_priv_size = sizeof(struct xhci_plat_priv), |
35 | .reset = xhci_plat_setup, |
36 | .start = xhci_plat_start, |
37 | }; |
38 | |
39 | static void xhci_priv_plat_start(struct usb_hcd *hcd) |
40 | { |
41 | struct xhci_plat_priv *priv = hcd_to_xhci_priv(hcd); |
42 | |
43 | if (priv->plat_start) |
44 | priv->plat_start(hcd); |
45 | } |
46 | |
47 | static int xhci_priv_init_quirk(struct usb_hcd *hcd) |
48 | { |
49 | struct xhci_plat_priv *priv = hcd_to_xhci_priv(hcd); |
50 | |
51 | if (!priv->init_quirk) |
52 | return 0; |
53 | |
54 | return priv->init_quirk(hcd); |
55 | } |
56 | |
57 | static int xhci_priv_suspend_quirk(struct usb_hcd *hcd) |
58 | { |
59 | struct xhci_plat_priv *priv = hcd_to_xhci_priv(hcd); |
60 | |
61 | if (!priv->suspend_quirk) |
62 | return 0; |
63 | |
64 | return priv->suspend_quirk(hcd); |
65 | } |
66 | |
67 | static int xhci_priv_resume_quirk(struct usb_hcd *hcd) |
68 | { |
69 | struct xhci_plat_priv *priv = hcd_to_xhci_priv(hcd); |
70 | |
71 | if (!priv->resume_quirk) |
72 | return 0; |
73 | |
74 | return priv->resume_quirk(hcd); |
75 | } |
76 | |
77 | static void xhci_plat_quirks(struct device *dev, struct xhci_hcd *xhci) |
78 | { |
79 | struct xhci_plat_priv *priv = xhci_to_priv(xhci); |
80 | |
81 | xhci->quirks |= priv->quirks; |
82 | } |
83 | |
84 | /* called during probe() after chip reset completes */ |
85 | static int xhci_plat_setup(struct usb_hcd *hcd) |
86 | { |
87 | int ret; |
88 | |
89 | |
90 | ret = xhci_priv_init_quirk(hcd); |
91 | if (ret) |
92 | return ret; |
93 | |
94 | return xhci_gen_setup(hcd, get_quirks: xhci_plat_quirks); |
95 | } |
96 | |
97 | static int xhci_plat_start(struct usb_hcd *hcd) |
98 | { |
99 | xhci_priv_plat_start(hcd); |
100 | return xhci_run(hcd); |
101 | } |
102 | |
103 | #ifdef CONFIG_OF |
104 | static const struct xhci_plat_priv xhci_plat_marvell_armada = { |
105 | .init_quirk = xhci_mvebu_mbus_init_quirk, |
106 | }; |
107 | |
108 | static const struct xhci_plat_priv xhci_plat_marvell_armada3700 = { |
109 | .init_quirk = xhci_mvebu_a3700_init_quirk, |
110 | }; |
111 | |
112 | static const struct xhci_plat_priv xhci_plat_brcm = { |
113 | .quirks = XHCI_RESET_ON_RESUME | XHCI_SUSPEND_RESUME_CLKS, |
114 | }; |
115 | |
116 | static const struct of_device_id usb_xhci_of_match[] = { |
117 | { |
118 | .compatible = "generic-xhci" , |
119 | }, { |
120 | .compatible = "xhci-platform" , |
121 | }, { |
122 | .compatible = "marvell,armada-375-xhci" , |
123 | .data = &xhci_plat_marvell_armada, |
124 | }, { |
125 | .compatible = "marvell,armada-380-xhci" , |
126 | .data = &xhci_plat_marvell_armada, |
127 | }, { |
128 | .compatible = "marvell,armada3700-xhci" , |
129 | .data = &xhci_plat_marvell_armada3700, |
130 | }, { |
131 | .compatible = "brcm,xhci-brcm-v2" , |
132 | .data = &xhci_plat_brcm, |
133 | }, { |
134 | .compatible = "brcm,bcm2711-xhci" , |
135 | .data = &xhci_plat_brcm, |
136 | }, { |
137 | .compatible = "brcm,bcm7445-xhci" , |
138 | .data = &xhci_plat_brcm, |
139 | }, |
140 | {}, |
141 | }; |
142 | MODULE_DEVICE_TABLE(of, usb_xhci_of_match); |
143 | #endif |
144 | |
145 | int xhci_plat_probe(struct platform_device *pdev, struct device *sysdev, const struct xhci_plat_priv *priv_match) |
146 | { |
147 | const struct hc_driver *driver; |
148 | struct device *tmpdev; |
149 | struct xhci_hcd *xhci; |
150 | struct resource *res; |
151 | struct usb_hcd *hcd, *usb3_hcd; |
152 | int ret; |
153 | int irq; |
154 | struct xhci_plat_priv *priv = NULL; |
155 | bool of_match; |
156 | |
157 | if (usb_disabled()) |
158 | return -ENODEV; |
159 | |
160 | driver = &xhci_plat_hc_driver; |
161 | |
162 | irq = platform_get_irq(pdev, 0); |
163 | if (irq < 0) |
164 | return irq; |
165 | |
166 | if (!sysdev) |
167 | sysdev = &pdev->dev; |
168 | |
169 | ret = dma_set_mask_and_coherent(dev: sysdev, DMA_BIT_MASK(64)); |
170 | if (ret) |
171 | return ret; |
172 | |
173 | pm_runtime_set_active(dev: &pdev->dev); |
174 | pm_runtime_enable(dev: &pdev->dev); |
175 | pm_runtime_get_noresume(dev: &pdev->dev); |
176 | |
177 | hcd = __usb_create_hcd(driver, sysdev, dev: &pdev->dev, |
178 | bus_name: dev_name(dev: &pdev->dev), NULL); |
179 | if (!hcd) { |
180 | ret = -ENOMEM; |
181 | goto disable_runtime; |
182 | } |
183 | |
184 | hcd->regs = devm_platform_get_and_ioremap_resource(pdev, index: 0, res: &res); |
185 | if (IS_ERR(ptr: hcd->regs)) { |
186 | ret = PTR_ERR(ptr: hcd->regs); |
187 | goto put_hcd; |
188 | } |
189 | |
190 | hcd->rsrc_start = res->start; |
191 | hcd->rsrc_len = resource_size(res); |
192 | |
193 | xhci = hcd_to_xhci(hcd); |
194 | |
195 | xhci->allow_single_roothub = 1; |
196 | |
197 | /* |
198 | * Not all platforms have clks so it is not an error if the |
199 | * clock do not exist. |
200 | */ |
201 | xhci->reg_clk = devm_clk_get_optional(dev: &pdev->dev, id: "reg" ); |
202 | if (IS_ERR(ptr: xhci->reg_clk)) { |
203 | ret = PTR_ERR(ptr: xhci->reg_clk); |
204 | goto put_hcd; |
205 | } |
206 | |
207 | xhci->clk = devm_clk_get_optional(dev: &pdev->dev, NULL); |
208 | if (IS_ERR(ptr: xhci->clk)) { |
209 | ret = PTR_ERR(ptr: xhci->clk); |
210 | goto put_hcd; |
211 | } |
212 | |
213 | xhci->reset = devm_reset_control_array_get_optional_shared(dev: &pdev->dev); |
214 | if (IS_ERR(ptr: xhci->reset)) { |
215 | ret = PTR_ERR(ptr: xhci->reset); |
216 | goto put_hcd; |
217 | } |
218 | |
219 | ret = reset_control_deassert(rstc: xhci->reset); |
220 | if (ret) |
221 | goto put_hcd; |
222 | |
223 | ret = clk_prepare_enable(clk: xhci->reg_clk); |
224 | if (ret) |
225 | goto err_reset; |
226 | |
227 | ret = clk_prepare_enable(clk: xhci->clk); |
228 | if (ret) |
229 | goto disable_reg_clk; |
230 | |
231 | if (priv_match) { |
232 | priv = hcd_to_xhci_priv(hcd); |
233 | /* Just copy data for now */ |
234 | *priv = *priv_match; |
235 | } |
236 | |
237 | device_set_wakeup_capable(dev: &pdev->dev, capable: true); |
238 | |
239 | xhci->main_hcd = hcd; |
240 | |
241 | /* imod_interval is the interrupt moderation value in nanoseconds. */ |
242 | xhci->imod_interval = 40000; |
243 | |
244 | /* Iterate over all parent nodes for finding quirks */ |
245 | for (tmpdev = &pdev->dev; tmpdev; tmpdev = tmpdev->parent) { |
246 | |
247 | if (device_property_read_bool(dev: tmpdev, propname: "usb2-lpm-disable" )) |
248 | xhci->quirks |= XHCI_HW_LPM_DISABLE; |
249 | |
250 | if (device_property_read_bool(dev: tmpdev, propname: "usb3-lpm-capable" )) |
251 | xhci->quirks |= XHCI_LPM_SUPPORT; |
252 | |
253 | if (device_property_read_bool(dev: tmpdev, propname: "quirk-broken-port-ped" )) |
254 | xhci->quirks |= XHCI_BROKEN_PORT_PED; |
255 | |
256 | if (device_property_read_bool(dev: tmpdev, propname: "xhci-sg-trb-cache-size-quirk" )) |
257 | xhci->quirks |= XHCI_SG_TRB_CACHE_SIZE_QUIRK; |
258 | |
259 | device_property_read_u32(dev: tmpdev, propname: "imod-interval-ns" , |
260 | val: &xhci->imod_interval); |
261 | } |
262 | |
263 | /* |
264 | * Drivers such as dwc3 manages PHYs themself (and rely on driver name |
265 | * matching for the xhci platform device). |
266 | */ |
267 | of_match = of_match_device(matches: pdev->dev.driver->of_match_table, dev: &pdev->dev); |
268 | if (of_match) { |
269 | hcd->usb_phy = devm_usb_get_phy_by_phandle(dev: sysdev, phandle: "usb-phy" , index: 0); |
270 | if (IS_ERR(ptr: hcd->usb_phy)) { |
271 | ret = PTR_ERR(ptr: hcd->usb_phy); |
272 | if (ret == -EPROBE_DEFER) |
273 | goto disable_clk; |
274 | hcd->usb_phy = NULL; |
275 | } else { |
276 | ret = usb_phy_init(x: hcd->usb_phy); |
277 | if (ret) |
278 | goto disable_clk; |
279 | } |
280 | } |
281 | |
282 | hcd->tpl_support = of_usb_host_tpl_support(np: sysdev->of_node); |
283 | |
284 | if (priv && (priv->quirks & XHCI_SKIP_PHY_INIT)) |
285 | hcd->skip_phy_initialization = 1; |
286 | |
287 | if (priv && (priv->quirks & XHCI_SG_TRB_CACHE_SIZE_QUIRK)) |
288 | xhci->quirks |= XHCI_SG_TRB_CACHE_SIZE_QUIRK; |
289 | |
290 | ret = usb_add_hcd(hcd, irqnum: irq, IRQF_SHARED); |
291 | if (ret) |
292 | goto disable_usb_phy; |
293 | |
294 | if (!xhci_has_one_roothub(xhci)) { |
295 | xhci->shared_hcd = __usb_create_hcd(driver, sysdev, dev: &pdev->dev, |
296 | bus_name: dev_name(dev: &pdev->dev), primary_hcd: hcd); |
297 | if (!xhci->shared_hcd) { |
298 | ret = -ENOMEM; |
299 | goto dealloc_usb2_hcd; |
300 | } |
301 | |
302 | if (of_match) { |
303 | xhci->shared_hcd->usb_phy = devm_usb_get_phy_by_phandle(dev: sysdev, |
304 | phandle: "usb-phy" , index: 1); |
305 | if (IS_ERR(ptr: xhci->shared_hcd->usb_phy)) { |
306 | xhci->shared_hcd->usb_phy = NULL; |
307 | } else { |
308 | ret = usb_phy_init(x: xhci->shared_hcd->usb_phy); |
309 | if (ret) |
310 | dev_err(sysdev, "%s init usb3phy fail (ret=%d)\n" , |
311 | __func__, ret); |
312 | } |
313 | } |
314 | |
315 | xhci->shared_hcd->tpl_support = hcd->tpl_support; |
316 | } |
317 | |
318 | usb3_hcd = xhci_get_usb3_hcd(xhci); |
319 | if (usb3_hcd && HCC_MAX_PSA(xhci->hcc_params) >= 4) |
320 | usb3_hcd->can_do_streams = 1; |
321 | |
322 | if (xhci->shared_hcd) { |
323 | ret = usb_add_hcd(hcd: xhci->shared_hcd, irqnum: irq, IRQF_SHARED); |
324 | if (ret) |
325 | goto put_usb3_hcd; |
326 | } |
327 | |
328 | device_enable_async_suspend(dev: &pdev->dev); |
329 | pm_runtime_put_noidle(dev: &pdev->dev); |
330 | |
331 | /* |
332 | * Prevent runtime pm from being on as default, users should enable |
333 | * runtime pm using power/control in sysfs. |
334 | */ |
335 | pm_runtime_forbid(dev: &pdev->dev); |
336 | |
337 | return 0; |
338 | |
339 | |
340 | put_usb3_hcd: |
341 | usb_put_hcd(hcd: xhci->shared_hcd); |
342 | |
343 | dealloc_usb2_hcd: |
344 | usb_remove_hcd(hcd); |
345 | |
346 | disable_usb_phy: |
347 | usb_phy_shutdown(x: hcd->usb_phy); |
348 | |
349 | disable_clk: |
350 | clk_disable_unprepare(clk: xhci->clk); |
351 | |
352 | disable_reg_clk: |
353 | clk_disable_unprepare(clk: xhci->reg_clk); |
354 | |
355 | err_reset: |
356 | reset_control_assert(rstc: xhci->reset); |
357 | |
358 | put_hcd: |
359 | usb_put_hcd(hcd); |
360 | |
361 | disable_runtime: |
362 | pm_runtime_put_noidle(dev: &pdev->dev); |
363 | pm_runtime_disable(dev: &pdev->dev); |
364 | |
365 | return ret; |
366 | } |
367 | EXPORT_SYMBOL_GPL(xhci_plat_probe); |
368 | |
369 | static int xhci_generic_plat_probe(struct platform_device *pdev) |
370 | { |
371 | const struct xhci_plat_priv *priv_match; |
372 | struct device *sysdev; |
373 | int ret; |
374 | |
375 | /* |
376 | * sysdev must point to a device that is known to the system firmware |
377 | * or PCI hardware. We handle these three cases here: |
378 | * 1. xhci_plat comes from firmware |
379 | * 2. xhci_plat is child of a device from firmware (dwc3-plat) |
380 | * 3. xhci_plat is grandchild of a pci device (dwc3-pci) |
381 | */ |
382 | for (sysdev = &pdev->dev; sysdev; sysdev = sysdev->parent) { |
383 | if (is_of_node(fwnode: sysdev->fwnode) || |
384 | is_acpi_device_node(fwnode: sysdev->fwnode)) |
385 | break; |
386 | else if (dev_is_pci(sysdev)) |
387 | break; |
388 | } |
389 | |
390 | if (!sysdev) |
391 | sysdev = &pdev->dev; |
392 | |
393 | if (WARN_ON(!sysdev->dma_mask)) { |
394 | /* Platform did not initialize dma_mask */ |
395 | ret = dma_coerce_mask_and_coherent(dev: sysdev, DMA_BIT_MASK(64)); |
396 | if (ret) |
397 | return ret; |
398 | } |
399 | |
400 | if (pdev->dev.of_node) |
401 | priv_match = of_device_get_match_data(dev: &pdev->dev); |
402 | else |
403 | priv_match = dev_get_platdata(dev: &pdev->dev); |
404 | |
405 | return xhci_plat_probe(pdev, sysdev, priv_match); |
406 | } |
407 | |
408 | void xhci_plat_remove(struct platform_device *dev) |
409 | { |
410 | struct usb_hcd *hcd = platform_get_drvdata(pdev: dev); |
411 | struct xhci_hcd *xhci = hcd_to_xhci(hcd); |
412 | struct clk *clk = xhci->clk; |
413 | struct clk *reg_clk = xhci->reg_clk; |
414 | struct usb_hcd *shared_hcd = xhci->shared_hcd; |
415 | |
416 | xhci->xhc_state |= XHCI_STATE_REMOVING; |
417 | pm_runtime_get_sync(dev: &dev->dev); |
418 | |
419 | if (shared_hcd) { |
420 | usb_remove_hcd(hcd: shared_hcd); |
421 | xhci->shared_hcd = NULL; |
422 | } |
423 | |
424 | usb_phy_shutdown(x: hcd->usb_phy); |
425 | |
426 | usb_remove_hcd(hcd); |
427 | |
428 | if (shared_hcd) |
429 | usb_put_hcd(hcd: shared_hcd); |
430 | |
431 | clk_disable_unprepare(clk); |
432 | clk_disable_unprepare(clk: reg_clk); |
433 | reset_control_assert(rstc: xhci->reset); |
434 | usb_put_hcd(hcd); |
435 | |
436 | pm_runtime_disable(dev: &dev->dev); |
437 | pm_runtime_put_noidle(dev: &dev->dev); |
438 | pm_runtime_set_suspended(dev: &dev->dev); |
439 | } |
440 | EXPORT_SYMBOL_GPL(xhci_plat_remove); |
441 | |
442 | static int xhci_plat_suspend(struct device *dev) |
443 | { |
444 | struct usb_hcd *hcd = dev_get_drvdata(dev); |
445 | struct xhci_hcd *xhci = hcd_to_xhci(hcd); |
446 | int ret; |
447 | |
448 | if (pm_runtime_suspended(dev)) |
449 | pm_runtime_resume(dev); |
450 | |
451 | ret = xhci_priv_suspend_quirk(hcd); |
452 | if (ret) |
453 | return ret; |
454 | /* |
455 | * xhci_suspend() needs `do_wakeup` to know whether host is allowed |
456 | * to do wakeup during suspend. |
457 | */ |
458 | ret = xhci_suspend(xhci, do_wakeup: device_may_wakeup(dev)); |
459 | if (ret) |
460 | return ret; |
461 | |
462 | if (!device_may_wakeup(dev) && (xhci->quirks & XHCI_SUSPEND_RESUME_CLKS)) { |
463 | clk_disable_unprepare(clk: xhci->clk); |
464 | clk_disable_unprepare(clk: xhci->reg_clk); |
465 | } |
466 | |
467 | return 0; |
468 | } |
469 | |
470 | static int xhci_plat_resume_common(struct device *dev, struct pm_message pmsg) |
471 | { |
472 | struct usb_hcd *hcd = dev_get_drvdata(dev); |
473 | struct xhci_hcd *xhci = hcd_to_xhci(hcd); |
474 | int ret; |
475 | |
476 | if (!device_may_wakeup(dev) && (xhci->quirks & XHCI_SUSPEND_RESUME_CLKS)) { |
477 | ret = clk_prepare_enable(clk: xhci->clk); |
478 | if (ret) |
479 | return ret; |
480 | |
481 | ret = clk_prepare_enable(clk: xhci->reg_clk); |
482 | if (ret) { |
483 | clk_disable_unprepare(clk: xhci->clk); |
484 | return ret; |
485 | } |
486 | } |
487 | |
488 | ret = xhci_priv_resume_quirk(hcd); |
489 | if (ret) |
490 | goto disable_clks; |
491 | |
492 | ret = xhci_resume(xhci, msg: pmsg); |
493 | if (ret) |
494 | goto disable_clks; |
495 | |
496 | pm_runtime_disable(dev); |
497 | pm_runtime_set_active(dev); |
498 | pm_runtime_enable(dev); |
499 | |
500 | return 0; |
501 | |
502 | disable_clks: |
503 | if (!device_may_wakeup(dev) && (xhci->quirks & XHCI_SUSPEND_RESUME_CLKS)) { |
504 | clk_disable_unprepare(clk: xhci->clk); |
505 | clk_disable_unprepare(clk: xhci->reg_clk); |
506 | } |
507 | |
508 | return ret; |
509 | } |
510 | |
511 | static int xhci_plat_resume(struct device *dev) |
512 | { |
513 | return xhci_plat_resume_common(dev, PMSG_RESUME); |
514 | } |
515 | |
516 | static int xhci_plat_restore(struct device *dev) |
517 | { |
518 | return xhci_plat_resume_common(dev, PMSG_RESTORE); |
519 | } |
520 | |
521 | static int __maybe_unused xhci_plat_runtime_suspend(struct device *dev) |
522 | { |
523 | struct usb_hcd *hcd = dev_get_drvdata(dev); |
524 | struct xhci_hcd *xhci = hcd_to_xhci(hcd); |
525 | int ret; |
526 | |
527 | ret = xhci_priv_suspend_quirk(hcd); |
528 | if (ret) |
529 | return ret; |
530 | |
531 | return xhci_suspend(xhci, do_wakeup: true); |
532 | } |
533 | |
534 | static int __maybe_unused xhci_plat_runtime_resume(struct device *dev) |
535 | { |
536 | struct usb_hcd *hcd = dev_get_drvdata(dev); |
537 | struct xhci_hcd *xhci = hcd_to_xhci(hcd); |
538 | |
539 | return xhci_resume(xhci, PMSG_AUTO_RESUME); |
540 | } |
541 | |
542 | const struct dev_pm_ops xhci_plat_pm_ops = { |
543 | .suspend = pm_sleep_ptr(xhci_plat_suspend), |
544 | .resume = pm_sleep_ptr(xhci_plat_resume), |
545 | .freeze = pm_sleep_ptr(xhci_plat_suspend), |
546 | .thaw = pm_sleep_ptr(xhci_plat_resume), |
547 | .poweroff = pm_sleep_ptr(xhci_plat_suspend), |
548 | .restore = pm_sleep_ptr(xhci_plat_restore), |
549 | |
550 | SET_RUNTIME_PM_OPS(xhci_plat_runtime_suspend, |
551 | xhci_plat_runtime_resume, |
552 | NULL) |
553 | }; |
554 | EXPORT_SYMBOL_GPL(xhci_plat_pm_ops); |
555 | |
556 | #ifdef CONFIG_ACPI |
557 | static const struct acpi_device_id usb_xhci_acpi_match[] = { |
558 | /* XHCI-compliant USB Controller */ |
559 | { "PNP0D10" , }, |
560 | { } |
561 | }; |
562 | MODULE_DEVICE_TABLE(acpi, usb_xhci_acpi_match); |
563 | #endif |
564 | |
565 | static struct platform_driver usb_generic_xhci_driver = { |
566 | .probe = xhci_generic_plat_probe, |
567 | .remove_new = xhci_plat_remove, |
568 | .shutdown = usb_hcd_platform_shutdown, |
569 | .driver = { |
570 | .name = "xhci-hcd" , |
571 | .pm = &xhci_plat_pm_ops, |
572 | .of_match_table = of_match_ptr(usb_xhci_of_match), |
573 | .acpi_match_table = ACPI_PTR(usb_xhci_acpi_match), |
574 | }, |
575 | }; |
576 | MODULE_ALIAS("platform:xhci-hcd" ); |
577 | |
578 | static int __init xhci_plat_init(void) |
579 | { |
580 | xhci_init_driver(drv: &xhci_plat_hc_driver, over: &xhci_plat_overrides); |
581 | return platform_driver_register(&usb_generic_xhci_driver); |
582 | } |
583 | module_init(xhci_plat_init); |
584 | |
585 | static void __exit xhci_plat_exit(void) |
586 | { |
587 | platform_driver_unregister(&usb_generic_xhci_driver); |
588 | } |
589 | module_exit(xhci_plat_exit); |
590 | |
591 | MODULE_DESCRIPTION("xHCI Platform Host Controller Driver" ); |
592 | MODULE_LICENSE("GPL" ); |
593 | |