extcon: add extcon-k1xci driver for usb2.0 otg
Change-Id: I747ece41e555ff3bb0c189cdeb0e3dda0eb7adc9
This commit is contained in:
@@ -41,6 +41,13 @@ config EXTCON_FSA9480
|
||||
I2C and enables USB data, stereo and mono audio, video, microphone
|
||||
and UART data to use a common connector port.
|
||||
|
||||
config EXTCON_USB_K1XCI
|
||||
tristate "Spacemit K1-x USB extcon support"
|
||||
depends on GPIOLIB || COMPILE_TEST
|
||||
help
|
||||
say Y here to enable spacemit k1-x usb wakeup irq based USB cable detection extcon support.
|
||||
Used typically if wakeup irq is used for USB ID pin detection.
|
||||
|
||||
config EXTCON_GPIO
|
||||
tristate "GPIO extcon support"
|
||||
depends on GPIOLIB || COMPILE_TEST
|
||||
|
||||
@@ -22,6 +22,7 @@ obj-$(CONFIG_EXTCON_PTN5150) += extcon-ptn5150.o
|
||||
obj-$(CONFIG_EXTCON_QCOM_SPMI_MISC) += extcon-qcom-spmi-misc.o
|
||||
obj-$(CONFIG_EXTCON_RT8973A) += extcon-rt8973a.o
|
||||
obj-$(CONFIG_EXTCON_SM5502) += extcon-sm5502.o
|
||||
obj-$(CONFIG_EXTCON_USB_K1XCI) += extcon-k1xci.o
|
||||
obj-$(CONFIG_EXTCON_USB_GPIO) += extcon-usb-gpio.o
|
||||
obj-$(CONFIG_EXTCON_USBC_CROS_EC) += extcon-usbc-cros-ec.o
|
||||
obj-$(CONFIG_EXTCON_USBC_TUSB320) += extcon-usbc-tusb320.o
|
||||
|
||||
358
drivers/extcon/extcon-k1xci.c
Normal file
358
drivers/extcon/extcon-k1xci.c
Normal file
@@ -0,0 +1,358 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* extcon-k1xci.c - Driver for usb vbus/id detect
|
||||
*
|
||||
* Copyright (c) 2023, Spacemit Corporation.
|
||||
*/
|
||||
|
||||
#include <linux/extcon.h>
|
||||
#include <linux/extcon-provider.h>
|
||||
#include <linux/init.h>
|
||||
#include <linux/interrupt.h>
|
||||
#include <linux/irq.h>
|
||||
#include <linux/kernel.h>
|
||||
#include <linux/module.h>
|
||||
#include <linux/platform_device.h>
|
||||
#include <linux/slab.h>
|
||||
#include <linux/workqueue.h>
|
||||
#include <linux/of.h>
|
||||
#include <linux/pm_qos.h>
|
||||
#include <linux/debugfs.h>
|
||||
#include <linux/uaccess.h>
|
||||
#include <linux/pm_runtime.h>
|
||||
|
||||
#define USB_GPIO_DEBOUNCE_MS 200 /* ms */
|
||||
|
||||
/* PMU_SD_ROT_WAKE_CLR */
|
||||
#define USB_VBUS_WK_MASK BIT(10)
|
||||
#define USB_ID_WK_MASK BIT(11)
|
||||
|
||||
#define USB_VBUS_WK_CLR BIT(18)
|
||||
#define USB_ID_WK_CLR BIT(19)
|
||||
|
||||
#define USB_VBUS_WK_STATUS BIT(26)
|
||||
#define USB_ID_WK_STATUS BIT(27)
|
||||
|
||||
/* PMUA_USB_PHY_READ */
|
||||
#define USB_ID BIT(1)
|
||||
#define USB_VBUS BIT(2)
|
||||
|
||||
struct mv_usb_extcon_info {
|
||||
struct device *dev;
|
||||
struct extcon_dev *edev;
|
||||
|
||||
void __iomem *pmuap_reg;
|
||||
void __iomem *pin_state_reg;
|
||||
|
||||
int irq;
|
||||
|
||||
unsigned long debounce_jiffies;
|
||||
struct delayed_work wq_detcable;
|
||||
|
||||
struct freq_qos_request qos_idle;
|
||||
u32 lpm_qos;
|
||||
|
||||
/* debugfs interface for user-space */
|
||||
struct dentry *dbgfs;
|
||||
char dbgfs_name[32];
|
||||
uint32_t dbgfs_qos_mode;
|
||||
};
|
||||
|
||||
static const unsigned int usb_extcon_cable[] = {
|
||||
EXTCON_USB,
|
||||
EXTCON_USB_HOST,
|
||||
EXTCON_NONE,
|
||||
};
|
||||
|
||||
static void mv_enable_wakeup_irqs(struct mv_usb_extcon_info *info)
|
||||
{
|
||||
u32 reg;
|
||||
reg = readl(info->pmuap_reg);
|
||||
reg |= (USB_VBUS_WK_MASK | USB_ID_WK_MASK);
|
||||
writel(reg, info->pmuap_reg);
|
||||
}
|
||||
|
||||
static void mv_disable_wakeup_irqs(struct mv_usb_extcon_info *info)
|
||||
{
|
||||
u32 reg;
|
||||
reg = readl(info->pmuap_reg);
|
||||
reg &= ~(USB_VBUS_WK_MASK | USB_ID_WK_MASK);
|
||||
writel(reg, info->pmuap_reg);
|
||||
}
|
||||
|
||||
/*
|
||||
* "USB" = VBUS and "USB-HOST" = !ID, so we have:
|
||||
* Both "USB" and "USB-HOST" can't be set as active at the
|
||||
* same time so if "USB-HOST" is active (i.e. ID is 0) we keep "USB" inactive
|
||||
* even if VBUS is on.
|
||||
*
|
||||
* State | ID | VBUS
|
||||
* ----------------------------------------
|
||||
* [1] USB | H | H
|
||||
* [2] none | H | L
|
||||
* [3] USB-HOST | L | H
|
||||
* [4] USB-HOST | L | L
|
||||
*
|
||||
* In case we have only one of these signals:
|
||||
* - VBUS only - we want to distinguish between [1] and [2], so ID is always 1.
|
||||
* - ID only - we want to distinguish between [1] and [4], so VBUS = ID.
|
||||
*/
|
||||
|
||||
static void usb_detect_cable(struct work_struct *work)
|
||||
{
|
||||
int id, vbus;
|
||||
int id_state, vbus_state;
|
||||
u32 reg;
|
||||
u32 state;
|
||||
|
||||
struct mv_usb_extcon_info *info = container_of(
|
||||
to_delayed_work(work), struct mv_usb_extcon_info, wq_detcable);
|
||||
|
||||
reg = readl(info->pmuap_reg);
|
||||
id = reg & USB_ID_WK_STATUS;
|
||||
vbus = reg & USB_VBUS_WK_STATUS;
|
||||
|
||||
pr_info("info->pmuap_reg: 0x%x id: %d vbus: %d \n", reg, id, vbus);
|
||||
if (id || vbus) {
|
||||
state = readl(info->pin_state_reg);
|
||||
id_state = state & USB_ID;
|
||||
vbus_state = state & USB_VBUS;
|
||||
|
||||
if (!id_state) {
|
||||
dev_info(info->dev, "USB we as host connected\n");
|
||||
extcon_set_state_sync(info->edev, EXTCON_USB_HOST,
|
||||
true);
|
||||
} else {
|
||||
dev_info(info->dev, "USB we as host disconnected\n");
|
||||
extcon_set_state_sync(info->edev, EXTCON_USB_HOST,
|
||||
false);
|
||||
|
||||
if (!vbus_state) {
|
||||
dev_info(info->dev,
|
||||
"USB we as peripheral disconnected\n");
|
||||
extcon_set_state_sync(info->edev, EXTCON_USB,
|
||||
false);
|
||||
} else {
|
||||
dev_dbg(info->dev, "dbgfs_qos_mode = %d \n",
|
||||
info->dbgfs_qos_mode);
|
||||
dev_info(info->dev,
|
||||
"USB we as peripheral connected\n");
|
||||
extcon_set_state_sync(info->edev, EXTCON_USB,
|
||||
true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reg |= (USB_VBUS_WK_CLR | USB_ID_WK_CLR);
|
||||
writel(reg, info->pmuap_reg);
|
||||
mv_enable_wakeup_irqs(info);
|
||||
}
|
||||
|
||||
static irqreturn_t mv_wakeup_interrupt(int irq, void *_info)
|
||||
{
|
||||
struct mv_usb_extcon_info *info = (struct mv_usb_extcon_info *)_info;
|
||||
|
||||
pr_info("extcon_mvci: mv_wakeup_interrupt... \n");
|
||||
mv_disable_wakeup_irqs(info);
|
||||
|
||||
queue_delayed_work(system_power_efficient_wq, &info->wq_detcable,
|
||||
info->debounce_jiffies);
|
||||
|
||||
return IRQ_HANDLED;
|
||||
}
|
||||
|
||||
static ssize_t extcon_mvci_dbgfs_read(struct file *filp, char __user *user_buf,
|
||||
size_t size, loff_t *ppos)
|
||||
{
|
||||
struct mv_usb_extcon_info *info = filp->private_data;
|
||||
char buf[64];
|
||||
int ret, n, copy;
|
||||
|
||||
n = min(sizeof(buf) - 1, size);
|
||||
|
||||
if (info->dbgfs_qos_mode == 1)
|
||||
copy = sprintf(buf, "enable mvci qos_hold\n");
|
||||
else
|
||||
copy = sprintf(buf, "disable mvci qos_hold\n");
|
||||
|
||||
copy = min(n, copy);
|
||||
ret = simple_read_from_buffer(user_buf, size, ppos, buf, copy);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static ssize_t extcon_mvci_dbgfs_write(struct file *filp,
|
||||
const char __user *user_buf, size_t size,
|
||||
loff_t *ppos)
|
||||
{
|
||||
struct mv_usb_extcon_info *info = filp->private_data;
|
||||
char buf[32];
|
||||
int buf_size, i = 0;
|
||||
|
||||
buf_size = min(size, sizeof(buf) - 1);
|
||||
if (copy_from_user(buf, user_buf, buf_size))
|
||||
return -EFAULT;
|
||||
|
||||
*(buf + buf_size) = '\0';
|
||||
while (*(buf + i) != '\n' && *(buf + i) != '\0')
|
||||
i++;
|
||||
*(buf + i) = '\0';
|
||||
|
||||
i = 0;
|
||||
while (*(buf + i) == ' ')
|
||||
i++;
|
||||
|
||||
if (!strncmp(buf + i, "enable", 6)) {
|
||||
info->dbgfs_qos_mode = 1;
|
||||
} else if (!strncmp(buf + i, "disable", 7)) {
|
||||
info->dbgfs_qos_mode = 0;
|
||||
dev_info(info->dev, "mvci qos release\n");
|
||||
} else {
|
||||
dev_err(info->dev, "only accept: enable, disable\n");
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
static const struct file_operations extcon_mvci_dbgfs_ops = {
|
||||
.owner = THIS_MODULE,
|
||||
.open = simple_open,
|
||||
.read = extcon_mvci_dbgfs_read,
|
||||
.write = extcon_mvci_dbgfs_write,
|
||||
};
|
||||
|
||||
static int mv_usb_extcon_probe(struct platform_device *pdev)
|
||||
{
|
||||
struct device *dev = &pdev->dev;
|
||||
struct device_node *np = dev->of_node;
|
||||
struct resource *res;
|
||||
struct mv_usb_extcon_info *info;
|
||||
int ret;
|
||||
u32 property;
|
||||
#ifdef CONFIG_PM
|
||||
//struct freq_constraints *idle_qos;
|
||||
#endif
|
||||
|
||||
if (!np)
|
||||
return -EINVAL;
|
||||
|
||||
dev_info(dev, "mv_usb_extcon_probe\n");
|
||||
info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL);
|
||||
if (!info)
|
||||
return -ENOMEM;
|
||||
|
||||
info->dev = dev;
|
||||
info->irq = platform_get_irq(pdev, 0);
|
||||
if (info->irq < 0) {
|
||||
dev_err(dev, "missing IRQ resource\n");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "reg_pmuap");
|
||||
if (!res) {
|
||||
dev_err(dev, "missing memory base resource\n");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
info->pmuap_reg =
|
||||
devm_ioremap(&pdev->dev, res->start, resource_size(res));
|
||||
if (!info->pmuap_reg) {
|
||||
dev_err(dev, "ioremap failed\n");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "pin_state");
|
||||
if (!res) {
|
||||
dev_err(dev, "missing memory base resource\n");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
info->pin_state_reg =
|
||||
devm_ioremap(&pdev->dev, res->start, resource_size(res));
|
||||
if (!info->pin_state_reg) {
|
||||
dev_err(dev, "ioremap failed\n");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
info->edev = devm_extcon_dev_allocate(dev, usb_extcon_cable);
|
||||
if (IS_ERR(info->edev)) {
|
||||
dev_err(dev, "failed to allocate extcon device\n");
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
ret = devm_extcon_dev_register(dev, info->edev);
|
||||
if (ret < 0) {
|
||||
dev_err(dev, "failed to register extcon device\n");
|
||||
return ret;
|
||||
}
|
||||
|
||||
info->dbgfs_qos_mode = 1;
|
||||
|
||||
ret = devm_request_irq(dev, info->irq, mv_wakeup_interrupt,
|
||||
IRQF_NO_SUSPEND, "mv-wakeup", info);
|
||||
if (ret) {
|
||||
dev_err(dev, "failed to request IRQ #%d --> %d\n", info->irq,
|
||||
ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (!of_property_read_s32(pdev->dev.of_node, "lpm-qos", &property))
|
||||
info->lpm_qos = property;
|
||||
else
|
||||
info->lpm_qos = 15;
|
||||
|
||||
platform_set_drvdata(pdev, info);
|
||||
device_init_wakeup(dev, 1);
|
||||
|
||||
info->debounce_jiffies = msecs_to_jiffies(USB_GPIO_DEBOUNCE_MS);
|
||||
|
||||
INIT_DELAYED_WORK(&info->wq_detcable, usb_detect_cable);
|
||||
|
||||
mv_enable_wakeup_irqs(info);
|
||||
|
||||
/* Perform initial detection */
|
||||
usb_detect_cable(&info->wq_detcable.work);
|
||||
|
||||
info->dbgfs = debugfs_create_file("mvci_extcon_qos", 0644, NULL, info,
|
||||
&extcon_mvci_dbgfs_ops);
|
||||
if (!info->dbgfs) {
|
||||
dev_err(info->dev, "failed to create debugfs\n");
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mv_usb_extcon_remove(struct platform_device *pdev)
|
||||
{
|
||||
struct mv_usb_extcon_info *info = platform_get_drvdata(pdev);
|
||||
|
||||
cancel_delayed_work_sync(&info->wq_detcable);
|
||||
device_init_wakeup(&pdev->dev, 0);
|
||||
|
||||
freq_qos_remove_request(&info->qos_idle);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct of_device_id mv_usb_extcon_dt_match[] = {
|
||||
{
|
||||
.compatible = "spacemit,vbus-id",
|
||||
},
|
||||
{}
|
||||
};
|
||||
|
||||
MODULE_DEVICE_TABLE(of, mv_usb_extcon_dt_match);
|
||||
|
||||
static struct platform_driver usb_extcon_driver = {
|
||||
.probe = mv_usb_extcon_probe,
|
||||
.remove = mv_usb_extcon_remove,
|
||||
.driver = {
|
||||
.name = "extcon-k1xci-usb",
|
||||
.of_match_table = mv_usb_extcon_dt_match,
|
||||
},
|
||||
};
|
||||
|
||||
module_platform_driver(usb_extcon_driver);
|
||||
|
||||
MODULE_LICENSE("GPL v2");
|
||||
Reference in New Issue
Block a user