extcon: add extcon-k1xci driver for usb2.0 otg

Change-Id: I747ece41e555ff3bb0c189cdeb0e3dda0eb7adc9
This commit is contained in:
Pan Junzhong
2024-01-04 19:19:32 +08:00
committed by zhangmeng
parent d87cc622ac
commit ffb94c5a3d
3 changed files with 366 additions and 0 deletions

View File

@@ -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

View File

@@ -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

View 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");