forked from Minki/linux
297d716f62
Change the ownership of power_supply structure from each driver implementing the class to the power supply core. The patch changes power_supply_register() function thus all drivers implementing power supply class are adjusted. Each driver provides the implementation of power supply. However it should not be the owner of power supply class instance because it is exposed by core to other subsystems with power_supply_get_by_name(). These other subsystems have no knowledge when the driver will unregister the power supply. This leads to several issues when driver is unbound - mostly because user of power supply accesses freed memory. Instead let the core own the instance of struct 'power_supply'. Other users of this power supply will still access valid memory because it will be freed when device reference count reaches 0. Currently this means "it will leak" but power_supply_put() call in next patches will solve it. This solves invalid memory references in following race condition scenario: Thread 1: charger manager Thread 2: power supply driver, used by charger manager THREAD 1 (charger manager) THREAD 2 (power supply driver) ========================== ============================== psy = power_supply_get_by_name() Driver unbind, .remove power_supply_unregister() Device fully removed psy->get_property() The 'get_property' call is executed in invalid context because the driver was unbound and struct 'power_supply' memory was freed. This could be observed easily with charger manager driver (here compiled with max17040 fuel gauge): $ cat /sys/devices/virtual/power_supply/cm-battery/capacity & $ echo "1-0036" > /sys/bus/i2c/drivers/max17040/unbind [ 55.725123] Unable to handle kernel NULL pointer dereference at virtual address 00000000 [ 55.732584] pgd = d98d4000 [ 55.734060] [00000000] *pgd=5afa2831, *pte=00000000, *ppte=00000000 [ 55.740318] Internal error: Oops: 80000007 [#1] PREEMPT SMP ARM [ 55.746210] Modules linked in: [ 55.749259] CPU: 1 PID: 2936 Comm: cat Tainted: G W 3.19.0-rc1-next-20141226-00048-gf79f475f3c44-dirty #1496 [ 55.760190] Hardware name: SAMSUNG EXYNOS (Flattened Device Tree) [ 55.766270] task: d9b76f00 ti: daf54000 task.ti: daf54000 [ 55.771647] PC is at 0x0 [ 55.774182] LR is at charger_get_property+0x2f4/0x36c [ 55.779201] pc : [<00000000>] lr : [<c034b0b4>] psr: 60000013 [ 55.779201] sp : daf55e90 ip : 00000003 fp : 00000000 [ 55.790657] r10: 00000000 r9 : c06e2878 r8 : d9b26c68 [ 55.795865] r7 : dad81610 r6 : daec7410 r5 : daf55ebc r4 : 00000000 [ 55.802367] r3 : 00000000 r2 : daf55ebc r1 : 0000002a r0 : d9b26c68 [ 55.808879] Flags: nZCv IRQs on FIQs on Mode SVC_32 ISA ARM Segment user [ 55.815994] Control: 10c5387d Table: 598d406a DAC: 00000015 [ 55.821723] Process cat (pid: 2936, stack limit = 0xdaf54210) [ 55.827451] Stack: (0xdaf55e90 to 0xdaf56000) [ 55.831795] 5e80: 60000013 c01459c4 0000002a c06f8ef8 [ 55.839956] 5ea0: db651000 c06f8ef8 daebac00 c04cb668 daebac08 c0346864 00000000 c01459c4 [ 55.848115] 5ec0: d99eaa80 c06f8ef8 00000fff 00001000 db651000 c027f25c c027f240 d99eaa80 [ 55.856274] 5ee0: d9a06c00 c0146218 daf55f18 00001000 d99eaa80 db4c18c0 00000001 00000001 [ 55.864468] 5f00: daf55f80 c0144c78 c0144c54 c0107f90 00015000 d99eaab0 00000000 00000000 [ 55.872603] 5f20: 000051c7 00000000 db4c18c0 c04a9370 00015000 00001000 daf55f80 00001000 [ 55.880763] 5f40: daf54000 00015000 00000000 c00e53dc db4c18c0 c00e548c 0000000d 00008124 [ 55.888937] 5f60: 00000001 00000000 00000000 db4c18c0 db4c18c0 00001000 00015000 c00e5550 [ 55.897099] 5f80: 00000000 00000000 00001000 00001000 00015000 00000003 00000003 c000f364 [ 55.905239] 5fa0: 00000000 c000f1a0 00001000 00015000 00000003 00015000 00001000 0001333c [ 55.913399] 5fc0: 00001000 00015000 00000003 00000003 00000002 00000000 00000000 00000000 [ 55.921560] 5fe0: 7fffe000 be999850 0000a225 b6f3c19c 60000010 00000003 00000000 00000000 [ 55.929744] [<c034b0b4>] (charger_get_property) from [<c0346864>] (power_supply_show_property+0x48/0x20c) [ 55.939286] [<c0346864>] (power_supply_show_property) from [<c027f25c>] (dev_attr_show+0x1c/0x48) [ 55.948130] [<c027f25c>] (dev_attr_show) from [<c0146218>] (sysfs_kf_seq_show+0x84/0x104) [ 55.956298] [<c0146218>] (sysfs_kf_seq_show) from [<c0144c78>] (kernfs_seq_show+0x24/0x28) [ 55.964536] [<c0144c78>] (kernfs_seq_show) from [<c0107f90>] (seq_read+0x1b0/0x484) [ 55.972172] [<c0107f90>] (seq_read) from [<c00e53dc>] (__vfs_read+0x18/0x4c) [ 55.979188] [<c00e53dc>] (__vfs_read) from [<c00e548c>] (vfs_read+0x7c/0x100) [ 55.986304] [<c00e548c>] (vfs_read) from [<c00e5550>] (SyS_read+0x40/0x8c) [ 55.993164] [<c00e5550>] (SyS_read) from [<c000f1a0>] (ret_fast_syscall+0x0/0x48) [ 56.000626] Code: bad PC value [ 56.011652] ---[ end trace 7b64343fbdae8ef1 ]--- Signed-off-by: Krzysztof Kozlowski <k.kozlowski@samsung.com> Reviewed-by: Bartlomiej Zolnierkiewicz <b.zolnierkie@samsung.com> [for the nvec part] Reviewed-by: Marc Dietrich <marvin24@gmx.de> [for compal-laptop.c] Acked-by: Darren Hart <dvhart@linux.intel.com> [for the mfd part] Acked-by: Lee Jones <lee.jones@linaro.org> [for the hid part] Acked-by: Jiri Kosina <jkosina@suse.cz> [for the acpi part] Acked-by: Rafael J. Wysocki <rafael.j.wysocki@intel.com> Signed-off-by: Sebastian Reichel <sre@kernel.org>
739 lines
18 KiB
C
739 lines
18 KiB
C
/*
|
|
* TWL4030/TPS65950 BCI (Battery Charger Interface) driver
|
|
*
|
|
* Copyright (C) 2010 Gražvydas Ignotas <notasas@gmail.com>
|
|
*
|
|
* based on twl4030_bci_battery.c by TI
|
|
* Copyright (C) 2008 Texas Instruments, Inc.
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
#include <linux/init.h>
|
|
#include <linux/module.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/err.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/i2c/twl.h>
|
|
#include <linux/power_supply.h>
|
|
#include <linux/notifier.h>
|
|
#include <linux/usb/otg.h>
|
|
#include <linux/regulator/machine.h>
|
|
|
|
#define TWL4030_BCIMSTATEC 0x02
|
|
#define TWL4030_BCIICHG 0x08
|
|
#define TWL4030_BCIVAC 0x0a
|
|
#define TWL4030_BCIVBUS 0x0c
|
|
#define TWL4030_BCIMFSTS3 0x0F
|
|
#define TWL4030_BCIMFSTS4 0x10
|
|
#define TWL4030_BCICTL1 0x23
|
|
#define TWL4030_BB_CFG 0x12
|
|
|
|
#define TWL4030_BCIMFSTS1 0x01
|
|
|
|
#define TWL4030_BCIAUTOWEN BIT(5)
|
|
#define TWL4030_CONFIG_DONE BIT(4)
|
|
#define TWL4030_BCIAUTOUSB BIT(1)
|
|
#define TWL4030_BCIAUTOAC BIT(0)
|
|
#define TWL4030_CGAIN BIT(5)
|
|
#define TWL4030_USBFASTMCHG BIT(2)
|
|
#define TWL4030_STS_VBUS BIT(7)
|
|
#define TWL4030_STS_USB_ID BIT(2)
|
|
#define TWL4030_BBCHEN BIT(4)
|
|
#define TWL4030_BBSEL_MASK 0x0c
|
|
#define TWL4030_BBSEL_2V5 0x00
|
|
#define TWL4030_BBSEL_3V0 0x04
|
|
#define TWL4030_BBSEL_3V1 0x08
|
|
#define TWL4030_BBSEL_3V2 0x0c
|
|
#define TWL4030_BBISEL_MASK 0x03
|
|
#define TWL4030_BBISEL_25uA 0x00
|
|
#define TWL4030_BBISEL_150uA 0x01
|
|
#define TWL4030_BBISEL_500uA 0x02
|
|
#define TWL4030_BBISEL_1000uA 0x03
|
|
|
|
#define TWL4030_BATSTSPCHG BIT(2)
|
|
#define TWL4030_BATSTSMCHG BIT(6)
|
|
|
|
/* BCI interrupts */
|
|
#define TWL4030_WOVF BIT(0) /* Watchdog overflow */
|
|
#define TWL4030_TMOVF BIT(1) /* Timer overflow */
|
|
#define TWL4030_ICHGHIGH BIT(2) /* Battery charge current high */
|
|
#define TWL4030_ICHGLOW BIT(3) /* Battery cc. low / FSM state change */
|
|
#define TWL4030_ICHGEOC BIT(4) /* Battery current end-of-charge */
|
|
#define TWL4030_TBATOR2 BIT(5) /* Battery temperature out of range 2 */
|
|
#define TWL4030_TBATOR1 BIT(6) /* Battery temperature out of range 1 */
|
|
#define TWL4030_BATSTS BIT(7) /* Battery status */
|
|
|
|
#define TWL4030_VBATLVL BIT(0) /* VBAT level */
|
|
#define TWL4030_VBATOV BIT(1) /* VBAT overvoltage */
|
|
#define TWL4030_VBUSOV BIT(2) /* VBUS overvoltage */
|
|
#define TWL4030_ACCHGOV BIT(3) /* Ac charger overvoltage */
|
|
|
|
#define TWL4030_MSTATEC_USB BIT(4)
|
|
#define TWL4030_MSTATEC_AC BIT(5)
|
|
#define TWL4030_MSTATEC_MASK 0x0f
|
|
#define TWL4030_MSTATEC_QUICK1 0x02
|
|
#define TWL4030_MSTATEC_QUICK7 0x07
|
|
#define TWL4030_MSTATEC_COMPLETE1 0x0b
|
|
#define TWL4030_MSTATEC_COMPLETE4 0x0e
|
|
|
|
static bool allow_usb;
|
|
module_param(allow_usb, bool, 0644);
|
|
MODULE_PARM_DESC(allow_usb, "Allow USB charge drawing default current");
|
|
|
|
struct twl4030_bci {
|
|
struct device *dev;
|
|
struct power_supply *ac;
|
|
struct power_supply *usb;
|
|
struct usb_phy *transceiver;
|
|
struct notifier_block usb_nb;
|
|
struct work_struct work;
|
|
int irq_chg;
|
|
int irq_bci;
|
|
struct regulator *usb_reg;
|
|
int usb_enabled;
|
|
|
|
unsigned long event;
|
|
};
|
|
|
|
/*
|
|
* clear and set bits on an given register on a given module
|
|
*/
|
|
static int twl4030_clear_set(u8 mod_no, u8 clear, u8 set, u8 reg)
|
|
{
|
|
u8 val = 0;
|
|
int ret;
|
|
|
|
ret = twl_i2c_read_u8(mod_no, &val, reg);
|
|
if (ret)
|
|
return ret;
|
|
|
|
val &= ~clear;
|
|
val |= set;
|
|
|
|
return twl_i2c_write_u8(mod_no, val, reg);
|
|
}
|
|
|
|
static int twl4030_bci_read(u8 reg, u8 *val)
|
|
{
|
|
return twl_i2c_read_u8(TWL_MODULE_MAIN_CHARGE, val, reg);
|
|
}
|
|
|
|
static int twl4030_clear_set_boot_bci(u8 clear, u8 set)
|
|
{
|
|
return twl4030_clear_set(TWL_MODULE_PM_MASTER, clear,
|
|
TWL4030_CONFIG_DONE | TWL4030_BCIAUTOWEN | set,
|
|
TWL4030_PM_MASTER_BOOT_BCI);
|
|
}
|
|
|
|
static int twl4030bci_read_adc_val(u8 reg)
|
|
{
|
|
int ret, temp;
|
|
u8 val;
|
|
|
|
/* read MSB */
|
|
ret = twl4030_bci_read(reg + 1, &val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
temp = (int)(val & 0x03) << 8;
|
|
|
|
/* read LSB */
|
|
ret = twl4030_bci_read(reg, &val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return temp | val;
|
|
}
|
|
|
|
/*
|
|
* Check if Battery Pack was present
|
|
*/
|
|
static int twl4030_is_battery_present(struct twl4030_bci *bci)
|
|
{
|
|
int ret;
|
|
u8 val = 0;
|
|
|
|
/* Battery presence in Main charge? */
|
|
ret = twl_i2c_read_u8(TWL_MODULE_MAIN_CHARGE, &val, TWL4030_BCIMFSTS3);
|
|
if (ret)
|
|
return ret;
|
|
if (val & TWL4030_BATSTSMCHG)
|
|
return 0;
|
|
|
|
/*
|
|
* OK, It could be that bootloader did not enable main charger,
|
|
* pre-charge is h/w auto. So, Battery presence in Pre-charge?
|
|
*/
|
|
ret = twl_i2c_read_u8(TWL4030_MODULE_PRECHARGE, &val,
|
|
TWL4030_BCIMFSTS1);
|
|
if (ret)
|
|
return ret;
|
|
if (val & TWL4030_BATSTSPCHG)
|
|
return 0;
|
|
|
|
return -ENODEV;
|
|
}
|
|
|
|
/*
|
|
* Check if VBUS power is present
|
|
*/
|
|
static int twl4030_bci_have_vbus(struct twl4030_bci *bci)
|
|
{
|
|
int ret;
|
|
u8 hwsts;
|
|
|
|
ret = twl_i2c_read_u8(TWL_MODULE_PM_MASTER, &hwsts,
|
|
TWL4030_PM_MASTER_STS_HW_CONDITIONS);
|
|
if (ret < 0)
|
|
return 0;
|
|
|
|
dev_dbg(bci->dev, "check_vbus: HW_CONDITIONS %02x\n", hwsts);
|
|
|
|
/* in case we also have STS_USB_ID, VBUS is driven by TWL itself */
|
|
if ((hwsts & TWL4030_STS_VBUS) && !(hwsts & TWL4030_STS_USB_ID))
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Enable/Disable USB Charge functionality.
|
|
*/
|
|
static int twl4030_charger_enable_usb(struct twl4030_bci *bci, bool enable)
|
|
{
|
|
int ret;
|
|
|
|
if (enable) {
|
|
/* Check for USB charger connected */
|
|
if (!twl4030_bci_have_vbus(bci))
|
|
return -ENODEV;
|
|
|
|
/*
|
|
* Until we can find out what current the device can provide,
|
|
* require a module param to enable USB charging.
|
|
*/
|
|
if (!allow_usb) {
|
|
dev_warn(bci->dev, "USB charging is disabled.\n");
|
|
return -EACCES;
|
|
}
|
|
|
|
/* Need to keep regulator on */
|
|
if (!bci->usb_enabled) {
|
|
ret = regulator_enable(bci->usb_reg);
|
|
if (ret) {
|
|
dev_err(bci->dev,
|
|
"Failed to enable regulator\n");
|
|
return ret;
|
|
}
|
|
bci->usb_enabled = 1;
|
|
}
|
|
|
|
/* forcing the field BCIAUTOUSB (BOOT_BCI[1]) to 1 */
|
|
ret = twl4030_clear_set_boot_bci(0, TWL4030_BCIAUTOUSB);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
/* forcing USBFASTMCHG(BCIMFSTS4[2]) to 1 */
|
|
ret = twl4030_clear_set(TWL_MODULE_MAIN_CHARGE, 0,
|
|
TWL4030_USBFASTMCHG, TWL4030_BCIMFSTS4);
|
|
} else {
|
|
ret = twl4030_clear_set_boot_bci(TWL4030_BCIAUTOUSB, 0);
|
|
if (bci->usb_enabled) {
|
|
regulator_disable(bci->usb_reg);
|
|
bci->usb_enabled = 0;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Enable/Disable AC Charge funtionality.
|
|
*/
|
|
static int twl4030_charger_enable_ac(bool enable)
|
|
{
|
|
int ret;
|
|
|
|
if (enable)
|
|
ret = twl4030_clear_set_boot_bci(0, TWL4030_BCIAUTOAC);
|
|
else
|
|
ret = twl4030_clear_set_boot_bci(TWL4030_BCIAUTOAC, 0);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Enable/Disable charging of Backup Battery.
|
|
*/
|
|
static int twl4030_charger_enable_backup(int uvolt, int uamp)
|
|
{
|
|
int ret;
|
|
u8 flags;
|
|
|
|
if (uvolt < 2500000 ||
|
|
uamp < 25) {
|
|
/* disable charging of backup battery */
|
|
ret = twl4030_clear_set(TWL_MODULE_PM_RECEIVER,
|
|
TWL4030_BBCHEN, 0, TWL4030_BB_CFG);
|
|
return ret;
|
|
}
|
|
|
|
flags = TWL4030_BBCHEN;
|
|
if (uvolt >= 3200000)
|
|
flags |= TWL4030_BBSEL_3V2;
|
|
else if (uvolt >= 3100000)
|
|
flags |= TWL4030_BBSEL_3V1;
|
|
else if (uvolt >= 3000000)
|
|
flags |= TWL4030_BBSEL_3V0;
|
|
else
|
|
flags |= TWL4030_BBSEL_2V5;
|
|
|
|
if (uamp >= 1000)
|
|
flags |= TWL4030_BBISEL_1000uA;
|
|
else if (uamp >= 500)
|
|
flags |= TWL4030_BBISEL_500uA;
|
|
else if (uamp >= 150)
|
|
flags |= TWL4030_BBISEL_150uA;
|
|
else
|
|
flags |= TWL4030_BBISEL_25uA;
|
|
|
|
ret = twl4030_clear_set(TWL_MODULE_PM_RECEIVER,
|
|
TWL4030_BBSEL_MASK | TWL4030_BBISEL_MASK,
|
|
flags,
|
|
TWL4030_BB_CFG);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* TWL4030 CHG_PRES (AC charger presence) events
|
|
*/
|
|
static irqreturn_t twl4030_charger_interrupt(int irq, void *arg)
|
|
{
|
|
struct twl4030_bci *bci = arg;
|
|
|
|
dev_dbg(bci->dev, "CHG_PRES irq\n");
|
|
power_supply_changed(bci->ac);
|
|
power_supply_changed(bci->usb);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
/*
|
|
* TWL4030 BCI monitoring events
|
|
*/
|
|
static irqreturn_t twl4030_bci_interrupt(int irq, void *arg)
|
|
{
|
|
struct twl4030_bci *bci = arg;
|
|
u8 irqs1, irqs2;
|
|
int ret;
|
|
|
|
ret = twl_i2c_read_u8(TWL4030_MODULE_INTERRUPTS, &irqs1,
|
|
TWL4030_INTERRUPTS_BCIISR1A);
|
|
if (ret < 0)
|
|
return IRQ_HANDLED;
|
|
|
|
ret = twl_i2c_read_u8(TWL4030_MODULE_INTERRUPTS, &irqs2,
|
|
TWL4030_INTERRUPTS_BCIISR2A);
|
|
if (ret < 0)
|
|
return IRQ_HANDLED;
|
|
|
|
dev_dbg(bci->dev, "BCI irq %02x %02x\n", irqs2, irqs1);
|
|
|
|
if (irqs1 & (TWL4030_ICHGLOW | TWL4030_ICHGEOC)) {
|
|
/* charger state change, inform the core */
|
|
power_supply_changed(bci->ac);
|
|
power_supply_changed(bci->usb);
|
|
}
|
|
|
|
/* various monitoring events, for now we just log them here */
|
|
if (irqs1 & (TWL4030_TBATOR2 | TWL4030_TBATOR1))
|
|
dev_warn(bci->dev, "battery temperature out of range\n");
|
|
|
|
if (irqs1 & TWL4030_BATSTS)
|
|
dev_crit(bci->dev, "battery disconnected\n");
|
|
|
|
if (irqs2 & TWL4030_VBATOV)
|
|
dev_crit(bci->dev, "VBAT overvoltage\n");
|
|
|
|
if (irqs2 & TWL4030_VBUSOV)
|
|
dev_crit(bci->dev, "VBUS overvoltage\n");
|
|
|
|
if (irqs2 & TWL4030_ACCHGOV)
|
|
dev_crit(bci->dev, "Ac charger overvoltage\n");
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static void twl4030_bci_usb_work(struct work_struct *data)
|
|
{
|
|
struct twl4030_bci *bci = container_of(data, struct twl4030_bci, work);
|
|
|
|
switch (bci->event) {
|
|
case USB_EVENT_VBUS:
|
|
case USB_EVENT_CHARGER:
|
|
twl4030_charger_enable_usb(bci, true);
|
|
break;
|
|
case USB_EVENT_NONE:
|
|
twl4030_charger_enable_usb(bci, false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static int twl4030_bci_usb_ncb(struct notifier_block *nb, unsigned long val,
|
|
void *priv)
|
|
{
|
|
struct twl4030_bci *bci = container_of(nb, struct twl4030_bci, usb_nb);
|
|
|
|
dev_dbg(bci->dev, "OTG notify %lu\n", val);
|
|
|
|
bci->event = val;
|
|
schedule_work(&bci->work);
|
|
|
|
return NOTIFY_OK;
|
|
}
|
|
|
|
/*
|
|
* TI provided formulas:
|
|
* CGAIN == 0: ICHG = (BCIICHG * 1.7) / (2^10 - 1) - 0.85
|
|
* CGAIN == 1: ICHG = (BCIICHG * 3.4) / (2^10 - 1) - 1.7
|
|
* Here we use integer approximation of:
|
|
* CGAIN == 0: val * 1.6618 - 0.85
|
|
* CGAIN == 1: (val * 1.6618 - 0.85) * 2
|
|
*/
|
|
static int twl4030_charger_get_current(void)
|
|
{
|
|
int curr;
|
|
int ret;
|
|
u8 bcictl1;
|
|
|
|
curr = twl4030bci_read_adc_val(TWL4030_BCIICHG);
|
|
if (curr < 0)
|
|
return curr;
|
|
|
|
ret = twl4030_bci_read(TWL4030_BCICTL1, &bcictl1);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = (curr * 16618 - 850 * 10000) / 10;
|
|
if (bcictl1 & TWL4030_CGAIN)
|
|
ret *= 2;
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Returns the main charge FSM state
|
|
* Or < 0 on failure.
|
|
*/
|
|
static int twl4030bci_state(struct twl4030_bci *bci)
|
|
{
|
|
int ret;
|
|
u8 state;
|
|
|
|
ret = twl4030_bci_read(TWL4030_BCIMSTATEC, &state);
|
|
if (ret) {
|
|
pr_err("twl4030_bci: error reading BCIMSTATEC\n");
|
|
return ret;
|
|
}
|
|
|
|
dev_dbg(bci->dev, "state: %02x\n", state);
|
|
|
|
return state;
|
|
}
|
|
|
|
static int twl4030_bci_state_to_status(int state)
|
|
{
|
|
state &= TWL4030_MSTATEC_MASK;
|
|
if (TWL4030_MSTATEC_QUICK1 <= state && state <= TWL4030_MSTATEC_QUICK7)
|
|
return POWER_SUPPLY_STATUS_CHARGING;
|
|
else if (TWL4030_MSTATEC_COMPLETE1 <= state &&
|
|
state <= TWL4030_MSTATEC_COMPLETE4)
|
|
return POWER_SUPPLY_STATUS_FULL;
|
|
else
|
|
return POWER_SUPPLY_STATUS_NOT_CHARGING;
|
|
}
|
|
|
|
static int twl4030_bci_get_property(struct power_supply *psy,
|
|
enum power_supply_property psp,
|
|
union power_supply_propval *val)
|
|
{
|
|
struct twl4030_bci *bci = dev_get_drvdata(psy->dev.parent);
|
|
int is_charging;
|
|
int state;
|
|
int ret;
|
|
|
|
state = twl4030bci_state(bci);
|
|
if (state < 0)
|
|
return state;
|
|
|
|
if (psy->desc->type == POWER_SUPPLY_TYPE_USB)
|
|
is_charging = state & TWL4030_MSTATEC_USB;
|
|
else
|
|
is_charging = state & TWL4030_MSTATEC_AC;
|
|
|
|
switch (psp) {
|
|
case POWER_SUPPLY_PROP_STATUS:
|
|
if (is_charging)
|
|
val->intval = twl4030_bci_state_to_status(state);
|
|
else
|
|
val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING;
|
|
break;
|
|
case POWER_SUPPLY_PROP_VOLTAGE_NOW:
|
|
/* charging must be active for meaningful result */
|
|
if (!is_charging)
|
|
return -ENODATA;
|
|
if (psy->desc->type == POWER_SUPPLY_TYPE_USB) {
|
|
ret = twl4030bci_read_adc_val(TWL4030_BCIVBUS);
|
|
if (ret < 0)
|
|
return ret;
|
|
/* BCIVBUS uses ADCIN8, 7/1023 V/step */
|
|
val->intval = ret * 6843;
|
|
} else {
|
|
ret = twl4030bci_read_adc_val(TWL4030_BCIVAC);
|
|
if (ret < 0)
|
|
return ret;
|
|
/* BCIVAC uses ADCIN11, 10/1023 V/step */
|
|
val->intval = ret * 9775;
|
|
}
|
|
break;
|
|
case POWER_SUPPLY_PROP_CURRENT_NOW:
|
|
if (!is_charging)
|
|
return -ENODATA;
|
|
/* current measurement is shared between AC and USB */
|
|
ret = twl4030_charger_get_current();
|
|
if (ret < 0)
|
|
return ret;
|
|
val->intval = ret;
|
|
break;
|
|
case POWER_SUPPLY_PROP_ONLINE:
|
|
val->intval = is_charging &&
|
|
twl4030_bci_state_to_status(state) !=
|
|
POWER_SUPPLY_STATUS_NOT_CHARGING;
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static enum power_supply_property twl4030_charger_props[] = {
|
|
POWER_SUPPLY_PROP_STATUS,
|
|
POWER_SUPPLY_PROP_ONLINE,
|
|
POWER_SUPPLY_PROP_VOLTAGE_NOW,
|
|
POWER_SUPPLY_PROP_CURRENT_NOW,
|
|
};
|
|
|
|
#ifdef CONFIG_OF
|
|
static const struct twl4030_bci_platform_data *
|
|
twl4030_bci_parse_dt(struct device *dev)
|
|
{
|
|
struct device_node *np = dev->of_node;
|
|
struct twl4030_bci_platform_data *pdata;
|
|
u32 num;
|
|
|
|
if (!np)
|
|
return NULL;
|
|
pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL);
|
|
if (!pdata)
|
|
return pdata;
|
|
|
|
if (of_property_read_u32(np, "ti,bb-uvolt", &num) == 0)
|
|
pdata->bb_uvolt = num;
|
|
if (of_property_read_u32(np, "ti,bb-uamp", &num) == 0)
|
|
pdata->bb_uamp = num;
|
|
return pdata;
|
|
}
|
|
#else
|
|
static inline const struct twl4030_bci_platform_data *
|
|
twl4030_bci_parse_dt(struct device *dev)
|
|
{
|
|
return NULL;
|
|
}
|
|
#endif
|
|
|
|
static const struct power_supply_desc twl4030_bci_ac_desc = {
|
|
.name = "twl4030_ac",
|
|
.type = POWER_SUPPLY_TYPE_MAINS,
|
|
.properties = twl4030_charger_props,
|
|
.num_properties = ARRAY_SIZE(twl4030_charger_props),
|
|
.get_property = twl4030_bci_get_property,
|
|
};
|
|
|
|
static const struct power_supply_desc twl4030_bci_usb_desc = {
|
|
.name = "twl4030_usb",
|
|
.type = POWER_SUPPLY_TYPE_USB,
|
|
.properties = twl4030_charger_props,
|
|
.num_properties = ARRAY_SIZE(twl4030_charger_props),
|
|
.get_property = twl4030_bci_get_property,
|
|
};
|
|
|
|
static int __init twl4030_bci_probe(struct platform_device *pdev)
|
|
{
|
|
struct twl4030_bci *bci;
|
|
const struct twl4030_bci_platform_data *pdata = pdev->dev.platform_data;
|
|
int ret;
|
|
u32 reg;
|
|
|
|
bci = kzalloc(sizeof(*bci), GFP_KERNEL);
|
|
if (bci == NULL)
|
|
return -ENOMEM;
|
|
|
|
if (!pdata)
|
|
pdata = twl4030_bci_parse_dt(&pdev->dev);
|
|
|
|
bci->dev = &pdev->dev;
|
|
bci->irq_chg = platform_get_irq(pdev, 0);
|
|
bci->irq_bci = platform_get_irq(pdev, 1);
|
|
|
|
/* Only proceed further *IF* battery is physically present */
|
|
ret = twl4030_is_battery_present(bci);
|
|
if (ret) {
|
|
dev_crit(&pdev->dev, "Battery was not detected:%d\n", ret);
|
|
goto fail_no_battery;
|
|
}
|
|
|
|
platform_set_drvdata(pdev, bci);
|
|
|
|
bci->ac = power_supply_register(&pdev->dev, &twl4030_bci_ac_desc,
|
|
NULL);
|
|
if (IS_ERR(bci->ac)) {
|
|
ret = PTR_ERR(bci->ac);
|
|
dev_err(&pdev->dev, "failed to register ac: %d\n", ret);
|
|
goto fail_register_ac;
|
|
}
|
|
|
|
bci->usb_reg = regulator_get(bci->dev, "bci3v1");
|
|
|
|
bci->usb = power_supply_register(&pdev->dev, &twl4030_bci_usb_desc,
|
|
NULL);
|
|
if (IS_ERR(bci->usb)) {
|
|
ret = PTR_ERR(bci->usb);
|
|
dev_err(&pdev->dev, "failed to register usb: %d\n", ret);
|
|
goto fail_register_usb;
|
|
}
|
|
|
|
ret = request_threaded_irq(bci->irq_chg, NULL,
|
|
twl4030_charger_interrupt, IRQF_ONESHOT, pdev->name,
|
|
bci);
|
|
if (ret < 0) {
|
|
dev_err(&pdev->dev, "could not request irq %d, status %d\n",
|
|
bci->irq_chg, ret);
|
|
goto fail_chg_irq;
|
|
}
|
|
|
|
ret = request_threaded_irq(bci->irq_bci, NULL,
|
|
twl4030_bci_interrupt, IRQF_ONESHOT, pdev->name, bci);
|
|
if (ret < 0) {
|
|
dev_err(&pdev->dev, "could not request irq %d, status %d\n",
|
|
bci->irq_bci, ret);
|
|
goto fail_bci_irq;
|
|
}
|
|
|
|
INIT_WORK(&bci->work, twl4030_bci_usb_work);
|
|
|
|
bci->transceiver = usb_get_phy(USB_PHY_TYPE_USB2);
|
|
if (!IS_ERR_OR_NULL(bci->transceiver)) {
|
|
bci->usb_nb.notifier_call = twl4030_bci_usb_ncb;
|
|
usb_register_notifier(bci->transceiver, &bci->usb_nb);
|
|
}
|
|
|
|
/* Enable interrupts now. */
|
|
reg = ~(u32)(TWL4030_ICHGLOW | TWL4030_ICHGEOC | TWL4030_TBATOR2 |
|
|
TWL4030_TBATOR1 | TWL4030_BATSTS);
|
|
ret = twl_i2c_write_u8(TWL4030_MODULE_INTERRUPTS, reg,
|
|
TWL4030_INTERRUPTS_BCIIMR1A);
|
|
if (ret < 0) {
|
|
dev_err(&pdev->dev, "failed to unmask interrupts: %d\n", ret);
|
|
goto fail_unmask_interrupts;
|
|
}
|
|
|
|
reg = ~(u32)(TWL4030_VBATOV | TWL4030_VBUSOV | TWL4030_ACCHGOV);
|
|
ret = twl_i2c_write_u8(TWL4030_MODULE_INTERRUPTS, reg,
|
|
TWL4030_INTERRUPTS_BCIIMR2A);
|
|
if (ret < 0)
|
|
dev_warn(&pdev->dev, "failed to unmask interrupts: %d\n", ret);
|
|
|
|
twl4030_charger_enable_ac(true);
|
|
twl4030_charger_enable_usb(bci, true);
|
|
if (pdata)
|
|
twl4030_charger_enable_backup(pdata->bb_uvolt,
|
|
pdata->bb_uamp);
|
|
else
|
|
twl4030_charger_enable_backup(0, 0);
|
|
|
|
return 0;
|
|
|
|
fail_unmask_interrupts:
|
|
if (!IS_ERR_OR_NULL(bci->transceiver)) {
|
|
usb_unregister_notifier(bci->transceiver, &bci->usb_nb);
|
|
usb_put_phy(bci->transceiver);
|
|
}
|
|
free_irq(bci->irq_bci, bci);
|
|
fail_bci_irq:
|
|
free_irq(bci->irq_chg, bci);
|
|
fail_chg_irq:
|
|
power_supply_unregister(bci->usb);
|
|
fail_register_usb:
|
|
power_supply_unregister(bci->ac);
|
|
fail_register_ac:
|
|
fail_no_battery:
|
|
kfree(bci);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int __exit twl4030_bci_remove(struct platform_device *pdev)
|
|
{
|
|
struct twl4030_bci *bci = platform_get_drvdata(pdev);
|
|
|
|
twl4030_charger_enable_ac(false);
|
|
twl4030_charger_enable_usb(bci, false);
|
|
twl4030_charger_enable_backup(0, 0);
|
|
|
|
/* mask interrupts */
|
|
twl_i2c_write_u8(TWL4030_MODULE_INTERRUPTS, 0xff,
|
|
TWL4030_INTERRUPTS_BCIIMR1A);
|
|
twl_i2c_write_u8(TWL4030_MODULE_INTERRUPTS, 0xff,
|
|
TWL4030_INTERRUPTS_BCIIMR2A);
|
|
|
|
if (!IS_ERR_OR_NULL(bci->transceiver)) {
|
|
usb_unregister_notifier(bci->transceiver, &bci->usb_nb);
|
|
usb_put_phy(bci->transceiver);
|
|
}
|
|
free_irq(bci->irq_bci, bci);
|
|
free_irq(bci->irq_chg, bci);
|
|
power_supply_unregister(bci->usb);
|
|
power_supply_unregister(bci->ac);
|
|
kfree(bci);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct of_device_id twl_bci_of_match[] = {
|
|
{.compatible = "ti,twl4030-bci", },
|
|
{ }
|
|
};
|
|
MODULE_DEVICE_TABLE(of, twl_bci_of_match);
|
|
|
|
static struct platform_driver twl4030_bci_driver = {
|
|
.driver = {
|
|
.name = "twl4030_bci",
|
|
.of_match_table = of_match_ptr(twl_bci_of_match),
|
|
},
|
|
.remove = __exit_p(twl4030_bci_remove),
|
|
};
|
|
|
|
module_platform_driver_probe(twl4030_bci_driver, twl4030_bci_probe);
|
|
|
|
MODULE_AUTHOR("Gražvydas Ignotas");
|
|
MODULE_DESCRIPTION("TWL4030 Battery Charger Interface driver");
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_ALIAS("platform:twl4030_bci");
|