mirror of
https://github.com/torvalds/linux.git
synced 2024-11-27 22:51:35 +00:00
a5cfc9d658
Wake on connect/disconnect is only supported while runtime suspend for now, which is obviously necessary. It is also not inherently desired for the system to wakeup on Thunderbolt/USB4 hot plug events. However, we can still make user in control of waking up the system in the events of hot plug/unplug. This patch adds 'wakeup' attribute under 'usb4_portX/power' sysfs attribute and only enables wakes on connect/disconnect to the respective port when 'wakeup' is set to 'enabled'. The attribute is set to 'disabled' by default. Signed-off-by: Rajat Khandelwal <rajat.khandelwal@intel.com> Signed-off-by: Mika Westerberg <mika.westerberg@linux.intel.com>
324 lines
7.1 KiB
C
324 lines
7.1 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* USB4 port device
|
|
*
|
|
* Copyright (C) 2021, Intel Corporation
|
|
* Author: Mika Westerberg <mika.westerberg@linux.intel.com>
|
|
*/
|
|
|
|
#include <linux/pm_runtime.h>
|
|
#include <linux/component.h>
|
|
#include <linux/property.h>
|
|
|
|
#include "tb.h"
|
|
|
|
static int connector_bind(struct device *dev, struct device *connector, void *data)
|
|
{
|
|
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;
|
|
}
|
|
|
|
static void connector_unbind(struct device *dev, struct device *connector, void *data)
|
|
{
|
|
sysfs_remove_link(&connector->kobj, dev_name(dev));
|
|
sysfs_remove_link(&dev->kobj, "connector");
|
|
}
|
|
|
|
static const struct component_ops connector_ops = {
|
|
.bind = connector_bind,
|
|
.unbind = connector_unbind,
|
|
};
|
|
|
|
static ssize_t link_show(struct device *dev, struct device_attribute *attr,
|
|
char *buf)
|
|
{
|
|
struct usb4_port *usb4 = tb_to_usb4_port_device(dev);
|
|
struct tb_port *port = usb4->port;
|
|
struct tb *tb = port->sw->tb;
|
|
const char *link;
|
|
|
|
if (mutex_lock_interruptible(&tb->lock))
|
|
return -ERESTARTSYS;
|
|
|
|
if (tb_is_upstream_port(port))
|
|
link = port->sw->link_usb4 ? "usb4" : "tbt";
|
|
else if (tb_port_has_remote(port))
|
|
link = port->remote->sw->link_usb4 ? "usb4" : "tbt";
|
|
else if (port->xdomain)
|
|
link = port->xdomain->link_usb4 ? "usb4" : "tbt";
|
|
else
|
|
link = "none";
|
|
|
|
mutex_unlock(&tb->lock);
|
|
|
|
return sysfs_emit(buf, "%s\n", link);
|
|
}
|
|
static DEVICE_ATTR_RO(link);
|
|
|
|
static struct attribute *common_attrs[] = {
|
|
&dev_attr_link.attr,
|
|
NULL
|
|
};
|
|
|
|
static const struct attribute_group common_group = {
|
|
.attrs = common_attrs,
|
|
};
|
|
|
|
static int usb4_port_offline(struct usb4_port *usb4)
|
|
{
|
|
struct tb_port *port = usb4->port;
|
|
int ret;
|
|
|
|
ret = tb_acpi_power_on_retimers(port);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = usb4_port_router_offline(port);
|
|
if (ret) {
|
|
tb_acpi_power_off_retimers(port);
|
|
return ret;
|
|
}
|
|
|
|
ret = tb_retimer_scan(port, false);
|
|
if (ret) {
|
|
usb4_port_router_online(port);
|
|
tb_acpi_power_off_retimers(port);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void usb4_port_online(struct usb4_port *usb4)
|
|
{
|
|
struct tb_port *port = usb4->port;
|
|
|
|
usb4_port_router_online(port);
|
|
tb_acpi_power_off_retimers(port);
|
|
}
|
|
|
|
static ssize_t offline_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct usb4_port *usb4 = tb_to_usb4_port_device(dev);
|
|
|
|
return sysfs_emit(buf, "%d\n", usb4->offline);
|
|
}
|
|
|
|
static ssize_t offline_store(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
struct usb4_port *usb4 = tb_to_usb4_port_device(dev);
|
|
struct tb_port *port = usb4->port;
|
|
struct tb *tb = port->sw->tb;
|
|
bool val;
|
|
int ret;
|
|
|
|
ret = kstrtobool(buf, &val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
pm_runtime_get_sync(&usb4->dev);
|
|
|
|
if (mutex_lock_interruptible(&tb->lock)) {
|
|
ret = -ERESTARTSYS;
|
|
goto out_rpm;
|
|
}
|
|
|
|
if (val == usb4->offline)
|
|
goto out_unlock;
|
|
|
|
/* Offline mode works only for ports that are not connected */
|
|
if (tb_port_has_remote(port)) {
|
|
ret = -EBUSY;
|
|
goto out_unlock;
|
|
}
|
|
|
|
if (val) {
|
|
ret = usb4_port_offline(usb4);
|
|
if (ret)
|
|
goto out_unlock;
|
|
} else {
|
|
usb4_port_online(usb4);
|
|
tb_retimer_remove_all(port);
|
|
}
|
|
|
|
usb4->offline = val;
|
|
tb_port_dbg(port, "%s offline mode\n", val ? "enter" : "exit");
|
|
|
|
out_unlock:
|
|
mutex_unlock(&tb->lock);
|
|
out_rpm:
|
|
pm_runtime_mark_last_busy(&usb4->dev);
|
|
pm_runtime_put_autosuspend(&usb4->dev);
|
|
|
|
return ret ? ret : count;
|
|
}
|
|
static DEVICE_ATTR_RW(offline);
|
|
|
|
static ssize_t rescan_store(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
struct usb4_port *usb4 = tb_to_usb4_port_device(dev);
|
|
struct tb_port *port = usb4->port;
|
|
struct tb *tb = port->sw->tb;
|
|
bool val;
|
|
int ret;
|
|
|
|
ret = kstrtobool(buf, &val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (!val)
|
|
return count;
|
|
|
|
pm_runtime_get_sync(&usb4->dev);
|
|
|
|
if (mutex_lock_interruptible(&tb->lock)) {
|
|
ret = -ERESTARTSYS;
|
|
goto out_rpm;
|
|
}
|
|
|
|
/* Must be in offline mode already */
|
|
if (!usb4->offline) {
|
|
ret = -EINVAL;
|
|
goto out_unlock;
|
|
}
|
|
|
|
tb_retimer_remove_all(port);
|
|
ret = tb_retimer_scan(port, true);
|
|
|
|
out_unlock:
|
|
mutex_unlock(&tb->lock);
|
|
out_rpm:
|
|
pm_runtime_mark_last_busy(&usb4->dev);
|
|
pm_runtime_put_autosuspend(&usb4->dev);
|
|
|
|
return ret ? ret : count;
|
|
}
|
|
static DEVICE_ATTR_WO(rescan);
|
|
|
|
static struct attribute *service_attrs[] = {
|
|
&dev_attr_offline.attr,
|
|
&dev_attr_rescan.attr,
|
|
NULL
|
|
};
|
|
|
|
static umode_t service_attr_is_visible(struct kobject *kobj,
|
|
struct attribute *attr, int n)
|
|
{
|
|
struct device *dev = kobj_to_dev(kobj);
|
|
struct usb4_port *usb4 = tb_to_usb4_port_device(dev);
|
|
|
|
/*
|
|
* Always need some platform help to cycle the modes so that
|
|
* retimers can be accessed through the sideband.
|
|
*/
|
|
return usb4->can_offline ? attr->mode : 0;
|
|
}
|
|
|
|
static const struct attribute_group service_group = {
|
|
.attrs = service_attrs,
|
|
.is_visible = service_attr_is_visible,
|
|
};
|
|
|
|
static const struct attribute_group *usb4_port_device_groups[] = {
|
|
&common_group,
|
|
&service_group,
|
|
NULL
|
|
};
|
|
|
|
static void usb4_port_device_release(struct device *dev)
|
|
{
|
|
struct usb4_port *usb4 = container_of(dev, struct usb4_port, dev);
|
|
|
|
kfree(usb4);
|
|
}
|
|
|
|
struct device_type usb4_port_device_type = {
|
|
.name = "usb4_port",
|
|
.groups = usb4_port_device_groups,
|
|
.release = usb4_port_device_release,
|
|
};
|
|
|
|
/**
|
|
* usb4_port_device_add() - Add USB4 port device
|
|
* @port: Lane 0 adapter port to add the USB4 port
|
|
*
|
|
* Creates and registers a USB4 port device for @port. Returns the new
|
|
* USB4 port device pointer or ERR_PTR() in case of error.
|
|
*/
|
|
struct usb4_port *usb4_port_device_add(struct tb_port *port)
|
|
{
|
|
struct usb4_port *usb4;
|
|
int ret;
|
|
|
|
usb4 = kzalloc(sizeof(*usb4), GFP_KERNEL);
|
|
if (!usb4)
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
usb4->port = port;
|
|
usb4->dev.type = &usb4_port_device_type;
|
|
usb4->dev.parent = &port->sw->dev;
|
|
dev_set_name(&usb4->dev, "usb4_port%d", port->port);
|
|
|
|
ret = device_register(&usb4->dev);
|
|
if (ret) {
|
|
put_device(&usb4->dev);
|
|
return ERR_PTR(ret);
|
|
}
|
|
|
|
if (dev_fwnode(&usb4->dev)) {
|
|
ret = component_add(&usb4->dev, &connector_ops);
|
|
if (ret) {
|
|
dev_err(&usb4->dev, "failed to add component\n");
|
|
device_unregister(&usb4->dev);
|
|
}
|
|
}
|
|
|
|
if (!tb_is_upstream_port(port))
|
|
device_set_wakeup_capable(&usb4->dev, true);
|
|
|
|
pm_runtime_no_callbacks(&usb4->dev);
|
|
pm_runtime_set_active(&usb4->dev);
|
|
pm_runtime_enable(&usb4->dev);
|
|
pm_runtime_set_autosuspend_delay(&usb4->dev, TB_AUTOSUSPEND_DELAY);
|
|
pm_runtime_mark_last_busy(&usb4->dev);
|
|
pm_runtime_use_autosuspend(&usb4->dev);
|
|
|
|
return usb4;
|
|
}
|
|
|
|
/**
|
|
* usb4_port_device_remove() - Removes USB4 port device
|
|
* @usb4: USB4 port device
|
|
*
|
|
* Unregisters the USB4 port device from the system. The device will be
|
|
* released when the last reference is dropped.
|
|
*/
|
|
void usb4_port_device_remove(struct usb4_port *usb4)
|
|
{
|
|
if (dev_fwnode(&usb4->dev))
|
|
component_del(&usb4->dev, &connector_ops);
|
|
device_unregister(&usb4->dev);
|
|
}
|
|
|
|
/**
|
|
* usb4_port_device_resume() - Resumes USB4 port device
|
|
* @usb4: USB4 port device
|
|
*
|
|
* Used to resume USB4 port device after sleep state.
|
|
*/
|
|
int usb4_port_device_resume(struct usb4_port *usb4)
|
|
{
|
|
return usb4->offline ? usb4_port_offline(usb4) : 0;
|
|
}
|