blob: 675a28b1d5b1cf3e09f2b7e86570c66bef17555a [file] [log] [blame]
/*
* extcon-tsu6111.c - TSU6111 extcon driver
*
* Copyright (C) 2013 Intel Corporation
* Ramakrishna Pallala <ramakrishna.pallala@intel.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/err.h>
#include <linux/device.h>
#include <linux/workqueue.h>
#include <linux/usb/otg.h>
#include <linux/notifier.h>
#include <linux/extcon.h>
#include <linux/pm_runtime.h>
#include <linux/acpi.h>
#include <linux/power_supply.h>
#include <linux/wakelock.h>
#include <linux/delay.h>
#include <linux/extcon/extcon-tsu6111.h>
#define TSU_REG_CONTROL 0x02
#define TSU_REG_ADC 0x07
#define TSU_REG_DEVICETYPE1 0x0A
#define TSU_REG_MANUALSW1 0x13
#define STAT_CHRG_TYPE_MASK 0xFC
#define TSU_CHARGE_CUR_DCP 2000
#define TSU_CHARGE_CUR_CDP 1500
#define TSU_CHARGE_CUR_SDP_100 100
#define TSU_CHARGE_CUR_SDP_500 500
#define TSU6111_EXTCON_USB "USB"
#define TSU6111_EXTCON_SDP "CHARGER_USB_SDP"
#define TSU6111_EXTCON_DCP "CHARGER_USB_DCP"
#define TSU6111_EXTCON_CDP "CHARGER_USB_CDP"
#define TSU6111_DET_SDP 0x04
#define TSU6111_DET_CDP 0x20
#define TSU6111_DET_DCP 0x40
#define TSU6111_DET_OTG 0x80
static const char *tsu6111_extcon_cable[] = {
TSU6111_EXTCON_SDP,
TSU6111_EXTCON_DCP,
TSU6111_EXTCON_CDP,
NULL,
};
struct tsu6111_chip {
struct i2c_client *client;
struct tsu6111_pdata *pdata;
struct usb_phy *otg;
struct work_struct otg_work;
struct work_struct vbus_work;
struct notifier_block host_nb;
struct notifier_block id_nb;
struct notifier_block vbus_nb;
bool id_short;
bool is_sdp;
bool det_started;
spinlock_t tsu_lock;
struct extcon_dev *edev;
struct wake_lock wakelock;
struct extcon_specific_cable_nb cable_obj;
struct extcon_specific_cable_nb host_cable_obj;
};
static int tsu6111_write_reg(struct i2c_client *client,
int reg, int value)
{
int ret;
ret = i2c_smbus_write_byte_data(client, reg, value);
if (ret < 0)
dev_err(&client->dev, "%s: err %d\n", __func__, ret);
return ret;
}
static int tsu6111_read_reg(struct i2c_client *client, int reg)
{
int ret;
ret = i2c_smbus_read_byte_data(client, reg);
if (ret < 0)
dev_err(&client->dev, "%s: err %d\n", __func__, ret);
return ret;
}
static int tsu6111_detect_dev(struct tsu6111_chip *chip)
{
struct i2c_client *client = chip->client;
static bool notify_otg, notify_charger;
static char *cable;
static struct power_supply_cable_props cable_props;
int cfg, ret, vbus_mask = 0;
u8 chrg_type;
bool vbus_attach = false;
dev_info(&chip->client->dev, "%s\n", __func__);
/*
* get VBUS status from external IC like
* PMIC or Charger as TSU6111 chip can not
* be accessed with out VBUS.
*/
ret = chip->pdata->is_vbus_online();
if (ret < 0) {
dev_info(&chip->client->dev, "get vbus stat error\n");
return ret;
}
if (ret) {
dev_info(&chip->client->dev, "VBUS present\n");
vbus_attach = true;
} else {
dev_info(&chip->client->dev, "VBUS NOT present\n");
vbus_attach = false;
cable_props.ma = 0;
cable_props.chrg_evt = POWER_SUPPLY_CHARGER_EVENT_DISCONNECT;
tsu6111_write_reg(client, TSU_REG_MANUALSW1, 0x00);
goto notify_otg_em;
}
/* dont proceed with charger detection in host mode */
if (chip->id_short) {
/*
* only after reading the status register
* MUX path is being closed. And by default
* MUX is to connected Host mode path.
*/
dev_info(&chip->client->dev, "id shorted!!\n");
return ret;
}
mdelay(100);
ret = tsu6111_read_reg(client, TSU_REG_DEVICETYPE1);
if (ret < 0)
goto dev_det_i2c_failed;
else
cfg = ret;
dev_info(&client->dev, "Cfg:%x menual_sw:%x\n", cfg,
tsu6111_read_reg(client, TSU_REG_MANUALSW1));
chrg_type = cfg;
chip->is_sdp = false;
if (chrg_type == TSU6111_DET_SDP) {
dev_info(&chip->client->dev,
"SDP cable connecetd\n");
notify_otg = true;
vbus_mask = 1;
notify_charger = true;
chip->is_sdp = true;
cable = TSU6111_EXTCON_SDP;
cable_props.chrg_evt = POWER_SUPPLY_CHARGER_EVENT_CONNECT;
cable_props.chrg_type = POWER_SUPPLY_CHARGER_TYPE_USB_SDP;
if (chip->pdata->charging_compliance_override)
cable_props.ma = TSU_CHARGE_CUR_SDP_500;
else
cable_props.ma = TSU_CHARGE_CUR_SDP_100;
} else if (chrg_type == TSU6111_DET_CDP) {
dev_info(&chip->client->dev,
"CDP cable connecetd\n");
notify_otg = true;
vbus_mask = 1;
notify_charger = true;
cable = TSU6111_EXTCON_CDP;
cable_props.chrg_evt = POWER_SUPPLY_CHARGER_EVENT_CONNECT;
cable_props.chrg_type = POWER_SUPPLY_CHARGER_TYPE_USB_CDP;
cable_props.ma = TSU_CHARGE_CUR_CDP;
} else if (chrg_type == TSU6111_DET_DCP) {
dev_info(&chip->client->dev,
"DCP/SE1 cable connecetd\n");
notify_charger = true;
cable = TSU6111_EXTCON_DCP;
cable_props.chrg_evt = POWER_SUPPLY_CHARGER_EVENT_CONNECT;
cable_props.chrg_type = POWER_SUPPLY_CHARGER_TYPE_USB_DCP;
cable_props.ma = TSU_CHARGE_CUR_DCP;
if (!wake_lock_active(&chip->wakelock))
wake_lock(&chip->wakelock);
} else {
dev_warn(&chip->client->dev,
"unknown type: %x\n", chrg_type);
cable_props.ma = 0;
cable_props.chrg_evt = POWER_SUPPLY_CHARGER_EVENT_DISCONNECT;
}
notify_otg_em:
if (!vbus_attach) { /* disconnevt event */
if (notify_otg) {
atomic_notifier_call_chain(&chip->otg->notifier,
vbus_mask ? USB_EVENT_VBUS : USB_EVENT_NONE,
NULL);
notify_otg = false;
}
if (notify_charger) {
/*
* not supporting extcon events currently.
* extcon_set_cable_state(chip->edev, cable, false);
*/
atomic_notifier_call_chain(&power_supply_notifier,
PSY_CABLE_EVENT, &cable_props);
notify_charger = false;
cable = NULL;
}
if (wake_lock_active(&chip->wakelock))
wake_unlock(&chip->wakelock);
} else {
if (notify_otg) {
/* close mux path to enable device mode */
tsu6111_write_reg(client, TSU_REG_MANUALSW1, 0x6c);
atomic_notifier_call_chain(&chip->otg->notifier,
vbus_mask ? USB_EVENT_VBUS : USB_EVENT_NONE,
NULL);
}
if (notify_charger) {
/*
* not supporting extcon events currently.
* extcon_set_cable_state(chip->edev, cable, true);
*/
atomic_notifier_call_chain(&power_supply_notifier,
PSY_CABLE_EVENT, &cable_props);
}
}
return 0;
dev_det_i2c_failed:
if (chip->pdata->is_vbus_online())
dev_err(&chip->client->dev,
"vbus present: i2c read failed:%d\n", ret);
else
dev_info(&chip->client->dev,
"vbus removed: i2c read failed:%d\n", ret);
return ret;
}
static irqreturn_t tsu6111_irq_handler(int irq, void *data)
{
unsigned long flags;
struct tsu6111_chip *chip = data;
pm_runtime_get_sync(&chip->client->dev);
dev_info(&chip->client->dev, "TSU USB INT!\n");
spin_lock_irqsave(&chip->tsu_lock, flags);
if (!chip->det_started) {
chip->det_started = 1;
spin_unlock_irqrestore(&chip->tsu_lock, flags);
tsu6111_detect_dev(chip);
chip->det_started = 0;
} else
spin_unlock_irqrestore(&chip->tsu_lock, flags);
pm_runtime_put_sync(&chip->client->dev);
return IRQ_HANDLED;
}
static void tsu6111_otg_event_worker(struct work_struct *work)
{
struct tsu6111_chip *chip =
container_of(work, struct tsu6111_chip, otg_work);
int ret;
pm_runtime_get_sync(&chip->client->dev);
if (chip->id_short) {
ret = chip->pdata->enable_vbus();
msleep(50);
/* enable mux2 path for host */
tsu6111_write_reg(chip->client, TSU_REG_MANUALSW1, 0x24);
} else {
ret = chip->pdata->disable_vbus();
tsu6111_write_reg(chip->client, TSU_REG_MANUALSW1, 0x0);
}
if (ret < 0)
dev_warn(&chip->client->dev, "id vbus control failed\n");
pm_runtime_put_sync(&chip->client->dev);
}
static int tsu6111_handle_host_notification(struct notifier_block *nb,
unsigned long event, void *param)
{
struct tsu6111_chip *chip =
container_of(nb, struct tsu6111_chip, host_nb);
struct extcon_dev *edev = param;
int usb_host = !!edev->state;
if (usb_host) {
dev_info(&chip->client->dev,
"USB-Host notification: host\n");
chip->id_short = true;
} else {
dev_info(&chip->client->dev,
"USB-Host notification: peripheral\n");
chip->id_short = false;
}
schedule_work(&chip->otg_work);
return NOTIFY_OK;
}
static int tsu6111_handle_otg_notification(struct notifier_block *nb,
unsigned long event, void *param)
{
struct tsu6111_chip *chip =
container_of(nb, struct tsu6111_chip, id_nb);
struct power_supply_cable_props cable_props;
int *val = (int *)param;
if ((event != USB_EVENT_ID) &&
(event != USB_EVENT_NONE) &&
(event != USB_EVENT_ENUMERATED))
return NOTIFY_DONE;
if ((event == USB_EVENT_ENUMERATED) && !param)
return NOTIFY_DONE;
dev_info(&chip->client->dev,
"[OTG notification]evt:%lu val:%d\n", event,
val ? *val : -1);
switch (event) {
case USB_EVENT_ID:
case USB_EVENT_NONE:
schedule_work(&chip->otg_work);
break;
case USB_EVENT_ENUMERATED:
/*
* ignore cable plug/unplug events as TSU
* had already send those event notifications.
* Also only handle notifications for SDP case.
*/
/* No need to change SDP inlimit based on enumeration status
* if platform can voilate charging_compliance.
*/
if (chip->pdata->charging_compliance_override ||
!chip->is_sdp ||
(*val == TSU_CHARGE_CUR_SDP_100))
break;
/*
* if current limit is < 100mA
* treat it as suspend event.
*/
if (*val < TSU_CHARGE_CUR_SDP_100)
cable_props.chrg_evt =
POWER_SUPPLY_CHARGER_EVENT_SUSPEND;
else
cable_props.chrg_evt =
POWER_SUPPLY_CHARGER_EVENT_CONNECT;
cable_props.chrg_type = POWER_SUPPLY_CHARGER_TYPE_USB_SDP;
cable_props.ma = *val;
atomic_notifier_call_chain(&power_supply_notifier,
PSY_CABLE_EVENT, &cable_props);
break;
default:
dev_warn(&chip->client->dev, "invalid OTG event\n");
}
return NOTIFY_OK;
}
static void tsu6111_pwrsrc_event_worker(struct work_struct *work)
{
unsigned long flags;
int ret;
struct tsu6111_chip *chip =
container_of(work, struct tsu6111_chip, vbus_work);
pm_runtime_get_sync(&chip->client->dev);
spin_lock_irqsave(&chip->tsu_lock, flags);
if (!chip->det_started) {
chip->det_started = 1;
spin_unlock_irqrestore(&chip->tsu_lock, flags);
ret = tsu6111_detect_dev(chip);
chip->det_started = 0;
} else
spin_unlock_irqrestore(&chip->tsu_lock, flags);
pm_runtime_put_sync(&chip->client->dev);
}
static int tsu6111_handle_pwrsrc_notification(struct notifier_block *nb,
unsigned long event, void *param)
{
struct tsu6111_chip *chip =
container_of(nb, struct tsu6111_chip, vbus_nb);
dev_info(&chip->client->dev, "[PWRSRC notification]: %lu\n", event);
schedule_work(&chip->vbus_work);
return NOTIFY_OK;
}
static int tsu6111_irq_init(struct tsu6111_chip *chip)
{
const struct acpi_device_id *id;
struct i2c_client *client = chip->client;
struct device *dev;
struct gpio_desc *gpio;
int ret;
if (!client)
return -EINVAL;
dev = &client->dev;
if (!ACPI_HANDLE(dev))
return -ENODEV;
id = acpi_match_device(dev->driver->acpi_match_table, dev);
if (!id) {
dev_err(dev, "%s: no acpi dev match\n", __func__);
return -ENODEV;
}
gpio = devm_gpiod_get_index(dev, "tsu6111_int", 0);
if (IS_ERR(gpio)) {
dev_err(dev, "acpi gpio get index failed\n");
return PTR_ERR(gpio);
}
ret = gpiod_to_irq(gpio);
if (ret < 0) {
dev_err(dev, "%s: invalid irq for the gpio\n", __func__);
return ret;
} else
dev_info(dev, "%s: irq = %d\n", __func__, ret);
/* get irq number */
chip->client->irq = ret;
if (client->irq) {
ret = request_threaded_irq(client->irq, NULL,
tsu6111_irq_handler,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"tsu6111", chip);
if (ret) {
dev_err(&client->dev, "failed to reqeust IRQ\n");
return ret;
}
enable_irq_wake(client->irq);
} else {
dev_err(&client->dev, "IRQ not set\n");
return -EINVAL;
}
return 0;
}
static int tsu6111_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
const struct acpi_device_id *acpi_id;
struct tsu6111_chip *chip;
struct device *dev;
int ret, val;
chip = kzalloc(sizeof(struct tsu6111_chip), GFP_KERNEL);
if (!chip) {
dev_err(&client->dev, "failed to allocate driver data\n");
return -ENOMEM;
}
dev = &client->dev;
if (!ACPI_HANDLE(dev)) {
dev_err(&client->dev, "ACPI_HANDLE is invalid\n");
return -ENODEV;
}
acpi_id = acpi_match_device(dev->driver->acpi_match_table, dev);
if (!acpi_id) {
dev_err(&client->dev, "null id while ACPI_HANDLE is valid\n");
return -ENODEV;
}
chip->client = client;
chip->pdata = (struct tsu6111_pdata *)acpi_id->driver_data;
i2c_set_clientdata(client, chip);
spin_lock_init(&chip->tsu_lock);
wake_lock_init(&chip->wakelock, WAKE_LOCK_SUSPEND,
"tsu6111_wakelock");
/* register with extcon */
chip->edev = kzalloc(sizeof(struct extcon_dev), GFP_KERNEL);
if (!chip->edev) {
dev_err(&client->dev, "mem alloc failed\n");
ret = -ENOMEM;
goto extcon_mem_failed;
}
chip->edev->name = "tsu6111";
chip->edev->supported_cable = tsu6111_extcon_cable;
ret = extcon_dev_register(chip->edev);
if (ret) {
dev_err(&client->dev, "extcon registration failed!!\n");
goto extcon_reg_failed;
}
/* register for EXTCON USB notification */
INIT_WORK(&chip->vbus_work, tsu6111_pwrsrc_event_worker);
chip->vbus_nb.notifier_call = tsu6111_handle_pwrsrc_notification;
ret = extcon_register_interest(&chip->cable_obj, NULL,
TSU6111_EXTCON_USB, &chip->vbus_nb);
if (ret < 0)
dev_err(&client->dev, "register pwrsrc notification failed!\n");
/* OTG notification */
chip->otg = usb_get_phy(USB_PHY_TYPE_USB2);
if (!chip->otg) {
dev_warn(&client->dev, "Failed to get otg transceiver!!\n");
goto otg_reg_failed;
}
INIT_WORK(&chip->otg_work, tsu6111_otg_event_worker);
chip->id_nb.notifier_call = tsu6111_handle_otg_notification;
ret = usb_register_notifier(chip->otg, &chip->id_nb);
if (ret) {
dev_err(&chip->client->dev,
"failed to register otg notifier\n");
goto id_reg_failed;
}
chip->host_nb.notifier_call = tsu6111_handle_host_notification;
ret = extcon_register_interest(&chip->host_cable_obj, NULL, "USB-Host",
&chip->host_nb);
if (ret)
dev_err(&client->dev, "register host notification failed!\n");
ret = tsu6111_irq_init(chip);
if (ret) {
dev_err(&client->dev, "tsu6111_irq_init failed %d\n", ret);
goto intr_reg_failed;
}
/* Set manual switching, and unmask interrupt */
val = tsu6111_read_reg(chip->client, TSU_REG_CONTROL);
val = val & 0xfa;
tsu6111_write_reg(chip->client, TSU_REG_CONTROL, val);
/* open all switches, set correct status later by tsu6111_detect_dev */
tsu6111_write_reg(chip->client, TSU_REG_MANUALSW1, 0x00);
if (0 == tsu6111_read_reg(chip->client, TSU_REG_ADC)) {
dev_info(&client->dev, "probe: id shorted!!\n");
chip->id_short = true;
}
if (chip->id_short)
atomic_notifier_call_chain(&chip->otg->notifier,
USB_EVENT_ID, &chip->id_short);
else
tsu6111_detect_dev(chip);
/* Init Runtime PM State */
pm_runtime_put_noidle(&chip->client->dev);
pm_schedule_suspend(&chip->client->dev, MSEC_PER_SEC);
return 0;
intr_reg_failed:
usb_unregister_notifier(chip->otg, &chip->id_nb);
id_reg_failed:
usb_put_phy(chip->otg);
otg_reg_failed:
extcon_dev_unregister(chip->edev);
extcon_reg_failed:
kfree(chip->edev);
extcon_mem_failed:
wake_lock_destroy(&chip->wakelock);
kfree(chip);
return ret;
}
static int tsu6111_remove(struct i2c_client *client)
{
struct tsu6111_chip *chip = i2c_get_clientdata(client);
free_irq(client->irq, chip);
usb_put_phy(chip->otg);
extcon_dev_unregister(chip->edev);
wake_lock_destroy(&chip->wakelock);
kfree(chip->edev);
pm_runtime_get_noresume(&chip->client->dev);
kfree(chip);
return 0;
}
static void tsu6111_shutdown(struct i2c_client *client)
{
dev_dbg(&client->dev, "tsu6111 shutdown\n");
if (client->irq > 0)
disable_irq(client->irq);
return;
}
static int tsu6111_suspend(struct device *dev)
{
struct tsu6111_chip *chip = dev_get_drvdata(dev);
if (chip->client->irq > 0)
disable_irq(chip->client->irq);
dev_dbg(dev, "%s called\n", __func__);
return 0;
}
static int tsu6111_resume(struct device *dev)
{
struct tsu6111_chip *chip = dev_get_drvdata(dev);
if (chip->client->irq > 0)
enable_irq(chip->client->irq);
dev_dbg(dev, "%s called\n", __func__);
return 0;
}
static int tsu6111_runtime_suspend(struct device *dev)
{
dev_dbg(dev, "%s called\n", __func__);
return 0;
}
static int tsu6111_runtime_resume(struct device *dev)
{
dev_dbg(dev, "%s called\n", __func__);
return 0;
}
static int tsu6111_runtime_idle(struct device *dev)
{
dev_dbg(dev, "%s called\n", __func__);
return 0;
}
static const struct dev_pm_ops tsu6111_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(tsu6111_suspend,
tsu6111_resume)
SET_RUNTIME_PM_OPS(tsu6111_runtime_suspend,
tsu6111_runtime_resume,
tsu6111_runtime_idle)
};
static struct tsu6111_pdata tsu_drvdata = {
#ifdef CONFIG_CHARGER_BQ24192
.enable_vbus = bq24192_vbus_enable,
.disable_vbus = bq24192_vbus_disable,
.is_vbus_online = dc_ti_vbus_on_status,
#else
.enable_vbus = dummy_vbus_enable,
.disable_vbus = dummy_vbus_disable,
.is_vbus_online = dummy_vbus_status,
#endif
.charging_compliance_override = true,
};
static const struct i2c_device_id tsu6111_id[] = {
{"TSUM6111", (kernel_ulong_t)&tsu_drvdata},
{}
};
MODULE_DEVICE_TABLE(i2c, tsu6111_id);
static const struct acpi_device_id acpi_tsu6111_id[] = {
{"TSUM6111", (kernel_ulong_t)&tsu_drvdata},
{}
};
MODULE_DEVICE_TABLE(acpi, acpi_tsu6111_id);
static struct i2c_driver tsu6111_i2c_driver = {
.driver = {
.name = "TSUM6111",
.owner = THIS_MODULE,
.pm = &tsu6111_pm_ops,
.acpi_match_table = ACPI_PTR(acpi_tsu6111_id),
},
.probe = tsu6111_probe,
.remove = tsu6111_remove,
.id_table = tsu6111_id,
.shutdown = tsu6111_shutdown,
};
static int __init tsu6111_extcon_init(void)
{
int ret = i2c_add_driver(&tsu6111_i2c_driver);
return ret;
}
late_initcall(tsu6111_extcon_init);
static void __exit tsu6111_extcon_exit(void)
{
i2c_del_driver(&tsu6111_i2c_driver);
}
module_exit(tsu6111_extcon_exit);
MODULE_AUTHOR("Fei Yang <fei.yang@intel.com>");
MODULE_DESCRIPTION("TSU6111 extcon driver");
MODULE_LICENSE("GPL");