| /* Copyright (c) 2014 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) "SMB349-dual %s: " fmt, __func__ |
| |
| #include <linux/i2c.h> |
| #include <linux/debugfs.h> |
| #include <linux/gpio.h> |
| #include <linux/errno.h> |
| #include <linux/delay.h> |
| #include <linux/module.h> |
| #include <linux/interrupt.h> |
| #include <linux/slab.h> |
| #include <linux/power_supply.h> |
| #include <linux/regulator/driver.h> |
| #include <linux/regulator/of_regulator.h> |
| #include <linux/regulator/machine.h> |
| #include <linux/of.h> |
| #include <linux/of_gpio.h> |
| #include <linux/mutex.h> |
| #include "smb349-charger.h" |
| |
| #define LOW_CHARGE_DELAY_MS 2000 |
| #define PERIODIC_DELAY_MS 120000 |
| #define LOW_CHARGE_CURRENT_MA 200 |
| #define SMB_RECHARGE_THRESHOLD_MV 50 |
| #define DELAY_IRQ_LEVEL_MS 20 |
| |
| enum wake_reason { |
| PM_SMB349_IRQ_HANDLING = BIT(0), |
| PM_SMB349_DC_INSERTED = BIT(1), |
| }; |
| |
| struct smb349_dual_charger { |
| struct i2c_client *client; |
| struct device *dev; |
| |
| enum wake_reason wake_reasons; |
| bool recharge_disabled; |
| int recharge_mv; |
| bool iterm_disabled; |
| int iterm_ma; |
| int vfloat_mv; |
| int chg_stat_gpio; |
| int chg_present; |
| const char *ext_psy_name; |
| |
| bool charging_disabled; |
| int fastchg_current_max_ma; |
| |
| struct delayed_work irq_handler_dwork; |
| struct delayed_work periodic_charge_handler_dwork; |
| |
| struct power_supply *ext_psy; |
| struct power_supply cradle_psy; |
| |
| struct mutex irq_complete; |
| struct mutex pm_lock; |
| |
| struct dentry *debug_root; |
| u32 peek_poke_address; |
| }; |
| |
| struct smb_irq_info { |
| const char *name; |
| int (*smb_irq)(struct smb349_dual_charger *chip, u8 rt_stat); |
| int high; |
| int low; |
| }; |
| |
| struct irq_handler_info { |
| u8 stat_reg; |
| u8 val; |
| u8 prev_val; |
| struct smb_irq_info irq_info[4]; |
| }; |
| |
| struct chg_current_map { |
| int chg_current_ma; |
| u8 value; |
| }; |
| |
| static int smb349_read_reg(struct smb349_dual_charger *chip, int reg, u8 *val) |
| { |
| s32 ret; |
| |
| ret = i2c_smbus_read_byte_data(chip->client, reg); |
| if (ret < 0) { |
| dev_err(chip->dev, |
| "i2c read fail: can't read from %02x: %d\n", reg, ret); |
| return ret; |
| } else { |
| *val = ret; |
| } |
| |
| return 0; |
| } |
| |
| static int smb349_write_reg(struct smb349_dual_charger *chip, int reg, u8 val) |
| { |
| s32 ret; |
| |
| ret = i2c_smbus_write_byte_data(chip->client, reg, val); |
| if (ret < 0) { |
| dev_err(chip->dev, |
| "i2c write fail: can't write %02x to %02x: %d\n", |
| val, reg, ret); |
| return ret; |
| } |
| return 0; |
| } |
| |
| static int smb349_masked_write(struct smb349_dual_charger *chip, int reg, |
| u8 mask, u8 val) |
| { |
| s32 rc; |
| u8 temp; |
| |
| rc = smb349_read_reg(chip, reg, &temp); |
| if (rc) { |
| dev_err(chip->dev, |
| "smb349_read_reg Failed: reg=%03X, rc=%d\n", reg, rc); |
| return rc; |
| } |
| temp &= ~mask; |
| temp |= val & mask; |
| rc = smb349_write_reg(chip, reg, temp); |
| if (rc) { |
| dev_err(chip->dev, |
| "smb349_write Failed: reg=%03X, rc=%d\n", reg, rc); |
| return rc; |
| } |
| return 0; |
| } |
| |
| static int smb349_enable_volatile_writes(struct smb349_dual_charger *chip) |
| { |
| int rc; |
| |
| rc = smb349_masked_write(chip, CMD_A_REG, CMD_A_VOLATILE_W_PERM_BIT, |
| CMD_A_VOLATILE_W_PERM_BIT); |
| if (rc) |
| dev_err(chip->dev, "Couldn't write VOLATILE_W_PERM_BIT rc=%d\n", |
| rc); |
| |
| return rc; |
| } |
| |
| static int smb349_fastchg_current_set(struct smb349_dual_charger *chip) |
| { |
| u8 temp; |
| |
| if ((chip->fastchg_current_max_ma < SMB349_FAST_CHG_MIN_MA) || |
| (chip->fastchg_current_max_ma > SMB349_FAST_CHG_MAX_MA)) { |
| dev_dbg(chip->dev, "bad fastchg current mA=%d asked to set\n", |
| chip->fastchg_current_max_ma); |
| return -EINVAL; |
| } |
| |
| temp = (chip->fastchg_current_max_ma - SMB349_FAST_CHG_MIN_MA) |
| / SMB349_FAST_CHG_STEP_MA; |
| |
| temp = temp << SMB349_FAST_CHG_SHIFT; |
| dev_dbg(chip->dev, "fastchg limit=%d setting %02x\n", |
| chip->fastchg_current_max_ma, temp); |
| |
| return smb349_masked_write(chip, CHG_CURRENT_CTRL_REG, |
| SMB_FAST_CHG_CURRENT_MASK, temp); |
| } |
| |
| static int smb349_float_voltage_set(struct smb349_dual_charger *chip, |
| int vfloat_mv) |
| { |
| u8 temp; |
| |
| if ((vfloat_mv < MIN_FLOAT_MV) || (vfloat_mv > MAX_FLOAT_MV)) { |
| dev_err(chip->dev, "bad float voltage mv =%d asked to set\n", |
| vfloat_mv); |
| return -EINVAL; |
| } |
| |
| temp = (vfloat_mv - MIN_FLOAT_MV) / VFLOAT_STEP_MV; |
| |
| return smb349_masked_write(chip, VFLOAT_REG, VFLOAT_MASK, temp); |
| } |
| |
| static int smb349_hw_init(struct smb349_dual_charger *chip) |
| { |
| int rc; |
| u8 reg = 0; |
| |
| rc = smb349_enable_volatile_writes(chip); |
| if (rc) { |
| dev_err(chip->dev, "Couldn't configure volatile writes rc=%d\n", |
| rc); |
| return rc; |
| } |
| |
| /* overwrite OTP defaults */ |
| rc = smb349_masked_write(chip, CHG_CURRENT_CTRL_REG, 0xFF, 0x5F); |
| if (rc) { |
| dev_err(chip->dev, "Couldn't set current control rc=%d\n", |
| rc); |
| return rc; |
| } |
| rc = smb349_masked_write(chip, THERM_A_CTRL_REG, 0xFF, 0xCF); |
| if (rc) { |
| dev_err(chip->dev, "Couldn't set thermal control rc=%d\n", |
| rc); |
| return rc; |
| } |
| rc = smb349_masked_write(chip, OTG_TLIM_THERM_CTRL_REG, 0xFF, 0x5B); |
| if (rc) { |
| dev_err(chip->dev, "Couldn't set otg thermal limits rc=%d\n", |
| rc); |
| return rc; |
| } |
| rc = smb349_masked_write(chip, HARD_SOFT_TLIM_CTRL_REG, 0xFF, 0x23); |
| if (rc) { |
| dev_err(chip->dev, "Couldn't set thermal limits rc=%d\n", |
| rc); |
| return rc; |
| } |
| |
| /* set the fast charge current limit */ |
| rc = smb349_fastchg_current_set(chip); |
| if (rc) { |
| dev_err(chip->dev, "Couldn't set fastchg current rc=%d\n", rc); |
| return rc; |
| } |
| |
| /* set the float voltage */ |
| if (chip->vfloat_mv != -EINVAL) { |
| rc = smb349_float_voltage_set(chip, chip->vfloat_mv); |
| if (rc < 0) { |
| dev_err(chip->dev, |
| "Couldn't set float voltage rc = %d\n", rc); |
| return rc; |
| } |
| } |
| |
| /* set iterm */ |
| if (chip->iterm_ma != -EINVAL) { |
| if (chip->iterm_disabled) { |
| dev_err(chip->dev, "Error: Both iterm_disabled and iterm_ma set\n"); |
| return -EINVAL; |
| } else { |
| if (chip->iterm_ma <= 100) |
| reg = CHG_ITERM_100MA; |
| else if (chip->iterm_ma <= 200) |
| reg = CHG_ITERM_200MA; |
| else if (chip->iterm_ma <= 300) |
| reg = CHG_ITERM_300MA; |
| else if (chip->iterm_ma <= 400) |
| reg = CHG_ITERM_400MA; |
| else if (chip->iterm_ma <= 500) |
| reg = CHG_ITERM_500MA; |
| else if (chip->iterm_ma <= 600) |
| reg = CHG_ITERM_600MA; |
| else |
| reg = CHG_ITERM_700MA; |
| |
| rc = smb349_masked_write(chip, CHG_OTH_CURRENT_CTRL_REG, |
| CHG_ITERM_MASK, reg); |
| if (rc) { |
| dev_err(chip->dev, |
| "Couldn't set iterm rc = %d\n", rc); |
| return rc; |
| } |
| |
| rc = smb349_masked_write(chip, CHG_CTRL_REG, |
| CHG_CTRL_CURR_TERM_END_MASK, 0); |
| if (rc) { |
| dev_err(chip->dev, |
| "Couldn't enable iterm rc = %d\n", rc); |
| return rc; |
| } |
| } |
| } else if (chip->iterm_disabled) { |
| rc = smb349_masked_write(chip, CHG_CTRL_REG, |
| CHG_CTRL_CURR_TERM_END_MASK, |
| CHG_CTRL_CURR_TERM_END_MASK); |
| if (rc) { |
| dev_err(chip->dev, "Couldn't set iterm rc = %d\n", |
| rc); |
| return rc; |
| } |
| } |
| |
| /* set recharge-threshold */ |
| if (chip->recharge_mv != -EINVAL) { |
| if (chip->recharge_disabled) { |
| dev_err(chip->dev, "Error: Both recharge_disabled and recharge_mv set\n"); |
| return -EINVAL; |
| } else { |
| reg = 0; |
| if (chip->recharge_mv > SMB_RECHARGE_THRESHOLD_MV) |
| reg = CHG_CTRL_RECHG_100MV_BIT; |
| |
| rc = smb349_masked_write(chip, CHG_CTRL_REG, |
| CHG_CTRL_RECHG_50_100_MASK | |
| CHG_CTRL_AUTO_RECHARGE_MASK, reg); |
| if (rc) { |
| dev_err(chip->dev, |
| "Couldn't set rechg-cfg rc = %d\n", rc); |
| return rc; |
| } |
| } |
| } else if (chip->recharge_disabled) { |
| rc = smb349_masked_write(chip, CHG_CTRL_REG, |
| CHG_CTRL_AUTO_RECHARGE_MASK, |
| CHG_CTRL_AUTO_RECHARGE_MASK); |
| if (rc) { |
| dev_err(chip->dev, |
| "Couldn't disable auto-rechg rc = %d\n", rc); |
| return rc; |
| } |
| } |
| |
| /* enable/disable charging */ |
| rc = smb349_masked_write(chip, CMD_A_REG, CMD_A_CHG_ENABLE_BIT, |
| chip->charging_disabled ? 0 : CMD_A_CHG_ENABLE_BIT); |
| if (rc) { |
| dev_err(chip->dev, "Unable to %s charging. rc=%d\n", |
| chip->charging_disabled ? "disable" : "enable", rc); |
| } |
| |
| return rc; |
| } |
| |
| static enum power_supply_property smb349_cradle_properties[] = { |
| POWER_SUPPLY_PROP_STATUS, |
| POWER_SUPPLY_PROP_ONLINE, |
| POWER_SUPPLY_PROP_PRESENT, |
| POWER_SUPPLY_PROP_CHARGING_ENABLED, |
| POWER_SUPPLY_PROP_CHARGE_TYPE, |
| POWER_SUPPLY_PROP_TECHNOLOGY, |
| POWER_SUPPLY_PROP_MODEL_NAME, |
| }; |
| |
| static int smb349_get_prop_cradle_status(struct smb349_dual_charger *chip) |
| { |
| int rc; |
| u8 reg = 0; |
| |
| if (chip->chg_present && !chip->charging_disabled) { |
| rc = smb349_read_reg(chip, STATUS_C_REG, ®); |
| if (rc) { |
| dev_dbg(chip->dev, "Couldn't read STAT_C rc = %d\n", |
| rc); |
| return POWER_SUPPLY_STATUS_UNKNOWN; |
| } |
| |
| dev_dbg(chip->dev, "%s: STATUS_C_REG=%x\n", __func__, reg); |
| if ((reg & STATUS_C_CHARGING_MASK) && |
| !(reg & STATUS_C_CHG_ERR_STATUS_BIT)) |
| return POWER_SUPPLY_STATUS_CHARGING; |
| } |
| |
| return POWER_SUPPLY_STATUS_DISCHARGING; |
| } |
| |
| static int smb349_get_prop_cradle_present(struct smb349_dual_charger *chip) |
| { |
| return chip->chg_present && !chip->charging_disabled; |
| } |
| |
| static int smb349_get_prop_charge_type(struct smb349_dual_charger *chip) |
| { |
| int rc; |
| u8 reg = 0; |
| |
| if (chip->chg_present && !chip->charging_disabled) { |
| rc = smb349_read_reg(chip, STATUS_C_REG, ®); |
| if (rc) { |
| dev_err(chip->dev, "Couldn't read STAT_C rc = %d\n", |
| rc); |
| return POWER_SUPPLY_CHARGE_TYPE_UNKNOWN; |
| } |
| |
| dev_dbg(chip->dev, "%s: STATUS_C_REG=%x\n", __func__, reg); |
| |
| reg &= STATUS_C_CHARGING_MASK; |
| |
| if ((reg == STATUS_C_FAST_CHARGING) |
| || (reg == STATUS_C_TAPER_CHARGING)) |
| return POWER_SUPPLY_CHARGE_TYPE_FAST; |
| else if (reg == STATUS_C_PRE_CHARGING) |
| return POWER_SUPPLY_CHARGE_TYPE_TRICKLE; |
| } |
| |
| return POWER_SUPPLY_CHARGE_TYPE_NONE; |
| } |
| |
| static int smb349_get_charging_status(struct smb349_dual_charger *chip) |
| { |
| int rc; |
| u8 reg = 0; |
| |
| if (chip->chg_present && !chip->charging_disabled) { |
| rc = smb349_read_reg(chip, STATUS_C_REG, ®); |
| if (rc) { |
| dev_err(chip->dev, "Couldn't read STAT_C rc = %d\n", |
| rc); |
| return 0; |
| } |
| |
| return (reg & STATUS_C_CHG_ENABLE_STATUS_BIT) ? 1 : 0; |
| } |
| |
| return 0; |
| } |
| |
| static int smb349_charging(struct smb349_dual_charger *chip, int enable) |
| { |
| int rc = 0; |
| |
| dev_dbg(chip->dev, "%s: charging enable = %d\n", __func__, enable); |
| |
| if (chip->chg_present) { |
| rc = smb349_masked_write(chip, CMD_A_REG, CMD_A_CHG_ENABLE_BIT, |
| enable ? CMD_A_CHG_ENABLE_BIT : 0); |
| if (rc) |
| dev_err(chip->dev, "Couldn't enable = %d rc = %d\n", |
| enable, rc); |
| dev_dbg(chip->dev, "cradle psy changed\n"); |
| power_supply_changed(&chip->cradle_psy); |
| } |
| |
| chip->charging_disabled = !enable; |
| return rc; |
| } |
| |
| static int |
| smb349_cradle_property_is_writeable(struct power_supply *psy, |
| enum power_supply_property psp) |
| { |
| switch (psp) { |
| case POWER_SUPPLY_PROP_CHARGING_ENABLED: |
| return 1; |
| default: |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static int smb349_cradle_set_property(struct power_supply *psy, |
| enum power_supply_property prop, |
| const union power_supply_propval *val) |
| { |
| struct smb349_dual_charger *chip = container_of(psy, |
| struct smb349_dual_charger, cradle_psy); |
| |
| switch (prop) { |
| case POWER_SUPPLY_PROP_CHARGING_ENABLED: |
| smb349_charging(chip, val->intval); |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static int smb349_cradle_get_property(struct power_supply *psy, |
| enum power_supply_property prop, |
| union power_supply_propval *val) |
| { |
| struct smb349_dual_charger *chip = container_of(psy, |
| struct smb349_dual_charger, cradle_psy); |
| |
| switch (prop) { |
| case POWER_SUPPLY_PROP_STATUS: |
| val->intval = smb349_get_prop_cradle_status(chip); |
| break; |
| case POWER_SUPPLY_PROP_ONLINE: |
| val->intval = smb349_get_prop_cradle_present(chip); |
| break; |
| case POWER_SUPPLY_PROP_PRESENT: |
| val->intval = smb349_get_prop_cradle_present(chip); |
| break; |
| case POWER_SUPPLY_PROP_CHARGING_ENABLED: |
| val->intval = smb349_get_charging_status(chip); |
| break; |
| case POWER_SUPPLY_PROP_CHARGE_TYPE: |
| val->intval = smb349_get_prop_charge_type(chip); |
| break; |
| case POWER_SUPPLY_PROP_TECHNOLOGY: |
| val->intval = POWER_SUPPLY_TECHNOLOGY_LION; |
| break; |
| case POWER_SUPPLY_PROP_MODEL_NAME: |
| val->strval = "SMB349 Dual Charger"; |
| break; |
| default: |
| return -EINVAL; |
| } |
| return 0; |
| } |
| |
| static int fast_chg(struct smb349_dual_charger *chip, u8 status) |
| { |
| dev_dbg(chip->dev, "%s\n", __func__); |
| return 0; |
| } |
| |
| static int chg_term(struct smb349_dual_charger *chip, u8 status) |
| { |
| dev_dbg(chip->dev, "%s\n", __func__); |
| return 0; |
| } |
| |
| static int chg_type_start(struct smb349_dual_charger *chip, u8 status) |
| { |
| dev_dbg(chip->dev, "%s status=%d\n", __func__, status); |
| return 0; |
| } |
| |
| static int chg_type_taper(struct smb349_dual_charger *chip, u8 status) |
| { |
| dev_dbg(chip->dev, "%s status=%d\n", __func__, status); |
| return 0; |
| } |
| |
| static int chg_type_recharge(struct smb349_dual_charger *chip, u8 status) |
| { |
| dev_dbg(chip->dev, "%s status=%d\n", __func__, status); |
| return 0; |
| } |
| |
| static struct irq_handler_info handlers[] = { |
| [0] = { |
| .stat_reg = IRQ_A_REG, |
| .val = 0, |
| .prev_val = 0, |
| .irq_info = { |
| { |
| .name = "cold_soft", |
| }, |
| { |
| .name = "hot_soft", |
| }, |
| { |
| .name = "cold_hard", |
| }, |
| { |
| .name = "hot_hard", |
| }, |
| }, |
| }, |
| [1] = { |
| .stat_reg = IRQ_B_REG, |
| .val = 0, |
| .prev_val = 0, |
| .irq_info = { |
| { |
| .name = "fast_chg", |
| .smb_irq = fast_chg, |
| }, |
| { |
| .name = "vbat_low", |
| }, |
| { |
| .name = "battery_missing", |
| }, |
| { |
| .name = "battery_ov", |
| }, |
| }, |
| }, |
| [2] = { |
| .stat_reg = IRQ_C_REG, |
| .val = 0, |
| .prev_val = 0, |
| .irq_info = { |
| { |
| .name = "chg_term", |
| .smb_irq = chg_term, |
| }, |
| { |
| .name = "taper", |
| .smb_irq = chg_type_taper, |
| }, |
| { |
| .name = "recharge", |
| .smb_irq = chg_type_recharge, |
| }, |
| { |
| .name = "chg_hot", |
| }, |
| }, |
| }, |
| [3] = { |
| .stat_reg = IRQ_D_REG, |
| .val = 0, |
| .prev_val = 0, |
| .irq_info = { |
| { |
| .name = "prechg_timeout", |
| }, |
| { |
| .name = "safety_timeout", |
| }, |
| { |
| .name = "aicl_complete", |
| .smb_irq = chg_type_start, |
| }, |
| { |
| .name = "src_detect", |
| }, |
| }, |
| }, |
| [4] = { |
| .stat_reg = IRQ_E_REG, |
| .val = 0, |
| .prev_val = 0, |
| .irq_info = { |
| { |
| .name = "dcin_uv", |
| }, |
| { |
| .name = "dcin_ov", |
| }, |
| { |
| .name = "afvc_active", |
| }, |
| { |
| .name = "unknown", |
| }, |
| }, |
| }, |
| [5] = { |
| .stat_reg = IRQ_F_REG, |
| .val = 0, |
| .prev_val = 0, |
| .irq_info = { |
| { |
| .name = "power_ok", |
| }, |
| { |
| .name = "otg_det", |
| }, |
| { |
| .name = "otg_batt_uv", |
| }, |
| { |
| .name = "otg_oc", |
| }, |
| }, |
| }, |
| }; |
| |
| static void smb349_pm_stay_awake(struct smb349_dual_charger *chip, int reason) |
| { |
| int reasons; |
| |
| mutex_lock(&chip->pm_lock); |
| reasons = chip->wake_reasons | reason; |
| if (reasons != 0 && chip->wake_reasons == 0) { |
| dev_dbg(chip->dev, "staying awake: 0x%02x (bit %d)\n", |
| reasons, reason); |
| pm_stay_awake(chip->dev); |
| } |
| chip->wake_reasons = reasons; |
| mutex_unlock(&chip->pm_lock); |
| } |
| |
| static void smb349_pm_relax(struct smb349_dual_charger *chip, int reason) |
| { |
| int reasons; |
| |
| mutex_lock(&chip->pm_lock); |
| reasons = chip->wake_reasons & (~reason); |
| if (reasons == 0 && chip->wake_reasons != 0) { |
| dev_dbg(chip->dev, "relaxing: 0x%02x (bit %d)\n", |
| reasons, reason); |
| pm_relax(chip->dev); |
| } |
| chip->wake_reasons = reasons; |
| mutex_unlock(&chip->pm_lock); |
| } |
| |
| static irqreturn_t smb349_chg_stat_handler(int irq, void *dev_id) |
| { |
| struct smb349_dual_charger *chip = dev_id; |
| |
| smb349_pm_stay_awake(chip, PM_SMB349_IRQ_HANDLING); |
| schedule_delayed_work(&chip->irq_handler_dwork, |
| round_jiffies_relative(msecs_to_jiffies(DELAY_IRQ_LEVEL_MS))); |
| |
| return IRQ_HANDLED; |
| } |
| |
| static void handle_stat_irqs(struct smb349_dual_charger *chip) |
| { |
| int i, j; |
| u8 triggered; |
| u8 changed; |
| u8 rt_stat, prev_rt_stat; |
| int rc; |
| int handler_count = 0; |
| |
| dev_dbg(chip->dev, "%s\n", __func__); |
| |
| for (i = 0; i < ARRAY_SIZE(handlers); i++) { |
| rc = smb349_read_reg(chip, handlers[i].stat_reg, |
| &handlers[i].val); |
| if (rc < 0) { |
| dev_err(chip->dev, "Couldn't read %d rc = %d\n", |
| handlers[i].stat_reg, rc); |
| continue; |
| } |
| |
| for (j = 0; j < ARRAY_SIZE(handlers[i].irq_info); j++) { |
| triggered = handlers[i].val |
| & (IRQ_LATCHED_MASK << (j * BITS_PER_IRQ)); |
| rt_stat = handlers[i].val |
| & (IRQ_STATUS_MASK << (j * BITS_PER_IRQ)); |
| prev_rt_stat = handlers[i].prev_val |
| & (IRQ_STATUS_MASK << (j * BITS_PER_IRQ)); |
| changed = prev_rt_stat ^ rt_stat; |
| |
| if (triggered || changed) |
| rt_stat ? handlers[i].irq_info[j].high++ : |
| handlers[i].irq_info[j].low++; |
| |
| if ((triggered || changed) |
| && handlers[i].irq_info[j].smb_irq != NULL) { |
| handler_count++; |
| rc = handlers[i].irq_info[j].smb_irq(chip, |
| rt_stat); |
| if (rc < 0) |
| dev_err(chip->dev, |
| "Couldn't handle %d irq for reg 0x%02x rc = %d\n", |
| j, handlers[i].stat_reg, rc); |
| } |
| } |
| handlers[i].prev_val = handlers[i].val; |
| } |
| |
| pr_debug("handler count = %d\n", handler_count); |
| if (handler_count) { |
| pr_debug("cradle psy changed\n"); |
| power_supply_changed(&chip->cradle_psy); |
| } |
| } |
| |
| static int smb_parse_dt(struct smb349_dual_charger *chip) |
| { |
| int rc; |
| enum of_gpio_flags gpio_flags; |
| struct device_node *node = chip->dev->of_node; |
| |
| if (!node) { |
| dev_err(chip->dev, "device tree info. missing\n"); |
| return -EINVAL; |
| } |
| |
| chip->charging_disabled = of_property_read_bool(node, |
| "qcom,charging-disabled"); |
| |
| rc = of_property_read_string(node, "qcom,ext-psy-name", |
| &chip->ext_psy_name); |
| if (rc) { |
| dev_err(chip->dev, "Invalid qcom,ext-psy-name, rc = %d\n", rc); |
| return -EINVAL; |
| } |
| |
| chip->chg_stat_gpio = of_get_named_gpio_flags(node, |
| "qcom,chg-stat-gpio", 0, &gpio_flags); |
| if (!gpio_is_valid(chip->chg_stat_gpio)) { |
| dev_err(chip->dev, "Invalid qcom,chg-stat-gpio, rc = %d\n", |
| rc); |
| return -EINVAL; |
| } |
| |
| rc = of_property_read_u32(node, "qcom,fastchg-current-max-ma", |
| &chip->fastchg_current_max_ma); |
| if (rc) |
| chip->fastchg_current_max_ma = SMB349_FAST_CHG_MAX_MA; |
| |
| chip->iterm_disabled = of_property_read_bool(node, |
| "qcom,iterm-disabled"); |
| |
| rc = of_property_read_u32(node, "qcom,iterm-ma", &chip->iterm_ma); |
| if (rc < 0) |
| chip->iterm_ma = -EINVAL; |
| |
| rc = of_property_read_u32(node, "qcom,float-voltage-mv", |
| &chip->vfloat_mv); |
| if (rc < 0) |
| chip->vfloat_mv = -EINVAL; |
| |
| rc = of_property_read_u32(node, "qcom,recharge-mv", |
| &chip->recharge_mv); |
| if (rc < 0) |
| chip->recharge_mv = -EINVAL; |
| |
| chip->recharge_disabled = of_property_read_bool(node, |
| "qcom,recharge-disabled"); |
| |
| return 0; |
| } |
| |
| static void notify_external_charger(struct smb349_dual_charger *chip, int on) |
| { |
| union power_supply_propval prop = {on,}; |
| |
| chip->ext_psy->set_property(chip->ext_psy, |
| POWER_SUPPLY_PROP_CHARGING_ENABLED, &prop); |
| } |
| |
| static inline void handle_dc_inout(struct smb349_dual_charger *chip, |
| int present) |
| { |
| if (chip->chg_present != present) { |
| chip->chg_present = present; |
| power_supply_changed(&chip->cradle_psy); |
| |
| if (present) { |
| dev_dbg(chip->dev, "DC IN\n"); |
| smb349_hw_init(chip); |
| smb349_pm_stay_awake(chip, PM_SMB349_DC_INSERTED); |
| schedule_delayed_work( |
| &chip->periodic_charge_handler_dwork, |
| round_jiffies_relative( |
| msecs_to_jiffies(PERIODIC_DELAY_MS))); |
| notify_external_charger(chip, 0); |
| dev_dbg(chip->dev, "Periodic work started\n"); |
| } else { |
| dev_dbg(chip->dev, "DC OUT\n"); |
| cancel_delayed_work( |
| &chip->periodic_charge_handler_dwork); |
| smb349_pm_relax(chip, PM_SMB349_DC_INSERTED); |
| notify_external_charger(chip, 1); |
| dev_dbg(chip->dev, "Periodic work stopped\n"); |
| } |
| } |
| } |
| |
| static int determine_initial_state(struct smb349_dual_charger *chip) |
| { |
| u8 level; |
| |
| if (gpio_is_valid(chip->chg_stat_gpio)) |
| level = gpio_get_value_cansleep(chip->chg_stat_gpio); |
| else |
| return -EINVAL; |
| |
| handle_dc_inout(chip, !level); |
| |
| return 0; |
| } |
| |
| static void stat_irq_work(struct work_struct *work) |
| { |
| struct smb349_dual_charger *chip = container_of(work, |
| struct smb349_dual_charger, |
| irq_handler_dwork.work); |
| int level; |
| |
| mutex_lock(&chip->irq_complete); |
| level = gpio_get_value_cansleep(chip->chg_stat_gpio); |
| if (level < 0) { |
| dev_err(chip->dev, "Couldn't read chg_valid gpio=%d\n", |
| chip->chg_stat_gpio); |
| mutex_unlock(&chip->irq_complete); |
| return; |
| } |
| |
| dev_dbg(chip->dev, "%s level=%d\n", __func__, level); |
| |
| if (chip->chg_present == !level) |
| handle_stat_irqs(chip); |
| else |
| handle_dc_inout(chip, !level); |
| smb349_pm_relax(chip, PM_SMB349_IRQ_HANDLING); |
| mutex_unlock(&chip->irq_complete); |
| } |
| |
| /* |
| * There is a PMI Fuel Gauge requirement to lower |
| * the Fast Charge current for 2 seconds each 2 minutes by at least 200mA |
| */ |
| static void periodic_charge_work(struct work_struct *work) |
| { |
| int save_current, rc; |
| struct smb349_dual_charger *chip = container_of(work, |
| struct smb349_dual_charger, |
| periodic_charge_handler_dwork.work); |
| |
| rc = smb349_enable_volatile_writes(chip); |
| if (rc) { |
| dev_dbg(chip->dev, "Couldn't configure volatile writes rc=%d\n", |
| rc); |
| goto resched; |
| } |
| |
| save_current = chip->fastchg_current_max_ma; |
| chip->fastchg_current_max_ma -= LOW_CHARGE_CURRENT_MA; |
| |
| /* lower the fast charge current limit to allow PMIC FG metering */ |
| rc = smb349_fastchg_current_set(chip); |
| if (rc) { |
| dev_dbg(chip->dev, "Couldn't set fastchg current rc=%d\n", rc); |
| goto resched; |
| } |
| |
| /* The required delay to ensure PMI FG detects the transient */ |
| msleep(LOW_CHARGE_DELAY_MS); |
| |
| chip->fastchg_current_max_ma = save_current; |
| /* set the fast charge current limit */ |
| rc = smb349_fastchg_current_set(chip); |
| if (rc) { |
| dev_dbg(chip->dev, "Couldn't set fastchg current rc=%d\n", rc); |
| goto resched; |
| } |
| |
| resched: |
| schedule_delayed_work(&chip->periodic_charge_handler_dwork, |
| round_jiffies_relative(msecs_to_jiffies(PERIODIC_DELAY_MS))); |
| return; |
| } |
| |
| static int smb349_dual_charger_probe(struct i2c_client *client, |
| const struct i2c_device_id *id) |
| { |
| int rc, irq; |
| struct smb349_dual_charger *chip; |
| |
| chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); |
| if (!chip) { |
| dev_err(&client->dev, "Couldn't allocate memory\n"); |
| return -ENOMEM; |
| } |
| |
| chip->client = client; |
| chip->dev = &client->dev; |
| chip->chg_present = -EINVAL; |
| |
| INIT_DELAYED_WORK(&chip->irq_handler_dwork, stat_irq_work); |
| INIT_DELAYED_WORK(&chip->periodic_charge_handler_dwork, |
| periodic_charge_work); |
| |
| rc = smb_parse_dt(chip); |
| if (rc) { |
| dev_err(&client->dev, "Couldn't parse DT nodes rc=%d\n", rc); |
| return rc; |
| } |
| |
| i2c_set_clientdata(client, chip); |
| |
| chip->cradle_psy.name = "cradle-charger"; |
| chip->cradle_psy.type = POWER_SUPPLY_TYPE_MAINS; |
| chip->cradle_psy.get_property = smb349_cradle_get_property; |
| chip->cradle_psy.set_property = smb349_cradle_set_property; |
| chip->cradle_psy.property_is_writeable = |
| smb349_cradle_property_is_writeable; |
| chip->cradle_psy.properties = smb349_cradle_properties; |
| chip->cradle_psy.num_properties = ARRAY_SIZE(smb349_cradle_properties); |
| |
| chip->ext_psy = power_supply_get_by_name((char *)chip->ext_psy_name); |
| if (!chip->ext_psy) { |
| dev_err(chip->dev, |
| "Waiting for '%s' psy to become available\n", |
| (char *)chip->ext_psy_name); |
| return -EPROBE_DEFER; |
| } |
| |
| mutex_init(&chip->irq_complete); |
| mutex_init(&chip->pm_lock); |
| |
| rc = power_supply_register(chip->dev, &chip->cradle_psy); |
| if (rc < 0) { |
| dev_err(&client->dev, "Couldn't register '%s' psy rc=%d\n", |
| chip->cradle_psy.name, rc); |
| return rc; |
| } |
| |
| rc = gpio_request(chip->chg_stat_gpio, "smb349_chg_stat"); |
| if (rc) { |
| dev_err(&client->dev, |
| "gpio_request for %d failed rc=%d\n", |
| chip->chg_stat_gpio, rc); |
| goto fail_chg_stat_irq; |
| } |
| |
| irq = gpio_to_irq(chip->chg_stat_gpio); |
| if (irq < 0) { |
| dev_err(&client->dev, |
| "Invalid chg_stat irq = %d\n", irq); |
| goto fail_chg_stat_irq; |
| } |
| rc = devm_request_threaded_irq(&client->dev, irq, |
| NULL, smb349_chg_stat_handler, |
| IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING | IRQF_ONESHOT, |
| "smb349_chg_stat_irq", chip); |
| if (rc) { |
| dev_err(&client->dev, |
| "Failed request_irq irq=%d, gpio=%d rc=%d\n", |
| irq, chip->chg_stat_gpio, rc); |
| goto fail_chg_stat_irq; |
| } |
| |
| determine_initial_state(chip); |
| |
| dev_info(chip->dev, "SMB349 successfully initialized in STAT driven mode. charger=%d\n", |
| chip->chg_present); |
| return 0; |
| |
| fail_chg_stat_irq: |
| if (gpio_is_valid(chip->chg_stat_gpio)) |
| gpio_free(chip->chg_stat_gpio); |
| power_supply_unregister(&chip->cradle_psy); |
| return rc; |
| } |
| |
| static int smb349_dual_charger_remove(struct i2c_client *client) |
| { |
| struct smb349_dual_charger *chip = i2c_get_clientdata(client); |
| |
| power_supply_unregister(&chip->cradle_psy); |
| if (gpio_is_valid(chip->chg_stat_gpio)) |
| gpio_free(chip->chg_stat_gpio); |
| |
| mutex_destroy(&chip->pm_lock); |
| mutex_destroy(&chip->irq_complete); |
| return 0; |
| } |
| |
| static struct of_device_id smb349_match_table[] = { |
| { .compatible = "qcom,smb349-dual-charger",}, |
| {}, |
| }; |
| |
| static const struct i2c_device_id smb349_dual_charger_id[] = { |
| {"smb349-dual-charger", 0}, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(i2c, smb349_dual_charger_id); |
| |
| static struct i2c_driver smb349_dual_charger_driver = { |
| .driver = { |
| .name = "smb349-dual-charger", |
| .owner = THIS_MODULE, |
| .of_match_table = smb349_match_table, |
| }, |
| .probe = smb349_dual_charger_probe, |
| .remove = smb349_dual_charger_remove, |
| .id_table = smb349_dual_charger_id, |
| }; |
| |
| module_i2c_driver(smb349_dual_charger_driver); |
| |
| MODULE_DESCRIPTION("SMB349 Dual Charger"); |
| MODULE_LICENSE("GPL v2"); |
| MODULE_ALIAS("i2c:smb349-dual-charger"); |