X-Git-Url: https://git.openpandora.org/cgi-bin/gitweb.cgi?p=pandora-kernel.git;a=blobdiff_plain;f=drivers%2Fpower%2Ftwl4030_charger.c;h=d8467d2994e524b4c6fbbedf9734d53f623d0276;hp=0a45c24b4120e0e30cdee1207f6823afa472ae2f;hb=5381d34f61545951669fd14fc0146191e70370ff;hpb=dbe5d2ec3b8bf2045decab984c2783b9851f0e2e diff --git a/drivers/power/twl4030_charger.c b/drivers/power/twl4030_charger.c index 0a45c24b4120..d8467d2994e5 100644 --- a/drivers/power/twl4030_charger.c +++ b/drivers/power/twl4030_charger.c @@ -23,6 +23,7 @@ #include #include #include +#include #define TWL4030_BCIMSTATEC 0x02 #define TWL4030_BCIICHG 0x08 @@ -31,6 +32,7 @@ #define TWL4030_BCIMFSTS3 0x0f #define TWL4030_BCIMFSTS4 0x10 #define TWL4030_BCIMFKEY 0x11 +#define TWL4030_BCIMFTH8 0x1d #define TWL4030_BCICTL1 0x23 #define TWL4030_BCIIREF1 0x27 #define TWL4030_BCIIREF2 0x28 @@ -44,6 +46,7 @@ #define TWL4030_USBFASTMCHG BIT(2) #define TWL4030_STS_VBUS BIT(7) #define TWL4030_STS_USB_ID BIT(2) +#define TWL4030_STS_CHG BIT(1) /* BCI interrupts */ #define TWL4030_WOVF BIT(0) /* Watchdog overflow */ @@ -69,8 +72,18 @@ #define TWL4030_MSTATEC_COMPLETE4 0x0e #define TWL4030_KEY_IIREF 0xe7 +#define TWL4030_KEY_MFTH8 0xf4 #define TWL4030_BATSTSMCHG BIT(6) +#define IRQ_CHECK_PERIOD (3 * HZ) +#define IRQ_CHECK_THRESHOLD 4 + +/* By default, 80mA */ +static unsigned end_of_charge_current = 80000; +module_param(end_of_charge_current, uint, 0); +MODULE_PARM_DESC(end_of_charge_current, + "Stop charging when the charger current goes below this threshold"); + static bool allow_usb = 1; module_param(allow_usb, bool, 0644); MODULE_PARM_DESC(allow_usb, "Allow USB charge drawing default current"); @@ -91,6 +104,17 @@ struct twl4030_bci { enum power_supply_type current_supply; struct regulator *usb_reg; int usb_enabled; + int irq_had_charger; + + unsigned charge_current; + unsigned eoc_current; + + unsigned long irq_check_count_time; + int irq_check_count; + int irq_check_ac_disabled; + + struct led_trigger *charging_any_trig; + int was_charging_any; unsigned long event; struct ratelimit_state ratelimit; @@ -151,6 +175,20 @@ static int twl4030bci_read_adc_val(u8 reg) return temp | val; } +static int twl4030bci_get_cgain(struct twl4030_bci *bci, int *cgain) { + int ret; + u8 reg; + + ret = twl4030_bci_read(TWL4030_BCICTL1, ®); + if (ret < 0) { + dev_warn(bci->dev, "error fetching CGAIN value from BCICTL1 register"); + return ret; + } + *cgain = (reg & TWL4030_CGAIN) ? 1 : 0; + + return 0; +} + /* * Check if VBUS power is present */ @@ -236,26 +274,62 @@ static int twl4030_charger_enable_ac(bool enable) return ret; } -static int set_charge_current(struct twl4030_bci *bci, int new_current) +static int update_charge_parameters(struct twl4030_bci *bci, + int charge_current, int eoc_current) { - u8 val, boot_bci_prev, cgain_set, cgain_clear; - int ret, ret2; + int old_charge_current = bci->charge_current; + int old_eoc_current = bci->eoc_current; + int ret = 0; + int ret2; + u16 hw_charge_current; + u8 hw_eoc_current, boot_bci_prev, reg, cgain_set, cgain_clear; + bool need_cgain = false; + bool restore_boot_reg = false; + + if (charge_current < 0 && eoc_current < 0) + return -EINVAL; - ret = twl4030_bci_read(TWL4030_BCIMFSTS3, &val); + ret = twl4030_bci_read(TWL4030_BCIMFSTS3, ®); if (ret) - goto out_norestore; + goto fail_norestore; + if (!(reg & TWL4030_BATSTSMCHG)) { + dev_err(bci->dev, "missing battery, can't change charging parameters\n"); + goto fail_norestore; + } + + if (charge_current >= 0) + bci->charge_current = charge_current; + if (eoc_current >= 0) + bci->eoc_current = eoc_current; + + hw_eoc_current = bci->eoc_current / 13310; + if (hw_eoc_current > 15) + need_cgain = true; - if (!(val & TWL4030_BATSTSMCHG)) { - dev_err(bci->dev, "missing battery, can't change charge_current\n"); - goto out_norestore; + hw_charge_current = (bci->charge_current * 1000) / 1666666; + if (hw_charge_current > 511) + need_cgain = true; + + if (need_cgain) { + hw_eoc_current /= 2; + hw_charge_current /= 2; + } + + if (hw_eoc_current > 15) { + ret = -EINVAL; + goto fail; + } + if (hw_charge_current > 511) { + ret = -EINVAL; + goto fail; } ret = twl_i2c_read_u8(TWL4030_MODULE_PM_MASTER, &boot_bci_prev, TWL4030_PM_MASTER_BOOT_BCI); if (ret) - goto out_norestore; + goto fail; - /* + /* * Stop automatic charging here, because charge current change * requires multiple register writes and CGAIN change requires * automatic charge to be stopped (and CV mode disabled too). @@ -263,26 +337,32 @@ static int set_charge_current(struct twl4030_bci *bci, int new_current) ret = twl4030_clear_set_boot_bci( TWL4030_CVENAC | TWL4030_BCIAUTOAC | TWL4030_BCIAUTOUSB, 0); if (ret) - goto out; + goto fail; + else + restore_boot_reg = true; - ret = twl4030_bci_write(TWL4030_BCIMFKEY, TWL4030_KEY_IIREF); + ret = twl4030_bci_write(TWL4030_BCIMFKEY, TWL4030_KEY_MFTH8); if (ret) - goto out; - - ret = twl4030_bci_write(TWL4030_BCIIREF1, new_current & 0xff); + goto fail; + ret = twl4030_clear_set(TWL4030_MODULE_MAIN_CHARGE, 0xf0, + hw_eoc_current << 4, TWL4030_BCIMFTH8); if (ret) - goto out; + goto fail; ret = twl4030_bci_write(TWL4030_BCIMFKEY, TWL4030_KEY_IIREF); if (ret) - goto out; - - ret = twl4030_bci_write(TWL4030_BCIIREF2, (new_current >> 8) & 0x1); + goto fail; + ret = twl4030_bci_write(TWL4030_BCIIREF1, hw_charge_current & 0xff); if (ret) - goto out; + goto fail; + ret = twl4030_bci_write(TWL4030_BCIMFKEY, TWL4030_KEY_IIREF); + if (ret) + goto fail; + ret = twl4030_bci_write(TWL4030_BCIIREF2, (hw_charge_current >> 8) & 0x1); + if (ret) + goto fail; - /* Set CGAIN = 0 or 1 */ - if (new_current > 511) { + if (need_cgain) { cgain_set = TWL4030_CGAIN; cgain_clear = 0; } else { @@ -293,23 +373,32 @@ static int set_charge_current(struct twl4030_bci *bci, int new_current) ret = twl4030_clear_set(TWL4030_MODULE_MAIN_CHARGE, cgain_clear, cgain_set, TWL4030_BCICTL1); if (ret) - goto out; + goto fail; - ret = twl4030_bci_read(TWL4030_BCICTL1, &val); - if (ret != 0 || (val & TWL4030_CGAIN) != cgain_set) { + ret = twl4030_bci_read(TWL4030_BCICTL1, ®); + if (ret != 0 || (reg & TWL4030_CGAIN) != cgain_set) { dev_err(bci->dev, "CGAIN change failed\n"); - goto out; + goto fail; } -out: - ret2 = twl_i2c_write_u8(TWL4030_MODULE_PM_MASTER, boot_bci_prev, - TWL4030_PM_MASTER_BOOT_BCI); - if (ret2 != 0) +fail: + if (restore_boot_reg && (ret2 = twl_i2c_write_u8(TWL4030_MODULE_PM_MASTER, + boot_bci_prev, TWL4030_PM_MASTER_BOOT_BCI))) { dev_err(bci->dev, "failed boot_bci restore: %d\n", ret2); + if (!ret) + ret = ret2; + } -out_norestore: - if (ret != 0) - dev_err(bci->dev, "charge current change failed: %d\n", ret); + if (ret) { + if (charge_current >= 0) + bci->charge_current = old_charge_current; + if (eoc_current >= 0) + bci->eoc_current = old_eoc_current; + } + +fail_norestore: + if (ret) + dev_err(bci->dev, "failed to change charging parameters\n"); return ret; } @@ -320,11 +409,49 @@ out_norestore: static irqreturn_t twl4030_charger_interrupt(int irq, void *arg) { struct twl4030_bci *bci = arg; + int have_charger; + u8 hw_cond; + int ret; + + ret = twl_i2c_read_u8(TWL4030_MODULE_PM_MASTER, &hw_cond, + TWL4030_PM_MASTER_STS_HW_CONDITIONS); + if (ret < 0) { + dev_err(bci->dev, "HW_CONDITIONS read failed: %d\n", ret); + goto out; + } + + have_charger = (hw_cond & TWL4030_STS_CHG) ? 1 : 0; + if (have_charger == bci->irq_had_charger) + goto out; + bci->irq_had_charger = have_charger; + + dev_dbg(bci->dev, "CHG_PRES irq, hw_cond %02x\n", hw_cond); + + /* + * deal with rare mysterious issue of CHG_PRES changing states at ~4Hz + * without any charger connected or anything + */ + if (time_before(jiffies, bci->irq_check_count_time + IRQ_CHECK_PERIOD)) { + bci->irq_check_count++; + if (have_charger && bci->irq_check_count > IRQ_CHECK_THRESHOLD) { + dev_err(bci->dev, "spurious CHG_PRES irqs detected (%d), disabling charger\n", + bci->irq_check_count); + twl4030_charger_enable_ac(false); + bci->irq_check_ac_disabled = true; + } + } else { + bci->irq_check_count_time = jiffies; + bci->irq_check_count = 1; + if (have_charger && bci->irq_check_ac_disabled) { + twl4030_charger_enable_ac(true); + bci->irq_check_ac_disabled = false; + } + } - dev_dbg(bci->dev, "CHG_PRES irq\n"); power_supply_changed(&bci->ac); power_supply_changed(&bci->usb); +out: return IRQ_HANDLED; } @@ -372,11 +499,13 @@ static irqreturn_t twl4030_bci_interrupt(int irq, void *arg) if (irqs2 & TWL4030_ACCHGOV && __ratelimit(&bci->ratelimit)) dev_crit(bci->dev, "Ac charger overvoltage\n"); +#if 0 /* ack the interrupts */ twl_i2c_write_u8(TWL4030_MODULE_INTERRUPTS, irqs1, TWL4030_INTERRUPTS_BCIISR1A); twl_i2c_write_u8(TWL4030_MODULE_INTERRUPTS, irqs2, TWL4030_INTERRUPTS_BCIISR2A); +#endif return IRQ_HANDLED; } @@ -417,27 +546,66 @@ static int twl4030_bci_usb_ncb(struct notifier_block *nb, unsigned long val, * CGAIN == 0: val * 1.6618 - 0.85 * CGAIN == 1: (val * 1.6618 - 0.85) * 2 */ -static int twl4030_charger_get_current(void) +static int twl4030_charger_get_current(struct twl4030_bci *bci) { int curr; int ret; - u8 bcictl1; + int cgain; curr = twl4030bci_read_adc_val(TWL4030_BCIICHG); if (curr < 0) return curr; - ret = twl4030_bci_read(TWL4030_BCICTL1, &bcictl1); - if (ret) + if ((ret = twl4030bci_get_cgain(bci, &cgain))) return ret; ret = (curr * 16618 - 850 * 10000) / 10; - if (bcictl1 & TWL4030_CGAIN) + if (cgain) ret *= 2; return ret; } +static int twl4030bci_get_charging_current(struct twl4030_bci *bci, int *curr) +{ + int ret, val, cgain; + + val = twl4030bci_read_adc_val(TWL4030_BCIIREF1); + if (val < 0) + return val; + if ((ret = twl4030bci_get_cgain(bci, &cgain))) + return ret; + + val &= 0x1ff; + val *= 1666; + if (cgain) + val *= 2; + + *curr = val; + + return 0; +} + +static int twl4030bci_get_eoc_current(struct twl4030_bci *bci, int *curr) +{ + int ret, cgain, ichgeocth; + u8 reg; + + if ((ret = twl4030bci_get_cgain(bci, &cgain))) + return ret; + + ret = twl4030_bci_read(TWL4030_BCIMFTH8, ®); + if (ret < 0) { + dev_warn(bci->dev, "error fetching BCIMFTH8 register"); + return ret; + } + ichgeocth = reg >> 4; + + *curr = (ichgeocth * 13310) * ((cgain) ? 2 : 1); + + return 0; +} + static ssize_t twl4030_bci_ac_show_enable(struct device *dev, struct device_attribute *attr, char *buf) @@ -515,19 +683,12 @@ static struct device_attribute dev_attr_enable_usb = static ssize_t show_charge_current(struct device *dev, struct device_attribute *attr, char *buf) { + struct power_supply *psy = dev_get_drvdata(dev); + struct twl4030_bci *bci = dev_get_drvdata(psy->dev->parent); int ret, val; - u8 ctl; - - val = twl4030bci_read_adc_val(TWL4030_BCIIREF1); - if (val < 0) - return val; - ret = twl4030_bci_read(TWL4030_BCICTL1, &ctl); - if (ret < 0) - return ret; - val &= 0x1ff; - if (ctl & TWL4030_CGAIN) - val |= 0x200; + if ((ret = twl4030bci_get_charging_current(bci, &val))) + return ret; return sprintf(buf, "%d\n", val); } @@ -545,7 +706,20 @@ static ssize_t store_charge_current(struct device *dev, if (ret) return -EINVAL; - ret = set_charge_current(bci, new_current); + /* + * Previously, this sysfs parameter used a different raw-register + * format. All legal values in this format fall numerically into the + * range [0, 1023]. Since in the new format, these encode values so + * low as to be meaningless, reject them here so that anybody trying + * to use the old format will have at least a chance of figuring out + * why it isn't working any more. Note that in both formats, the value + * 0 has the same meaning, so it is allowed where values in the range + * [1, 1023] are not. + */ + if (new_current < 1024 && new_current != 0) + return -EINVAL; + + ret = update_charge_parameters(bci, new_current, -1); if (ret) return ret; @@ -559,18 +733,60 @@ static ssize_t store_charge_current(struct device *dev, static DEVICE_ATTR(charge_current, S_IRUGO | S_IWUSR, show_charge_current, store_charge_current); +static ssize_t show_end_of_charge_current(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct twl4030_bci *bci = dev_get_drvdata(psy->dev->parent); + int ret, eoc_current; + + if ((ret = twl4030bci_get_eoc_current(bci, &eoc_current))) + return ret; + + return sprintf(buf, "%d\n", eoc_current); +} + +static ssize_t store_end_of_charge_current(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + struct power_supply *psy = dev_get_drvdata(dev); + struct twl4030_bci *bci = dev_get_drvdata(psy->dev->parent); + unsigned long new_eoc_current; + int eoc_current, ret; + + ret = strict_strtoul(buf, 10, &new_eoc_current); + if (ret) + return -EINVAL; + + eoc_current = new_eoc_current; + /* check for overflow */ + if (eoc_current != new_eoc_current) + return -EINVAL; + + if ((ret = update_charge_parameters(bci, -1, eoc_current))) + return ret; + + return count; +} + +static DEVICE_ATTR(end_of_charge_current, S_IRUGO | S_IWUSR, + show_end_of_charge_current, store_end_of_charge_current); + static struct attribute *bci_ac_attrs[] = { &dev_attr_enable_ac.attr, &dev_attr_charge_current.attr, + &dev_attr_end_of_charge_current.attr, NULL, }; static struct attribute *bci_usb_attrs[] = { &dev_attr_enable_usb.attr, &dev_attr_charge_current.attr, + &dev_attr_end_of_charge_current.attr, NULL, }; - + static const struct attribute_group bci_ac_attr_group = { .attrs = bci_ac_attrs, }; @@ -616,7 +832,8 @@ static int twl4030_bci_get_property(struct power_supply *psy, union power_supply_propval *val) { struct twl4030_bci *bci = dev_get_drvdata(psy->dev->parent); - int is_charging; + int is_charging_any = 0; + int is_charging = 0; int state; int ret; @@ -624,15 +841,27 @@ static int twl4030_bci_get_property(struct power_supply *psy, if (state < 0) return state; - if (psy->type == POWER_SUPPLY_TYPE_USB) - is_charging = state & TWL4030_MSTATEC_USB; - else - is_charging = state & TWL4030_MSTATEC_AC; + if (twl4030_bci_state_to_status(state) == + POWER_SUPPLY_STATUS_CHARGING) { + is_charging_any = + state & (TWL4030_MSTATEC_USB | TWL4030_MSTATEC_AC); + if (psy->type == POWER_SUPPLY_TYPE_USB) + is_charging = state & TWL4030_MSTATEC_USB; + else + is_charging = state & TWL4030_MSTATEC_AC; + } + + if (is_charging_any != bci->was_charging_any) { + led_trigger_event(bci->charging_any_trig, + is_charging_any ? LED_FULL : LED_OFF); + bci->was_charging_any = is_charging_any; + } + if (is_charging && psy->type != bci->current_supply) { if (psy->type == POWER_SUPPLY_TYPE_USB) - set_charge_current(bci, bci->usb_current); + update_charge_parameters(bci, bci->usb_current, -1); else - set_charge_current(bci, bci->ac_current); + update_charge_parameters(bci, bci->ac_current, -1); bci->current_supply = psy->type; } @@ -665,7 +894,7 @@ static int twl4030_bci_get_property(struct power_supply *psy, if (!is_charging) return -ENODATA; /* current measurement is shared between AC and USB */ - ret = twl4030_charger_get_current(); + ret = twl4030_charger_get_current(bci); if (ret < 0) return ret; val->intval = ret; @@ -708,13 +937,35 @@ static int __init twl4030_bci_probe(struct platform_device *pdev) bci->dev = &pdev->dev; bci->irq_chg = platform_get_irq(pdev, 0); bci->irq_bci = platform_get_irq(pdev, 1); - bci->ac_current = 860; /* ~1.2A */ - bci->usb_current = 360; /* ~600mA */ + bci->ac_current = 1200000; /* ~1.2A */ + bci->usb_current = 530000; /* ~530mA */ + + /* + * Make sure to set sane charging settings + * + * charge_current: + * ~500mA (doesn't really get used, overridden by ac_current or + * usb_current as soon as a charger is plugged in). + * + * end_of_charge_current: + * defaults to ~400mA (can be set with the eoc_current option) + * This is as high as it can be set in the TWL4030. Seems to be a + * fairly sane value. Certainly more sane then the default 80mA. + */ + if ((ret = update_charge_parameters(bci, + 500000, end_of_charge_current))) + goto fail_set_charge_params; + + bci->irq_had_charger = -1; + bci->irq_check_count_time = jiffies; platform_set_drvdata(pdev, bci); ratelimit_state_init(&bci->ratelimit, HZ, 2); + led_trigger_register_simple("twl4030_bci-charging", + &bci->charging_any_trig); + bci->ac.name = "twl4030_ac"; bci->ac.type = POWER_SUPPLY_TYPE_MAINS; bci->ac.properties = twl4030_charger_props; @@ -824,8 +1075,18 @@ fail_chg_irq: power_supply_unregister(&bci->usb); fail_register_usb: power_supply_unregister(&bci->ac); + + if (bci->usb_reg) { + if (bci->usb_enabled) + regulator_disable(bci->usb_reg); + regulator_put(bci->usb_reg); + } + fail_register_ac: + led_trigger_unregister_simple(bci->charging_any_trig); platform_set_drvdata(pdev, NULL); + +fail_set_charge_params: kfree(bci); return ret; @@ -855,6 +1116,14 @@ static int __exit twl4030_bci_remove(struct platform_device *pdev) free_irq(bci->irq_chg, bci); power_supply_unregister(&bci->usb); power_supply_unregister(&bci->ac); + + if (bci->usb_reg) { + if (bci->usb_enabled) + regulator_disable(bci->usb_reg); + regulator_put(bci->usb_reg); + } + + led_trigger_unregister_simple(bci->charging_any_trig); platform_set_drvdata(pdev, NULL); kfree(bci);