429 lines
9.8 KiB
C

/*
* Copyright (c) 2016, Fuzhou Rockchip Electronics Co., Ltd
* Copyright (c) 2019 Radxa Limited
* Copyright (c) 2019 Amarula Solutions(India)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*/
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <linux/extcon-provider.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/regulator/consumer.h>
struct virtual_pd {
struct extcon_dev *extcon;
struct gpio_desc *gpio_irq;
struct regulator *dp_pwr;
struct device *dev;
bool flip;
bool usb_ss;
bool enable;
u8 mode;
int irq;
int enable_irq;
u8 plug_state;
struct workqueue_struct *virtual_pd_wq;
spinlock_t irq_lock;
struct delayed_work irq_work;
int shake_lev;
};
static const unsigned int vpd_cable[] = {
EXTCON_USB,
EXTCON_USB_HOST,
EXTCON_USB_VBUS_EN,
EXTCON_CHG_USB_SDP,
EXTCON_CHG_USB_CDP,
EXTCON_CHG_USB_DCP,
/*
FIXME: There's no real pd phy, control the charging is very
dangerous, just rely on the BC detection. We don't use slow
and fast.
*/
EXTCON_CHG_USB_SLOW,
EXTCON_CHG_USB_FAST,
EXTCON_DISP_DP,
EXTCON_NONE,
};
enum vpd_mode {
VPD_DFP = 0,
VPD_UFP,
VPD_DP,
VPD_DP_UFP,
};
static void vpd_set_vbus_enable(struct virtual_pd *vpd, bool enable)
{
extcon_set_state(vpd->extcon, EXTCON_USB_VBUS_EN, enable);
extcon_sync(vpd->extcon, EXTCON_USB_VBUS_EN);
}
static void vpd_extcon_notify(struct virtual_pd *vpd, bool flip, bool usb_ss,
bool dfp, bool ufp, bool dp)
{
union extcon_property_value property;
property.intval = flip;
extcon_set_property(vpd->extcon, EXTCON_USB,
EXTCON_PROP_USB_TYPEC_POLARITY, property);
extcon_set_property(vpd->extcon, EXTCON_USB_HOST,
EXTCON_PROP_USB_TYPEC_POLARITY, property);
extcon_set_property(vpd->extcon, EXTCON_DISP_DP,
EXTCON_PROP_USB_TYPEC_POLARITY, property);
property.intval = usb_ss;
extcon_set_property(vpd->extcon, EXTCON_USB,
EXTCON_PROP_USB_SS, property);
extcon_set_property(vpd->extcon, EXTCON_USB_HOST,
EXTCON_PROP_USB_SS, property);
extcon_set_property(vpd->extcon, EXTCON_DISP_DP,
EXTCON_PROP_USB_SS, property);
extcon_set_state(vpd->extcon, EXTCON_USB, ufp);
extcon_set_state(vpd->extcon, EXTCON_USB_HOST, dfp);
extcon_set_state(vpd->extcon, EXTCON_DISP_DP, dp);
extcon_sync(vpd->extcon, EXTCON_USB);
extcon_sync(vpd->extcon, EXTCON_USB_HOST);
extcon_sync(vpd->extcon, EXTCON_DISP_DP);
}
static void vpd_extcon_notify_set(struct virtual_pd *vpd)
{
bool flip = vpd->flip, usb_ss = vpd->usb_ss;
bool dfp = 0, ufp = 0, dp = 0;
switch (vpd->mode) {
case VPD_DFP:
dfp = 1;
break;
case VPD_DP:
dp = 1;
dfp = 1;
break;
case VPD_DP_UFP:
dp = 1;
ufp = 1;
break;
case VPD_UFP:
/* fall through */
default:
ufp = 1;
break;
}
vpd_set_vbus_enable(vpd, !ufp);
vpd_extcon_notify(vpd, flip, usb_ss, dfp, ufp, dp);
}
static void vpd_extcon_notify_clr(struct virtual_pd *vpd)
{
vpd_set_vbus_enable(vpd, 0);
vpd_extcon_notify(vpd, vpd->flip, vpd->usb_ss, 0, 0, 0);
}
void vpd_irq_disable(struct virtual_pd *vpd)
{
unsigned long irqflags = 0;
spin_lock_irqsave(&vpd->irq_lock, irqflags);
if (!vpd->enable_irq) {
disable_irq_nosync(vpd->irq);
vpd->enable_irq = 1;
} else {
dev_warn(vpd->dev, "irq have already disabled\n");
}
spin_unlock_irqrestore(&vpd->irq_lock, irqflags);
}
void vpd_irq_enable(struct virtual_pd *vpd)
{
unsigned long irqflags = 0;
spin_lock_irqsave(&vpd->irq_lock, irqflags);
if (vpd->enable_irq) {
enable_irq(vpd->irq);
vpd->enable_irq = 0;
}
spin_unlock_irqrestore(&vpd->irq_lock, irqflags);
}
static void extcon_pd_delay_irq_work(struct work_struct *work)
{
struct virtual_pd *vpd =
container_of(work, struct virtual_pd, irq_work.work);
int lev;
lev = gpiod_get_raw_value(vpd->gpio_irq);
if (vpd->shake_lev != lev) {
vpd_irq_enable(vpd);
return;
}
switch (vpd->plug_state) {
case 1:
if (lev == 0) {
vpd->enable = false;
vpd_extcon_notify_clr(vpd);
vpd->plug_state=0;
}
break;
case 0:
if (lev == 1) {
vpd->enable = true;
vpd_extcon_notify_set(vpd);
vpd->plug_state=1;
}
break;
default:
break;
}
vpd_irq_enable(vpd);
}
static irqreturn_t dp_det_irq_handler(int irq, void *dev_id)
{
struct virtual_pd *vpd = dev_id;
int lev;
lev = gpiod_get_raw_value(vpd->gpio_irq);
vpd->shake_lev = lev;
schedule_delayed_work(&vpd->irq_work, msecs_to_jiffies(10));
vpd_irq_disable(vpd);
return IRQ_HANDLED;
}
static void vpd_extcon_init(struct virtual_pd *vpd)
{
struct device *dev = vpd->dev;
u32 tmp = 0;
int ret = 0;
ret = device_property_read_u32(dev, "vpd,init-flip", &tmp);
if (ret < 0)
vpd->flip = 0;
else
vpd->flip = tmp;
dev_dbg(dev, "init-flip = %d\n", vpd->flip);
ret = device_property_read_u32(dev, "vpd,init-ss", &tmp);
if (ret < 0)
vpd->usb_ss = 0;
else
vpd->usb_ss = tmp;
dev_dbg(dev, "init-ss = %d\n", vpd->usb_ss);
ret = device_property_read_u32(dev, "vpd,init-mode", &tmp);
if (ret < 0)
vpd->mode = 0;
else
vpd->mode = tmp;
dev_dbg(dev, "init-mode = %d\n", vpd->mode);
if(gpiod_get_raw_value(vpd->gpio_irq)) {
vpd_extcon_notify_set(vpd);
vpd->plug_state=1;
}
}
static int vpd_extcon_probe(struct platform_device *pdev)
{
struct virtual_pd *vpd;
struct device *dev = &pdev->dev;
int ret = 0;
dev_info(dev, "probe start\n");
vpd = devm_kzalloc(dev, sizeof(*vpd), GFP_KERNEL);
if (!vpd)
return -ENOMEM;
vpd->dev = dev;
dev_set_drvdata(dev, vpd);
vpd->enable = 1;
vpd->extcon = devm_extcon_dev_allocate(dev, vpd_cable);
if (IS_ERR(vpd->extcon)) {
dev_err(dev, "allocat extcon failed\n");
return PTR_ERR(vpd->extcon);
}
ret = devm_extcon_dev_register(dev, vpd->extcon);
if (ret) {
dev_err(dev, "register extcon failed: %d\n", ret);
return ret;
}
vpd->gpio_irq = devm_gpiod_get_optional(dev,"hpd", GPIOD_IN);
if (IS_ERR(vpd->gpio_irq)) {
dev_warn(dev, "maybe miss named GPIO for hpd\n");
vpd->gpio_irq = NULL;
}
vpd->dp_pwr = devm_regulator_get_optional(dev, "dp-pwr");
if (IS_ERR(vpd->dp_pwr)) {
dev_warn(dev, "failed to get dp-pwr\n");
vpd->dp_pwr = NULL;
}
ret = regulator_enable(vpd->dp_pwr);
if (ret)
dev_warn(dev, "failed to enable dp-pwr\n");
ret = extcon_set_property_capability(vpd->extcon, EXTCON_USB,
EXTCON_PROP_USB_TYPEC_POLARITY);
if (ret) {
dev_err(dev,
"set USB property capability failed: %d\n", ret);
return ret;
}
ret = extcon_set_property_capability(vpd->extcon, EXTCON_USB_HOST,
EXTCON_PROP_USB_TYPEC_POLARITY);
if (ret) {
dev_err(dev,
"set USB_HOST property capability failed: %d\n",
ret);
return ret;
}
ret = extcon_set_property_capability(vpd->extcon, EXTCON_DISP_DP,
EXTCON_PROP_USB_TYPEC_POLARITY);
if (ret) {
dev_err(dev,
"set DISP_DP property capability failed: %d\n",
ret);
return ret;
}
ret = extcon_set_property_capability(vpd->extcon, EXTCON_USB,
EXTCON_PROP_USB_SS);
if (ret) {
dev_err(dev,
"set USB USB_SS property capability failed: %d\n",
ret);
return ret;
}
ret = extcon_set_property_capability(vpd->extcon, EXTCON_USB_HOST,
EXTCON_PROP_USB_SS);
if (ret) {
dev_err(dev,
"set USB_HOST USB_SS property capability failed: %d\n",
ret);
return ret;
}
ret = extcon_set_property_capability(vpd->extcon, EXTCON_DISP_DP,
EXTCON_PROP_USB_SS);
if (ret) {
dev_err(dev,
"set DISP_DP USB_SS property capability failed: %d\n",
ret);
return ret;
}
ret = extcon_set_property_capability(vpd->extcon, EXTCON_CHG_USB_FAST,
EXTCON_PROP_USB_TYPEC_POLARITY);
if (ret) {
dev_err(dev,
"set USB_PD property capability failed: %d\n", ret);
return ret;
}
vpd_extcon_init(vpd);
INIT_DELAYED_WORK(&vpd->irq_work, extcon_pd_delay_irq_work);
vpd->irq=gpiod_to_irq(vpd->gpio_irq);
if (vpd->irq){
ret = devm_request_threaded_irq(dev,
vpd->irq,
NULL,
dp_det_irq_handler,
IRQF_TRIGGER_FALLING |IRQF_TRIGGER_RISING | IRQF_ONESHOT ,
NULL,
vpd);
}
else
dev_err(dev,"gpio can not be irq !\n");
vpd->virtual_pd_wq = create_workqueue("virtual_pd_wq");
dev_info(dev, "probe success\n");
return 0;
}
static int vpd_extcon_remove(struct platform_device *pdev)
{
struct virtual_pd *vpd = platform_get_drvdata(pdev);
regulator_disable(vpd->dp_pwr);
return 0;
}
#ifdef CONFIG_PM_SLEEP
static int vpd_extcon_suspend(struct device *dev)
{
struct virtual_pd *vpd = dev_get_drvdata(dev);
int lev=0;
lev = gpiod_get_raw_value(vpd->gpio_irq);
cancel_delayed_work_sync(&vpd->irq_work);
vpd_irq_disable(vpd);
return 0;
}
static int vpd_extcon_resume(struct device *dev)
{
struct virtual_pd *vpd = dev_get_drvdata(dev);
vpd_irq_enable(vpd);
return 0;
}
#endif
static SIMPLE_DEV_PM_OPS(vpd_extcon_pm_ops,
vpd_extcon_suspend, vpd_extcon_resume);
static const struct of_device_id vpd_extcon_dt_match[] = {
{ .compatible = "linux,extcon-pd-virtual", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, vpd_extcon_dt_match);
static struct platform_driver vpd_extcon_driver = {
.probe = vpd_extcon_probe,
.remove = vpd_extcon_remove,
.driver = {
.name = "extcon-pd-virtual",
.pm = &vpd_extcon_pm_ops,
.of_match_table = vpd_extcon_dt_match,
},
};
static int __init __vpd_extcon_init(void)
{
return platform_driver_register(&vpd_extcon_driver);
}
static void __exit __vpd_extcon_exit(void)
{
platform_driver_unregister(&vpd_extcon_driver);
}
module_init(__vpd_extcon_init);
module_exit(__vpd_extcon_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("rockchip");
MODULE_DESCRIPTION("Virtual Typec-pd extcon driver");