forked from torvalds/linux
Permalink
Show file tree
Hide file tree
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
USB: misc: Add onboard_usb_hub driver
The main issue this driver addresses is that a USB hub needs to be powered before it can be discovered. For onboard hubs this is often solved by supplying the hub with an 'always-on' regulator, which is kind of a hack. Some onboard hubs may require further initialization steps, like changing the state of a GPIO or enabling a clock, which requires further hacks. This driver creates a platform device representing the hub which performs the necessary initialization. Currently it only supports switching on a single regulator, support for multiple regulators or other actions can be added as needed. Different initialization sequences can be supported based on the compatible string. Besides performing the initialization the driver can be configured to power the hub off during system suspend. This can help to extend battery life on battery powered devices, which have no requirements to keep the hub powered during suspend. The driver can also be configured to leave the hub powered when a wakeup capable USB device is connected when suspending, and keeping it powered otherwise. Technically the driver consists of two drivers, the platform driver described above and a very thin USB driver that subclasses the generic hub driver. The purpose of this driver is to provide the platform driver with the USB devices corresponding to the hub(s) (a hub controller may provide multiple 'logical' hubs, e.g. one to support USB 2.0 and another for USB 3.x). Co-developed-by: Ravi Chandra Sadineni <ravisadineni@chromium.org> Signed-off-by: Ravi Chandra Sadineni <ravisadineni@chromium.org> Signed-off-by: Matthias Kaehlcke <mka@chromium.org>
- Loading branch information
1 parent
e2328b8
commit 81bbd2f3188c281e1c2f3fccb36d53d6ff3d9a89
Showing
3 changed files
with
322 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,306 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-only | ||
| /* | ||
| * Driver for onboard USB hubs | ||
| * | ||
| * Copyright (c) 2020, Google LLC | ||
| */ | ||
|
|
||
| #include <linux/init.h> | ||
| #include <linux/kernel.h> | ||
| #include <linux/module.h> | ||
| #include <linux/mutex.h> | ||
| #include <linux/of.h> | ||
| #include <linux/of_platform.h> | ||
| #include <linux/platform_device.h> | ||
| #include <linux/regulator/consumer.h> | ||
| #include <linux/suspend.h> | ||
| #include <linux/usb.h> | ||
| #include <linux/usb/hcd.h> | ||
| #include "../core/usb.h" | ||
|
|
||
| /************************** Platform driver **************************/ | ||
|
|
||
| struct udev_node { | ||
| struct usb_device *udev; | ||
| struct list_head list; | ||
| }; | ||
|
|
||
| struct onboard_hub { | ||
| struct regulator *vdd; | ||
| struct device *dev; | ||
| struct { | ||
| bool power_off_in_suspend; | ||
| bool wakeup_source; | ||
| } cfg; | ||
| struct list_head udev_list; | ||
| struct mutex lock; | ||
| bool has_wakeup_capable_descendants; | ||
| }; | ||
|
|
||
| static int onboard_hub_power_on(struct onboard_hub *hub) | ||
| { | ||
| int err; | ||
|
|
||
| err = regulator_enable(hub->vdd); | ||
| if (err) { | ||
| dev_err(hub->dev, "failed to enable regulator: %d\n", err); | ||
| return err; | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| static int onboard_hub_power_off(struct onboard_hub *hub) | ||
| { | ||
| int err; | ||
|
|
||
| err = regulator_disable(hub->vdd); | ||
| if (err) { | ||
| dev_err(hub->dev, "failed to enable regulator: %d\n", err); | ||
| return err; | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| #ifdef CONFIG_PM | ||
|
|
||
| static int onboard_hub_suspend(struct platform_device *pdev, pm_message_t msg) | ||
| { | ||
| struct onboard_hub *hub = dev_get_drvdata(&pdev->dev); | ||
| int rc = 0; | ||
|
|
||
| if (!hub->cfg.power_off_in_suspend) | ||
| return 0; | ||
|
|
||
| hub->has_wakeup_capable_descendants = false; | ||
|
|
||
| if (hub->cfg.wakeup_source) { | ||
| struct udev_node *node; | ||
|
|
||
| mutex_lock(&hub->lock); | ||
|
|
||
| list_for_each_entry(node, &hub->udev_list, list) { | ||
| if (usb_wakeup_enabled_descendants(node->udev)) { | ||
| hub->has_wakeup_capable_descendants = true; | ||
| break; | ||
| } | ||
|
|
||
| mutex_unlock(&hub->lock); | ||
| } | ||
|
|
||
| if (!hub->has_wakeup_capable_descendants) | ||
| rc = onboard_hub_power_off(hub); | ||
|
|
||
| return rc; | ||
| } | ||
|
|
||
| static int onboard_hub_resume(struct platform_device *pdev) | ||
| { | ||
| struct onboard_hub *hub = dev_get_drvdata(&pdev->dev); | ||
| int rc = 0; | ||
|
|
||
| if (hub->cfg.power_off_in_suspend && !hub->has_wakeup_capable_descendants) | ||
| rc = onboard_hub_power_on(hub); | ||
|
|
||
| return rc; | ||
| } | ||
|
|
||
| #endif | ||
|
|
||
| static int onboard_hub_add_usbdev(struct onboard_hub *hub, struct usb_device *udev) | ||
| { | ||
| struct udev_node *node; | ||
|
|
||
| node = devm_kzalloc(hub->dev, sizeof(*node), GFP_KERNEL); | ||
| if (!node) | ||
| return -ENOMEM; | ||
|
|
||
| node->udev = udev; | ||
|
|
||
| mutex_lock(&hub->lock); | ||
| list_add(&node->list, &hub->udev_list); | ||
| mutex_unlock(&hub->lock); | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| static int onboard_hub_remove_usbdev(struct onboard_hub *hub, struct usb_device *udev) | ||
| { | ||
| struct udev_node *node; | ||
|
|
||
| mutex_lock(&hub->lock); | ||
|
|
||
| list_for_each_entry(node, &hub->udev_list, list) { | ||
| if (node->udev == udev) { | ||
| list_del(&node->list); | ||
| devm_kfree(hub->dev, node); | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| mutex_unlock(&hub->lock); | ||
|
|
||
| if (node == NULL) | ||
| return -EINVAL; | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| static int onboard_hub_probe(struct platform_device *pdev) | ||
| { | ||
| struct device *dev = &pdev->dev; | ||
| struct onboard_hub *hub; | ||
|
|
||
| hub = devm_kzalloc(dev, sizeof(*hub), GFP_KERNEL); | ||
| if (!hub) | ||
| return -ENOMEM; | ||
|
|
||
| hub->vdd = devm_regulator_get(dev, "vdd"); | ||
| if (IS_ERR(hub->vdd)) | ||
| return PTR_ERR(hub->vdd); | ||
|
|
||
| hub->dev = dev; | ||
| mutex_init(&hub->lock); | ||
| INIT_LIST_HEAD(&hub->udev_list); | ||
|
|
||
| hub->cfg.power_off_in_suspend = of_property_read_bool(dev->of_node, "power-off-in-suspend"); | ||
| hub->cfg.wakeup_source = of_property_read_bool(dev->of_node, "wakeup-source"); | ||
|
|
||
| dev_set_drvdata(dev, hub); | ||
|
|
||
| return onboard_hub_power_on(hub); | ||
| } | ||
|
|
||
| static int onboard_hub_remove(struct platform_device *pdev) | ||
| { | ||
| struct onboard_hub *hub = dev_get_drvdata(&pdev->dev); | ||
|
|
||
| return onboard_hub_power_off(hub); | ||
| } | ||
|
|
||
| static const struct of_device_id onboard_hub_match[] = { | ||
| { .compatible = "onboard-usb-hub" }, | ||
| { .compatible = "realtek,rts5411" }, | ||
| {} | ||
| }; | ||
| MODULE_DEVICE_TABLE(of, onboard_hub_match); | ||
|
|
||
| static struct platform_driver onboard_hub_driver = { | ||
| .probe = onboard_hub_probe, | ||
| .remove = onboard_hub_remove, | ||
| #ifdef CONFIG_PM | ||
| .suspend = onboard_hub_suspend, | ||
| .resume = onboard_hub_resume, | ||
| #endif | ||
| .driver = { | ||
| .name = "onboard-usb-hub", | ||
| .of_match_table = onboard_hub_match, | ||
| }, | ||
| }; | ||
|
|
||
| /************************** USB driver **************************/ | ||
|
|
||
| #define VENDOR_ID_REALTEK 0x0bda | ||
|
|
||
| static struct onboard_hub *_find_onboard_hub(struct device *dev) | ||
| { | ||
| const phandle *ph; | ||
| struct device_node *np; | ||
| struct platform_device *pdev; | ||
|
|
||
| ph = of_get_property(dev->of_node, "hub", NULL); | ||
| if (!ph) { | ||
| dev_err(dev, "failed to read 'hub' property\n"); | ||
| return ERR_PTR(-EINVAL); | ||
| } | ||
|
|
||
| np = of_find_node_by_phandle(be32_to_cpu(*ph)); | ||
| if (!np) { | ||
| dev_err(dev, "failed find device node for onboard hub\n"); | ||
| return ERR_PTR(-EINVAL); | ||
| } | ||
|
|
||
| pdev = of_find_device_by_node(np); | ||
| of_node_put(np); | ||
| if (!pdev) | ||
| return ERR_PTR(-EPROBE_DEFER); | ||
|
|
||
| return dev_get_drvdata(&pdev->dev); | ||
| } | ||
|
|
||
| static int onboard_hub_usbdev_probe(struct usb_device *udev) | ||
| { | ||
| struct device *dev = &udev->dev; | ||
| struct onboard_hub *hub; | ||
|
|
||
| /* ignore supported hubs without device tree node */ | ||
| if (!dev->of_node) | ||
| return -ENODEV; | ||
|
|
||
| hub = _find_onboard_hub(dev); | ||
| if (IS_ERR(hub)) | ||
| return PTR_ERR(dev); | ||
|
|
||
| dev_set_drvdata(dev, hub); | ||
|
|
||
| onboard_hub_add_usbdev(hub, udev); | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| static void onboard_hub_usbdev_disconnect(struct usb_device *udev) | ||
| { | ||
| struct onboard_hub *hub = dev_get_drvdata(&udev->dev); | ||
|
|
||
| onboard_hub_remove_usbdev(hub, udev); | ||
|
|
||
| put_device(hub->dev); | ||
| } | ||
|
|
||
| static const struct usb_device_id onboard_hub_id_table[] = { | ||
| { .idVendor = VENDOR_ID_REALTEK, | ||
| .idProduct = 0x0411, /* RTS5411 USB 3.0 */ | ||
| .match_flags = USB_DEVICE_ID_MATCH_DEVICE }, | ||
| { .idVendor = VENDOR_ID_REALTEK, | ||
| .idProduct = 0x5411, /* RTS5411 USB 2.0 */ | ||
| .match_flags = USB_DEVICE_ID_MATCH_DEVICE }, | ||
| {}, | ||
| }; | ||
|
|
||
| MODULE_DEVICE_TABLE(usb, onboard_hub_id_table); | ||
|
|
||
| static struct usb_device_driver onboard_hub_usbdev_driver = { | ||
|
|
||
| .name = "onboard-usb-hub", | ||
| .probe = onboard_hub_usbdev_probe, | ||
| .disconnect = onboard_hub_usbdev_disconnect, | ||
| .generic_subclass = 1, | ||
| .supports_autosuspend = 1, | ||
| .id_table = onboard_hub_id_table, | ||
| }; | ||
|
|
||
| /************************** Driver (de)registration **************************/ | ||
|
|
||
| static int __init onboard_hub_init(void) | ||
| { | ||
| int rc; | ||
|
|
||
| rc = platform_driver_register(&onboard_hub_driver); | ||
| if (rc) | ||
| return rc; | ||
|
|
||
| return usb_register_device_driver(&onboard_hub_usbdev_driver, THIS_MODULE); | ||
| } | ||
| device_initcall(onboard_hub_init); | ||
|
|
||
| static void __exit onboard_hub_exit(void) | ||
| { | ||
| usb_deregister_device_driver(&onboard_hub_usbdev_driver); | ||
| platform_driver_unregister(&onboard_hub_driver); | ||
| } | ||
| module_exit(onboard_hub_exit); | ||
|
|
||
| MODULE_AUTHOR("Matthias Kaehlcke <mka@chromium.org>"); | ||
| MODULE_DESCRIPTION("Onboard USB Hub driver"); | ||
| MODULE_LICENSE("GPL v2"); |