blob: 3ad979ebcc1e239d008126b47ed08e0375f46688 [file] [log] [blame]
/*
* Copyright 2018 Google, 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.
*
* 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) KBUILD_MODNAME ": " fmt
#include <linux/kernel.h>
#include <linux/printk.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/power_supply.h>
#include <linux/pm_wakeup.h>
#include <linux/pmic-voter.h>
#ifdef CONFIG_DEBUG_FS
#include <linux/debugfs.h>
#include <linux/seq_file.h>
#endif
#define CHG_TEMP_NB_LIMITS_MAX 10
#define CHG_VOLT_NB_LIMITS_MAX 5
#define CHG_DELAY_INIT_MS 250
#define CHG_DELAY_INIT_DETECT_MS 1000
#define DEFAULT_CHARGE_STOP_LEVEL 100
#define DEFAULT_CHARGE_START_LEVEL 0
#define MSC_CHG_VOTER "msc_chg"
#define CHG_WORK_ERROR_RETRY_MS 1000
#define CHG_WORK_BD_TRIGGERED_MS (5 * 60 * 1000)
#define MAX_BD_VOLTAGE 4300000
#define MIN_BD_VOLTAGE 3800000
#define MAX_BD_SOC 100
#define MIN_BD_SOC 0
#define MAX_BD_TEMP 500
#define MIN_BD_TEMP 0
struct chg_profile {
u32 update_interval;
u32 battery_capacity;
int temp_nb_limits;
s32 temp_limits[CHG_TEMP_NB_LIMITS_MAX];
int volt_nb_limits;
s32 volt_limits[CHG_VOLT_NB_LIMITS_MAX];
/* Array of constant current limits */
s32 *cccm_limits;
u32 cv_hw_resolution;
u32 cc_hw_resolution;
u32 fv_uv_margin_dpct;
u32 cv_range_accuracy;
u32 cv_otv_margin;
u32 cv_debounce_cnt;
u32 cv_update_interval;
u32 chg_cc_tolerance;
u32 cv_tier_ov_cnt;
u32 cv_tier_switch_cnt;
};
/* Taper WA */
struct taper_wa_struct {
bool taper_wa_en;
u32 v_taper[CHG_VOLT_NB_LIMITS_MAX];
s32 i_taper[CHG_VOLT_NB_LIMITS_MAX];
u32 soc_taper[CHG_VOLT_NB_LIMITS_MAX];
int taper_cnt_target;
int taper_wa_cnt;
unsigned long last_taper_wa_cnt_time;
};
struct bd_data {
u32 bd_trigger_voltage; /* also recharge upper bound */
u32 bd_trigger_temp; /* single reading */
u32 bd_trigger_time; /* debounce window after trigger*/
u32 bd_drainto_soc;
u32 bd_recharge_voltage;
u32 bd_recharge_soc;
u32 bd_resume_abs_temp; /* at any time after trigger */
u32 bd_resume_soc; /* any time after disconnect */
u32 bd_resume_time; /* debounce window for resume temp */
u32 bd_resume_temp; /* check resume_time after disconnect */
long long temp_sum;
time_t time_sum;
int last_voltage;
int last_temp;
time_t last_update;
time_t disconnect_time;
u32 triggered; /* (d) */
u32 enabled; /* (d) */
u32 bd_temp_enable; /* for UI setting interface */
bool lowerbd_reached;
};
struct chg_drv {
struct votable *usb_icl_votable;
struct votable *dc_suspend_votable;
struct device *device;
struct power_supply *chg_psy;
const char *chg_psy_name;
struct power_supply *usb_psy;
struct power_supply *wlc_psy;
const char *wlc_psy_name;
struct power_supply *bat_psy;
const char *bat_psy_name;
struct chg_profile chg_profile;
struct notifier_block psy_nb;
struct delayed_work init_work;
struct delayed_work chg_work;
struct wakeup_source chg_ws;
bool stop_charging;
int temp_idx;
int vbatt_idx;
int checked_cv_cnt;
int checked_ov_cnt;
int checked_tier_switch_cnt;
int plugged;
int chg_mode;
int fv_uv;
/* retail & battery defender */
struct delayed_work bd_work;
struct mutex bd_lock;
struct bd_data bd_state;
struct wakeup_source bd_ws;
bool overheat; /* battery reports overheat */
bool freeze_soc; /* battery soc freeze & thaw */
int disable_charging;
int disable_pwrsrc;
bool lowerdb_reached;
int charge_stop_level;
int charge_start_level;
unsigned long last_cnt_time;
bool is_full;
struct taper_wa_struct taper;
};
/* Used as left operand also */
#define CCCM_LIMITS(profile, ti, vi) \
profile->cccm_limits[(ti * profile->volt_nb_limits) + vi]
#define get_boot_sec() div_u64(ktime_to_ns(ktime_get_boottime()), NSEC_PER_SEC)
static int psy_changed(struct notifier_block *nb,
unsigned long action, void *data)
{
struct power_supply *psy = data;
struct chg_drv *chg_drv = container_of(nb, struct chg_drv, psy_nb);
pr_debug("name=%s evt=%lu\n", psy->desc->name, action);
if ((action != PSY_EVENT_PROP_CHANGED) ||
(psy == NULL) || (psy->desc == NULL) || (psy->desc->name == NULL))
return NOTIFY_OK;
if (action == PSY_EVENT_PROP_CHANGED &&
(!strcmp(psy->desc->name, chg_drv->chg_psy_name) ||
!strcmp(psy->desc->name, chg_drv->bat_psy_name) ||
!strcmp(psy->desc->name, "usb") ||
(chg_drv->wlc_psy_name &&
!strcmp(psy->desc->name, chg_drv->wlc_psy_name)))) {
cancel_delayed_work(&chg_drv->chg_work);
schedule_delayed_work(&chg_drv->chg_work, 0);
}
return NOTIFY_OK;
}
static char *psy_chgt_str[] = {
"Unknown", "N/A", "Trickle", "Fast", "Taper"
};
static char *psy_usb_type_str[] = {
"Unknown", "Battery", "UPS", "Mains", "USB", "USB_DCP",
"USB_CDP", "USB_ACA", "USB_HVDCP", "USB_HVDCP_3", "USB_PD",
"Wireless", "USB_FLOAT", "BMS", "Parallel", "Main", "Wipower",
"TYPEC", "TYPEC_UFP", "TYPEC_DFP"
};
#define PSY_GET_PROP(psy, psp) psy_get_prop(psy, psp, #psp)
static inline int psy_get_prop(struct power_supply *psy,
enum power_supply_property psp, char *prop_name)
{
union power_supply_propval val;
int ret = 0;
if (!psy)
return -EINVAL;
ret = power_supply_get_property(psy, psp, &val);
if (ret < 0) {
pr_err("failed to get %s from '%s', ret=%d\n",
prop_name, psy->desc->name, ret);
return -EINVAL;
}
pr_debug("get %s for '%s' => %d\n",
prop_name, psy->desc->name, val.intval);
return val.intval;
}
#define PSY_SET_PROP(psy, psp, val) psy_set_prop(psy, psp, val, #psp)
static inline int psy_set_prop(struct power_supply *psy,
enum power_supply_property psp,
int intval, char *prop_name)
{
union power_supply_propval val;
int ret = 0;
if (!psy)
return -EINVAL;
val.intval = intval;
pr_debug("set %s for '%s' to %d\n", prop_name, psy->desc->name, intval);
ret = power_supply_set_property(psy, psp, &val);
if (ret < 0) {
pr_err("failed to set %s for '%s', ret=%d\n",
prop_name, psy->desc->name, ret);
return -EINVAL;
}
return 0;
}
static inline void reset_chg_drv_state(struct chg_drv *chg_drv)
{
chg_drv->temp_idx = -1;
chg_drv->vbatt_idx = -1;
chg_drv->fv_uv = -1;
chg_drv->checked_cv_cnt = 0;
chg_drv->checked_ov_cnt = 0;
chg_drv->checked_tier_switch_cnt = 0;
chg_drv->disable_charging = -1;
chg_drv->disable_pwrsrc = -1;
chg_drv->lowerdb_reached = true;
chg_drv->stop_charging = true;
chg_drv->last_cnt_time = 0;
chg_drv->taper.taper_wa_cnt = 0;
chg_drv->taper.last_taper_wa_cnt_time = 0;
PSY_SET_PROP(chg_drv->chg_psy,
POWER_SUPPLY_PROP_TAPER_CONTROL,
POWER_SUPPLY_TAPER_CONTROL_OFF);
PSY_SET_PROP(chg_drv->chg_psy, POWER_SUPPLY_PROP_CHARGE_DISABLE, 1);
}
static void pr_info_states(struct power_supply *chg_psy,
struct power_supply *usb_psy,
struct power_supply *wlc_psy,
int temp, int ibatt, int vbatt, int vchrg,
int chg_type, int fv_uv,
int soc, int usb_present, int wlc_online)
{
int usb_type = PSY_GET_PROP(usb_psy, POWER_SUPPLY_PROP_REAL_TYPE);
pr_info("l=%d vb=%d vc=%d c=%d fv=%d t=%d s=%s usb=%d wlc=%d\n",
soc, vbatt / 1000, vchrg / 1000, ibatt / 1000,
fv_uv, temp, psy_chgt_str[chg_type],
usb_present, wlc_online);
if (usb_present)
pr_info("usbchg=%s usbv=%d usbc=%d usbMv=%d usbMc=%d\n",
psy_usb_type_str[usb_type],
PSY_GET_PROP(usb_psy,
POWER_SUPPLY_PROP_VOLTAGE_NOW) / 1000,
PSY_GET_PROP(usb_psy,
POWER_SUPPLY_PROP_INPUT_CURRENT_NOW)/1000,
PSY_GET_PROP(usb_psy,
POWER_SUPPLY_PROP_VOLTAGE_MAX) / 1000,
PSY_GET_PROP(usb_psy,
POWER_SUPPLY_PROP_CURRENT_MAX) / 1000);
if (wlc_online)
pr_info("wlcv=%d wlcc=%d wlcMv=%d wlcMc=%d wlct=%d\n",
PSY_GET_PROP(wlc_psy,
POWER_SUPPLY_PROP_VOLTAGE_NOW) / 1000,
PSY_GET_PROP(wlc_psy,
POWER_SUPPLY_PROP_CURRENT_NOW) / 1000,
PSY_GET_PROP(wlc_psy,
POWER_SUPPLY_PROP_VOLTAGE_MAX) / 1000,
PSY_GET_PROP(wlc_psy,
POWER_SUPPLY_PROP_CURRENT_MAX) / 1000,
PSY_GET_PROP(wlc_psy, POWER_SUPPLY_PROP_TEMP));
}
/* returns 1 if charging should be disabled given the current battery capacity
* given in percent, return 0 if charging should happen
*/
static int is_charging_disabled(struct chg_drv *chg_drv, int capacity)
{
int disable_charging = 0;
int upperbd = chg_drv->charge_stop_level;
int lowerbd = chg_drv->charge_start_level;
if ((upperbd == DEFAULT_CHARGE_STOP_LEVEL) &&
(lowerbd == DEFAULT_CHARGE_START_LEVEL))
return 0;
if ((upperbd > lowerbd) &&
(upperbd <= DEFAULT_CHARGE_STOP_LEVEL) &&
(lowerbd >= DEFAULT_CHARGE_START_LEVEL)) {
if (chg_drv->lowerdb_reached && upperbd <= capacity) {
pr_info("%s: lowerbd=%d, upperbd=%d, capacity=%d, lowerdb_reached=1->0, charging off\n",
__func__, lowerbd, upperbd, capacity);
disable_charging = 1;
chg_drv->lowerdb_reached = false;
} else if (!chg_drv->lowerdb_reached && lowerbd < capacity) {
pr_info("%s: lowerbd=%d, upperbd=%d, capacity=%d, charging off\n",
__func__, lowerbd, upperbd, capacity);
disable_charging = 1;
} else if (!chg_drv->lowerdb_reached && capacity <= lowerbd) {
pr_info("%s: lowerbd=%d, upperbd=%d, capacity=%d, lowerdb_reached=0->1, charging on\n",
__func__, lowerbd, upperbd, capacity);
chg_drv->lowerdb_reached = true;
} else {
pr_info("%s: lowerbd=%d, upperbd=%d, capacity=%d, charging on\n",
__func__, lowerbd, upperbd, capacity);
}
}
return disable_charging;
}
/* 1. charge profile idx based on the battery temperature */
static int msc_temp_idx(struct chg_profile *profile, int temp)
{
int temp_idx = 0;
while (temp_idx < profile->temp_nb_limits - 1 &&
temp >= profile->temp_limits[temp_idx + 1])
temp_idx++;
return temp_idx;
}
/* 2. compute the step index given the battery voltage
* When selecting an index need to make sure that headroom for the tier voltage
* will allow to send to the battery _at least_ next tier max FCC current and
* well over charge termination current.
*/
static int msc_voltage_idx(struct chg_profile *profile, int vbatt)
{
int vbatt_idx = 0;
while (vbatt_idx < profile->volt_nb_limits - 1 &&
vbatt > profile->volt_limits[vbatt_idx])
vbatt_idx++;
/* assumes that 3 times the hardware resolution is ok */
if (vbatt_idx != profile->volt_nb_limits - 1) {
const int vt = profile->volt_limits[vbatt_idx];
const int headr = profile->cv_hw_resolution * 3;
if ((vt - vbatt) < headr)
vbatt_idx += 1;
}
return vbatt_idx;
}
/* Cap to fv_uv_margin_pct of VTIER if needed */
static int msc_round_fv_uv(struct chg_profile *profile, int vtier, int fv_uv)
{
int result;
const unsigned int fv_uv_max = (vtier / 1000)
* profile->fv_uv_margin_dpct;
if (fv_uv_max != 0 && fv_uv > fv_uv_max)
fv_uv = fv_uv_max;
result = fv_uv - (fv_uv % profile->cv_hw_resolution);
if (fv_uv_max != 0)
pr_info("MSC_ROUND: fv_uv=%d vtier=%d fv_uv_max=%d -> %d\n",
fv_uv, vtier, fv_uv_max, result);
return result;
}
/* TODO: now created in qcom code, create in chg_create_votables() */
static int chg_find_votables(struct chg_drv *chg_drv)
{
if (!chg_drv->usb_icl_votable)
chg_drv->usb_icl_votable = find_votable("USB_ICL");
if (!chg_drv->dc_suspend_votable)
chg_drv->dc_suspend_votable = find_votable("DC_SUSPEND");
return (!chg_drv->usb_icl_votable || !chg_drv->dc_suspend_votable)
? -EINVAL : 0;
}
/* input suspend votes 0 ICL and call suspend on DC_ICL */
static int chg_vote_input_suspend(struct chg_drv *chg_drv, char *voter,
bool suspend)
{
int rc;
if (chg_find_votables(chg_drv) < 0)
return -EINVAL;
rc = vote(chg_drv->usb_icl_votable, voter, suspend, 0);
if (rc < 0) {
dev_err(chg_drv->device, "Couldn't vote to %s USB rc=%d\n",
suspend ? "suspend" : "resume", rc);
return rc;
}
rc = vote(chg_drv->dc_suspend_votable, voter, suspend, 0);
if (rc < 0) {
dev_err(chg_drv->device, "Couldn't vote to %s DC rc=%d\n",
suspend ? "suspend" : "resume", rc);
return rc;
}
return 0;
}
/* check for retail, battery defender */
static void chg_update_charging_state(struct chg_drv *chg_drv,
int disable_charging,
int disable_pwrsrc)
{
/* disable charging is set in retail mode */
if (disable_charging != chg_drv->disable_charging) {
pr_info("MSC_CHG disable_charging %d -> %d",
chg_drv->disable_charging, disable_charging);
PSY_SET_PROP(chg_drv->chg_psy, POWER_SUPPLY_PROP_CHARGE_DISABLE,
disable_charging);
}
chg_drv->disable_charging = disable_charging;
/* when disable_pwrsrc is set, disable_charging is set also */
if (disable_pwrsrc != chg_drv->disable_pwrsrc) {
pr_info("MSC_CHG disable_pwrsrc %d -> %d",
chg_drv->disable_pwrsrc, disable_pwrsrc);
/* applied right away */
chg_vote_input_suspend(chg_drv, MSC_CHG_VOTER, disable_pwrsrc);
/* take a wakelock */
if (disable_pwrsrc)
__pm_stay_awake(&chg_drv->bd_ws);
else
__pm_relax(&chg_drv->bd_ws);
}
chg_drv->disable_pwrsrc = disable_pwrsrc;
}
/* called on init, and to reset the trigger */
static void bd_reset(struct bd_data *bd_state)
{
bool can_resume = bd_state->bd_resume_abs_temp ||
bd_state->bd_resume_time ||
(bd_state->bd_resume_time == 0 &&
bd_state->bd_resume_soc == 0);
bd_state->time_sum = 0;
bd_state->temp_sum = 0;
bd_state->last_update = 0;
bd_state->last_voltage = 0;
bd_state->last_temp = 0;
bd_state->triggered = 0;
/* also disabled when externally triggered, resume_temp is optional */
bd_state->enabled = ((bd_state->bd_trigger_voltage &&
bd_state->bd_recharge_voltage) ||
(bd_state->bd_drainto_soc &&
bd_state->bd_recharge_soc)) &&
bd_state->bd_trigger_time &&
bd_state->bd_trigger_temp &&
bd_state->bd_temp_enable &&
can_resume;
}
static void bd_init(struct bd_data *bd_state, struct device *dev)
{
int ret;
ret = of_property_read_u32(dev->of_node, "google,bd-trigger-voltage",
&bd_state->bd_trigger_voltage);
if (ret < 0)
bd_state->bd_trigger_voltage = 0;
ret = of_property_read_u32(dev->of_node, "google,bd-drainto-soc",
&bd_state->bd_drainto_soc);
if (ret < 0)
bd_state->bd_drainto_soc = 0;
ret = of_property_read_u32(dev->of_node, "google,bd-trigger-temp",
&bd_state->bd_trigger_temp);
if (ret < 0)
bd_state->bd_trigger_temp = 0;
ret = of_property_read_u32(dev->of_node, "google,bd-trigger-time",
&bd_state->bd_trigger_time);
if (ret < 0)
bd_state->bd_trigger_time = 0;
ret = of_property_read_u32(dev->of_node, "google,bd-recharge-voltage",
&bd_state->bd_recharge_voltage);
if (ret < 0)
bd_state->bd_recharge_voltage = 0;
ret = of_property_read_u32(dev->of_node, "google,bd-recharge-soc",
&bd_state->bd_recharge_soc);
if (ret < 0)
bd_state->bd_recharge_soc = 0;
ret = of_property_read_u32(dev->of_node, "google,bd-resume-abs-temp",
&bd_state->bd_resume_abs_temp);
if (ret < 0)
bd_state->bd_resume_abs_temp = 0;
ret = of_property_read_u32(dev->of_node, "google,bd-resume-soc",
&bd_state->bd_resume_soc);
if (ret < 0)
bd_state->bd_resume_soc = 0;
ret = of_property_read_u32(dev->of_node, "google,bd-resume-temp",
&bd_state->bd_resume_temp);
if (ret < 0)
bd_state->bd_resume_temp = 0;
ret = of_property_read_u32(dev->of_node, "google,bd-resume-time",
&bd_state->bd_resume_time);
if (ret < 0)
bd_state->bd_resume_time = 0;
/* also call to resume charging */
bd_state->bd_temp_enable = 1;
bd_reset(bd_state);
if (!bd_state->enabled)
dev_warn(dev, "TEMP-DEFEND not enabled\n", ret);
pr_info("MSC_BD: trig volt=%d,%d temp=%d,time=%d drainto=%d,%d resume=%d,%d %d,%d\n",
bd_state->bd_trigger_voltage, bd_state->bd_recharge_voltage,
bd_state->bd_trigger_temp, bd_state->bd_trigger_time,
bd_state->bd_drainto_soc, bd_state->bd_recharge_soc,
bd_state->bd_resume_abs_temp, bd_state->bd_resume_soc,
bd_state->bd_resume_temp, bd_state->bd_resume_time);
}
/* */
static int bd_update_stats(struct bd_data *bd_state,
struct power_supply *bat_psy)
{
const bool triggered = bd_state->triggered;
const time_t now = get_boot_sec();
int vbatt, temp;
long long temp_avg;
if (!bd_state->enabled)
return 0;
vbatt = PSY_GET_PROP(bat_psy, POWER_SUPPLY_PROP_VOLTAGE_NOW);
if (vbatt == -EINVAL)
return 0;
/* not over vbat and !triggered, nothing to see here */
if (vbatt < bd_state->bd_trigger_voltage && !triggered)
return 0;
/* it needs to keep averaging after trigger */
temp = PSY_GET_PROP(bat_psy, POWER_SUPPLY_PROP_TEMP);
if (temp == -EINVAL)
return 0;
/* it needs to keep averaging if triggered */
if (bd_state->last_update == 0)
bd_state->last_update = now;
if (temp >= bd_state->bd_trigger_temp) {
bd_state->time_sum += now - bd_state->last_update;
bd_state->temp_sum += temp * (now - bd_state->last_update);
}
bd_state->last_voltage = vbatt;
bd_state->last_update = now;
bd_state->last_temp = temp;
/* wait until we have at least bd_trigger_time */
if (bd_state->time_sum < bd_state->bd_trigger_time)
return 0;
/* exit and entry criteria */
temp_avg = bd_state->temp_sum / bd_state->time_sum;
if (triggered && temp <= bd_state->bd_resume_abs_temp) {
pr_info("MSC_BD: resume time_sum=%lld, temp_sum=%lld, temp_avg=%d\n",
bd_state->time_sum, bd_state->temp_sum, temp_avg);
bd_reset(bd_state);
} else if (temp_avg >= bd_state->bd_trigger_temp) {
pr_info("MSC_BD: trigger time_sum=%lld, temp_sum=%lld, temp_avg=%d\n",
bd_state->time_sum, bd_state->temp_sum, temp_avg);
bd_state->triggered = 1;
}
return 0;
}
/* @return true if BD needs to be triggered */
static int bd_recharge_logic(struct bd_data *bd_state, int val)
{
int lowerbd, upperbd;
int disable_charging = 0;
if (!bd_state->triggered)
return 0;
if (bd_state->bd_drainto_soc && bd_state->bd_recharge_soc) {
lowerbd = bd_state->bd_recharge_soc;
upperbd = bd_state->bd_drainto_soc;
} else if (bd_state->bd_recharge_voltage &&
bd_state->bd_trigger_voltage) {
lowerbd = bd_state->bd_recharge_voltage;
upperbd = bd_state->bd_trigger_voltage;
} else {
return 0;
}
/* recharge logic between bd_recharge_voltage and bd_trigger_voltage */
if (bd_state->lowerbd_reached && val >= upperbd) {
pr_info("MSC_BD lowerbd=%d, upperbd=%d, val=%d, lowerbd_reached=1->0, charging off\n",
lowerbd, upperbd, val);
bd_state->lowerbd_reached = false;
disable_charging = 1;
} else if (!bd_state->lowerbd_reached && val > lowerbd) {
pr_info("MSC_BD lowerbd=%d, upperbd=%d, val=%d, charging off\n",
lowerbd, upperbd, val);
disable_charging = 1;
} else if (!bd_state->lowerbd_reached && val <= lowerbd) {
pr_info("MSC_BD lowerbd=%d, upperbd=%d, val=%d, lowerbd_reached=0->1, charging on\n",
lowerbd, upperbd, val);
bd_state->lowerbd_reached = true;
} else {
pr_info("MSC_BD lowerbd=%d, upperbd=%d, val=%d, charging on\n",
lowerbd, upperbd, val);
}
return disable_charging;
}
/* ingnore the failure to set CAPACITY, it might not be implemented */
static int bd_batt_set_soc(struct chg_drv *chg_drv , int soc)
{
const bool freeze = soc >= 0;
int rc = 0;
pr_debug("MSC_BD soc=%d %s (%d)\n", soc,
soc == -1 ? "THAW" : "FREEZE", rc);
chg_drv->freeze_soc = freeze;
return 0;
}
static int bd_batt_set_overheat(struct chg_drv *chg_drv , bool hot)
{
const int health = hot ? POWER_SUPPLY_HEALTH_OVERHEAT :
POWER_SUPPLY_HEALTH_UNKNOWN;
int ret;
ret = PSY_SET_PROP(chg_drv->bat_psy, POWER_SUPPLY_PROP_HEALTH, health);
if (ret == 0)
chg_drv->overheat = hot;
pr_info("MSC_BD OVERHEAT hot=%d (%d)\n", hot, ret);
return ret;
}
static int bd_batt_set_state(struct chg_drv *chg_drv, bool hot, int soc)
{
const bool freeze = soc != -1;
int ret = 0;
/* OVERHEAT is used to rebuild the curve */
if (freeze != chg_drv->freeze_soc)
ret = bd_batt_set_soc(chg_drv, soc);
if (ret == 0 && chg_drv->overheat != hot)
ret = bd_batt_set_overheat(chg_drv, hot);
return ret;
}
static int chg_work_read_soc(struct power_supply *bat_psy, int *soc)
{
union power_supply_propval val;
int ret = 0;
ret = power_supply_get_property(bat_psy, POWER_SUPPLY_PROP_CAPACITY,
&val);
if (ret == 0)
*soc = val.intval;
return ret;
}
/*
* Run in background after disconnect to reset the trigger.
* The UI% is not frozen here: only battery health state remain
* POWER_SUPPLY_HEALTH_OVERHEAT until the condition clears.
*/
static void bd_work(struct work_struct *work)
{
struct chg_drv *chg_drv =
container_of(work, struct chg_drv, bd_work.work);
struct bd_data *bd_state = &chg_drv->bd_state;
int interval_ms = CHG_WORK_BD_TRIGGERED_MS;
const time_t now = get_boot_sec();
const long long delta_time = now - bd_state->disconnect_time;
int ret, soc = 0;
__pm_stay_awake(&chg_drv->bd_ws);
mutex_lock(&chg_drv->bd_lock);
pr_debug("MSC_BD_WORK: triggered=%d dsc_time=%ld delta=%lld\n",
bd_state->triggered, bd_state->disconnect_time, delta_time);
/* stop running if the cable in. */
if (!chg_drv->stop_charging)
goto bd_done;
/* stop running if battery defender or retail mode are triggered */
if (!bd_state->triggered || !bd_state->disconnect_time)
goto bd_done;
if (bd_state->bd_resume_soc &&
chg_work_read_soc(chg_drv->bat_psy, &soc) == 0 &&
soc < bd_state->bd_resume_soc) {
pr_info("MSC_BD_WORK: done soc=%d limit=%d\n",
soc, bd_state->bd_resume_soc);
bd_reset(bd_state);
goto bd_rerun;
}
/* reset based on average or instant temp */
ret = bd_update_stats(bd_state, chg_drv->bat_psy);
if (ret < 0) {
pr_err("MSC_BD_WORK: update stats: %d\n", ret);
interval_ms = 1000;
goto bd_rerun;
}
/* reset on time since disconnect & optional temperature reading */
if (bd_state->bd_resume_time && delta_time > bd_state->bd_resume_time) {
const int temp = bd_state->last_temp; /* or use avg */
int triggered;
/* single reading < of resume temp, or total average temp */
triggered = temp > bd_state->bd_resume_temp;
if (!triggered) {
pr_info("MSC_BD_WORK: done time=%lld limit=%d, temp=%d limit=%d\n",
delta_time, bd_state->bd_resume_time,
temp, bd_state->bd_resume_temp);
bd_reset(bd_state);
goto bd_rerun;
}
}
pr_info("MSC_BD_WORK: triggered=%d time=%lld < %lld soc=%d > %d temp=%lld > %d\n",
bd_state->triggered,
delta_time, bd_state->bd_resume_time,
soc, bd_state->bd_resume_soc,
bd_state->last_temp,
bd_state->bd_resume_temp);
bd_rerun:
if (!bd_state->triggered) {
/* disable the overheat flag, race with DWELL-DEFEND */
bd_batt_set_overheat(chg_drv, false);
} else {
schedule_delayed_work(&chg_drv->bd_work,
msecs_to_jiffies(interval_ms));
}
bd_done:
mutex_unlock(&chg_drv->bd_lock);
__pm_relax(&chg_drv->bd_ws);
}
static void chg_work(struct work_struct *work)
{
struct chg_drv *chg_drv =
container_of(work, struct chg_drv, chg_work.work);
struct chg_profile *profile = &chg_drv->chg_profile;
union power_supply_propval val;
struct power_supply *chg_psy = chg_drv->chg_psy;
struct power_supply *usb_psy = chg_drv->usb_psy;
struct power_supply *wlc_psy = chg_drv->wlc_psy;
struct power_supply *bat_psy = chg_drv->bat_psy;
int temp, ibatt, vbatt, vchrg, soc, chg_type;
int usb_present, wlc_online = 0;
int vbatt_idx = chg_drv->vbatt_idx, fv_uv = chg_drv->fv_uv, temp_idx;
int update_interval = chg_drv->chg_profile.update_interval;
int plugged, batt_status;
bool rerun_work = false;
int disable_charging;
int disable_pwrsrc = 0;
int rc;
static bool cable_in;
__pm_stay_awake(&chg_drv->chg_ws);
pr_debug("battery charging work item\n");
usb_present = PSY_GET_PROP(usb_psy, POWER_SUPPLY_PROP_PRESENT);
if (wlc_psy)
wlc_online = PSY_GET_PROP(wlc_psy, POWER_SUPPLY_PROP_ONLINE);
plugged = usb_present || wlc_online;
if (chg_drv->plugged != plugged) {
int ret = 0;
/* only after boot */
if (chg_drv->plugged != -1) {
ret = PSY_SET_PROP(bat_psy,
POWER_SUPPLY_PROP_BATT_CE_CTRL,
plugged);
}
if (ret == 0)
chg_drv->plugged = plugged;
}
/* If no power source, disable charging and exit */
if (!usb_present && !wlc_online) {
const bool stop_charging = chg_drv->stop_charging != 1;
struct bd_data *bd_state = &chg_drv->bd_state;
if (stop_charging) {
pr_info("no power source detected, disabling charging\n");
if (!bd_state->triggered)
bd_reset(bd_state);
reset_chg_drv_state(chg_drv);
chg_drv->stop_charging = 1;
bd_batt_set_state(chg_drv, false, -1);
}
mutex_lock(&chg_drv->bd_lock);
if (bd_state->triggered) {
const bool bd_ena = bd_state->bd_resume_soc ||
bd_state->bd_resume_time;
/*
* clear overheat and thaw soc on disconnect.
* Don not clear the defender state, will
* re-evaluate on next connect.
*/
if (!bd_state->disconnect_time) {
rc = bd_batt_set_state(chg_drv, false, -1);
if (rc < 0) {
pr_err("MSC_BD set_batt_state (%d)\n",
rc);
mutex_unlock(&chg_drv->bd_lock);
goto error_rerun;
}
bd_state->disconnect_time = get_boot_sec();
}
if (bd_ena)
mod_delayed_work(system_wq,
&chg_drv->bd_work, 0);
}
mutex_unlock(&chg_drv->bd_lock);
/* allow sleep (if disconnected) while draining */
if (chg_drv->disable_pwrsrc)
__pm_relax(&chg_drv->bd_ws);
goto exit_chg_work;
}
/* debug option */
if (chg_drv->chg_mode)
bat_psy = chg_psy;
temp = PSY_GET_PROP(bat_psy, POWER_SUPPLY_PROP_TEMP);
ibatt = PSY_GET_PROP(bat_psy, POWER_SUPPLY_PROP_CURRENT_NOW);
vbatt = PSY_GET_PROP(bat_psy, POWER_SUPPLY_PROP_VOLTAGE_NOW);
soc = PSY_GET_PROP(bat_psy, POWER_SUPPLY_PROP_CAPACITY);
vchrg = PSY_GET_PROP(chg_psy, POWER_SUPPLY_PROP_VOLTAGE_NOW);
chg_type = PSY_GET_PROP(chg_psy, POWER_SUPPLY_PROP_CHARGE_TYPE);
if (temp == -EINVAL || ibatt == -EINVAL || vbatt == -EINVAL ||
usb_present == -EINVAL || wlc_online == -EINVAL)
goto error_rerun;
pr_info_states(chg_psy, usb_psy, wlc_psy,
temp, ibatt, vbatt, vchrg,
chg_type, chg_drv->fv_uv,
soc, usb_present, wlc_online);
if (temp < profile->temp_limits[0] ||
temp > profile->temp_limits[profile->temp_nb_limits - 1]) {
if (!chg_drv->stop_charging) {
pr_info("batt. temp. off limits, disabling charging\n");
reset_chg_drv_state(chg_drv);
}
/* status will be discharging when disabled but we want to keep
* monitoring temperature to re-enable charging
*/
rerun_work = true;
goto handle_rerun;
} else if (chg_drv->stop_charging) {
pr_info("batt. temp. ok, enabling charging\n");
PSY_SET_PROP(chg_psy, POWER_SUPPLY_PROP_CHARGE_DISABLE, 0);
chg_drv->stop_charging = false;
cable_in = true;
}
/* BD and retail: disable_pwrsrc = fast drain to the stop level */
mutex_lock(&chg_drv->bd_lock);
disable_charging = is_charging_disabled(chg_drv, soc);
if (disable_charging && soc > chg_drv->charge_stop_level)
disable_pwrsrc = 1;
if (disable_charging) {
/* force TEMP-DEFEND off, stop bd_work() */
if (chg_drv->bd_state.triggered) {
bd_reset(&chg_drv->bd_state);
bd_batt_set_state(chg_drv, false, -1);
}
chg_drv->bd_state.enabled = false;
/*
* This mode can be enabled from DWELL-DEFEND when in "idle",
* while TEMP-DEFEND is triggered and from Retail Mode.
*
* Retail should probably not flag OVERHEAT as this might
* have unwanted interactions with the demo application.
*/
if (!chg_drv->disable_charging) {
rc = bd_batt_set_state(chg_drv, false, -1);
if (rc < 0)
pr_err("MSC_BD batt reset (%d)\n", rc);
}
} else if (chg_drv->bd_state.enabled) {
struct bd_data *bd_state = &chg_drv->bd_state;
const bool was_triggered = bd_state->triggered;
/* bd_work() is not running here */
rc = bd_update_stats(bd_state, bat_psy);
if (rc < 0)
pr_debug("MSC_DB BD update stats: %d\n", rc);
/* thaw SOC, clear overheat */
if (!bd_state->triggered && was_triggered) {
rc = bd_batt_set_state(chg_drv, false, -1);
if (rc < 0)
pr_err("MSC_BD cannot reset state (%d)\n", rc);
} else if (bd_state->triggered) {
const int lock_soc = soc; /* or set to 100 */
/* recharge logic */
if (bd_state->bd_drainto_soc) {
disable_charging =
bd_recharge_logic(bd_state, soc);
if (disable_charging)
disable_pwrsrc =
soc > bd_state->bd_drainto_soc;
} else {
disable_charging = bd_recharge_logic(bd_state,
bd_state->last_voltage);
if (disable_charging)
disable_pwrsrc =
bd_state->last_voltage >
bd_state->bd_trigger_voltage;
}
/* overheat and freeze, on trigger and reconnect */
if (!was_triggered || chg_drv->stop_charging
|| cable_in) {
cable_in = false;
rc = bd_batt_set_state(chg_drv, true, lock_soc);
if (rc < 0)
pr_err("MSC_BD cannot set overheat (%d)\n",
rc);
}
}
} else if (chg_drv->disable_charging) {
/* re-enable TEMP-DEFEND */
bd_reset(&chg_drv->bd_state);
/* DWELL-DEFEND handles OVERHEAT status */
}
/* state in chg_drv->disable_charging, chg_drv->disable_pwrsrc */
chg_update_charging_state(chg_drv, disable_charging, disable_pwrsrc);
mutex_unlock(&chg_drv->bd_lock);
/* no need to reschedule, battery psy event will reschedule work item */
if (chg_drv->disable_charging || chg_drv->disable_pwrsrc) {
rerun_work = false;
goto exit_chg_work;
}
/* Multi Step Chargings with compensation of IRDROP
* vbatt_idx = chg_drv->vbatt_idx, fv_uv = chg_drv->fv_uv
*/
temp_idx = msc_temp_idx(profile, temp);
if (temp_idx != chg_drv->temp_idx || chg_drv->fv_uv == -1
|| chg_drv->vbatt_idx == -1) {
/* seed voltage only when really needed */
if (chg_drv->vbatt_idx == -1)
vbatt_idx = msc_voltage_idx(profile, vbatt);
pr_info("MSC_SEED temp=%d vbatt=%d temp_idx:%d->%d, vbatt_idx:%d->%d\n",
temp, vbatt, chg_drv->temp_idx, temp_idx,
chg_drv->vbatt_idx, vbatt_idx);
/* Debounce tier switch only when not already switching */
if (chg_drv->checked_tier_switch_cnt == 0)
chg_drv->checked_cv_cnt = profile->cv_debounce_cnt;
} else if (ibatt > 0) {
/* Track battery voltage if discharging is due to system load,
* low ILIM or lack of headroom; stop charging work and reset
* chg_drv state() when discharging is due to disconnect.
* NOTE: POWER_SUPPLY_PROP_STATUS return *_DISCHARGING only on
* disconnect.
* NOTE: same vbat_idx will not change fv_uv
*/
vbatt_idx = msc_voltage_idx(profile, vbatt);
update_interval = profile->cv_update_interval;
pr_info("MSC_DSG vbatt_idx:%d->%d vbatt=%d ibatt=%d fv_uv=%d cv_cnt=%d ov_cnt=%d\n",
chg_drv->vbatt_idx, vbatt_idx,
vbatt, ibatt, fv_uv,
chg_drv->checked_cv_cnt,
chg_drv->checked_ov_cnt);
} else if (chg_drv->vbatt_idx == profile->volt_nb_limits - 1) {
/* will not adjust charger voltage only in the configured
* last tier.
* NOTE: might not be the "real" last tier since can I have
* tiers with max charge current == 0.
* NOTE: should I use a voltage limit instead?
*/
pr_info("MSC_LAST vbatt=%d ibatt=%d fv_uv=%d\n",
vbatt, ibatt, fv_uv);
} else {
const int vtier = profile->volt_limits[vbatt_idx];
const int utv_margin = profile->cv_range_accuracy;
const int otv_margin = profile->cv_otv_margin;
const int switch_cnt = profile->cv_tier_switch_cnt;
const int cc_next_max = CCCM_LIMITS(profile, temp_idx,
vbatt_idx + 1);
unsigned long cur_time, last_cnt_interval;
bool skip_cnt = false;
cur_time = jiffies_to_msecs(jiffies);
last_cnt_interval = cur_time - chg_drv->last_cnt_time;
skip_cnt = (last_cnt_interval <
profile->cv_update_interval) ? true : false;
if ((vbatt - vtier) > otv_margin) {
/* OVER: vbatt over vtier for more than margin (usually 0) */
const int cc_max =
CCCM_LIMITS(profile, temp_idx, vbatt_idx);
/* pullback when over tier voltage, fast poll, penalty
* on TAPER_RAISE and no cv debounce (so will consider
* switching voltage tiers if the current is right).
* NOTE: lowering voltage might cause a small drop in
* current (we should remain under next tier)
*/
fv_uv = msc_round_fv_uv(profile, vtier,
fv_uv - profile->cv_hw_resolution);
if (fv_uv < vtier)
fv_uv = vtier;
update_interval = profile->cv_update_interval;
chg_drv->checked_ov_cnt = profile->cv_tier_ov_cnt;
chg_drv->checked_cv_cnt = 0;
if (chg_drv->checked_tier_switch_cnt > 0) {
/* no pullback, next tier if already counting */
vbatt_idx = chg_drv->vbatt_idx + 1;
pr_info("MSC_VSWITCH vt=%d vb=%d ibatt=%d\n",
vtier, vbatt, ibatt);
} else if (-ibatt == cc_max) {
/* pullback, double penalty if at full current */
chg_drv->checked_ov_cnt *= 2;
pr_info("MSC_VOVER vt=%d vb=%d ibatt=%d fv_uv=%d->%d\n",
vtier, vbatt, ibatt,
chg_drv->fv_uv, fv_uv);
} else {
pr_info("MSC_PULLBACK vt=%d vb=%d ibatt=%d fv_uv=%d->%d\n",
vtier, vbatt, ibatt,
chg_drv->fv_uv, fv_uv);
}
/* NOTE: might get here after windup because algo will
* track the voltage drop caused from load as IRDROP.
* TODO: make sure that being current limited clear
* the taper condition.
*/
} else if (chg_type == POWER_SUPPLY_CHARGE_TYPE_FAST) {
/* FAST: usual compensation (vchrg is vqcom)
* NOTE: there is a race in reading from charger and data here
* might not be consistent (b/110318684)
* NOTE: could add PID loop for management of thermals
*/
if (vchrg > vbatt) {
fv_uv = msc_round_fv_uv(profile, vtier,
vtier + (vchrg - vbatt));
} else {
/* could keep it steady instead */
fv_uv = vtier;
}
/* no tier switch during fast charge */
if (chg_drv->checked_cv_cnt == 0)
chg_drv->checked_cv_cnt = 1;
pr_info("MSC_FAST vt=%d vb=%d fv_uv=%d->%d vchrg=%d cv_cnt=%d \n",
vtier, vbatt, chg_drv->fv_uv, fv_uv,
vchrg, chg_drv->checked_cv_cnt);
} else if (chg_type != POWER_SUPPLY_CHARGE_TYPE_TAPER) {
/* Not fast or taper: set checked_cv_cnt=0 to make sure we test
* for current and avoid early termination in case of lack of
* headroom (Vfloat ~= Vbatt)
* NOTE: this can cause early switch on low ilim
*/
update_interval = profile->cv_update_interval;
chg_drv->checked_cv_cnt = 0;
pr_info("MSC_TYPE vt=%d vb=%d fv_uv=%d chg_type=%d\n",
vtier, vbatt, fv_uv, chg_type);
} else if (chg_drv->checked_cv_cnt + chg_drv->checked_ov_cnt) {
/* TAPER_COUNTDOWN: countdown to raise fv_uv and/or check
* for tier switch, will keep steady...
*/
pr_info("MSC_DLY vt=%d vb=%d fv_uv=%d margin=%d cv_cnt=%d, ov_cnt=%d, skip_cnt=%d\n",
vtier, vbatt, fv_uv, profile->cv_range_accuracy,
chg_drv->checked_cv_cnt,
chg_drv->checked_ov_cnt, skip_cnt);
update_interval = profile->cv_update_interval;
if (!skip_cnt) {
if (chg_drv->checked_cv_cnt)
chg_drv->checked_cv_cnt -= 1;
if (chg_drv->checked_ov_cnt)
chg_drv->checked_ov_cnt -= 1;
chg_drv->last_cnt_time = cur_time;
}
} else if ((vtier - vbatt) < utv_margin) {
/* TAPER_STEADY: close enough to tier, don't need to adjust */
update_interval = profile->cv_update_interval;
pr_info("MSC_STEADY vt=%d vb=%d fv_uv=%d margin=%d\n",
vtier, vbatt, fv_uv,
profile->cv_range_accuracy);
} else {
/* TAPER_RAISE: under tier vlim, raise one click & debounce
* taper (see above handling of "close enough")
*/
fv_uv = msc_round_fv_uv(profile, vtier,
fv_uv + profile->cv_hw_resolution);
update_interval = profile->cv_update_interval;
/* debounce next taper voltage adjustment */
chg_drv->checked_cv_cnt = profile->cv_debounce_cnt;
pr_info("MSC_RAISE vt=%d vb=%d fv_uv=%d->%d\n",
vtier, vbatt, chg_drv->fv_uv, fv_uv);
}
if (chg_drv->checked_cv_cnt > 0) {
/* debounce period on tier switch */
pr_info("MSC_WAIT vt=%d vb=%d fv_uv=%d ibatt=%d cv_cnt=%d ov_cnt=%d\n",
vtier, vbatt, fv_uv, ibatt,
chg_drv->checked_cv_cnt,
chg_drv->checked_ov_cnt);
} else if (-ibatt > cc_next_max) {
/* current over next tier, reset tier switch count */
chg_drv->checked_tier_switch_cnt = 0;
pr_info("MSC_RSTC vt=%d vb=%d fv_uv=%d ibatt=%d cc_next_max=%d t_cnt=%d\n",
vtier, vbatt, fv_uv, ibatt, cc_next_max,
chg_drv->checked_tier_switch_cnt);
} else if (chg_drv->checked_tier_switch_cnt >= switch_cnt) {
/* next tier, fv_uv detemined at MSC_SET */
vbatt_idx = chg_drv->vbatt_idx + 1;
pr_info("MSC_NEXT tier vb=%d ibatt=%d vbatt_idx=%d->%d\n",
vbatt, ibatt, chg_drv->vbatt_idx, vbatt_idx);
} else {
/* current under next tier, increase tier switch count */
if (!skip_cnt) {
chg_drv->checked_tier_switch_cnt++;
chg_drv->last_cnt_time = cur_time;
}
pr_info("MSC_NYET ibatt=%d cc_next_max=%d t_cnt=%d, skip_cnt=%d\n",
ibatt, cc_next_max,
chg_drv->checked_tier_switch_cnt, skip_cnt);
}
if (skip_cnt &&
(update_interval == profile->cv_update_interval)) {
update_interval -= last_cnt_interval;
}
}
/* TAPER WA */
if (chg_drv->taper.taper_wa_en &&
(chg_type == POWER_SUPPLY_CHARGE_TYPE_FAST) &&
(vbatt_idx != profile->volt_nb_limits - 1)) {
struct taper_wa_struct *taper = &chg_drv->taper;
int chg_status_fast;
unsigned long curr_time, last_cnt_interval;
bool skip_cnt = true;
chg_status_fast =
PSY_GET_PROP(chg_psy,
POWER_SUPPLY_PROP_CHARGER_STATUS_FAST);
curr_time = jiffies_to_msecs(jiffies);
last_cnt_interval = curr_time - taper->last_taper_wa_cnt_time;
skip_cnt = (update_interval != profile->update_interval) ||
(update_interval > last_cnt_interval);
if (((vbatt / 1000) > taper->v_taper[vbatt_idx]) &&
((-ibatt / 1000) < taper->i_taper[vbatt_idx]) &&
(soc > taper->soc_taper[vbatt_idx]) &&
chg_status_fast) {
if (!skip_cnt) {
taper->taper_wa_cnt++;
taper->last_taper_wa_cnt_time = curr_time;
pr_info("MSC_TAPER ibatt=%d, vbat=%d, soc=%d, taper_cnt=%d, chg_status_fast=%d\n",
vbatt, ibatt, soc, taper->taper_wa_cnt,
chg_status_fast);
}
if (taper->taper_wa_cnt > taper->taper_cnt_target) {
vbatt_idx++;
taper->taper_wa_cnt = 0;
taper->last_taper_wa_cnt_time = 0;
pr_info("MSC_TAPER next vbatt_idx=%d->%d\n",
chg_drv->vbatt_idx, vbatt_idx);
}
} else {
taper->taper_wa_cnt = 0;
taper->last_taper_wa_cnt_time = 0;
}
} else {
chg_drv->taper.taper_wa_cnt = 0;
chg_drv->taper.last_taper_wa_cnt_time = 0;
}
/* update fv or cc will change in last tier... */
if ((vbatt_idx != chg_drv->vbatt_idx) || (temp_idx != chg_drv->temp_idx)
|| (fv_uv != chg_drv->fv_uv)) {
const int cc_max = CCCM_LIMITS(profile, temp_idx, vbatt_idx);
int rc;
/* need a new fv_uv only on a new voltage tier */
if (vbatt_idx != chg_drv->vbatt_idx) {
fv_uv = profile->volt_limits[vbatt_idx];
chg_drv->checked_tier_switch_cnt = 0;
chg_drv->checked_ov_cnt = 0;
}
pr_info("MSC_SET cv_cnt=%d ov_cnt=%d temp_idx:%d->%d, vbatt_idx:%d->%d, fv=%d->%d, cc_max=%d\n",
chg_drv->checked_cv_cnt, chg_drv->checked_ov_cnt,
chg_drv->temp_idx, temp_idx, chg_drv->vbatt_idx,
vbatt_idx, chg_drv->fv_uv, fv_uv, cc_max);
/* taper control on last tier with nonzero charge current */
if (vbatt_idx == (profile->volt_nb_limits - 1) ||
CCCM_LIMITS(profile, temp_idx, vbatt_idx + 1) == 0) {
PSY_SET_PROP(chg_drv->chg_psy,
POWER_SUPPLY_PROP_TAPER_CONTROL,
POWER_SUPPLY_TAPER_CONTROL_MODE_IMMEDIATE);
}
rc = PSY_SET_PROP(chg_psy,
POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX, cc_max);
if (rc == 0)
rc = PSY_SET_PROP(chg_psy,
POWER_SUPPLY_PROP_VOLTAGE_MAX, fv_uv);
if (rc != 0) {
pr_err("MSC_SET: error rc=%d\n", rc);
goto error_rerun;
}
chg_drv->vbatt_idx = vbatt_idx;
chg_drv->temp_idx = temp_idx;
chg_drv->fv_uv = fv_uv;
}
/* DISCHARGING only when not connected */
batt_status = PSY_GET_PROP(chg_psy, POWER_SUPPLY_PROP_STATUS);
/* reset charger if status full but soc < 100%, except recharge */
if (batt_status == POWER_SUPPLY_STATUS_FULL) {
if (soc == chg_drv->charge_stop_level) {
chg_drv->is_full = true;
} else if (!chg_drv->is_full) {
pr_info("MSC_RESET: charge full in unexpected soc. reset chg\n");
reset_chg_drv_state(chg_drv);
}
} else {
chg_drv->is_full = false;
}
switch (batt_status) {
case POWER_SUPPLY_STATUS_DISCHARGING:
rerun_work = false;
break;
case POWER_SUPPLY_STATUS_CHARGING:
case POWER_SUPPLY_STATUS_FULL:
case POWER_SUPPLY_STATUS_NOT_CHARGING:
rerun_work = true;
break;
case POWER_SUPPLY_STATUS_UNKNOWN:
pr_err("chg_work charging status UNKNOWN\n");
goto error_rerun;
default:
pr_err("chg_work invalid charging status %d\n", val.intval);
goto error_rerun;
}
handle_rerun:
if (rerun_work) {
pr_debug("rerun battery charging work in %d ms\n",
update_interval);
schedule_delayed_work(&chg_drv->chg_work,
msecs_to_jiffies(update_interval));
} else {
pr_info("stop battery charging work: batt_status=%d\n",
batt_status);
reset_chg_drv_state(chg_drv);
}
goto exit_chg_work;
error_rerun:
pr_err("error occurred, rerun battery charging work in %d ms\n",
CHG_WORK_ERROR_RETRY_MS);
schedule_delayed_work(&chg_drv->chg_work,
msecs_to_jiffies(CHG_WORK_ERROR_RETRY_MS));
exit_chg_work:
__pm_relax(&chg_drv->chg_ws);
}
static void dump_profile(struct chg_profile *profile)
{
char buff[256];
int ti, vi, count, len = sizeof(buff);
pr_info("Profile constant charge limits:\n");
count = 0;
for (vi = 0; vi < profile->volt_nb_limits; vi++) {
count += scnprintf(buff + count, len - count, " %4d",
profile->volt_limits[vi] / 1000);
}
pr_info("|T \\ V%s\n", buff);
for (ti = 0; ti < profile->temp_nb_limits - 1; ti++) {
count = 0;
count += scnprintf(buff + count, len - count, "|%2d:%2d",
profile->temp_limits[ti] / 10,
profile->temp_limits[ti + 1] / 10);
for (vi = 0; vi < profile->volt_nb_limits; vi++) {
count += scnprintf(buff + count, len - count, " %4d",
CCCM_LIMITS(profile, ti, vi) / 1000);
}
pr_info("%s\n", buff);
}
}
static int chg_init_chg_profile(struct chg_drv *chg_drv)
{
struct device *dev = chg_drv->device;
struct device_node *node = dev->of_node;
struct chg_profile *profile = &chg_drv->chg_profile;
struct taper_wa_struct *taper = &chg_drv->taper;
u32 cccm_array_size, ccm;
int ret = 0, vi, ti;
int taper_tmp;
ret = of_property_read_u32(node, "google,chg-update-interval",
&profile->update_interval);
if (ret < 0) {
pr_err("cannot read chg-update-interval, ret=%d\n", ret);
return ret;
}
ret = of_property_read_u32(node, "google,chg-battery-capacity",
&profile->battery_capacity);
if (ret < 0) {
pr_err("cannot read chg-battery-capacity, ret=%d\n", ret);
return ret;
}
ret = of_property_read_u32(node, "google,cv-hw-resolution",
&profile->cv_hw_resolution);
if (ret < 0)
profile->cv_hw_resolution = 25000;
ret = of_property_read_u32(node, "google,cc-hw-resolution",
&profile->cc_hw_resolution);
if (ret < 0)
profile->cc_hw_resolution = 25000;
/* IEEE1725, default to 0, 1030 for 3% of VTIER */
ret = of_property_read_u32(node, "google,fv-uv-margin-dpct",
&profile->fv_uv_margin_dpct);
if (ret < 0)
profile->fv_uv_margin_dpct = 0;
ret = of_property_read_u32(node, "google,cv-range-accuracy",
&profile->cv_range_accuracy);
if (ret < 0)
profile->cv_range_accuracy = profile->cv_hw_resolution / 2;
/* allow being "a little" over tier voltage, experimental */
ret = of_property_read_u32(node, "google,cv-otv-margin",
&profile->cv_otv_margin);
if (ret < 0)
profile->cv_otv_margin = 0;
ret = of_property_read_u32(node, "google,cv-debounce-cnt",
&profile->cv_debounce_cnt);
if (ret < 0)
profile->cv_debounce_cnt = 3;
ret = of_property_read_u32(node, "google,cv-update-interval",
&profile->cv_update_interval);
if (ret < 0)
profile->cv_update_interval = 2000;
ret = of_property_read_u32(node, "google,cv-tier-ov-cnt",
&profile->cv_tier_ov_cnt);
if (ret < 0)
profile->cv_tier_ov_cnt = 10;
ret = of_property_read_u32(node, "google,cv-tier-switch-cnt",
&profile->cv_tier_switch_cnt);
if (ret < 0)
profile->cv_tier_switch_cnt = 3;
profile->temp_nb_limits =
of_property_count_elems_of_size(node, "google,chg-temp-limits",
sizeof(u32));
if (profile->temp_nb_limits <= 0) {
ret = profile->temp_nb_limits;
pr_err("cannot read chg-temp-limits, ret=%d\n", ret);
return ret;
}
if (profile->temp_nb_limits > CHG_TEMP_NB_LIMITS_MAX) {
pr_err("chg-temp-nb-limits exceeds driver max: %d\n",
CHG_TEMP_NB_LIMITS_MAX);
return -EINVAL;
}
ret = of_property_read_u32_array(node, "google,chg-temp-limits",
profile->temp_limits,
profile->temp_nb_limits);
if (ret < 0) {
pr_err("cannot read chg-temp-limits table, ret=%d\n", ret);
return ret;
}
profile->volt_nb_limits =
of_property_count_elems_of_size(node, "google,chg-cv-limits",
sizeof(u32));
if (profile->volt_nb_limits <= 0) {
ret = profile->volt_nb_limits;
pr_err("cannot read chg-cv-limits, ret=%d\n", ret);
return ret;
}
if (profile->volt_nb_limits > CHG_VOLT_NB_LIMITS_MAX) {
pr_err("chg-cv-nb-limits exceeds driver max: %d\n",
CHG_VOLT_NB_LIMITS_MAX);
return -EINVAL;
}
ret = of_property_read_u32_array(node, "google,chg-cv-limits",
profile->volt_limits,
profile->volt_nb_limits);
if (ret < 0) {
pr_err("cannot read chg-cv-limits table, ret=%d\n", ret);
return ret;
}
for (vi = 0; vi < profile->volt_nb_limits; vi++)
profile->volt_limits[vi] = profile->volt_limits[vi] /
profile->cv_hw_resolution * profile->cv_hw_resolution;
cccm_array_size =
(profile->temp_nb_limits - 1) * profile->volt_nb_limits;
profile->cccm_limits = devm_kzalloc(dev,
sizeof(s32) * cccm_array_size,
GFP_KERNEL);
ret = of_property_read_u32_array(node, "google,chg-cc-limits",
profile->cccm_limits, cccm_array_size);
if (ret < 0) {
pr_err("cannot read chg-cc-limits table, ret=%d\n", ret);
return ret;
}
ret = of_property_read_u32(node, "google,chg-cc-tolerance",
&profile->chg_cc_tolerance);
if (ret < 0)
profile->chg_cc_tolerance = 0;
else if (profile->chg_cc_tolerance > 250)
profile->chg_cc_tolerance = 250;
/* chg-battery-capacity is in mAh, chg-cc-limits relative to 100 */
for (ti = 0; ti < profile->temp_nb_limits - 1; ti++) {
for (vi = 0; vi < profile->volt_nb_limits; vi++) {
ccm = CCCM_LIMITS(profile, ti, vi);
ccm *= profile->battery_capacity * 10;
ccm = ccm * (1000 - profile->chg_cc_tolerance) / 1000;
// round to the nearest resolution the PMIC can handle
ccm = DIV_ROUND_CLOSEST(ccm, profile->cc_hw_resolution)
* profile->cc_hw_resolution;
CCCM_LIMITS(profile, ti, vi) = ccm;
}
}
/* Taper WA */
taper->taper_wa_en = of_property_read_bool(node, "google,chg-taper-wa");
taper_tmp = of_property_count_elems_of_size(node, "google,chg-vtaper",
sizeof(u32));
if (taper_tmp != profile->volt_nb_limits) {
pr_err("wrong vtaper size\n");
taper->taper_wa_en = false;
} else {
ret = of_property_read_u32_array(node, "google,chg-vtaper",
taper->v_taper,
profile->volt_nb_limits);
if (ret < 0) {
pr_err("can't read vtaper, ret=%d\n", ret);
taper->taper_wa_en = false;
}
}
taper_tmp = of_property_count_elems_of_size(node, "google,chg-itaper",
sizeof(u32));
if (taper_tmp != profile->volt_nb_limits) {
pr_err("wrong itaper size\n");
taper->taper_wa_en = false;
} else {
ret = of_property_read_u32_array(node, "google,chg-itaper",
taper->i_taper,
profile->volt_nb_limits);
if (ret < 0) {
pr_err("can't read itaper, ret=%d\n", ret);
taper->taper_wa_en = false;
}
}
taper_tmp = of_property_count_elems_of_size(node, "google,chg-soctaper",
sizeof(u32));
if (taper_tmp != profile->volt_nb_limits) {
pr_err("wrong soctaper size\n");
taper->taper_wa_en = false;
} else {
ret = of_property_read_u32_array(node, "google,chg-soctaper",
taper->soc_taper,
profile->volt_nb_limits);
if (ret < 0) {
pr_err("can't read soctaper, ret=%d\n", ret);
taper->taper_wa_en = false;
}
}
ret = of_property_read_u32(node, "google,chg-taper-cnt-target",
&taper->taper_cnt_target);
if (ret < 0) {
pr_err("can't read taper-cnt-target, ret=%d\n", ret);
taper->taper_wa_en = false;
}
pr_info("successfully read charging profile:\n");
dump_profile(profile);
return 0;
}
static ssize_t show_tier_ovc(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
return scnprintf(buf, PAGE_SIZE, "%d\n",
chg_drv->chg_profile.cv_tier_ov_cnt);
}
static ssize_t set_tier_ovc(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
int ret = 0, val;
ret = kstrtoint(buf, 0, &val);
if (ret < 0)
return ret;
if (val < 0)
return count;
chg_drv->chg_profile.cv_tier_ov_cnt = val;
return count;
}
static DEVICE_ATTR(tier_ovc, 0660, show_tier_ovc, set_tier_ovc);
static ssize_t show_cv_update_interval(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
return scnprintf(buf, PAGE_SIZE, "%d\n",
chg_drv->chg_profile.cv_update_interval);
}
static ssize_t set_cv_update_interval(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
int ret = 0, val;
ret = kstrtoint(buf, 0, &val);
if (ret < 0)
return ret;
if (val < 1500)
return count;
chg_drv->chg_profile.cv_update_interval = val;
return count;
}
static DEVICE_ATTR(cv_update_interval, 0660,
show_cv_update_interval,
set_cv_update_interval);
static ssize_t show_cv_range_accuracy(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
return scnprintf(buf, PAGE_SIZE, "%d\n",
chg_drv->chg_profile.cv_range_accuracy);
}
static ssize_t set_cv_range_accuracy(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
int ret = 0, val;
ret = kstrtoint(buf, 0, &val);
if (ret < 0)
return ret;
chg_drv->chg_profile.cv_range_accuracy = val;
return count;
}
static DEVICE_ATTR(cv_range_accuracy, 0660,
show_cv_range_accuracy,
set_cv_range_accuracy);
static ssize_t show_charge_stop_level(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
return scnprintf(buf, PAGE_SIZE, "%d\n", chg_drv->charge_stop_level);
}
static ssize_t set_charge_stop_level(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
int ret = 0, val;
ret = kstrtoint(buf, 0, &val);
if (ret < 0)
return ret;
if (!chg_drv->bat_psy) {
pr_err("chg_drv->bat_psy is not ready");
return -ENODATA;
}
if ((val == chg_drv->charge_stop_level) ||
(val <= chg_drv->charge_start_level) ||
(val > DEFAULT_CHARGE_STOP_LEVEL))
return count;
chg_drv->charge_stop_level = val;
if (chg_drv->bat_psy)
power_supply_changed(chg_drv->bat_psy);
return count;
}
static DEVICE_ATTR(charge_stop_level, 0660,
show_charge_stop_level, set_charge_stop_level);
static ssize_t
show_charge_start_level(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
return scnprintf(buf, PAGE_SIZE, "%d\n", chg_drv->charge_start_level);
}
static ssize_t set_charge_start_level(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
int ret = 0, val;
ret = kstrtoint(buf, 0, &val);
if (ret < 0)
return ret;
if (!chg_drv->bat_psy) {
pr_err("chg_drv->bat_psy is not ready");
return -ENODATA;
}
if ((val == chg_drv->charge_start_level) ||
(val >= chg_drv->charge_stop_level) ||
(val < DEFAULT_CHARGE_START_LEVEL))
return count;
chg_drv->charge_start_level = val;
if (chg_drv->bat_psy)
power_supply_changed(chg_drv->bat_psy);
return count;
}
static DEVICE_ATTR(charge_start_level, 0660,
show_charge_start_level, set_charge_start_level);
static ssize_t show_bd_temp_enable(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
return scnprintf(buf, PAGE_SIZE, "%d\n",
chg_drv->bd_state.bd_temp_enable);
}
static ssize_t set_bd_temp_enable(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
int ret = 0, val;
ret = kstrtoint(buf, 0, &val);
if (ret < 0)
return ret;
if (chg_drv->bd_state.bd_temp_enable == val)
return count;
chg_drv->bd_state.bd_temp_enable = val;
bd_reset(&chg_drv->bd_state);
if (chg_drv->bat_psy)
power_supply_changed(chg_drv->bat_psy);
return count;
}
static DEVICE_ATTR(bd_temp_enable, 0660,
show_bd_temp_enable, set_bd_temp_enable);
static ssize_t show_bd_trigger_voltage(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
return scnprintf(buf, PAGE_SIZE, "%d\n",
chg_drv->bd_state.bd_trigger_voltage);
}
static ssize_t set_bd_trigger_voltage(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
int ret = 0, val;
ret = kstrtoint(buf, 0, &val);
if (ret < 0)
return ret;
if (val > MAX_BD_VOLTAGE || val < MIN_BD_VOLTAGE)
return -EINVAL;
chg_drv->bd_state.bd_trigger_voltage = val;
return count;
}
static DEVICE_ATTR(bd_trigger_voltage, 0660,
show_bd_trigger_voltage, set_bd_trigger_voltage);
static ssize_t show_bd_drainto_soc(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
return scnprintf(buf, PAGE_SIZE, "%d\n",
chg_drv->bd_state.bd_drainto_soc);
}
static ssize_t set_bd_drainto_soc(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
int ret = 0, val;
ret = kstrtoint(buf, 0, &val);
if (ret < 0)
return ret;
if (val > MAX_BD_SOC || val < MIN_BD_SOC)
return -EINVAL;
chg_drv->bd_state.bd_drainto_soc = val;
return count;
}
static DEVICE_ATTR(bd_drainto_soc, 0660,
show_bd_drainto_soc, set_bd_drainto_soc);
static ssize_t show_bd_trigger_temp(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
return scnprintf(buf, PAGE_SIZE, "%d\n",
chg_drv->bd_state.bd_trigger_temp);
}
static ssize_t set_bd_trigger_temp(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
int ret = 0, val;
ret = kstrtoint(buf, 0, &val);
if (ret < 0)
return ret;
if (val > MAX_BD_TEMP || val < MIN_BD_TEMP)
return -EINVAL;
chg_drv->bd_state.bd_trigger_temp = val;
return count;
}
static DEVICE_ATTR(bd_trigger_temp, 0660,
show_bd_trigger_temp, set_bd_trigger_temp);
static ssize_t show_bd_trigger_time(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
return scnprintf(buf, PAGE_SIZE, "%d\n",
chg_drv->bd_state.bd_trigger_time);
}
static ssize_t set_bd_trigger_time(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
int ret = 0, val;
ret = kstrtoint(buf, 0, &val);
if (ret < 0)
return ret;
if (val < 0)
return -EINVAL;
chg_drv->bd_state.bd_trigger_time = val;
return count;
}
static DEVICE_ATTR(bd_trigger_time, 0660,
show_bd_trigger_time, set_bd_trigger_time);
static ssize_t show_bd_recharge_voltage(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
return scnprintf(buf, PAGE_SIZE, "%d\n",
chg_drv->bd_state.bd_recharge_voltage);
}
static ssize_t set_bd_recharge_voltage(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
int ret = 0, val;
ret = kstrtoint(buf, 0, &val);
if (ret < 0)
return ret;
if (val > MAX_BD_VOLTAGE || val < MIN_BD_VOLTAGE)
return -EINVAL;
chg_drv->bd_state.bd_recharge_voltage = val;
return count;
}
static DEVICE_ATTR(bd_recharge_voltage, 0660,
show_bd_recharge_voltage, set_bd_recharge_voltage);
static ssize_t show_bd_recharge_soc(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
return scnprintf(buf, PAGE_SIZE, "%d\n",
chg_drv->bd_state.bd_recharge_soc);
}
static ssize_t set_bd_recharge_soc(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
int ret = 0, val;
ret = kstrtoint(buf, 0, &val);
if (ret < 0)
return ret;
if (val > MAX_BD_SOC || val < MIN_BD_SOC)
return -EINVAL;
chg_drv->bd_state.bd_recharge_soc = val;
return count;
}
static DEVICE_ATTR(bd_recharge_soc, 0660,
show_bd_recharge_soc, set_bd_recharge_soc);
static ssize_t show_bd_resume_abs_temp(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
return scnprintf(buf, PAGE_SIZE, "%d\n",
chg_drv->bd_state.bd_resume_abs_temp);
}
static ssize_t set_bd_resume_abs_temp(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
int ret = 0, val;
ret = kstrtoint(buf, 0, &val);
if (ret < 0)
return ret;
if (val > MAX_BD_TEMP || val < MIN_BD_TEMP)
return -EINVAL;
chg_drv->bd_state.bd_resume_abs_temp = val;
return count;
}
static DEVICE_ATTR(bd_resume_abs_temp, 0660,
show_bd_resume_abs_temp, set_bd_resume_abs_temp);
static ssize_t show_bd_resume_time(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
return scnprintf(buf, PAGE_SIZE, "%d\n",
chg_drv->bd_state.bd_resume_time);
}
static ssize_t set_bd_resume_time(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
int ret = 0, val;
ret = kstrtoint(buf, 0, &val);
if (ret < 0)
return ret;
if (val < 0)
return -EINVAL;
chg_drv->bd_state.bd_resume_time = val;
return count;
}
static DEVICE_ATTR(bd_resume_time, 0660,
show_bd_resume_time, set_bd_resume_time);
static ssize_t show_bd_resume_temp(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
return scnprintf(buf, PAGE_SIZE, "%d\n",
chg_drv->bd_state.bd_resume_temp);
}
static ssize_t set_bd_resume_temp(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
int ret = 0, val;
ret = kstrtoint(buf, 0, &val);
if (ret < 0)
return ret;
if (val > MAX_BD_TEMP || val < MIN_BD_TEMP)
return -EINVAL;
chg_drv->bd_state.bd_resume_temp = val;
return count;
}
static DEVICE_ATTR(bd_resume_temp, 0660,
show_bd_resume_temp, set_bd_resume_temp);
static ssize_t show_bd_resume_soc(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
return scnprintf(buf, PAGE_SIZE, "%d\n",
chg_drv->bd_state.bd_resume_soc);
}
static ssize_t set_bd_resume_soc(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct chg_drv *chg_drv = dev_get_drvdata(dev);
int ret = 0, val;
ret = kstrtoint(buf, 0, &val);
if (ret < 0)
return ret;
if (val > MAX_BD_SOC || val < MIN_BD_SOC)
return -EINVAL;
chg_drv->bd_state.bd_resume_soc = val;
return count;
}
static DEVICE_ATTR(bd_resume_soc, 0660, show_bd_resume_soc, set_bd_resume_soc);
#ifdef CONFIG_DEBUG_FS
/* use qcom VS maxim fg and more... */
static int get_chg_mode(void *data, u64 *val)
{
struct chg_drv *chg_drv = (struct chg_drv *)data;
*val = chg_drv->chg_mode;
return 0;
}
static int set_chg_mode(void *data, u64 val)
{
struct chg_drv *chg_drv = (struct chg_drv *)data;
chg_drv->chg_mode = val;
return 0;
}
DEFINE_SIMPLE_ATTRIBUTE(chg_mode_fops, get_chg_mode, set_chg_mode, "%llu\n");
/* allow vbatt to be over vtier by margin */
static int get_cv_otv_margin(void *data, u64 *val)
{
struct chg_drv *chg_drv = (struct chg_drv *)data;
*val = chg_drv->chg_profile.cv_otv_margin;
return 0;
}
static int set_cv_otv_margin(void *data, u64 val)
{
struct chg_drv *chg_drv = (struct chg_drv *)data;
chg_drv->chg_profile.cv_otv_margin = val;
return 0;
}
DEFINE_SIMPLE_ATTRIBUTE(cv_otv_margin_fops, get_cv_otv_margin,
set_cv_otv_margin, "%llu\n");
/* cap fv_uv to a percentage of vtier (1000 -> vtier, 1020 -> 2% over vtier) */
static int get_fv_uv_margin(void *data, u64 *val)
{
struct chg_drv *chg_drv = (struct chg_drv *)data;
*val = chg_drv->chg_profile.fv_uv_margin_dpct;
return 0;
}
static int set_fv_uv_margin(void *data, u64 val)
{
struct chg_drv *chg_drv = (struct chg_drv *)data;
chg_drv->chg_profile.fv_uv_margin_dpct = val;
return 0;
}
DEFINE_SIMPLE_ATTRIBUTE(fv_uv_margin_fops, get_fv_uv_margin,
set_fv_uv_margin, "%llu\n");
static int bd_enabled_get(void *data, u64 *val)
{
struct chg_drv *chg_drv = (struct chg_drv *)data;
mutex_lock(&chg_drv->bd_lock);
*val = chg_drv->bd_state.enabled;
mutex_unlock(&chg_drv->bd_lock);
return 0;
}
static int bd_enabled_set(void *data, u64 val)
{
struct chg_drv *chg_drv = (struct chg_drv *)data;
mutex_lock(&chg_drv->bd_lock);
bd_reset(&chg_drv->bd_state);
if (val == 0)
chg_drv->bd_state.enabled = 0;
mutex_unlock(&chg_drv->bd_lock);
return 0;
}
DEFINE_SIMPLE_ATTRIBUTE(bd_enabled_fops, bd_enabled_get,
bd_enabled_set, "%lld\n");
#endif
static int init_debugfs(struct chg_drv *chg_drv)
{
#ifdef CONFIG_DEBUG_FS
struct dentry *de;
de = debugfs_create_dir("google_charger", 0);
if (de) {
debugfs_create_file("chg_mode", 0644, de,
chg_drv, &chg_mode_fops);
debugfs_create_file("cv_otv_margin", 0644, de,
chg_drv, &cv_otv_margin_fops);
debugfs_create_file("fv_uv_margin", 0644, de,
chg_drv, &fv_uv_margin_fops);
debugfs_create_u32("bd_triggered", 0644, de,
&chg_drv->bd_state.triggered);
debugfs_create_file("bd_enabled", 0600, de,
chg_drv, &bd_enabled_fops);
}
#endif
return 0;
}
static void google_charger_init_work(struct work_struct *work)
{
struct chg_drv *chg_drv = container_of(work, struct chg_drv,
init_work.work);
struct power_supply *chg_psy, *usb_psy, *wlc_psy = NULL, *bat_psy;
int ret = 0;
chg_psy = power_supply_get_by_name(chg_drv->chg_psy_name);
if (!chg_psy) {
pr_info("failed to get \"%s\" power supply\n",
chg_drv->chg_psy_name);
goto retry_init_work;
}
bat_psy = power_supply_get_by_name(chg_drv->bat_psy_name);
if (!bat_psy) {
pr_info("failed to get \"%s\" power supply\n",
chg_drv->bat_psy_name);
power_supply_put(chg_psy);
goto retry_init_work;
}
usb_psy = power_supply_get_by_name("usb");
if (!usb_psy) {
pr_info("failed to get \"usb\" power supply\n");
power_supply_put(chg_psy);
power_supply_put(bat_psy);
goto retry_init_work;
}
if (chg_drv->wlc_psy_name) {
wlc_psy = power_supply_get_by_name(chg_drv->wlc_psy_name);
if (!wlc_psy) {
pr_info("failed to get \"%s\" power supply\n",
chg_drv->wlc_psy_name);
power_supply_put(chg_psy);
power_supply_put(bat_psy);
power_supply_put(usb_psy);
goto retry_init_work;
}
}
chg_drv->chg_psy = chg_psy;
chg_drv->wlc_psy = wlc_psy;
chg_drv->usb_psy = usb_psy;
chg_drv->bat_psy = bat_psy;
chg_drv->charge_stop_level = DEFAULT_CHARGE_STOP_LEVEL;
chg_drv->charge_start_level = DEFAULT_CHARGE_START_LEVEL;
reset_chg_drv_state(chg_drv);
chg_drv->plugged = -1;
chg_drv->psy_nb.notifier_call = psy_changed;
ret = power_supply_reg_notifier(&chg_drv->psy_nb);
if (ret < 0)
pr_err("Cannot register power supply notifer, ret=%d\n", ret);
wakeup_source_init(&chg_drv->chg_ws, "google-charger");
pr_info("google_charger_init_work done\n");
mutex_init(&chg_drv->bd_lock);
wakeup_source_init(&chg_drv->bd_ws, "defender");
INIT_DELAYED_WORK(&chg_drv->bd_work, bd_work);
bd_init(&chg_drv->bd_state, chg_drv->device);
/* catch state changes that happened before registering the notifier */
schedule_delayed_work(&chg_drv->chg_work,
msecs_to_jiffies(CHG_DELAY_INIT_DETECT_MS));
return;
retry_init_work:
schedule_delayed_work(&chg_drv->init_work,
msecs_to_jiffies(CHG_DELAY_INIT_MS));
}
static int google_charger_probe(struct platform_device *pdev)
{
const char *chg_psy_name, *bat_psy_name, *wlc_psy_name = NULL;
struct chg_drv *chg_drv;
int ret;
chg_drv = devm_kzalloc(&pdev->dev, sizeof(*chg_drv), GFP_KERNEL);
if (!chg_drv)
return -ENOMEM;
chg_drv->device = &pdev->dev;
ret = of_property_read_string(pdev->dev.of_node,
"google,chg-power-supply",
&chg_psy_name);
if (ret != 0) {
pr_err("cannot read google,chg-power-supply, ret=%d\n", ret);
return -EINVAL;
}
chg_drv->chg_psy_name =
devm_kstrdup(&pdev->dev, chg_psy_name, GFP_KERNEL);
if (!chg_drv->chg_psy_name)
return -ENOMEM;
ret = of_property_read_string(pdev->dev.of_node,
"google,bat-power-supply",
&bat_psy_name);
if (ret != 0) {
pr_err("cannot read google,bat-power-supply, ret=%d\n", ret);
return -EINVAL;
}
chg_drv->bat_psy_name =
devm_kstrdup(&pdev->dev, bat_psy_name, GFP_KERNEL);
if (!chg_drv->bat_psy_name)
return -ENOMEM;
ret = of_property_read_string(pdev->dev.of_node,
"google,wlc-power-supply",
&wlc_psy_name);
if (ret != 0)
pr_warn("google,wlc-power-supply not defined\n");
if (wlc_psy_name) {
chg_drv->wlc_psy_name =
devm_kstrdup(&pdev->dev, wlc_psy_name, GFP_KERNEL);
if (!chg_drv->wlc_psy_name)
return -ENOMEM;
}
ret = chg_init_chg_profile(chg_drv);
if (ret < 0) {
pr_err("cannot read charging profile from dt, ret=%d\n", ret);
return ret;
}
ret = device_create_file(&pdev->dev, &dev_attr_charge_stop_level);
if (ret != 0) {
pr_err("Failed to create charge_stop_level files, ret=%d\n",
ret);
return ret;
}
ret = device_create_file(&pdev->dev, &dev_attr_charge_start_level);
if (ret != 0) {
pr_err("Failed to create charge_start_level files, ret=%d\n",
ret);
return ret;
}
ret = device_create_file(chg_drv->device, &dev_attr_bd_temp_enable);
if (ret != 0) {
pr_err("Failed to create bd_temp_enable files, ret=%d\n", ret);
return ret;
}
ret = device_create_file(chg_drv->device, &dev_attr_bd_trigger_voltage);
if (ret != 0) {
pr_err("Failed to create bd_trigger_voltage files, ret=%d\n",
ret);
return ret;
}
ret = device_create_file(chg_drv->device, &dev_attr_bd_drainto_soc);
if (ret != 0) {
pr_err("Failed to create bd_drainto_soc files, ret=%d\n",
ret);
return ret;
}
ret = device_create_file(chg_drv->device, &dev_attr_bd_trigger_temp);
if (ret != 0) {
pr_err("Failed to create bd_trigger_temp files, ret=%d\n",
ret);
return ret;
}
ret = device_create_file(chg_drv->device, &dev_attr_bd_trigger_time);
if (ret != 0) {
pr_err("Failed to create bd_trigger_time files, ret=%d\n",
ret);
return ret;
}
ret = device_create_file(chg_drv->device,
&dev_attr_bd_recharge_voltage);
if (ret != 0) {
pr_err("Failed to create bd_recharge_voltage files, ret=%d\n",
ret);
return ret;
}
ret = device_create_file(chg_drv->device, &dev_attr_bd_recharge_soc);
if (ret != 0) {
pr_err("Failed to create bd_recharge_soc files, ret=%d\n",
ret);
return ret;
}
ret = device_create_file(chg_drv->device, &dev_attr_bd_resume_abs_temp);
if (ret != 0) {
pr_err("Failed to create bd_resume_abs_temp files, ret=%d\n",
ret);
return ret;
}
ret = device_create_file(chg_drv->device, &dev_attr_bd_resume_time);
if (ret != 0) {
pr_err("Failed to create bd_trigger_time files, ret=%d\n",
ret);
return ret;
}
ret = device_create_file(chg_drv->device, &dev_attr_bd_resume_temp);
if (ret != 0) {
pr_err("Failed to create bd_resume_temp files, ret=%d\n",
ret);
return ret;
}
ret = device_create_file(chg_drv->device, &dev_attr_bd_resume_soc);
if (ret != 0) {
pr_err("Failed to create bd_resume_soc files, ret=%d\n",
ret);
return ret;
}
// TODO: move to debugfs
ret = device_create_file(&pdev->dev, &dev_attr_cv_range_accuracy);
if (ret != 0) {
pr_err("Failed to create cv_range_accuracy files, ret=%d\n",
ret);
return ret;
}
// TODO: move to debugfs
ret = device_create_file(&pdev->dev, &dev_attr_cv_update_interval);
if (ret != 0) {
pr_err("Failed to create cv_update_interval files, ret=%d\n",
ret);
return ret;
}
// TODO: move to debugfs
ret = device_create_file(&pdev->dev, &dev_attr_tier_ovc);
if (ret != 0) {
pr_err("Failed to create tier_ovc files, ret=%d\n", ret);
return ret;
}
/* debug */
init_debugfs(chg_drv);
INIT_DELAYED_WORK(&chg_drv->init_work, google_charger_init_work);
INIT_DELAYED_WORK(&chg_drv->chg_work, chg_work);
platform_set_drvdata(pdev, chg_drv);
schedule_delayed_work(&chg_drv->init_work,
msecs_to_jiffies(CHG_DELAY_INIT_MS));
return 0;
}
static int google_charger_remove(struct platform_device *pdev)
{
struct chg_drv *chg_drv = platform_get_drvdata(pdev);
if (chg_drv) {
if (chg_drv->chg_psy)
power_supply_put(chg_drv->chg_psy);
if (chg_drv->bat_psy)
power_supply_put(chg_drv->bat_psy);
if (chg_drv->usb_psy)
power_supply_put(chg_drv->usb_psy);
if (chg_drv->wlc_psy)
power_supply_put(chg_drv->wlc_psy);
wakeup_source_trash(&chg_drv->bd_ws);
wakeup_source_trash(&chg_drv->chg_ws);
}
return 0;
}
static const struct of_device_id match_table[] = {
{.compatible = "google,charger"},
{},
};
static struct platform_driver charger_driver = {
.driver = {
.name = "google,charger",
.owner = THIS_MODULE,
.of_match_table = match_table,
},
.probe = google_charger_probe,
.remove = google_charger_remove,
};
static int __init google_charger_init(void)
{
int ret;
ret = platform_driver_register(&charger_driver);
if (ret < 0) {
pr_err("device registration failed: %d\n", ret);
return ret;
}
return 0;
}
static void __init google_charger_exit(void)
{
platform_driver_unregister(&charger_driver);
pr_info("unregistered platform driver\n");
}
module_init(google_charger_init);
module_exit(google_charger_exit);
MODULE_DESCRIPTION("Multi-step battery charger driver");
MODULE_AUTHOR("Thierry Strudel <tstrudel@google.com>");
MODULE_LICENSE("GPL");