| // 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 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 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; |
| 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(dev, "Error could not acquire lock\n"); |
| return -EIO; |
| } |
| |
| pm_runtime_get_sync(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(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 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); |
| |
| 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"); |