/* 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #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 "); MODULE_DESCRIPTION("Driver for ISL9519Q Charger chip"); MODULE_LICENSE("GPL v2");