25fd330370
The function to retrieve battery info (from the device tree) assumes we have a static info struct that gets populated by calling into power_supply_get_battery_info(). This is awkward since I want to support tables of static battery info by just assigning a pointer to all info based on e.g. a compatible value in the device tree. We also have a mixture of static and dynamically allocated variables here. Bite the bullet and let power_supply_get_battery_info() allocate also the memory used for the very top level struct power_supply_battery_info container. Pass pointers around and lifecycle this with the psy device just like the stuff we allocate inside it. Change all current users over. As part of the change, initializers need to be added to some previously uninitialized fields in struct power_supply_battery_info. Reviewed-By: Matti Vaittinen <matti.vaittinen@fi.rohmeurope.com> Signed-off-by: Linus Walleij <linus.walleij@linaro.org> Signed-off-by: Sebastian Reichel <sebastian.reichel@collabora.com>
765 lines
19 KiB
C
765 lines
19 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Fuel gauge driver for CellWise 2013 / 2015
|
|
*
|
|
* Copyright (C) 2012, RockChip
|
|
* Copyright (C) 2020, Tobias Schramm
|
|
*
|
|
* Authors: xuhuicong <xhc@rock-chips.com>
|
|
* Authors: Tobias Schramm <t.schramm@manjaro.org>
|
|
*/
|
|
|
|
#include <linux/bits.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/i2c.h>
|
|
#include <linux/gfp.h>
|
|
#include <linux/gpio/consumer.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/module.h>
|
|
#include <linux/power_supply.h>
|
|
#include <linux/property.h>
|
|
#include <linux/regmap.h>
|
|
#include <linux/time.h>
|
|
#include <linux/workqueue.h>
|
|
|
|
#define CW2015_SIZE_BATINFO 64
|
|
|
|
#define CW2015_RESET_TRIES 5
|
|
|
|
#define CW2015_REG_VERSION 0x00
|
|
#define CW2015_REG_VCELL 0x02
|
|
#define CW2015_REG_SOC 0x04
|
|
#define CW2015_REG_RRT_ALERT 0x06
|
|
#define CW2015_REG_CONFIG 0x08
|
|
#define CW2015_REG_MODE 0x0A
|
|
#define CW2015_REG_BATINFO 0x10
|
|
|
|
#define CW2015_MODE_SLEEP_MASK GENMASK(7, 6)
|
|
#define CW2015_MODE_SLEEP (0x03 << 6)
|
|
#define CW2015_MODE_NORMAL (0x00 << 6)
|
|
#define CW2015_MODE_QUICK_START (0x03 << 4)
|
|
#define CW2015_MODE_RESTART (0x0f << 0)
|
|
|
|
#define CW2015_CONFIG_UPDATE_FLG (0x01 << 1)
|
|
#define CW2015_ATHD(x) ((x) << 3)
|
|
#define CW2015_MASK_ATHD GENMASK(7, 3)
|
|
#define CW2015_MASK_SOC GENMASK(12, 0)
|
|
|
|
/* reset gauge of no valid state of charge could be polled for 40s */
|
|
#define CW2015_BAT_SOC_ERROR_MS (40 * MSEC_PER_SEC)
|
|
/* reset gauge if state of charge stuck for half an hour during charging */
|
|
#define CW2015_BAT_CHARGING_STUCK_MS (1800 * MSEC_PER_SEC)
|
|
|
|
/* poll interval from CellWise GPL Android driver example */
|
|
#define CW2015_DEFAULT_POLL_INTERVAL_MS 8000
|
|
|
|
#define CW2015_AVERAGING_SAMPLES 3
|
|
|
|
struct cw_battery {
|
|
struct device *dev;
|
|
struct workqueue_struct *battery_workqueue;
|
|
struct delayed_work battery_delay_work;
|
|
struct regmap *regmap;
|
|
struct power_supply *rk_bat;
|
|
struct power_supply_battery_info *battery;
|
|
u8 *bat_profile;
|
|
|
|
bool charger_attached;
|
|
bool battery_changed;
|
|
|
|
int soc;
|
|
int voltage_mv;
|
|
int status;
|
|
int time_to_empty;
|
|
int charge_count;
|
|
|
|
u32 poll_interval_ms;
|
|
u8 alert_level;
|
|
|
|
unsigned int read_errors;
|
|
unsigned int charge_stuck_cnt;
|
|
};
|
|
|
|
static int cw_read_word(struct cw_battery *cw_bat, u8 reg, u16 *val)
|
|
{
|
|
__be16 value;
|
|
int ret;
|
|
|
|
ret = regmap_bulk_read(cw_bat->regmap, reg, &value, sizeof(value));
|
|
if (ret)
|
|
return ret;
|
|
|
|
*val = be16_to_cpu(value);
|
|
return 0;
|
|
}
|
|
|
|
static int cw_update_profile(struct cw_battery *cw_bat)
|
|
{
|
|
int ret;
|
|
unsigned int reg_val;
|
|
u8 reset_val;
|
|
|
|
/* make sure gauge is not in sleep mode */
|
|
ret = regmap_read(cw_bat->regmap, CW2015_REG_MODE, ®_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
reset_val = reg_val;
|
|
if ((reg_val & CW2015_MODE_SLEEP_MASK) == CW2015_MODE_SLEEP) {
|
|
dev_err(cw_bat->dev,
|
|
"Gauge is in sleep mode, can't update battery info\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* write new battery info */
|
|
ret = regmap_raw_write(cw_bat->regmap, CW2015_REG_BATINFO,
|
|
cw_bat->bat_profile,
|
|
CW2015_SIZE_BATINFO);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* set config update flag */
|
|
reg_val |= CW2015_CONFIG_UPDATE_FLG;
|
|
reg_val &= ~CW2015_MASK_ATHD;
|
|
reg_val |= CW2015_ATHD(cw_bat->alert_level);
|
|
ret = regmap_write(cw_bat->regmap, CW2015_REG_CONFIG, reg_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* reset gauge to apply new battery profile */
|
|
reset_val &= ~CW2015_MODE_RESTART;
|
|
reg_val = reset_val | CW2015_MODE_RESTART;
|
|
ret = regmap_write(cw_bat->regmap, CW2015_REG_MODE, reg_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* wait for gauge to reset */
|
|
msleep(20);
|
|
|
|
/* clear reset flag */
|
|
ret = regmap_write(cw_bat->regmap, CW2015_REG_MODE, reset_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* wait for gauge to become ready */
|
|
ret = regmap_read_poll_timeout(cw_bat->regmap, CW2015_REG_SOC,
|
|
reg_val, reg_val <= 100,
|
|
10 * USEC_PER_MSEC, 10 * USEC_PER_SEC);
|
|
if (ret)
|
|
dev_err(cw_bat->dev,
|
|
"Gauge did not become ready after profile upload\n");
|
|
else
|
|
dev_dbg(cw_bat->dev, "Battery profile updated\n");
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int cw_init(struct cw_battery *cw_bat)
|
|
{
|
|
int ret;
|
|
unsigned int reg_val = CW2015_MODE_SLEEP;
|
|
|
|
if ((reg_val & CW2015_MODE_SLEEP_MASK) == CW2015_MODE_SLEEP) {
|
|
reg_val = CW2015_MODE_NORMAL;
|
|
ret = regmap_write(cw_bat->regmap, CW2015_REG_MODE, reg_val);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
ret = regmap_read(cw_bat->regmap, CW2015_REG_CONFIG, ®_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if ((reg_val & CW2015_MASK_ATHD) != CW2015_ATHD(cw_bat->alert_level)) {
|
|
dev_dbg(cw_bat->dev, "Setting new alert level\n");
|
|
reg_val &= ~CW2015_MASK_ATHD;
|
|
reg_val |= ~CW2015_ATHD(cw_bat->alert_level);
|
|
ret = regmap_write(cw_bat->regmap, CW2015_REG_CONFIG, reg_val);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
ret = regmap_read(cw_bat->regmap, CW2015_REG_CONFIG, ®_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (!(reg_val & CW2015_CONFIG_UPDATE_FLG)) {
|
|
dev_dbg(cw_bat->dev,
|
|
"Battery profile not present, uploading battery profile\n");
|
|
if (cw_bat->bat_profile) {
|
|
ret = cw_update_profile(cw_bat);
|
|
if (ret) {
|
|
dev_err(cw_bat->dev,
|
|
"Failed to upload battery profile\n");
|
|
return ret;
|
|
}
|
|
} else {
|
|
dev_warn(cw_bat->dev,
|
|
"No profile specified, continuing without profile\n");
|
|
}
|
|
} else if (cw_bat->bat_profile) {
|
|
u8 bat_info[CW2015_SIZE_BATINFO];
|
|
|
|
ret = regmap_raw_read(cw_bat->regmap, CW2015_REG_BATINFO,
|
|
bat_info, CW2015_SIZE_BATINFO);
|
|
if (ret) {
|
|
dev_err(cw_bat->dev,
|
|
"Failed to read stored battery profile\n");
|
|
return ret;
|
|
}
|
|
|
|
if (memcmp(bat_info, cw_bat->bat_profile, CW2015_SIZE_BATINFO)) {
|
|
dev_warn(cw_bat->dev, "Replacing stored battery profile\n");
|
|
ret = cw_update_profile(cw_bat);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
} else {
|
|
dev_warn(cw_bat->dev,
|
|
"Can't check current battery profile, no profile provided\n");
|
|
}
|
|
|
|
dev_dbg(cw_bat->dev, "Battery profile configured\n");
|
|
return 0;
|
|
}
|
|
|
|
static int cw_power_on_reset(struct cw_battery *cw_bat)
|
|
{
|
|
int ret;
|
|
unsigned char reset_val;
|
|
|
|
reset_val = CW2015_MODE_SLEEP;
|
|
ret = regmap_write(cw_bat->regmap, CW2015_REG_MODE, reset_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* wait for gauge to enter sleep */
|
|
msleep(20);
|
|
|
|
reset_val = CW2015_MODE_NORMAL;
|
|
ret = regmap_write(cw_bat->regmap, CW2015_REG_MODE, reset_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = cw_init(cw_bat);
|
|
if (ret)
|
|
return ret;
|
|
return 0;
|
|
}
|
|
|
|
#define HYSTERESIS(current, previous, up, down) \
|
|
(((current) < (previous) + (up)) && ((current) > (previous) - (down)))
|
|
|
|
static int cw_get_soc(struct cw_battery *cw_bat)
|
|
{
|
|
unsigned int soc;
|
|
int ret;
|
|
|
|
ret = regmap_read(cw_bat->regmap, CW2015_REG_SOC, &soc);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (soc > 100) {
|
|
int max_error_cycles =
|
|
CW2015_BAT_SOC_ERROR_MS / cw_bat->poll_interval_ms;
|
|
|
|
dev_err(cw_bat->dev, "Invalid SoC %d%%\n", soc);
|
|
cw_bat->read_errors++;
|
|
if (cw_bat->read_errors > max_error_cycles) {
|
|
dev_warn(cw_bat->dev,
|
|
"Too many invalid SoC reports, resetting gauge\n");
|
|
cw_power_on_reset(cw_bat);
|
|
cw_bat->read_errors = 0;
|
|
}
|
|
return cw_bat->soc;
|
|
}
|
|
cw_bat->read_errors = 0;
|
|
|
|
/* Reset gauge if stuck while charging */
|
|
if (cw_bat->status == POWER_SUPPLY_STATUS_CHARGING && soc == cw_bat->soc) {
|
|
int max_stuck_cycles =
|
|
CW2015_BAT_CHARGING_STUCK_MS / cw_bat->poll_interval_ms;
|
|
|
|
cw_bat->charge_stuck_cnt++;
|
|
if (cw_bat->charge_stuck_cnt > max_stuck_cycles) {
|
|
dev_warn(cw_bat->dev,
|
|
"SoC stuck @%u%%, resetting gauge\n", soc);
|
|
cw_power_on_reset(cw_bat);
|
|
cw_bat->charge_stuck_cnt = 0;
|
|
}
|
|
} else {
|
|
cw_bat->charge_stuck_cnt = 0;
|
|
}
|
|
|
|
/* Ignore voltage dips during charge */
|
|
if (cw_bat->charger_attached && HYSTERESIS(soc, cw_bat->soc, 0, 3))
|
|
soc = cw_bat->soc;
|
|
|
|
/* Ignore voltage spikes during discharge */
|
|
if (!cw_bat->charger_attached && HYSTERESIS(soc, cw_bat->soc, 3, 0))
|
|
soc = cw_bat->soc;
|
|
|
|
return soc;
|
|
}
|
|
|
|
static int cw_get_voltage(struct cw_battery *cw_bat)
|
|
{
|
|
int ret, i, voltage_mv;
|
|
u16 reg_val;
|
|
u32 avg = 0;
|
|
|
|
for (i = 0; i < CW2015_AVERAGING_SAMPLES; i++) {
|
|
ret = cw_read_word(cw_bat, CW2015_REG_VCELL, ®_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
avg += reg_val;
|
|
}
|
|
avg /= CW2015_AVERAGING_SAMPLES;
|
|
|
|
/*
|
|
* 305 uV per ADC step
|
|
* Use 312 / 1024 as efficient approximation of 305 / 1000
|
|
* Negligible error of 0.1%
|
|
*/
|
|
voltage_mv = avg * 312 / 1024;
|
|
|
|
dev_dbg(cw_bat->dev, "Read voltage: %d mV, raw=0x%04x\n",
|
|
voltage_mv, reg_val);
|
|
return voltage_mv;
|
|
}
|
|
|
|
static int cw_get_time_to_empty(struct cw_battery *cw_bat)
|
|
{
|
|
int ret;
|
|
u16 value16;
|
|
|
|
ret = cw_read_word(cw_bat, CW2015_REG_RRT_ALERT, &value16);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return value16 & CW2015_MASK_SOC;
|
|
}
|
|
|
|
static void cw_update_charge_status(struct cw_battery *cw_bat)
|
|
{
|
|
int ret;
|
|
|
|
ret = power_supply_am_i_supplied(cw_bat->rk_bat);
|
|
if (ret < 0) {
|
|
dev_warn(cw_bat->dev, "Failed to get supply state: %d\n", ret);
|
|
} else {
|
|
bool charger_attached;
|
|
|
|
charger_attached = !!ret;
|
|
if (cw_bat->charger_attached != charger_attached) {
|
|
cw_bat->battery_changed = true;
|
|
if (charger_attached)
|
|
cw_bat->charge_count++;
|
|
}
|
|
cw_bat->charger_attached = charger_attached;
|
|
}
|
|
}
|
|
|
|
static void cw_update_soc(struct cw_battery *cw_bat)
|
|
{
|
|
int soc;
|
|
|
|
soc = cw_get_soc(cw_bat);
|
|
if (soc < 0)
|
|
dev_err(cw_bat->dev, "Failed to get SoC from gauge: %d\n", soc);
|
|
else if (cw_bat->soc != soc) {
|
|
cw_bat->soc = soc;
|
|
cw_bat->battery_changed = true;
|
|
}
|
|
}
|
|
|
|
static void cw_update_voltage(struct cw_battery *cw_bat)
|
|
{
|
|
int voltage_mv;
|
|
|
|
voltage_mv = cw_get_voltage(cw_bat);
|
|
if (voltage_mv < 0)
|
|
dev_err(cw_bat->dev, "Failed to get voltage from gauge: %d\n",
|
|
voltage_mv);
|
|
else
|
|
cw_bat->voltage_mv = voltage_mv;
|
|
}
|
|
|
|
static void cw_update_status(struct cw_battery *cw_bat)
|
|
{
|
|
int status = POWER_SUPPLY_STATUS_DISCHARGING;
|
|
|
|
if (cw_bat->charger_attached) {
|
|
if (cw_bat->soc >= 100)
|
|
status = POWER_SUPPLY_STATUS_FULL;
|
|
else
|
|
status = POWER_SUPPLY_STATUS_CHARGING;
|
|
}
|
|
|
|
if (cw_bat->status != status)
|
|
cw_bat->battery_changed = true;
|
|
cw_bat->status = status;
|
|
}
|
|
|
|
static void cw_update_time_to_empty(struct cw_battery *cw_bat)
|
|
{
|
|
int time_to_empty;
|
|
|
|
time_to_empty = cw_get_time_to_empty(cw_bat);
|
|
if (time_to_empty < 0)
|
|
dev_err(cw_bat->dev, "Failed to get time to empty from gauge: %d\n",
|
|
time_to_empty);
|
|
else if (cw_bat->time_to_empty != time_to_empty) {
|
|
cw_bat->time_to_empty = time_to_empty;
|
|
cw_bat->battery_changed = true;
|
|
}
|
|
}
|
|
|
|
static void cw_bat_work(struct work_struct *work)
|
|
{
|
|
struct delayed_work *delay_work;
|
|
struct cw_battery *cw_bat;
|
|
int ret;
|
|
unsigned int reg_val;
|
|
|
|
delay_work = to_delayed_work(work);
|
|
cw_bat = container_of(delay_work, struct cw_battery, battery_delay_work);
|
|
ret = regmap_read(cw_bat->regmap, CW2015_REG_MODE, ®_val);
|
|
if (ret) {
|
|
dev_err(cw_bat->dev, "Failed to read mode from gauge: %d\n", ret);
|
|
} else {
|
|
if ((reg_val & CW2015_MODE_SLEEP_MASK) == CW2015_MODE_SLEEP) {
|
|
int i;
|
|
|
|
for (i = 0; i < CW2015_RESET_TRIES; i++) {
|
|
if (!cw_power_on_reset(cw_bat))
|
|
break;
|
|
}
|
|
}
|
|
cw_update_soc(cw_bat);
|
|
cw_update_voltage(cw_bat);
|
|
cw_update_charge_status(cw_bat);
|
|
cw_update_status(cw_bat);
|
|
cw_update_time_to_empty(cw_bat);
|
|
}
|
|
dev_dbg(cw_bat->dev, "charger_attached = %d\n", cw_bat->charger_attached);
|
|
dev_dbg(cw_bat->dev, "status = %d\n", cw_bat->status);
|
|
dev_dbg(cw_bat->dev, "soc = %d%%\n", cw_bat->soc);
|
|
dev_dbg(cw_bat->dev, "voltage = %dmV\n", cw_bat->voltage_mv);
|
|
|
|
if (cw_bat->battery_changed)
|
|
power_supply_changed(cw_bat->rk_bat);
|
|
cw_bat->battery_changed = false;
|
|
|
|
queue_delayed_work(cw_bat->battery_workqueue,
|
|
&cw_bat->battery_delay_work,
|
|
msecs_to_jiffies(cw_bat->poll_interval_ms));
|
|
}
|
|
|
|
static bool cw_battery_valid_time_to_empty(struct cw_battery *cw_bat)
|
|
{
|
|
return cw_bat->time_to_empty > 0 &&
|
|
cw_bat->time_to_empty < CW2015_MASK_SOC &&
|
|
cw_bat->status == POWER_SUPPLY_STATUS_DISCHARGING;
|
|
}
|
|
|
|
static int cw_battery_get_property(struct power_supply *psy,
|
|
enum power_supply_property psp,
|
|
union power_supply_propval *val)
|
|
{
|
|
struct cw_battery *cw_bat;
|
|
|
|
cw_bat = power_supply_get_drvdata(psy);
|
|
switch (psp) {
|
|
case POWER_SUPPLY_PROP_CAPACITY:
|
|
val->intval = cw_bat->soc;
|
|
break;
|
|
|
|
case POWER_SUPPLY_PROP_STATUS:
|
|
val->intval = cw_bat->status;
|
|
break;
|
|
|
|
case POWER_SUPPLY_PROP_PRESENT:
|
|
val->intval = !!cw_bat->voltage_mv;
|
|
break;
|
|
|
|
case POWER_SUPPLY_PROP_VOLTAGE_NOW:
|
|
val->intval = cw_bat->voltage_mv * 1000;
|
|
break;
|
|
|
|
case POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW:
|
|
if (cw_battery_valid_time_to_empty(cw_bat))
|
|
val->intval = cw_bat->time_to_empty;
|
|
else
|
|
val->intval = 0;
|
|
break;
|
|
|
|
case POWER_SUPPLY_PROP_TECHNOLOGY:
|
|
val->intval = POWER_SUPPLY_TECHNOLOGY_LION;
|
|
break;
|
|
|
|
case POWER_SUPPLY_PROP_CHARGE_COUNTER:
|
|
val->intval = cw_bat->charge_count;
|
|
break;
|
|
|
|
case POWER_SUPPLY_PROP_CHARGE_FULL:
|
|
case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN:
|
|
if (cw_bat->battery->charge_full_design_uah > 0)
|
|
val->intval = cw_bat->battery->charge_full_design_uah;
|
|
else
|
|
val->intval = 0;
|
|
break;
|
|
|
|
case POWER_SUPPLY_PROP_CHARGE_NOW:
|
|
val->intval = cw_bat->battery->charge_full_design_uah;
|
|
val->intval = val->intval * cw_bat->soc / 100;
|
|
break;
|
|
|
|
case POWER_SUPPLY_PROP_CURRENT_NOW:
|
|
if (cw_battery_valid_time_to_empty(cw_bat) &&
|
|
cw_bat->battery->charge_full_design_uah > 0) {
|
|
/* calculate remaining capacity */
|
|
val->intval = cw_bat->battery->charge_full_design_uah;
|
|
val->intval = val->intval * cw_bat->soc / 100;
|
|
|
|
/* estimate current based on time to empty */
|
|
val->intval = 60 * val->intval / cw_bat->time_to_empty;
|
|
} else {
|
|
val->intval = 0;
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static enum power_supply_property cw_battery_properties[] = {
|
|
POWER_SUPPLY_PROP_CAPACITY,
|
|
POWER_SUPPLY_PROP_STATUS,
|
|
POWER_SUPPLY_PROP_PRESENT,
|
|
POWER_SUPPLY_PROP_VOLTAGE_NOW,
|
|
POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW,
|
|
POWER_SUPPLY_PROP_TECHNOLOGY,
|
|
POWER_SUPPLY_PROP_CHARGE_COUNTER,
|
|
POWER_SUPPLY_PROP_CHARGE_FULL,
|
|
POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN,
|
|
POWER_SUPPLY_PROP_CHARGE_NOW,
|
|
POWER_SUPPLY_PROP_CURRENT_NOW,
|
|
};
|
|
|
|
static const struct power_supply_desc cw2015_bat_desc = {
|
|
.name = "cw2015-battery",
|
|
.type = POWER_SUPPLY_TYPE_BATTERY,
|
|
.properties = cw_battery_properties,
|
|
.num_properties = ARRAY_SIZE(cw_battery_properties),
|
|
.get_property = cw_battery_get_property,
|
|
};
|
|
|
|
static int cw2015_parse_properties(struct cw_battery *cw_bat)
|
|
{
|
|
struct device *dev = cw_bat->dev;
|
|
int length;
|
|
int ret;
|
|
|
|
length = device_property_count_u8(dev, "cellwise,battery-profile");
|
|
if (length < 0) {
|
|
dev_warn(cw_bat->dev,
|
|
"No battery-profile found, using current flash contents\n");
|
|
} else if (length != CW2015_SIZE_BATINFO) {
|
|
dev_err(cw_bat->dev, "battery-profile must be %d bytes\n",
|
|
CW2015_SIZE_BATINFO);
|
|
return -EINVAL;
|
|
} else {
|
|
cw_bat->bat_profile = devm_kzalloc(dev, length, GFP_KERNEL);
|
|
if (!cw_bat->bat_profile)
|
|
return -ENOMEM;
|
|
|
|
ret = device_property_read_u8_array(dev,
|
|
"cellwise,battery-profile",
|
|
cw_bat->bat_profile,
|
|
length);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
ret = device_property_read_u32(dev, "cellwise,monitor-interval-ms",
|
|
&cw_bat->poll_interval_ms);
|
|
if (ret) {
|
|
dev_dbg(cw_bat->dev, "Using default poll interval\n");
|
|
cw_bat->poll_interval_ms = CW2015_DEFAULT_POLL_INTERVAL_MS;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct regmap_range regmap_ranges_rd_yes[] = {
|
|
regmap_reg_range(CW2015_REG_VERSION, CW2015_REG_VERSION),
|
|
regmap_reg_range(CW2015_REG_VCELL, CW2015_REG_CONFIG),
|
|
regmap_reg_range(CW2015_REG_MODE, CW2015_REG_MODE),
|
|
regmap_reg_range(CW2015_REG_BATINFO,
|
|
CW2015_REG_BATINFO + CW2015_SIZE_BATINFO - 1),
|
|
};
|
|
|
|
static const struct regmap_access_table regmap_rd_table = {
|
|
.yes_ranges = regmap_ranges_rd_yes,
|
|
.n_yes_ranges = 4,
|
|
};
|
|
|
|
static const struct regmap_range regmap_ranges_wr_yes[] = {
|
|
regmap_reg_range(CW2015_REG_RRT_ALERT, CW2015_REG_CONFIG),
|
|
regmap_reg_range(CW2015_REG_MODE, CW2015_REG_MODE),
|
|
regmap_reg_range(CW2015_REG_BATINFO,
|
|
CW2015_REG_BATINFO + CW2015_SIZE_BATINFO - 1),
|
|
};
|
|
|
|
static const struct regmap_access_table regmap_wr_table = {
|
|
.yes_ranges = regmap_ranges_wr_yes,
|
|
.n_yes_ranges = 3,
|
|
};
|
|
|
|
static const struct regmap_range regmap_ranges_vol_yes[] = {
|
|
regmap_reg_range(CW2015_REG_VCELL, CW2015_REG_SOC + 1),
|
|
};
|
|
|
|
static const struct regmap_access_table regmap_vol_table = {
|
|
.yes_ranges = regmap_ranges_vol_yes,
|
|
.n_yes_ranges = 1,
|
|
};
|
|
|
|
static const struct regmap_config cw2015_regmap_config = {
|
|
.reg_bits = 8,
|
|
.val_bits = 8,
|
|
.rd_table = ®map_rd_table,
|
|
.wr_table = ®map_wr_table,
|
|
.volatile_table = ®map_vol_table,
|
|
.max_register = CW2015_REG_BATINFO + CW2015_SIZE_BATINFO - 1,
|
|
};
|
|
|
|
static int cw_bat_probe(struct i2c_client *client)
|
|
{
|
|
int ret;
|
|
struct cw_battery *cw_bat;
|
|
struct power_supply_config psy_cfg = { 0 };
|
|
|
|
cw_bat = devm_kzalloc(&client->dev, sizeof(*cw_bat), GFP_KERNEL);
|
|
if (!cw_bat)
|
|
return -ENOMEM;
|
|
|
|
i2c_set_clientdata(client, cw_bat);
|
|
cw_bat->dev = &client->dev;
|
|
cw_bat->soc = 1;
|
|
|
|
ret = cw2015_parse_properties(cw_bat);
|
|
if (ret) {
|
|
dev_err(cw_bat->dev, "Failed to parse cw2015 properties\n");
|
|
return ret;
|
|
}
|
|
|
|
cw_bat->regmap = devm_regmap_init_i2c(client, &cw2015_regmap_config);
|
|
if (IS_ERR(cw_bat->regmap)) {
|
|
dev_err(cw_bat->dev, "Failed to allocate regmap: %ld\n",
|
|
PTR_ERR(cw_bat->regmap));
|
|
return PTR_ERR(cw_bat->regmap);
|
|
}
|
|
|
|
ret = cw_init(cw_bat);
|
|
if (ret) {
|
|
dev_err(cw_bat->dev, "Init failed: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
psy_cfg.drv_data = cw_bat;
|
|
psy_cfg.fwnode = dev_fwnode(cw_bat->dev);
|
|
|
|
cw_bat->rk_bat = devm_power_supply_register(&client->dev,
|
|
&cw2015_bat_desc,
|
|
&psy_cfg);
|
|
if (IS_ERR(cw_bat->rk_bat)) {
|
|
/* try again if this happens */
|
|
dev_err_probe(&client->dev, PTR_ERR(cw_bat->rk_bat),
|
|
"Failed to register power supply\n");
|
|
return PTR_ERR(cw_bat->rk_bat);
|
|
}
|
|
|
|
ret = power_supply_get_battery_info(cw_bat->rk_bat, &cw_bat->battery);
|
|
if (ret) {
|
|
/* Allocate an empty battery */
|
|
cw_bat->battery = devm_kzalloc(&client->dev,
|
|
sizeof(cw_bat->battery),
|
|
GFP_KERNEL);
|
|
if (!cw_bat->battery)
|
|
return -ENOMEM;
|
|
dev_warn(cw_bat->dev,
|
|
"No monitored battery, some properties will be missing\n");
|
|
}
|
|
|
|
cw_bat->battery_workqueue = create_singlethread_workqueue("rk_battery");
|
|
INIT_DELAYED_WORK(&cw_bat->battery_delay_work, cw_bat_work);
|
|
queue_delayed_work(cw_bat->battery_workqueue,
|
|
&cw_bat->battery_delay_work, msecs_to_jiffies(10));
|
|
return 0;
|
|
}
|
|
|
|
static int __maybe_unused cw_bat_suspend(struct device *dev)
|
|
{
|
|
struct i2c_client *client = to_i2c_client(dev);
|
|
struct cw_battery *cw_bat = i2c_get_clientdata(client);
|
|
|
|
cancel_delayed_work_sync(&cw_bat->battery_delay_work);
|
|
return 0;
|
|
}
|
|
|
|
static int __maybe_unused cw_bat_resume(struct device *dev)
|
|
{
|
|
struct i2c_client *client = to_i2c_client(dev);
|
|
struct cw_battery *cw_bat = i2c_get_clientdata(client);
|
|
|
|
queue_delayed_work(cw_bat->battery_workqueue,
|
|
&cw_bat->battery_delay_work, 0);
|
|
return 0;
|
|
}
|
|
|
|
static SIMPLE_DEV_PM_OPS(cw_bat_pm_ops, cw_bat_suspend, cw_bat_resume);
|
|
|
|
static int cw_bat_remove(struct i2c_client *client)
|
|
{
|
|
struct cw_battery *cw_bat = i2c_get_clientdata(client);
|
|
|
|
cancel_delayed_work_sync(&cw_bat->battery_delay_work);
|
|
power_supply_put_battery_info(cw_bat->rk_bat, cw_bat->battery);
|
|
return 0;
|
|
}
|
|
|
|
static const struct i2c_device_id cw_bat_id_table[] = {
|
|
{ "cw2015", 0 },
|
|
{ }
|
|
};
|
|
|
|
static const struct of_device_id cw2015_of_match[] = {
|
|
{ .compatible = "cellwise,cw2015" },
|
|
{ }
|
|
};
|
|
MODULE_DEVICE_TABLE(of, cw2015_of_match);
|
|
|
|
static struct i2c_driver cw_bat_driver = {
|
|
.driver = {
|
|
.name = "cw2015",
|
|
.of_match_table = cw2015_of_match,
|
|
.pm = &cw_bat_pm_ops,
|
|
},
|
|
.probe_new = cw_bat_probe,
|
|
.remove = cw_bat_remove,
|
|
.id_table = cw_bat_id_table,
|
|
};
|
|
|
|
module_i2c_driver(cw_bat_driver);
|
|
|
|
MODULE_AUTHOR("xhc<xhc@rock-chips.com>");
|
|
MODULE_AUTHOR("Tobias Schramm <t.schramm@manjaro.org>");
|
|
MODULE_DESCRIPTION("cw2015/cw2013 battery driver");
|
|
MODULE_LICENSE("GPL");
|