| /* |
| * max77665-charger.c - Battery charger driver |
| * |
| * Copyright (c) 2012-2013, NVIDIA CORPORATION. All rights reserved. |
| * Syed Rafiuddin <srafiuddin@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/err.h> |
| #include <linux/module.h> |
| #include <linux/slab.h> |
| #include <linux/platform_device.h> |
| #include <linux/power_supply.h> |
| #include <linux/alarmtimer.h> |
| #include <linux/timer.h> |
| #include <linux/wakelock.h> |
| #include <linux/mfd/max77665.h> |
| #include <linux/max77665-charger.h> |
| #include <linux/power/max17042_battery.h> |
| #include <linux/interrupt.h> |
| |
| #define CHARGER_TYPE_DETECTION_DEBOUNCE_TIME_MS 500 |
| |
| /* fast charge current in mA */ |
| static const uint32_t chg_cc[] = { |
| 0, 33, 66, 99, 133, 166, 199, 233, 266, 299, |
| 333, 366, 399, 432, 466, 499, 532, 566, 599, 632, |
| 666, 699, 732, 765, 799, 832, 865, 899, 932, 965, |
| 999, 1032, 1065, 1098, 1132, 1165, 1198, 1232, 1265, |
| 1298, 1332, 1365, 1398, 1421, 1465, 1498, 1531, 1565, |
| 1598, 1631, 1665, 1698, 1731, 1764, 1798, 1831, 1864, |
| 1898, 1931, 1964, 1998, 2031, 2064, 2097, |
| }; |
| |
| /* primary charge termination voltage in mV */ |
| static const uint32_t chg_cv_prm[] = { |
| 3650, 3675, 3700, 3725, 3750, |
| 3775, 3800, 3825, 3850, 3875, |
| 3900, 3925, 3950, 3975, 4000, |
| 4025, 4050, 4075, 4100, 4125, |
| 4150, 4175, 4200, 4225, 4250, |
| 4275, 4300, 4325, 4340, 4350, |
| 4375, 4400, |
| }; |
| |
| /* maxim input current limit in mA*/ |
| static const uint32_t chgin_ilim[] = { |
| 0, 100, 200, 300, 400, 500, 600, 700, |
| 800, 900, 1000, 1100, 1200, 1300, 1400, |
| 1500, 1600, 1700, 1800, 1900, 2000, 2100, |
| 2200, 2300, 2400, 2500, |
| }; |
| |
| struct max77665_charger { |
| struct device *dev; |
| struct power_supply ac; |
| struct power_supply usb; |
| struct max77665_charger_plat_data *plat_data; |
| uint8_t ac_online; |
| uint8_t usb_online; |
| uint8_t num_cables; |
| struct extcon_dev *edev; |
| struct alarm wdt_alarm; |
| struct delayed_work wdt_ack_work; |
| struct wake_lock wdt_wake_lock; |
| }; |
| |
| static enum power_supply_property max77665_ac_props[] = { |
| POWER_SUPPLY_PROP_ONLINE, |
| }; |
| |
| static enum power_supply_property max77665_usb_props[] = { |
| POWER_SUPPLY_PROP_ONLINE, |
| }; |
| |
| static int max77665_write_reg(struct max77665_charger *charger, |
| uint8_t reg, uint8_t value) |
| { |
| int ret = 0; |
| struct device *dev = charger->dev; |
| |
| ret = max77665_write(dev->parent, MAX77665_I2C_SLAVE_PMIC, reg, value); |
| if (ret < 0) |
| dev_err(charger->dev, "Failed to write to reg 0x%x\n", reg); |
| return ret; |
| } |
| |
| static int max77665_read_reg(struct max77665_charger *charger, |
| uint8_t reg, uint32_t *value) |
| { |
| int ret = 0; |
| uint8_t read_val; |
| |
| struct device *dev = charger->dev; |
| |
| ret = max77665_read(dev->parent, MAX77665_I2C_SLAVE_PMIC, |
| reg, &read_val); |
| if (!ret) |
| *value = read_val; |
| |
| return ret; |
| } |
| |
| static int max77665_update_reg(struct max77665_charger *charger, |
| uint8_t reg, uint8_t value) |
| { |
| int ret = 0; |
| uint8_t read_val; |
| struct device *dev = charger->dev; |
| |
| ret = max77665_read(dev->parent, MAX77665_I2C_SLAVE_PMIC, |
| reg, &read_val); |
| if (ret) |
| return ret; |
| |
| ret = max77665_write(dev->parent, MAX77665_I2C_SLAVE_PMIC, reg, |
| read_val | value); |
| if (ret) |
| return ret; |
| |
| return ret; |
| } |
| |
| /* Convert current to register value using lookup table */ |
| static int convert_to_reg(const unsigned int *tbl, size_t size, |
| unsigned int val) |
| { |
| size_t i; |
| |
| for (i = 0; i < size; i++) |
| if (val < tbl[i]) |
| break; |
| return i > 0 ? i - 1 : -EINVAL; |
| } |
| |
| static int max77665_ac_get_property(struct power_supply *psy, |
| enum power_supply_property psp, |
| union power_supply_propval *val) |
| { |
| struct max77665_charger *chip = container_of(psy, |
| struct max77665_charger, ac); |
| |
| if (psp == POWER_SUPPLY_PROP_ONLINE) |
| val->intval = chip->ac_online; |
| else |
| return -EINVAL; |
| |
| return 0; |
| } |
| |
| static int max77665_usb_get_property(struct power_supply *psy, |
| enum power_supply_property psp, |
| union power_supply_propval *val) |
| { |
| struct max77665_charger *chip = container_of(psy, |
| struct max77665_charger, usb); |
| |
| if (psp == POWER_SUPPLY_PROP_ONLINE) |
| val->intval = chip->usb_online; |
| else |
| return -EINVAL; |
| |
| return 0; |
| } |
| |
| static int max77665_enable_write(struct max77665_charger *charger, int access) |
| { |
| int ret = 0; |
| |
| if (access) |
| /* enable write acces to registers */ |
| ret = max77665_write_reg(charger, MAX77665_CHG_CNFG_06, 0x0c); |
| else |
| /* Disable write acces to registers */ |
| ret = max77665_write_reg(charger, MAX77665_CHG_CNFG_06, 0x00); |
| return ret; |
| } |
| |
| static int max77665_charger_enable(struct max77665_charger *charger, |
| enum max77665_mode mode) |
| { |
| int ret; |
| int flags; |
| |
| ret = max77665_enable_write(charger, true); |
| if (ret < 0) { |
| dev_err(charger->dev, "failed to enable write acess\n"); |
| return ret; |
| } |
| |
| if (mode == CHARGER) { |
| /* enable charging */ |
| flags = CHARGER_ON_OTG_OFF_BUCK_OFF_BOOST_ON | WDTEN; |
| ret = max77665_write_reg(charger, MAX77665_CHG_CNFG_00, flags); |
| if (ret < 0) |
| return ret; |
| } else if (mode == OTG) { |
| /* enable OTG mode */ |
| flags = CHARGER_OFF_OTG_ON_BUCK_OFF_BOOST_ON; |
| ret = max77665_write_reg(charger, MAX77665_CHG_CNFG_00, flags); |
| if (ret < 0) |
| return ret; |
| } |
| |
| ret = max77665_enable_write(charger, false); |
| if (ret < 0) { |
| dev_err(charger->dev, "failed to disable write acess\n"); |
| return ret; |
| } |
| return 0; |
| } |
| |
| static int max77665_charger_init(struct max77665_charger *charger) |
| { |
| int ret = 0; |
| |
| ret = max77665_enable_write(charger, true); |
| if (ret < 0) { |
| dev_err(charger->dev, "failed to enable write acess\n"); |
| goto error; |
| } |
| |
| ret = max77665_update_reg(charger, MAX77665_CHG_CNFG_01, 0xa4); |
| if (ret < 0) { |
| dev_err(charger->dev, "Failed in writing to register 0x%x\n", |
| MAX77665_CHG_CNFG_01); |
| goto error; |
| } |
| |
| if (charger->plat_data->fast_chg_cc) { |
| ret = convert_to_reg(chg_cc, ARRAY_SIZE(chg_cc), |
| charger->plat_data->fast_chg_cc); |
| if (ret < 0) |
| goto error; |
| |
| ret = max77665_update_reg(charger, MAX77665_CHG_CNFG_02, ret); |
| if (ret < 0) { |
| dev_err(charger->dev, "Failed in writing to register 0x%x\n", |
| MAX77665_CHG_CNFG_02); |
| goto error; |
| } |
| } |
| |
| if (charger->plat_data->term_volt) { |
| ret = convert_to_reg(chg_cv_prm, ARRAY_SIZE(chg_cv_prm), |
| charger->plat_data->term_volt); |
| if (ret < 0) |
| goto error; |
| |
| ret = max77665_update_reg(charger, |
| MAX77665_CHG_CNFG_04, ret+1); |
| if (ret < 0) { |
| dev_err(charger->dev, "Failed writing to reg:0x%x\n", |
| MAX77665_CHG_CNFG_04); |
| goto error; |
| } |
| } |
| |
| if (charger->plat_data->curr_lim) { |
| ret = convert_to_reg(chgin_ilim, ARRAY_SIZE(chgin_ilim), |
| charger->plat_data->curr_lim); |
| if (ret < 0) |
| goto error; |
| |
| ret = max77665_write_reg(charger, |
| MAX77665_CHG_CNFG_09, ret*5); |
| if (ret < 0) |
| goto error; |
| } |
| error: |
| ret = max77665_enable_write(charger, false); |
| if (ret < 0) { |
| dev_err(charger->dev, "failed to enable write acess\n"); |
| return ret; |
| } |
| return ret; |
| } |
| |
| static int max77665_enable_charger(struct max77665_charger *charger, |
| struct extcon_dev *edev) |
| { |
| int ret = 0; |
| uint32_t val = 0; |
| int ilim; |
| |
| charger->usb_online = 0; |
| charger->ac_online = 0; |
| |
| ret = max77665_read_reg(charger, MAX77665_CHG_CNFG_09, &val); |
| if (0 > ret) |
| return ret; |
| val &= 0x7F; |
| ilim = max_t(int, 60, val * 20); |
| |
| if (charger->plat_data->update_status) |
| charger->plat_data->update_status(false); |
| |
| if (true == extcon_get_cable_state(edev, "USB-Host")) { |
| ret = max77665_charger_enable(charger, OTG); |
| if (0 > ret) |
| dev_err(charger->dev, |
| "failed to set device to USB-host mode"); |
| power_supply_changed(&charger->usb); |
| power_supply_changed(&charger->ac); |
| return ret; |
| } |
| |
| if (true == extcon_get_cable_state(edev, "USB")) { |
| charger->usb_online = 1; |
| charger->plat_data->curr_lim = 500; |
| } else if (true == extcon_get_cable_state(edev, "Charge-downstream")) { |
| charger->usb_online = 1; |
| } else if (true == extcon_get_cable_state(edev, "TA")) { |
| charger->ac_online = 1; |
| } else if (true == extcon_get_cable_state(edev, "Fast-charger")) { |
| charger->ac_online = 1; |
| } else if (true == extcon_get_cable_state(edev, "Slow-charger")) { |
| charger->ac_online = 1; |
| charger->plat_data->curr_lim = 500; |
| } else { |
| /* no cable connected */ |
| return 0; |
| } |
| |
| power_supply_changed(charger->usb_online ? |
| &charger->usb : &charger->ac); |
| |
| if (charger->plat_data->update_status) |
| charger->plat_data->update_status(ilim); |
| |
| ret = max77665_charger_enable(charger, CHARGER); |
| if (ret < 0) { |
| dev_err(charger->dev, "failed to set device to charger mode\n"); |
| return ret; |
| } |
| |
| ret = max77665_charger_init(charger); |
| if (ret < 0) { |
| dev_err(charger->dev, "failed to initialize charger\n"); |
| return ret; |
| } |
| |
| /* set the charging watchdog timer */ |
| alarm_start(&charger->wdt_alarm, ktime_add(ktime_get_boottime(), |
| ktime_set(MAX77665_WATCHDOG_TIMER_PERIOD_S / 2, 0))); |
| |
| return 0; |
| } |
| |
| static void charger_extcon_handle_notifier(struct work_struct *w) |
| { |
| struct max77665_charger_cable *cable = container_of(to_delayed_work(w), |
| struct max77665_charger_cable, extcon_notifier_work); |
| |
| if (cable->event == 0) { |
| cable->charger->ac_online = 0; |
| cable->charger->usb_online = 0; |
| if (cable->charger->plat_data->update_status) |
| cable->charger->plat_data->update_status(0); |
| power_supply_changed(&cable->charger->usb); |
| power_supply_changed(&cable->charger->ac); |
| } else { |
| max77665_enable_charger(cable->charger, |
| cable->extcon_dev->edev); |
| } |
| } |
| |
| static void max77665_charger_wdt_ack_work_handler(struct work_struct *w) |
| { |
| struct max77665_charger *charger = container_of(to_delayed_work(w), |
| struct max77665_charger, wdt_ack_work); |
| |
| if (0 > max77665_update_reg(charger, MAX77665_CHG_CNFG_06, WDTCLR)) |
| dev_err(charger->dev, "fail to ack charging WDT\n"); |
| |
| alarm_start(&charger->wdt_alarm, |
| ktime_add(ktime_get_boottime(), ktime_set(30, 0))); |
| wake_unlock(&charger->wdt_wake_lock); |
| } |
| |
| static enum alarmtimer_restart max77665_charger_wdt_timer(struct alarm *alarm, |
| ktime_t now) |
| { |
| struct max77665_charger *charger = |
| container_of(alarm, struct max77665_charger, wdt_alarm); |
| |
| wake_lock(&charger->wdt_wake_lock); |
| schedule_delayed_work(&charger->wdt_ack_work, 0); |
| return ALARMTIMER_NORESTART; |
| } |
| |
| static void max77665_charger_disable_wdt(struct max77665_charger *charger) |
| { |
| cancel_delayed_work_sync(&charger->wdt_ack_work); |
| alarm_cancel(&charger->wdt_alarm); |
| } |
| |
| static int charger_extcon_notifier(struct notifier_block *self, |
| unsigned long event, void *ptr) |
| { |
| struct max77665_charger_cable *cable = container_of(self, |
| struct max77665_charger_cable, nb); |
| |
| cable->event = event; |
| cancel_delayed_work(&cable->extcon_notifier_work); |
| schedule_delayed_work(&cable->extcon_notifier_work, |
| msecs_to_jiffies(CHARGER_TYPE_DETECTION_DEBOUNCE_TIME_MS)); |
| |
| return NOTIFY_DONE; |
| } |
| |
| static int max77665_display_charger_status(struct max77665_charger *charger, |
| uint32_t val) |
| { |
| int i; |
| int bits[] = { BYP_OK, DETBAT_OK, BAT_OK, CHG_OK, CHGIN_OK }; |
| char *info[] = { |
| "bypass", |
| "main battery presence", |
| "battery", |
| "charger", |
| "charging input" |
| }; |
| |
| for (i = 0; i < ARRAY_SIZE(bits); i++) |
| dev_err(charger->dev, "%s %s OK", info[i], |
| (val & bits[i]) ? "is" : "is not"); |
| |
| return 0; |
| } |
| |
| static int max77665_update_charger_status(struct max77665_charger *charger) |
| { |
| int ret; |
| uint32_t read_val; |
| |
| ret = max77665_read_reg(charger, MAX77665_CHG_INT, &read_val); |
| if (ret < 0) { |
| dev_err(charger->dev, "failed in reading register: 0x%x\n", |
| MAX77665_CHG_INT); |
| goto error; |
| } |
| |
| ret = max77665_read_reg(charger, MAX77665_CHG_INT_OK, &read_val); |
| if (ret < 0) { |
| dev_err(charger->dev, "failed to reading reg: 0x%x\n", |
| MAX77665_CHG_INT_OK); |
| goto error; |
| } |
| max77665_display_charger_status(charger, read_val); |
| |
| ret = max77665_write_reg(charger, MAX77665_CHG_INT_MASK, 0x0a); |
| error: |
| return ret; |
| } |
| |
| static irqreturn_t max77665_charger_irq_handler(int irq, void *data) |
| { |
| struct max77665_charger *charger = data; |
| int ret; |
| ret = max77665_update_charger_status(charger); |
| return IRQ_HANDLED; |
| } |
| |
| static __devinit int max77665_battery_probe(struct platform_device *pdev) |
| { |
| int ret = 0; |
| uint8_t j; |
| uint32_t read_val; |
| struct max77665_charger *charger; |
| |
| charger = devm_kzalloc(&pdev->dev, sizeof(*charger), GFP_KERNEL); |
| if (!charger) { |
| dev_err(&pdev->dev, "failed to allocate memory status\n"); |
| return -ENOMEM; |
| } |
| |
| charger->dev = &pdev->dev; |
| |
| charger->plat_data = pdev->dev.platform_data; |
| dev_set_drvdata(&pdev->dev, charger); |
| |
| /* modify OTP setting of input current limit to 100ma */ |
| ret = max77665_write_reg(charger, MAX77665_CHG_CNFG_09, 0x05); |
| if (ret < 0) |
| goto error; |
| |
| /* check for battery presence */ |
| ret = max77665_read_reg(charger, MAX77665_CHG_DTLS_01, &read_val); |
| if (ret) { |
| dev_err(&pdev->dev, "error in reading register 0x%x\n", |
| MAX77665_CHG_DTLS_01); |
| return -ENODEV; |
| } else if (!(read_val & 0xe0)) { |
| dev_err(&pdev->dev, "Battery not detected exiting driver..\n"); |
| return -ENODEV; |
| } |
| |
| /* differentiate between E1236 and E1587*/ |
| ret = maxim_get_temp(); |
| if (ret == 0xff) { |
| dev_err(&pdev->dev, "failed in reading temperaure\n"); |
| return -ENODEV; |
| } else if ((ret < MIN_TEMP) || (ret > MAX_TEMP)) { |
| dev_err(&pdev->dev, "E1236 detected exiting driver....\n"); |
| return -ENODEV; |
| } |
| |
| ret = max77665_write_reg(charger, MAX77665_CHG_INT_MASK, 0x0a); |
| if (ret < 0) |
| goto error; |
| |
| charger->ac.name = "ac"; |
| charger->ac.type = POWER_SUPPLY_TYPE_MAINS; |
| charger->ac.get_property = max77665_ac_get_property; |
| charger->ac.properties = max77665_ac_props; |
| charger->ac.num_properties = ARRAY_SIZE(max77665_ac_props); |
| |
| ret = power_supply_register(charger->dev, &charger->ac); |
| if (ret) { |
| dev_err(charger->dev, "failed: power supply register\n"); |
| goto error; |
| } |
| |
| charger->usb.name = "usb"; |
| charger->usb.type = POWER_SUPPLY_TYPE_USB; |
| charger->usb.get_property = max77665_usb_get_property; |
| charger->usb.properties = max77665_usb_props; |
| charger->usb.num_properties = ARRAY_SIZE(max77665_usb_props); |
| |
| ret = power_supply_register(charger->dev, &charger->usb); |
| if (ret) { |
| dev_err(charger->dev, "failed: power supply register\n"); |
| goto pwr_sply_error; |
| } |
| |
| for (j = 0 ; j < charger->plat_data->num_cables; j++) { |
| struct max77665_charger_cable *cable = |
| &charger->plat_data->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, "failed to allocate memory for extcon_dev\n"); |
| return -ENOMEM; |
| } |
| |
| INIT_DELAYED_WORK(&cable->extcon_notifier_work, |
| charger_extcon_handle_notifier); |
| |
| cable->charger = charger; |
| cable->nb.notifier_call = charger_extcon_notifier; |
| ret = extcon_register_interest(cable->extcon_dev, |
| charger->plat_data->extcon_name, |
| cable->name, &cable->nb); |
| |
| if (ret < 0) { |
| dev_err(charger->dev, "Cannot register for cable: %s\n", |
| cable->name); |
| ret = -EINVAL; |
| } |
| } |
| |
| charger->edev = extcon_get_extcon_dev(charger->plat_data->extcon_name); |
| if (!charger->edev) |
| return -ENODEV; |
| |
| if (charger->plat_data->irq_base) { |
| ret = devm_request_threaded_irq(&pdev->dev, |
| charger->plat_data->irq_base + MAX77665_IRQ_CHARGER, |
| NULL, max77665_charger_irq_handler, |
| 0, "charger_irq", charger); |
| if (ret) { |
| dev_err(&pdev->dev, |
| "failed: irq request error :%d)\n", ret); |
| goto chrg_error; |
| } |
| } |
| |
| ret = max77665_enable_charger(charger, charger->edev); |
| if (ret < 0) { |
| dev_err(charger->dev, "failed to enable charger\n"); |
| goto chrg_error; |
| } |
| |
| wake_lock_init(&charger->wdt_wake_lock, WAKE_LOCK_SUSPEND, |
| "max77665-charger-wdt"); |
| alarm_init(&charger->wdt_alarm, ALARM_BOOTTIME, |
| max77665_charger_wdt_timer); |
| INIT_DELAYED_WORK(&charger->wdt_ack_work, |
| max77665_charger_wdt_ack_work_handler); |
| |
| return 0; |
| |
| chrg_error: |
| power_supply_unregister(&charger->usb); |
| pwr_sply_error: |
| power_supply_unregister(&charger->ac); |
| error: |
| return ret; |
| } |
| |
| static int __devexit max77665_battery_remove(struct platform_device *pdev) |
| { |
| struct max77665_charger *charger = platform_get_drvdata(pdev); |
| |
| power_supply_unregister(&charger->ac); |
| power_supply_unregister(&charger->usb); |
| |
| return 0; |
| } |
| #ifdef CONFIG_PM_SLEEP |
| static int max77665_suspend(struct device *dev) |
| { |
| return 0; |
| } |
| static int max77665_resume(struct device *dev) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct max77665_charger *charger = platform_get_drvdata(pdev); |
| int ret; |
| ret = max77665_update_charger_status(charger); |
| if (ret < 0) |
| dev_err(charger->dev, "error occured in resume\n"); |
| return ret; |
| } |
| |
| static const struct dev_pm_ops max77665_pm = { |
| .suspend = max77665_suspend, |
| .resume = max77665_resume, |
| }; |
| #define MAX77665_PM (&max77665_pm) |
| #else |
| #define MAX77665_PM NULL |
| #endif |
| static struct platform_driver max77665_battery_driver = { |
| .driver = { |
| .name = "max77665-charger", |
| .owner = THIS_MODULE, |
| .pm = MAX77665_PM, |
| }, |
| .probe = max77665_battery_probe, |
| .remove = __devexit_p(max77665_battery_remove), |
| |
| }; |
| |
| static int __init max77665_battery_init(void) |
| { |
| return platform_driver_register(&max77665_battery_driver); |
| } |
| |
| static void __exit max77665_battery_exit(void) |
| { |
| platform_driver_unregister(&max77665_battery_driver); |
| } |
| |
| late_initcall(max77665_battery_init); |
| module_exit(max77665_battery_exit); |
| |
| MODULE_DESCRIPTION("MAXIM MAX77665 battery charging driver"); |
| MODULE_AUTHOR("Syed Rafiuddin <srafiuddin@nvidia.com>"); |
| MODULE_LICENSE("GPL v2"); |