summaryrefslogtreecommitdiff
path: root/drivers/usb/core/port.c
diff options
context:
space:
mode:
authorMichael Grzeschik <m.grzeschik@pengutronix.de>2022-06-07 13:45:22 +0200
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>2022-06-12 06:49:46 +0200
commitf061f43d7418cb62b8d073e221ec75d3f5b89e17 (patch)
tree59d311ae1af0105d165ee26ec129c122c9e6e545 /drivers/usb/core/port.c
parent5fd6c4f0a649c956b2c50c18e0da765b7f5e3eff (diff)
usb: hub: port: add sysfs entry to switch port power
In some cases the port of an hub needs to be disabled or switched off and on again. E.g. when the connected device needs to be re-enumerated. Or it needs to be explicitly disabled while the rest of the usb tree stays working. For this purpose this patch adds an sysfs switch to enable/disable the port on any hub. In the case the hub is supporting power switching, the power line will be disabled to the connected device. When the port gets disabled, the associated device gets disconnected and removed from the logical usb tree. No further device will be enumerated on that port until the port gets enabled again. Reviewed-by: Alan Stern <stern@rowland.harvard.edu> Signed-off-by: Michael Grzeschik <m.grzeschik@pengutronix.de> Link: https://lore.kernel.org/r/20220607114522.3359148-1-m.grzeschik@pengutronix.de Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Diffstat (limited to 'drivers/usb/core/port.c')
-rw-r--r--drivers/usb/core/port.c83
1 files changed, 83 insertions, 0 deletions
diff --git a/drivers/usb/core/port.c b/drivers/usb/core/port.c
index d5bc36ca5b1f..38c1a4f4fdea 100644
--- a/drivers/usb/core/port.c
+++ b/drivers/usb/core/port.c
@@ -17,6 +17,88 @@ static int usb_port_block_power_off;
static const struct attribute_group *port_dev_group[];
+static ssize_t disable_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct usb_port *port_dev = to_usb_port(dev);
+ struct usb_device *hdev = to_usb_device(dev->parent->parent);
+ struct usb_hub *hub = usb_hub_to_struct_hub(hdev);
+ struct usb_interface *intf = to_usb_interface(hub->intfdev);
+ int port1 = port_dev->portnum;
+ u16 portstatus, unused;
+ bool disabled;
+ int rc;
+
+ rc = usb_autopm_get_interface(intf);
+ if (rc < 0)
+ return rc;
+
+ usb_lock_device(hdev);
+ if (hub->disconnected) {
+ rc = -ENODEV;
+ goto out_hdev_lock;
+ }
+
+ usb_hub_port_status(hub, port1, &portstatus, &unused);
+ disabled = !usb_port_is_power_on(hub, portstatus);
+
+out_hdev_lock:
+ usb_unlock_device(hdev);
+ usb_autopm_put_interface(intf);
+
+ if (rc)
+ return rc;
+
+ return sysfs_emit(buf, "%s\n", disabled ? "1" : "0");
+}
+
+static ssize_t disable_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct usb_port *port_dev = to_usb_port(dev);
+ struct usb_device *hdev = to_usb_device(dev->parent->parent);
+ struct usb_hub *hub = usb_hub_to_struct_hub(hdev);
+ struct usb_interface *intf = to_usb_interface(hub->intfdev);
+ int port1 = port_dev->portnum;
+ bool disabled;
+ int rc;
+
+ rc = strtobool(buf, &disabled);
+ if (rc)
+ return rc;
+
+ rc = usb_autopm_get_interface(intf);
+ if (rc < 0)
+ return rc;
+
+ usb_lock_device(hdev);
+ if (hub->disconnected) {
+ rc = -ENODEV;
+ goto out_hdev_lock;
+ }
+
+ if (disabled && port_dev->child)
+ usb_disconnect(&port_dev->child);
+
+ rc = usb_hub_set_port_power(hdev, hub, port1, !disabled);
+
+ if (disabled) {
+ usb_clear_port_feature(hdev, port1, USB_PORT_FEAT_C_CONNECTION);
+ if (!port_dev->is_superspeed)
+ usb_clear_port_feature(hdev, port1, USB_PORT_FEAT_C_ENABLE);
+ }
+
+ if (!rc)
+ rc = count;
+
+out_hdev_lock:
+ usb_unlock_device(hdev);
+ usb_autopm_put_interface(intf);
+
+ return rc;
+}
+static DEVICE_ATTR_RW(disable);
+
static ssize_t location_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
@@ -153,6 +235,7 @@ static struct attribute *port_dev_attrs[] = {
&dev_attr_location.attr,
&dev_attr_quirks.attr,
&dev_attr_over_current_count.attr,
+ &dev_attr_disable.attr,
NULL,
};