blob: e4729f20c5497994b99c155c1ab66af53b881fe3 [file] [log] [blame]
/* Copyright (c) 2010-2011, Code Aurora Forum. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 and
* only 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/i2c.h>
#include <linux/gpio.h>
#include <linux/errno.h>
#include <linux/delay.h>
#include <linux/module.h>
#include <linux/debugfs.h>
#include <linux/workqueue.h>
#include <linux/interrupt.h>
#include <linux/msm-charger.h>
#include <linux/mfd/pm8xxx/pm8921-charger.h>
#include <linux/slab.h>
#include <linux/i2c/isl9519.h>
#include <linux/msm_adc.h>
#include <linux/spinlock.h>
#define CHG_CURRENT_REG 0x14
#define MAX_SYS_VOLTAGE_REG 0x15
#define CONTROL_REG 0x3D
#define MIN_SYS_VOLTAGE_REG 0x3E
#define INPUT_CURRENT_REG 0x3F
#define MANUFACTURER_ID_REG 0xFE
#define DEVICE_ID_REG 0xFF
#define TRCKL_CHG_STATUS_BIT 0x80
#define ISL9519_CHG_PERIOD_SEC 150
struct isl9519q_struct {
struct i2c_client *client;
struct delayed_work charge_work;
int present;
int batt_present;
bool charging;
int chgcurrent;
int term_current;
int input_current;
int max_system_voltage;
int min_system_voltage;
int valid_n_gpio;
struct dentry *dent;
struct msm_hardware_charger adapter_hw_chg;
int suspended;
int charge_at_resume;
struct ext_chg_pm8921 ext_chg;
spinlock_t lock;
bool notify_by_pmic;
bool trickle;
};
static struct isl9519q_struct *the_isl_chg;
static int isl9519q_read_reg(struct i2c_client *client, int reg,
u16 *val)
{
int ret;
struct isl9519q_struct *isl_chg;
isl_chg = i2c_get_clientdata(client);
ret = i2c_smbus_read_word_data(isl_chg->client, reg);
if (ret < 0) {
dev_err(&isl_chg->client->dev,
"i2c read fail: can't read from %02x: %d\n", reg, ret);
return -EAGAIN;
} else
*val = ret;
pr_debug("%s.reg=0x%x.val=0x%x.\n", __func__, reg, *val);
return 0;
}
static int isl9519q_write_reg(struct i2c_client *client, int reg,
u16 val)
{
int ret;
struct isl9519q_struct *isl_chg;
pr_debug("%s.reg=0x%x.val=0x%x.\n", __func__, reg, val);
isl_chg = i2c_get_clientdata(client);
ret = i2c_smbus_write_word_data(isl_chg->client, reg, val);
if (ret < 0) {
dev_err(&isl_chg->client->dev,
"i2c write fail: can't write %02x to %02x: %d\n",
val, reg, ret);
return -EAGAIN;
}
return 0;
}
/**
* Read charge-current via ADC.
*
* The ISL CCMON (charge-current-monitor) pin is connected to
* the PMIC MPP#X pin.
* This not required when notify_by_pmic is used where the PMIC
* uses BMS to notify the ISL on charging-done / charge-resume.
*/
static int isl_read_adc(int channel, int *mv_reading)
{
int ret;
void *h;
struct adc_chan_result adc_chan_result;
struct completion conv_complete_evt;
pr_debug("%s: called for %d\n", __func__, channel);
ret = adc_channel_open(channel, &h);
if (ret) {
pr_err("%s: couldnt open channel %d ret=%d\n",
__func__, channel, ret);
goto out;
}
init_completion(&conv_complete_evt);
ret = adc_channel_request_conv(h, &conv_complete_evt);
if (ret) {
pr_err("%s: couldnt request conv channel %d ret=%d\n",
__func__, channel, ret);
goto out;
}
ret = wait_for_completion_interruptible(&conv_complete_evt);
if (ret) {
pr_err("%s: wait interrupted channel %d ret=%d\n",
__func__, channel, ret);
goto out;
}
ret = adc_channel_read_result(h, &adc_chan_result);
if (ret) {
pr_err("%s: couldnt read result channel %d ret=%d\n",
__func__, channel, ret);
goto out;
}
ret = adc_channel_close(h);
if (ret)
pr_err("%s: couldnt close channel %d ret=%d\n",
__func__, channel, ret);
if (mv_reading)
*mv_reading = (int)adc_chan_result.measurement;
pr_debug("%s: done for %d\n", __func__, channel);
return adc_chan_result.physical;
out:
*mv_reading = 0;
pr_debug("%s: done with error for %d\n", __func__, channel);
return -EINVAL;
}
static bool is_trickle_charging(struct isl9519q_struct *isl_chg)
{
u16 ctrl = 0;
int ret;
ret = isl9519q_read_reg(isl_chg->client, CONTROL_REG, &ctrl);
if (!ret) {
pr_debug("%s.control_reg=0x%x.\n", __func__, ctrl);
} else {
dev_err(&isl_chg->client->dev,
"%s couldnt read cntrl reg\n", __func__);
}
if (ctrl & TRCKL_CHG_STATUS_BIT)
return true;
return false;
}
static void isl_adapter_check_ichg(struct isl9519q_struct *isl_chg)
{
int ichg; /* isl charger current */
int mv_reading = 0;
ichg = isl_read_adc(CHANNEL_ADC_BATT_AMON, &mv_reading);
dev_dbg(&isl_chg->client->dev, "%s mv_reading=%d\n",
__func__, mv_reading);
dev_dbg(&isl_chg->client->dev, "%s isl_charger_current=%d\n",
__func__, ichg);
if (ichg >= 0 && ichg <= isl_chg->term_current)
msm_charger_notify_event(&isl_chg->adapter_hw_chg,
CHG_DONE_EVENT);
isl_chg->trickle = is_trickle_charging(isl_chg);
if (isl_chg->trickle)
msm_charger_notify_event(&isl_chg->adapter_hw_chg,
CHG_BATT_BEGIN_FAST_CHARGING);
}
/**
* isl9519q_worker
*
* Periodic task required to kick the ISL HW watchdog to keep
* charging.
*
* @isl9519_work: work context.
*/
static void isl9519q_worker(struct work_struct *isl9519_work)
{
struct isl9519q_struct *isl_chg;
isl_chg = container_of(isl9519_work, struct isl9519q_struct,
charge_work.work);
dev_dbg(&isl_chg->client->dev, "%s\n", __func__);
if (!isl_chg->charging) {
pr_info("%s.stop charging.\n", __func__);
isl9519q_write_reg(isl_chg->client, CHG_CURRENT_REG, 0);
return; /* Stop periodic worker */
}
/* Kick the dog by writting to CHG_CURRENT_REG */
isl9519q_write_reg(isl_chg->client, CHG_CURRENT_REG,
isl_chg->chgcurrent);
if (isl_chg->notify_by_pmic)
isl_chg->trickle = is_trickle_charging(isl_chg);
else
isl_adapter_check_ichg(isl_chg);
schedule_delayed_work(&isl_chg->charge_work,
(ISL9519_CHG_PERIOD_SEC * HZ));
}
static int isl9519q_start_charging(struct isl9519q_struct *isl_chg,
int chg_voltage, int chg_current)
{
int ret = 0;
pr_info("%s.\n", __func__);
if (isl_chg->charging) {
pr_warn("%s.already charging.\n", __func__);
return 0;
}
if (isl_chg->suspended) {
pr_warn("%s.suspended - can't start charging.\n", __func__);
isl_chg->charge_at_resume = 1;
return 0;
}
dev_dbg(&isl_chg->client->dev,
"%s starting timed work.period=%d seconds.\n",
__func__, (int) ISL9519_CHG_PERIOD_SEC);
/*
* The ISL will start charging from the worker context.
* This API might be called from interrupt context.
*/
schedule_delayed_work(&isl_chg->charge_work, 1);
isl_chg->charging = true;
return ret;
}
static int isl9519q_stop_charging(struct isl9519q_struct *isl_chg)
{
pr_info("%s.\n", __func__);
if (!(isl_chg->charging)) {
pr_warn("%s.already not charging.\n", __func__);
return 0;
}
if (isl_chg->suspended) {
isl_chg->charge_at_resume = 0;
return 0;
}
dev_dbg(&isl_chg->client->dev, "%s\n", __func__);
isl_chg->charging = false;
isl_chg->trickle = false;
/*
* The ISL will stop charging from the worker context.
* This API might be called from interrupt context.
*/
schedule_delayed_work(&isl_chg->charge_work, 1);
return 0;
}
static int isl_ext_start_charging(void *ctx)
{
int rc;
struct isl9519q_struct *isl_chg = ctx;
unsigned long flags;
spin_lock_irqsave(&isl_chg->lock, flags);
rc = isl9519q_start_charging(isl_chg, 0, isl_chg->chgcurrent);
spin_unlock_irqrestore(&isl_chg->lock, flags);
return rc;
}
static int isl_ext_stop_charging(void *ctx)
{
int rc;
struct isl9519q_struct *isl_chg = ctx;
unsigned long flags;
spin_lock_irqsave(&isl_chg->lock, flags);
rc = isl9519q_stop_charging(isl_chg);
spin_unlock_irqrestore(&isl_chg->lock, flags);
return rc;
}
static bool isl_ext_is_trickle(void *ctx)
{
struct isl9519q_struct *isl_chg = ctx;
return isl_chg->trickle;
}
static int isl_adapter_start_charging(struct msm_hardware_charger *hw_chg,
int chg_voltage, int chg_current)
{
int rc;
struct isl9519q_struct *isl_chg;
isl_chg = container_of(hw_chg, struct isl9519q_struct, adapter_hw_chg);
rc = isl9519q_start_charging(isl_chg, chg_voltage, chg_current);
return rc;
}
static int isl_adapter_stop_charging(struct msm_hardware_charger *hw_chg)
{
int rc;
struct isl9519q_struct *isl_chg;
isl_chg = container_of(hw_chg, struct isl9519q_struct, adapter_hw_chg);
rc = isl9519q_stop_charging(isl_chg);
return rc;
}
static int isl9519q_charging_switched(struct msm_hardware_charger *hw_chg)
{
struct isl9519q_struct *isl_chg;
isl_chg = container_of(hw_chg, struct isl9519q_struct, adapter_hw_chg);
dev_dbg(&isl_chg->client->dev, "%s\n", __func__);
return 0;
}
static irqreturn_t isl_valid_handler(int irq, void *dev_id)
{
int val;
struct isl9519q_struct *isl_chg;
struct i2c_client *client = dev_id;
isl_chg = i2c_get_clientdata(client);
val = gpio_get_value_cansleep(isl_chg->valid_n_gpio);
if (val < 0) {
dev_err(&isl_chg->client->dev,
"%s gpio_get_value failed for %d ret=%d\n", __func__,
isl_chg->valid_n_gpio, val);
goto err;
}
dev_dbg(&isl_chg->client->dev, "%s val=%d\n", __func__, val);
if (val) {
if (isl_chg->present == 1) {
msm_charger_notify_event(&isl_chg->adapter_hw_chg,
CHG_REMOVED_EVENT);
isl_chg->present = 0;
}
} else {
if (isl_chg->present == 0) {
msm_charger_notify_event(&isl_chg->adapter_hw_chg,
CHG_INSERTED_EVENT);
isl_chg->present = 1;
}
}
err:
return IRQ_HANDLED;
}
#define MAX_VOLTAGE_REG_MASK 0x3FF0
#define MIN_VOLTAGE_REG_MASK 0x3F00
#define DEFAULT_MAX_VOLTAGE_REG_VALUE 0x1070
#define DEFAULT_MIN_VOLTAGE_REG_VALUE 0x0D00
static int __devinit isl9519q_init_adapter(struct isl9519q_struct *isl_chg)
{
int ret;
struct isl_platform_data *pdata = isl_chg->client->dev.platform_data;
struct i2c_client *client = isl_chg->client;
isl_chg->adapter_hw_chg.type = CHG_TYPE_AC;
isl_chg->adapter_hw_chg.rating = 2;
isl_chg->adapter_hw_chg.name = "isl-adapter";
isl_chg->adapter_hw_chg.start_charging = isl_adapter_start_charging;
isl_chg->adapter_hw_chg.stop_charging = isl_adapter_stop_charging;
isl_chg->adapter_hw_chg.charging_switched = isl9519q_charging_switched;
ret = gpio_request(pdata->valid_n_gpio, "isl_charger_valid");
if (ret) {
dev_err(&client->dev, "%s gpio_request failed "
"for %d ret=%d\n",
__func__, pdata->valid_n_gpio, ret);
goto out;
}
ret = msm_charger_register(&isl_chg->adapter_hw_chg);
if (ret) {
dev_err(&client->dev,
"%s msm_charger_register failed for ret =%d\n",
__func__, ret);
goto free_gpio;
}
ret = request_threaded_irq(client->irq, NULL,
isl_valid_handler,
IRQF_TRIGGER_FALLING |
IRQF_TRIGGER_RISING,
"isl_charger_valid", client);
if (ret) {
dev_err(&client->dev,
"%s request_threaded_irq failed "
"for %d ret =%d\n",
__func__, client->irq, ret);
goto unregister;
}
irq_set_irq_wake(client->irq, 1);
ret = gpio_get_value_cansleep(isl_chg->valid_n_gpio);
if (ret < 0) {
dev_err(&client->dev,
"%s gpio_get_value failed for %d ret=%d\n",
__func__, pdata->valid_n_gpio, ret);
/* assume absent */
ret = 1;
}
if (!ret) {
msm_charger_notify_event(&isl_chg->adapter_hw_chg,
CHG_INSERTED_EVENT);
isl_chg->present = 1;
}
return 0;
unregister:
msm_charger_unregister(&isl_chg->adapter_hw_chg);
free_gpio:
gpio_free(pdata->valid_n_gpio);
out:
return ret;
}
static int __devinit isl9519q_init_ext_chg(struct isl9519q_struct *isl_chg)
{
int ret;
isl_chg->ext_chg.name = "isl9519q";
isl_chg->ext_chg.ctx = isl_chg;
isl_chg->ext_chg.start_charging = isl_ext_start_charging;
isl_chg->ext_chg.stop_charging = isl_ext_stop_charging;
isl_chg->ext_chg.is_trickle = isl_ext_is_trickle;
ret = register_external_dc_charger(&isl_chg->ext_chg);
if (ret) {
pr_err("%s.failed to register external dc charger.ret=%d.\n",
__func__, ret);
return ret;
}
return 0;
}
static int set_reg(void *data, u64 val)
{
int addr = (int)data;
int ret;
u16 temp;
temp = (u16) val;
ret = isl9519q_write_reg(the_isl_chg->client, addr, temp);
if (ret) {
pr_err("isl9519q_write_reg to %x value =%d errored = %d\n",
addr, temp, ret);
return -EAGAIN;
}
return 0;
}
static int get_reg(void *data, u64 *val)
{
int addr = (int)data;
int ret;
u16 temp;
ret = isl9519q_read_reg(the_isl_chg->client, addr, &temp);
if (ret) {
pr_err("isl9519q_read_reg to %x value =%d errored = %d\n",
addr, temp, ret);
return -EAGAIN;
}
*val = temp;
return 0;
}
DEFINE_SIMPLE_ATTRIBUTE(reg_fops, get_reg, set_reg, "0x%02llx\n");
static void create_debugfs_entries(struct isl9519q_struct *isl_chg)
{
isl_chg->dent = debugfs_create_dir("isl9519q", NULL);
if (IS_ERR(isl_chg->dent)) {
pr_err("isl9519q driver couldn't create debugfs dir\n");
return;
}
debugfs_create_file("CHG_CURRENT_REG", 0644, isl_chg->dent,
(void *) CHG_CURRENT_REG, &reg_fops);
debugfs_create_file("MAX_SYS_VOLTAGE_REG", 0644, isl_chg->dent,
(void *) MAX_SYS_VOLTAGE_REG, &reg_fops);
debugfs_create_file("CONTROL_REG", 0644, isl_chg->dent,
(void *) CONTROL_REG, &reg_fops);
debugfs_create_file("MIN_SYS_VOLTAGE_REG", 0644, isl_chg->dent,
(void *) MIN_SYS_VOLTAGE_REG, &reg_fops);
debugfs_create_file("INPUT_CURRENT_REG", 0644, isl_chg->dent,
(void *) INPUT_CURRENT_REG, &reg_fops);
debugfs_create_file("MANUFACTURER_ID_REG", 0644, isl_chg->dent,
(void *) MANUFACTURER_ID_REG, &reg_fops);
debugfs_create_file("DEVICE_ID_REG", 0644, isl_chg->dent,
(void *) DEVICE_ID_REG, &reg_fops);
}
static void remove_debugfs_entries(struct isl9519q_struct *isl_chg)
{
if (isl_chg->dent)
debugfs_remove_recursive(isl_chg->dent);
}
static int __devinit isl9519q_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct isl_platform_data *pdata;
struct isl9519q_struct *isl_chg;
int ret;
ret = 0;
pdata = client->dev.platform_data;
pr_info("%s.\n", __func__);
if (pdata == NULL) {
dev_err(&client->dev, "%s no platform data\n", __func__);
ret = -EINVAL;
goto out;
}
if (!i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_WORD_DATA)) {
ret = -EIO;
goto out;
}
isl_chg = kzalloc(sizeof(*isl_chg), GFP_KERNEL);
if (!isl_chg) {
ret = -ENOMEM;
goto out;
}
spin_lock_init(&isl_chg->lock);
INIT_DELAYED_WORK(&isl_chg->charge_work, isl9519q_worker);
isl_chg->client = client;
isl_chg->chgcurrent = pdata->chgcurrent;
isl_chg->term_current = pdata->term_current;
isl_chg->input_current = pdata->input_current;
isl_chg->max_system_voltage = pdata->max_system_voltage;
isl_chg->min_system_voltage = pdata->min_system_voltage;
isl_chg->valid_n_gpio = pdata->valid_n_gpio;
/* h/w ignores lower 7 bits of charging current and input current */
isl_chg->chgcurrent &= ~0x7F;
isl_chg->input_current &= ~0x7F;
/**
* ISL is Notified by PMIC to start/stop charging, rather than
* handling interrupt from ISL for End-Of-Chargring, and
* monitoring the charge-current periodically. The valid_n_gpio
* is also not used, dc-present is detected by PMIC.
*/
isl_chg->notify_by_pmic = (client->irq == 0);
i2c_set_clientdata(client, isl_chg);
if (pdata->chg_detection_config) {
ret = pdata->chg_detection_config();
if (ret) {
dev_err(&client->dev, "%s valid config failed ret=%d\n",
__func__, ret);
goto free_isl_chg;
}
}
isl_chg->max_system_voltage &= MAX_VOLTAGE_REG_MASK;
isl_chg->min_system_voltage &= MIN_VOLTAGE_REG_MASK;
if (isl_chg->max_system_voltage == 0)
isl_chg->max_system_voltage = DEFAULT_MAX_VOLTAGE_REG_VALUE;
if (isl_chg->min_system_voltage == 0)
isl_chg->min_system_voltage = DEFAULT_MIN_VOLTAGE_REG_VALUE;
ret = isl9519q_write_reg(isl_chg->client, MAX_SYS_VOLTAGE_REG,
isl_chg->max_system_voltage);
if (ret)
goto free_isl_chg;
ret = isl9519q_write_reg(isl_chg->client, MIN_SYS_VOLTAGE_REG,
isl_chg->min_system_voltage);
if (ret)
goto free_isl_chg;
if (isl_chg->input_current) {
ret = isl9519q_write_reg(isl_chg->client,
INPUT_CURRENT_REG,
isl_chg->input_current);
if (ret)
goto free_isl_chg;
}
if (isl_chg->notify_by_pmic)
ret = isl9519q_init_ext_chg(isl_chg);
else
ret = isl9519q_init_adapter(isl_chg);
if (ret)
goto free_isl_chg;
the_isl_chg = isl_chg;
create_debugfs_entries(isl_chg);
pr_info("%s OK.\n", __func__);
return 0;
free_isl_chg:
kfree(isl_chg);
out:
return ret;
}
static int __devexit isl9519q_remove(struct i2c_client *client)
{
struct isl_platform_data *pdata;
struct isl9519q_struct *isl_chg = i2c_get_clientdata(client);
pdata = client->dev.platform_data;
gpio_free(pdata->valid_n_gpio);
free_irq(client->irq, client);
cancel_delayed_work_sync(&isl_chg->charge_work);
msm_charger_notify_event(&isl_chg->adapter_hw_chg, CHG_REMOVED_EVENT);
msm_charger_unregister(&isl_chg->adapter_hw_chg);
remove_debugfs_entries(isl_chg);
the_isl_chg = NULL;
kfree(isl_chg);
return 0;
}
static const struct i2c_device_id isl9519q_id[] = {
{"isl9519q", 0},
{},
};
#ifdef CONFIG_PM
static int isl9519q_suspend(struct device *dev)
{
struct isl9519q_struct *isl_chg = dev_get_drvdata(dev);
dev_dbg(&isl_chg->client->dev, "%s\n", __func__);
/*
* do not suspend while we are charging
* because we need to periodically update the register
* for charging to proceed
*/
if (isl_chg->charging)
return -EBUSY;
isl_chg->suspended = 1;
return 0;
}
static int isl9519q_resume(struct device *dev)
{
struct isl9519q_struct *isl_chg = dev_get_drvdata(dev);
dev_dbg(&isl_chg->client->dev, "%s\n", __func__);
isl_chg->suspended = 0;
if (isl_chg->charge_at_resume) {
isl_chg->charge_at_resume = 0;
isl9519q_start_charging(isl_chg, 0, 0);
}
return 0;
}
static const struct dev_pm_ops isl9519q_pm_ops = {
.suspend = isl9519q_suspend,
.resume = isl9519q_resume,
};
#endif
static struct i2c_driver isl9519q_driver = {
.driver = {
.name = "isl9519q",
.owner = THIS_MODULE,
#ifdef CONFIG_PM
.pm = &isl9519q_pm_ops,
#endif
},
.probe = isl9519q_probe,
.remove = __devexit_p(isl9519q_remove),
.id_table = isl9519q_id,
};
static int __init isl9519q_init(void)
{
pr_info("%s. isl9519q SW rev 1.01\n", __func__);
return i2c_add_driver(&isl9519q_driver);
}
late_initcall_sync(isl9519q_init);
static void __exit isl9519q_exit(void)
{
return i2c_del_driver(&isl9519q_driver);
}
module_exit(isl9519q_exit);
MODULE_AUTHOR("Abhijeet Dharmapurikar <adharmap@codeaurora.org>");
MODULE_DESCRIPTION("Driver for ISL9519Q Charger chip");
MODULE_LICENSE("GPL v2");