diff options
Diffstat (limited to '')
-rw-r--r-- | drivers/usb/roles/intel-xhci-usb-role-switch.c | 229 |
1 files changed, 229 insertions, 0 deletions
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..5c96e929a --- /dev/null +++ b/drivers/usb/roles/intel-xhci-usb-role-switch.c @@ -0,0 +1,229 @@ +// 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/property.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 SW_SWITCH_EN BIT(16) + +#define DRD_CONFIG_DYNAMIC 0 +#define DRD_CONFIG_STATIC_HOST 1 +#define DRD_CONFIG_STATIC_DEVICE 2 +#define DRD_CONFIG_MASK 3 + +#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 device *dev; + struct usb_role_switch *role_sw; + void __iomem *base; + bool enable_sw_switch; +}; + +static const struct software_node intel_xhci_usb_node = { + "intel-xhci-usb-sw", +}; + +static int intel_xhci_usb_set_role(struct usb_role_switch *sw, + enum usb_role role) +{ + struct intel_xhci_usb_data *data = usb_role_switch_get_drvdata(sw); + unsigned long timeout; + acpi_status status; + u32 glk, val; + u32 drd_config = DRD_CONFIG_DYNAMIC; + + /* + * 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(data->dev, "Error could not acquire lock\n"); + return -EIO; + } + + pm_runtime_get_sync(data->dev); + + /* + * Set idpin value as requested. + * Since some devices rely on firmware setting DRD_CONFIG and + * SW_SWITCH_EN bits to be zero for role switch, + * do not set these bits for those devices. + */ + val = readl(data->base + DUAL_ROLE_CFG0); + switch (role) { + case USB_ROLE_NONE: + val |= SW_IDPIN; + val &= ~SW_VBUS_VALID; + drd_config = DRD_CONFIG_DYNAMIC; + break; + case USB_ROLE_HOST: + val &= ~SW_IDPIN; + val &= ~SW_VBUS_VALID; + drd_config = DRD_CONFIG_STATIC_HOST; + break; + case USB_ROLE_DEVICE: + val |= SW_IDPIN; + val |= SW_VBUS_VALID; + drd_config = DRD_CONFIG_STATIC_DEVICE; + break; + } + val |= SW_IDPIN_EN; + if (data->enable_sw_switch) { + val &= ~DRD_CONFIG_MASK; + val |= SW_SWITCH_EN | drd_config; + } + 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(data->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(data->dev); + + dev_warn(data->dev, "Timeout waiting for role-switch\n"); + return -ETIMEDOUT; +} + +static enum usb_role intel_xhci_usb_get_role(struct usb_role_switch *sw) +{ + struct intel_xhci_usb_data *data = usb_role_switch_get_drvdata(sw); + enum usb_role role; + u32 val; + + pm_runtime_get_sync(data->dev); + val = readl(data->base + DUAL_ROLE_CFG0); + pm_runtime_put(data->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 int intel_xhci_usb_probe(struct platform_device *pdev) +{ + struct usb_role_switch_desc sw_desc = { }; + struct device *dev = &pdev->dev; + struct intel_xhci_usb_data *data; + struct resource *res; + int ret; + + 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(dev, res->start, resource_size(res)); + if (!data->base) + return -ENOMEM; + + platform_set_drvdata(pdev, data); + + ret = software_node_register(&intel_xhci_usb_node); + if (ret) + return ret; + + sw_desc.set = intel_xhci_usb_set_role, + sw_desc.get = intel_xhci_usb_get_role, + sw_desc.allow_userspace_control = true, + sw_desc.fwnode = software_node_fwnode(&intel_xhci_usb_node); + sw_desc.driver_data = data; + + data->dev = dev; + data->enable_sw_switch = !device_property_read_bool(dev, + "sw_switch_disable"); + + data->role_sw = usb_role_switch_register(dev, &sw_desc); + if (IS_ERR(data->role_sw)) { + fwnode_handle_put(sw_desc.fwnode); + 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); + fwnode_handle_put(software_node_fwnode(&intel_xhci_usb_node)); + + 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"); |