@@ -275,3 +275,17 @@ config USB_CHAOSKEY
To compile this driver as a module, choose M here: the
module will be called chaoskey.
+
+config USB_HUB_PWR
+ tristate "Control power supply for onboard USB hubs"
+ depends on PM
+ help
+ Say Y here if you want to control the power supply of an
+ onboard USB hub. The driver switches the power supply of the
+ hub on, to make sure the hub can be discovered. During system
+ suspend the power supply is switched off, unless a wakeup
+ capable device is connected to the hub. This may reduce power
+ consumption on battery powered devices.
+
+ To compile this driver as a module, choose M here: the
+ module will be called usb_hub_pwr.
@@ -31,3 +31,4 @@ obj-$(CONFIG_USB_CHAOSKEY) += chaoskey.o
obj-$(CONFIG_USB_SISUSBVGA) += sisusbvga/
obj-$(CONFIG_USB_LINK_LAYER_TEST) += lvstest.o
+obj-$(CONFIG_USB_HUB_PWR) += usb_hub_pwr.o usb_hub_psupply.o
new file mode 100644
@@ -0,0 +1,112 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2020, Google LLC
+ */
+
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/regulator/consumer.h>
+
+struct usb_hub_psupply_dev {
+ struct regulator *vdd;
+};
+
+int usb_hub_psupply_on(struct device *dev)
+{
+ struct usb_hub_psupply_dev *usb_hub_psupply = dev_get_drvdata(dev);
+ int err;
+
+ err = regulator_enable(usb_hub_psupply->vdd);
+ if (err) {
+ dev_err(dev, "failed to enable regulator: %d\n", err);
+ return err;
+ }
+
+ return 0;
+
+}
+EXPORT_SYMBOL_GPL(usb_hub_psupply_on);
+
+int usb_hub_psupply_off(struct device *dev)
+{
+ struct usb_hub_psupply_dev *usb_hub_psupply = dev_get_drvdata(dev);
+ int err;
+
+ err = regulator_disable(usb_hub_psupply->vdd);
+ if (err) {
+ dev_err(dev, "failed to enable regulator: %d\n", err);
+ return err;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(usb_hub_psupply_off);
+
+static int usb_hub_psupply_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct usb_hub_psupply_dev *usb_hub_psupply;
+
+ usb_hub_psupply = devm_kzalloc(dev, sizeof(*usb_hub_psupply), GFP_KERNEL);
+ if (!usb_hub_psupply)
+ return -ENOMEM;
+
+ dev_set_drvdata(dev, usb_hub_psupply);
+
+ usb_hub_psupply->vdd = devm_regulator_get(dev, "vdd");
+ if (IS_ERR(usb_hub_psupply->vdd))
+ return PTR_ERR(usb_hub_psupply->vdd);
+
+ return usb_hub_psupply_on(dev);
+}
+
+static int usb_hub_psupply_remove(struct platform_device *pdev)
+{
+ return usb_hub_psupply_off(&pdev->dev);
+}
+
+static int usb_hub_psupply_suspend(struct platform_device *pdev, pm_message_t msg)
+{
+ return usb_hub_psupply_off(&pdev->dev);
+}
+
+static int usb_hub_psupply_resume(struct platform_device *pdev)
+{
+ return usb_hub_psupply_on(&pdev->dev);
+}
+
+static const struct of_device_id usb_hub_psupply_match[] = {
+ { .compatible = "linux,usb_hub_psupply" },
+ {}
+};
+MODULE_DEVICE_TABLE(of, usb_hub_psupply_match);
+
+static struct platform_driver usb_hub_psupply_driver = {
+ .probe = usb_hub_psupply_probe,
+ .remove = usb_hub_psupply_remove,
+ .suspend = usb_hub_psupply_suspend,
+ .resume = usb_hub_psupply_resume,
+ .driver = {
+ .name = "usb-hub-psupply",
+ .of_match_table = usb_hub_psupply_match,
+ },
+};
+
+static int __init usb_hub_psupply_init(void)
+{
+ return platform_driver_register(&usb_hub_psupply_driver);
+}
+device_initcall(usb_hub_psupply_init);
+
+static void __exit usb_hub_psupply_exit(void)
+{
+ platform_driver_unregister(&usb_hub_psupply_driver);
+}
+module_exit(usb_hub_psupply_exit);
+
+MODULE_AUTHOR("Matthias Kaehlcke <mka@chromium.org>");
+MODULE_DESCRIPTION("USB Hub Power Supply");
+MODULE_LICENSE("GPL v2");
new file mode 100644
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#ifndef _USB_HUB_PSUPPLY_H
+#define _USB_HUB_PSUPPLY_H
+
+int usb_hub_psupply_on(struct device *dev);
+int usb_hub_psupply_off(struct device *dev);
+
+#endif /* _USB_HUB_PSUPPLY_H */
new file mode 100644
@@ -0,0 +1,177 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * USB hub power control
+ *
+ * Copyright (c) 2020, Google LLC
+ */
+
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_platform.h>
+#include <linux/power_supply.h>
+#include <linux/regulator/consumer.h>
+#include <linux/slab.h>
+#include <linux/usb.h>
+#include <linux/usb/hcd.h>
+#include "../core/usb.h"
+#include "usb_hub_psupply.h"
+
+#define VENDOR_ID_REALTEK 0x0bda
+
+struct usb_hub_pwr_dev {
+ struct regulator *vdd;
+ struct device *psupply_dev;
+ bool powered_off;
+};
+
+static struct device *usb_pwr_find_psupply_dev(struct device *dev)
+{
+ const phandle *ph;
+ struct device_node *np;
+ struct platform_device *pdev;
+
+ ph = of_get_property(dev->of_node, "psupply", NULL);
+ if (!ph) {
+ dev_err(dev, "failed to read 'psupply' 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 power supply\n");
+ return ERR_PTR(-EINVAL);
+ }
+
+ pdev = of_find_device_by_node(np);
+ of_node_put(np);
+ if (!pdev)
+ return ERR_PTR(-EPROBE_DEFER);
+
+ return &pdev->dev;
+}
+
+static int usb_hub_pwr_probe(struct usb_device *udev)
+{
+ struct device *dev = &udev->dev;
+ struct usb_hub_pwr_dev *uhpw;
+ struct device *psupply_dev;
+ int err;
+
+ /* ignore supported hubs without device tree node */
+ if (!dev->of_node)
+ return -ENODEV;
+
+ psupply_dev = usb_pwr_find_psupply_dev(dev);
+ if (IS_ERR(psupply_dev))
+ return PTR_ERR(psupply_dev);
+
+ err = usb_generic_driver_probe(udev);
+ if (err) {
+ put_device(psupply_dev);
+ return err;
+ }
+
+ uhpw = devm_kzalloc(dev, sizeof(*uhpw), GFP_KERNEL);
+ if (!uhpw) {
+ put_device(psupply_dev);
+ return -ENOMEM;
+ }
+
+ dev_set_drvdata(&udev->dev, uhpw);
+
+ uhpw->psupply_dev = psupply_dev;
+
+ err = usb_hub_psupply_on(psupply_dev);
+ if (err) {
+ dev_err(dev, "failed to enable regulator: %d\n", err);
+ put_device(psupply_dev);
+ return err;
+ }
+
+ return 0;
+}
+
+static void usb_hub_pwr_disconnect(struct usb_device *udev)
+{
+ struct usb_hub_pwr_dev *uhpw = dev_get_drvdata(&udev->dev);
+
+ usb_hub_psupply_off(uhpw->psupply_dev);
+ put_device(uhpw->psupply_dev);
+}
+
+static int usb_hub_pwr_suspend(struct usb_device *udev, pm_message_t msg)
+{
+ struct usb_hub_pwr_dev *uhpw = dev_get_drvdata(&udev->dev);
+ int err;
+
+ err = usb_generic_driver_suspend(udev, msg);
+ if (err)
+ return err;
+
+ if (!usb_wakeup_enabled_descendants(udev)) {
+ usb_port_disable(udev);
+
+ err = usb_hub_psupply_off(uhpw->psupply_dev);
+ if (err)
+ return err;
+
+ uhpw->powered_off = true;
+ }
+
+ return 0;
+}
+
+static int usb_hub_pwr_resume(struct usb_device *udev, pm_message_t msg)
+{
+ struct usb_hub_pwr_dev *uhpw = dev_get_drvdata(&udev->dev);
+ int err;
+
+ if (uhpw->powered_off) {
+ err = usb_hub_psupply_on(uhpw->psupply_dev);
+ if (err)
+ return err;
+
+ uhpw->powered_off = false;
+ }
+
+ return usb_generic_driver_resume(udev, msg);
+}
+
+static const struct usb_device_id 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, hub_id_table);
+
+static struct usb_device_driver usb_hub_pwr_driver = {
+
+ .name = "usb-hub-pwr",
+ .probe = usb_hub_pwr_probe,
+ .disconnect = usb_hub_pwr_disconnect,
+ .suspend = usb_hub_pwr_suspend,
+ .resume = usb_hub_pwr_resume,
+ .id_table = hub_id_table,
+};
+
+static int __init usb_hub_pwr_driver_init(void)
+{
+ return usb_register_device_driver(&usb_hub_pwr_driver, THIS_MODULE);
+}
+
+static void __exit usb_hub_pwr_driver_exit(void)
+{
+ usb_deregister_device_driver(&usb_hub_pwr_driver);
+}
+
+module_init(usb_hub_pwr_driver_init);
+module_exit(usb_hub_pwr_driver_exit);
+
+MODULE_AUTHOR("Matthias Kaehlcke <mka@chromium.org>");
+MODULE_DESCRIPTION("USB Hub Power Control");
+MODULE_LICENSE("GPL v2");