190 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			190 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
// 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");
 |