850 lines
20 KiB
C
850 lines
20 KiB
C
/* Copyright (c) 2010-2012, The Linux Foundation. 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.
|
|
*
|
|
*/
|
|
#define pr_fmt(fmt) "%s: " fmt, __func__
|
|
|
|
#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/power_supply.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 power_supply dc_psy;
|
|
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("reg=0x%x.val=0x%x.\n", 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("reg=0x%x.val=0x%x.\n", 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("called for %d\n", channel);
|
|
ret = adc_channel_open(channel, &h);
|
|
if (ret) {
|
|
pr_err("couldnt open channel %d ret=%d\n", channel, ret);
|
|
goto out;
|
|
}
|
|
init_completion(&conv_complete_evt);
|
|
ret = adc_channel_request_conv(h, &conv_complete_evt);
|
|
if (ret) {
|
|
pr_err("couldnt request conv channel %d ret=%d\n",
|
|
channel, ret);
|
|
goto out;
|
|
}
|
|
ret = wait_for_completion_interruptible(&conv_complete_evt);
|
|
if (ret) {
|
|
pr_err("wait interrupted channel %d ret=%d\n", channel, ret);
|
|
goto out;
|
|
}
|
|
ret = adc_channel_read_result(h, &adc_chan_result);
|
|
if (ret) {
|
|
pr_err("couldnt read result channel %d ret=%d\n",
|
|
channel, ret);
|
|
goto out;
|
|
}
|
|
ret = adc_channel_close(h);
|
|
if (ret)
|
|
pr_err("couldnt close channel %d ret=%d\n", channel, ret);
|
|
if (mv_reading)
|
|
*mv_reading = (int)adc_chan_result.measurement;
|
|
|
|
pr_debug("done for %d\n", channel);
|
|
return adc_chan_result.physical;
|
|
out:
|
|
*mv_reading = 0;
|
|
pr_debug("done with error for %d\n", 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("control_reg=0x%x.\n", 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_debug("stop charging.\n");
|
|
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)
|
|
{
|
|
pr_debug("\n");
|
|
|
|
if (isl_chg->charging) {
|
|
pr_warn("already charging.\n");
|
|
return 0;
|
|
}
|
|
|
|
if (isl_chg->suspended) {
|
|
pr_warn("suspended - can't start charging.\n");
|
|
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 0;
|
|
}
|
|
|
|
static int isl9519q_stop_charging(struct isl9519q_struct *isl_chg)
|
|
{
|
|
pr_debug("\n");
|
|
|
|
if (!(isl_chg->charging)) {
|
|
pr_warn("already not charging.\n");
|
|
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_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;
|
|
}
|
|
|
|
static enum power_supply_property pm_power_props[] = {
|
|
POWER_SUPPLY_PROP_ONLINE,
|
|
POWER_SUPPLY_PROP_CURRENT_MAX,
|
|
POWER_SUPPLY_PROP_CHARGE_TYPE,
|
|
};
|
|
|
|
static char *pm_power_supplied_to[] = {
|
|
"battery",
|
|
};
|
|
|
|
static int get_prop_charge_type(struct isl9519q_struct *isl_chg)
|
|
{
|
|
if (!isl_chg->present)
|
|
return POWER_SUPPLY_CHARGE_TYPE_NONE;
|
|
|
|
if (isl_chg->trickle)
|
|
return POWER_SUPPLY_CHARGE_TYPE_TRICKLE;
|
|
|
|
if (isl_chg->charging)
|
|
return POWER_SUPPLY_CHARGE_TYPE_FAST;
|
|
|
|
return POWER_SUPPLY_CHARGE_TYPE_NONE;
|
|
}
|
|
static int pm_power_get_property(struct power_supply *psy,
|
|
enum power_supply_property psp,
|
|
union power_supply_propval *val)
|
|
{
|
|
struct isl9519q_struct *isl_chg = container_of(psy,
|
|
struct isl9519q_struct,
|
|
dc_psy);
|
|
|
|
switch (psp) {
|
|
case POWER_SUPPLY_PROP_CURRENT_MAX:
|
|
val->intval = isl_chg->chgcurrent;
|
|
break;
|
|
case POWER_SUPPLY_PROP_ONLINE:
|
|
val->intval = (int)isl_chg->present;
|
|
break;
|
|
case POWER_SUPPLY_PROP_CHARGE_TYPE:
|
|
val->intval = get_prop_charge_type(isl_chg);
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int pm_power_set_property(struct power_supply *psy,
|
|
enum power_supply_property psp,
|
|
const union power_supply_propval *val)
|
|
{
|
|
struct isl9519q_struct *isl_chg = container_of(psy,
|
|
struct isl9519q_struct,
|
|
dc_psy);
|
|
unsigned long flags;
|
|
int rc;
|
|
|
|
switch (psp) {
|
|
case POWER_SUPPLY_PROP_ONLINE:
|
|
if (val->intval) {
|
|
isl_chg->present = val->intval;
|
|
} else {
|
|
isl_chg->present = 0;
|
|
if (isl_chg->charging)
|
|
goto stop_charging;
|
|
}
|
|
break;
|
|
case POWER_SUPPLY_PROP_CURRENT_MAX:
|
|
if (val->intval) {
|
|
if (isl_chg->chgcurrent != val->intval)
|
|
return -EINVAL;
|
|
}
|
|
break;
|
|
case POWER_SUPPLY_PROP_CHARGE_TYPE:
|
|
if (val->intval && isl_chg->present) {
|
|
if (val->intval == POWER_SUPPLY_CHARGE_TYPE_FAST)
|
|
goto start_charging;
|
|
if (val->intval == POWER_SUPPLY_CHARGE_TYPE_NONE)
|
|
goto stop_charging;
|
|
} else {
|
|
return -EINVAL;
|
|
}
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
power_supply_changed(&isl_chg->dc_psy);
|
|
return 0;
|
|
|
|
start_charging:
|
|
spin_lock_irqsave(&isl_chg->lock, flags);
|
|
rc = isl9519q_start_charging(isl_chg, 0, isl_chg->chgcurrent);
|
|
if (rc)
|
|
pr_err("Failed to start charging rc=%d\n", rc);
|
|
spin_unlock_irqrestore(&isl_chg->lock, flags);
|
|
power_supply_changed(&isl_chg->dc_psy);
|
|
return rc;
|
|
|
|
stop_charging:
|
|
spin_lock_irqsave(&isl_chg->lock, flags);
|
|
rc = isl9519q_stop_charging(isl_chg);
|
|
if (rc)
|
|
pr_err("Failed to start charging rc=%d\n", rc);
|
|
spin_unlock_irqrestore(&isl_chg->lock, flags);
|
|
power_supply_changed(&isl_chg->dc_psy);
|
|
return rc;
|
|
}
|
|
|
|
#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 i2c_client *client = isl_chg->client;
|
|
struct isl_platform_data *pdata = client->dev.platform_data;
|
|
|
|
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->dc_psy.name = "dc";
|
|
isl_chg->dc_psy.type = POWER_SUPPLY_TYPE_MAINS;
|
|
isl_chg->dc_psy.supplied_to = pm_power_supplied_to;
|
|
isl_chg->dc_psy.num_supplicants = ARRAY_SIZE(pm_power_supplied_to);
|
|
isl_chg->dc_psy.properties = pm_power_props;
|
|
isl_chg->dc_psy.num_properties = ARRAY_SIZE(pm_power_props);
|
|
isl_chg->dc_psy.get_property = pm_power_get_property;
|
|
isl_chg->dc_psy.set_property = pm_power_set_property;
|
|
|
|
ret = power_supply_register(&isl_chg->client->dev, &isl_chg->dc_psy);
|
|
if (ret) {
|
|
pr_err("failed to register dc charger.ret=%d.\n", 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, ®_fops);
|
|
debugfs_create_file("MAX_SYS_VOLTAGE_REG", 0644, isl_chg->dent,
|
|
(void *) MAX_SYS_VOLTAGE_REG, ®_fops);
|
|
debugfs_create_file("CONTROL_REG", 0644, isl_chg->dent,
|
|
(void *) CONTROL_REG, ®_fops);
|
|
debugfs_create_file("MIN_SYS_VOLTAGE_REG", 0644, isl_chg->dent,
|
|
(void *) MIN_SYS_VOLTAGE_REG, ®_fops);
|
|
debugfs_create_file("INPUT_CURRENT_REG", 0644, isl_chg->dent,
|
|
(void *) INPUT_CURRENT_REG, ®_fops);
|
|
debugfs_create_file("MANUFACTURER_ID_REG", 0644, isl_chg->dent,
|
|
(void *) MANUFACTURER_ID_REG, ®_fops);
|
|
debugfs_create_file("DEVICE_ID_REG", 0644, isl_chg->dent,
|
|
(void *) DEVICE_ID_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_hwinit(struct isl9519q_struct *isl_chg)
|
|
{
|
|
int ret;
|
|
|
|
ret = isl9519q_write_reg(isl_chg->client, MAX_SYS_VOLTAGE_REG,
|
|
isl_chg->max_system_voltage);
|
|
if (ret) {
|
|
pr_err("Failed to set MAX_SYS_VOLTAGE rc=%d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
ret = isl9519q_write_reg(isl_chg->client, MIN_SYS_VOLTAGE_REG,
|
|
isl_chg->min_system_voltage);
|
|
if (ret) {
|
|
pr_err("Failed to set MIN_SYS_VOLTAGE rc=%d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
if (isl_chg->input_current) {
|
|
ret = isl9519q_write_reg(isl_chg->client,
|
|
INPUT_CURRENT_REG,
|
|
isl_chg->input_current);
|
|
if (ret) {
|
|
pr_err("Failed to set INPUT_CURRENT rc=%d\n", ret);
|
|
return ret;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
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_debug("\n");
|
|
|
|
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_hwinit(isl_chg);
|
|
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("OK.\n");
|
|
|
|
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);
|
|
if (isl_chg->notify_by_pmic) {
|
|
power_supply_unregister(&isl_chg->dc_psy);
|
|
} else {
|
|
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)
|
|
{
|
|
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");
|