mirror of https://github.com/Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2099 lines
58 KiB
2099 lines
58 KiB
// SPDX-License-Identifier: GPL-2.0-only |
|
/* |
|
* Copyright (C) ST-Ericsson SA 2012 |
|
* Copyright (c) 2012 Sony Mobile Communications AB |
|
* |
|
* Charging algorithm driver for abx500 variants |
|
* |
|
* Authors: |
|
* Johan Palsson <[email protected]> |
|
* Karl Komierowski <[email protected]> |
|
* Arun R Murthy <[email protected]> |
|
* Author: Imre Sunyi <[email protected]> |
|
*/ |
|
|
|
#include <linux/init.h> |
|
#include <linux/module.h> |
|
#include <linux/device.h> |
|
#include <linux/hrtimer.h> |
|
#include <linux/interrupt.h> |
|
#include <linux/delay.h> |
|
#include <linux/slab.h> |
|
#include <linux/platform_device.h> |
|
#include <linux/power_supply.h> |
|
#include <linux/completion.h> |
|
#include <linux/workqueue.h> |
|
#include <linux/kobject.h> |
|
#include <linux/of.h> |
|
#include <linux/mfd/core.h> |
|
#include <linux/mfd/abx500.h> |
|
#include <linux/mfd/abx500/ab8500.h> |
|
#include <linux/mfd/abx500/ux500_chargalg.h> |
|
#include <linux/mfd/abx500/ab8500-bm.h> |
|
#include <linux/notifier.h> |
|
|
|
/* Watchdog kick interval */ |
|
#define CHG_WD_INTERVAL (6 * HZ) |
|
|
|
/* End-of-charge criteria counter */ |
|
#define EOC_COND_CNT 10 |
|
|
|
/* One hour expressed in seconds */ |
|
#define ONE_HOUR_IN_SECONDS 3600 |
|
|
|
/* Five minutes expressed in seconds */ |
|
#define FIVE_MINUTES_IN_SECONDS 300 |
|
|
|
#define CHARGALG_CURR_STEP_LOW 0 |
|
#define CHARGALG_CURR_STEP_HIGH 100 |
|
|
|
enum abx500_chargers { |
|
NO_CHG, |
|
AC_CHG, |
|
USB_CHG, |
|
}; |
|
|
|
struct abx500_chargalg_charger_info { |
|
enum abx500_chargers conn_chg; |
|
enum abx500_chargers prev_conn_chg; |
|
enum abx500_chargers online_chg; |
|
enum abx500_chargers prev_online_chg; |
|
enum abx500_chargers charger_type; |
|
bool usb_chg_ok; |
|
bool ac_chg_ok; |
|
int usb_volt; |
|
int usb_curr; |
|
int ac_volt; |
|
int ac_curr; |
|
int usb_vset; |
|
int usb_iset; |
|
int ac_vset; |
|
int ac_iset; |
|
}; |
|
|
|
struct abx500_chargalg_suspension_status { |
|
bool suspended_change; |
|
bool ac_suspended; |
|
bool usb_suspended; |
|
}; |
|
|
|
struct abx500_chargalg_current_step_status { |
|
bool curr_step_change; |
|
int curr_step; |
|
}; |
|
|
|
struct abx500_chargalg_battery_data { |
|
int temp; |
|
int volt; |
|
int avg_curr; |
|
int inst_curr; |
|
int percent; |
|
}; |
|
|
|
enum abx500_chargalg_states { |
|
STATE_HANDHELD_INIT, |
|
STATE_HANDHELD, |
|
STATE_CHG_NOT_OK_INIT, |
|
STATE_CHG_NOT_OK, |
|
STATE_HW_TEMP_PROTECT_INIT, |
|
STATE_HW_TEMP_PROTECT, |
|
STATE_NORMAL_INIT, |
|
STATE_NORMAL, |
|
STATE_WAIT_FOR_RECHARGE_INIT, |
|
STATE_WAIT_FOR_RECHARGE, |
|
STATE_MAINTENANCE_A_INIT, |
|
STATE_MAINTENANCE_A, |
|
STATE_MAINTENANCE_B_INIT, |
|
STATE_MAINTENANCE_B, |
|
STATE_TEMP_UNDEROVER_INIT, |
|
STATE_TEMP_UNDEROVER, |
|
STATE_TEMP_LOWHIGH_INIT, |
|
STATE_TEMP_LOWHIGH, |
|
STATE_SUSPENDED_INIT, |
|
STATE_SUSPENDED, |
|
STATE_OVV_PROTECT_INIT, |
|
STATE_OVV_PROTECT, |
|
STATE_SAFETY_TIMER_EXPIRED_INIT, |
|
STATE_SAFETY_TIMER_EXPIRED, |
|
STATE_BATT_REMOVED_INIT, |
|
STATE_BATT_REMOVED, |
|
STATE_WD_EXPIRED_INIT, |
|
STATE_WD_EXPIRED, |
|
}; |
|
|
|
static const char *states[] = { |
|
"HANDHELD_INIT", |
|
"HANDHELD", |
|
"CHG_NOT_OK_INIT", |
|
"CHG_NOT_OK", |
|
"HW_TEMP_PROTECT_INIT", |
|
"HW_TEMP_PROTECT", |
|
"NORMAL_INIT", |
|
"NORMAL", |
|
"WAIT_FOR_RECHARGE_INIT", |
|
"WAIT_FOR_RECHARGE", |
|
"MAINTENANCE_A_INIT", |
|
"MAINTENANCE_A", |
|
"MAINTENANCE_B_INIT", |
|
"MAINTENANCE_B", |
|
"TEMP_UNDEROVER_INIT", |
|
"TEMP_UNDEROVER", |
|
"TEMP_LOWHIGH_INIT", |
|
"TEMP_LOWHIGH", |
|
"SUSPENDED_INIT", |
|
"SUSPENDED", |
|
"OVV_PROTECT_INIT", |
|
"OVV_PROTECT", |
|
"SAFETY_TIMER_EXPIRED_INIT", |
|
"SAFETY_TIMER_EXPIRED", |
|
"BATT_REMOVED_INIT", |
|
"BATT_REMOVED", |
|
"WD_EXPIRED_INIT", |
|
"WD_EXPIRED", |
|
}; |
|
|
|
struct abx500_chargalg_events { |
|
bool batt_unknown; |
|
bool mainextchnotok; |
|
bool batt_ovv; |
|
bool batt_rem; |
|
bool btemp_underover; |
|
bool btemp_lowhigh; |
|
bool main_thermal_prot; |
|
bool usb_thermal_prot; |
|
bool main_ovv; |
|
bool vbus_ovv; |
|
bool usbchargernotok; |
|
bool safety_timer_expired; |
|
bool maintenance_timer_expired; |
|
bool ac_wd_expired; |
|
bool usb_wd_expired; |
|
bool ac_cv_active; |
|
bool usb_cv_active; |
|
bool vbus_collapsed; |
|
}; |
|
|
|
/** |
|
* struct abx500_charge_curr_maximization - Charger maximization parameters |
|
* @original_iset: the non optimized/maximised charger current |
|
* @current_iset: the charging current used at this moment |
|
* @test_delta_i: the delta between the current we want to charge and the |
|
current that is really going into the battery |
|
* @condition_cnt: number of iterations needed before a new charger current |
|
is set |
|
* @max_current: maximum charger current |
|
* @wait_cnt: to avoid too fast current step down in case of charger |
|
* voltage collapse, we insert this delay between step |
|
* down |
|
* @level: tells in how many steps the charging current has been |
|
increased |
|
*/ |
|
struct abx500_charge_curr_maximization { |
|
int original_iset; |
|
int current_iset; |
|
int test_delta_i; |
|
int condition_cnt; |
|
int max_current; |
|
int wait_cnt; |
|
u8 level; |
|
}; |
|
|
|
enum maxim_ret { |
|
MAXIM_RET_NOACTION, |
|
MAXIM_RET_CHANGE, |
|
MAXIM_RET_IBAT_TOO_HIGH, |
|
}; |
|
|
|
/** |
|
* struct abx500_chargalg - abx500 Charging algorithm device information |
|
* @dev: pointer to the structure device |
|
* @charge_status: battery operating status |
|
* @eoc_cnt: counter used to determine end-of_charge |
|
* @maintenance_chg: indicate if maintenance charge is active |
|
* @t_hyst_norm temperature hysteresis when the temperature has been |
|
* over or under normal limits |
|
* @t_hyst_lowhigh temperature hysteresis when the temperature has been |
|
* over or under the high or low limits |
|
* @charge_state: current state of the charging algorithm |
|
* @ccm charging current maximization parameters |
|
* @chg_info: information about connected charger types |
|
* @batt_data: data of the battery |
|
* @susp_status: current charger suspension status |
|
* @bm: Platform specific battery management information |
|
* @curr_status: Current step status for over-current protection |
|
* @parent: pointer to the struct abx500 |
|
* @chargalg_psy: structure that holds the battery properties exposed by |
|
* the charging algorithm |
|
* @events: structure for information about events triggered |
|
* @chargalg_wq: work queue for running the charging algorithm |
|
* @chargalg_periodic_work: work to run the charging algorithm periodically |
|
* @chargalg_wd_work: work to kick the charger watchdog periodically |
|
* @chargalg_work: work to run the charging algorithm instantly |
|
* @safety_timer: charging safety timer |
|
* @maintenance_timer: maintenance charging timer |
|
* @chargalg_kobject: structure of type kobject |
|
*/ |
|
struct abx500_chargalg { |
|
struct device *dev; |
|
int charge_status; |
|
int eoc_cnt; |
|
bool maintenance_chg; |
|
int t_hyst_norm; |
|
int t_hyst_lowhigh; |
|
enum abx500_chargalg_states charge_state; |
|
struct abx500_charge_curr_maximization ccm; |
|
struct abx500_chargalg_charger_info chg_info; |
|
struct abx500_chargalg_battery_data batt_data; |
|
struct abx500_chargalg_suspension_status susp_status; |
|
struct ab8500 *parent; |
|
struct abx500_chargalg_current_step_status curr_status; |
|
struct abx500_bm_data *bm; |
|
struct power_supply *chargalg_psy; |
|
struct ux500_charger *ac_chg; |
|
struct ux500_charger *usb_chg; |
|
struct abx500_chargalg_events events; |
|
struct workqueue_struct *chargalg_wq; |
|
struct delayed_work chargalg_periodic_work; |
|
struct delayed_work chargalg_wd_work; |
|
struct work_struct chargalg_work; |
|
struct hrtimer safety_timer; |
|
struct hrtimer maintenance_timer; |
|
struct kobject chargalg_kobject; |
|
}; |
|
|
|
/*External charger prepare notifier*/ |
|
BLOCKING_NOTIFIER_HEAD(charger_notifier_list); |
|
|
|
/* Main battery properties */ |
|
static enum power_supply_property abx500_chargalg_props[] = { |
|
POWER_SUPPLY_PROP_STATUS, |
|
POWER_SUPPLY_PROP_HEALTH, |
|
}; |
|
|
|
struct abx500_chargalg_sysfs_entry { |
|
struct attribute attr; |
|
ssize_t (*show)(struct abx500_chargalg *, char *); |
|
ssize_t (*store)(struct abx500_chargalg *, const char *, size_t); |
|
}; |
|
|
|
/** |
|
* abx500_chargalg_safety_timer_expired() - Expiration of the safety timer |
|
* @timer: pointer to the hrtimer structure |
|
* |
|
* This function gets called when the safety timer for the charger |
|
* expires |
|
*/ |
|
static enum hrtimer_restart |
|
abx500_chargalg_safety_timer_expired(struct hrtimer *timer) |
|
{ |
|
struct abx500_chargalg *di = container_of(timer, struct abx500_chargalg, |
|
safety_timer); |
|
dev_err(di->dev, "Safety timer expired\n"); |
|
di->events.safety_timer_expired = true; |
|
|
|
/* Trigger execution of the algorithm instantly */ |
|
queue_work(di->chargalg_wq, &di->chargalg_work); |
|
|
|
return HRTIMER_NORESTART; |
|
} |
|
|
|
/** |
|
* abx500_chargalg_maintenance_timer_expired() - Expiration of |
|
* the maintenance timer |
|
* @timer: pointer to the timer structure |
|
* |
|
* This function gets called when the maintenence timer |
|
* expires |
|
*/ |
|
static enum hrtimer_restart |
|
abx500_chargalg_maintenance_timer_expired(struct hrtimer *timer) |
|
{ |
|
|
|
struct abx500_chargalg *di = container_of(timer, struct abx500_chargalg, |
|
maintenance_timer); |
|
|
|
dev_dbg(di->dev, "Maintenance timer expired\n"); |
|
di->events.maintenance_timer_expired = true; |
|
|
|
/* Trigger execution of the algorithm instantly */ |
|
queue_work(di->chargalg_wq, &di->chargalg_work); |
|
|
|
return HRTIMER_NORESTART; |
|
} |
|
|
|
/** |
|
* abx500_chargalg_state_to() - Change charge state |
|
* @di: pointer to the abx500_chargalg structure |
|
* |
|
* This function gets called when a charge state change should occur |
|
*/ |
|
static void abx500_chargalg_state_to(struct abx500_chargalg *di, |
|
enum abx500_chargalg_states state) |
|
{ |
|
dev_dbg(di->dev, |
|
"State changed: %s (From state: [%d] %s =to=> [%d] %s )\n", |
|
di->charge_state == state ? "NO" : "YES", |
|
di->charge_state, |
|
states[di->charge_state], |
|
state, |
|
states[state]); |
|
|
|
di->charge_state = state; |
|
} |
|
|
|
static int abx500_chargalg_check_charger_enable(struct abx500_chargalg *di) |
|
{ |
|
switch (di->charge_state) { |
|
case STATE_NORMAL: |
|
case STATE_MAINTENANCE_A: |
|
case STATE_MAINTENANCE_B: |
|
break; |
|
default: |
|
return 0; |
|
} |
|
|
|
if (di->chg_info.charger_type & USB_CHG) { |
|
return di->usb_chg->ops.check_enable(di->usb_chg, |
|
di->bm->bat_type[di->bm->batt_id].normal_vol_lvl, |
|
di->bm->bat_type[di->bm->batt_id].normal_cur_lvl); |
|
} else if ((di->chg_info.charger_type & AC_CHG) && |
|
!(di->ac_chg->external)) { |
|
return di->ac_chg->ops.check_enable(di->ac_chg, |
|
di->bm->bat_type[di->bm->batt_id].normal_vol_lvl, |
|
di->bm->bat_type[di->bm->batt_id].normal_cur_lvl); |
|
} |
|
return 0; |
|
} |
|
|
|
/** |
|
* abx500_chargalg_check_charger_connection() - Check charger connection change |
|
* @di: pointer to the abx500_chargalg structure |
|
* |
|
* This function will check if there is a change in the charger connection |
|
* and change charge state accordingly. AC has precedence over USB. |
|
*/ |
|
static int abx500_chargalg_check_charger_connection(struct abx500_chargalg *di) |
|
{ |
|
if (di->chg_info.conn_chg != di->chg_info.prev_conn_chg || |
|
di->susp_status.suspended_change) { |
|
/* |
|
* Charger state changed or suspension |
|
* has changed since last update |
|
*/ |
|
if ((di->chg_info.conn_chg & AC_CHG) && |
|
!di->susp_status.ac_suspended) { |
|
dev_dbg(di->dev, "Charging source is AC\n"); |
|
if (di->chg_info.charger_type != AC_CHG) { |
|
di->chg_info.charger_type = AC_CHG; |
|
abx500_chargalg_state_to(di, STATE_NORMAL_INIT); |
|
} |
|
} else if ((di->chg_info.conn_chg & USB_CHG) && |
|
!di->susp_status.usb_suspended) { |
|
dev_dbg(di->dev, "Charging source is USB\n"); |
|
di->chg_info.charger_type = USB_CHG; |
|
abx500_chargalg_state_to(di, STATE_NORMAL_INIT); |
|
} else if (di->chg_info.conn_chg && |
|
(di->susp_status.ac_suspended || |
|
di->susp_status.usb_suspended)) { |
|
dev_dbg(di->dev, "Charging is suspended\n"); |
|
di->chg_info.charger_type = NO_CHG; |
|
abx500_chargalg_state_to(di, STATE_SUSPENDED_INIT); |
|
} else { |
|
dev_dbg(di->dev, "Charging source is OFF\n"); |
|
di->chg_info.charger_type = NO_CHG; |
|
abx500_chargalg_state_to(di, STATE_HANDHELD_INIT); |
|
} |
|
di->chg_info.prev_conn_chg = di->chg_info.conn_chg; |
|
di->susp_status.suspended_change = false; |
|
} |
|
return di->chg_info.conn_chg; |
|
} |
|
|
|
/** |
|
* abx500_chargalg_check_current_step_status() - Check charging current |
|
* step status. |
|
* @di: pointer to the abx500_chargalg structure |
|
* |
|
* This function will check if there is a change in the charging current step |
|
* and change charge state accordingly. |
|
*/ |
|
static void abx500_chargalg_check_current_step_status |
|
(struct abx500_chargalg *di) |
|
{ |
|
if (di->curr_status.curr_step_change) |
|
abx500_chargalg_state_to(di, STATE_NORMAL_INIT); |
|
di->curr_status.curr_step_change = false; |
|
} |
|
|
|
/** |
|
* abx500_chargalg_start_safety_timer() - Start charging safety timer |
|
* @di: pointer to the abx500_chargalg structure |
|
* |
|
* The safety timer is used to avoid overcharging of old or bad batteries. |
|
* There are different timers for AC and USB |
|
*/ |
|
static void abx500_chargalg_start_safety_timer(struct abx500_chargalg *di) |
|
{ |
|
/* Charger-dependent expiration time in hours*/ |
|
int timer_expiration = 0; |
|
|
|
switch (di->chg_info.charger_type) { |
|
case AC_CHG: |
|
timer_expiration = di->bm->main_safety_tmr_h; |
|
break; |
|
|
|
case USB_CHG: |
|
timer_expiration = di->bm->usb_safety_tmr_h; |
|
break; |
|
|
|
default: |
|
dev_err(di->dev, "Unknown charger to charge from\n"); |
|
break; |
|
} |
|
|
|
di->events.safety_timer_expired = false; |
|
hrtimer_set_expires_range(&di->safety_timer, |
|
ktime_set(timer_expiration * ONE_HOUR_IN_SECONDS, 0), |
|
ktime_set(FIVE_MINUTES_IN_SECONDS, 0)); |
|
hrtimer_start_expires(&di->safety_timer, HRTIMER_MODE_REL); |
|
} |
|
|
|
/** |
|
* abx500_chargalg_stop_safety_timer() - Stop charging safety timer |
|
* @di: pointer to the abx500_chargalg structure |
|
* |
|
* The safety timer is stopped whenever the NORMAL state is exited |
|
*/ |
|
static void abx500_chargalg_stop_safety_timer(struct abx500_chargalg *di) |
|
{ |
|
if (hrtimer_try_to_cancel(&di->safety_timer) >= 0) |
|
di->events.safety_timer_expired = false; |
|
} |
|
|
|
/** |
|
* abx500_chargalg_start_maintenance_timer() - Start charging maintenance timer |
|
* @di: pointer to the abx500_chargalg structure |
|
* @duration: duration of ther maintenance timer in hours |
|
* |
|
* The maintenance timer is used to maintain the charge in the battery once |
|
* the battery is considered full. These timers are chosen to match the |
|
* discharge curve of the battery |
|
*/ |
|
static void abx500_chargalg_start_maintenance_timer(struct abx500_chargalg *di, |
|
int duration) |
|
{ |
|
hrtimer_set_expires_range(&di->maintenance_timer, |
|
ktime_set(duration * ONE_HOUR_IN_SECONDS, 0), |
|
ktime_set(FIVE_MINUTES_IN_SECONDS, 0)); |
|
di->events.maintenance_timer_expired = false; |
|
hrtimer_start_expires(&di->maintenance_timer, HRTIMER_MODE_REL); |
|
} |
|
|
|
/** |
|
* abx500_chargalg_stop_maintenance_timer() - Stop maintenance timer |
|
* @di: pointer to the abx500_chargalg structure |
|
* |
|
* The maintenance timer is stopped whenever maintenance ends or when another |
|
* state is entered |
|
*/ |
|
static void abx500_chargalg_stop_maintenance_timer(struct abx500_chargalg *di) |
|
{ |
|
if (hrtimer_try_to_cancel(&di->maintenance_timer) >= 0) |
|
di->events.maintenance_timer_expired = false; |
|
} |
|
|
|
/** |
|
* abx500_chargalg_kick_watchdog() - Kick charger watchdog |
|
* @di: pointer to the abx500_chargalg structure |
|
* |
|
* The charger watchdog have to be kicked periodically whenever the charger is |
|
* on, else the ABB will reset the system |
|
*/ |
|
static int abx500_chargalg_kick_watchdog(struct abx500_chargalg *di) |
|
{ |
|
/* Check if charger exists and kick watchdog if charging */ |
|
if (di->ac_chg && di->ac_chg->ops.kick_wd && |
|
di->chg_info.online_chg & AC_CHG) { |
|
/* |
|
* If AB charger watchdog expired, pm2xxx charging |
|
* gets disabled. To be safe, kick both AB charger watchdog |
|
* and pm2xxx watchdog. |
|
*/ |
|
if (di->ac_chg->external && |
|
di->usb_chg && di->usb_chg->ops.kick_wd) |
|
di->usb_chg->ops.kick_wd(di->usb_chg); |
|
|
|
return di->ac_chg->ops.kick_wd(di->ac_chg); |
|
} |
|
else if (di->usb_chg && di->usb_chg->ops.kick_wd && |
|
di->chg_info.online_chg & USB_CHG) |
|
return di->usb_chg->ops.kick_wd(di->usb_chg); |
|
|
|
return -ENXIO; |
|
} |
|
|
|
/** |
|
* abx500_chargalg_ac_en() - Turn on/off the AC charger |
|
* @di: pointer to the abx500_chargalg structure |
|
* @enable: charger on/off |
|
* @vset: requested charger output voltage |
|
* @iset: requested charger output current |
|
* |
|
* The AC charger will be turned on/off with the requested charge voltage and |
|
* current |
|
*/ |
|
static int abx500_chargalg_ac_en(struct abx500_chargalg *di, int enable, |
|
int vset, int iset) |
|
{ |
|
static int abx500_chargalg_ex_ac_enable_toggle; |
|
|
|
if (!di->ac_chg || !di->ac_chg->ops.enable) |
|
return -ENXIO; |
|
|
|
/* Select maximum of what both the charger and the battery supports */ |
|
if (di->ac_chg->max_out_volt) |
|
vset = min(vset, di->ac_chg->max_out_volt); |
|
if (di->ac_chg->max_out_curr) |
|
iset = min(iset, di->ac_chg->max_out_curr); |
|
|
|
di->chg_info.ac_iset = iset; |
|
di->chg_info.ac_vset = vset; |
|
|
|
/* Enable external charger */ |
|
if (enable && di->ac_chg->external && |
|
!abx500_chargalg_ex_ac_enable_toggle) { |
|
blocking_notifier_call_chain(&charger_notifier_list, |
|
0, di->dev); |
|
abx500_chargalg_ex_ac_enable_toggle++; |
|
} |
|
|
|
return di->ac_chg->ops.enable(di->ac_chg, enable, vset, iset); |
|
} |
|
|
|
/** |
|
* abx500_chargalg_usb_en() - Turn on/off the USB charger |
|
* @di: pointer to the abx500_chargalg structure |
|
* @enable: charger on/off |
|
* @vset: requested charger output voltage |
|
* @iset: requested charger output current |
|
* |
|
* The USB charger will be turned on/off with the requested charge voltage and |
|
* current |
|
*/ |
|
static int abx500_chargalg_usb_en(struct abx500_chargalg *di, int enable, |
|
int vset, int iset) |
|
{ |
|
if (!di->usb_chg || !di->usb_chg->ops.enable) |
|
return -ENXIO; |
|
|
|
/* Select maximum of what both the charger and the battery supports */ |
|
if (di->usb_chg->max_out_volt) |
|
vset = min(vset, di->usb_chg->max_out_volt); |
|
if (di->usb_chg->max_out_curr) |
|
iset = min(iset, di->usb_chg->max_out_curr); |
|
|
|
di->chg_info.usb_iset = iset; |
|
di->chg_info.usb_vset = vset; |
|
|
|
return di->usb_chg->ops.enable(di->usb_chg, enable, vset, iset); |
|
} |
|
|
|
/** |
|
* abx500_chargalg_update_chg_curr() - Update charger current |
|
* @di: pointer to the abx500_chargalg structure |
|
* @iset: requested charger output current |
|
* |
|
* The charger output current will be updated for the charger |
|
* that is currently in use |
|
*/ |
|
static int abx500_chargalg_update_chg_curr(struct abx500_chargalg *di, |
|
int iset) |
|
{ |
|
/* Check if charger exists and update current if charging */ |
|
if (di->ac_chg && di->ac_chg->ops.update_curr && |
|
di->chg_info.charger_type & AC_CHG) { |
|
/* |
|
* Select maximum of what both the charger |
|
* and the battery supports |
|
*/ |
|
if (di->ac_chg->max_out_curr) |
|
iset = min(iset, di->ac_chg->max_out_curr); |
|
|
|
di->chg_info.ac_iset = iset; |
|
|
|
return di->ac_chg->ops.update_curr(di->ac_chg, iset); |
|
} else if (di->usb_chg && di->usb_chg->ops.update_curr && |
|
di->chg_info.charger_type & USB_CHG) { |
|
/* |
|
* Select maximum of what both the charger |
|
* and the battery supports |
|
*/ |
|
if (di->usb_chg->max_out_curr) |
|
iset = min(iset, di->usb_chg->max_out_curr); |
|
|
|
di->chg_info.usb_iset = iset; |
|
|
|
return di->usb_chg->ops.update_curr(di->usb_chg, iset); |
|
} |
|
|
|
return -ENXIO; |
|
} |
|
|
|
/** |
|
* abx500_chargalg_stop_charging() - Stop charging |
|
* @di: pointer to the abx500_chargalg structure |
|
* |
|
* This function is called from any state where charging should be stopped. |
|
* All charging is disabled and all status parameters and timers are changed |
|
* accordingly |
|
*/ |
|
static void abx500_chargalg_stop_charging(struct abx500_chargalg *di) |
|
{ |
|
abx500_chargalg_ac_en(di, false, 0, 0); |
|
abx500_chargalg_usb_en(di, false, 0, 0); |
|
abx500_chargalg_stop_safety_timer(di); |
|
abx500_chargalg_stop_maintenance_timer(di); |
|
di->charge_status = POWER_SUPPLY_STATUS_NOT_CHARGING; |
|
di->maintenance_chg = false; |
|
cancel_delayed_work(&di->chargalg_wd_work); |
|
power_supply_changed(di->chargalg_psy); |
|
} |
|
|
|
/** |
|
* abx500_chargalg_hold_charging() - Pauses charging |
|
* @di: pointer to the abx500_chargalg structure |
|
* |
|
* This function is called in the case where maintenance charging has been |
|
* disabled and instead a battery voltage mode is entered to check when the |
|
* battery voltage has reached a certain recharge voltage |
|
*/ |
|
static void abx500_chargalg_hold_charging(struct abx500_chargalg *di) |
|
{ |
|
abx500_chargalg_ac_en(di, false, 0, 0); |
|
abx500_chargalg_usb_en(di, false, 0, 0); |
|
abx500_chargalg_stop_safety_timer(di); |
|
abx500_chargalg_stop_maintenance_timer(di); |
|
di->charge_status = POWER_SUPPLY_STATUS_CHARGING; |
|
di->maintenance_chg = false; |
|
cancel_delayed_work(&di->chargalg_wd_work); |
|
power_supply_changed(di->chargalg_psy); |
|
} |
|
|
|
/** |
|
* abx500_chargalg_start_charging() - Start the charger |
|
* @di: pointer to the abx500_chargalg structure |
|
* @vset: requested charger output voltage |
|
* @iset: requested charger output current |
|
* |
|
* A charger will be enabled depending on the requested charger type that was |
|
* detected previously. |
|
*/ |
|
static void abx500_chargalg_start_charging(struct abx500_chargalg *di, |
|
int vset, int iset) |
|
{ |
|
switch (di->chg_info.charger_type) { |
|
case AC_CHG: |
|
dev_dbg(di->dev, |
|
"AC parameters: Vset %d, Ich %d\n", vset, iset); |
|
abx500_chargalg_usb_en(di, false, 0, 0); |
|
abx500_chargalg_ac_en(di, true, vset, iset); |
|
break; |
|
|
|
case USB_CHG: |
|
dev_dbg(di->dev, |
|
"USB parameters: Vset %d, Ich %d\n", vset, iset); |
|
abx500_chargalg_ac_en(di, false, 0, 0); |
|
abx500_chargalg_usb_en(di, true, vset, iset); |
|
break; |
|
|
|
default: |
|
dev_err(di->dev, "Unknown charger to charge from\n"); |
|
break; |
|
} |
|
} |
|
|
|
/** |
|
* abx500_chargalg_check_temp() - Check battery temperature ranges |
|
* @di: pointer to the abx500_chargalg structure |
|
* |
|
* The battery temperature is checked against the predefined limits and the |
|
* charge state is changed accordingly |
|
*/ |
|
static void abx500_chargalg_check_temp(struct abx500_chargalg *di) |
|
{ |
|
if (di->batt_data.temp > (di->bm->temp_low + di->t_hyst_norm) && |
|
di->batt_data.temp < (di->bm->temp_high - di->t_hyst_norm)) { |
|
/* Temp OK! */ |
|
di->events.btemp_underover = false; |
|
di->events.btemp_lowhigh = false; |
|
di->t_hyst_norm = 0; |
|
di->t_hyst_lowhigh = 0; |
|
} else { |
|
if (((di->batt_data.temp >= di->bm->temp_high) && |
|
(di->batt_data.temp < |
|
(di->bm->temp_over - di->t_hyst_lowhigh))) || |
|
((di->batt_data.temp > |
|
(di->bm->temp_under + di->t_hyst_lowhigh)) && |
|
(di->batt_data.temp <= di->bm->temp_low))) { |
|
/* TEMP minor!!!!! */ |
|
di->events.btemp_underover = false; |
|
di->events.btemp_lowhigh = true; |
|
di->t_hyst_norm = di->bm->temp_hysteresis; |
|
di->t_hyst_lowhigh = 0; |
|
} else if (di->batt_data.temp <= di->bm->temp_under || |
|
di->batt_data.temp >= di->bm->temp_over) { |
|
/* TEMP major!!!!! */ |
|
di->events.btemp_underover = true; |
|
di->events.btemp_lowhigh = false; |
|
di->t_hyst_norm = 0; |
|
di->t_hyst_lowhigh = di->bm->temp_hysteresis; |
|
} else { |
|
/* Within hysteresis */ |
|
dev_dbg(di->dev, "Within hysteresis limit temp: %d " |
|
"hyst_lowhigh %d, hyst normal %d\n", |
|
di->batt_data.temp, di->t_hyst_lowhigh, |
|
di->t_hyst_norm); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* abx500_chargalg_check_charger_voltage() - Check charger voltage |
|
* @di: pointer to the abx500_chargalg structure |
|
* |
|
* Charger voltage is checked against maximum limit |
|
*/ |
|
static void abx500_chargalg_check_charger_voltage(struct abx500_chargalg *di) |
|
{ |
|
if (di->chg_info.usb_volt > di->bm->chg_params->usb_volt_max) |
|
di->chg_info.usb_chg_ok = false; |
|
else |
|
di->chg_info.usb_chg_ok = true; |
|
|
|
if (di->chg_info.ac_volt > di->bm->chg_params->ac_volt_max) |
|
di->chg_info.ac_chg_ok = false; |
|
else |
|
di->chg_info.ac_chg_ok = true; |
|
|
|
} |
|
|
|
/** |
|
* abx500_chargalg_end_of_charge() - Check if end-of-charge criteria is fulfilled |
|
* @di: pointer to the abx500_chargalg structure |
|
* |
|
* End-of-charge criteria is fulfilled when the battery voltage is above a |
|
* certain limit and the battery current is below a certain limit for a |
|
* predefined number of consecutive seconds. If true, the battery is full |
|
*/ |
|
static void abx500_chargalg_end_of_charge(struct abx500_chargalg *di) |
|
{ |
|
if (di->charge_status == POWER_SUPPLY_STATUS_CHARGING && |
|
di->charge_state == STATE_NORMAL && |
|
!di->maintenance_chg && (di->batt_data.volt >= |
|
di->bm->bat_type[di->bm->batt_id].termination_vol || |
|
di->events.usb_cv_active || di->events.ac_cv_active) && |
|
di->batt_data.avg_curr < |
|
di->bm->bat_type[di->bm->batt_id].termination_curr && |
|
di->batt_data.avg_curr > 0) { |
|
if (++di->eoc_cnt >= EOC_COND_CNT) { |
|
di->eoc_cnt = 0; |
|
di->charge_status = POWER_SUPPLY_STATUS_FULL; |
|
di->maintenance_chg = true; |
|
dev_dbg(di->dev, "EOC reached!\n"); |
|
power_supply_changed(di->chargalg_psy); |
|
} else { |
|
dev_dbg(di->dev, |
|
" EOC limit reached for the %d" |
|
" time, out of %d before EOC\n", |
|
di->eoc_cnt, |
|
EOC_COND_CNT); |
|
} |
|
} else { |
|
di->eoc_cnt = 0; |
|
} |
|
} |
|
|
|
static void init_maxim_chg_curr(struct abx500_chargalg *di) |
|
{ |
|
di->ccm.original_iset = |
|
di->bm->bat_type[di->bm->batt_id].normal_cur_lvl; |
|
di->ccm.current_iset = |
|
di->bm->bat_type[di->bm->batt_id].normal_cur_lvl; |
|
di->ccm.test_delta_i = di->bm->maxi->charger_curr_step; |
|
di->ccm.max_current = di->bm->maxi->chg_curr; |
|
di->ccm.condition_cnt = di->bm->maxi->wait_cycles; |
|
di->ccm.level = 0; |
|
} |
|
|
|
/** |
|
* abx500_chargalg_chg_curr_maxim - increases the charger current to |
|
* compensate for the system load |
|
* @di pointer to the abx500_chargalg structure |
|
* |
|
* This maximization function is used to raise the charger current to get the |
|
* battery current as close to the optimal value as possible. The battery |
|
* current during charging is affected by the system load |
|
*/ |
|
static enum maxim_ret abx500_chargalg_chg_curr_maxim(struct abx500_chargalg *di) |
|
{ |
|
int delta_i; |
|
|
|
if (!di->bm->maxi->ena_maxi) |
|
return MAXIM_RET_NOACTION; |
|
|
|
delta_i = di->ccm.original_iset - di->batt_data.inst_curr; |
|
|
|
if (di->events.vbus_collapsed) { |
|
dev_dbg(di->dev, "Charger voltage has collapsed %d\n", |
|
di->ccm.wait_cnt); |
|
if (di->ccm.wait_cnt == 0) { |
|
dev_dbg(di->dev, "lowering current\n"); |
|
di->ccm.wait_cnt++; |
|
di->ccm.condition_cnt = di->bm->maxi->wait_cycles; |
|
di->ccm.max_current = |
|
di->ccm.current_iset - di->ccm.test_delta_i; |
|
di->ccm.current_iset = di->ccm.max_current; |
|
di->ccm.level--; |
|
return MAXIM_RET_CHANGE; |
|
} else { |
|
dev_dbg(di->dev, "waiting\n"); |
|
/* Let's go in here twice before lowering curr again */ |
|
di->ccm.wait_cnt = (di->ccm.wait_cnt + 1) % 3; |
|
return MAXIM_RET_NOACTION; |
|
} |
|
} |
|
|
|
di->ccm.wait_cnt = 0; |
|
|
|
if ((di->batt_data.inst_curr > di->ccm.original_iset)) { |
|
dev_dbg(di->dev, " Maximization Ibat (%dmA) too high" |
|
" (limit %dmA) (current iset: %dmA)!\n", |
|
di->batt_data.inst_curr, di->ccm.original_iset, |
|
di->ccm.current_iset); |
|
|
|
if (di->ccm.current_iset == di->ccm.original_iset) |
|
return MAXIM_RET_NOACTION; |
|
|
|
di->ccm.condition_cnt = di->bm->maxi->wait_cycles; |
|
di->ccm.current_iset = di->ccm.original_iset; |
|
di->ccm.level = 0; |
|
|
|
return MAXIM_RET_IBAT_TOO_HIGH; |
|
} |
|
|
|
if (delta_i > di->ccm.test_delta_i && |
|
(di->ccm.current_iset + di->ccm.test_delta_i) < |
|
di->ccm.max_current) { |
|
if (di->ccm.condition_cnt-- == 0) { |
|
/* Increse the iset with cco.test_delta_i */ |
|
di->ccm.condition_cnt = di->bm->maxi->wait_cycles; |
|
di->ccm.current_iset += di->ccm.test_delta_i; |
|
di->ccm.level++; |
|
dev_dbg(di->dev, " Maximization needed, increase" |
|
" with %d mA to %dmA (Optimal ibat: %d)" |
|
" Level %d\n", |
|
di->ccm.test_delta_i, |
|
di->ccm.current_iset, |
|
di->ccm.original_iset, |
|
di->ccm.level); |
|
return MAXIM_RET_CHANGE; |
|
} else { |
|
return MAXIM_RET_NOACTION; |
|
} |
|
} else { |
|
di->ccm.condition_cnt = di->bm->maxi->wait_cycles; |
|
return MAXIM_RET_NOACTION; |
|
} |
|
} |
|
|
|
static void handle_maxim_chg_curr(struct abx500_chargalg *di) |
|
{ |
|
enum maxim_ret ret; |
|
int result; |
|
|
|
ret = abx500_chargalg_chg_curr_maxim(di); |
|
switch (ret) { |
|
case MAXIM_RET_CHANGE: |
|
result = abx500_chargalg_update_chg_curr(di, |
|
di->ccm.current_iset); |
|
if (result) |
|
dev_err(di->dev, "failed to set chg curr\n"); |
|
break; |
|
case MAXIM_RET_IBAT_TOO_HIGH: |
|
result = abx500_chargalg_update_chg_curr(di, |
|
di->bm->bat_type[di->bm->batt_id].normal_cur_lvl); |
|
if (result) |
|
dev_err(di->dev, "failed to set chg curr\n"); |
|
break; |
|
|
|
case MAXIM_RET_NOACTION: |
|
default: |
|
/* Do nothing..*/ |
|
break; |
|
} |
|
} |
|
|
|
static int abx500_chargalg_get_ext_psy_data(struct device *dev, void *data) |
|
{ |
|
struct power_supply *psy; |
|
struct power_supply *ext = dev_get_drvdata(dev); |
|
const char **supplicants = (const char **)ext->supplied_to; |
|
struct abx500_chargalg *di; |
|
union power_supply_propval ret; |
|
int j; |
|
bool capacity_updated = false; |
|
|
|
psy = (struct power_supply *)data; |
|
di = power_supply_get_drvdata(psy); |
|
/* For all psy where the driver name appears in any supplied_to */ |
|
j = match_string(supplicants, ext->num_supplicants, psy->desc->name); |
|
if (j < 0) |
|
return 0; |
|
|
|
/* |
|
* If external is not registering 'POWER_SUPPLY_PROP_CAPACITY' to its |
|
* property because of handling that sysfs entry on its own, this is |
|
* the place to get the battery capacity. |
|
*/ |
|
if (!power_supply_get_property(ext, POWER_SUPPLY_PROP_CAPACITY, &ret)) { |
|
di->batt_data.percent = ret.intval; |
|
capacity_updated = true; |
|
} |
|
|
|
/* Go through all properties for the psy */ |
|
for (j = 0; j < ext->desc->num_properties; j++) { |
|
enum power_supply_property prop; |
|
prop = ext->desc->properties[j]; |
|
|
|
/* |
|
* Initialize chargers if not already done. |
|
* The ab8500_charger*/ |
|
if (!di->ac_chg && |
|
ext->desc->type == POWER_SUPPLY_TYPE_MAINS) |
|
di->ac_chg = psy_to_ux500_charger(ext); |
|
else if (!di->usb_chg && |
|
ext->desc->type == POWER_SUPPLY_TYPE_USB) |
|
di->usb_chg = psy_to_ux500_charger(ext); |
|
|
|
if (power_supply_get_property(ext, prop, &ret)) |
|
continue; |
|
switch (prop) { |
|
case POWER_SUPPLY_PROP_PRESENT: |
|
switch (ext->desc->type) { |
|
case POWER_SUPPLY_TYPE_BATTERY: |
|
/* Battery present */ |
|
if (ret.intval) |
|
di->events.batt_rem = false; |
|
/* Battery removed */ |
|
else |
|
di->events.batt_rem = true; |
|
break; |
|
case POWER_SUPPLY_TYPE_MAINS: |
|
/* AC disconnected */ |
|
if (!ret.intval && |
|
(di->chg_info.conn_chg & AC_CHG)) { |
|
di->chg_info.prev_conn_chg = |
|
di->chg_info.conn_chg; |
|
di->chg_info.conn_chg &= ~AC_CHG; |
|
} |
|
/* AC connected */ |
|
else if (ret.intval && |
|
!(di->chg_info.conn_chg & AC_CHG)) { |
|
di->chg_info.prev_conn_chg = |
|
di->chg_info.conn_chg; |
|
di->chg_info.conn_chg |= AC_CHG; |
|
} |
|
break; |
|
case POWER_SUPPLY_TYPE_USB: |
|
/* USB disconnected */ |
|
if (!ret.intval && |
|
(di->chg_info.conn_chg & USB_CHG)) { |
|
di->chg_info.prev_conn_chg = |
|
di->chg_info.conn_chg; |
|
di->chg_info.conn_chg &= ~USB_CHG; |
|
} |
|
/* USB connected */ |
|
else if (ret.intval && |
|
!(di->chg_info.conn_chg & USB_CHG)) { |
|
di->chg_info.prev_conn_chg = |
|
di->chg_info.conn_chg; |
|
di->chg_info.conn_chg |= USB_CHG; |
|
} |
|
break; |
|
default: |
|
break; |
|
} |
|
break; |
|
|
|
case POWER_SUPPLY_PROP_ONLINE: |
|
switch (ext->desc->type) { |
|
case POWER_SUPPLY_TYPE_BATTERY: |
|
break; |
|
case POWER_SUPPLY_TYPE_MAINS: |
|
/* AC offline */ |
|
if (!ret.intval && |
|
(di->chg_info.online_chg & AC_CHG)) { |
|
di->chg_info.prev_online_chg = |
|
di->chg_info.online_chg; |
|
di->chg_info.online_chg &= ~AC_CHG; |
|
} |
|
/* AC online */ |
|
else if (ret.intval && |
|
!(di->chg_info.online_chg & AC_CHG)) { |
|
di->chg_info.prev_online_chg = |
|
di->chg_info.online_chg; |
|
di->chg_info.online_chg |= AC_CHG; |
|
queue_delayed_work(di->chargalg_wq, |
|
&di->chargalg_wd_work, 0); |
|
} |
|
break; |
|
case POWER_SUPPLY_TYPE_USB: |
|
/* USB offline */ |
|
if (!ret.intval && |
|
(di->chg_info.online_chg & USB_CHG)) { |
|
di->chg_info.prev_online_chg = |
|
di->chg_info.online_chg; |
|
di->chg_info.online_chg &= ~USB_CHG; |
|
} |
|
/* USB online */ |
|
else if (ret.intval && |
|
!(di->chg_info.online_chg & USB_CHG)) { |
|
di->chg_info.prev_online_chg = |
|
di->chg_info.online_chg; |
|
di->chg_info.online_chg |= USB_CHG; |
|
queue_delayed_work(di->chargalg_wq, |
|
&di->chargalg_wd_work, 0); |
|
} |
|
break; |
|
default: |
|
break; |
|
} |
|
break; |
|
|
|
case POWER_SUPPLY_PROP_HEALTH: |
|
switch (ext->desc->type) { |
|
case POWER_SUPPLY_TYPE_BATTERY: |
|
break; |
|
case POWER_SUPPLY_TYPE_MAINS: |
|
switch (ret.intval) { |
|
case POWER_SUPPLY_HEALTH_UNSPEC_FAILURE: |
|
di->events.mainextchnotok = true; |
|
di->events.main_thermal_prot = false; |
|
di->events.main_ovv = false; |
|
di->events.ac_wd_expired = false; |
|
break; |
|
case POWER_SUPPLY_HEALTH_DEAD: |
|
di->events.ac_wd_expired = true; |
|
di->events.mainextchnotok = false; |
|
di->events.main_ovv = false; |
|
di->events.main_thermal_prot = false; |
|
break; |
|
case POWER_SUPPLY_HEALTH_COLD: |
|
case POWER_SUPPLY_HEALTH_OVERHEAT: |
|
di->events.main_thermal_prot = true; |
|
di->events.mainextchnotok = false; |
|
di->events.main_ovv = false; |
|
di->events.ac_wd_expired = false; |
|
break; |
|
case POWER_SUPPLY_HEALTH_OVERVOLTAGE: |
|
di->events.main_ovv = true; |
|
di->events.mainextchnotok = false; |
|
di->events.main_thermal_prot = false; |
|
di->events.ac_wd_expired = false; |
|
break; |
|
case POWER_SUPPLY_HEALTH_GOOD: |
|
di->events.main_thermal_prot = false; |
|
di->events.mainextchnotok = false; |
|
di->events.main_ovv = false; |
|
di->events.ac_wd_expired = false; |
|
break; |
|
default: |
|
break; |
|
} |
|
break; |
|
|
|
case POWER_SUPPLY_TYPE_USB: |
|
switch (ret.intval) { |
|
case POWER_SUPPLY_HEALTH_UNSPEC_FAILURE: |
|
di->events.usbchargernotok = true; |
|
di->events.usb_thermal_prot = false; |
|
di->events.vbus_ovv = false; |
|
di->events.usb_wd_expired = false; |
|
break; |
|
case POWER_SUPPLY_HEALTH_DEAD: |
|
di->events.usb_wd_expired = true; |
|
di->events.usbchargernotok = false; |
|
di->events.usb_thermal_prot = false; |
|
di->events.vbus_ovv = false; |
|
break; |
|
case POWER_SUPPLY_HEALTH_COLD: |
|
case POWER_SUPPLY_HEALTH_OVERHEAT: |
|
di->events.usb_thermal_prot = true; |
|
di->events.usbchargernotok = false; |
|
di->events.vbus_ovv = false; |
|
di->events.usb_wd_expired = false; |
|
break; |
|
case POWER_SUPPLY_HEALTH_OVERVOLTAGE: |
|
di->events.vbus_ovv = true; |
|
di->events.usbchargernotok = false; |
|
di->events.usb_thermal_prot = false; |
|
di->events.usb_wd_expired = false; |
|
break; |
|
case POWER_SUPPLY_HEALTH_GOOD: |
|
di->events.usbchargernotok = false; |
|
di->events.usb_thermal_prot = false; |
|
di->events.vbus_ovv = false; |
|
di->events.usb_wd_expired = false; |
|
break; |
|
default: |
|
break; |
|
} |
|
default: |
|
break; |
|
} |
|
break; |
|
|
|
case POWER_SUPPLY_PROP_VOLTAGE_NOW: |
|
switch (ext->desc->type) { |
|
case POWER_SUPPLY_TYPE_BATTERY: |
|
di->batt_data.volt = ret.intval / 1000; |
|
break; |
|
case POWER_SUPPLY_TYPE_MAINS: |
|
di->chg_info.ac_volt = ret.intval / 1000; |
|
break; |
|
case POWER_SUPPLY_TYPE_USB: |
|
di->chg_info.usb_volt = ret.intval / 1000; |
|
break; |
|
default: |
|
break; |
|
} |
|
break; |
|
|
|
case POWER_SUPPLY_PROP_VOLTAGE_AVG: |
|
switch (ext->desc->type) { |
|
case POWER_SUPPLY_TYPE_MAINS: |
|
/* AVG is used to indicate when we are |
|
* in CV mode */ |
|
if (ret.intval) |
|
di->events.ac_cv_active = true; |
|
else |
|
di->events.ac_cv_active = false; |
|
|
|
break; |
|
case POWER_SUPPLY_TYPE_USB: |
|
/* AVG is used to indicate when we are |
|
* in CV mode */ |
|
if (ret.intval) |
|
di->events.usb_cv_active = true; |
|
else |
|
di->events.usb_cv_active = false; |
|
|
|
break; |
|
default: |
|
break; |
|
} |
|
break; |
|
|
|
case POWER_SUPPLY_PROP_TECHNOLOGY: |
|
switch (ext->desc->type) { |
|
case POWER_SUPPLY_TYPE_BATTERY: |
|
if (ret.intval) |
|
di->events.batt_unknown = false; |
|
else |
|
di->events.batt_unknown = true; |
|
|
|
break; |
|
default: |
|
break; |
|
} |
|
break; |
|
|
|
case POWER_SUPPLY_PROP_TEMP: |
|
di->batt_data.temp = ret.intval / 10; |
|
break; |
|
|
|
case POWER_SUPPLY_PROP_CURRENT_NOW: |
|
switch (ext->desc->type) { |
|
case POWER_SUPPLY_TYPE_MAINS: |
|
di->chg_info.ac_curr = |
|
ret.intval / 1000; |
|
break; |
|
case POWER_SUPPLY_TYPE_USB: |
|
di->chg_info.usb_curr = |
|
ret.intval / 1000; |
|
break; |
|
case POWER_SUPPLY_TYPE_BATTERY: |
|
di->batt_data.inst_curr = ret.intval / 1000; |
|
break; |
|
default: |
|
break; |
|
} |
|
break; |
|
|
|
case POWER_SUPPLY_PROP_CURRENT_AVG: |
|
switch (ext->desc->type) { |
|
case POWER_SUPPLY_TYPE_BATTERY: |
|
di->batt_data.avg_curr = ret.intval / 1000; |
|
break; |
|
case POWER_SUPPLY_TYPE_USB: |
|
if (ret.intval) |
|
di->events.vbus_collapsed = true; |
|
else |
|
di->events.vbus_collapsed = false; |
|
break; |
|
default: |
|
break; |
|
} |
|
break; |
|
case POWER_SUPPLY_PROP_CAPACITY: |
|
if (!capacity_updated) |
|
di->batt_data.percent = ret.intval; |
|
break; |
|
default: |
|
break; |
|
} |
|
} |
|
return 0; |
|
} |
|
|
|
/** |
|
* abx500_chargalg_external_power_changed() - callback for power supply changes |
|
* @psy: pointer to the structure power_supply |
|
* |
|
* This function is the entry point of the pointer external_power_changed |
|
* of the structure power_supply. |
|
* This function gets executed when there is a change in any external power |
|
* supply that this driver needs to be notified of. |
|
*/ |
|
static void abx500_chargalg_external_power_changed(struct power_supply *psy) |
|
{ |
|
struct abx500_chargalg *di = power_supply_get_drvdata(psy); |
|
|
|
/* |
|
* Trigger execution of the algorithm instantly and read |
|
* all power_supply properties there instead |
|
*/ |
|
queue_work(di->chargalg_wq, &di->chargalg_work); |
|
} |
|
|
|
/** |
|
* abx500_chargalg_algorithm() - Main function for the algorithm |
|
* @di: pointer to the abx500_chargalg structure |
|
* |
|
* This is the main control function for the charging algorithm. |
|
* It is called periodically or when something happens that will |
|
* trigger a state change |
|
*/ |
|
static void abx500_chargalg_algorithm(struct abx500_chargalg *di) |
|
{ |
|
int charger_status; |
|
int ret; |
|
int curr_step_lvl; |
|
|
|
/* Collect data from all power_supply class devices */ |
|
class_for_each_device(power_supply_class, NULL, |
|
di->chargalg_psy, abx500_chargalg_get_ext_psy_data); |
|
|
|
abx500_chargalg_end_of_charge(di); |
|
abx500_chargalg_check_temp(di); |
|
abx500_chargalg_check_charger_voltage(di); |
|
|
|
charger_status = abx500_chargalg_check_charger_connection(di); |
|
abx500_chargalg_check_current_step_status(di); |
|
|
|
if (is_ab8500(di->parent)) { |
|
ret = abx500_chargalg_check_charger_enable(di); |
|
if (ret < 0) |
|
dev_err(di->dev, "Checking charger is enabled error" |
|
": Returned Value %d\n", ret); |
|
} |
|
|
|
/* |
|
* First check if we have a charger connected. |
|
* Also we don't allow charging of unknown batteries if configured |
|
* this way |
|
*/ |
|
if (!charger_status || |
|
(di->events.batt_unknown && !di->bm->chg_unknown_bat)) { |
|
if (di->charge_state != STATE_HANDHELD) { |
|
di->events.safety_timer_expired = false; |
|
abx500_chargalg_state_to(di, STATE_HANDHELD_INIT); |
|
} |
|
} |
|
|
|
/* If suspended, we should not continue checking the flags */ |
|
else if (di->charge_state == STATE_SUSPENDED_INIT || |
|
di->charge_state == STATE_SUSPENDED) { |
|
/* We don't do anything here, just don,t continue */ |
|
} |
|
|
|
/* Safety timer expiration */ |
|
else if (di->events.safety_timer_expired) { |
|
if (di->charge_state != STATE_SAFETY_TIMER_EXPIRED) |
|
abx500_chargalg_state_to(di, |
|
STATE_SAFETY_TIMER_EXPIRED_INIT); |
|
} |
|
/* |
|
* Check if any interrupts has occured |
|
* that will prevent us from charging |
|
*/ |
|
|
|
/* Battery removed */ |
|
else if (di->events.batt_rem) { |
|
if (di->charge_state != STATE_BATT_REMOVED) |
|
abx500_chargalg_state_to(di, STATE_BATT_REMOVED_INIT); |
|
} |
|
/* Main or USB charger not ok. */ |
|
else if (di->events.mainextchnotok || di->events.usbchargernotok) { |
|
/* |
|
* If vbus_collapsed is set, we have to lower the charger |
|
* current, which is done in the normal state below |
|
*/ |
|
if (di->charge_state != STATE_CHG_NOT_OK && |
|
!di->events.vbus_collapsed) |
|
abx500_chargalg_state_to(di, STATE_CHG_NOT_OK_INIT); |
|
} |
|
/* VBUS, Main or VBAT OVV. */ |
|
else if (di->events.vbus_ovv || |
|
di->events.main_ovv || |
|
di->events.batt_ovv || |
|
!di->chg_info.usb_chg_ok || |
|
!di->chg_info.ac_chg_ok) { |
|
if (di->charge_state != STATE_OVV_PROTECT) |
|
abx500_chargalg_state_to(di, STATE_OVV_PROTECT_INIT); |
|
} |
|
/* USB Thermal, stop charging */ |
|
else if (di->events.main_thermal_prot || |
|
di->events.usb_thermal_prot) { |
|
if (di->charge_state != STATE_HW_TEMP_PROTECT) |
|
abx500_chargalg_state_to(di, |
|
STATE_HW_TEMP_PROTECT_INIT); |
|
} |
|
/* Battery temp over/under */ |
|
else if (di->events.btemp_underover) { |
|
if (di->charge_state != STATE_TEMP_UNDEROVER) |
|
abx500_chargalg_state_to(di, |
|
STATE_TEMP_UNDEROVER_INIT); |
|
} |
|
/* Watchdog expired */ |
|
else if (di->events.ac_wd_expired || |
|
di->events.usb_wd_expired) { |
|
if (di->charge_state != STATE_WD_EXPIRED) |
|
abx500_chargalg_state_to(di, STATE_WD_EXPIRED_INIT); |
|
} |
|
/* Battery temp high/low */ |
|
else if (di->events.btemp_lowhigh) { |
|
if (di->charge_state != STATE_TEMP_LOWHIGH) |
|
abx500_chargalg_state_to(di, STATE_TEMP_LOWHIGH_INIT); |
|
} |
|
|
|
dev_dbg(di->dev, |
|
"[CHARGALG] Vb %d Ib_avg %d Ib_inst %d Tb %d Cap %d Maint %d " |
|
"State %s Active_chg %d Chg_status %d AC %d USB %d " |
|
"AC_online %d USB_online %d AC_CV %d USB_CV %d AC_I %d " |
|
"USB_I %d AC_Vset %d AC_Iset %d USB_Vset %d USB_Iset %d\n", |
|
di->batt_data.volt, |
|
di->batt_data.avg_curr, |
|
di->batt_data.inst_curr, |
|
di->batt_data.temp, |
|
di->batt_data.percent, |
|
di->maintenance_chg, |
|
states[di->charge_state], |
|
di->chg_info.charger_type, |
|
di->charge_status, |
|
di->chg_info.conn_chg & AC_CHG, |
|
di->chg_info.conn_chg & USB_CHG, |
|
di->chg_info.online_chg & AC_CHG, |
|
di->chg_info.online_chg & USB_CHG, |
|
di->events.ac_cv_active, |
|
di->events.usb_cv_active, |
|
di->chg_info.ac_curr, |
|
di->chg_info.usb_curr, |
|
di->chg_info.ac_vset, |
|
di->chg_info.ac_iset, |
|
di->chg_info.usb_vset, |
|
di->chg_info.usb_iset); |
|
|
|
switch (di->charge_state) { |
|
case STATE_HANDHELD_INIT: |
|
abx500_chargalg_stop_charging(di); |
|
di->charge_status = POWER_SUPPLY_STATUS_DISCHARGING; |
|
abx500_chargalg_state_to(di, STATE_HANDHELD); |
|
fallthrough; |
|
|
|
case STATE_HANDHELD: |
|
break; |
|
|
|
case STATE_SUSPENDED_INIT: |
|
if (di->susp_status.ac_suspended) |
|
abx500_chargalg_ac_en(di, false, 0, 0); |
|
if (di->susp_status.usb_suspended) |
|
abx500_chargalg_usb_en(di, false, 0, 0); |
|
abx500_chargalg_stop_safety_timer(di); |
|
abx500_chargalg_stop_maintenance_timer(di); |
|
di->charge_status = POWER_SUPPLY_STATUS_NOT_CHARGING; |
|
di->maintenance_chg = false; |
|
abx500_chargalg_state_to(di, STATE_SUSPENDED); |
|
power_supply_changed(di->chargalg_psy); |
|
fallthrough; |
|
|
|
case STATE_SUSPENDED: |
|
/* CHARGING is suspended */ |
|
break; |
|
|
|
case STATE_BATT_REMOVED_INIT: |
|
abx500_chargalg_stop_charging(di); |
|
abx500_chargalg_state_to(di, STATE_BATT_REMOVED); |
|
fallthrough; |
|
|
|
case STATE_BATT_REMOVED: |
|
if (!di->events.batt_rem) |
|
abx500_chargalg_state_to(di, STATE_NORMAL_INIT); |
|
break; |
|
|
|
case STATE_HW_TEMP_PROTECT_INIT: |
|
abx500_chargalg_stop_charging(di); |
|
abx500_chargalg_state_to(di, STATE_HW_TEMP_PROTECT); |
|
fallthrough; |
|
|
|
case STATE_HW_TEMP_PROTECT: |
|
if (!di->events.main_thermal_prot && |
|
!di->events.usb_thermal_prot) |
|
abx500_chargalg_state_to(di, STATE_NORMAL_INIT); |
|
break; |
|
|
|
case STATE_OVV_PROTECT_INIT: |
|
abx500_chargalg_stop_charging(di); |
|
abx500_chargalg_state_to(di, STATE_OVV_PROTECT); |
|
fallthrough; |
|
|
|
case STATE_OVV_PROTECT: |
|
if (!di->events.vbus_ovv && |
|
!di->events.main_ovv && |
|
!di->events.batt_ovv && |
|
di->chg_info.usb_chg_ok && |
|
di->chg_info.ac_chg_ok) |
|
abx500_chargalg_state_to(di, STATE_NORMAL_INIT); |
|
break; |
|
|
|
case STATE_CHG_NOT_OK_INIT: |
|
abx500_chargalg_stop_charging(di); |
|
abx500_chargalg_state_to(di, STATE_CHG_NOT_OK); |
|
fallthrough; |
|
|
|
case STATE_CHG_NOT_OK: |
|
if (!di->events.mainextchnotok && |
|
!di->events.usbchargernotok) |
|
abx500_chargalg_state_to(di, STATE_NORMAL_INIT); |
|
break; |
|
|
|
case STATE_SAFETY_TIMER_EXPIRED_INIT: |
|
abx500_chargalg_stop_charging(di); |
|
abx500_chargalg_state_to(di, STATE_SAFETY_TIMER_EXPIRED); |
|
fallthrough; |
|
|
|
case STATE_SAFETY_TIMER_EXPIRED: |
|
/* We exit this state when charger is removed */ |
|
break; |
|
|
|
case STATE_NORMAL_INIT: |
|
if (di->curr_status.curr_step == CHARGALG_CURR_STEP_LOW) |
|
abx500_chargalg_stop_charging(di); |
|
else { |
|
curr_step_lvl = di->bm->bat_type[ |
|
di->bm->batt_id].normal_cur_lvl |
|
* di->curr_status.curr_step |
|
/ CHARGALG_CURR_STEP_HIGH; |
|
abx500_chargalg_start_charging(di, |
|
di->bm->bat_type[di->bm->batt_id] |
|
.normal_vol_lvl, curr_step_lvl); |
|
} |
|
|
|
abx500_chargalg_state_to(di, STATE_NORMAL); |
|
abx500_chargalg_start_safety_timer(di); |
|
abx500_chargalg_stop_maintenance_timer(di); |
|
init_maxim_chg_curr(di); |
|
di->charge_status = POWER_SUPPLY_STATUS_CHARGING; |
|
di->eoc_cnt = 0; |
|
di->maintenance_chg = false; |
|
power_supply_changed(di->chargalg_psy); |
|
|
|
break; |
|
|
|
case STATE_NORMAL: |
|
handle_maxim_chg_curr(di); |
|
if (di->charge_status == POWER_SUPPLY_STATUS_FULL && |
|
di->maintenance_chg) { |
|
if (di->bm->no_maintenance) |
|
abx500_chargalg_state_to(di, |
|
STATE_WAIT_FOR_RECHARGE_INIT); |
|
else |
|
abx500_chargalg_state_to(di, |
|
STATE_MAINTENANCE_A_INIT); |
|
} |
|
break; |
|
|
|
/* This state will be used when the maintenance state is disabled */ |
|
case STATE_WAIT_FOR_RECHARGE_INIT: |
|
abx500_chargalg_hold_charging(di); |
|
abx500_chargalg_state_to(di, STATE_WAIT_FOR_RECHARGE); |
|
fallthrough; |
|
|
|
case STATE_WAIT_FOR_RECHARGE: |
|
if (di->batt_data.percent <= |
|
di->bm->bat_type[di->bm->batt_id]. |
|
recharge_cap) |
|
abx500_chargalg_state_to(di, STATE_NORMAL_INIT); |
|
break; |
|
|
|
case STATE_MAINTENANCE_A_INIT: |
|
abx500_chargalg_stop_safety_timer(di); |
|
abx500_chargalg_start_maintenance_timer(di, |
|
di->bm->bat_type[ |
|
di->bm->batt_id].maint_a_chg_timer_h); |
|
abx500_chargalg_start_charging(di, |
|
di->bm->bat_type[ |
|
di->bm->batt_id].maint_a_vol_lvl, |
|
di->bm->bat_type[ |
|
di->bm->batt_id].maint_a_cur_lvl); |
|
abx500_chargalg_state_to(di, STATE_MAINTENANCE_A); |
|
power_supply_changed(di->chargalg_psy); |
|
fallthrough; |
|
|
|
case STATE_MAINTENANCE_A: |
|
if (di->events.maintenance_timer_expired) { |
|
abx500_chargalg_stop_maintenance_timer(di); |
|
abx500_chargalg_state_to(di, STATE_MAINTENANCE_B_INIT); |
|
} |
|
break; |
|
|
|
case STATE_MAINTENANCE_B_INIT: |
|
abx500_chargalg_start_maintenance_timer(di, |
|
di->bm->bat_type[ |
|
di->bm->batt_id].maint_b_chg_timer_h); |
|
abx500_chargalg_start_charging(di, |
|
di->bm->bat_type[ |
|
di->bm->batt_id].maint_b_vol_lvl, |
|
di->bm->bat_type[ |
|
di->bm->batt_id].maint_b_cur_lvl); |
|
abx500_chargalg_state_to(di, STATE_MAINTENANCE_B); |
|
power_supply_changed(di->chargalg_psy); |
|
fallthrough; |
|
|
|
case STATE_MAINTENANCE_B: |
|
if (di->events.maintenance_timer_expired) { |
|
abx500_chargalg_stop_maintenance_timer(di); |
|
abx500_chargalg_state_to(di, STATE_NORMAL_INIT); |
|
} |
|
break; |
|
|
|
case STATE_TEMP_LOWHIGH_INIT: |
|
abx500_chargalg_start_charging(di, |
|
di->bm->bat_type[ |
|
di->bm->batt_id].low_high_vol_lvl, |
|
di->bm->bat_type[ |
|
di->bm->batt_id].low_high_cur_lvl); |
|
abx500_chargalg_stop_maintenance_timer(di); |
|
di->charge_status = POWER_SUPPLY_STATUS_CHARGING; |
|
abx500_chargalg_state_to(di, STATE_TEMP_LOWHIGH); |
|
power_supply_changed(di->chargalg_psy); |
|
fallthrough; |
|
|
|
case STATE_TEMP_LOWHIGH: |
|
if (!di->events.btemp_lowhigh) |
|
abx500_chargalg_state_to(di, STATE_NORMAL_INIT); |
|
break; |
|
|
|
case STATE_WD_EXPIRED_INIT: |
|
abx500_chargalg_stop_charging(di); |
|
abx500_chargalg_state_to(di, STATE_WD_EXPIRED); |
|
fallthrough; |
|
|
|
case STATE_WD_EXPIRED: |
|
if (!di->events.ac_wd_expired && |
|
!di->events.usb_wd_expired) |
|
abx500_chargalg_state_to(di, STATE_NORMAL_INIT); |
|
break; |
|
|
|
case STATE_TEMP_UNDEROVER_INIT: |
|
abx500_chargalg_stop_charging(di); |
|
abx500_chargalg_state_to(di, STATE_TEMP_UNDEROVER); |
|
fallthrough; |
|
|
|
case STATE_TEMP_UNDEROVER: |
|
if (!di->events.btemp_underover) |
|
abx500_chargalg_state_to(di, STATE_NORMAL_INIT); |
|
break; |
|
} |
|
|
|
/* Start charging directly if the new state is a charge state */ |
|
if (di->charge_state == STATE_NORMAL_INIT || |
|
di->charge_state == STATE_MAINTENANCE_A_INIT || |
|
di->charge_state == STATE_MAINTENANCE_B_INIT) |
|
queue_work(di->chargalg_wq, &di->chargalg_work); |
|
} |
|
|
|
/** |
|
* abx500_chargalg_periodic_work() - Periodic work for the algorithm |
|
* @work: pointer to the work_struct structure |
|
* |
|
* Work queue function for the charging algorithm |
|
*/ |
|
static void abx500_chargalg_periodic_work(struct work_struct *work) |
|
{ |
|
struct abx500_chargalg *di = container_of(work, |
|
struct abx500_chargalg, chargalg_periodic_work.work); |
|
|
|
abx500_chargalg_algorithm(di); |
|
|
|
/* |
|
* If a charger is connected then the battery has to be monitored |
|
* frequently, else the work can be delayed. |
|
*/ |
|
if (di->chg_info.conn_chg) |
|
queue_delayed_work(di->chargalg_wq, |
|
&di->chargalg_periodic_work, |
|
di->bm->interval_charging * HZ); |
|
else |
|
queue_delayed_work(di->chargalg_wq, |
|
&di->chargalg_periodic_work, |
|
di->bm->interval_not_charging * HZ); |
|
} |
|
|
|
/** |
|
* abx500_chargalg_wd_work() - periodic work to kick the charger watchdog |
|
* @work: pointer to the work_struct structure |
|
* |
|
* Work queue function for kicking the charger watchdog |
|
*/ |
|
static void abx500_chargalg_wd_work(struct work_struct *work) |
|
{ |
|
int ret; |
|
struct abx500_chargalg *di = container_of(work, |
|
struct abx500_chargalg, chargalg_wd_work.work); |
|
|
|
dev_dbg(di->dev, "abx500_chargalg_wd_work\n"); |
|
|
|
ret = abx500_chargalg_kick_watchdog(di); |
|
if (ret < 0) |
|
dev_err(di->dev, "failed to kick watchdog\n"); |
|
|
|
queue_delayed_work(di->chargalg_wq, |
|
&di->chargalg_wd_work, CHG_WD_INTERVAL); |
|
} |
|
|
|
/** |
|
* abx500_chargalg_work() - Work to run the charging algorithm instantly |
|
* @work: pointer to the work_struct structure |
|
* |
|
* Work queue function for calling the charging algorithm |
|
*/ |
|
static void abx500_chargalg_work(struct work_struct *work) |
|
{ |
|
struct abx500_chargalg *di = container_of(work, |
|
struct abx500_chargalg, chargalg_work); |
|
|
|
abx500_chargalg_algorithm(di); |
|
} |
|
|
|
/** |
|
* abx500_chargalg_get_property() - get the chargalg properties |
|
* @psy: pointer to the power_supply structure |
|
* @psp: pointer to the power_supply_property structure |
|
* @val: pointer to the power_supply_propval union |
|
* |
|
* This function gets called when an application tries to get the |
|
* chargalg properties by reading the sysfs files. |
|
* status: charging/discharging/full/unknown |
|
* health: health of the battery |
|
* Returns error code in case of failure else 0 on success |
|
*/ |
|
static int abx500_chargalg_get_property(struct power_supply *psy, |
|
enum power_supply_property psp, |
|
union power_supply_propval *val) |
|
{ |
|
struct abx500_chargalg *di = power_supply_get_drvdata(psy); |
|
|
|
switch (psp) { |
|
case POWER_SUPPLY_PROP_STATUS: |
|
val->intval = di->charge_status; |
|
break; |
|
case POWER_SUPPLY_PROP_HEALTH: |
|
if (di->events.batt_ovv) { |
|
val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; |
|
} else if (di->events.btemp_underover) { |
|
if (di->batt_data.temp <= di->bm->temp_under) |
|
val->intval = POWER_SUPPLY_HEALTH_COLD; |
|
else |
|
val->intval = POWER_SUPPLY_HEALTH_OVERHEAT; |
|
} else if (di->charge_state == STATE_SAFETY_TIMER_EXPIRED || |
|
di->charge_state == STATE_SAFETY_TIMER_EXPIRED_INIT) { |
|
val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE; |
|
} else { |
|
val->intval = POWER_SUPPLY_HEALTH_GOOD; |
|
} |
|
break; |
|
default: |
|
return -EINVAL; |
|
} |
|
return 0; |
|
} |
|
|
|
/* Exposure to the sysfs interface */ |
|
|
|
static ssize_t abx500_chargalg_curr_step_show(struct abx500_chargalg *di, |
|
char *buf) |
|
{ |
|
return sprintf(buf, "%d\n", di->curr_status.curr_step); |
|
} |
|
|
|
static ssize_t abx500_chargalg_curr_step_store(struct abx500_chargalg *di, |
|
const char *buf, size_t length) |
|
{ |
|
long int param; |
|
int ret; |
|
|
|
ret = kstrtol(buf, 10, ¶m); |
|
if (ret < 0) |
|
return ret; |
|
|
|
di->curr_status.curr_step = param; |
|
if (di->curr_status.curr_step >= CHARGALG_CURR_STEP_LOW && |
|
di->curr_status.curr_step <= CHARGALG_CURR_STEP_HIGH) { |
|
di->curr_status.curr_step_change = true; |
|
queue_work(di->chargalg_wq, &di->chargalg_work); |
|
} else |
|
dev_info(di->dev, "Wrong current step\n" |
|
"Enter 0. Disable AC/USB Charging\n" |
|
"1--100. Set AC/USB charging current step\n" |
|
"100. Enable AC/USB Charging\n"); |
|
|
|
return strlen(buf); |
|
} |
|
|
|
|
|
static ssize_t abx500_chargalg_en_show(struct abx500_chargalg *di, |
|
char *buf) |
|
{ |
|
return sprintf(buf, "%d\n", |
|
di->susp_status.ac_suspended && |
|
di->susp_status.usb_suspended); |
|
} |
|
|
|
static ssize_t abx500_chargalg_en_store(struct abx500_chargalg *di, |
|
const char *buf, size_t length) |
|
{ |
|
long int param; |
|
int ac_usb; |
|
int ret; |
|
|
|
ret = kstrtol(buf, 10, ¶m); |
|
if (ret < 0) |
|
return ret; |
|
|
|
ac_usb = param; |
|
switch (ac_usb) { |
|
case 0: |
|
/* Disable charging */ |
|
di->susp_status.ac_suspended = true; |
|
di->susp_status.usb_suspended = true; |
|
di->susp_status.suspended_change = true; |
|
/* Trigger a state change */ |
|
queue_work(di->chargalg_wq, |
|
&di->chargalg_work); |
|
break; |
|
case 1: |
|
/* Enable AC Charging */ |
|
di->susp_status.ac_suspended = false; |
|
di->susp_status.suspended_change = true; |
|
/* Trigger a state change */ |
|
queue_work(di->chargalg_wq, |
|
&di->chargalg_work); |
|
break; |
|
case 2: |
|
/* Enable USB charging */ |
|
di->susp_status.usb_suspended = false; |
|
di->susp_status.suspended_change = true; |
|
/* Trigger a state change */ |
|
queue_work(di->chargalg_wq, |
|
&di->chargalg_work); |
|
break; |
|
default: |
|
dev_info(di->dev, "Wrong input\n" |
|
"Enter 0. Disable AC/USB Charging\n" |
|
"1. Enable AC charging\n" |
|
"2. Enable USB Charging\n"); |
|
} |
|
return strlen(buf); |
|
} |
|
|
|
static struct abx500_chargalg_sysfs_entry abx500_chargalg_en_charger = |
|
__ATTR(chargalg, 0644, abx500_chargalg_en_show, |
|
abx500_chargalg_en_store); |
|
|
|
static struct abx500_chargalg_sysfs_entry abx500_chargalg_curr_step = |
|
__ATTR(chargalg_curr_step, 0644, abx500_chargalg_curr_step_show, |
|
abx500_chargalg_curr_step_store); |
|
|
|
static ssize_t abx500_chargalg_sysfs_show(struct kobject *kobj, |
|
struct attribute *attr, char *buf) |
|
{ |
|
struct abx500_chargalg_sysfs_entry *entry = container_of(attr, |
|
struct abx500_chargalg_sysfs_entry, attr); |
|
|
|
struct abx500_chargalg *di = container_of(kobj, |
|
struct abx500_chargalg, chargalg_kobject); |
|
|
|
if (!entry->show) |
|
return -EIO; |
|
|
|
return entry->show(di, buf); |
|
} |
|
|
|
static ssize_t abx500_chargalg_sysfs_charger(struct kobject *kobj, |
|
struct attribute *attr, const char *buf, size_t length) |
|
{ |
|
struct abx500_chargalg_sysfs_entry *entry = container_of(attr, |
|
struct abx500_chargalg_sysfs_entry, attr); |
|
|
|
struct abx500_chargalg *di = container_of(kobj, |
|
struct abx500_chargalg, chargalg_kobject); |
|
|
|
if (!entry->store) |
|
return -EIO; |
|
|
|
return entry->store(di, buf, length); |
|
} |
|
|
|
static struct attribute *abx500_chargalg_chg[] = { |
|
&abx500_chargalg_en_charger.attr, |
|
&abx500_chargalg_curr_step.attr, |
|
NULL, |
|
}; |
|
|
|
static const struct sysfs_ops abx500_chargalg_sysfs_ops = { |
|
.show = abx500_chargalg_sysfs_show, |
|
.store = abx500_chargalg_sysfs_charger, |
|
}; |
|
|
|
static struct kobj_type abx500_chargalg_ktype = { |
|
.sysfs_ops = &abx500_chargalg_sysfs_ops, |
|
.default_attrs = abx500_chargalg_chg, |
|
}; |
|
|
|
/** |
|
* abx500_chargalg_sysfs_exit() - de-init of sysfs entry |
|
* @di: pointer to the struct abx500_chargalg |
|
* |
|
* This function removes the entry in sysfs. |
|
*/ |
|
static void abx500_chargalg_sysfs_exit(struct abx500_chargalg *di) |
|
{ |
|
kobject_del(&di->chargalg_kobject); |
|
} |
|
|
|
/** |
|
* abx500_chargalg_sysfs_init() - init of sysfs entry |
|
* @di: pointer to the struct abx500_chargalg |
|
* |
|
* This function adds an entry in sysfs. |
|
* Returns error code in case of failure else 0(on success) |
|
*/ |
|
static int abx500_chargalg_sysfs_init(struct abx500_chargalg *di) |
|
{ |
|
int ret = 0; |
|
|
|
ret = kobject_init_and_add(&di->chargalg_kobject, |
|
&abx500_chargalg_ktype, |
|
NULL, "abx500_chargalg"); |
|
if (ret < 0) |
|
dev_err(di->dev, "failed to create sysfs entry\n"); |
|
|
|
return ret; |
|
} |
|
/* Exposure to the sysfs interface <<END>> */ |
|
|
|
static int __maybe_unused abx500_chargalg_resume(struct device *dev) |
|
{ |
|
struct abx500_chargalg *di = dev_get_drvdata(dev); |
|
|
|
/* Kick charger watchdog if charging (any charger online) */ |
|
if (di->chg_info.online_chg) |
|
queue_delayed_work(di->chargalg_wq, &di->chargalg_wd_work, 0); |
|
|
|
/* |
|
* Run the charging algorithm directly to be sure we don't |
|
* do it too seldom |
|
*/ |
|
queue_delayed_work(di->chargalg_wq, &di->chargalg_periodic_work, 0); |
|
|
|
return 0; |
|
} |
|
|
|
static int __maybe_unused abx500_chargalg_suspend(struct device *dev) |
|
{ |
|
struct abx500_chargalg *di = dev_get_drvdata(dev); |
|
|
|
if (di->chg_info.online_chg) |
|
cancel_delayed_work_sync(&di->chargalg_wd_work); |
|
|
|
cancel_delayed_work_sync(&di->chargalg_periodic_work); |
|
|
|
return 0; |
|
} |
|
|
|
static int abx500_chargalg_remove(struct platform_device *pdev) |
|
{ |
|
struct abx500_chargalg *di = platform_get_drvdata(pdev); |
|
|
|
/* sysfs interface to enable/disbale charging from user space */ |
|
abx500_chargalg_sysfs_exit(di); |
|
|
|
hrtimer_cancel(&di->safety_timer); |
|
hrtimer_cancel(&di->maintenance_timer); |
|
|
|
cancel_delayed_work_sync(&di->chargalg_periodic_work); |
|
cancel_delayed_work_sync(&di->chargalg_wd_work); |
|
cancel_work_sync(&di->chargalg_work); |
|
|
|
/* Delete the work queue */ |
|
destroy_workqueue(di->chargalg_wq); |
|
|
|
power_supply_unregister(di->chargalg_psy); |
|
|
|
return 0; |
|
} |
|
|
|
static char *supply_interface[] = { |
|
"ab8500_fg", |
|
}; |
|
|
|
static const struct power_supply_desc abx500_chargalg_desc = { |
|
.name = "abx500_chargalg", |
|
.type = POWER_SUPPLY_TYPE_BATTERY, |
|
.properties = abx500_chargalg_props, |
|
.num_properties = ARRAY_SIZE(abx500_chargalg_props), |
|
.get_property = abx500_chargalg_get_property, |
|
.external_power_changed = abx500_chargalg_external_power_changed, |
|
}; |
|
|
|
static int abx500_chargalg_probe(struct platform_device *pdev) |
|
{ |
|
struct device_node *np = pdev->dev.of_node; |
|
struct abx500_bm_data *plat = pdev->dev.platform_data; |
|
struct power_supply_config psy_cfg = {}; |
|
struct abx500_chargalg *di; |
|
int ret = 0; |
|
|
|
di = devm_kzalloc(&pdev->dev, sizeof(*di), GFP_KERNEL); |
|
if (!di) { |
|
dev_err(&pdev->dev, "%s no mem for ab8500_chargalg\n", __func__); |
|
return -ENOMEM; |
|
} |
|
|
|
if (!plat) { |
|
dev_err(&pdev->dev, "no battery management data supplied\n"); |
|
return -EINVAL; |
|
} |
|
di->bm = plat; |
|
|
|
if (np) { |
|
ret = ab8500_bm_of_probe(&pdev->dev, np, di->bm); |
|
if (ret) { |
|
dev_err(&pdev->dev, "failed to get battery information\n"); |
|
return ret; |
|
} |
|
} |
|
|
|
/* get device struct and parent */ |
|
di->dev = &pdev->dev; |
|
di->parent = dev_get_drvdata(pdev->dev.parent); |
|
|
|
psy_cfg.supplied_to = supply_interface; |
|
psy_cfg.num_supplicants = ARRAY_SIZE(supply_interface); |
|
psy_cfg.drv_data = di; |
|
|
|
/* Initilialize safety timer */ |
|
hrtimer_init(&di->safety_timer, CLOCK_REALTIME, HRTIMER_MODE_ABS); |
|
di->safety_timer.function = abx500_chargalg_safety_timer_expired; |
|
|
|
/* Initilialize maintenance timer */ |
|
hrtimer_init(&di->maintenance_timer, CLOCK_REALTIME, HRTIMER_MODE_ABS); |
|
di->maintenance_timer.function = |
|
abx500_chargalg_maintenance_timer_expired; |
|
|
|
/* Create a work queue for the chargalg */ |
|
di->chargalg_wq = alloc_ordered_workqueue("abx500_chargalg_wq", |
|
WQ_MEM_RECLAIM); |
|
if (di->chargalg_wq == NULL) { |
|
dev_err(di->dev, "failed to create work queue\n"); |
|
return -ENOMEM; |
|
} |
|
|
|
/* Init work for chargalg */ |
|
INIT_DEFERRABLE_WORK(&di->chargalg_periodic_work, |
|
abx500_chargalg_periodic_work); |
|
INIT_DEFERRABLE_WORK(&di->chargalg_wd_work, |
|
abx500_chargalg_wd_work); |
|
|
|
/* Init work for chargalg */ |
|
INIT_WORK(&di->chargalg_work, abx500_chargalg_work); |
|
|
|
/* To detect charger at startup */ |
|
di->chg_info.prev_conn_chg = -1; |
|
|
|
/* Register chargalg power supply class */ |
|
di->chargalg_psy = power_supply_register(di->dev, &abx500_chargalg_desc, |
|
&psy_cfg); |
|
if (IS_ERR(di->chargalg_psy)) { |
|
dev_err(di->dev, "failed to register chargalg psy\n"); |
|
ret = PTR_ERR(di->chargalg_psy); |
|
goto free_chargalg_wq; |
|
} |
|
|
|
platform_set_drvdata(pdev, di); |
|
|
|
/* sysfs interface to enable/disable charging from user space */ |
|
ret = abx500_chargalg_sysfs_init(di); |
|
if (ret) { |
|
dev_err(di->dev, "failed to create sysfs entry\n"); |
|
goto free_psy; |
|
} |
|
di->curr_status.curr_step = CHARGALG_CURR_STEP_HIGH; |
|
|
|
/* Run the charging algorithm */ |
|
queue_delayed_work(di->chargalg_wq, &di->chargalg_periodic_work, 0); |
|
|
|
dev_info(di->dev, "probe success\n"); |
|
return ret; |
|
|
|
free_psy: |
|
power_supply_unregister(di->chargalg_psy); |
|
free_chargalg_wq: |
|
destroy_workqueue(di->chargalg_wq); |
|
return ret; |
|
} |
|
|
|
static SIMPLE_DEV_PM_OPS(abx500_chargalg_pm_ops, abx500_chargalg_suspend, abx500_chargalg_resume); |
|
|
|
static const struct of_device_id ab8500_chargalg_match[] = { |
|
{ .compatible = "stericsson,ab8500-chargalg", }, |
|
{ }, |
|
}; |
|
|
|
static struct platform_driver abx500_chargalg_driver = { |
|
.probe = abx500_chargalg_probe, |
|
.remove = abx500_chargalg_remove, |
|
.driver = { |
|
.name = "ab8500-chargalg", |
|
.of_match_table = ab8500_chargalg_match, |
|
.pm = &abx500_chargalg_pm_ops, |
|
}, |
|
}; |
|
|
|
module_platform_driver(abx500_chargalg_driver); |
|
|
|
MODULE_LICENSE("GPL v2"); |
|
MODULE_AUTHOR("Johan Palsson, Karl Komierowski"); |
|
MODULE_ALIAS("platform:abx500-chargalg"); |
|
MODULE_DESCRIPTION("abx500 battery charging algorithm");
|
|
|