| From 0b5d788d69d7fd9ccf3e6741e7438cd78508ff95 Mon Sep 17 00:00:00 2001 |
| From: Matthias Kaehlcke <mka@chromium.org> |
| Date: Thu, 3 Sep 2020 16:22:07 -0700 |
| Subject: [PATCH] CHROMIUM: USB: misc: Add onboard_usb_hub driver |
| |
| [This patch was posted for upstream review and was well received, |
| however the DT bindings were refused. The latest posted version |
| is https://lore.kernel.org/patchwork/patch/1313001/. |
| |
| Since USB maintainers are generally supportive of the idea we intend to |
| land the functionality eventually, but the final implementation will |
| likely look different. |
| |
| Changes with respect to v4 posted on the list: |
| - use late suspend to make sure info about wakeup enabled descendants |
| is updated |
| - renamed sysfs attribute 'power_off_in_suspend' to |
| 'always_powered_in_suspend' |
| - fix log for regulator_disable() failure |
| - removed 'realtek' compatible string which is not needed at this |
| point |
| ] |
| |
| The main issue this driver addresses is that a USB hub needs to be |
| powered before it can be discovered. For discrete onboard hubs (an |
| example for such a hub is the Realtek RTS5411) 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 even more 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 power it off otherwise. |
| |
| Technically the driver consists of two drivers, the platform driver |
| described above and a very thin USB driver that subclasses the |
| generic 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). |
| |
| BUG=b:120563698 |
| TEST=on trogdor: |
| echo enabled > /sys/devices/platform/soc@0/a6f8800.usb/a600000.dwc3/xhci-hcd.13.auto/power/wakeup |
| suspend with USB mouse connected |
| => USB hub stays powered on |
| suspend without USB mouse connected |
| => USB hub is powered off |
| |
| Change-Id: I7c9a1f1d6ced41dd8310e8a03da666a32364e790 |
| 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> |
| Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/kernel/+/2104490 |
| Reviewed-by: Douglas Anderson <dianders@chromium.org> |
| [rebase510(groeck): |
| Squashed: |
| FIXUP: CHROMIUM: USB: misc: Add onboard_usb_hub driver |
| FIXUP: CHROMIUM: USB: misc: Add onboard_usb_hub driver (take 2) |
| ] |
| Signed-off-by: Guenter Roeck <groeck@chromium.org> |
| --- |
| MAINTAINERS | 7 + |
| drivers/usb/misc/Kconfig | 17 ++ |
| drivers/usb/misc/Makefile | 1 + |
| drivers/usb/misc/onboard_usb_hub.c | 371 +++++++++++++++++++++++++++++ |
| 4 files changed, 396 insertions(+) |
| create mode 100644 drivers/usb/misc/onboard_usb_hub.c |
| |
| diff --git a/MAINTAINERS b/MAINTAINERS |
| index c80ad735b384..c2187cf437e5 100644 |
| --- a/MAINTAINERS |
| +++ b/MAINTAINERS |
| @@ -13295,6 +13295,13 @@ S: Maintained |
| T: git git://linuxtv.org/media_tree.git |
| F: drivers/media/i2c/ov9734.c |
| |
| +ONBOARD USB HUB DRIVER |
| +M: Matthias Kaehlcke <mka@chromium.org> |
| +L: linux-usb@vger.kernel.org |
| +S: Maintained |
| +F: Documentation/devicetree/bindings/usb/onboard_usb_hub.yaml |
| +F: drivers/usb/misc/onboard_usb_hub.c |
| + |
| ONENAND FLASH DRIVER |
| M: Kyungmin Park <kyungmin.park@samsung.com> |
| L: linux-mtd@lists.infradead.org |
| diff --git a/drivers/usb/misc/Kconfig b/drivers/usb/misc/Kconfig |
| index 8f1144359012..f534269fbb20 100644 |
| --- a/drivers/usb/misc/Kconfig |
| +++ b/drivers/usb/misc/Kconfig |
| @@ -284,3 +284,20 @@ config BRCM_USB_PINMAP |
| This option enables support for remapping some USB external |
| signals, which are typically on dedicated pins on the chip, |
| to any gpio. |
| + |
| +config USB_ONBOARD_HUB |
| + tristate "Onboard USB hub support" |
| + depends on OF || COMPILE_TEST |
| + help |
| + Say Y here if you want to support discrete onboard USB hubs that |
| + don't require an additional control bus for initialization, but |
| + need some nontrivial form of initialization, such as enabling a |
| + power regulator. An example for such a hub is the Realtek |
| + RTS5411. |
| + |
| + The driver can be configured to turn off the power of the hub |
| + during system suspend. This may reduce power consumption while |
| + the system is suspended. |
| + |
| + To compile this driver as a module, choose M here: the |
| + module will be called onboard_usb_hub. |
| diff --git a/drivers/usb/misc/Makefile b/drivers/usb/misc/Makefile |
| index 5f4e598573ab..2c5aec6f1b26 100644 |
| --- a/drivers/usb/misc/Makefile |
| +++ b/drivers/usb/misc/Makefile |
| @@ -32,3 +32,4 @@ obj-$(CONFIG_USB_CHAOSKEY) += chaoskey.o |
| obj-$(CONFIG_USB_SISUSBVGA) += sisusbvga/ |
| obj-$(CONFIG_USB_LINK_LAYER_TEST) += lvstest.o |
| obj-$(CONFIG_BRCM_USB_PINMAP) += brcmstb-usb-pinmap.o |
| +obj-$(CONFIG_USB_ONBOARD_HUB) += onboard_usb_hub.o |
| diff --git a/drivers/usb/misc/onboard_usb_hub.c b/drivers/usb/misc/onboard_usb_hub.c |
| new file mode 100644 |
| index 000000000000..6cc869fa3df0 |
| --- /dev/null |
| +++ b/drivers/usb/misc/onboard_usb_hub.c |
| @@ -0,0 +1,371 @@ |
| +// SPDX-License-Identifier: GPL-2.0-only |
| +/* |
| + * Driver for onboard USB hubs |
| + * |
| + * Copyright (c) 2020, Google LLC |
| + */ |
| + |
| +#include <linux/device.h> |
| +#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> |
| + |
| +/************************** Platform driver **************************/ |
| + |
| +struct udev_node { |
| + struct usb_device *udev; |
| + struct list_head list; |
| +}; |
| + |
| +struct onboard_hub { |
| + struct regulator *vdd; |
| + struct device *dev; |
| + bool always_powered_in_suspend; |
| + bool is_powered_on; |
| + bool going_away; |
| + struct list_head udev_list; |
| + struct mutex lock; |
| +}; |
| + |
| +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; |
| + } |
| + |
| + hub->is_powered_on = true; |
| + |
| + 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 disable regulator: %d\n", err); |
| + return err; |
| + } |
| + |
| + hub->is_powered_on = false; |
| + |
| + return 0; |
| +} |
| + |
| +static int __maybe_unused onboard_hub_suspend(struct device *dev) |
| +{ |
| + struct onboard_hub *hub = dev_get_drvdata(dev); |
| + struct udev_node *node; |
| + bool power_off; |
| + int rc = 0; |
| + |
| + if (hub->always_powered_in_suspend) |
| + return 0; |
| + |
| + power_off = true; |
| + |
| + mutex_lock(&hub->lock); |
| + |
| + list_for_each_entry(node, &hub->udev_list, list) { |
| + if (!device_may_wakeup(node->udev->bus->controller)) |
| + continue; |
| + |
| + if (usb_wakeup_enabled_descendants(node->udev)) { |
| + power_off = false; |
| + break; |
| + } |
| + } |
| + |
| + mutex_unlock(&hub->lock); |
| + |
| + if (power_off) |
| + rc = onboard_hub_power_off(hub); |
| + |
| + return rc; |
| +} |
| + |
| +static int __maybe_unused onboard_hub_resume(struct device *dev) |
| +{ |
| + struct onboard_hub *hub = dev_get_drvdata(dev); |
| + int rc = 0; |
| + |
| + if (!hub->is_powered_on) |
| + rc = onboard_hub_power_on(hub); |
| + |
| + return rc; |
| +} |
| + |
| +static int onboard_hub_add_usbdev(struct onboard_hub *hub, struct usb_device *udev) |
| +{ |
| + struct udev_node *node; |
| + int ret = 0; |
| + |
| + mutex_lock(&hub->lock); |
| + |
| + if (hub->going_away) { |
| + ret = -EINVAL; |
| + goto unlock; |
| + } |
| + |
| + node = devm_kzalloc(hub->dev, sizeof(*node), GFP_KERNEL); |
| + if (!node) { |
| + ret = -ENOMEM; |
| + goto unlock; |
| + } |
| + |
| + node->udev = udev; |
| + |
| + list_add(&node->list, &hub->udev_list); |
| + |
| +unlock: |
| + mutex_unlock(&hub->lock); |
| + |
| + return ret; |
| +} |
| + |
| +static void 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); |
| + break; |
| + } |
| + } |
| + |
| + mutex_unlock(&hub->lock); |
| +} |
| + |
| +static ssize_t always_powered_in_suspend_show(struct device *dev, struct device_attribute *attr, |
| + char *buf) |
| +{ |
| + struct onboard_hub *hub = dev_get_drvdata(dev); |
| + |
| + return sprintf(buf, "%d\n", hub->always_powered_in_suspend); |
| +} |
| + |
| +static ssize_t always_powered_in_suspend_store(struct device *dev, struct device_attribute *attr, |
| + const char *buf, size_t count) |
| +{ |
| + struct onboard_hub *hub = dev_get_drvdata(dev); |
| + bool val; |
| + int ret; |
| + |
| + ret = kstrtobool(buf, &val); |
| + if (ret < 0) |
| + return ret; |
| + |
| + hub->always_powered_in_suspend = val; |
| + |
| + return count; |
| +} |
| +static DEVICE_ATTR_RW(always_powered_in_suspend); |
| + |
| +static struct attribute *onboard_hub_sysfs_entries[] = { |
| + &dev_attr_always_powered_in_suspend.attr, |
| + NULL, |
| +}; |
| + |
| +static const struct attribute_group onboard_hub_sysfs_group = { |
| + .attrs = onboard_hub_sysfs_entries, |
| +}; |
| + |
| +static int onboard_hub_probe(struct platform_device *pdev) |
| +{ |
| + struct device *dev = &pdev->dev; |
| + struct onboard_hub *hub; |
| + int err; |
| + |
| + 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); |
| + |
| + dev_set_drvdata(dev, hub); |
| + |
| + err = devm_device_add_group(dev, &onboard_hub_sysfs_group); |
| + if (err) { |
| + dev_err(dev, "failed to create sysfs entries: %d\n", err); |
| + return err; |
| + } |
| + |
| + return onboard_hub_power_on(hub); |
| +} |
| + |
| +static int onboard_hub_remove(struct platform_device *pdev) |
| +{ |
| + struct onboard_hub *hub = dev_get_drvdata(&pdev->dev); |
| + struct udev_node *node; |
| + struct usb_device *udev; |
| + |
| + hub->going_away = true; |
| + |
| + mutex_lock(&hub->lock); |
| + |
| + /* unbind the USB devices to avoid dangling references to this device */ |
| + while (!list_empty(&hub->udev_list)) { |
| + node = list_first_entry(&hub->udev_list, struct udev_node, list); |
| + udev = node->udev; |
| + |
| + /* |
| + * Unbinding the driver will call onboard_hub_remove_usbdev(), |
| + * which acquires hub->lock. We must release the lock first. |
| + */ |
| + get_device(&udev->dev); |
| + mutex_unlock(&hub->lock); |
| + device_release_driver(&udev->dev); |
| + put_device(&udev->dev); |
| + mutex_lock(&hub->lock); |
| + } |
| + |
| + mutex_unlock(&hub->lock); |
| + |
| + return onboard_hub_power_off(hub); |
| +} |
| + |
| +static const struct of_device_id onboard_hub_match[] = { |
| + { .compatible = "onboard-usb-hub" }, |
| + {} |
| +}; |
| +MODULE_DEVICE_TABLE(of, onboard_hub_match); |
| + |
| +static const struct dev_pm_ops __maybe_unused onboard_hub_pm_ops = { |
| + SET_LATE_SYSTEM_SLEEP_PM_OPS(onboard_hub_suspend, onboard_hub_resume) |
| +}; |
| + |
| +static struct platform_driver onboard_hub_driver = { |
| + .probe = onboard_hub_probe, |
| + .remove = onboard_hub_remove, |
| + |
| + .driver = { |
| + .name = "onboard-usb-hub", |
| + .of_match_table = onboard_hub_match, |
| + .pm = pm_ptr(&onboard_hub_pm_ops), |
| + }, |
| +}; |
| + |
| +/************************** USB driver **************************/ |
| + |
| +#define VENDOR_ID_REALTEK 0x0bda |
| + |
| +static struct onboard_hub *_find_onboard_hub(struct device *dev) |
| +{ |
| + phandle ph; |
| + struct device_node *np; |
| + struct platform_device *pdev; |
| + |
| + if (of_property_read_u32(dev->of_node, "hub", &ph)) { |
| + dev_err(dev, "failed to read 'hub' property\n"); |
| + return ERR_PTR(-EINVAL); |
| + } |
| + |
| + np = of_find_node_by_phandle(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); |
| + |
| + put_device(&pdev->dev); |
| + |
| + 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(hub); |
| + |
| + dev_set_drvdata(dev, hub); |
| + |
| + return onboard_hub_add_usbdev(hub, udev); |
| +} |
| + |
| +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); |
| +} |
| + |
| +static const struct usb_device_id onboard_hub_id_table[] = { |
| + { USB_DEVICE(VENDOR_ID_REALTEK, 0x0411) }, /* RTS5411 USB 3.0 */ |
| + { USB_DEVICE(VENDOR_ID_REALTEK, 0x5411) }, /* RTS5411 USB 2.0 */ |
| + {}, |
| +}; |
| + |
| +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 ret; |
| + |
| + ret = platform_driver_register(&onboard_hub_driver); |
| + if (ret) |
| + return ret; |
| + |
| + ret = usb_register_device_driver(&onboard_hub_usbdev_driver, THIS_MODULE); |
| + if (ret) |
| + platform_driver_unregister(&onboard_hub_driver); |
| + |
| + return ret; |
| +} |
| +module_init(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("Driver for discrete onboard USB hubs"); |
| +MODULE_LICENSE("GPL v2"); |
| -- |
| 2.17.1 |
| |