blob: 7bdac20e940c18207e68c0a06b7bca29ad4e86c9 [file] [log] [blame]
/*
* power_supply_extcon: Power supply detection through extcon.
*
* Copyright (c) 2012-2013, NVIDIA CORPORATION. All rights reserved.
* Laxman Dewangan <ldewangan@nvidia.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include <linux/delay.h>
#include <linux/err.h>
#include <linux/err.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/power_supply.h>
#include <linux/power/power_supply_extcon.h>
#include <linux/slab.h>
#include <linux/extcon.h>
#define CHARGER_TYPE_DETECTION_DEFAULT_DEBOUNCE_TIME_MS 500
struct power_supply_extcon {
struct device *dev;
struct extcon_dev *edev;
struct power_supply ac;
struct power_supply usb;
uint8_t ac_online;
uint8_t usb_online;
struct power_supply_extcon_plat_data *pdata;
};
struct power_supply_cables {
const char *name;
long int event;
struct power_supply_extcon *psy_extcon;
struct notifier_block nb;
struct extcon_specific_cable_nb *extcon_dev;
struct delayed_work extcon_notifier_work;
};
static struct power_supply_cables psy_cables[] = {
{
.name = "USB",
},
{
.name = "TA",
},
{
.name = "Fast-charger",
},
{
.name = "Slow-charger",
},
{
.name = "Charge-downstream",
},
};
static enum power_supply_property power_supply_extcon_props[] = {
POWER_SUPPLY_PROP_ONLINE,
};
static int power_supply_extcon_get_property(struct power_supply *psy,
enum power_supply_property psp, union power_supply_propval *val)
{
int online;
int ret = 0;
struct power_supply_extcon *psy_extcon;
if (psy->type == POWER_SUPPLY_TYPE_MAINS) {
psy_extcon = container_of(psy, struct power_supply_extcon, ac);
online = psy_extcon->ac_online;
} else if (psy->type == POWER_SUPPLY_TYPE_USB) {
psy_extcon = container_of(psy, struct power_supply_extcon, usb);
online = psy_extcon->usb_online;
} else {
return -EINVAL;
}
switch (psp) {
case POWER_SUPPLY_PROP_ONLINE:
val->intval = online;
break;
default:
return -EINVAL;
}
return ret;
}
static int power_supply_extcon_remove_cable(
struct power_supply_extcon *psy_extcon,
struct extcon_dev *edev)
{
dev_info(psy_extcon->dev, "Charging cable removed\n");
psy_extcon->ac_online = 0;
psy_extcon->usb_online = 0;
power_supply_changed(&psy_extcon->usb);
power_supply_changed(&psy_extcon->ac);
return 0;
}
static int power_supply_extcon_attach_cable(
struct power_supply_extcon *psy_extcon,
struct extcon_dev *edev)
{
psy_extcon->usb_online = 0;
psy_extcon->ac_online = 0;
if (true == extcon_get_cable_state(edev, "USB")) {
psy_extcon->usb_online = 1;
dev_info(psy_extcon->dev, "USB charger cable detected\n");
} else if (true == extcon_get_cable_state(edev, "Charge-downstream")) {
psy_extcon->usb_online = 1;
dev_info(psy_extcon->dev,
"USB charger downstream cable detected\n");
} else if (true == extcon_get_cable_state(edev, "TA")) {
psy_extcon->ac_online = 1;
dev_info(psy_extcon->dev, "USB TA cable detected\n");
} else if (true == extcon_get_cable_state(edev, "Fast-charger")) {
psy_extcon->ac_online = 1;
dev_info(psy_extcon->dev, "USB Fast-charger cable detected\n");
} else if (true == extcon_get_cable_state(edev, "Slow-charger")) {
psy_extcon->ac_online = 1;
dev_info(psy_extcon->dev, "USB Slow-charger cable detected\n");
} else {
dev_info(psy_extcon->dev, "Unknown cable detected\n");
}
power_supply_changed(&psy_extcon->usb);
power_supply_changed(&psy_extcon->ac);
return 0;
}
static void psy_extcon_extcon_handle_notifier(struct work_struct *w)
{
struct power_supply_cables *cable = container_of(to_delayed_work(w),
struct power_supply_cables, extcon_notifier_work);
struct power_supply_extcon *psy_extcon = cable->psy_extcon;
struct extcon_dev *edev = cable->extcon_dev->edev;
if (cable->event == 0)
power_supply_extcon_remove_cable(psy_extcon, edev);
else if (cable->event == 1)
power_supply_extcon_attach_cable(psy_extcon, edev);
}
static int psy_extcon_extcon_notifier(struct notifier_block *self,
unsigned long event, void *ptr)
{
struct power_supply_cables *cable = container_of(self,
struct power_supply_cables, nb);
cable->event = event;
cancel_delayed_work(&cable->extcon_notifier_work);
schedule_delayed_work(&cable->extcon_notifier_work,
msecs_to_jiffies(CHARGER_TYPE_DETECTION_DEFAULT_DEBOUNCE_TIME_MS));
return NOTIFY_DONE;
}
static int psy_extcon_probe(struct platform_device *pdev)
{
int ret = 0;
uint8_t j;
struct power_supply_extcon *psy_extcon;
struct power_supply_extcon_plat_data *pdata = pdev->dev.platform_data;
if (!pdata) {
dev_err(&pdev->dev, "No platform data, exiting..\n");
return -ENODEV;
}
psy_extcon = devm_kzalloc(&pdev->dev, sizeof(*psy_extcon), GFP_KERNEL);
if (!psy_extcon) {
dev_err(&pdev->dev, "failed to allocate memory status\n");
return -ENOMEM;
}
psy_extcon->dev = &pdev->dev;
dev_set_drvdata(&pdev->dev, psy_extcon);
psy_extcon->ac.name = "ac";
psy_extcon->ac.type = POWER_SUPPLY_TYPE_MAINS;
psy_extcon->ac.get_property = power_supply_extcon_get_property;
psy_extcon->ac.properties = power_supply_extcon_props;
psy_extcon->ac.num_properties = ARRAY_SIZE(power_supply_extcon_props);
ret = power_supply_register(psy_extcon->dev, &psy_extcon->ac);
if (ret) {
dev_err(psy_extcon->dev, "failed: power supply register\n");
return ret;
}
psy_extcon->usb = psy_extcon->ac;
psy_extcon->usb.name = "usb";
psy_extcon->usb.type = POWER_SUPPLY_TYPE_USB;
ret = power_supply_register(psy_extcon->dev, &psy_extcon->usb);
if (ret) {
dev_err(psy_extcon->dev, "failed: power supply register\n");
goto pwr_sply_error;
}
for (j = 0 ; j < ARRAY_SIZE(psy_cables); j++) {
struct power_supply_cables *cable = &psy_cables[j];
cable->extcon_dev = devm_kzalloc(&pdev->dev,
sizeof(struct extcon_specific_cable_nb),
GFP_KERNEL);
if (!cable->extcon_dev) {
dev_err(&pdev->dev, "Malloc for extcon_dev failed\n");
goto econ_err;
}
INIT_DELAYED_WORK(&cable->extcon_notifier_work,
psy_extcon_extcon_handle_notifier);
cable->psy_extcon = psy_extcon;
cable->nb.notifier_call = psy_extcon_extcon_notifier;
ret = extcon_register_interest(cable->extcon_dev,
pdata->extcon_name,
cable->name, &cable->nb);
if (ret < 0)
dev_err(psy_extcon->dev, "Cannot register for cable: %s\n",
cable->name);
}
psy_extcon->edev = extcon_get_extcon_dev(pdata->extcon_name);
if (!psy_extcon->edev)
goto econ_err;
power_supply_extcon_attach_cable(psy_extcon, psy_extcon->edev);
dev_info(&pdev->dev, "%s() get success\n", __func__);
return 0;
econ_err:
power_supply_unregister(&psy_extcon->usb);
pwr_sply_error:
power_supply_unregister(&psy_extcon->ac);
return ret;
}
static int psy_extcon_remove(struct platform_device *pdev)
{
struct power_supply_extcon *psy_extcon = platform_get_drvdata(pdev);
power_supply_unregister(&psy_extcon->ac);
power_supply_unregister(&psy_extcon->usb);
return 0;
}
static struct platform_driver power_supply_extcon_driver = {
.driver = {
.name = "power-supply-extcon",
.owner = THIS_MODULE,
},
.probe = psy_extcon_probe,
.remove = psy_extcon_remove,
};
static int __init psy_extcon_init(void)
{
return platform_driver_register(&power_supply_extcon_driver);
}
static void __exit psy_extcon_exit(void)
{
platform_driver_unregister(&power_supply_extcon_driver);
}
late_initcall(psy_extcon_init);
module_exit(psy_extcon_exit);
MODULE_DESCRIPTION("Power supply detection through extcon driver");
MODULE_AUTHOR("Laxman Dewangan <ldewangan@nvidia.com>");
MODULE_LICENSE("GPL v2");