| /* |
| * 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 GBMS_ICL_MIN 100000 // 100 mA |
| |
| 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 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; |
| 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] |
| |
| 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 = 0; |
| chg_drv->disable_pwrsrc = 0; |
| 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. |
| * If online is true, set ICL to a minimum threshold to leave the |
| * power supply online. |
| */ |
| static int chg_vote_input_suspend(struct chg_drv *chg_drv, char *voter, |
| bool suspend, bool online) |
| { |
| int rc; |
| int icl = 0; |
| |
| if (chg_find_votables(chg_drv) < 0) |
| return -EINVAL; |
| |
| if (online) |
| icl = GBMS_ICL_MIN; |
| |
| rc = vote(chg_drv->usb_icl_votable, voter, suspend, icl); |
| 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; |
| } |
| |
| #define CHG_WORK_ERROR_RETRY_MS 1000 |
| 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; |
| |
| __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) { |
| pr_info("no power source detected, disabling charging\n"); |
| reset_chg_drv_state(chg_drv); |
| 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; |
| } |
| |
| disable_charging = is_charging_disabled(chg_drv, soc); |
| if (disable_charging && soc > chg_drv->charge_stop_level) |
| disable_pwrsrc = 1; |
| else |
| disable_pwrsrc = 0; |
| |
| if (disable_charging != chg_drv->disable_charging) { |
| pr_info("set disable_charging(%d)", disable_charging); |
| PSY_SET_PROP(chg_psy, POWER_SUPPLY_PROP_CHARGE_DISABLE, |
| disable_charging); |
| } |
| chg_drv->disable_charging = disable_charging; |
| |
| if (disable_pwrsrc != chg_drv->disable_pwrsrc) { |
| pr_info("MSC_CHG disable_pwrsrc %d -> %d", |
| chg_drv->disable_pwrsrc, disable_pwrsrc); |
| |
| chg_vote_input_suspend(chg_drv, MSC_CHG_VOTER, disable_pwrsrc, |
| true); |
| } |
| chg_drv->disable_pwrsrc = disable_pwrsrc; |
| |
| /* 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); |
| |
| #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"); |
| #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); |
| } |
| #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"); |
| |
| /* 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; |
| } |
| |
| // 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->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"); |