diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-06 01:02:30 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-06 01:02:30 +0000 |
commit | 76cb841cb886eef6b3bee341a2266c76578724ad (patch) | |
tree | f5892e5ba6cc11949952a6ce4ecbe6d516d6ce58 /drivers/usb/roles | |
parent | Initial commit. (diff) | |
download | linux-76cb841cb886eef6b3bee341a2266c76578724ad.tar.xz linux-76cb841cb886eef6b3bee341a2266c76578724ad.zip |
Adding upstream version 4.19.249.upstream/4.19.249
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'drivers/usb/roles')
-rw-r--r-- | drivers/usb/roles/Kconfig | 27 | ||||
-rw-r--r-- | drivers/usb/roles/Makefile | 3 | ||||
-rw-r--r-- | drivers/usb/roles/class.c | 314 | ||||
-rw-r--r-- | drivers/usb/roles/intel-xhci-usb-role-switch.c | 189 |
4 files changed, 533 insertions, 0 deletions
diff --git a/drivers/usb/roles/Kconfig b/drivers/usb/roles/Kconfig new file mode 100644 index 000000000..e4194ac94 --- /dev/null +++ b/drivers/usb/roles/Kconfig @@ -0,0 +1,27 @@ +config USB_ROLE_SWITCH + tristate "USB Role Switch Support" + help + USB Role Switch is a device that can select the USB role - host or + device - for a USB port (connector). In most cases dual-role capable + USB controller will also represent the switch, but on some platforms + multiplexer/demultiplexer switch is used to route the data lines on + the USB connector between separate USB host and device controllers. + + Say Y here if your USB connectors support both device and host roles. + To compile the driver as module, choose M here: the module will be + called roles.ko. + +if USB_ROLE_SWITCH + +config USB_ROLES_INTEL_XHCI + tristate "Intel XHCI USB Role Switch" + depends on ACPI && X86 + help + Driver for the internal USB role switch for switching the USB data + lines between the xHCI host controller and the dwc3 gadget controller + found on various Intel SoCs. + + To compile the driver as a module, choose M here: the module will + be called intel-xhci-usb-role-switch. + +endif # USB_ROLE_SWITCH diff --git a/drivers/usb/roles/Makefile b/drivers/usb/roles/Makefile new file mode 100644 index 000000000..c02873206 --- /dev/null +++ b/drivers/usb/roles/Makefile @@ -0,0 +1,3 @@ +obj-$(CONFIG_USB_ROLE_SWITCH) += roles.o +roles-y := class.o +obj-$(CONFIG_USB_ROLES_INTEL_XHCI) += intel-xhci-usb-role-switch.o diff --git a/drivers/usb/roles/class.c b/drivers/usb/roles/class.c new file mode 100644 index 000000000..1dd492e89 --- /dev/null +++ b/drivers/usb/roles/class.c @@ -0,0 +1,314 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * USB Role Switch Support + * + * Copyright (C) 2018 Intel Corporation + * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com> + * Hans de Goede <hdegoede@redhat.com> + */ + +#include <linux/usb/role.h> +#include <linux/device.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/slab.h> + +static struct class *role_class; + +struct usb_role_switch { + struct device dev; + struct mutex lock; /* device lock*/ + enum usb_role role; + + /* From descriptor */ + struct device *usb2_port; + struct device *usb3_port; + struct device *udc; + usb_role_switch_set_t set; + usb_role_switch_get_t get; + bool allow_userspace_control; +}; + +#define to_role_switch(d) container_of(d, struct usb_role_switch, dev) + +/** + * usb_role_switch_set_role - Set USB role for a switch + * @sw: USB role switch + * @role: USB role to be switched to + * + * Set USB role @role for @sw. + */ +int usb_role_switch_set_role(struct usb_role_switch *sw, enum usb_role role) +{ + int ret; + + if (IS_ERR_OR_NULL(sw)) + return 0; + + mutex_lock(&sw->lock); + + ret = sw->set(sw->dev.parent, role); + if (!ret) + sw->role = role; + + mutex_unlock(&sw->lock); + + return ret; +} +EXPORT_SYMBOL_GPL(usb_role_switch_set_role); + +/** + * usb_role_switch_get_role - Get the USB role for a switch + * @sw: USB role switch + * + * Depending on the role-switch-driver this function returns either a cached + * value of the last set role, or reads back the actual value from the hardware. + */ +enum usb_role usb_role_switch_get_role(struct usb_role_switch *sw) +{ + enum usb_role role; + + if (IS_ERR_OR_NULL(sw)) + return USB_ROLE_NONE; + + mutex_lock(&sw->lock); + + if (sw->get) + role = sw->get(sw->dev.parent); + else + role = sw->role; + + mutex_unlock(&sw->lock); + + return role; +} +EXPORT_SYMBOL_GPL(usb_role_switch_get_role); + +static int __switch_match(struct device *dev, const void *name) +{ + return !strcmp((const char *)name, dev_name(dev)); +} + +static void *usb_role_switch_match(struct device_connection *con, int ep, + void *data) +{ + struct device *dev; + + dev = class_find_device(role_class, NULL, con->endpoint[ep], + __switch_match); + + return dev ? to_role_switch(dev) : ERR_PTR(-EPROBE_DEFER); +} + +/** + * usb_role_switch_get - Find USB role switch linked with the caller + * @dev: The caller device + * + * Finds and returns role switch linked with @dev. The reference count for the + * found switch is incremented. + */ +struct usb_role_switch *usb_role_switch_get(struct device *dev) +{ + struct usb_role_switch *sw; + + sw = device_connection_find_match(dev, "usb-role-switch", NULL, + usb_role_switch_match); + + if (!IS_ERR_OR_NULL(sw)) + WARN_ON(!try_module_get(sw->dev.parent->driver->owner)); + + return sw; +} +EXPORT_SYMBOL_GPL(usb_role_switch_get); + +/** + * usb_role_switch_put - Release handle to a switch + * @sw: USB Role Switch + * + * Decrement reference count for @sw. + */ +void usb_role_switch_put(struct usb_role_switch *sw) +{ + if (!IS_ERR_OR_NULL(sw)) { + module_put(sw->dev.parent->driver->owner); + put_device(&sw->dev); + } +} +EXPORT_SYMBOL_GPL(usb_role_switch_put); + +static umode_t +usb_role_switch_is_visible(struct kobject *kobj, struct attribute *attr, int n) +{ + struct device *dev = container_of(kobj, typeof(*dev), kobj); + struct usb_role_switch *sw = to_role_switch(dev); + + if (sw->allow_userspace_control) + return attr->mode; + + return 0; +} + +static const char * const usb_roles[] = { + [USB_ROLE_NONE] = "none", + [USB_ROLE_HOST] = "host", + [USB_ROLE_DEVICE] = "device", +}; + +static ssize_t +role_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct usb_role_switch *sw = to_role_switch(dev); + enum usb_role role = usb_role_switch_get_role(sw); + + return sprintf(buf, "%s\n", usb_roles[role]); +} + +static ssize_t role_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t size) +{ + struct usb_role_switch *sw = to_role_switch(dev); + int ret; + + ret = sysfs_match_string(usb_roles, buf); + if (ret < 0) { + bool res; + + /* Extra check if the user wants to disable the switch */ + ret = kstrtobool(buf, &res); + if (ret || res) + return -EINVAL; + } + + ret = usb_role_switch_set_role(sw, ret); + if (ret) + return ret; + + return size; +} +static DEVICE_ATTR_RW(role); + +static struct attribute *usb_role_switch_attrs[] = { + &dev_attr_role.attr, + NULL, +}; + +static const struct attribute_group usb_role_switch_group = { + .is_visible = usb_role_switch_is_visible, + .attrs = usb_role_switch_attrs, +}; + +static const struct attribute_group *usb_role_switch_groups[] = { + &usb_role_switch_group, + NULL, +}; + +static int +usb_role_switch_uevent(struct device *dev, struct kobj_uevent_env *env) +{ + int ret; + + ret = add_uevent_var(env, "USB_ROLE_SWITCH=%s", dev_name(dev)); + if (ret) + dev_err(dev, "failed to add uevent USB_ROLE_SWITCH\n"); + + return ret; +} + +static void usb_role_switch_release(struct device *dev) +{ + struct usb_role_switch *sw = to_role_switch(dev); + + kfree(sw); +} + +static const struct device_type usb_role_dev_type = { + .name = "usb_role_switch", + .groups = usb_role_switch_groups, + .uevent = usb_role_switch_uevent, + .release = usb_role_switch_release, +}; + +/** + * usb_role_switch_register - Register USB Role Switch + * @parent: Parent device for the switch + * @desc: Description of the switch + * + * USB Role Switch is a device capable or choosing the role for USB connector. + * On platforms where the USB controller is dual-role capable, the controller + * driver will need to register the switch. On platforms where the USB host and + * USB device controllers behind the connector are separate, there will be a + * mux, and the driver for that mux will need to register the switch. + * + * Returns handle to a new role switch or ERR_PTR. The content of @desc is + * copied. + */ +struct usb_role_switch * +usb_role_switch_register(struct device *parent, + const struct usb_role_switch_desc *desc) +{ + struct usb_role_switch *sw; + int ret; + + if (!desc || !desc->set) + return ERR_PTR(-EINVAL); + + sw = kzalloc(sizeof(*sw), GFP_KERNEL); + if (!sw) + return ERR_PTR(-ENOMEM); + + mutex_init(&sw->lock); + + sw->allow_userspace_control = desc->allow_userspace_control; + sw->usb2_port = desc->usb2_port; + sw->usb3_port = desc->usb3_port; + sw->udc = desc->udc; + sw->set = desc->set; + sw->get = desc->get; + + sw->dev.parent = parent; + sw->dev.class = role_class; + sw->dev.type = &usb_role_dev_type; + dev_set_name(&sw->dev, "%s-role-switch", dev_name(parent)); + + ret = device_register(&sw->dev); + if (ret) { + put_device(&sw->dev); + return ERR_PTR(ret); + } + + /* TODO: Symlinks for the host port and the device controller. */ + + return sw; +} +EXPORT_SYMBOL_GPL(usb_role_switch_register); + +/** + * usb_role_switch_unregister - Unregsiter USB Role Switch + * @sw: USB Role Switch + * + * Unregister switch that was registered with usb_role_switch_register(). + */ +void usb_role_switch_unregister(struct usb_role_switch *sw) +{ + if (!IS_ERR_OR_NULL(sw)) + device_unregister(&sw->dev); +} +EXPORT_SYMBOL_GPL(usb_role_switch_unregister); + +static int __init usb_roles_init(void) +{ + role_class = class_create(THIS_MODULE, "usb_role"); + return PTR_ERR_OR_ZERO(role_class); +} +subsys_initcall(usb_roles_init); + +static void __exit usb_roles_exit(void) +{ + class_destroy(role_class); +} +module_exit(usb_roles_exit); + +MODULE_AUTHOR("Heikki Krogerus <heikki.krogerus@linux.intel.com>"); +MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>"); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("USB Role Class"); diff --git a/drivers/usb/roles/intel-xhci-usb-role-switch.c b/drivers/usb/roles/intel-xhci-usb-role-switch.c new file mode 100644 index 000000000..277de9618 --- /dev/null +++ b/drivers/usb/roles/intel-xhci-usb-role-switch.c @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Intel XHCI (Cherry Trail, Broxton and others) USB OTG role switch driver + * + * Copyright (c) 2016-2017 Hans de Goede <hdegoede@redhat.com> + * + * Loosely based on android x86 kernel code which is: + * + * Copyright (C) 2014 Intel Corp. + * + * Author: Wu, Hao + */ + +#include <linux/acpi.h> +#include <linux/delay.h> +#include <linux/err.h> +#include <linux/io.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/pm_runtime.h> +#include <linux/usb/role.h> + +/* register definition */ +#define DUAL_ROLE_CFG0 0x68 +#define SW_VBUS_VALID BIT(24) +#define SW_IDPIN_EN BIT(21) +#define SW_IDPIN BIT(20) + +#define DUAL_ROLE_CFG1 0x6c +#define HOST_MODE BIT(29) + +#define DUAL_ROLE_CFG1_POLL_TIMEOUT 1000 + +#define DRV_NAME "intel_xhci_usb_sw" + +struct intel_xhci_usb_data { + struct usb_role_switch *role_sw; + void __iomem *base; +}; + +static int intel_xhci_usb_set_role(struct device *dev, enum usb_role role) +{ + struct intel_xhci_usb_data *data = dev_get_drvdata(dev); + unsigned long timeout; + acpi_status status; + u32 glk, val; + + /* + * On many CHT devices ACPI event (_AEI) handlers read / modify / + * write the cfg0 register, just like we do. Take the ACPI lock + * to avoid us racing with the AML code. + */ + status = acpi_acquire_global_lock(ACPI_WAIT_FOREVER, &glk); + if (ACPI_FAILURE(status) && status != AE_NOT_CONFIGURED) { + dev_err(dev, "Error could not acquire lock\n"); + return -EIO; + } + + pm_runtime_get_sync(dev); + + /* Set idpin value as requested */ + val = readl(data->base + DUAL_ROLE_CFG0); + switch (role) { + case USB_ROLE_NONE: + val |= SW_IDPIN; + val &= ~SW_VBUS_VALID; + break; + case USB_ROLE_HOST: + val &= ~SW_IDPIN; + val &= ~SW_VBUS_VALID; + break; + case USB_ROLE_DEVICE: + val |= SW_IDPIN; + val |= SW_VBUS_VALID; + break; + } + val |= SW_IDPIN_EN; + + writel(val, data->base + DUAL_ROLE_CFG0); + + acpi_release_global_lock(glk); + + /* In most case it takes about 600ms to finish mode switching */ + timeout = jiffies + msecs_to_jiffies(DUAL_ROLE_CFG1_POLL_TIMEOUT); + + /* Polling on CFG1 register to confirm mode switch.*/ + do { + val = readl(data->base + DUAL_ROLE_CFG1); + if (!!(val & HOST_MODE) == (role == USB_ROLE_HOST)) { + pm_runtime_put(dev); + return 0; + } + + /* Interval for polling is set to about 5 - 10 ms */ + usleep_range(5000, 10000); + } while (time_before(jiffies, timeout)); + + pm_runtime_put(dev); + + dev_warn(dev, "Timeout waiting for role-switch\n"); + return -ETIMEDOUT; +} + +static enum usb_role intel_xhci_usb_get_role(struct device *dev) +{ + struct intel_xhci_usb_data *data = dev_get_drvdata(dev); + enum usb_role role; + u32 val; + + pm_runtime_get_sync(dev); + val = readl(data->base + DUAL_ROLE_CFG0); + pm_runtime_put(dev); + + if (!(val & SW_IDPIN)) + role = USB_ROLE_HOST; + else if (val & SW_VBUS_VALID) + role = USB_ROLE_DEVICE; + else + role = USB_ROLE_NONE; + + return role; +} + +static const struct usb_role_switch_desc sw_desc = { + .set = intel_xhci_usb_set_role, + .get = intel_xhci_usb_get_role, + .allow_userspace_control = true, +}; + +static int intel_xhci_usb_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct intel_xhci_usb_data *data; + struct resource *res; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) + return -EINVAL; + data->base = devm_ioremap_nocache(dev, res->start, resource_size(res)); + if (!data->base) + return -ENOMEM; + + platform_set_drvdata(pdev, data); + + data->role_sw = usb_role_switch_register(dev, &sw_desc); + if (IS_ERR(data->role_sw)) + return PTR_ERR(data->role_sw); + + pm_runtime_set_active(dev); + pm_runtime_enable(dev); + + return 0; +} + +static int intel_xhci_usb_remove(struct platform_device *pdev) +{ + struct intel_xhci_usb_data *data = platform_get_drvdata(pdev); + + pm_runtime_disable(&pdev->dev); + + usb_role_switch_unregister(data->role_sw); + return 0; +} + +static const struct platform_device_id intel_xhci_usb_table[] = { + { .name = DRV_NAME }, + {} +}; +MODULE_DEVICE_TABLE(platform, intel_xhci_usb_table); + +static struct platform_driver intel_xhci_usb_driver = { + .driver = { + .name = DRV_NAME, + }, + .id_table = intel_xhci_usb_table, + .probe = intel_xhci_usb_probe, + .remove = intel_xhci_usb_remove, +}; + +module_platform_driver(intel_xhci_usb_driver); + +MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>"); +MODULE_DESCRIPTION("Intel XHCI USB role switch driver"); +MODULE_LICENSE("GPL"); |