blob: bc1b4d7c5625234eed4f054cfa3b22c61dd50eae [file] [log] [blame]
/* drivers/power/ds2784_battery.c
*
* Copyright (C) 2009 HTC Corporation
* Copyright (C) 2009 Google, Inc.
*
* This software is licensed under the terms of the GNU General Public
* License version 2, as published by the Free Software Foundation, and
* may be copied, distributed, and modified under those terms.
*
* 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.
*
*/
#include <linux/module.h>
#include <linux/param.h>
#include <linux/jiffies.h>
#include <linux/workqueue.h>
#include <linux/pm.h>
#include <linux/platform_device.h>
#include <linux/power_supply.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/android_alarm.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/err.h>
#include <linux/wakelock.h>
#include <linux/debugfs.h>
#include <linux/seq_file.h>
#include <linux/mutex.h>
#include <linux/ds2784_battery.h>
#include "../w1/w1.h"
#include "w1_ds2784.h"
extern int is_ac_power_supplied(void);
struct battery_status {
int timestamp;
int voltage_uV; /* units of uV */
int current_uA; /* units of uA */
int current_avg_uA;
int charge_uAh;
u16 temp_C; /* units of 0.1 C */
u8 percentage; /* battery percentage */
u8 charge_source;
u8 status_reg;
u8 battery_full; /* battery full (don't charge) */
u8 cooldown; /* was overtemp */
u8 charge_mode;
} __attribute__((packed));
#define SOURCE_NONE 0
#define SOURCE_USB 1
#define SOURCE_AC 2
#define CHARGE_OFF 0
#define CHARGE_SLOW 1
#define CHARGE_FAST 2
#define CHARGE_BATT_DISABLE 3 /* disable charging at battery */
#define TEMP_CRITICAL 600 /* no charging at all */
#define TEMP_HOT 500 /* no fast charge, no charge > 4.1v */
#define TEMP_WARM 450 /* no fast charge above this */
#define TEMP_HOT_MAX_MV 4100 /* stop charging here when hot */
#define TEMP_HOT_MIN_MV 3800 /* resume charging here when hot */
#define CE_DISABLE_MIN_MV 4100
#define BATTERY_LOG_MAX 1024
#define BATTERY_LOG_MASK (BATTERY_LOG_MAX - 1)
/* When we're awake or running on wall power, sample the battery
* gauge every FAST_POLL seconds. If we're asleep and on battery
* power, sample every SLOW_POLL seconds
*/
#define FAST_POLL (1 * 60)
#define SLOW_POLL (10 * 60)
static DEFINE_MUTEX(battery_log_lock);
static struct battery_status battery_log[BATTERY_LOG_MAX];
static unsigned battery_log_head;
static unsigned battery_log_tail;
void battery_log_status(struct battery_status *s)
{
unsigned n;
mutex_lock(&battery_log_lock);
n = battery_log_head;
memcpy(battery_log + n, s, sizeof(struct battery_status));
n = (n + 1) & BATTERY_LOG_MASK;
if (n == battery_log_tail)
battery_log_tail = (battery_log_tail + 1) & BATTERY_LOG_MASK;
battery_log_head = n;
mutex_unlock(&battery_log_lock);
}
static const char *battery_source[3] = { "none", " usb", " ac" };
static const char *battery_mode[4] = { " off", "slow", "fast", "full" };
static int battery_log_print(struct seq_file *sf, void *private)
{
unsigned n;
mutex_lock(&battery_log_lock);
seq_printf(sf, "timestamp mV mA avg mA uAh dC %% src mode reg full\n");
for (n = battery_log_tail; n != battery_log_head; n = (n + 1) & BATTERY_LOG_MASK) {
struct battery_status *s = battery_log + n;
seq_printf(sf, "%9d %5d %6d %6d %8d %4d %3d %s %s 0x%02x %d\n",
s->timestamp, s->voltage_uV / 1000,
s->current_uA / 1000, s->current_avg_uA / 1000,
s->charge_uAh, s->temp_C,
s->percentage,
battery_source[s->charge_source],
battery_mode[s->charge_mode],
s->status_reg, s->battery_full);
}
mutex_unlock(&battery_log_lock);
return 0;
}
struct ds2784_device_info {
struct device *dev;
/* DS2784 data, valid after calling ds2784_battery_read_status() */
char raw[DS2784_DATA_SIZE]; /* raw DS2784 data */
struct battery_status status;
struct power_supply bat;
struct workqueue_struct *monitor_wqueue;
struct work_struct monitor_work;
struct alarm alarm;
struct wake_lock work_wake_lock;
int (*charge)(int on, int fast);
struct w1_slave *w1_slave;
u8 dummy; /* dummy battery flag */
u8 last_charge_mode; /* previous charger state */
u8 slow_poll;
ktime_t last_poll;
ktime_t last_charge_seen;
};
#define psy_to_dev_info(x) container_of((x), struct ds2784_device_info, bat)
static struct wake_lock vbus_wake_lock;
#define BATT_RSNSP (67) /*Passion battery source 1*/
static enum power_supply_property battery_properties[] = {
POWER_SUPPLY_PROP_STATUS,
POWER_SUPPLY_PROP_HEALTH,
POWER_SUPPLY_PROP_PRESENT,
POWER_SUPPLY_PROP_TECHNOLOGY,
POWER_SUPPLY_PROP_CAPACITY,
POWER_SUPPLY_PROP_VOLTAGE_NOW,
POWER_SUPPLY_PROP_TEMP,
POWER_SUPPLY_PROP_CURRENT_NOW,
POWER_SUPPLY_PROP_CURRENT_AVG,
POWER_SUPPLY_PROP_CHARGE_COUNTER,
};
static int battery_initial;
static int battery_get_property(struct power_supply *psy,
enum power_supply_property psp,
union power_supply_propval *val);
static void battery_ext_power_changed(struct power_supply *psy);
#define to_ds2784_device_info(x) container_of((x), struct ds2784_device_info, \
bat);
static void ds2784_parse_data(u8 *raw, struct battery_status *s)
{
short n;
/* Get status reg */
s->status_reg = raw[DS2784_REG_STS];
/* Get Level */
s->percentage = raw[DS2784_REG_RARC];
/* Get Voltage: Unit=4.886mV, range is 0V to 4.99V */
n = (((raw[DS2784_REG_VOLT_MSB] << 8) |
(raw[DS2784_REG_VOLT_LSB])) >> 5);
s->voltage_uV = n * 4886;
/* Get Current: Unit= 1.5625uV x Rsnsp(67)=104.68 */
n = ((raw[DS2784_REG_CURR_MSB]) << 8) |
raw[DS2784_REG_CURR_LSB];
s->current_uA = ((n * 15625) / 10000) * 67;
n = ((raw[DS2784_REG_AVG_CURR_MSB]) << 8) |
raw[DS2784_REG_AVG_CURR_LSB];
s->current_avg_uA = ((n * 15625) / 10000) * 67;
/* Get Temperature:
* 11 bit signed result in Unit=0.125 degree C.
* Convert to integer tenths of degree C.
*/
n = ((raw[DS2784_REG_TEMP_MSB] << 8) |
(raw[DS2784_REG_TEMP_LSB])) >> 5;
s->temp_C = (n * 10) / 8;
/* RAAC is in units of 1.6mAh */
s->charge_uAh = ((raw[DS2784_REG_RAAC_MSB] << 8) |
raw[DS2784_REG_RAAC_LSB]) * 1600;
}
static int w1_ds2784_io(struct w1_slave *sl, char *buf, int addr, size_t count, int io)
{
if (!sl)
return 0;
mutex_lock(&sl->master->mutex);
if (addr > DS2784_DATA_SIZE || addr < 0) {
count = 0;
goto out;
}
if (addr + count > DS2784_DATA_SIZE)
count = DS2784_DATA_SIZE - addr;
if (!w1_reset_select_slave(sl)) {
if (!io) {
w1_write_8(sl->master, W1_DS2784_READ_DATA);
w1_write_8(sl->master, addr);
count = w1_read_block(sl->master, buf, count);
} else {
w1_write_8(sl->master, W1_DS2784_WRITE_DATA);
w1_write_8(sl->master, addr);
w1_write_block(sl->master, buf, count);
/* XXX w1_write_block returns void, not n_written */
}
}
out:
mutex_unlock(&sl->master->mutex);
return count;
}
static int w1_ds2784_read(struct w1_slave *sl, char *buf, int addr, size_t count)
{
return w1_ds2784_io(sl, buf, addr, count, 0);
}
static int w1_ds2784_write(struct w1_slave *sl, char *buf, int addr, size_t count)
{
return w1_ds2784_io(sl, buf, addr, count, 1);
}
static int ds2784_set_cc(struct ds2784_device_info *di, bool enable)
{
int ret;
if (enable)
di->raw[DS2784_REG_PORT] |= 0x02;
else
di->raw[DS2784_REG_PORT] &= ~0x02;
ret = w1_ds2784_write(di->w1_slave, di->raw + DS2784_REG_PORT,
DS2784_REG_PORT, 1);
if (ret != 1) {
dev_warn(di->dev, "call to w1_ds2784_write failed (0x%p)\n",
di->w1_slave);
return 1;
}
return 0;
}
static int ds2784_battery_read_status(struct ds2784_device_info *di)
{
int ret, start, count;
/* The first time we read the entire contents of SRAM/EEPROM,
* but after that we just read the interesting bits that change. */
if (di->raw[DS2784_REG_RSNSP] == 0x00) {
start = 0;
count = DS2784_DATA_SIZE;
} else {
start = DS2784_REG_PORT;
count = DS2784_REG_CURR_LSB - start + 1;
}
ret = w1_ds2784_read(di->w1_slave, di->raw + start, start, count);
if (ret != count) {
dev_warn(di->dev, "call to w1_ds2784_read failed (0x%p)\n",
di->w1_slave);
return 1;
}
if (battery_initial == 0) {
if (!memcmp(di->raw + 0x20, "DUMMY!", 6)) {
unsigned char acr[2];
di->dummy = 1;
pr_info("batt: dummy battery detected\n");
/* reset ACC register to ~500mAh, since it may have zeroed out */
acr[0] = 0x05;
acr[1] = 0x06;
w1_ds2784_write(di->w1_slave, acr, DS2784_REG_ACCUMULATE_CURR_MSB, 2);
}
battery_initial = 1;
}
ds2784_parse_data(di->raw, &di->status);
pr_info("batt: %3d%%, %d mV, %d mA (%d avg), %d.%d C, %d mAh\n",
di->status.percentage,
di->status.voltage_uV / 1000, di->status.current_uA / 1000,
di->status.current_avg_uA / 1000,
di->status.temp_C / 10, di->status.temp_C % 10,
di->status.charge_uAh / 1000);
return 0;
}
static int battery_get_property(struct power_supply *psy,
enum power_supply_property psp,
union power_supply_propval *val)
{
struct ds2784_device_info *di = psy_to_dev_info(psy);
switch (psp) {
case POWER_SUPPLY_PROP_STATUS:
switch (di->status.charge_source) {
case CHARGE_OFF:
val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
break;
case CHARGE_FAST:
case CHARGE_SLOW:
if (di->status.battery_full)
val->intval = POWER_SUPPLY_STATUS_FULL;
else if (di->status.charge_mode == CHARGE_OFF ||
di->status.charge_mode == CHARGE_BATT_DISABLE)
val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING;
else
val->intval = POWER_SUPPLY_STATUS_CHARGING;
break;
default:
val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
break;
}
break;
case POWER_SUPPLY_PROP_HEALTH:
if (di->status.temp_C >= TEMP_HOT)
val->intval = POWER_SUPPLY_HEALTH_OVERHEAT;
else
val->intval = POWER_SUPPLY_HEALTH_GOOD;
break;
case POWER_SUPPLY_PROP_PRESENT:
/* XXX todo */
val->intval = 1;
break;
case POWER_SUPPLY_PROP_TECHNOLOGY:
if (di->dummy)
val->intval = POWER_SUPPLY_TECHNOLOGY_UNKNOWN;
else
val->intval = POWER_SUPPLY_TECHNOLOGY_LION;
break;
case POWER_SUPPLY_PROP_CAPACITY:
if (di->dummy)
val->intval = 75;
else
val->intval = di->status.percentage;
break;
case POWER_SUPPLY_PROP_VOLTAGE_NOW:
val->intval = di->status.voltage_uV;
break;
case POWER_SUPPLY_PROP_TEMP:
val->intval = di->status.temp_C;
break;
case POWER_SUPPLY_PROP_CURRENT_NOW:
val->intval = di->status.current_uA;
break;
case POWER_SUPPLY_PROP_CURRENT_AVG:
val->intval = di->status.current_avg_uA;
break;
case POWER_SUPPLY_PROP_CHARGE_COUNTER:
val->intval = di->status.charge_uAh;
break;
default:
return -EINVAL;
}
return 0;
}
static void ds2784_battery_update_status(struct ds2784_device_info *di)
{
u8 last_level;
last_level = di->status.percentage;
ds2784_battery_read_status(di);
if ((last_level != di->status.percentage) || (di->status.temp_C > 450))
power_supply_changed(&di->bat);
}
static DEFINE_MUTEX(charge_state_lock);
static bool check_timeout(ktime_t now, ktime_t last, int seconds)
{
ktime_t timeout = ktime_add(last, ktime_set(seconds, 0));
return ktime_sub(timeout, now).tv64 < 0;
}
static int battery_adjust_charge_state(struct ds2784_device_info *di)
{
unsigned source;
int rc = 0;
int temp, volt;
u8 charge_mode;
bool charge_timeout = false;
mutex_lock(&charge_state_lock);
temp = di->status.temp_C;
volt = di->status.voltage_uV / 1000;
source = di->status.charge_source;
/* initially our charge mode matches our source:
* NONE:OFF, USB:SLOW, AC:FAST
*/
charge_mode = source;
/* shut off charger when full:
* - CHGTF flag is set
*/
if (di->status.status_reg & 0x80) {
di->status.battery_full = 1;
charge_mode = CHARGE_BATT_DISABLE;
} else
di->status.battery_full = 0;
if (temp >= TEMP_HOT) {
if (temp >= TEMP_CRITICAL)
charge_mode = CHARGE_BATT_DISABLE;
/* once we charge to max voltage when hot, disable
* charging until the temp drops or the voltage drops
*/
if (volt >= TEMP_HOT_MAX_MV)
di->status.cooldown = 1;
}
/* when the battery is warm, only charge in slow charge mode */
if ((temp >= TEMP_WARM) && (charge_mode == CHARGE_FAST))
charge_mode = CHARGE_SLOW;
if (di->status.cooldown) {
if ((temp < TEMP_WARM) || (volt <= TEMP_HOT_MIN_MV))
di->status.cooldown = 0;
else
charge_mode = CHARGE_BATT_DISABLE;
}
if (di->status.current_uA > 1024)
di->last_charge_seen = di->last_poll;
else if (di->last_charge_mode != CHARGE_OFF &&
check_timeout(di->last_poll, di->last_charge_seen, 60 * 60)) {
if (di->last_charge_mode == CHARGE_BATT_DISABLE) {
/* The charger is only powering the phone. Toggle the
* enable line periodically to prevent auto shutdown.
*/
di->last_charge_seen = di->last_poll;
pr_info("batt: charging POKE CHARGER\n");
di->charge(0, 0);
udelay(10);
di->charge(1, source == CHARGE_FAST);
} else {
/* The charger has probably stopped charging. Turn it
* off until the next sample period.
*/
charge_timeout = true;
charge_mode = CHARGE_OFF;
}
}
if (source == CHARGE_OFF)
charge_mode = CHARGE_OFF;
/* Don't use CHARGE_BATT_DISABLE unless the voltage is high since the
* voltage drop over the discharge-path diode can cause a shutdown.
*/
if (charge_mode == CHARGE_BATT_DISABLE && volt < CE_DISABLE_MIN_MV)
charge_mode = CHARGE_OFF;
if (di->last_charge_mode == charge_mode)
goto done;
di->last_charge_mode = charge_mode;
di->status.charge_mode = charge_mode;
switch (charge_mode) {
case CHARGE_OFF:
di->charge(0, 0);
ds2784_set_cc(di, true);
if (temp >= TEMP_CRITICAL)
pr_info("batt: charging OFF [OVERTEMP]\n");
else if (di->status.cooldown)
pr_info("batt: charging OFF [COOLDOWN]\n");
else if (di->status.battery_full)
pr_info("batt: charging OFF [FULL]\n");
else if (charge_timeout)
pr_info("batt: charging OFF [TIMEOUT]\n");
else
pr_info("batt: charging OFF\n");
break;
case CHARGE_BATT_DISABLE:
di->last_charge_seen = di->last_poll;
ds2784_set_cc(di, false);
di->charge(1, source == CHARGE_FAST);
if (temp >= TEMP_CRITICAL)
pr_info("batt: charging BATTOFF [OVERTEMP]\n");
else if (di->status.cooldown)
pr_info("batt: charging BATTOFF [COOLDOWN]\n");
else if (di->status.battery_full)
pr_info("batt: charging BATTOFF [FULL]\n");
else
pr_info("batt: charging BATTOFF [UNKNOWN]\n");
break;
case CHARGE_SLOW:
di->last_charge_seen = di->last_poll;
ds2784_set_cc(di, true);
di->charge(1, 0);
pr_info("batt: charging SLOW\n");
break;
case CHARGE_FAST:
di->last_charge_seen = di->last_poll;
ds2784_set_cc(di, true);
di->charge(1, 1);
pr_info("batt: charging FAST\n");
break;
}
rc = 1;
done:
mutex_unlock(&charge_state_lock);
return rc;
}
static void ds2784_program_alarm(struct ds2784_device_info *di, int seconds)
{
ktime_t low_interval = ktime_set(seconds - 10, 0);
ktime_t slack = ktime_set(20, 0);
ktime_t next;
next = ktime_add(di->last_poll, low_interval);
alarm_start_range(&di->alarm, next, ktime_add(next, slack));
}
static void ds2784_battery_work(struct work_struct *work)
{
struct ds2784_device_info *di =
container_of(work, struct ds2784_device_info, monitor_work);
struct timespec ts;
unsigned long flags;
ds2784_battery_update_status(di);
di->last_poll = alarm_get_elapsed_realtime();
if (battery_adjust_charge_state(di))
power_supply_changed(&di->bat);
ts = ktime_to_timespec(di->last_poll);
di->status.timestamp = ts.tv_sec;
battery_log_status(&di->status);
/* prevent suspend before starting the alarm */
local_irq_save(flags);
wake_unlock(&di->work_wake_lock);
ds2784_program_alarm(di, FAST_POLL);
local_irq_restore(flags);
}
static void ds2784_battery_alarm(struct alarm *alarm)
{
struct ds2784_device_info *di =
container_of(alarm, struct ds2784_device_info, alarm);
wake_lock(&di->work_wake_lock);
queue_work(di->monitor_wqueue, &di->monitor_work);
}
static void battery_ext_power_changed(struct power_supply *psy)
{
struct ds2784_device_info *di;
int got_power;
di = psy_to_dev_info(psy);
got_power = power_supply_am_i_supplied(psy);
if (got_power) {
if (is_ac_power_supplied())
di->status.charge_source = SOURCE_AC;
else
di->status.charge_source = SOURCE_USB;
wake_lock(&vbus_wake_lock);
} else {
di->status.charge_source = SOURCE_NONE;
/* give userspace some time to see the uevent and update
* LED state or whatnot...
*/
wake_lock_timeout(&vbus_wake_lock, HZ / 2);
}
battery_adjust_charge_state(di);
power_supply_changed(psy);
}
static int ds2784_battery_probe(struct platform_device *pdev)
{
int rc;
struct ds2784_device_info *di;
struct ds2784_platform_data *pdata;
di = kzalloc(sizeof(*di), GFP_KERNEL);
if (!di)
return -ENOMEM;
platform_set_drvdata(pdev, di);
pdata = pdev->dev.platform_data;
if (!pdata || !pdata->charge || !pdata->w1_slave) {
pr_err("%s: pdata missing or invalid\n", __func__);
rc = -EINVAL;
goto fail_register;
}
di->charge = pdata->charge;
di->w1_slave = pdata->w1_slave;
di->dev = &pdev->dev;
di->bat.name = "battery";
di->bat.type = POWER_SUPPLY_TYPE_BATTERY;
di->bat.properties = battery_properties;
di->bat.num_properties = ARRAY_SIZE(battery_properties);
di->bat.external_power_changed = battery_ext_power_changed;
di->bat.get_property = battery_get_property;
di->last_charge_mode = 0xff;
rc = power_supply_register(&pdev->dev, &di->bat);
if (rc)
goto fail_register;
INIT_WORK(&di->monitor_work, ds2784_battery_work);
di->monitor_wqueue = create_freezeable_workqueue(dev_name(&pdev->dev));
/* init to something sane */
di->last_poll = alarm_get_elapsed_realtime();
if (!di->monitor_wqueue) {
rc = -ESRCH;
goto fail_workqueue;
}
wake_lock_init(&di->work_wake_lock, WAKE_LOCK_SUSPEND,
"ds2784-battery");
alarm_init(&di->alarm, ANDROID_ALARM_ELAPSED_REALTIME_WAKEUP,
ds2784_battery_alarm);
wake_lock(&di->work_wake_lock);
queue_work(di->monitor_wqueue, &di->monitor_work);
return 0;
fail_workqueue:
power_supply_unregister(&di->bat);
fail_register:
kfree(di);
return rc;
}
static int ds2784_suspend(struct device *dev)
{
struct ds2784_device_info *di = dev_get_drvdata(dev);
/* If we are on battery, reduce our update rate until
* we next resume.
*/
if (di->status.charge_source == SOURCE_NONE) {
ds2784_program_alarm(di, SLOW_POLL);
di->slow_poll = 1;
}
return 0;
}
static int ds2784_resume(struct device *dev)
{
struct ds2784_device_info *di = dev_get_drvdata(dev);
/* We might be on a slow sample cycle. If we're
* resuming we should resample the battery state
* if it's been over a minute since we last did
* so, and move back to sampling every minute until
* we suspend again.
*/
if (di->slow_poll) {
ds2784_program_alarm(di, FAST_POLL);
di->slow_poll = 0;
}
return 0;
}
static struct dev_pm_ops ds2784_pm_ops = {
.suspend = ds2784_suspend,
.resume = ds2784_resume,
};
static struct platform_driver ds2784_battery_driver = {
.driver = {
.name = "ds2784-battery",
.pm = &ds2784_pm_ops,
},
.probe = ds2784_battery_probe,
};
static int battery_log_open(struct inode *inode, struct file *file)
{
return single_open(file, battery_log_print, NULL);
}
static struct file_operations battery_log_fops = {
.open = battery_log_open,
.read = seq_read,
.llseek = seq_lseek,
.release = single_release,
};
static int __init ds2784_battery_init(void)
{
debugfs_create_file("battery_log", 0444, NULL, NULL, &battery_log_fops);
wake_lock_init(&vbus_wake_lock, WAKE_LOCK_SUSPEND, "vbus_present");
return platform_driver_register(&ds2784_battery_driver);
}
module_init(ds2784_battery_init);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Justin Lin <Justin_lin@htc.com>");
MODULE_DESCRIPTION("ds2784 battery driver");