blob: a45d2867381c108d093079dd341f3c973aff6031 [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/slab.h>
#include <linux/i2c/isl9519.h>
#include <linux/msm_adc.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 ((HZ) * 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;
};
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;
return 0;
}
static int isl9519q_write_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_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;
}
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 void isl9519q_charge(struct work_struct *isl9519_work)
{
u16 temp;
int ret;
struct isl9519q_struct *isl_chg;
int isl_charger_current;
int mv_reading;
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) {
isl_charger_current = 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__, isl_charger_current);
if (isl_charger_current >= 0
&& isl_charger_current <= isl_chg->term_current) {
msm_charger_notify_event(
&isl_chg->adapter_hw_chg,
CHG_DONE_EVENT);
}
isl9519q_write_reg(isl_chg->client, CHG_CURRENT_REG,
isl_chg->chgcurrent);
ret = isl9519q_read_reg(isl_chg->client, CONTROL_REG, &temp);
if (!ret) {
if (!(temp & TRCKL_CHG_STATUS_BIT))
msm_charger_notify_event(
&isl_chg->adapter_hw_chg,
CHG_BATT_BEGIN_FAST_CHARGING);
} else {
dev_err(&isl_chg->client->dev,
"%s couldnt read cntrl reg\n", __func__);
}
schedule_delayed_work(&isl_chg->charge_work,
ISL9519_CHG_PERIOD);
}
}
static int isl9519q_start_charging(struct msm_hardware_charger *hw_chg,
int chg_voltage, int chg_current)
{
struct isl9519q_struct *isl_chg;
int ret = 0;
isl_chg = container_of(hw_chg, struct isl9519q_struct, adapter_hw_chg);
if (isl_chg->charging)
/* we are already charging */
return 0;
if (isl_chg->suspended) {
isl_chg->charge_at_resume = 1;
return 0;
}
dev_dbg(&isl_chg->client->dev, "%s\n", __func__);
ret = isl9519q_write_reg(isl_chg->client, CHG_CURRENT_REG,
isl_chg->chgcurrent);
if (ret) {
dev_err(&isl_chg->client->dev,
"%s coulnt write to current_reg\n", __func__);
goto out;
}
dev_dbg(&isl_chg->client->dev, "%s starting timed work\n",
__func__);
schedule_delayed_work(&isl_chg->charge_work,
ISL9519_CHG_PERIOD);
isl_chg->charging = true;
out:
return ret;
}
static int isl9519q_stop_charging(struct msm_hardware_charger *hw_chg)
{
struct isl9519q_struct *isl_chg;
int ret = 0;
isl_chg = container_of(hw_chg, struct isl9519q_struct, adapter_hw_chg);
if (!(isl_chg->charging))
/* we arent charging */
return 0;
if (isl_chg->suspended) {
isl_chg->charge_at_resume = 0;
return 0;
}
dev_dbg(&isl_chg->client->dev, "%s\n", __func__);
ret = isl9519q_write_reg(isl_chg->client, CHG_CURRENT_REG, 0);
if (ret) {
dev_err(&isl_chg->client->dev,
"%s coulnt write to current_reg\n", __func__);
goto out;
}
isl_chg->charging = false;
cancel_delayed_work(&isl_chg->charge_work);
out:
return ret;
}
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_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;
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;
}
INIT_DELAYED_WORK(&isl_chg->charge_work, isl9519q_charge);
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_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 = isl9519q_start_charging;
isl_chg->adapter_hw_chg.stop_charging = isl9519q_stop_charging;
isl_chg->adapter_hw_chg.charging_switched = isl9519q_charging_switched;
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;
}
}
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 free_isl_chg;
}
i2c_set_clientdata(client, isl_chg);
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);
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) {
dev_err(&client->dev,
"%s couldnt write to MAX_SYS_VOLTAGE_REG ret=%d\n",
__func__, ret);
goto free_irq;
}
ret = isl9519q_write_reg(isl_chg->client, MIN_SYS_VOLTAGE_REG,
isl_chg->min_system_voltage);
if (ret) {
dev_err(&client->dev,
"%s couldnt write to MIN_SYS_VOLTAGE_REG ret=%d\n",
__func__, ret);
goto free_irq;
}
if (isl_chg->input_current) {
ret = isl9519q_write_reg(isl_chg->client,
INPUT_CURRENT_REG,
isl_chg->input_current);
if (ret) {
dev_err(&client->dev,
"%s couldnt write INPUT_CURRENT_REG ret=%d\n",
__func__, ret);
goto free_irq;
}
}
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;
}
pr_debug("%s OK chg_present=%d\n", __func__, isl_chg->present);
return 0;
free_irq:
free_irq(client->irq, NULL);
unregister:
msm_charger_unregister(&isl_chg->adapter_hw_chg);
free_gpio:
gpio_free(pdata->valid_n_gpio);
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);
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->adapter_hw_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)
{
return i2c_add_driver(&isl9519q_driver);
}
module_init(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");