diff options
Diffstat (limited to 'drivers/usb/core/port.c')
| -rw-r--r-- | drivers/usb/core/port.c | 321 |
1 files changed, 298 insertions, 23 deletions
diff --git a/drivers/usb/core/port.c b/drivers/usb/core/port.c index 460c855be0d0..f54198171b6a 100644 --- a/drivers/usb/core/port.c +++ b/drivers/usb/core/port.c @@ -1,23 +1,19 @@ +// SPDX-License-Identifier: GPL-2.0 /* * usb port device code * * Copyright (C) 2012 Intel Corp * * Author: Lan Tianyu <tianyu.lan@intel.com> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * for more details. - * */ +#include <linux/kstrtox.h> #include <linux/slab.h> +#include <linux/string_choices.h> +#include <linux/sysfs.h> #include <linux/pm_qos.h> +#include <linux/component.h> +#include <linux/usb/of.h> #include "hub.h" @@ -25,6 +21,157 @@ static int usb_port_block_power_off; static const struct attribute_group *port_dev_group[]; +static ssize_t early_stop_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct usb_port *port_dev = to_usb_port(dev); + + return sysfs_emit(buf, "%s\n", str_yes_no(port_dev->early_stop)); +} + +static ssize_t early_stop_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct usb_port *port_dev = to_usb_port(dev); + bool value; + + if (kstrtobool(buf, &value)) + return -EINVAL; + + if (value) + port_dev->early_stop = 1; + else + port_dev->early_stop = 0; + + return count; +} +static DEVICE_ATTR_RW(early_stop); + +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(dev->parent); + int port1 = port_dev->portnum; + u16 portstatus, unused; + bool disabled; + int rc; + struct kernfs_node *kn; + + if (!hub) + return -ENODEV; + hub_get(hub); + rc = usb_autopm_get_interface(intf); + if (rc < 0) + goto out_hub_get; + + /* + * Prevent deadlock if another process is concurrently + * trying to unregister hdev. + */ + kn = sysfs_break_active_protection(&dev->kobj, &attr->attr); + if (!kn) { + rc = -ENODEV; + goto out_autopm; + } + 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); + sysfs_unbreak_active_protection(kn); + out_autopm: + usb_autopm_put_interface(intf); + out_hub_get: + hub_put(hub); + + 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(dev->parent); + int port1 = port_dev->portnum; + bool disabled; + int rc; + struct kernfs_node *kn; + + if (!hub) + return -ENODEV; + rc = kstrtobool(buf, &disabled); + if (rc) + return rc; + + hub_get(hub); + rc = usb_autopm_get_interface(intf); + if (rc < 0) + goto out_hub_get; + + /* + * Prevent deadlock if another process is concurrently + * trying to unregister hdev. + */ + kn = sysfs_break_active_protection(&dev->kobj, &attr->attr); + if (!kn) { + rc = -ENODEV; + goto out_autopm; + } + 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); + sysfs_unbreak_active_protection(kn); + out_autopm: + usb_autopm_put_interface(intf); + out_hub_get: + hub_put(hub); + + return rc; +} +static DEVICE_ATTR_RW(disable); + +static ssize_t location_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct usb_port *port_dev = to_usb_port(dev); + + return sysfs_emit(buf, "0x%08x\n", port_dev->location); +} +static DEVICE_ATTR_RO(location); + static ssize_t connect_type_show(struct device *dev, struct device_attribute *attr, char *buf) { @@ -46,10 +193,51 @@ static ssize_t connect_type_show(struct device *dev, break; } - return sprintf(buf, "%s\n", result); + return sysfs_emit(buf, "%s\n", result); } static DEVICE_ATTR_RO(connect_type); +static ssize_t state_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct usb_port *port_dev = to_usb_port(dev); + enum usb_device_state state = READ_ONCE(port_dev->state); + + return sysfs_emit(buf, "%s\n", usb_state_string(state)); +} +static DEVICE_ATTR_RO(state); + +static ssize_t over_current_count_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct usb_port *port_dev = to_usb_port(dev); + + return sysfs_emit(buf, "%u\n", port_dev->over_current_count); +} +static DEVICE_ATTR_RO(over_current_count); + +static ssize_t quirks_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct usb_port *port_dev = to_usb_port(dev); + + return sysfs_emit(buf, "%08x\n", port_dev->quirks); +} + +static ssize_t quirks_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct usb_port *port_dev = to_usb_port(dev); + u32 value; + + if (kstrtou32(buf, 16, &value)) + return -EINVAL; + + port_dev->quirks = value; + return count; +} +static DEVICE_ATTR_RW(quirks); + static ssize_t usb3_lpm_permit_show(struct device *dev, struct device_attribute *attr, char *buf) { @@ -68,7 +256,7 @@ static ssize_t usb3_lpm_permit_show(struct device *dev, p = "0"; } - return sprintf(buf, "%s\n", p); + return sysfs_emit(buf, "%s\n", p); } static ssize_t usb3_lpm_permit_store(struct device *dev, @@ -118,10 +306,16 @@ static DEVICE_ATTR_RW(usb3_lpm_permit); static struct attribute *port_dev_attrs[] = { &dev_attr_connect_type.attr, + &dev_attr_state.attr, + &dev_attr_location.attr, + &dev_attr_quirks.attr, + &dev_attr_over_current_count.attr, + &dev_attr_disable.attr, + &dev_attr_early_stop.attr, NULL, }; -static struct attribute_group port_dev_attr_grp = { +static const struct attribute_group port_dev_attr_grp = { .attrs = port_dev_attrs, }; @@ -135,7 +329,7 @@ static struct attribute *port_dev_usb3_attrs[] = { NULL, }; -static struct attribute_group port_dev_usb3_attr_grp = { +static const struct attribute_group port_dev_usb3_attr_grp = { .attrs = port_dev_usb3_attrs, }; @@ -179,7 +373,10 @@ static int usb_port_runtime_resume(struct device *dev) if (!port_dev->is_superspeed && peer) pm_runtime_get_sync(&peer->dev); - usb_autopm_get_interface(intf); + retval = usb_autopm_get_interface(intf); + if (retval < 0) + return retval; + retval = usb_hub_set_port_power(hdev, hub, port1, true); msleep(hub_power_on_good_delay(hub)); if (udev && !retval) { @@ -232,7 +429,10 @@ static int usb_port_runtime_suspend(struct device *dev) if (usb_port_block_power_off) return -EBUSY; - usb_autopm_get_interface(intf); + retval = usb_autopm_get_interface(intf); + if (retval < 0) + return retval; + retval = usb_hub_set_port_power(hdev, hub, port1, false); usb_clear_port_feature(hdev, port1, USB_PORT_FEAT_C_CONNECTION); if (!port_dev->is_superspeed) @@ -251,6 +451,17 @@ static int usb_port_runtime_suspend(struct device *dev) } #endif +static void usb_port_shutdown(struct device *dev) +{ + struct usb_port *port_dev = to_usb_port(dev); + struct usb_device *udev = port_dev->child; + + if (udev && !udev->port_is_suspended) { + usb_disable_usb2_hardware_lpm(udev); + usb_unlocked_disable_lpm(udev); + } +} + static const struct dev_pm_ops usb_port_pm_ops = { #ifdef CONFIG_PM .runtime_suspend = usb_port_runtime_suspend, @@ -258,7 +469,7 @@ static const struct dev_pm_ops usb_port_pm_ops = { #endif }; -struct device_type usb_port_device_type = { +const struct device_type usb_port_device_type = { .name = "usb_port", .release = usb_port_device_release, .pm = &usb_port_pm_ops, @@ -267,6 +478,7 @@ struct device_type usb_port_device_type = { static struct device_driver usb_port_driver = { .name = "usb", .owner = THIS_MODULE, + .shutdown = usb_port_shutdown, }; static int link_peers(struct usb_port *left, struct usb_port *right) @@ -401,7 +613,7 @@ static int match_location(struct usb_device *peer_hdev, void *p) struct usb_hub *peer_hub = usb_hub_to_struct_hub(peer_hdev); struct usb_device *hdev = to_usb_device(port_dev->dev.parent->parent); - if (!peer_hub) + if (!peer_hub || port_dev->connect_type == USB_PORT_NOT_USED) return 0; hcd = bus_to_hcd(hdev->bus); @@ -412,7 +624,8 @@ static int match_location(struct usb_device *peer_hdev, void *p) for (port1 = 1; port1 <= peer_hdev->maxchild; port1++) { peer = peer_hub->ports[port1 - 1]; - if (peer && peer->location == port_dev->location) { + if (peer && peer->connect_type != USB_PORT_NOT_USED && + peer->location == port_dev->location) { link_peers_report(port_dev, peer); return 1; /* done */ } @@ -479,6 +692,47 @@ static void find_and_link_peer(struct usb_hub *hub, int port1) link_peers_report(port_dev, peer); } +static int connector_bind(struct device *dev, struct device *connector, void *data) +{ + struct usb_port *port_dev = to_usb_port(dev); + int ret; + + ret = sysfs_create_link(&dev->kobj, &connector->kobj, "connector"); + if (ret) + return ret; + + ret = sysfs_create_link(&connector->kobj, &dev->kobj, dev_name(dev)); + if (ret) { + sysfs_remove_link(&dev->kobj, "connector"); + return ret; + } + + port_dev->connector = data; + + /* + * If there is already USB device connected to the port, letting the + * Type-C connector know about it immediately. + */ + if (port_dev->child) + typec_attach(port_dev->connector, &port_dev->child->dev); + + return 0; +} + +static void connector_unbind(struct device *dev, struct device *connector, void *data) +{ + struct usb_port *port_dev = to_usb_port(dev); + + sysfs_remove_link(&connector->kobj, dev_name(dev)); + sysfs_remove_link(&dev->kobj, "connector"); + port_dev->connector = NULL; +} + +static const struct component_ops connector_ops = { + .bind = connector_bind, + .unbind = connector_unbind, +}; + int usb_hub_create_port_device(struct usb_hub *hub, int port1) { struct usb_port *port_dev; @@ -495,11 +749,13 @@ int usb_hub_create_port_device(struct usb_hub *hub, int port1) return -ENOMEM; } + port_dev->connect_type = usb_of_get_connect_type(hdev, port1); hub->ports[port1 - 1] = port_dev; port_dev->portnum = port1; set_bit(port1, hub->power_bits); port_dev->dev.parent = hub->intfdev; if (hub_is_superspeed(hdev)) { + port_dev->is_superspeed = 1; port_dev->usb3_lpm_u1_permit = 1; port_dev->usb3_lpm_u2_permit = 1; port_dev->dev.groups = port_dev_usb3_group; @@ -507,8 +763,6 @@ int usb_hub_create_port_device(struct usb_hub *hub, int port1) port_dev->dev.groups = port_dev_group; port_dev->dev.type = &usb_port_device_type; port_dev->dev.driver = &usb_port_driver; - if (hub_is_superspeed(hub->hdev)) - port_dev->is_superspeed = 1; dev_set_name(&port_dev->dev, "%s-port%d", dev_name(&hub->hdev->dev), port1); mutex_init(&port_dev->status_lock); @@ -518,12 +772,24 @@ int usb_hub_create_port_device(struct usb_hub *hub, int port1) return retval; } + port_dev->state_kn = sysfs_get_dirent(port_dev->dev.kobj.sd, "state"); + if (!port_dev->state_kn) { + dev_err(&port_dev->dev, "failed to sysfs_get_dirent 'state'\n"); + retval = -ENODEV; + goto err_unregister; + } + /* Set default policy of port-poweroff disabled. */ retval = dev_pm_qos_add_request(&port_dev->dev, port_dev->req, DEV_PM_QOS_FLAGS, PM_QOS_FLAG_NO_POWER_OFF); if (retval < 0) { - device_unregister(&port_dev->dev); - return retval; + goto err_put_kn; + } + + retval = component_add(&port_dev->dev, &connector_ops); + if (retval) { + dev_warn(&port_dev->dev, "failed to add component\n"); + goto err_put_kn; } find_and_link_peer(hub, port1); @@ -560,6 +826,13 @@ int usb_hub_create_port_device(struct usb_hub *hub, int port1) port_dev->req = NULL; } return 0; + +err_put_kn: + sysfs_put(port_dev->state_kn); +err_unregister: + device_unregister(&port_dev->dev); + + return retval; } void usb_hub_remove_port_device(struct usb_hub *hub, int port1) @@ -570,5 +843,7 @@ void usb_hub_remove_port_device(struct usb_hub *hub, int port1) peer = port_dev->peer; if (peer) unlink_peers(port_dev, peer); + component_del(&port_dev->dev, &connector_ops); + sysfs_put(port_dev->state_kn); device_unregister(&port_dev->dev); } |
