| /* SPDX-License-Identifier: GPL-2.0 */ |
| /* |
| * Copyright 2020-2022 Google LLC |
| * |
| * 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 |
| |
| #ifdef CONFIG_PM_SLEEP |
| #define SUPPORT_PM_SLEEP 1 |
| #endif |
| |
| #include <linux/kernel.h> |
| #include <linux/printk.h> |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/of_gpio.h> |
| #include <linux/gpio.h> |
| #include <linux/pm_runtime.h> |
| #include <linux/platform_device.h> |
| #include <linux/thermal.h> |
| #include <linux/slab.h> |
| #include <misc/gvotable.h> |
| #include "gbms_power_supply.h" |
| #include "google_bms.h" |
| #include "google_dc_pps.h" |
| #include "google_psy.h" |
| |
| #include <linux/debugfs.h> |
| |
| #define get_boot_sec() div_u64(ktime_to_ns(ktime_get_boottime()), NSEC_PER_SEC) |
| |
| |
| /* Non DC Charger is the default */ |
| #define GCPM_DEFAULT_CHARGER 0 |
| /* TODO: handle capabilities based on index number */ |
| #define GCPM_INDEX_DC_DISABLE -1 |
| #define GCPM_INDEX_DC_ENABLE 1 |
| #define GCPM_MAX_CHARGERS 4 |
| |
| /* tier based, disabled now */ |
| #define GCPM_DEFAULT_DC_LIMIT_DEMAND 0 |
| /* thermal will change this */ |
| #define GCPM_DEFAULT_DC_LIMIT_CC_MIN 1000000 |
| #define GCPM_DEFAULT_DC_LIMIT_CC_MIN_WLC 2000000 |
| |
| /* voltage based */ |
| #define GCPM_DEFAULT_DC_LIMIT_VBATT_MIN 3600000 |
| #define GCPM_DEFAULT_DC_LIMIT_DELTA_LOW 200000 |
| |
| /* demand based limits */ |
| #define GCPM_DEFAULT_DC_LIMIT_VBATT_MAX 4450000 |
| #define GCPM_DEFAULT_DC_LIMIT_DELTA_HIGH 200000 |
| |
| /* SOC debounce */ |
| #define GCPM_DEFAULT_DC_LIMIT_SOC_HIGH 100 |
| |
| /* behavior in taper */ |
| #define GCPM_TAPER_STEP_FV_MARGIN 0 |
| #define GCPM_TAPER_STEP_CC_STEP 0 |
| #define GCPM_TAPER_STEP_COUNT 0 |
| #define GCPM_TAPER_STEP_GRACE 1 |
| #define GCPM_TAPER_STEP_VOLTAGE 0 |
| #define GCPM_TAPER_STEP_CURRENT 0 |
| /* enough time for the charger to settle to a new limit */ |
| #define GCPM_TAPER_STEP_INTERVAL_S 120 |
| |
| /* TODO: move to configuration */ |
| #define DC_TA_VMAX_MV 9800000 |
| /* TODO: move to configuration */ |
| #define DC_TA_VMIN_MV 8000000 |
| /* TODO: move to configuration */ |
| #define DC_VBATT_HEADROOM_MV 500000 |
| |
| enum gcpm_dc_state_t { |
| DC_DISABLED = -1, |
| DC_IDLE = 0, |
| DC_ENABLE, |
| DC_RUNNING, |
| DC_ENABLE_PASSTHROUGH, |
| DC_PASSTHROUGH, |
| }; |
| |
| /* DC_ERROR_RETRY_MS <= DC_RUN_DELAY_MS */ |
| #define DC_ENABLE_DELAY_MS 500 |
| #define DC_RUN_DELAY_MS 9000 |
| #define DC_ERROR_RETRY_MS PPS_ERROR_RETRY_MS |
| |
| #define PPS_PROG_TIMEOUT_S 10 |
| #define PPS_PROG_RETRY_MS 2000 |
| #define PPS_ACTIVE_RETRY_MS 1500 |
| #define PPS_ACTIVE_USB_TIMEOUT_S 25 |
| #define PPS_ACTIVE_WLC_TIMEOUT_S 500 |
| #define PPS_READY_DELTA_TIMEOUT_S 10 |
| |
| #define PPS_ERROR_RETRY_MS 1000 |
| |
| enum { |
| PPS_INDEX_NOT_SUPP = -1, |
| PPS_INDEX_TCPM = 1, |
| PPS_INDEX_WLC = 2, |
| PPS_INDEX_MAX = 2, |
| }; |
| |
| #define MDIS_OF_CDEV_NAME "google,mdis_charger" |
| #define MDIS_CDEV_NAME "chg_mdis" |
| #define MDIS_IN_MAX 4 |
| #define MDIS_OUT_MAX GCPM_MAX_CHARGERS |
| |
| struct mdis_thermal_device |
| { |
| struct gcpm_drv *gcpm; |
| struct mutex tdev_lock; |
| |
| struct thermal_cooling_device *tcd; |
| u32 *thermal_mitigation; |
| int thermal_levels; |
| int current_level; |
| int therm_fan_alarm_level; |
| }; |
| |
| struct gcpm_drv { |
| struct device *device; |
| struct power_supply *psy; |
| struct delayed_work init_work; |
| |
| /* charge limit for wireless DC (legacy) */ |
| struct gvotable_election *dc_fcc_votable; |
| |
| bool cp_fcc_hold; /* debounces CP */ |
| int cp_fcc_hold_limit; /* limit to re-enter CP */ |
| |
| /* MDIS: wired and wireless via main charger */ |
| struct gvotable_election *fcc_votable; |
| struct gvotable_election *dc_icl_votable; |
| struct gvotable_election *tx_icl_votable; |
| /* MDIS: wired and wireless via DC charger */ |
| struct gvotable_election *cp_votable; |
| /* MDIS: configuration */ |
| struct power_supply *mdis_in[MDIS_IN_MAX]; |
| int mdis_in_count; |
| struct power_supply *mdis_out[MDIS_OUT_MAX]; |
| int mdis_out_count; |
| u32 *mdis_out_limits[MDIS_OUT_MAX]; |
| u32 mdis_out_sel[MDIS_OUT_MAX]; |
| /* MDIS: device and current budget */ |
| struct mdis_thermal_device thermal_device; |
| struct gvotable_election *mdis_votable; |
| struct gvotable_election *fan_level_votable; |
| |
| /* CSI */ |
| struct gvotable_election *csi_status_votable; |
| |
| /* combine PPS, route to the active PPS source */ |
| struct power_supply *pps_psy; |
| |
| /* basically the same as mdis_out */ |
| int chg_psy_retries; |
| struct power_supply *chg_psy_avail[GCPM_MAX_CHARGERS]; |
| const char *chg_psy_names[GCPM_MAX_CHARGERS]; |
| struct gvotable_election *dc_chg_avail_votable; |
| struct mutex chg_psy_lock; |
| int chg_psy_active; |
| int chg_psy_count; |
| |
| /* wakelock */ |
| struct wakeup_source *gcpm_ws; |
| |
| /* force a charger, this might have side effects */ |
| int force_active; |
| |
| struct logbuffer *log; |
| |
| /* TCPM state for wired PPS charging */ |
| const char *tcpm_psy_name; |
| struct power_supply *tcpm_psy; |
| struct pd_pps_data tcpm_pps_data; |
| int log_psy_ratelimit; |
| u32 tcpm_phandle; |
| |
| /* TCPM state for wireless PPS charging */ |
| const char *wlc_dc_name; |
| struct power_supply *wlc_dc_psy; |
| struct pd_pps_data wlc_pps_data; |
| u32 wlc_phandle; |
| |
| struct delayed_work select_work; |
| |
| /* set to force PPS negotiation */ |
| bool force_pps; |
| /* pps state and detect */ |
| struct delayed_work pps_work; |
| /* request of output ua, */ |
| int out_ua; |
| int out_uv; |
| |
| int dcen_gpio; |
| u32 dcen_gpio_default; |
| |
| /* >0 when enabled, pps charger to use */ |
| int pps_index; |
| /* >0 when enabled, dc_charger */ |
| int dc_index; |
| /* dc_charging state */ |
| int dc_state; |
| |
| ktime_t dc_start_time; |
| |
| /* Disable DC control */ |
| int dc_ctl; |
| |
| /* force check of the DC limit again (debug) */ |
| bool new_dc_limit; |
| |
| /* taper off of current at tier, voltage */ |
| u32 taper_step_interval; /* countdown interval in seconds */ |
| u32 taper_step_voltage; /* voltage before countdown */ |
| u32 taper_step_current; /* current before countdown */ |
| u32 taper_step_grace; /* steps from voltage before countdown */ |
| u32 taper_step_count; /* countdown steps before dc_done */ |
| u32 taper_step_fv_margin; /* countdown steps before dc_done */ |
| u32 taper_step_cc_step; /* countdown steps before dc_done */ |
| int taper_step; /* actual countdown */ |
| |
| /* policy: soc% based limits for DC charging */ |
| u32 dc_limit_soc_high; /* DC will not start over high */ |
| /* policy: power demand limit for DC charging */ |
| u32 dc_limit_vbatt_low; /* DC will not stop until low */ |
| u32 dc_limit_vbatt_min; /* DC will start at min */ |
| u32 dc_limit_vbatt_high; /* DC will not start over high */ |
| u32 dc_limit_vbatt_max; /* DC stop at max */ |
| u32 dc_limit_demand; |
| |
| /* TODO: keep TCPM/DC state in a structure add there */ |
| u32 dc_limit_cc_min; /* PPS_DC stop if CC_MAX is under this */ |
| u32 dc_limit_cc_min_wlc; /* WLC_DC stop if CC_MAX is under this */ |
| |
| /* cc_max and fv_uv are the demand from google_charger */ |
| int cc_max; |
| int fv_uv; |
| |
| bool dc_init_complete; |
| bool init_complete; |
| bool resume_complete; |
| struct notifier_block chg_nb; |
| |
| /* tie up to charger mode */ |
| struct gvotable_election *gbms_mode; |
| |
| /* debug fs */ |
| struct dentry *debug_entry; |
| }; |
| |
| #define gcpm_psy_name(psy) \ |
| ((psy) && (psy)->desc && (psy)->desc->name ? (psy)->desc->name : "???") |
| |
| /* TODO: rename to "can_dc" and handle capabilities based on index number */ |
| #define gcpm_is_dc(gcpm, index) \ |
| ((index) >= GCPM_INDEX_DC_ENABLE) |
| |
| /* Logging ----------------------------------------------------------------- */ |
| |
| int debug_printk_prlog = LOGLEVEL_INFO; |
| |
| /* ------------------------------------------------------------------------- */ |
| |
| static struct gvotable_election *gcpm_get_cp_votable(struct gcpm_drv *gcpm) |
| { |
| if (!gcpm->cp_votable) { |
| struct gvotable_election *v; |
| |
| v = gvotable_election_get_handle("GCPM_FCC"); |
| if (!IS_ERR_OR_NULL(v)) |
| gcpm->cp_votable = v; |
| } |
| |
| return gcpm->cp_votable; |
| } |
| |
| static struct gvotable_election *gcpm_get_dc_icl_votable(struct gcpm_drv *gcpm) |
| { |
| if (!gcpm->dc_icl_votable) { |
| struct gvotable_election *v; |
| |
| v = gvotable_election_get_handle("DC_ICL"); |
| if (!IS_ERR_OR_NULL(v)) |
| gcpm->dc_icl_votable = v; |
| } |
| |
| return gcpm->dc_icl_votable; |
| } |
| |
| static struct gvotable_election *gcpm_get_fcc_votable(struct gcpm_drv *gcpm) |
| { |
| if (!gcpm->fcc_votable) { |
| struct gvotable_election *v; |
| |
| v = gvotable_election_get_handle("MSC_FCC"); |
| if (!IS_ERR_OR_NULL(v)) |
| gcpm->fcc_votable = v; |
| } |
| |
| return gcpm->fcc_votable; |
| } |
| |
| /* will kick gcpm_fcc_callback(), needs mutex_unlock(&gcpm->chg_psy_lock); */ |
| static int gcpm_update_gcpm_fcc(struct gcpm_drv *gcpm, const char *reason, |
| int limit, bool enable) |
| { |
| struct gvotable_election *el; |
| int ret = -ENODEV; |
| |
| el = gcpm_get_cp_votable(gcpm); |
| if (el) |
| ret = gvotable_cast_int_vote(el, reason, limit, enable); |
| |
| return ret; |
| } |
| |
| /* current limit for DC charging */ |
| static int gcpm_get_gcpm_fcc(struct gcpm_drv *gcpm) |
| { |
| struct gvotable_election *el; |
| int dc_iin = -1; |
| |
| el = gcpm_get_cp_votable(gcpm); |
| if (el) |
| dc_iin = gvotable_get_current_int_vote(el); |
| if (dc_iin < 0) |
| dc_iin = gcpm->cc_max; |
| |
| return dc_iin; |
| } |
| |
| /* ------------------------------------------------------------------------- */ |
| |
| static struct power_supply *gcpm_chg_get_charger(const struct gcpm_drv *gcpm, int index) |
| { |
| return (index < 0 || index >= gcpm->chg_psy_count) ? NULL : gcpm->chg_psy_avail[index]; |
| } |
| |
| static struct power_supply *gcpm_chg_get_default(const struct gcpm_drv *gcpm) |
| { |
| return gcpm_chg_get_charger(gcpm, GCPM_DEFAULT_CHARGER); |
| } |
| |
| /* TODO: place a lock around the operation? */ |
| static struct power_supply *gcpm_chg_get_active(const struct gcpm_drv *gcpm) |
| { |
| return gcpm_chg_get_charger(gcpm, gcpm->chg_psy_active); |
| } |
| |
| static bool gcpm_chg_is_cp_active(const struct gcpm_drv *gcpm) |
| { |
| return gcpm_is_dc(gcpm, gcpm->chg_psy_active); |
| } |
| |
| /* !=NULL if the adapter is not CP */ |
| static struct power_supply *gcpm_chg_get_active_cp(const struct gcpm_drv *gcpm) |
| { |
| struct power_supply *psy = NULL; |
| |
| if (gcpm_chg_is_cp_active(gcpm)) |
| psy = gcpm_chg_get_charger(gcpm, gcpm->chg_psy_active); |
| |
| return psy; |
| } |
| |
| |
| static int gcpm_chg_ping(struct gcpm_drv *gcpm, int index, bool online) |
| { |
| struct power_supply *chg_psy; |
| int ret; |
| |
| chg_psy = gcpm->chg_psy_avail[index]; |
| if (!chg_psy) |
| return 0; |
| |
| ret = GPSY_SET_PROP(chg_psy, POWER_SUPPLY_PROP_ONLINE, 0); |
| if (ret < 0) |
| pr_debug("adapter %d cannot ping (%d)", index, ret); |
| |
| return 0; |
| } |
| |
| /* use the charger one when avalaible or fallback to the generated one */ |
| static uint64_t gcpm_get_charger_state(const struct gcpm_drv *gcpm, |
| struct power_supply *chg_psy) |
| { |
| union gbms_charger_state chg_state; |
| int rc; |
| |
| rc = gbms_read_charger_state(&chg_state, chg_psy); |
| if (rc < 0) |
| return 0; |
| |
| return chg_state.v; |
| } |
| |
| /* |
| * chg_psy_active==-1 if index was active |
| * NOTE: GBMS_PROP_CHARGING_ENABLED will be pinged later on |
| */ |
| static int gcpm_chg_offline(struct gcpm_drv *gcpm, int index) |
| { |
| const int active_index = gcpm->chg_psy_active; |
| struct power_supply *chg_psy; |
| int ret; |
| |
| ret = gcpm_update_gcpm_fcc(gcpm, "CC_MAX", gcpm->cc_max, false); |
| if (ret < 0) |
| pr_debug("PPS_DC: offline cannot update cp_fcc (%d)\n", ret); |
| |
| |
| chg_psy = gcpm_chg_get_charger(gcpm, index); |
| if (!chg_psy) |
| return 0; |
| |
| /* OFFLINE should stop charging */ |
| ret = GPSY_SET_PROP(chg_psy, GBMS_PROP_CHARGING_ENABLED, 0); |
| if (ret == 0) |
| ret = GPSY_SET_PROP(chg_psy, POWER_SUPPLY_PROP_ONLINE, 0); |
| if (ret == 0 && gcpm->chg_psy_active == index) |
| gcpm->chg_psy_active = -1; |
| |
| pr_info("%s: %s active=%d->%d offline_ok=%d\n", __func__, |
| pps_name(chg_psy), active_index, gcpm->chg_psy_active, ret == 0); |
| |
| return ret; |
| } |
| |
| /* preset charging parameters */ |
| static int gcpm_chg_preset(struct power_supply *chg_psy, int fv_uv, int cc_max) |
| { |
| const char *name = gcpm_psy_name(chg_psy); |
| int ret; |
| |
| pr_debug("%s: %s fv_uv=%d cc_max=%d\n", __func__, name, fv_uv, cc_max); |
| |
| ret = GPSY_SET_PROP(chg_psy, POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX, |
| fv_uv); |
| if (ret < 0) { |
| pr_err("%s: %s no fv_uv (%d)\n", __func__, name, ret); |
| return ret; |
| } |
| |
| ret = GPSY_SET_PROP(chg_psy, POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX, |
| cc_max); |
| if (ret < 0) |
| pr_err("%s: %s no cc_max (%d)\n", __func__, name, ret); |
| |
| return ret; |
| } |
| |
| /* setting online might start charging (if ENABLE is set) */ |
| static int gcpm_chg_online(struct power_supply *chg_psy, int fv_uv, int cc_max) |
| { |
| const char *name = gcpm_psy_name(chg_psy); |
| bool preset_ok = true; |
| int ret; |
| |
| if (!chg_psy) { |
| pr_err("%s: invalid charger\n", __func__); |
| return -EINVAL; |
| } |
| |
| /* preset the new charger */ |
| ret = gcpm_chg_preset(chg_psy, fv_uv, cc_max); |
| if (ret < 0) |
| preset_ok = false; |
| |
| /* online (but so we can enable it) */ |
| ret = GPSY_SET_PROP(chg_psy, POWER_SUPPLY_PROP_ONLINE, 1); |
| if (ret < 0) { |
| pr_debug("%s: %s online failed (%d)\n", __func__, name, ret); |
| return ret; |
| } |
| |
| /* retry preset if failed */ |
| if (!preset_ok) |
| ret = gcpm_chg_preset(chg_psy, fv_uv, cc_max); |
| if (ret < 0) { |
| int rc; |
| |
| pr_err("%s: %s preset failed (%d)\n", __func__, name, ret); |
| |
| rc = GPSY_SET_PROP(chg_psy, POWER_SUPPLY_PROP_ONLINE, 0); |
| if (rc < 0) |
| pr_err("%s: %s offline failed (%d)\n", __func__, name, rc); |
| } |
| |
| return ret; |
| } |
| |
| /* |
| * gcpm->chg_psy_active == gcpm->dc_index on success. |
| * NOTE: call with a lock around gcpm->chg_psy_lock |
| */ |
| static int gcpm_chg_start(struct gcpm_drv *gcpm, int index, int fv_uv, int cc_max) |
| { |
| const int active_index = gcpm->chg_psy_active; |
| struct power_supply *chg_psy; |
| int ret = -EINVAL; |
| |
| if (index == active_index) |
| return 0; |
| |
| if (active_index != -1) |
| pr_err("%s: %d->%d not idle\n", __func__, active_index, index); |
| |
| /* validate the index before switch */ |
| chg_psy = gcpm_chg_get_charger(gcpm, index); |
| if (chg_psy) |
| ret = gcpm_chg_online(chg_psy, fv_uv, cc_max); |
| if (ret < 0) { |
| /* TODO: force active_index if != -1 */ |
| pr_debug("%s: index=%d not online (%d)\n", |
| __func__, index, ret); |
| return ret; |
| } |
| |
| pr_debug("%s: active=%d->%d\n", __func__, active_index, index); |
| |
| gcpm->chg_psy_active = index; |
| return ret; |
| } |
| |
| /* |
| * Enable DirectCharge mode, PPS and DC charger must be already initialized |
| * NOTE: disable might restart the default charger with stale settings |
| */ |
| static int gcpm_dc_enable(struct gcpm_drv *gcpm, bool enabled) |
| { |
| if (gcpm->dcen_gpio >= 0 && !gcpm->dcen_gpio_default) |
| gpio_set_value(gcpm->dcen_gpio, enabled); |
| |
| if (!gcpm->gbms_mode) { |
| struct gvotable_election *v; |
| |
| v = gvotable_election_get_handle(GBMS_MODE_VOTABLE); |
| if (IS_ERR_OR_NULL(v)) |
| return -ENODEV; |
| gcpm->gbms_mode = v; |
| } |
| |
| return gvotable_cast_long_vote(gcpm->gbms_mode, "GCPM", |
| GBMS_CHGR_MODE_CHGR_DC, enabled); |
| } |
| |
| /* |
| * disable DC and switch back to the default charger. Final DC statate is |
| * DC_IDLE (i.e. this can be used to reset dc_state from DC_DISABLED). |
| * NOTE: call with a lock around gcpm->chg_psy_lock |
| * NOTE: I could pass in and return dc_state instead of changing gcpm |
| * must hold a lock on mutex_lock(&gcpm->chg_psy_lock); |
| */ |
| static int gcpm_dc_stop(struct gcpm_drv *gcpm, int index) |
| { |
| int dc_state = gcpm->dc_state; |
| int ret = 0; |
| |
| if (!gcpm_is_dc(gcpm, index)) |
| dc_state = DC_ENABLE_PASSTHROUGH; |
| |
| switch (dc_state) { |
| case DC_RUNNING: |
| case DC_PASSTHROUGH: |
| ret = gcpm_chg_offline(gcpm, index); |
| if (ret < 0) |
| pr_warn("DC_PPS: Cannot offline DC index=%d (%d)", |
| index, ret); |
| else |
| gcpm->dc_state = DC_ENABLE; |
| /* Fall Through */ |
| case DC_ENABLE: |
| case DC_ENABLE_PASSTHROUGH: |
| ret = gcpm_dc_enable(gcpm, false); |
| if (ret < 0) { |
| pr_err("DC_PPS: Cannot disable DC (%d)", ret); |
| break; |
| } |
| /* Fall Through */ |
| default: |
| gcpm->dc_state = DC_DISABLED; |
| break; |
| } |
| |
| return ret; |
| } |
| |
| /* |
| * route the dc_limit to MSC_FCC for wireless charing. |
| * @return <0 on error, 0 on limit not applied, 1 on limit applied (and positive) |
| * call holding a lock on mutex_lock(&gcpm->chg_psy_lock); |
| */ |
| static int gcpm_dc_fcc_update(struct gcpm_drv *gcpm, int value) |
| { |
| struct gvotable_election *msc_fcc; |
| int limit = value; |
| int ret = -ENODEV; |
| |
| msc_fcc = gcpm_get_fcc_votable(gcpm); |
| if (!msc_fcc) |
| goto error_exit; |
| |
| /* apply/enable DC_FCC only when a WLC_DC source is selected */ |
| if ((gcpm->pps_index != PPS_INDEX_WLC) || |
| (gcpm->dc_index <= GCPM_DEFAULT_CHARGER) || limit < 0) |
| limit = -1; |
| |
| /* |
| * The thermal voter for FCC wired must be disabled to allow higher |
| * charger rates for DC_FCC than for the wired case. |
| */ |
| ret = gvotable_cast_int_vote(msc_fcc, "DC_FCC", limit, limit >= 0); |
| if (ret < 0) |
| pr_err("%s: vote %d on MSC_FCC failed (%d)\n", __func__, |
| limit, ret); |
| else |
| ret = limit >= 0; |
| |
| error_exit: |
| dev_dbg(gcpm->device, "%s: DC_FCC->MSC_FCC pps_index=%d value=%d limit=%d applied=%d\n", |
| __func__, gcpm->pps_index, value, limit, ret); |
| |
| return ret; |
| } |
| |
| /* |
| * route the dc_limit to MSC_FCC for wireless charging and adjust the MDIS |
| * limits when switching between CP and non CP charging. |
| * NOTE: when in MDIS is at level = 0 the cooling zone disable the MDIS votes |
| * on MSC_FCC, DC_ICL and GCPM_FCC. |
| * NOTE: called with negative cp_limit when switching from WLC_CP to WLC and |
| * with the HOLD limit when re-starting PPS_DC and WLC_DC. |
| * @return <0 on error, 0 on limit not applied, 1 on limit applied and positive |
| * call holding a lock on mutex_lock(&gcpm->chg_psy_lock); |
| */ |
| static int gcpm_update_votes(struct gcpm_drv *gcpm, int cp_limit) |
| { |
| const bool enable = gcpm->thermal_device.current_level > 0; |
| struct gvotable_election *el; |
| int ret; |
| |
| pr_debug("%s: cp_limit=%d\n", __func__, cp_limit); |
| |
| /* update DC_FCC limit before disabling the others */ |
| if (cp_limit > 0) |
| ret = gcpm_dc_fcc_update(gcpm, enable ? cp_limit : -1); |
| |
| /* vote on DC_ICL */ |
| el = gcpm_get_dc_icl_votable(gcpm); |
| if (el) |
| gvotable_recast_ballot(el, "MDIS", enable && cp_limit == 0); |
| |
| /* vote on MSC_FCC: applied only when CP is not enabled */ |
| el = gcpm_get_fcc_votable(gcpm); |
| if (el) |
| gvotable_recast_ballot(el, "MDIS", enable && cp_limit == 0); |
| |
| /* vote on GCPM_FCC: valid only on cp */ |
| el = gcpm_get_cp_votable(gcpm); |
| if (el) |
| gvotable_recast_ballot(el, "MDIS", enable && cp_limit); |
| |
| /* update DC_FCC limit after enabling the others */ |
| if (cp_limit <= 0) |
| ret = gcpm_dc_fcc_update(gcpm, enable ? cp_limit : -1); |
| |
| return ret; |
| } |
| |
| |
| /* NOTE: call with a lock around gcpm->chg_psy_lock */ |
| static int gcpm_dc_start(struct gcpm_drv *gcpm, int index) |
| { |
| const int dc_iin = gcpm_get_gcpm_fcc(gcpm); |
| struct power_supply *dc_psy; |
| int ret; |
| |
| pr_info("PPS_DC: index=%d dc_iin=%d hold=%d\n", |
| index, dc_iin, gcpm->cp_fcc_hold_limit); |
| |
| /* ENABLE will be called by the dc_pps workloop */ |
| ret = gcpm_chg_start(gcpm, index, gcpm->fv_uv, dc_iin); |
| if (ret < 0) { |
| pr_err("PPS_DC: index=%d not started (%d)\n", index, ret); |
| return ret; |
| } |
| |
| /* |
| * Restoring the DC_FCC limit might change charging current and cause |
| * demand to fall under dc_limit_demand. The possible resulting loop |
| * (enable/disable) is solved in gcpm_chg_select_work(). |
| * NOTE: ->cp_fcc_hold_limit cannot be 0 |
| */ |
| ret = gcpm_update_votes(gcpm, gcpm->cp_fcc_hold_limit); |
| if (ret < 0) |
| pr_debug("PPS_DC: start cannot update votes (%d)\n", ret); |
| |
| ret = gcpm_update_gcpm_fcc(gcpm, "CC_MAX", gcpm->cc_max, true); |
| if (ret < 0) |
| pr_debug("PPS_DC: start cannot update cp_fcc (%d)\n", ret); |
| |
| /* this is the CP */ |
| dc_psy = gcpm_chg_get_active_cp(gcpm); |
| if (!dc_psy) { |
| pr_err("PPS_DC: gcpm->dc_state == DC_READY, no adapter\n"); |
| return -ENODEV; |
| } |
| |
| /* set IIN_CFG (might not need) */ |
| ret = GPSY_SET_PROP(dc_psy, POWER_SUPPLY_PROP_CURRENT_MAX, |
| gcpm->out_ua); |
| if (ret < 0) { |
| pr_err("PPS_DC: no IIN (%d)\n", ret); |
| return ret; |
| } |
| |
| /* vote on MODE */ |
| ret = gcpm_dc_enable(gcpm, true); |
| if (ret < 0) { |
| pr_err("PPS_DC: dc_ready failed=%d\n", ret); |
| return ret; |
| } |
| |
| pr_debug("PPS_DC: dc_ready ok state=%d fv_uv=%d cc_max=%d, out_ua=%d\n", |
| gcpm->dc_state, gcpm->fv_uv, gcpm->cc_max, gcpm->out_ua); |
| |
| return 0; |
| } |
| |
| /* |
| * Select the DC charger using the thermal policy. |
| * DC charging is enabled when demand is over dc_limit (default 0) and |
| * vbatt > vbatt_min (default or device tree). DC is not disabled when |
| * vbatt is over vbat low. |
| * DC is stopped when vbatt is over vbatt_max (default or DT) and not started |
| * when vbatt is over vbatt_high (some default 200mV under vbatt_max). |
| * NOTE: program target before enabling chaging. |
| */ |
| enum gcpm_dc_ctl_t { |
| GCPM_DC_CTL_DEFAULT = 0, |
| GCPM_DC_CTL_DISABLE_WIRED, |
| GCPM_DC_CTL_DISABLE_WIRELESS, |
| GCPM_DC_CTL_DISABLE_BOTH, |
| }; |
| |
| /* |
| * the current source as index in mdis_in[]. |
| * < 0 error, the index in mdis_in[] if the source is in PPS mode |
| */ |
| static int gcpm_mdis_match_cp_source(struct gcpm_drv *gcpm, int *online) |
| { |
| union power_supply_propval pval; |
| int i, ret; |
| |
| for (i = 0; i < MDIS_IN_MAX; i++) { |
| if (!gcpm->mdis_in[i]) |
| continue; |
| |
| ret = power_supply_get_property(gcpm->mdis_in[i], |
| POWER_SUPPLY_PROP_ONLINE, |
| &pval); |
| if (ret || !pval.intval) |
| continue; |
| |
| *online = pval.intval; |
| return i; |
| } |
| |
| return -EINVAL; |
| } |
| |
| static int gcpm_mdis_in_is_wireless(struct gcpm_drv *gcpm, int index) |
| { |
| return index == 1; /* TODO: query at startup using type==WIRELESS */ |
| } |
| |
| /* return the PPS_CP or the WLC_CP limit */ |
| static int gcpm_chg_select_check_cp_limit(struct gcpm_drv *gcpm) |
| { |
| int online, cp_min = -1, in_idx; |
| |
| in_idx = gcpm_mdis_match_cp_source(gcpm, &online); |
| if (in_idx < 0 || gcpm_mdis_in_is_wireless(gcpm, in_idx)) { |
| if (gcpm->dc_limit_cc_min_wlc >= 0) |
| cp_min = gcpm->dc_limit_cc_min_wlc; |
| } else if (gcpm->dc_limit_cc_min >= 0) { |
| cp_min = gcpm->dc_limit_cc_min; |
| } |
| |
| /* wlc might use a different (higher) CP limit than wired */ |
| dev_dbg(gcpm->device, "%s: in_idx=%d cp_min=%d\n", __func__, in_idx, cp_min); |
| return cp_min; |
| } |
| |
| /* call holding mutex_lock(&gcpm->chg_psy_lock) */ |
| static int gcpm_chg_select_by_demand(struct gcpm_drv *gcpm) |
| { |
| int cc_max = gcpm->cc_max; /* from google_charger */ |
| int index = GCPM_DEFAULT_CHARGER; |
| int batt_demand = -1; |
| int cp_min; |
| |
| /* |
| * ->cc_max is lowered from the main-charger thermal limit and might |
| * prevent this code from selecting the CP charger again when thermals |
| * caused this code to switch from CP to the main charger. |
| * |
| * NOTE: Need to check the value directly because source selection is |
| * done holding a lock on &gcpm->chg_psy_lock (cc_max will become the |
| * same as gcpm->cp_fcc_hold_limit on exit). |
| */ |
| if (gcpm->cp_fcc_hold && gcpm->cp_fcc_hold_limit >= 0 && cc_max != 0) { |
| /* |
| * ->cp_fcc_hold is set when a thermal limit caused the switch |
| * from CP to main-charger. In this case ->cp_fcc_hold_limit |
| * keeps the current (alternate) CP limit that should be used |
| * to determine whether to go back to the Charge Pump. |
| * NOTE: ->cp_fcc_hold_limit is changed when the DC_FCC changes |
| * (wireless CP) AND when MDIS changes but is not changed when |
| * the MSC_FCC limit changes. This means that without MDIS |
| * CP will restart on PD wired only when the actual charging |
| * current exceeds the cp_min limit. |
| */ |
| dev_dbg(gcpm->device, "%s: change due to hold cc_max=%d->%d\n", |
| __func__, cc_max, gcpm->cp_fcc_hold_limit); |
| cc_max = gcpm->cp_fcc_hold_limit; |
| } |
| |
| /* keeps on default charger until we have valid charging parameters */ |
| if (cc_max <= 0 || gcpm->fv_uv <= 0) |
| goto exit_done; /* index == GCPM_DEFAULT_CHARGER; */ |
| |
| /* |
| * power demand comes from charging tier or thermal limit: leave |
| * dc_limit_demand to 0 to switch only on cp_min. |
| * TODO: handle capabilities based on index number |
| */ |
| batt_demand = (cc_max / 1000) * (gcpm->fv_uv / 1000); |
| if (batt_demand > gcpm->dc_limit_demand) |
| index = GCPM_INDEX_DC_ENABLE; |
| |
| dev_dbg(gcpm->device, "%s: index=%d cc_max=%d gcpm->fv_uv=%d demand=%d, dc_limit=%d\n", |
| __func__, index, cc_max / 1000, gcpm->fv_uv / 1000, |
| batt_demand, gcpm->dc_limit_demand); |
| |
| /* |
| * the limit for DC depends on the source that is active with the |
| * complication that using the DC_ICL disables the THERMAL limit |
| * on MSC_FCC and will cause an immediate reselection of CP. |
| * The code settig ->hold and ->cp_fcc_hold_limit needs to make sure |
| * that the limit is appropriate. |
| */ |
| cp_min = gcpm_chg_select_check_cp_limit(gcpm); |
| if (cp_min == -1 || cc_max <= cp_min) { |
| const bool cp_active = gcpm_chg_is_cp_active(gcpm); |
| |
| /* current demand less than min demand for CP */ |
| dev_dbg(gcpm->device, |
| "%s: cc_max=%d under cp_min=%d, ->hold=%d->%d index:%d->%d\n", |
| __func__, cc_max, cp_min, gcpm->cp_fcc_hold, |
| gcpm->cp_fcc_hold ? gcpm->cp_fcc_hold : cp_active, |
| index, GCPM_DEFAULT_CHARGER); |
| |
| /* |
| * Switch to the default charger and hold it. |
| * NOTE: ->cp_fcc_hold is reset in gcpm_dc_fcc_callback() |
| * and when the MDIS thermal limit changes. This piece is |
| * only for legacy dc_fcc since the mdis code handle this. |
| */ |
| if (!gcpm->cp_fcc_hold) |
| gcpm->cp_fcc_hold = cp_active; |
| |
| index = GCPM_DEFAULT_CHARGER; |
| } |
| |
| exit_done: |
| dev_dbg(gcpm->device, |
| "by_d: index:%d->%d demand=%d,limit=%d cc_max=%d,cp_min=%d, hold=%d", |
| gcpm->dc_index, index, batt_demand, gcpm->dc_limit_demand, |
| cc_max, cp_min, gcpm->cp_fcc_hold); |
| return index; |
| } |
| |
| /* |
| * called only before enabling DC to debounce HIGH SOC. |
| * call holding mutex_lock(&gcpm->chg_psy_lock) |
| */ |
| static int gcpm_chg_select_by_soc(struct power_supply *psy, |
| const struct gcpm_drv *gcpm) |
| { |
| union power_supply_propval pval = { }; |
| int index = gcpm->dc_index; /* debounce it */ |
| int ret; |
| |
| ret = power_supply_get_property(psy, POWER_SUPPLY_PROP_CAPACITY, &pval); |
| if (ret < 0 || pval.intval < gcpm->dc_limit_soc_high) |
| index = GCPM_INDEX_DC_ENABLE; |
| |
| pr_debug("%s: index=%d->%d ret=%d soc=%d limit=%d\n", __func__, |
| gcpm->dc_index, index, ret, pval.intval, |
| gcpm->dc_limit_soc_high); |
| |
| return index; |
| } |
| |
| /* call holding mutex_lock(&gcpm->chg_psy_lock) */ |
| static int gcpm_chg_select_by_voltage(struct power_supply *psy, |
| const struct gcpm_drv *gcpm) |
| { |
| const int vbatt_min = gcpm->dc_limit_vbatt_min; |
| const int vbatt_max = gcpm->dc_limit_vbatt_max; |
| const int vbatt_high = gcpm->dc_limit_vbatt_high; |
| const int vbatt_low = gcpm->dc_limit_vbatt_low; |
| int index = GCPM_DEFAULT_CHARGER; |
| int vbatt = -1; |
| |
| if (!vbatt_min && !vbatt_max) |
| return GCPM_INDEX_DC_ENABLE; |
| |
| vbatt = GPSY_GET_PROP(psy, POWER_SUPPLY_PROP_VOLTAGE_NOW); |
| if (vbatt < 0) { |
| pr_err("CHG_CHK cannot read vbatt %d\n", vbatt); |
| goto exit_done; /* index == GCPM_DEFAULT_CHARGER; */ |
| } |
| |
| /* vbatt_max is the hard limit */ |
| if (vbatt_max && vbatt > vbatt_max) |
| goto exit_done; /* index == GCPM_DEFAULT_CHARGER; */ |
| |
| /* |
| * Need to keep checking the voltage when vbatt is under low |
| * to make sure that DC starts at vbatt_min. Also keeps the |
| * same ->dc_index to avoid instability but keep checking |
| * demand. |
| */ |
| if (vbatt_low && vbatt < vbatt_low) { |
| index = gcpm->dc_index == GCPM_DEFAULT_CHARGER ? |
| -EAGAIN : gcpm->dc_index; /* debounce */ |
| } else if (vbatt_min && vbatt < vbatt_min) { |
| index = gcpm->dc_index == GCPM_DEFAULT_CHARGER ? |
| -EAGAIN : gcpm->dc_index; /* debounce */ |
| } else if (vbatt_high && vbatt > vbatt_high) { |
| index = gcpm->dc_index; /* debounce */ |
| } else { |
| /* vbatt_min <= vbatt <= vbatt_high */ |
| index = GCPM_INDEX_DC_ENABLE; |
| } |
| |
| exit_done: |
| |
| pr_debug("%s: index=%d->%d vbatt=%d: low=%d min=%d high=%d max=%d\n", |
| __func__, gcpm->dc_index, index, vbatt, vbatt_low, vbatt_min, |
| vbatt_high, vbatt_max); |
| |
| return index; |
| } |
| |
| /* call holding mutex_lock(&gcpm->chg_psy_lock) */ |
| static int gcpm_chg_select(struct gcpm_drv *gcpm) |
| { |
| int index = GCPM_DEFAULT_CHARGER; |
| int ret; |
| |
| if (!gcpm->dc_init_complete) |
| goto exit_done; /* index == GCPM_DEFAULT_CHARGER; */ |
| |
| /* overrides cp_fcc_hold, might trigger taper_control */ |
| if (gcpm->force_active >= 0) |
| return gcpm->force_active; |
| |
| /* kill switch */ |
| if (gcpm->dc_ctl == GCPM_DC_CTL_DISABLE_BOTH) |
| goto exit_done; /* index == GCPM_DEFAULT_CHARGER; */ |
| |
| ret = gvotable_get_current_int_vote(gcpm->dc_chg_avail_votable); |
| dev_dbg(gcpm->device, "%s: dc_chg_avail vote: %d\n", __func__, ret); |
| if (ret <= 0) |
| goto exit_done; |
| |
| /* |
| * check demand first to react to thermal engine, then voltage to |
| * make sure that we are over min and that we don't start over high |
| * and we stop at max, finally use SOC to not restart if over a |
| * SOC% |
| */ |
| index = gcpm_chg_select_by_demand(gcpm); |
| if (index == GCPM_INDEX_DC_ENABLE) { |
| struct power_supply *chg_psy; |
| |
| /* checking the current charger, should check battery? */ |
| chg_psy = gcpm_chg_get_default(gcpm); |
| if (chg_psy) { |
| index = gcpm_chg_select_by_voltage(chg_psy, gcpm); |
| if (index == GCPM_INDEX_DC_ENABLE) |
| index = gcpm_chg_select_by_soc(chg_psy, gcpm); |
| } |
| } |
| |
| exit_done: |
| |
| /* consistency check */ |
| if (index >= gcpm->chg_psy_count) { |
| pr_err("CHG_CHK index=%d out of bounds %d\n", index, |
| gcpm->chg_psy_count); |
| index = GCPM_DEFAULT_CHARGER; |
| } |
| |
| return index; |
| } |
| |
| static bool gcpm_chg_dc_check_source(const struct gcpm_drv *gcpm, int index) |
| { |
| /* Will run detection only the first time */ |
| if (gcpm->tcpm_pps_data.stage == PPS_NOTSUPP && |
| gcpm->wlc_pps_data.stage == PPS_NOTSUPP ) |
| return false; |
| |
| return gcpm_is_dc(gcpm, index); |
| } |
| |
| /* reset gcpm pps state */ |
| static void gcpm_pps_online(struct gcpm_drv *gcpm) |
| { |
| /* reset setpoint */ |
| gcpm->out_ua = -1; |
| gcpm->out_uv = -1; |
| |
| /* reset detection */ |
| if (gcpm->tcpm_pps_data.pps_psy) { |
| pps_init_state(&gcpm->tcpm_pps_data); |
| if (gcpm->dc_ctl & GCPM_DC_CTL_DISABLE_WIRED) |
| gcpm->tcpm_pps_data.stage = PPS_NOTSUPP; |
| } |
| if (gcpm->wlc_pps_data.pps_psy) { |
| pps_init_state(&gcpm->wlc_pps_data); |
| if (gcpm->dc_ctl & GCPM_DC_CTL_DISABLE_WIRELESS) |
| gcpm->wlc_pps_data.stage = PPS_NOTSUPP; |
| } |
| gcpm->pps_index = 0; |
| } |
| |
| static struct pd_pps_data *gcpm_pps_data(struct gcpm_drv *gcpm) |
| { |
| struct pd_pps_data *pps_data = NULL; |
| |
| if (gcpm->pps_index == PPS_INDEX_TCPM) |
| pps_data = &gcpm->tcpm_pps_data; |
| else if (gcpm->pps_index == PPS_INDEX_WLC) |
| pps_data = &gcpm->wlc_pps_data; |
| |
| return pps_data; |
| } |
| |
| /* Wait for a source to become ready for the handoff */ |
| static int gcpm_pps_wait_for_ready(struct gcpm_drv *gcpm) |
| { |
| struct pd_pps_data *pps_data = gcpm_pps_data(gcpm); |
| int pps_ui, vout = -1, iout = -1; |
| bool pwr_ok = false; |
| |
| if (!pps_data) |
| return -ENODEV; |
| |
| /* determine the limit/levels if needed */ |
| if (gcpm->pps_index == PPS_INDEX_WLC) { |
| struct power_supply *chg_psy = gcpm_chg_get_active(gcpm); |
| int vbatt = -1; |
| |
| if (chg_psy) |
| vbatt = GPSY_GET_PROP(chg_psy, POWER_SUPPLY_PROP_VOLTAGE_NOW); |
| if (vbatt > 0) |
| vout = vbatt * 4; |
| } else { |
| pwr_ok = true; |
| } |
| |
| /* always need to ping */ |
| pps_ui = pps_update_adapter(pps_data, vout, iout, pps_data->pps_psy); |
| if (pps_ui < 0) { |
| pr_err("PPS_Work: pps update, dc_state=%d (%d)\n", |
| gcpm->dc_state, pps_ui); |
| return pps_ui; |
| } |
| |
| /* wait until adapter is at or over request */ |
| pwr_ok |= (vout <=0 || pps_data->out_uv >= vout) && |
| (iout <=0 || pps_data->op_ua >= iout ); |
| |
| pr_info("PPS_Work: pwr_ok=%d pps_ui=%d vout=%d out_uv=%d iout=%d op_ua=%d\n", |
| pwr_ok, pps_ui, vout, pps_data->out_uv, iout, pps_data->op_ua); |
| |
| return pwr_ok ? pps_ui : -EAGAIN; |
| } |
| |
| /* return true when pd_online=PROG_ONLINE and stage=ACTIVE */ |
| static int gcpm_pps_check_active(struct pd_pps_data *pps_data) |
| { |
| int pps_ui; |
| |
| /* not supported for this stage */ |
| if (pps_data->stage == PPS_NOTSUPP) |
| return 0; |
| |
| /* <0 for error, 0 for done, or the next polling interval */ |
| pps_ui = pps_work(pps_data, pps_data->pps_psy); |
| if (pps_data->pd_online < PPS_PSY_PROG_ONLINE) |
| pr_debug("PPS_Work: TCPM Wait %s pps_ui=%d online=%d, stage=%d\n", |
| pps_name(pps_data->pps_psy), pps_ui, pps_data->pd_online, |
| pps_data->stage); |
| |
| return pps_ui >= 0 && pps_data->stage == PPS_ACTIVE; |
| } |
| |
| /* |
| * Debounce online, enable PROG on each of the sources (ping) source that |
| * transition to PPS_ACTIVE and set the pps_index. |
| * |
| * ->stage == |
| * DISABLED => NONE -> AVAILABLE -> ACTIVE -> DISABLED |
| * -> DISABLED |
| * -> NOTSUPP |
| * |
| * return -EAGAIN if none of the sources is online, -ENODEV none of the sources |
| * supports PPS, 0 if one of the sources is ONLINE an |
| */ |
| static int gcpm_pps_work(struct gcpm_drv *gcpm) |
| { |
| int rc, ret = 0, online = 0, pps_index = 0; |
| |
| /* |
| * rc>0 when source has .pd_online==PROG_ONLINE and .stage==ACTIVE |
| * ,stage is active when the source has provided the source caps. |
| */ |
| rc = gcpm_pps_check_active(&gcpm->tcpm_pps_data); |
| if (rc <= 0) { |
| rc = gcpm_pps_check_active(&gcpm->wlc_pps_data); |
| if (rc > 0) |
| pps_index = PPS_INDEX_WLC; |
| else |
| online |= gcpm->wlc_pps_data.pd_online != 0; |
| |
| online |= gcpm->tcpm_pps_data.pd_online != 0; |
| } else { |
| pps_index = PPS_INDEX_TCPM; |
| } |
| |
| if (gcpm->pps_index != pps_index) |
| logbuffer_log(gcpm->log, "PPS_Work: pps_index %d->%d\n", |
| gcpm->pps_index, pps_index); |
| |
| /* handles PPS source offline or NOT active anymore */ |
| if (pps_index == 0 && gcpm->pps_index) |
| ret = -ENODEV; |
| else if (pps_index == 0 && !online) |
| ret = -EAGAIN; |
| |
| pr_debug("PPS_Work: tcpm[online=%d, stage=%d] wlc[online=%d, stage=%d] ol=%d ret=%d pps_index=%d->%d\n", |
| gcpm->tcpm_pps_data.pd_online, gcpm->tcpm_pps_data.stage, |
| gcpm->wlc_pps_data.pd_online, gcpm->wlc_pps_data.stage, |
| online, ret, gcpm->pps_index, pps_index); |
| |
| gcpm->pps_index = pps_index; |
| return ret; |
| } |
| |
| static int gcpm_pps_timeout(struct gcpm_drv *gcpm) |
| { |
| struct pd_pps_data *wlc_pps_data = &gcpm->wlc_pps_data; |
| |
| return (wlc_pps_data->stage != PPS_NOTSUPP && wlc_pps_data->pd_online) |
| ? PPS_ACTIVE_WLC_TIMEOUT_S : PPS_ACTIVE_USB_TIMEOUT_S; |
| } |
| |
| static int gcpm_pps_offline(struct gcpm_drv *gcpm) |
| { |
| int ret; |
| |
| /* TODO: migh be a no-op when pps_index == 0 */ |
| |
| if (gcpm->tcpm_pps_data.pps_psy) { |
| ret = pps_prog_offline(&gcpm->tcpm_pps_data, |
| gcpm->tcpm_pps_data.pps_psy); |
| if (ret < 0) |
| pr_err("PPS_DC: fail tcpm offline (%d)\n", ret); |
| } |
| |
| if (gcpm->wlc_pps_data.pps_psy) { |
| ret = pps_prog_offline(&gcpm->wlc_pps_data, |
| gcpm->wlc_pps_data.pps_psy); |
| if (ret < 0) |
| pr_err("PPS_DC: fail wlc offline (%d)\n", ret); |
| } |
| |
| gcpm->pps_index = 0; |
| return 0; |
| } |
| |
| /* <=0 to disable, > 0 to enable "n" counts */ |
| static bool gcpm_taper_ctl(struct gcpm_drv *gcpm, int count) |
| { |
| bool changed = false; |
| |
| if (count <= 0) { |
| changed = gcpm->taper_step != 0; |
| gcpm->taper_step = 0; |
| } else if (gcpm->taper_step == 0) { |
| gcpm->taper_step = count; |
| changed = true; |
| } |
| |
| return changed; |
| } |
| |
| /* |
| * taper off charging current to ease the transition out of CP charging. |
| * NOTE: this writes directly to the charging current. |
| */ |
| static bool gcpm_taper_step(const struct gcpm_drv *gcpm, |
| int dc_iin, int taper_step) |
| { |
| const int delta = gcpm->taper_step_count - taper_step; |
| int fv_uv = gcpm->fv_uv, cc_max = dc_iin; |
| struct power_supply *dc_psy; |
| |
| if (taper_step <= 0) |
| return true; |
| /* |
| * TODO: on a race between TAPER and select, active might not |
| * be a DC source. Force done to prevent voltage spikes. |
| */ |
| dc_psy = gcpm_chg_get_active_cp(gcpm); |
| if (!dc_psy) |
| return true; |
| |
| /* Optional dc voltage limit */ |
| if (gcpm->taper_step_voltage) { |
| int vbatt; |
| |
| vbatt = GPSY_GET_PROP(dc_psy, POWER_SUPPLY_PROP_VOLTAGE_NOW); |
| if (vbatt < 0) |
| pr_err("%s: cannot read voltage (%d)", __func__, vbatt); |
| else if (vbatt < gcpm->taper_step_voltage) |
| return false; |
| } |
| |
| /* Optional dc current limit */ |
| if (gcpm->taper_step_current) { |
| int ret, ibatt; |
| |
| /* TODO: use current average if available */ |
| ret = GPSY_GET_INT_PROP(dc_psy, POWER_SUPPLY_PROP_CURRENT_NOW, |
| &ibatt); |
| if (ret < 0) |
| pr_err("%s: cannot read current (%d)", __func__, ret); |
| else if (ibatt > gcpm->taper_step_current) |
| return false; |
| } |
| |
| /* delta < 0 during the grace period, which will increase cc_max */ |
| fv_uv -= gcpm->taper_step_fv_margin; |
| if (gcpm->taper_step_cc_step) { |
| cc_max -= delta * gcpm->taper_step_cc_step; |
| if (cc_max < dc_iin / 2) |
| cc_max = dc_iin / 2; |
| } |
| |
| /* increase of cc_max due to delta < 0 are ignored */ |
| if (cc_max < dc_iin) { |
| int ret; |
| |
| /* failure to preset stop taper and revert to main */ |
| ret = gcpm_chg_preset(dc_psy, fv_uv, cc_max); |
| pr_info("CHG_CHK: taper_step=%d fv_uv=%d->%d, dc_iin=%d->%d\n", |
| taper_step, gcpm->fv_uv, fv_uv, dc_iin, cc_max); |
| if (ret < 0) { |
| pr_err("CHG_CHK: taper_step=%d failed, revert (%d)\n", |
| taper_step, ret); |
| return true; |
| } |
| |
| } else { |
| pr_debug("CHG_CHK: grace taper_step=%d fv_uv=%d, dc_iin=%d\n", |
| taper_step, gcpm->fv_uv, dc_iin); |
| } |
| |
| logbuffer_log(gcpm->log, "taper_step=%d delta=%d fv_uv=%d->%d, dc_iin=%d->%d", |
| taper_step, delta, gcpm->fv_uv, fv_uv, dc_iin, cc_max); |
| |
| |
| /* not done */ |
| return false; |
| } |
| |
| /* needs mutex_lock(&gcpm->chg_psy_lock); */ |
| static int gcpm_chg_select_logic(struct gcpm_drv *gcpm) |
| { |
| int index, schedule_pps_interval = -1; |
| bool dc_done = false, dc_ena; |
| |
| dev_dbg(gcpm->device, "%s: init_ok=%d dc_state=%d dc_index=%d\n", __func__, |
| gcpm->dc_init_complete, gcpm->dc_state, gcpm->dc_index); |
| |
| if (!gcpm->dc_init_complete) |
| return -EAGAIN; |
| |
| index = gcpm_chg_select(gcpm); |
| if (index < 0) { |
| pr_debug("%s: index=%d dc_state=%d dc_index=%d\n", |
| __func__, index, gcpm->dc_state, gcpm->dc_index); |
| return -EAGAIN; |
| } |
| |
| /* will not try to enable if the source cannot do PPS */ |
| dc_ena = gcpm_chg_dc_check_source(gcpm, index); |
| |
| /* |
| * taper control reduces cc_max every gcpm->taper_step_interval seconds |
| * by a fixed amount for gcpm->taper_step_count seconds. fv_uv might |
| * also be lowered by a fixed amount. |
| */ |
| if (dc_ena && gcpm->taper_step > 0) { |
| const int interval = msecs_to_jiffies(gcpm->taper_step_interval * 1000); |
| int dc_iin = gcpm->cc_max; |
| |
| dc_done = gcpm_taper_step(gcpm, dc_iin, gcpm->taper_step - 1); |
| if (!dc_done) { |
| mod_delayed_work(system_wq, &gcpm->select_work, interval); |
| gcpm->taper_step -= 1; |
| } |
| |
| pr_debug("%s: taper_step=%d done=%d\n", __func__, |
| gcpm->taper_step, dc_done); |
| } else if (gcpm->taper_step != 0) { |
| const int vbatt_high = gcpm->dc_limit_vbatt_high; |
| |
| /* reset dc_state after taper step */ |
| gcpm_taper_ctl(gcpm, 0); |
| if (gcpm->fv_uv < vbatt_high && gcpm->dc_state == DC_DISABLED) |
| gcpm->dc_state = DC_IDLE; |
| } |
| |
| pr_debug("%s: DC dc_ena=%d dc_state=%d dc_index=%d->%d taper_step=%d\n", |
| __func__, dc_ena, gcpm->dc_state, gcpm->dc_index, index, |
| gcpm->taper_step); |
| |
| /* |
| * NOTE: disabling DC might need to transition to charger mode 0 |
| * same might apply when switching between WLC-DC and PPS-DC. |
| * Figure out a way to do this if needed. |
| */ |
| if (!dc_ena || dc_done) { |
| |
| if (gcpm->dc_state > DC_IDLE && gcpm->dc_index > 0) { |
| pr_info("CHG_CHK: dc_ena=%d dc_done=%d stop PPS_Work for dc_index=%d\n", |
| dc_ena, dc_done, gcpm->dc_index); |
| |
| /* |
| * dc_done will prevent DC to restart until disconnect |
| * or voltage goes over _high. |
| */ |
| gcpm->dc_index = dc_done ? GCPM_INDEX_DC_DISABLE : |
| GCPM_DEFAULT_CHARGER; |
| gcpm_taper_ctl(gcpm, 0); |
| schedule_pps_interval = 0; |
| } |
| } else if (gcpm->dc_state == DC_DISABLED) { |
| /* |
| * dc is disabled when we are done OR when the source doesn't |
| * support PPS or failed the authentication. |
| */ |
| pr_debug("%s: PPS_Work disabled for the session\n", __func__); |
| } else if (gcpm->dc_state == DC_IDLE) { |
| const ktime_t dc_start_time = get_boot_sec(); |
| |
| pr_info("CHG_CHK: start PPS_Work for dc_index=%d at %lld\n", |
| index, dc_start_time); |
| |
| /* reset pps state to re-enable detection */ |
| gcpm_pps_online(gcpm); |
| |
| /* TODO: DC_ENABLE or DC_PASSTHROUGH depending on index */ |
| gcpm->dc_state = DC_ENABLE_PASSTHROUGH; |
| gcpm->dc_index = index; |
| |
| /* grace period of 500ms, PPS Work not called during grace */ |
| gcpm->dc_start_time = dc_start_time; |
| schedule_pps_interval = DC_ENABLE_DELAY_MS; |
| |
| __pm_stay_awake(gcpm->gcpm_ws); |
| pr_debug("%s: pm gcpm stay awake\n", __func__); |
| } |
| |
| if (schedule_pps_interval >= 0) { |
| pr_debug("%s: DC schedule pps_work in %ds\n", __func__, |
| schedule_pps_interval / 1000); |
| |
| mod_delayed_work(system_wq, &gcpm->pps_work, |
| msecs_to_jiffies(schedule_pps_interval)); |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * triggered on every FV_UV and in DC_PASSTHROUGH |
| * will keep polling if in -EAGAIN |
| */ |
| static void gcpm_chg_select_work(struct work_struct *work) |
| { |
| struct gcpm_drv *gcpm = |
| container_of(work, struct gcpm_drv, select_work.work); |
| int ret; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| |
| pr_debug("%s: on=%d dc_state=%d dc_index=%d\n", __func__, |
| gcpm->dc_init_complete, gcpm->dc_state, gcpm->dc_index); |
| |
| ret = gcpm_chg_select_logic(gcpm); |
| if (ret == -EAGAIN) { |
| const int interval = 5; /* 5 seconds */ |
| |
| mod_delayed_work(system_wq, &gcpm->select_work, |
| msecs_to_jiffies(interval * 1000)); |
| } |
| |
| mutex_unlock(&gcpm->chg_psy_lock); |
| } |
| |
| static int gcpm_enable_default(struct gcpm_drv *gcpm) |
| { |
| struct power_supply *chg_psy = gcpm_chg_get_default(gcpm); |
| int ret; |
| |
| /* gcpm_chg_offline set GBMS_PROP_CHARGING_ENABLED = 0 */ |
| ret = GPSY_SET_PROP(chg_psy, GBMS_PROP_CHARGING_ENABLED, 1); |
| if (ret < 0) { |
| pr_debug("%s: failed 2 enable charging (%d)\n", __func__, ret); |
| return ret; |
| } |
| |
| /* (re) online and start the default charger */ |
| ret = gcpm_chg_start(gcpm, GCPM_DEFAULT_CHARGER, gcpm->fv_uv, gcpm->cc_max); |
| if (ret < 0) { |
| pr_debug("%s: failed 2 start (%d)\n", __func__, ret); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| /* online the default charger (do not change active, nor enable) */ |
| static int gcpm_online_default(struct gcpm_drv *gcpm) |
| { |
| return gcpm_chg_online(gcpm_chg_get_default(gcpm), gcpm->fv_uv, gcpm->cc_max); |
| } |
| |
| /* |
| * restart the default charger after DC or while trying to start it. |
| * Can come here during DC_ENABLE_PASSTHROUGH, with PPS enabled and |
| * after a failure to start DC or on a failure to disable the default |
| * charger. |
| * |
| * NOTE: the caller needs to reset gcpm->dc_index |
| */ |
| static int gcpm_pps_wlc_dc_restart_default(struct gcpm_drv *gcpm) |
| { |
| const int active_index = gcpm->chg_psy_active; /* will change */ |
| const int dc_state = gcpm->dc_state; /* will change */ |
| int pps_done, ret; |
| |
| /* DC_FCC limit might be enabled as soon as we enter WLC_DC */ |
| ret = gcpm_update_votes(gcpm, 0); |
| if (ret < 0) |
| pr_err("PPS_DC: wlc_dc_rd cannot update votes (%d)\n", ret); |
| |
| /* Clear taper count if not complete */ |
| gcpm_taper_ctl(gcpm, 0); |
| |
| /* |
| * in dc_state=DC_ENABLE_PASSTHROUGH it might be able to take |
| * the current charger offline BUT might fail to start DC. |
| */ |
| if (dc_state <= DC_IDLE) |
| return 0; |
| |
| /* online the default charger (do not change active, nor enable) |
| * TODO: possibly do nothing if the current charger is not DC. |
| */ |
| ret = gcpm_online_default(gcpm); |
| if (ret < 0) |
| pr_warn("%s: Cannot online default (%d)", __func__, ret); |
| |
| /* |
| * dc_state=DC_DISABLED, chg_psy_active==-1 a DC charger was active. |
| * in DC_ENABLE_PASSTHROUGH, gcpm_dc_stop() will vote on charger mode. |
| */ |
| ret = gcpm_dc_stop(gcpm, active_index); |
| if (ret < 0) { |
| pr_debug("%s: retry disable, dc_state=%d->%d (%d)\n", |
| __func__, dc_state, gcpm->dc_state, ret); |
| return -EAGAIN; |
| } |
| |
| /* |
| * Calling pps_offline is not really needed becasuse the adapter will |
| * revert to fixed once ping stops (pps state is re-initialized on |
| * DC start). I clear it to keep things neat and tidy. |
| * |
| * NOTE: Make sure that pps_prog_offline only changes from PROG to |
| * FIXED and not from OFFLINE to FIXED. Setting WLC from OFFLINE |
| * to FIXED (online) at the wrong time might interfere with |
| * the usecases that need to disable charging explicitly. |
| */ |
| pps_done = gcpm_pps_offline(gcpm); |
| if (pps_done < 0) |
| pr_debug("%s: fail 2 offline pps, dc_state=%d (%d)\n", |
| __func__, gcpm->dc_state, pps_done); |
| |
| ret = gcpm_enable_default(gcpm); |
| if (ret < 0) { |
| pr_err("%s: fail 2 restart default, dc_state=%d pps_done=%d (%d)\n", |
| __func__, gcpm->dc_state, pps_done >= 0 ? : pps_done, ret); |
| return -EAGAIN; |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * pps_data->stage: |
| * PPS_NONE -> PPS_AVAILABLE -> PPS_ACTIVE |
| * -> PPS_DISABLED -> PPS_DISABLED |
| * acquires mutex_lock(&gcpm->chg_psy_lock); |
| */ |
| static void gcpm_pps_wlc_dc_work(struct work_struct *work) |
| { |
| struct gcpm_drv *gcpm = |
| container_of(work, struct gcpm_drv, pps_work.work); |
| struct pd_pps_data *pps_data; |
| int ret, pps_ui = -ENODEV; |
| ktime_t elap; |
| |
| /* spurious during init */ |
| mutex_lock(&gcpm->chg_psy_lock); |
| |
| elap = gcpm->dc_start_time <= 0 ? 0 : get_boot_sec() - gcpm->dc_start_time; |
| |
| pr_debug("%s: ok=%d dc_index=%d dc_state=%d dc_start_time=%lld\n", |
| __func__, gcpm->resume_complete && gcpm->init_complete, |
| gcpm->dc_index, gcpm->dc_state, gcpm->dc_start_time); |
| |
| if (!gcpm->resume_complete || !gcpm->init_complete) { |
| pps_ui = DC_ERROR_RETRY_MS; |
| goto pps_dc_reschedule; |
| } |
| |
| /* disconnect, gcpm_chg_check() and most errors reset ->dc_index */ |
| if (gcpm->dc_index <= 0) { |
| const int active_index = gcpm->chg_psy_active; /* will change */ |
| const bool dc_disable = gcpm->dc_index == GCPM_INDEX_DC_DISABLE; |
| |
| pr_debug("%s: stop for gcpm->dc_index=%d\n", __func__, gcpm->dc_index); |
| |
| /* will leave gcpm->dc_state in DC_DISABLED */ |
| ret = gcpm_pps_wlc_dc_restart_default(gcpm); |
| if (ret < 0) { |
| pr_warn("PPS_Work: retry restart elap=%lld dc_state=%d %d->%d (%d)\n", |
| elap, gcpm->dc_state, active_index, |
| gcpm->chg_psy_active, ret); |
| |
| pps_ui = DC_ERROR_RETRY_MS; |
| goto pps_dc_reschedule; |
| } |
| |
| /* Re-enable DC if just switching to the default charger */ |
| if (!dc_disable) |
| gcpm->dc_state = DC_IDLE; |
| |
| gcpm->dc_start_time = 0; |
| |
| gbms_logbuffer_prlog(gcpm->log, LOGLEVEL_INFO, 0, debug_printk_prlog, |
| "PPS_Work: done%selap=%lld dc_state=%d %d->%d\n", |
| dc_disable ? "for the session " : " ", |
| elap, gcpm->dc_state, active_index, |
| gcpm->chg_psy_active); |
| |
| pr_debug("%s: pm gcpm relax\n", __func__); |
| __pm_relax(gcpm->gcpm_ws); |
| |
| /* TODO: send a ps event? */ |
| goto pps_dc_done; |
| } |
| |
| /* PPS was handed over to the DC driver, just monitor it... */ |
| if (gcpm->dc_state == DC_PASSTHROUGH) { |
| struct power_supply *dc_psy; |
| bool prog_online = false; |
| int index; |
| |
| /* the dc driver needs to keep the source online */ |
| pps_data = gcpm_pps_data(gcpm); |
| if (pps_data) |
| prog_online = pps_check_prog_online(pps_data); |
| if (!prog_online) { |
| pr_err("PPS_Work: PPS offline, elap=%lld dc_index:%d->0\n", |
| elap, gcpm->dc_index); |
| |
| gcpm->dc_index = GCPM_DEFAULT_CHARGER; |
| pps_ui = DC_ERROR_RETRY_MS; |
| goto pps_dc_reschedule; |
| } |
| |
| /* likely changed from debug, bail */ |
| dc_psy = gcpm_chg_get_active(gcpm); |
| if (!dc_psy) { |
| pr_err("PPS_Work: No adapter, elap=%lld in PASSTHROUGH\n", |
| elap); |
| |
| pps_ui = DC_ERROR_RETRY_MS; |
| goto pps_dc_reschedule; |
| } |
| |
| /* something is changed: kick the revert to default */ |
| index = gcpm_chg_select(gcpm); |
| if (index != gcpm->dc_index) |
| mod_delayed_work(system_wq, &gcpm->select_work, 0); |
| |
| /* ->pps_index valid: set/ping source to DC, ping watchdog */ |
| ret = GPSY_SET_PROP(dc_psy, GBMS_PROP_CHARGING_ENABLED, |
| gcpm->pps_index); |
| if (ret == 0) { |
| ret = gcpm_chg_ping(gcpm, GCPM_DEFAULT_CHARGER, 0); |
| if (ret < 0) |
| pr_err("PPS_Work: ping failed, elap=%lld with %d\n", |
| elap, ret); |
| |
| /* keep running to ping the adapters */ |
| pps_ui = DC_RUN_DELAY_MS; |
| } else if (ret == -EBUSY || ret == -EAGAIN) { |
| pps_ui = DC_ERROR_RETRY_MS; |
| } else { |
| pr_err("PPS_Work: ping DC failed, elap=%lld (%d)\n", elap, ret); |
| ret = gcpm_chg_offline(gcpm, gcpm->dc_index); |
| if (ret == 0) |
| ret = gcpm_enable_default(gcpm); |
| if (ret < 0) { |
| pr_err("PPS_Work: cannot online default %d\n", ret); |
| pps_ui = DC_ERROR_RETRY_MS; |
| } else { |
| pr_err("PPS_Work: dc offline\n"); |
| pps_ui = 0; |
| |
| pr_debug("%s: pm gcpm relax\n", __func__); |
| __pm_relax(gcpm->gcpm_ws); |
| } |
| } |
| |
| goto pps_dc_reschedule; |
| } |
| |
| /* |
| * Wait until one of the sources becomes online AND switch to prog |
| * mode. gcpm_pps_work will return <0 when PPS is not supported from |
| * ANY source. Deadline to PPS_PROG_TIMEOUT_S. |
| */ |
| ret = gcpm_pps_work(gcpm); |
| if (ret < 0) { |
| if (elap < PPS_PROG_TIMEOUT_S) { |
| pr_debug("PPS_Work: PROG elap=%lld ret=%d retry\n", elap, ret); |
| |
| /* retry for the session */ |
| pps_ui = PPS_PROG_RETRY_MS; |
| gcpm_pps_online(gcpm); |
| } else { |
| pr_err("PPS_Work: PROG timeout, elap=%lld dc_state=%d (%d)\n", |
| elap, gcpm->dc_state, ret); |
| |
| /* abort for the session */ |
| gcpm->dc_index = GCPM_INDEX_DC_DISABLE; |
| pps_ui = PPS_ERROR_RETRY_MS; |
| } |
| |
| goto pps_dc_reschedule; |
| } |
| |
| /* |
| * DC runs only when PPS is active (ie. online=PROG_ONLINE and |
| * ->stage=PPS_ACTIVE). Abort for the session if a source |
| * went PROG_ONLINE but is not active. |
| */ |
| pps_data = gcpm_pps_data(gcpm); |
| if (!pps_data) { |
| int timeout_s = gcpm_pps_timeout(gcpm); |
| |
| if (elap < timeout_s) { |
| pr_debug("PPS_Work: ACTIVE elap=%lld ret=%d retry\n", elap, ret); |
| |
| /* WLC + Auth might require a very long time */ |
| pps_ui = PPS_ACTIVE_RETRY_MS; |
| } else { |
| pr_err("PPS_Work: ACTIVE timeout=%d, start=%lld elap=%lld dc_state=%d (%d)\n", |
| timeout_s, elap, gcpm->dc_start_time, gcpm->dc_state, ret); |
| |
| /* abort for the session (until disconnect) */ |
| gcpm->dc_index = GCPM_INDEX_DC_DISABLE; |
| pps_ui = PPS_ERROR_RETRY_MS; |
| } |
| |
| goto pps_dc_reschedule; |
| } |
| |
| if (gcpm->dc_state == DC_ENABLE_PASSTHROUGH) { |
| int timeout_s = gcpm_pps_timeout(gcpm) + PPS_READY_DELTA_TIMEOUT_S; |
| int index; |
| |
| /* Also ping the source */ |
| pps_ui = gcpm_pps_wait_for_ready(gcpm); |
| if (pps_ui < 0) { |
| pr_info("PPS_Work: wait for source timeout=%d elap=%lld, dc_state=%d (%d)\n", |
| timeout_s, elap, gcpm->dc_state, pps_ui); |
| if (pps_ui != -EAGAIN) |
| gcpm->dc_index = GCPM_DEFAULT_CHARGER; |
| if (elap > timeout_s) |
| gcpm->dc_index = GCPM_DEFAULT_CHARGER; |
| |
| /* error retry */ |
| pps_ui = PPS_ERROR_RETRY_MS; |
| goto pps_dc_reschedule; |
| } |
| |
| /* |
| * source selection might have changed demand and disabled DC |
| * (WLC_DC has a different mincurrent). Revert the input |
| * selection, retry when ->cp_fcc_hold_limit changes. |
| */ |
| index = gcpm_chg_select(gcpm); |
| if (!gcpm_is_dc(gcpm, index)) { |
| pr_info("PPS_Work: selection changed index=%d\n", index); |
| |
| gcpm->dc_index = GCPM_DEFAULT_CHARGER; |
| pps_ui = PPS_ERROR_RETRY_MS; |
| goto pps_dc_reschedule; |
| } |
| |
| /* |
| * offine current adapter and start new. Charging is enabled |
| * in DC_PASSTHROUGH setting GBMS_PROP_CHARGING_ENABLED to |
| * the PPS source. |
| * TODO: preset the DC charger before handoff |
| * NOTE: There are a bunch of interesting recovery scenarios. |
| */ |
| ret = gcpm_chg_offline(gcpm, gcpm->chg_psy_active); |
| if (ret == 0) |
| ret = gcpm_dc_start(gcpm, gcpm->dc_index); |
| if (ret == 0) { |
| gcpm->dc_state = DC_PASSTHROUGH; |
| pps_ui = DC_ENABLE_DELAY_MS; |
| } else if (pps_ui > DC_ERROR_RETRY_MS) { |
| pps_ui = DC_ERROR_RETRY_MS; |
| } |
| |
| pr_debug("%s: pm gcpm relax\n", __func__); |
| __pm_relax(gcpm->gcpm_ws); |
| } else { |
| struct power_supply *pps_psy = pps_data->pps_psy; |
| |
| /* steady on PPS, if DC state is DC_ENABLE or DC_RUNNING */ |
| pps_ui = pps_update_adapter(pps_data, -1, -1, pps_psy); |
| |
| pr_info("PPS_Work: STEADY pd_online=%d pps_ui=%d dc_ena=%d dc_state=%d\n", |
| pps_data->pd_online, pps_ui, gcpm->dc_index, |
| gcpm->dc_state); |
| if (pps_ui < 0) |
| pps_ui = PPS_ERROR_RETRY_MS; |
| } |
| |
| pps_dc_reschedule: |
| if (pps_ui <= 0) { |
| pr_debug("PPS_Work: pps_ui=%d dc_index=%d dc_state=%d", |
| pps_ui, gcpm->dc_index, gcpm->dc_state); |
| } else { |
| pr_debug("PPS_Work: reschedule in %d dc_index=%d dc_state=%d (%d:%d)", |
| pps_ui, gcpm->dc_index, gcpm->dc_state, gcpm->out_uv, gcpm->out_ua); |
| |
| schedule_delayed_work(&gcpm->pps_work, msecs_to_jiffies(pps_ui)); |
| } |
| |
| pps_dc_done: |
| mutex_unlock(&gcpm->chg_psy_lock); |
| } |
| |
| /* |
| * coming in though the old dc_fcc votable. |
| * |
| * TODO: reimplement in terms of MDIS level remapping the limit |
| */ |
| static int gcpm_dc_fcc_callback(struct gvotable_election *el, |
| const char *reason, |
| void *value) |
| { |
| struct gcpm_drv *gcpm = gvotable_get_data(el); |
| const int limit = (long)value; |
| int changed = gcpm->cp_fcc_hold_limit != limit; |
| int applied; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| |
| /* |
| * ->cp_fcc_hold_limit is updated from MDIS and from the DC_FCC code. |
| * |
| * applied=1 here when the dc_limit has been applied to MSC_FCC and we |
| * need to re-run the selection. Here the limit is applied ONLY when |
| * using WLC_CP. |
| * |
| * NOTE: the thermal engine needs to vote on mdis_chg OR on dc_fcc, |
| * dc_icl and msc_fcc. |
| */ |
| applied = gcpm_dc_fcc_update(gcpm, limit); |
| if (applied < 0) |
| pr_err("%s: cannot enforce DC_FCC limit applied=%d\n", |
| __func__, applied); |
| else if (applied) |
| gcpm->cp_fcc_hold_limit = limit; |
| |
| /* |
| * ->cp_fcc_hold will be set in gcpm_chg_select_by_demand() for WLC_DC |
| * sessions when cc_max has fallen under the limit for WLC_DC charging |
| * (->dc_limit_cc_min_wlc). The hold keeps the device charging with |
| * the default charger (ie. non CP ie WLC) until released. |
| * |
| * Clearing the ->cp_fcc_hold when the thermal limit changes and is |
| * NON zero allows gcpm_chg_select_by_demand() to re-evaluate |
| * returning to CP charging. |
| */ |
| if (limit != 0 && gcpm->cp_fcc_hold) { |
| gcpm->cp_fcc_hold = false; |
| changed += 1; |
| } |
| |
| pr_debug("%s: CPM_THERM_DC_FCC limit=%d hold=%d applied=%d changed=%d\n", |
| __func__, limit, gcpm->cp_fcc_hold, applied, changed); |
| |
| /* |
| * ->cp_fcc_hold force the selection of GCPM_DEFAULT_CHARGER in |
| * gcpm_chg_select_by_demand(). |
| */ |
| if (applied || changed) |
| mod_delayed_work(system_wq, &gcpm->select_work, |
| msecs_to_jiffies(DC_ENABLE_DELAY_MS)); |
| |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return 0; |
| } |
| |
| static int gcpm_dc_chg_avail_callback(struct gvotable_election *el, |
| const char *reason, void *value) |
| { |
| struct gcpm_drv *gcpm = gvotable_get_data(el); |
| const int dc_chg_avail = GVOTABLE_PTR_TO_INT(value); |
| |
| if (!gcpm->init_complete) |
| return 0; |
| |
| mod_delayed_work(system_wq, &gcpm->select_work, 0); |
| pr_debug("DC_CHG_AVAIL: dc_avail=%d, reason=%s\n", dc_chg_avail, reason); |
| |
| return 0; |
| } |
| |
| /* --------------------------------------------------------------------- */ |
| |
| |
| static int gcpm_psy_set_property(struct power_supply *psy, |
| enum power_supply_property psp, |
| const union power_supply_propval *pval) |
| { |
| struct gcpm_drv *gcpm = power_supply_get_drvdata(psy); |
| struct power_supply *chg_psy = NULL; |
| bool ta_check = false; |
| bool route = true; |
| int ret = 0; |
| |
| pm_runtime_get_sync(gcpm->device); |
| if (!gcpm->init_complete || !gcpm->resume_complete) { |
| pm_runtime_put_sync(gcpm->device); |
| return -EAGAIN; |
| } |
| pm_runtime_put_sync(gcpm->device); |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| switch (psp) { |
| /* do not route to the active charger */ |
| case GBMS_PROP_TAPER_CONTROL: { |
| int count = 0; |
| |
| if (pval->intval != GBMS_TAPER_CONTROL_OFF) |
| count = gcpm->taper_step_count + gcpm->taper_step_grace; |
| |
| /* ta_check is set when taper control changes value */ |
| ta_check = gcpm_taper_ctl(gcpm, count); |
| route = false; |
| } break; |
| |
| /* route to the active charger in most cases */ |
| case GBMS_PROP_CHARGE_DISABLE: |
| |
| /* google_charger send this on disconnect and input_suspend. */ |
| pr_info("%s: ChargeDisable value=%d dc_index=%d dc_state=%d\n", |
| __func__, pval->intval, gcpm->dc_index, gcpm->dc_state); |
| |
| if (pval->intval) { |
| /* |
| * more or less the same as gcpm_pps_wlc_dc_work() when |
| * dc_index <= 0. But the default charger must not be |
| * restarted in this case though. |
| * TODO: factor the code with gcpm_pps_wlc_dc_work(). |
| */ |
| |
| /* |
| * No op if the current source is not DC (uncluding |
| * stop while in DC_ENABLE_), ->dc_state |
| * will be DC_DISABLED if this was actually disabled. |
| */ |
| ret = gcpm_dc_stop(gcpm, gcpm->chg_psy_active); |
| if (ret == -EAGAIN) { |
| pr_debug("%s: cannot disable, try again\n", __func__); |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return -EAGAIN; |
| } |
| |
| ret = gcpm_pps_offline(gcpm); |
| if (ret < 0) |
| pr_debug("%s: fail 2 offline pps, dc_state=%d (%d)\n", |
| __func__, gcpm->dc_state, ret); |
| |
| /* reset to the default charger, and clear taper */ |
| gcpm->dc_index = GCPM_DEFAULT_CHARGER; |
| gcpm_taper_ctl(gcpm, 0); |
| |
| /* |
| * no-op if dc was NOT running, set online the charger |
| * but do not start it otherwise. |
| */ |
| ret = gcpm_chg_start(gcpm, GCPM_DEFAULT_CHARGER, |
| gcpm->fv_uv, gcpm->cc_max); |
| if (ret < 0) |
| pr_err("%s: cannot start default (%d)\n", |
| __func__, ret); |
| |
| pr_info("%s: ChargeDisable value=%d dc_index=%d dc_state=%d\n", |
| __func__, pval->intval, gcpm->dc_index, gcpm->dc_state); |
| |
| /* |
| * route = true so active will get the property. |
| * No need to re-check the TA selection on disable. |
| */ |
| ta_check = false; |
| } else if (gcpm->dc_state <= DC_IDLE) { |
| /* |
| * ->dc_state will be DC_DISABLED if DC was disabled |
| * via GBMS_PROP_CHARGE_DISABLE(1) of from other |
| * conditions such as taper control. |
| */ |
| if (gcpm->dc_state == DC_DISABLED) |
| gcpm->dc_state = DC_IDLE; |
| |
| pr_info("%s: ChargeDisable value=%d dc_index=%d dc_state=%d\n", |
| __func__, pval->intval, gcpm->dc_index, gcpm->dc_state); |
| |
| gcpm_pps_online(gcpm); |
| ta_check = true; |
| } |
| |
| break; |
| case POWER_SUPPLY_PROP_ONLINE: |
| pr_info("%s: ONLINE value=%d dc_index=%d dc_state=%d\n", |
| __func__, pval->intval, gcpm->dc_index, |
| gcpm->dc_state); |
| ta_check = true; |
| break; |
| |
| case POWER_SUPPLY_PROP_VOLTAGE_MAX: |
| psp = POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX; |
| /* compat, fall through */ |
| case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: |
| ta_check = gcpm->fv_uv != pval->intval; |
| gcpm->fv_uv = pval->intval; |
| break; |
| |
| /* |
| * from google_charger (usually) with demand adjusted by classic |
| * thermal engine and/or special charging profiles. |
| * The MDIS vote on MSC_FCC is disabled by the thermal |
| * |
| * Used by the select logic to determine the best charging strategy and |
| * either routed to the main-charger directly or voted on GCPM_FCC. |
| */ |
| case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: |
| route = !gcpm_chg_is_cp_active(gcpm); |
| ta_check = gcpm->cc_max != pval->intval; |
| pr_debug("%s: route=%d ta_check=%d cc_max=%d->%d dc_index=%d\n", |
| __func__, route, ta_check, gcpm->cc_max, pval->intval, |
| gcpm->dc_index); |
| gcpm->cc_max = pval->intval; |
| break; |
| |
| /* just route to the active charger */ |
| default: |
| break; |
| } |
| |
| /* used only for debug */ |
| if (gcpm->new_dc_limit) { |
| gcpm->new_dc_limit = false; |
| ta_check = true; |
| } |
| |
| /* |
| * ta_check is set when the charging parameters change (cc_max, fv_uv) |
| * when changing the online state, in taper control and when charging |
| * is disabled. this code triggers the logic that selects DC charging |
| * or that causes charging to switch back the main charger. |
| */ |
| if (gcpm->dc_init_complete && ta_check) { |
| const bool was_dc = gcpm_is_dc(gcpm, gcpm->dc_index); |
| |
| /* |
| * Synchronous! might kick off gcpm_pps_wlc_dc_work to negotiate |
| * DC charging. -EAGAIN will cause this code to be called again. |
| * NOTE: gcpm_chg_select_logic() might change gcpm->dc_index |
| */ |
| ret = gcpm_chg_select_logic(gcpm); |
| if (ret == -EAGAIN) { |
| const int interval = 5; /* seconds */ |
| |
| /* let the setting go through but */ |
| mod_delayed_work(system_wq, &gcpm->select_work, |
| msecs_to_jiffies(interval * 1000)); |
| } |
| |
| /* |
| * Do not route while switching from DC to non DC because |
| * the DC charger might get the wrong limits. |
| * NOTE: gcpm_pps_wlc_dc_work() will configure the new charger |
| * on start (or on stop/timeout) |
| */ |
| if (was_dc && !gcpm_is_dc(gcpm, gcpm->dc_index)) |
| route = false; |
| } |
| |
| /* route to active charger only when needed */ |
| if (!route) |
| goto done; |
| |
| chg_psy = gcpm_chg_get_active(gcpm); |
| if (chg_psy) { |
| /* replace the pval with dc_iin limit when DC is selected */ |
| ret = power_supply_set_property(chg_psy, psp, pval); |
| if (ret < 0 && ret != -EAGAIN) { |
| pr_err("cannot route prop=%d to %d:%s (%d)\n", psp, |
| gcpm->chg_psy_active, gcpm_psy_name(chg_psy), |
| ret); |
| } |
| } else { |
| pr_err("invalid active charger = %d for prop=%d\n", |
| gcpm->chg_psy_active, psp); |
| } |
| |
| done: |
| /* |
| * route==false when using CP and when transitioning OUT of it. |
| * Will disable CC_MAX vote on GCPM_FCC when/if the limit is routed |
| * to the main-charger. |
| */ |
| if (psp == POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX) |
| gcpm_update_gcpm_fcc(gcpm, "CC_MAX", gcpm->cc_max, !route); |
| |
| mutex_unlock(&gcpm->chg_psy_lock); |
| |
| /* the charger should not call into gcpm: this can change though */ |
| return ret; |
| } |
| |
| static int gcpm_psy_get_property(struct power_supply *psy, |
| enum power_supply_property psp, |
| union power_supply_propval *pval) |
| { |
| struct gcpm_drv *gcpm = power_supply_get_drvdata(psy); |
| union gbms_charger_state chg_state; |
| struct power_supply *chg_psy; |
| bool route = false; |
| int ret = 0; |
| |
| pm_runtime_get_sync(gcpm->device); |
| if (!gcpm->init_complete || !gcpm->resume_complete) { |
| pm_runtime_put_sync(gcpm->device); |
| return -EAGAIN; |
| } |
| pm_runtime_put_sync(gcpm->device); |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| chg_psy = gcpm_chg_get_active(gcpm); |
| if (!chg_psy) { |
| pr_err("invalid active charger = %d for prop=%d\n", |
| gcpm->chg_psy_active, psp); |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return -ENODEV; |
| } |
| |
| switch (psp) { |
| /* handle locally for now */ |
| case GBMS_PROP_CHARGE_CHARGER_STATE: |
| chg_state.v = gcpm_get_charger_state(gcpm, chg_psy); |
| gbms_propval_int64val(pval) = chg_state.v; |
| break; |
| |
| /* route to the active charger */ |
| default: |
| route = true; |
| break; |
| } |
| |
| if (route) |
| ret = power_supply_get_property(chg_psy, psp, pval); |
| |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return ret; |
| } |
| |
| static int gcpm_psy_is_writeable(struct power_supply *psy, |
| enum power_supply_property psp) |
| { |
| switch (psp) { |
| case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: |
| case POWER_SUPPLY_PROP_VOLTAGE_MAX: |
| case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: |
| case POWER_SUPPLY_PROP_CURRENT_MAX: |
| case GBMS_PROP_CHARGE_DISABLE: |
| case GBMS_PROP_TAPER_CONTROL: |
| return 1; |
| default: |
| break; |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * TODO: POWER_SUPPLY_PROP_RERUN_AICL, POWER_SUPPLY_PROP_TEMP |
| */ |
| static enum power_supply_property gcpm_psy_properties[] = { |
| POWER_SUPPLY_PROP_ONLINE, |
| POWER_SUPPLY_PROP_PRESENT, |
| POWER_SUPPLY_PROP_CURRENT_NOW, |
| /* pixel battery management subsystem */ |
| POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX, /* cc_max */ |
| POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX, /* fv_uv */ |
| POWER_SUPPLY_PROP_CHARGE_TYPE, |
| POWER_SUPPLY_PROP_CURRENT_MAX, /* input current limit */ |
| POWER_SUPPLY_PROP_VOLTAGE_MAX, /* set float voltage, compat */ |
| POWER_SUPPLY_PROP_STATUS, |
| }; |
| |
| static struct power_supply_desc gcpm_psy_desc = { |
| .name = "gcpm", |
| .type = POWER_SUPPLY_TYPE_UNKNOWN, |
| .get_property = gcpm_psy_get_property, |
| .set_property = gcpm_psy_set_property, |
| .property_is_writeable = gcpm_psy_is_writeable, |
| .properties = gcpm_psy_properties, |
| .num_properties = ARRAY_SIZE(gcpm_psy_properties), |
| }; |
| |
| #define gcpm_psy_changed_tickle_pps(gcpm) \ |
| ((gcpm)->dc_state == DC_PASSTHROUGH || (gcpm)->dc_state == DC_RUNNING) |
| |
| static int gcpm_psy_changed(struct notifier_block *nb, unsigned long action, |
| void *data) |
| { |
| struct gcpm_drv *gcpm = container_of(nb, struct gcpm_drv, chg_nb); |
| const int index = gcpm->chg_psy_active; |
| struct power_supply *psy = data; |
| bool tickle_pps_work = false; |
| |
| if (index == -1) |
| return NOTIFY_OK; |
| |
| if ((action != PSY_EVENT_PROP_CHANGED) || |
| (psy == NULL) || (psy->desc == NULL) || (psy->desc->name == NULL)) |
| return NOTIFY_OK; |
| |
| if (strcmp(psy->desc->name, gcpm->chg_psy_names[index]) == 0) { |
| /* route upstream when the charger active and found */ |
| if (gcpm->chg_psy_avail[index]) |
| power_supply_changed(gcpm->psy); |
| |
| tickle_pps_work = gcpm_psy_changed_tickle_pps(gcpm); |
| } else if (strcmp(psy->desc->name, gcpm->chg_psy_names[0]) == 0) { |
| /* possibly JEITA or other violation, check PPS */ |
| tickle_pps_work = gcpm_psy_changed_tickle_pps(gcpm); |
| } else if (gcpm->tcpm_psy_name && |
| !strcmp(psy->desc->name, gcpm->tcpm_psy_name)) { |
| |
| /* from tcpm source (even if not selected) */ |
| tickle_pps_work = gcpm_psy_changed_tickle_pps(gcpm); |
| } else if (gcpm->wlc_dc_name && |
| !strcmp(psy->desc->name, gcpm->wlc_dc_name)) { |
| |
| /* from wc source (even if not selected) */ |
| tickle_pps_work = gcpm_psy_changed_tickle_pps(gcpm); |
| } |
| |
| /* should tickle the PPS loop only when is running */ |
| if (tickle_pps_work) |
| mod_delayed_work(system_wq, &gcpm->pps_work, 0); |
| |
| return NOTIFY_OK; |
| } |
| |
| static ssize_t dc_limit_demand_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| struct gcpm_drv *gcpm = dev_get_drvdata(dev); |
| |
| return scnprintf(buf, PAGE_SIZE, "%d\n", gcpm->dc_limit_demand); |
| } |
| static ssize_t dc_limit_demand_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct gcpm_drv *gcpm = dev_get_drvdata(dev); |
| int ret = 0; |
| u32 val; |
| |
| ret = kstrtou32(buf, 0, &val); |
| if (ret < 0) |
| return ret; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| if (gcpm->dc_limit_demand != val) { |
| gcpm->dc_limit_demand = val; |
| gcpm->new_dc_limit = true; |
| } |
| |
| mutex_unlock(&gcpm->chg_psy_lock); |
| |
| return count; |
| } |
| static DEVICE_ATTR_RW(dc_limit_demand); |
| |
| static ssize_t dc_limit_vbatt_max_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| struct gcpm_drv *gcpm = dev_get_drvdata(dev); |
| |
| return scnprintf(buf, PAGE_SIZE, "%d\n", gcpm->dc_limit_vbatt_max); |
| } |
| static ssize_t dc_limit_vbatt_max_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct gcpm_drv *gcpm = dev_get_drvdata(dev); |
| int ret = 0; |
| u32 val; |
| |
| ret = kstrtou32(buf, 0, &val); |
| if (ret < 0) |
| return ret; |
| |
| gcpm->dc_limit_vbatt_max = val; |
| |
| return count; |
| } |
| static DEVICE_ATTR_RW(dc_limit_vbatt_max); |
| |
| static ssize_t dc_limit_vbatt_min_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| struct gcpm_drv *gcpm = dev_get_drvdata(dev); |
| |
| return scnprintf(buf, PAGE_SIZE, "%d\n", gcpm->dc_limit_vbatt_min); |
| } |
| static ssize_t dc_limit_vbatt_min_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct gcpm_drv *gcpm = dev_get_drvdata(dev); |
| int ret = 0; |
| u32 val; |
| |
| ret = kstrtou32(buf, 0, &val); |
| if (ret < 0) |
| return ret; |
| |
| gcpm->dc_limit_vbatt_min = val; |
| |
| return count; |
| } |
| static DEVICE_ATTR_RW(dc_limit_vbatt_min); |
| |
| static ssize_t dc_ctl_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| struct gcpm_drv *gcpm = dev_get_drvdata(dev); |
| |
| return scnprintf(buf, PAGE_SIZE, "%d\n", gcpm->dc_ctl); |
| } |
| |
| static ssize_t dc_ctl_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct gcpm_drv *gcpm = dev_get_drvdata(dev); |
| int ret = 0, val; |
| |
| ret = kstrtoint(buf, 0, &val); |
| if (ret < 0) |
| return ret; |
| |
| /* |
| * 0: enable both (Default) |
| * 1: disable wired-DC |
| * 2: disable wireless-DC |
| * 3: disable both |
| */ |
| switch (val) { |
| case GCPM_DC_CTL_DEFAULT: |
| case GCPM_DC_CTL_DISABLE_WIRED: |
| case GCPM_DC_CTL_DISABLE_WIRELESS: |
| case GCPM_DC_CTL_DISABLE_BOTH: |
| gcpm->dc_ctl = val; |
| break; |
| default: |
| return -EINVAL; |
| }; |
| |
| return count; |
| } |
| static DEVICE_ATTR_RW(dc_ctl); |
| |
| static ssize_t thermal_mdis_fan_alarm_show(struct device *dev, |
| struct device_attribute *attr, |
| char *buf) |
| { |
| struct gcpm_drv *gcpm = dev_get_drvdata(dev); |
| |
| return scnprintf(buf, PAGE_SIZE, "%d\n", gcpm->thermal_device.therm_fan_alarm_level); |
| } |
| |
| static ssize_t thermal_mdis_fan_alarm_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| struct gcpm_drv *gcpm = dev_get_drvdata(dev); |
| int ret = 0; |
| u32 value; |
| |
| ret = kstrtou32(buf, 0, &value); |
| if (ret < 0) |
| return ret; |
| |
| if (value <= gcpm->thermal_device.thermal_levels) |
| gcpm->thermal_device.therm_fan_alarm_level = value; |
| |
| return count; |
| } |
| static DEVICE_ATTR_RW(thermal_mdis_fan_alarm); |
| |
| /* ------------------------------------------------------------------------ */ |
| |
| static int gcpm_get_max_charge_cntl_limit(struct thermal_cooling_device *tcd, |
| unsigned long *lvl) |
| { |
| struct mdis_thermal_device *tdev = tcd->devdata; |
| |
| *lvl = tdev->thermal_levels; |
| return 0; |
| } |
| |
| static int gcpm_get_cur_charge_cntl_limit(struct thermal_cooling_device *tcd, |
| unsigned long *lvl) |
| { |
| struct mdis_thermal_device *tdev = tcd->devdata; |
| |
| *lvl = tdev->current_level; |
| return 0; |
| } |
| |
| #define FAN_MDIS_ALARM_DEFAULT 3 |
| static int fan_get_level(struct mdis_thermal_device *tdev) |
| { |
| int fan_level = FAN_LVL_UNKNOWN; |
| |
| if (tdev->current_level <= 0) |
| fan_level = FAN_LVL_NOT_CARE; |
| else if (tdev->current_level >= tdev->therm_fan_alarm_level) |
| fan_level = FAN_LVL_ALARM; |
| else |
| fan_level = FAN_LVL_MED; |
| |
| return fan_level; |
| } |
| |
| static int gcpm_mdis_update_fan(struct gcpm_drv *gcpm) |
| { |
| int ret = 0; |
| |
| if (!gcpm->fan_level_votable) |
| gcpm->fan_level_votable = gvotable_election_get_handle(VOTABLE_FAN_LEVEL); |
| |
| if (gcpm->fan_level_votable) { |
| const int level = fan_get_level(&gcpm->thermal_device); |
| |
| ret = gvotable_cast_int_vote(gcpm->fan_level_votable, "THERMAL_MDIS", |
| level, true); |
| if (ret < 0) |
| pr_err("%s: cannot update fan level (%d)", __func__, ret); |
| } |
| |
| return ret; |
| } |
| |
| static inline int mdis_cast_vote(struct gvotable_election *el, int vote, bool enabled) |
| { |
| int ret = 0; |
| |
| if (enabled) |
| ret = gvotable_cast_int_vote(el, "MDIS", vote, true); |
| else if (vote >= 0) |
| ret = gvotable_cast_int_vote(el, "MDIS", vote, false); |
| else |
| gvotable_recast_ballot(el, "MDIS", false); |
| |
| return ret; |
| } |
| |
| static int mdis_set_wlc_online(struct gcpm_drv *gcpm) |
| { |
| struct power_supply *wlc_psy = gcpm->wlc_pps_data.pps_psy; |
| union power_supply_propval pval; |
| int ret; |
| |
| if (!wlc_psy) |
| return PPS_PSY_OFFLINE; |
| |
| ret = power_supply_get_property(wlc_psy, POWER_SUPPLY_PROP_ONLINE, &pval); |
| if (ret < 0 || pval.intval == PPS_PSY_OFFLINE) { |
| |
| pval.intval = PPS_PSY_FIXED_ONLINE; |
| ret = power_supply_set_property(wlc_psy, POWER_SUPPLY_PROP_ONLINE, |
| &pval); |
| if (ret < 0) |
| return ret; |
| } |
| |
| return pval.intval; |
| } |
| |
| /* |
| * A negative msc_fcc, dc_icl or cp_fcc disables the MDIS vote on the |
| * corresponding source. |
| * cp_fcc=0 re-enable the MDIS votes on MSC_FCC and DC_ICL and forces the |
| * transition to MW charging when/if using the charge pump (in this case |
| * charging will stop if MSC_FCC/DC_ICL are zero). |
| * |
| * needs mutex_unlock(&gcpm->chg_psy_lock); |
| */ |
| static int gcpm_mdis_update_limits(struct gcpm_drv *gcpm, int msc_fcc, |
| int dc_icl, int cp_fcc) |
| { |
| struct gvotable_election *dc_icl_votable; |
| struct gvotable_election *fcc_votable; |
| struct gvotable_election *cp_votable; |
| int ret; |
| |
| pr_info("MSC_MDIS msc_fcc=%d dc_icl=%d cp_fcc=%d\n", |
| msc_fcc, dc_icl, cp_fcc); |
| |
| cp_votable = gcpm_get_cp_votable(gcpm); |
| dc_icl_votable = gcpm_get_dc_icl_votable(gcpm); |
| |
| /* |
| * set (or reset) the MDIS limit for CP. |
| * The callback for GCPM_FCC needs to be locked. |
| */ |
| if (cp_fcc != 0 && cp_votable) { |
| ret = mdis_cast_vote(cp_votable, cp_fcc, cp_fcc > 0); |
| if (ret < 0) |
| dev_err(gcpm->device, "MDIS: vote %d on CP failed (%d)\n", |
| cp_fcc, ret); |
| } |
| |
| /* |
| * set (or reset) the MDIS limit for MSC_FCC. |
| * Turns off the main-charger from the charging loop in google_charger |
| * but will not be able to restart charging if/when the charging loop |
| * is not running (MSC_FCC might not have a callback that cause a |
| * respin of the usecase state machine) |
| * NOTE: this limit is enabled only when CP is not enabled |
| */ |
| fcc_votable = gcpm_get_fcc_votable(gcpm); |
| if (fcc_votable) { |
| ret = mdis_cast_vote(fcc_votable, msc_fcc, msc_fcc >= 0 && cp_fcc == 0); |
| if (ret < 0) |
| dev_err(gcpm->device, "MDIS: vote %d on MSC_FCC failed (%d)\n", |
| msc_fcc, ret); |
| } |
| |
| /* |
| * set (or reset) the MDIS limit for DC_ICL. |
| * NOTE: Can vote on DC_ICL even when using CP. |
| */ |
| if (dc_icl != 0 && dc_icl_votable) { |
| int wlc_state; |
| |
| /* need to set online WLC if not online */ |
| wlc_state = mdis_set_wlc_online(gcpm); |
| if (wlc_state == PPS_PSY_OFFLINE) |
| dev_err(gcpm->device, "MDIS: WLC offine\n"); |
| |
| /* turning ON after critical level for WLC is complicated */ |
| ret = mdis_cast_vote(dc_icl_votable, dc_icl, dc_icl > 0); |
| if (ret < 0) |
| dev_err(gcpm->device, "MDIS: vote %d on DC_ICL failed (%d)\n", |
| dc_icl, ret); |
| } |
| |
| /* adjust limit for RTX */ |
| if (!gcpm->tx_icl_votable) |
| gcpm->tx_icl_votable = gvotable_election_get_handle("TX_ICL"); |
| if (gcpm->tx_icl_votable) |
| gvotable_cast_int_vote(gcpm->tx_icl_votable, "MDIS", 0, dc_icl == 0); |
| |
| /* |
| * turns off the CP and will revert to main. |
| * NOTE: The limit for main charger MSC_FCC is updated above. |
| */ |
| if (cp_fcc == 0 && cp_votable) { |
| ret = mdis_cast_vote(cp_votable, 0, true); |
| if (ret < 0) |
| dev_err(gcpm->device, "MDIS: vote %d on CP failed (%d)\n", |
| cp_fcc, ret); |
| } |
| |
| /* turning off wireless charging equires disabling the wireless IC */ |
| if (dc_icl == 0 && dc_icl_votable) { |
| ret = mdis_cast_vote(dc_icl_votable, 0, true); |
| if (ret < 0) |
| dev_err(gcpm->device, "vote %d on DC_ICL failed (%d)\n", |
| dc_icl, ret); |
| } |
| |
| /* one or more might fail, consider retries */ |
| return 0; |
| } |
| |
| |
| /* max dissipation themal level: apply the limit */ |
| static int gcpm_set_mdis_charge_cntl_limit(struct thermal_cooling_device *tcd, |
| unsigned long lvl) |
| { |
| struct mdis_thermal_device *tdev = tcd->devdata; |
| struct gcpm_drv *gcpm = tdev->gcpm; |
| int online = 0, in_idx = -1; |
| int msc_fcc, dc_icl, cp_fcc, ret; |
| bool mdis_crit_lvl; |
| |
| if (tdev->thermal_levels <= 0 || lvl < 0 || lvl > tdev->thermal_levels) |
| return -EINVAL; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| |
| dev_dbg(gcpm->device, "MSC_THERM_MDIS lvl=%d->%d\n", tdev->current_level, (int)lvl); |
| |
| tdev->current_level = lvl; |
| mdis_crit_lvl = lvl == tdev->thermal_levels || tdev->thermal_mitigation[lvl] == 0; |
| if (mdis_crit_lvl) { |
| msc_fcc = dc_icl = cp_fcc = 0; |
| gcpm->cp_fcc_hold_limit = gcpm_chg_select_check_cp_limit(gcpm); |
| gcpm->cp_fcc_hold = true; |
| } else if (tdev->current_level == 0) { |
| msc_fcc = dc_icl = cp_fcc = -1; |
| /* mdis callback will clear hold and re-evaluate PPS */ |
| gcpm->cp_fcc_hold_limit = -1; |
| } else { |
| int cp_min = -1; |
| |
| /* 0 always is the main-charger */ |
| dc_icl = gcpm->mdis_out_limits[0][lvl + tdev->thermal_levels]; |
| msc_fcc = gcpm->mdis_out_limits[0][lvl]; |
| |
| /* |
| * cp_fcc limit is routed to DC when DC is selected or ignored. |
| * the code in gcpm_psy_set_property() uses cp_fcc and cc_max |
| * to determine when to swich source. |
| */ |
| in_idx = gcpm_mdis_match_cp_source(gcpm, &online); |
| if (in_idx < 0 || online != PPS_PSY_PROG_ONLINE) { |
| /* |
| * this happens when none of the sources are online |
| * or when not using the CP. It CAN happen when we |
| * resume after the thermal engine has shut this down. |
| * Forces cp_fcc to 0 to apply dc_icl and msc_fcc. |
| */ |
| cp_fcc = 0; |
| |
| /* forces wlc-overrides-fcc when wireless charging */ |
| if (online && gcpm_mdis_in_is_wireless(gcpm, in_idx)) |
| msc_fcc = -1; |
| } else if (gcpm_mdis_in_is_wireless(gcpm, in_idx)) { |
| /* WLC_CP use the charge pump with wireless charging */ |
| cp_fcc = gcpm->mdis_out_limits[1][lvl + tdev->thermal_levels]; |
| |
| if (gcpm->dc_limit_cc_min_wlc >= 0) |
| cp_min = gcpm->dc_limit_cc_min_wlc; |
| else if (gcpm->dc_limit_cc_min >= 0) |
| cp_min = gcpm->dc_limit_cc_min; |
| |
| /* |
| * forces wlc-overrides-fcc when wireless charging |
| * Reset only in PROG_ONLINE to allow transitioning |
| * OUT of WLC_DC when the charging current falls |
| * under the DC limit. |
| */ |
| msc_fcc = -1; |
| } else { |
| /* PPS_CP use the charge pump with TCPM */ |
| cp_fcc = gcpm->mdis_out_limits[1][lvl]; |
| if (gcpm->dc_limit_cc_min >= 0) |
| cp_min = gcpm->dc_limit_cc_min; |
| } |
| |
| /* |
| * validate the cp limit against cp_min and disable CP |
| * with hold if the new limit is under it. |
| * NOTE: there might be a corner case when the MSC_FCC or the |
| * DC_ICL limit doesn't change after re-enabling the vote. |
| */ |
| if (cp_min == -1) { |
| pr_debug("MSC_MDIS cp_fcc_hold_limit:%d->-1 cp_fcc=%d cp_min=%d\n", |
| gcpm->cp_fcc_hold_limit, cp_fcc, cp_min); |
| } else if (cp_fcc > cp_min) { |
| /* mdis callback will clear hold and re-evaluate PPS */ |
| gcpm->cp_fcc_hold_limit = -1; |
| pr_debug("MSC_MDIS cp_fcc_hold_limit:%d->-1 cp_fcc=%d cp_min=%d\n", |
| gcpm->cp_fcc_hold_limit, cp_fcc, cp_min); |
| } else if (cp_fcc <= cp_min) { |
| /* |
| * setting ->cp_fcc_hold_limit to 0 select the |
| main-charger in gcpm_chg_select_by_demand(). |
| */ |
| gcpm->cp_fcc_hold_limit = gcpm_mdis_in_is_wireless(gcpm, in_idx) ? |
| 0 : cp_min; |
| gcpm->cp_fcc_hold = true; |
| |
| pr_debug("MSC_MDIS cp_fcc:%d->0 hold_limit=%d cp_min=%d\n", |
| cp_fcc, gcpm->cp_fcc_hold_limit, cp_min); |
| cp_fcc = 0; |
| } |
| } |
| |
| dev_info(gcpm->device, |
| "MSC_THERM_MDIS lvl=%lu in_idx=%d online=%d cp_fcc=%d hold=%d, hold_limit=%d\n", |
| lvl, in_idx, online, cp_fcc, gcpm->cp_fcc_hold, |
| gcpm->cp_fcc_hold_limit); |
| |
| ret = gvotable_cast_int_vote(gcpm->dc_chg_avail_votable, REASON_MDIS, |
| !mdis_crit_lvl, 1); |
| if (ret < 0) |
| dev_err(gcpm->device, "Unable to cast vote for DC Chg avail (%d)\n", ret); |
| /* |
| * this might be in the callback for mdis_votable |
| * . cp_fcc == 0 will apply msc_fcc, dc_icl and must cause the |
| * transition from CP to MW |
| * . cp_fcc < 0 it only removes the MDIS limit on CP charging |
| * . msc_fcc = -1 when charging from dc_icl (wlc-overrides-fcc) |
| */ |
| ret = gcpm_mdis_update_limits(gcpm, msc_fcc, dc_icl, cp_fcc); |
| if (ret < 0) |
| pr_err("%s: cannot update limits (%d)", __func__, ret); |
| |
| /* fix the disable, run another charging loop */ |
| if (gcpm->mdis_votable) { |
| ret = gvotable_cast_int_vote(gcpm->mdis_votable, "MDIS", |
| lvl, lvl >= 0); |
| if (ret < 0) |
| pr_err("%s: cannot update MDIS level (%d)", __func__, ret); |
| |
| } |
| |
| mutex_unlock(&gcpm->chg_psy_lock); |
| |
| return 0; |
| } |
| |
| static ssize_t |
| state2power_table_show(struct device *dev, struct device_attribute *attr, char *buf) |
| { |
| struct thermal_cooling_device *tdev = to_cooling_device(dev); |
| struct mdis_thermal_device *mdev = tdev->devdata; |
| ssize_t count = 0; |
| int i; |
| |
| for (i = 0; i < mdev->thermal_levels; i++) { |
| const int budgetMw = mdev->thermal_mitigation[i] / 1000; |
| |
| count += sysfs_emit_at(buf, count, "%u ", budgetMw); |
| } |
| |
| /* b/231599097 add the implicit 0 at the end of the table */ |
| count += sysfs_emit_at(buf, count, "0\n"); |
| |
| return count; |
| } |
| |
| static DEVICE_ATTR_RO(state2power_table); |
| |
| static ssize_t |
| mdis_out_table_show(struct device *dev, struct device_attribute *attr, char *buf) |
| { |
| struct thermal_cooling_device *tdev = to_cooling_device(dev); |
| struct mdis_thermal_device *mdev = tdev->devdata; |
| struct gcpm_drv *gcpm = mdev->gcpm; |
| const int entries = mdev->thermal_levels * gcpm->mdis_in_count; |
| ssize_t count = 0; |
| int i, j; |
| |
| for (i = 0; i < gcpm->mdis_out_count; i++) { |
| |
| count += sysfs_emit_at(buf, count, "%d:", i); |
| |
| for (j = 0; j < entries; j++) { |
| const int limit = gcpm->mdis_out_limits[i][j]; |
| |
| count += sysfs_emit_at(buf, count, "%u ", limit); |
| } |
| |
| count += sysfs_emit_at(buf, count, "\n"); |
| } |
| |
| return count; |
| } |
| |
| static DEVICE_ATTR_RO(mdis_out_table); |
| |
| static const struct thermal_cooling_device_ops chg_mdis_tcd_ops = { |
| .get_max_state = gcpm_get_max_charge_cntl_limit, |
| .get_cur_state = gcpm_get_cur_charge_cntl_limit, |
| .set_cur_state = gcpm_set_mdis_charge_cntl_limit, |
| }; |
| |
| #ifdef CONFIG_DEBUG_FS |
| |
| static ssize_t mdis_tm_store(struct file *filp, const char __user *user_buf, |
| size_t count, loff_t *ppos) |
| { |
| struct gcpm_drv *gcpm = filp->private_data; |
| const int thermal_levels = gcpm->thermal_device.thermal_levels; |
| const int mem_size = count + 1; |
| char *str, *tmp, *saved_ptr; |
| unsigned long long value; |
| int ret, i; |
| |
| tmp = kzalloc(mem_size, GFP_KERNEL); |
| if (!tmp) |
| return -ENOMEM; |
| |
| ret = simple_write_to_buffer(tmp, mem_size, ppos, user_buf, count); |
| if (!ret) |
| goto error_done; |
| |
| for (saved_ptr = tmp, i = 0; i < thermal_levels; i++) { |
| str = strsep(&saved_ptr, " "); |
| if (!str) |
| goto error_done; |
| |
| ret = kstrtoull(str, 10, &value); |
| if (ret < 0) |
| goto error_done; |
| |
| gcpm->thermal_device.thermal_mitigation[i] = value * 1000; |
| } |
| |
| error_done: |
| kfree(tmp); |
| return count; |
| } |
| |
| DEBUG_ATTRIBUTE_WO(mdis_tm); |
| |
| static ssize_t mdis_out_store(struct file *filp, const char __user *user_buf, |
| size_t count, loff_t *ppos) |
| { |
| struct gcpm_drv *gcpm = filp->private_data; |
| const int levels = gcpm->thermal_device.thermal_levels; |
| const int mem_size = count + 1; |
| unsigned long long value, index; |
| char *str, *tmp, *saved_ptr; |
| int ret, i; |
| |
| tmp = kzalloc(mem_size, GFP_KERNEL); |
| if (!tmp) |
| return -ENOMEM; |
| |
| ret = simple_write_to_buffer(tmp, mem_size, ppos, user_buf, count); |
| if (!ret) |
| goto error_done; |
| |
| for (saved_ptr = tmp; true; ) { |
| |
| str = strsep(&saved_ptr, ":"); |
| if (!str) |
| goto error_done; |
| |
| ret = kstrtoull(str, 10, &index); |
| if (ret < 0) |
| goto error_done; |
| |
| if (index < 0 || index >= levels) |
| break; |
| |
| for (i = 0; i < levels * gcpm->mdis_in_count; i++) { |
| str = strsep(&saved_ptr, " "); |
| if (!str) |
| goto error_done; |
| |
| ret = kstrtoull(str, 10, &value); |
| if (ret < 0) |
| goto error_done; |
| |
| gcpm->mdis_out_limits[index][i] = value; |
| } |
| } |
| |
| error_done: |
| kfree(tmp); |
| return count; |
| } |
| |
| DEBUG_ATTRIBUTE_WO(mdis_out); |
| |
| #endif // CONFIG_DEBUG_FS |
| |
| /* ------------------------------------------------------------------------- */ |
| |
| static int mdis_out_init_sel_online(u32 *out_sel, int len, const struct gcpm_drv *gcpm) |
| { |
| static const char *name = "google,mdis-out-sel-online"; |
| struct device_node *node = gcpm->device->of_node; |
| int ret, count, byte_len; |
| |
| if (!of_find_property(node, name, &byte_len)) |
| return -ENOENT; |
| |
| count = byte_len / sizeof(u32); |
| if (count != len) |
| return -ERANGE; |
| |
| ret = of_property_read_u32_array(node, name, out_sel, count); |
| if (ret < 0) |
| return -EINVAL; |
| |
| return count; |
| } |
| |
| /* return the array and the len. Pass len = 0 to avoid check on length */ |
| static u32* gcpm_init_limits(const char *name, int *len, struct device *dev) |
| { |
| struct device_node *node = dev->of_node; |
| int ret, byte_len; |
| u32 *limits; |
| |
| if (!of_find_property(node, name, &byte_len)) |
| return ERR_PTR(-ENOENT); |
| |
| if (*len && (byte_len / sizeof(u32)) > *len) |
| return ERR_PTR(-EINVAL); |
| |
| limits = devm_kzalloc(dev, byte_len, GFP_KERNEL); |
| if (!limits) |
| return ERR_PTR(-ENOMEM); |
| |
| ret = of_property_read_u32_array(node, name, limits, byte_len / sizeof(u32)); |
| if (ret < 0) { |
| devm_kfree(dev, limits); |
| return ERR_PTR(-ERANGE); |
| } |
| |
| *len = byte_len / sizeof(u32); |
| return limits; |
| } |
| |
| /* ls /dev/thermal/cdev-by-name/ */ |
| static int gcpm_tdev_init(struct mdis_thermal_device *tdev, const char *name, |
| struct gcpm_drv *gcpm) |
| { |
| int levels = 0; |
| u32 *limits; |
| |
| mutex_init(&tdev->tdev_lock); |
| |
| limits = gcpm_init_limits(name, &levels, gcpm->device); |
| if (IS_ERR_OR_NULL(limits)) { |
| dev_err(gcpm->device, "Cannot create thermal device %s (%d)\n", |
| name, (int)PTR_ERR(limits)); |
| return PTR_ERR(limits); |
| } |
| |
| tdev->thermal_levels = levels; |
| tdev->thermal_mitigation = limits; |
| tdev->gcpm = gcpm; |
| return 0; |
| } |
| |
| static void chg_mdis_tdev_free(struct mdis_thermal_device *tdev, |
| struct gcpm_drv *gcpm) |
| { |
| devm_kfree(gcpm->device, tdev->thermal_mitigation); |
| tdev->thermal_mitigation = NULL; |
| } |
| |
| static int mdis_tdev_register(const char *of_name, const char *tcd_name, |
| struct mdis_thermal_device *ctdev, |
| const struct thermal_cooling_device_ops *ops) |
| { |
| struct device_node *cooling_node = NULL; |
| |
| cooling_node = of_find_node_by_name(NULL, of_name); |
| if (!cooling_node) { |
| pr_err("No %s OF node for cooling device\n", of_name); |
| return -EINVAL; |
| } |
| |
| ctdev->tcd = thermal_of_cooling_device_register(cooling_node, |
| tcd_name, |
| ctdev, |
| ops); |
| if (IS_ERR_OR_NULL(ctdev->tcd)) { |
| const long err = PTR_ERR(ctdev->tcd); |
| |
| pr_err("error registering %s cooling device (%ld)\n", tcd_name, err); |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * pick the adapter with the highest current under the budget |
| * needs mutex_lock(&gcpm->chg_psy_lock); |
| */ |
| static int gcpm_mdis_callback(struct gvotable_election *el, const char *reason, |
| void *value) |
| { |
| struct gcpm_drv *gcpm = gvotable_get_data(el); |
| struct mdis_thermal_device *tdev = &gcpm->thermal_device; |
| const int budget = (long)value; |
| bool trigger_select = budget != 0 && gcpm->cp_fcc_hold; |
| |
| pr_debug("MSC_MDIS callback lvl=%d budget=%d hold=%d cp_fcc_hold_limit=%d\n", |
| tdev->current_level, budget, gcpm->cp_fcc_hold, |
| gcpm->cp_fcc_hold_limit); |
| |
| /* |
| * the limit is cleared when charging current is greater that the limit. |
| * NOTE: Clearing the hold allows re-enabling PPS again. |
| */ |
| if (gcpm->cp_fcc_hold_limit == -1) |
| gcpm->cp_fcc_hold = false; |
| |
| /* |
| * gcpm->cp_fcc_hold is set when charging switched to main from DC |
| * due to the charging current falling under cc_mi n limit: clear the |
| * hold and give PPS a change if the limit is cleared. |
| * NOTE: need to |
| */ |
| if (trigger_select) { |
| int ret; |
| |
| ret = gcpm_chg_select_logic(gcpm); |
| if (ret == -EAGAIN) { |
| const int interval = 5; /* seconds */ |
| |
| /* let the setting go through but */ |
| mod_delayed_work(system_wq, &gcpm->select_work, |
| msecs_to_jiffies(interval * 1000)); |
| } |
| } |
| |
| gcpm_mdis_update_fan(gcpm); |
| |
| if (!gcpm->csi_status_votable) { |
| gcpm->csi_status_votable = gvotable_election_get_handle(VOTABLE_CSI_STATUS); |
| if (!gcpm->csi_status_votable) |
| return 0; |
| } |
| |
| /* this is a problem only when speed is affected */ |
| gvotable_cast_long_vote(gcpm->csi_status_votable, "CSI_STATUS_THERM_MDIS", |
| CSI_STATUS_System_Thermals, |
| tdev->current_level != 0); |
| |
| /* will trigger a power supply change now */ |
| power_supply_changed(gcpm->psy); |
| return 0; |
| } |
| |
| /* |
| * Callback for GCPM_FCC votable which routes the CP limit to the DC charger. |
| * The votable combines the CC_MAX limit from google_charger and the MDIS |
| * limit from the dissipation based cooling zone. |
| * NOTE: it might not benecessary if/when we route the MDIS vote to MSC_FCC |
| * directly. |
| * caller needs to hold mutex_lock(&gcpm->chg_psy_lock); |
| */ |
| static int gcpm_fcc_callback(struct gvotable_election *el, const char *reason, |
| void *value) |
| { |
| struct gcpm_drv *gcpm = gvotable_get_data(el); |
| const int limit = GVOTABLE_PTR_TO_INT(value); |
| struct power_supply *cp_psy; |
| int cp_min, ret; |
| |
| /* |
| * the current limit is changed, validate it against the min |
| * NOTE: this is also used to trigger select_work when restarting |
| * charging when coming off thermal mitigation. |
| */ |
| cp_min = gcpm_chg_select_check_cp_limit(gcpm); |
| if (cp_min != -1 && limit <= cp_min) { |
| pr_debug("MSC_GCPM_FCC: limit=%d reason=%s cpmin=%d trigger select\n", |
| limit, reason, cp_min); |
| mod_delayed_work(system_wq, &gcpm->select_work, 0); |
| return 0; |
| } |
| |
| /* route the vote to the CP when active */ |
| cp_psy = gcpm_chg_get_active_cp(gcpm); |
| if (!cp_psy) { |
| pr_debug("MSC_GCPM_FCC: not active limit=%d\n", limit); |
| return 0; |
| } |
| |
| ret = GPSY_SET_PROP(cp_psy, POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX, |
| limit); |
| if (ret < 0) |
| pr_err("MSC_GCPM_FCC: cannot apply cp_limit to cc_max=%d (%d)\n", |
| limit, ret); |
| |
| pr_debug("MSC_GCPM_FCC: applied new cp_limit=%d cp_min=%d ret=%d\n", |
| limit, cp_min, ret); |
| |
| return 0; |
| } |
| |
| #define INIT_DELAY_MS 100 |
| #define INIT_RETRY_DELAY_MS 1000 |
| #define GCPM_TCPM_PSY_MAX 2 |
| |
| /* Dissipation Based Thermal Management */ |
| static int gcpm_init_mdis(struct gcpm_drv *gcpm) |
| { |
| struct mdis_thermal_device *tdev = &gcpm->thermal_device; |
| int i, count, ret; |
| |
| ret = gcpm_tdev_init(tdev, "google,mdis-thermal-mitigation", gcpm); |
| if (ret < 0 || !tdev->thermal_levels) { |
| dev_err(gcpm->device, "No device (%d)\n", ret); |
| return -ENODEV; |
| } |
| |
| /* |
| * TODO: remove ->chg_psy_avail[] and ->chg_psy_count and rewrite to |
| * use ->mdis_out[]. |
| * NOTE: gcpm_init_work() needs to read into ->mdis_out[]. |
| */ |
| gcpm->mdis_out[0] = gcpm->chg_psy_avail[0]; |
| gcpm->mdis_out[1] = gcpm->chg_psy_avail[1]; |
| gcpm->mdis_out_count = gcpm->chg_psy_count; |
| |
| /* one for each out, call with #mdis out */ |
| count = mdis_out_init_sel_online(gcpm->mdis_out_sel, gcpm->chg_psy_count, gcpm); |
| if (count < 0) { |
| dev_err(gcpm->device, "mdis sel online (%d)\n", ret); |
| return -ERANGE; |
| } |
| |
| /* TODO: rewrite to parse handles in the device tree */ |
| gcpm->mdis_in[0] = gcpm->tcpm_psy; |
| gcpm->mdis_in[1] = gcpm->wlc_dc_psy; |
| gcpm->mdis_in_count = 2; |
| |
| /* |
| * max charging current for the each thermal level charger and |
| * mdis_out_sel mode. |
| */ |
| for (i = 0; i < gcpm->mdis_out_count; i++) { |
| int len = tdev->thermal_levels * gcpm->mdis_in_count; |
| char of_name[36]; |
| u32 *limits; |
| |
| scnprintf(of_name, sizeof(of_name), "google,mdis-out%d-limits", i); |
| |
| limits = gcpm_init_limits(of_name, &len, gcpm->device); |
| if (IS_ERR_OR_NULL(limits)) |
| return PTR_ERR(limits); |
| |
| gcpm->mdis_out_limits[i] = limits; |
| } |
| |
| ret = of_property_read_u32(gcpm->device->of_node, "google,mdis-fan-alarm-level", |
| &tdev->therm_fan_alarm_level); |
| if (ret < 0) |
| tdev->therm_fan_alarm_level = FAN_MDIS_ALARM_DEFAULT; |
| |
| /* mdis thermal engine uses this callback */ |
| gcpm->mdis_votable = |
| gvotable_create_int_election(NULL, gvotable_comparator_int_min, |
| gcpm_mdis_callback, gcpm); |
| if (IS_ERR_OR_NULL(gcpm->mdis_votable)) { |
| ret = PTR_ERR(gcpm->mdis_votable); |
| dev_err(gcpm->device, "no mdis votable (%d)\n", ret); |
| return ret; |
| } |
| |
| gvotable_set_default(gcpm->mdis_votable, (void *)-1); |
| gvotable_set_vote2str(gcpm->mdis_votable, gvotable_v2s_int); |
| gvotable_election_set_name(gcpm->mdis_votable, VOTABLE_MDIS); |
| |
| /* race with above */ |
| ret = mdis_tdev_register(MDIS_OF_CDEV_NAME, MDIS_CDEV_NAME, |
| tdev, &chg_mdis_tcd_ops); |
| if (ret) { |
| dev_err(gcpm->device, |
| "Couldn't register %s rc=%d\n", MDIS_OF_CDEV_NAME, ret); |
| |
| // Free the limits too! |
| chg_mdis_tdev_free(tdev, gcpm); |
| return -EINVAL; |
| } |
| |
| /* state and debug */ |
| ret = device_create_file(&tdev->tcd->device, &dev_attr_state2power_table); |
| if (ret) |
| dev_err(gcpm->device, "cound not create state table *(%d)\n", ret); |
| |
| ret = device_create_file(&tdev->tcd->device, &dev_attr_mdis_out_table); |
| if (ret) |
| dev_err(gcpm->device, "cound not create out table *(%d)\n", ret); |
| |
| if (!gcpm->debug_entry) |
| return 0; |
| |
| debugfs_create_file("state2power_table", 0644, gcpm->debug_entry, |
| gcpm, &mdis_tm_fops); |
| debugfs_create_file("mdis_out_table", 0644, gcpm->debug_entry, |
| gcpm, &mdis_out_fops); |
| |
| return 0; |
| } |
| |
| /* this can run */ |
| static void gcpm_init_work(struct work_struct *work) |
| { |
| struct gcpm_drv *gcpm = container_of(work, struct gcpm_drv, |
| init_work.work); |
| int i, found = 0, ret = 0; |
| bool dc_not_done; |
| |
| /* might run along set_property() */ |
| mutex_lock(&gcpm->chg_psy_lock); |
| |
| /* |
| * could call pps_init() in probe() and use lazy init for ->tcpm_psy |
| * when the device an APDO in the sink capabilities. |
| */ |
| if (gcpm->tcpm_phandle && !gcpm->tcpm_psy) { |
| struct power_supply *tcpm_psy; |
| |
| tcpm_psy = pps_get_tcpm_psy(gcpm->device->of_node, |
| GCPM_TCPM_PSY_MAX); |
| if (!IS_ERR_OR_NULL(tcpm_psy)) { |
| const char *name = tcpm_psy->desc->name; |
| |
| gcpm->tcpm_psy_name = name; |
| gcpm->tcpm_psy = tcpm_psy; |
| |
| /* PPS charging: needs an APDO */ |
| ret = pps_init(&gcpm->tcpm_pps_data, gcpm->device, |
| gcpm->tcpm_psy, "wired-pps"); |
| if (ret == 0 && gcpm->debug_entry) |
| pps_init_fs(&gcpm->tcpm_pps_data, gcpm->debug_entry); |
| if (ret < 0) { |
| pr_err("PPS init failure for %s (%d)\n", |
| name, ret); |
| } else { |
| gcpm->tcpm_pps_data.port_data = |
| power_supply_get_drvdata(tcpm_psy); |
| pps_init_state(&gcpm->tcpm_pps_data); |
| pps_set_logbuffer(&gcpm->tcpm_pps_data, gcpm->log); |
| pps_log(&gcpm->tcpm_pps_data, "TCPM_PPS for %s", gcpm->tcpm_psy_name); |
| } |
| |
| } else if (!tcpm_psy || !gcpm->log_psy_ratelimit) { |
| /* abort on an error */ |
| pr_warn("PPS not available for tcpm\n"); |
| gcpm->tcpm_phandle = 0; |
| } else { |
| pr_warn("tcpm power supply not found, retrying... ret:%d\n", |
| ret); |
| gcpm->log_psy_ratelimit--; |
| } |
| |
| } |
| |
| /* TODO: lookup by phandle as the dude above */ |
| if (gcpm->wlc_dc_name && !gcpm->wlc_dc_psy) { |
| struct power_supply *wlc_dc_psy; |
| |
| wlc_dc_psy = power_supply_get_by_name(gcpm->wlc_dc_name); |
| if (wlc_dc_psy) { |
| const char *name = gcpm->wlc_dc_name; |
| |
| gcpm->wlc_dc_psy = wlc_dc_psy; |
| |
| /* PPS charging: needs an APDO */ |
| ret = pps_init(&gcpm->wlc_pps_data, gcpm->device, |
| gcpm->wlc_dc_psy, "wireless-pps"); |
| if (ret == 0 && gcpm->debug_entry) |
| pps_init_fs(&gcpm->wlc_pps_data, gcpm->debug_entry); |
| if (ret < 0) { |
| pr_err("PPS init failure for %s (%d)\n", |
| name, ret); |
| } else { |
| gcpm->wlc_pps_data.port_data = NULL; |
| pps_init_state(&gcpm->wlc_pps_data); |
| pps_set_logbuffer(&gcpm->wlc_pps_data, gcpm->log); |
| pps_log(&gcpm->wlc_pps_data, "WLC_PPS for %s", gcpm->wlc_dc_name); |
| } |
| |
| } else if (!gcpm->log_psy_ratelimit) { |
| /* give up if wlc_dc_psy return an error */ |
| pr_warn("PPS not available for %s\n", gcpm->wlc_dc_name); |
| gcpm->wlc_dc_name = NULL; |
| } else { |
| pr_warn("%s power supply not found, retrying... ret:%d\n", |
| gcpm->wlc_dc_name, ret); |
| gcpm->log_psy_ratelimit--; |
| } |
| } |
| |
| /* default is index 0 */ |
| for (i = 0; i < gcpm->chg_psy_count; i++) { |
| if (!gcpm->chg_psy_avail[i]) { |
| const char *name = gcpm->chg_psy_names[i]; |
| |
| gcpm->chg_psy_avail[i] = power_supply_get_by_name(name); |
| if (gcpm->chg_psy_avail[i]) |
| pr_info("init_work found %d:%s\n", i, name); |
| } |
| |
| found += !!gcpm->chg_psy_avail[i]; |
| } |
| |
| /* sort of done when we have the primary, make it online */ |
| if (gcpm->chg_psy_avail[0] && !gcpm->init_complete) { |
| struct power_supply *def_psy = gcpm->chg_psy_avail[0]; |
| |
| gcpm->chg_nb.notifier_call = gcpm_psy_changed; |
| ret = power_supply_reg_notifier(&gcpm->chg_nb); |
| if (ret < 0) |
| pr_err("%s: no ps notifier, ret=%d\n", __func__, ret); |
| |
| ret = gcpm_enable_default(gcpm); |
| if (ret < 0) |
| pr_err("%s: default %s not online, ret=%d\n", __func__, |
| gcpm_psy_name(def_psy), ret); |
| |
| /* this is the reason why we need a lock here */ |
| gcpm->resume_complete = true; |
| gcpm->init_complete = true; |
| } |
| |
| /* keep looking for late arrivals, TCPM and WLC if set */ |
| if (found == gcpm->chg_psy_count) |
| gcpm->chg_psy_retries = 0; |
| else if (gcpm->chg_psy_retries) |
| gcpm->chg_psy_retries--; |
| |
| dc_not_done = (gcpm->tcpm_phandle && !gcpm->tcpm_psy) || |
| (gcpm->wlc_dc_name && !gcpm->wlc_dc_psy); |
| |
| pr_warn("%s retries=%d dc_not_done=%d tcpm_ok=%d wlc_ok=%d\n", |
| __func__, gcpm->chg_psy_retries, dc_not_done, |
| (!gcpm->tcpm_phandle || gcpm->tcpm_psy), |
| (!gcpm->wlc_dc_name || gcpm->wlc_dc_psy)); |
| |
| if (gcpm->chg_psy_retries || dc_not_done) { |
| const unsigned long jif = msecs_to_jiffies(INIT_RETRY_DELAY_MS); |
| |
| schedule_delayed_work(&gcpm->init_work, jif); |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return; |
| } |
| |
| pr_info("google_cpm init_work done %d/%d pps=%d wlc_dc=%d\n", |
| found, gcpm->chg_psy_count, |
| !!gcpm->tcpm_psy, !!gcpm->wlc_dc_psy); |
| |
| ret = gcpm_init_mdis(gcpm); |
| if (ret < 0) |
| pr_info("google_cpm: no mdis engine (%d)\n", ret); |
| |
| gcpm->dc_init_complete = true; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| |
| /* might run along set_property() */ |
| mod_delayed_work(system_wq, &gcpm->select_work, 0); |
| } |
| |
| /* ------------------------------------------------------------------------ */ |
| |
| static int gcpm_debug_get_active(void *data, u64 *val) |
| { |
| struct gcpm_drv *gcpm = data; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| *val = gcpm->dc_index; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return 0; |
| } |
| |
| static int gcpm_debug_set_active(void *data, u64 val) |
| { |
| struct gcpm_drv *gcpm = data; |
| int intval = (int)val; |
| |
| if (gcpm->force_active != -1 && val == gcpm->force_active) |
| intval = -1; |
| |
| pr_info("%s: val=%llu val=%lld intval=%d\n", __func__, val, val, intval); |
| |
| if (intval != -1 && (intval < 0 || intval >= gcpm->chg_psy_count)) |
| return -ERANGE; |
| if (intval != -1 && !gcpm_chg_get_charger(gcpm, intval)) |
| return -EINVAL; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| gcpm->force_active = intval; |
| mod_delayed_work(system_wq, &gcpm->select_work, 0); |
| mutex_unlock(&gcpm->chg_psy_lock); |
| |
| return 0; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(gcpm_debug_active_fops, gcpm_debug_get_active, |
| gcpm_debug_set_active, "%lld\n"); |
| |
| static int gcpm_debug_dc_limit_demand_show(void *data, u64 *val) |
| { |
| struct gcpm_drv *gcpm = data; |
| |
| *val = gcpm->dc_limit_demand; |
| return 0; |
| } |
| |
| static int gcpm_debug_dc_limit_demand_set(void *data, u64 val) |
| { |
| struct gcpm_drv *gcpm = data; |
| const int intval = val; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| if (gcpm->dc_limit_demand != intval) { |
| gcpm->dc_limit_demand = intval; |
| gcpm->new_dc_limit = true; |
| } |
| |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return 0; |
| } |
| |
| |
| DEFINE_SIMPLE_ATTRIBUTE(gcpm_debug_dc_limit_demand_fops, |
| gcpm_debug_dc_limit_demand_show, |
| gcpm_debug_dc_limit_demand_set, |
| "%llu\n"); |
| |
| |
| static int gcpm_debug_pps_stage_get(void *data, u64 *val) |
| { |
| struct gcpm_drv *gcpm = data; |
| struct pd_pps_data *pps_data; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| pps_data = gcpm_pps_data(gcpm); |
| if (pps_data) |
| *val = pps_data->stage; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return 0; |
| } |
| |
| static int gcpm_debug_pps_stage_set(void *data, u64 val) |
| { |
| struct gcpm_drv *gcpm = data; |
| const int intval = (int)val; |
| struct pd_pps_data *pps_data; |
| |
| if (intval < PPS_DISABLED || intval > PPS_ACTIVE) |
| return -EINVAL; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| pps_data = gcpm_pps_data(gcpm); |
| if (pps_data) |
| pps_data->stage = intval; |
| gcpm->force_pps = !pps_is_disabled(intval); |
| mod_delayed_work(system_wq, &gcpm->pps_work, 0); |
| mutex_unlock(&gcpm->chg_psy_lock); |
| |
| return 0; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(gcpm_debug_pps_stage_fops, gcpm_debug_pps_stage_get, |
| gcpm_debug_pps_stage_set, "%llu\n"); |
| |
| static int gcpm_debug_dc_state_get(void *data, u64 *val) |
| { |
| struct gcpm_drv *gcpm = data; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| *val = gcpm->dc_state; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return 0; |
| } |
| |
| static int gcpm_debug_dc_state_set(void *data, u64 val) |
| { |
| struct gcpm_drv *gcpm = data; |
| const int intval = (int)val; |
| |
| if (intval < DC_DISABLED || intval > DC_PASSTHROUGH) |
| return -EINVAL; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| gcpm->dc_state = intval; |
| mod_delayed_work(system_wq, &gcpm->select_work, 0); |
| mutex_unlock(&gcpm->chg_psy_lock); |
| |
| return 0; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(gcpm_debug_dc_state_fops, gcpm_debug_dc_state_get, |
| gcpm_debug_dc_state_set, "%llu\n"); |
| |
| |
| static int gcpm_debug_taper_ctl_get(void *data, u64 *val) |
| { |
| struct gcpm_drv *gcpm = data; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| *val = gcpm->taper_step; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return 0; |
| } |
| |
| static int gcpm_debug_taper_ctl_set(void *data, u64 val) |
| { |
| struct gcpm_drv *gcpm = data; |
| bool ta_check; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| |
| /* ta_check set when taper control changes value */ |
| ta_check = gcpm_taper_ctl(gcpm, val); |
| if (ta_check) |
| mod_delayed_work(system_wq, &gcpm->select_work, 0); |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return 0; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(gcpm_debug_taper_ctl_fops, gcpm_debug_taper_ctl_get, |
| gcpm_debug_taper_ctl_set, "%llu\n"); |
| |
| static int gcpm_debug_taper_step_fv_margin_get(void *data, u64 *val) |
| { |
| struct gcpm_drv *gcpm = data; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| *val = gcpm->taper_step_fv_margin; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return 0; |
| } |
| |
| static int gcpm_debug_taper_step_fv_margin_set(void *data, u64 val) |
| { |
| struct gcpm_drv *gcpm = data; |
| const int intval = (int)val; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| gcpm->taper_step_fv_margin = intval; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| |
| return 0; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(gcpm_debug_taper_step_fv_margin_fops, gcpm_debug_taper_step_fv_margin_get, |
| gcpm_debug_taper_step_fv_margin_set, "%llu\n"); |
| |
| static int gcpm_debug_taper_step_cc_step_get(void *data, u64 *val) |
| { |
| struct gcpm_drv *gcpm = data; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| *val = gcpm->taper_step_cc_step; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return 0; |
| } |
| |
| static int gcpm_debug_taper_step_cc_step_set(void *data, u64 val) |
| { |
| struct gcpm_drv *gcpm = data; |
| const int intval = (int)val; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| gcpm->taper_step_cc_step = intval; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| |
| return 0; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(gcpm_debug_taper_step_cc_step_fops, gcpm_debug_taper_step_cc_step_get, |
| gcpm_debug_taper_step_cc_step_set, "%llu\n"); |
| |
| static int gcpm_debug_taper_step_count_get(void *data, u64 *val) |
| { |
| struct gcpm_drv *gcpm = data; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| *val = gcpm->taper_step_count; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return 0; |
| } |
| |
| static int gcpm_debug_taper_step_count_set(void *data, u64 val) |
| { |
| struct gcpm_drv *gcpm = data; |
| const int intval = (int)val; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| gcpm->taper_step_count = intval; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| |
| return 0; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(gcpm_debug_taper_step_count_fops, gcpm_debug_taper_step_count_get, |
| gcpm_debug_taper_step_count_set, "%llu\n"); |
| |
| static int gcpm_debug_taper_step_grace_get(void *data, u64 *val) |
| { |
| struct gcpm_drv *gcpm = data; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| *val = gcpm->taper_step_grace; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return 0; |
| } |
| |
| static int gcpm_debug_taper_step_grace_set(void *data, u64 val) |
| { |
| struct gcpm_drv *gcpm = data; |
| const int intval = (int)val; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| gcpm->taper_step_grace = intval; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| |
| return 0; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(gcpm_debug_taper_step_grace_fops, gcpm_debug_taper_step_grace_get, |
| gcpm_debug_taper_step_grace_set, "%llu\n"); |
| |
| static int gcpm_debug_taper_step_voltage_get(void *data, u64 *val) |
| { |
| struct gcpm_drv *gcpm = data; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| *val = gcpm->taper_step_voltage; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return 0; |
| } |
| |
| static int gcpm_debug_taper_step_voltage_set(void *data, u64 val) |
| { |
| struct gcpm_drv *gcpm = data; |
| const int intval = (int)val; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| gcpm->taper_step_voltage = intval; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| |
| return 0; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(gcpm_debug_taper_step_voltage_fops, gcpm_debug_taper_step_voltage_get, |
| gcpm_debug_taper_step_voltage_set, "%llu\n"); |
| |
| static int gcpm_debug_taper_step_current_get(void *data, u64 *val) |
| { |
| struct gcpm_drv *gcpm = data; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| *val = gcpm->taper_step_current; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return 0; |
| } |
| |
| static int gcpm_debug_taper_step_current_set(void *data, u64 val) |
| { |
| struct gcpm_drv *gcpm = data; |
| const int intval = (int)val; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| gcpm->taper_step_current = intval; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| |
| return 0; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(gcpm_debug_taper_step_current_fops, gcpm_debug_taper_step_current_get, |
| gcpm_debug_taper_step_current_set, "%llu\n"); |
| |
| static int gcpm_debug_taper_step_interval_get(void *data, u64 *val) |
| { |
| struct gcpm_drv *gcpm = data; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| *val = gcpm->taper_step_interval; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return 0; |
| } |
| |
| static int gcpm_debug_taper_step_interval_set(void *data, u64 val) |
| { |
| struct gcpm_drv *gcpm = data; |
| const int intval = (int)val; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| gcpm->taper_step_interval = intval; |
| mutex_unlock(&gcpm->chg_psy_lock); |
| |
| return 0; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(gcpm_debug_taper_step_interval_fops, gcpm_debug_taper_step_interval_get, |
| gcpm_debug_taper_step_interval_set, "%llu\n"); |
| |
| static struct dentry *gcpm_init_fs(struct gcpm_drv *gcpm) |
| { |
| struct dentry *de; |
| |
| de = debugfs_create_dir("google_cpm", 0); |
| if (IS_ERR_OR_NULL(de)) |
| return NULL; |
| |
| debugfs_create_file("dc_state", 0644, de, gcpm, &gcpm_debug_dc_state_fops); |
| debugfs_create_file("active", 0644, de, gcpm, &gcpm_debug_active_fops); |
| debugfs_create_file("dc_limit_demand", 0644, de, gcpm, |
| &gcpm_debug_dc_limit_demand_fops); |
| |
| debugfs_create_u32("dc_limit_soc_high", 0644, de, &gcpm->dc_limit_soc_high); |
| |
| debugfs_create_file("pps_stage", 0644, de, gcpm, &gcpm_debug_pps_stage_fops); |
| |
| /* smooth exit from DC */ |
| debugfs_create_file("taper_ctl", 0644, de, gcpm, &gcpm_debug_taper_ctl_fops); |
| debugfs_create_file("taper_step_fv_margin", 0644, de, gcpm, &gcpm_debug_taper_step_fv_margin_fops); |
| debugfs_create_file("taper_step_cc_step", 0644, de, gcpm, &gcpm_debug_taper_step_cc_step_fops); |
| debugfs_create_file("taper_step_count", 0644, de, gcpm, &gcpm_debug_taper_step_count_fops); |
| debugfs_create_file("taper_step_grace", 0644, de, gcpm, &gcpm_debug_taper_step_grace_fops); |
| debugfs_create_file("taper_step_voltage", 0644, de, gcpm, &gcpm_debug_taper_step_voltage_fops); |
| debugfs_create_file("taper_step_current", 0644, de, gcpm, &gcpm_debug_taper_step_current_fops); |
| debugfs_create_file("taper_step_interval", 0644, de, gcpm, &gcpm_debug_taper_step_interval_fops); |
| |
| return de; |
| } |
| |
| /* ------------------------------------------------------------------------ */ |
| |
| static int gcpm_probe_psy_names(struct gcpm_drv *gcpm) |
| { |
| struct device *dev = gcpm->device; |
| int i, count, ret; |
| |
| if (!gcpm->device) |
| return -EINVAL; |
| |
| count = of_property_count_strings(dev->of_node, |
| "google,chg-power-supplies"); |
| if (count <= 0 || count > GCPM_MAX_CHARGERS) |
| return -ERANGE; |
| |
| ret = of_property_read_string_array(dev->of_node, |
| "google,chg-power-supplies", |
| (const char**)&gcpm->chg_psy_names, |
| count); |
| if (ret != count) |
| return -ERANGE; |
| |
| for (i = 0; i < count; i++) |
| dev_info(gcpm->device, "%d:%s\n", i, gcpm->chg_psy_names[i]); |
| |
| return count; |
| } |
| |
| /* ------------------------------------------------------------------------- |
| * Use to abstract the PPS adapter if needed. |
| */ |
| |
| static int gcpm_pps_psy_set_property(struct power_supply *psy, |
| enum power_supply_property prop, |
| const union power_supply_propval *val) |
| { |
| struct gcpm_drv *gcpm = power_supply_get_drvdata(psy); |
| struct pd_pps_data *pps_data; |
| int ret = 0; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| |
| pps_data = gcpm_pps_data(gcpm); |
| if (!pps_data || !pps_data->pps_psy) { |
| pr_debug("%s: no target prop=%d ret=%d\n", __func__, prop, ret); |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return -EAGAIN; |
| } |
| |
| switch (prop) { |
| default: |
| ret = power_supply_set_property(pps_data->pps_psy, prop, val); |
| break; |
| } |
| |
| mutex_unlock(&gcpm->chg_psy_lock); |
| pr_debug("%s: prop=%d val=%d ret=%d\n", __func__, |
| prop, val->intval, ret); |
| |
| return ret; |
| } |
| |
| static int gcpm_pps_psy_get_property(struct power_supply *psy, |
| enum power_supply_property prop, |
| union power_supply_propval *val) |
| { |
| struct gcpm_drv *gcpm = power_supply_get_drvdata(psy); |
| struct pd_pps_data *pps_data; |
| int ret = 0; |
| |
| mutex_lock(&gcpm->chg_psy_lock); |
| |
| pps_data = gcpm_pps_data(gcpm); |
| if (pps_data && pps_data->pps_psy) { |
| ret = power_supply_get_property(pps_data->pps_psy, prop, val); |
| pr_debug("%s: prop=%d val=%d ret=%d\n", __func__, |
| prop, val->intval, ret); |
| goto done; |
| } |
| |
| switch (prop) { |
| case POWER_SUPPLY_PROP_USB_TYPE: |
| val->intval = POWER_SUPPLY_USB_TYPE_UNKNOWN; |
| break; |
| default: |
| val->intval = 0; |
| break; |
| } |
| |
| done: |
| mutex_unlock(&gcpm->chg_psy_lock); |
| return ret; |
| } |
| |
| /* check pps_is_avail(), pps_prog_online() and pps_check_type() */ |
| static enum power_supply_property gcpm_pps_psy_properties[] = { |
| POWER_SUPPLY_PROP_VOLTAGE_MAX, |
| POWER_SUPPLY_PROP_VOLTAGE_MIN, |
| POWER_SUPPLY_PROP_CURRENT_MAX, |
| POWER_SUPPLY_PROP_CURRENT_NOW, /* 17 */ |
| POWER_SUPPLY_PROP_ONLINE, /* 4 */ |
| POWER_SUPPLY_PROP_PRESENT, /* 3 */ |
| POWER_SUPPLY_PROP_TYPE, /* */ |
| POWER_SUPPLY_PROP_USB_TYPE, /* */ |
| POWER_SUPPLY_PROP_VOLTAGE_NOW, /* */ |
| }; |
| |
| static int gcpm_pps_psy_is_writeable(struct power_supply *psy, |
| enum power_supply_property psp) |
| { |
| switch (psp) { |
| case POWER_SUPPLY_PROP_PRESENT: |
| case POWER_SUPPLY_PROP_ONLINE: |
| case POWER_SUPPLY_PROP_CURRENT_NOW: |
| case POWER_SUPPLY_PROP_VOLTAGE_NOW: |
| return 1; |
| default: |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static enum power_supply_usb_type gcpm_pps_usb_types[] = { |
| POWER_SUPPLY_USB_TYPE_UNKNOWN, |
| POWER_SUPPLY_USB_TYPE_PD_PPS |
| }; |
| |
| static const struct power_supply_desc gcpm_pps_psy_desc = { |
| .name = "gcpm_pps", |
| .type = POWER_SUPPLY_TYPE_UNKNOWN, |
| .get_property = gcpm_pps_psy_get_property, |
| .set_property = gcpm_pps_psy_set_property, |
| .properties = gcpm_pps_psy_properties, |
| .property_is_writeable = gcpm_pps_psy_is_writeable, |
| .num_properties = ARRAY_SIZE(gcpm_pps_psy_properties), |
| |
| /* POWER_SUPPLY_PROP_USB_TYPE requires an array of these */ |
| .usb_types = gcpm_pps_usb_types, |
| .num_usb_types = ARRAY_SIZE(gcpm_pps_usb_types), |
| }; |
| |
| /* ------------------------------------------------------------------------- */ |
| |
| #define LOG_PSY_RATELIMIT_CNT 200 |
| |
| static int google_cpm_probe(struct platform_device *pdev) |
| { |
| struct power_supply_config gcpm_pps_psy_cfg = { 0 }; |
| struct power_supply_config psy_cfg = { 0 }; |
| const char *tmp_name = NULL; |
| struct gcpm_drv *gcpm; |
| int ret; |
| |
| gcpm = devm_kzalloc(&pdev->dev, sizeof(*gcpm), GFP_KERNEL); |
| if (!gcpm) |
| return -ENOMEM; |
| |
| gcpm->device = &pdev->dev; |
| gcpm->force_active = -1; |
| gcpm->dc_ctl = GCPM_DC_CTL_DEFAULT; |
| gcpm->log_psy_ratelimit = LOG_PSY_RATELIMIT_CNT; |
| gcpm->chg_psy_retries = 10; /* chg_psy_retries * INIT_RETRY_DELAY_MS */ |
| gcpm->out_uv = -1; |
| gcpm->out_ua = -1; |
| gcpm->cp_fcc_hold_limit = -1; |
| |
| INIT_DELAYED_WORK(&gcpm->pps_work, gcpm_pps_wlc_dc_work); |
| INIT_DELAYED_WORK(&gcpm->select_work, gcpm_chg_select_work); |
| INIT_DELAYED_WORK(&gcpm->init_work, gcpm_init_work); |
| mutex_init(&gcpm->chg_psy_lock); |
| |
| gcpm->gcpm_ws = wakeup_source_register(NULL, "google-cpm"); |
| if (!gcpm->gcpm_ws) { |
| dev_err(gcpm->device, "Failed to register wakeup source\n"); |
| return -ENODEV; |
| } |
| |
| /* this is my name */ |
| ret = of_property_read_string(pdev->dev.of_node, "google,psy-name", |
| &tmp_name); |
| if (ret == 0) { |
| gcpm_psy_desc.name = devm_kstrdup(&pdev->dev, tmp_name, |
| GFP_KERNEL); |
| if (!gcpm_psy_desc.name) |
| return -ENOMEM; |
| } |
| |
| /* subs power supply names */ |
| gcpm->chg_psy_count = gcpm_probe_psy_names(gcpm); |
| if (gcpm->chg_psy_count <= 0) |
| return -ENODEV; |
| |
| /* DC/PPS needs at least one power supply of this type */ |
| ret = of_property_read_u32(pdev->dev.of_node, |
| "google,tcpm-power-supply", |
| &gcpm->tcpm_phandle); |
| if (ret < 0) |
| pr_warn("google,tcpm-power-supply not defined\n"); |
| |
| ret = of_property_read_string(pdev->dev.of_node, |
| "google,wlc_dc-power-supply", |
| &tmp_name); |
| if (ret == 0) { |
| gcpm->wlc_dc_name = devm_kstrdup(&pdev->dev, tmp_name, |
| GFP_KERNEL); |
| if (!gcpm->wlc_dc_name) |
| return -ENOMEM; |
| } |
| |
| /* GCPM might need a gpio to enable/disable DC/PPS */ |
| gcpm->dcen_gpio = of_get_named_gpio(pdev->dev.of_node, "google,dc-en", 0); |
| if (gcpm->dcen_gpio >= 0) { |
| unsigned long init_flags = GPIOF_OUT_INIT_LOW; |
| |
| of_property_read_u32(pdev->dev.of_node, "google,dc-en-value", |
| &gcpm->dcen_gpio_default); |
| if (gcpm->dcen_gpio_default) |
| init_flags = GPIOF_OUT_INIT_HIGH; |
| |
| ret = devm_gpio_request_one(&pdev->dev, gcpm->dcen_gpio, |
| init_flags, "dc_pu_pin"); |
| pr_info("google,dc-en value =%d ret=%d\n", |
| gcpm->dcen_gpio_default, ret); |
| } |
| |
| /* Triggers to enable dc charging */ |
| ret = of_property_read_u32(pdev->dev.of_node, "google,dc_limit-demand", |
| &gcpm->dc_limit_demand); |
| if (ret < 0) |
| gcpm->dc_limit_demand = GCPM_DEFAULT_DC_LIMIT_DEMAND; |
| |
| ret = of_property_read_u32(pdev->dev.of_node, "google,dc_limit-cc_min", |
| &gcpm->dc_limit_cc_min); |
| if (ret < 0) |
| gcpm->dc_limit_cc_min = GCPM_DEFAULT_DC_LIMIT_CC_MIN; |
| |
| ret = of_property_read_u32(pdev->dev.of_node, "google,dc_limit-cc_min_wlc", |
| &gcpm->dc_limit_cc_min); |
| if (ret < 0) |
| gcpm->dc_limit_cc_min_wlc = GCPM_DEFAULT_DC_LIMIT_CC_MIN_WLC; |
| |
| |
| /* voltage lower bound */ |
| ret = of_property_read_u32(pdev->dev.of_node, "google,dc_limit-vbatt_min", |
| &gcpm->dc_limit_vbatt_min); |
| if (ret < 0) |
| gcpm->dc_limit_vbatt_min = GCPM_DEFAULT_DC_LIMIT_VBATT_MIN; |
| ret = of_property_read_u32(pdev->dev.of_node, "google,dc_limit-vbatt_low", |
| &gcpm->dc_limit_vbatt_low); |
| if (ret < 0) |
| gcpm->dc_limit_vbatt_low = gcpm->dc_limit_vbatt_min - |
| GCPM_DEFAULT_DC_LIMIT_DELTA_LOW; |
| if (gcpm->dc_limit_vbatt_low > gcpm->dc_limit_vbatt_min) |
| gcpm->dc_limit_vbatt_low = gcpm->dc_limit_vbatt_min; |
| |
| /* voltage upper bound */ |
| ret = of_property_read_u32(pdev->dev.of_node, "google,dc_limit-vbatt_max", |
| &gcpm->dc_limit_vbatt_max); |
| if (ret < 0) |
| gcpm->dc_limit_vbatt_max = GCPM_DEFAULT_DC_LIMIT_VBATT_MAX; |
| ret = of_property_read_u32(pdev->dev.of_node, "google,dc_limit-vbatt_high", |
| &gcpm->dc_limit_vbatt_high); |
| if (ret < 0) |
| gcpm->dc_limit_vbatt_high = gcpm->dc_limit_vbatt_max - |
| GCPM_DEFAULT_DC_LIMIT_DELTA_HIGH; |
| if (gcpm->dc_limit_vbatt_high > gcpm->dc_limit_vbatt_max) |
| gcpm->dc_limit_vbatt_high = gcpm->dc_limit_vbatt_max; |
| |
| /* state of charge based limits */ |
| ret = of_property_read_u32(pdev->dev.of_node, "google,dc_limit-soc_high", |
| &gcpm->dc_limit_soc_high); |
| if (ret < 0) |
| gcpm->dc_limit_soc_high = GCPM_DEFAULT_DC_LIMIT_SOC_HIGH; |
| |
| /* taper control */ |
| gcpm->taper_step = -1; |
| ret = of_property_read_u32(pdev->dev.of_node, "google,taper_step-fv-margin", |
| &gcpm->taper_step_fv_margin); |
| if (ret < 0) |
| gcpm->taper_step_fv_margin = GCPM_TAPER_STEP_FV_MARGIN; |
| ret = of_property_read_u32(pdev->dev.of_node, "google,taper_step-cc-step", |
| &gcpm->taper_step_cc_step); |
| if (ret < 0) |
| gcpm->taper_step_cc_step = GCPM_TAPER_STEP_CC_STEP; |
| ret = of_property_read_u32(pdev->dev.of_node, "google,taper_step-interval", |
| &gcpm->taper_step_interval); |
| if (ret < 0) |
| gcpm->taper_step_interval = GCPM_TAPER_STEP_INTERVAL_S; |
| |
| ret = of_property_read_u32(pdev->dev.of_node, "google,taper_step-count", |
| &gcpm->taper_step_count); |
| if (ret < 0) |
| gcpm->taper_step_count = GCPM_TAPER_STEP_COUNT; |
| ret = of_property_read_u32(pdev->dev.of_node, "google,taper_step-grace", |
| &gcpm->taper_step_grace); |
| if (ret < 0) |
| gcpm->taper_step_grace = GCPM_TAPER_STEP_GRACE; |
| ret = of_property_read_u32(pdev->dev.of_node, "google,taper_step-voltage", |
| &gcpm->taper_step_voltage); |
| if (ret < 0) |
| gcpm->taper_step_voltage = GCPM_TAPER_STEP_VOLTAGE; |
| ret = of_property_read_u32(pdev->dev.of_node, "google,taper_step-current", |
| &gcpm->taper_step_current); |
| if (ret < 0) |
| gcpm->taper_step_current = GCPM_TAPER_STEP_CURRENT; |
| |
| dev_info(gcpm->device, "taper ts_m=%d ts_ccs=%d ts_i=%d ts_cnt=%d ts_g=%d ts_v=%d ts_c=%d\n", |
| gcpm->taper_step_fv_margin, gcpm->taper_step_cc_step, |
| gcpm->taper_step_interval, gcpm->taper_step_count, |
| gcpm->taper_step_grace, gcpm->taper_step_voltage, |
| gcpm->taper_step_current); |
| |
| /* GCPM_FCC has the current cc_max for the selected charger */ |
| gcpm->cp_votable = |
| gvotable_create_int_election(NULL, gvotable_comparator_int_min, |
| gcpm_fcc_callback, gcpm); |
| if (IS_ERR_OR_NULL(gcpm->cp_votable)) { |
| ret = PTR_ERR(gcpm->cp_votable); |
| dev_err(gcpm->device, "no GCPM_FCC votable (%d)\n", ret); |
| return ret; |
| } |
| |
| gvotable_set_default(gcpm->cp_votable, (void *)-1); |
| gvotable_set_vote2str(gcpm->cp_votable, gvotable_v2s_int); |
| gvotable_election_set_name(gcpm->cp_votable, "GCPM_FCC"); |
| |
| /* |
| * WirelessDC and Wireless charging use different thermal limit. |
| * The limit (level) comes from the thermal cooling zone msc_fcc and is |
| * mutually exclusive with the vote on dc_icl |
| */ |
| gcpm->dc_fcc_votable = |
| gvotable_create_int_election(NULL, gvotable_comparator_int_min, |
| gcpm_dc_fcc_callback, gcpm); |
| if (IS_ERR_OR_NULL(gcpm->dc_fcc_votable)) { |
| ret = PTR_ERR(gcpm->dc_fcc_votable); |
| dev_err(gcpm->device, "no dc_fcc votable (%d)\n", ret); |
| return ret; |
| } |
| |
| gvotable_set_default(gcpm->dc_fcc_votable, (void *)-1); |
| gvotable_set_vote2str(gcpm->dc_fcc_votable, gvotable_v2s_int); |
| gvotable_election_set_name(gcpm->dc_fcc_votable, "DC_FCC"); |
| |
| gcpm->dc_chg_avail_votable = gvotable_create_int_election( |
| NULL, gvotable_comparator_int_min, |
| gcpm_dc_chg_avail_callback, gcpm); |
| |
| if (IS_ERR_OR_NULL(gcpm->dc_chg_avail_votable)) { |
| ret = PTR_ERR(gcpm->dc_chg_avail_votable); |
| dev_err(gcpm->device, "no DC chg avail votable %d\n", ret); |
| return ret; |
| } |
| |
| gvotable_set_default(gcpm->dc_chg_avail_votable, (void *)1); |
| gvotable_set_vote2str(gcpm->dc_chg_avail_votable, gvotable_v2s_int); |
| gvotable_election_set_name(gcpm->dc_chg_avail_votable, VOTABLE_DC_CHG_AVAIL); |
| gvotable_use_default(gcpm->dc_chg_avail_votable, true); |
| |
| /* sysfs & debug */ |
| gcpm->debug_entry = gcpm_init_fs(gcpm); |
| if (!gcpm->debug_entry) |
| pr_warn("No debug control\n"); |
| |
| platform_set_drvdata(pdev, gcpm); |
| |
| psy_cfg.drv_data = gcpm; |
| psy_cfg.of_node = pdev->dev.of_node; |
| gcpm->psy = devm_power_supply_register(gcpm->device, |
| &gcpm_psy_desc, |
| &psy_cfg); |
| if (IS_ERR(gcpm->psy)) { |
| ret = PTR_ERR(gcpm->psy); |
| if (ret == -EPROBE_DEFER) |
| return -EPROBE_DEFER; |
| |
| dev_err(gcpm->device, "Couldn't register gcpm, (%d)\n", ret); |
| return -ENODEV; |
| } |
| |
| gcpm->log = logbuffer_register("cpm"); |
| if (IS_ERR(gcpm->log)) { |
| dev_err(gcpm->device, "Couldn't register logbuffer, (%ld)\n", |
| PTR_ERR(gcpm->log)); |
| gcpm->log = NULL; |
| } |
| |
| /* gcpm_pps_psy_cfg.of_node is used to find out the snk_pdos */ |
| gcpm_pps_psy_cfg.drv_data = gcpm; |
| gcpm_pps_psy_cfg.of_node = pdev->dev.of_node; |
| gcpm->pps_psy = devm_power_supply_register(gcpm->device, |
| &gcpm_pps_psy_desc, |
| &gcpm_pps_psy_cfg); |
| if (IS_ERR(gcpm->pps_psy)) { |
| ret = PTR_ERR(gcpm->pps_psy); |
| if (ret == -EPROBE_DEFER) |
| return -EPROBE_DEFER; |
| |
| dev_err(gcpm->device, "Couldn't register gcpm_pps (%d)\n", ret); |
| return -ENODEV; |
| } |
| |
| ret = device_create_file(gcpm->device, &dev_attr_dc_limit_demand); |
| if (ret) |
| dev_err(gcpm->device, "Failed to create dc_limit_demand\n"); |
| |
| ret = device_create_file(gcpm->device, &dev_attr_dc_limit_vbatt_max); |
| if (ret) |
| dev_err(gcpm->device, "Failed to create dc_limit_vbatt_max\n"); |
| |
| ret = device_create_file(gcpm->device, &dev_attr_dc_limit_vbatt_min); |
| if (ret) |
| dev_err(gcpm->device, "Failed to create dc_limit_vbatt_min\n"); |
| |
| ret = device_create_file(gcpm->device, &dev_attr_dc_ctl); |
| if (ret) |
| dev_err(gcpm->device, "Failed to create dc_crl\n"); |
| |
| ret = device_create_file(gcpm->device, &dev_attr_thermal_mdis_fan_alarm); |
| if (ret) |
| dev_err(gcpm->device, "Failed to create thermal_mdis_fan_alarm\n"); |
| |
| /* give time to fg driver to start */ |
| schedule_delayed_work(&gcpm->init_work, |
| msecs_to_jiffies(INIT_DELAY_MS)); |
| |
| return 0; |
| } |
| |
| static int google_cpm_remove(struct platform_device *pdev) |
| { |
| struct gcpm_drv *gcpm = platform_get_drvdata(pdev); |
| int i; |
| |
| if (!gcpm) |
| return 0; |
| |
| gvotable_destroy_election(gcpm->dc_fcc_votable); |
| |
| for (i = 0; i < gcpm->chg_psy_count; i++) { |
| if (!gcpm->chg_psy_avail[i]) |
| continue; |
| |
| power_supply_put(gcpm->chg_psy_avail[i]); |
| gcpm->chg_psy_avail[i] = NULL; |
| } |
| |
| pps_free(&gcpm->wlc_pps_data); |
| pps_free(&gcpm->tcpm_pps_data); |
| |
| wakeup_source_unregister(gcpm->gcpm_ws); |
| |
| if (gcpm->wlc_dc_psy) |
| power_supply_put(gcpm->wlc_dc_psy); |
| if (gcpm->log) |
| logbuffer_unregister(gcpm->log); |
| |
| return 0; |
| } |
| |
| static int __maybe_unused gcpm_pm_suspend(struct device *dev) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct gcpm_drv *gcpm = platform_get_drvdata(pdev); |
| |
| if (gcpm->init_complete) { |
| pm_runtime_get_sync(gcpm->device); |
| gcpm->resume_complete = false; |
| pm_runtime_put_sync(gcpm->device); |
| } |
| |
| return 0; |
| } |
| |
| static int __maybe_unused gcpm_pm_resume(struct device *dev) |
| { |
| struct platform_device *pdev = to_platform_device(dev); |
| struct gcpm_drv *gcpm = platform_get_drvdata(pdev); |
| |
| if (gcpm->init_complete) { |
| pm_runtime_get_sync(gcpm->device); |
| gcpm->resume_complete = true; |
| pm_runtime_put_sync(gcpm->device); |
| } |
| |
| return 0; |
| } |
| |
| static SIMPLE_DEV_PM_OPS(gcpm_pm_ops, |
| gcpm_pm_suspend, |
| gcpm_pm_resume); |
| |
| static const struct of_device_id google_cpm_of_match[] = { |
| {.compatible = "google,cpm"}, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(of, google_cpm_of_match); |
| |
| |
| static struct platform_driver google_cpm_driver = { |
| .driver = { |
| .name = "google_cpm", |
| .owner = THIS_MODULE, |
| .of_match_table = google_cpm_of_match, |
| .probe_type = PROBE_PREFER_ASYNCHRONOUS, |
| .pm = &gcpm_pm_ops, |
| }, |
| .probe = google_cpm_probe, |
| .remove = google_cpm_remove, |
| }; |
| |
| module_platform_driver(google_cpm_driver); |
| |
| MODULE_DESCRIPTION("Google Charging Policy Manager"); |
| MODULE_AUTHOR("AleX Pelosi <[email protected]>"); |
| MODULE_LICENSE("GPL"); |
| |
| #if 0 |
| |
| /* NOTE: call with a lock around gcpm->chg_psy_lock */ |
| static int gcpm_dc_charging(struct gcpm_drv *gcpm) |
| { |
| struct power_supply *dc_psy; |
| int vchg, ichg, status; |
| |
| dc_psy = gcpm_chg_get_active(gcpm); |
| if (!dc_psy) { |
| pr_err("DC_CHG: invalid charger\n"); |
| return -ENODEV; |
| } |
| |
| vchg = GPSY_GET_PROP(dc_psy, POWER_SUPPLY_PROP_VOLTAGE_NOW); |
| ichg = GPSY_GET_PROP(dc_psy, POWER_SUPPLY_PROP_CURRENT_NOW); |
| status = GPSY_GET_PROP(dc_psy, POWER_SUPPLY_PROP_STATUS); |
| |
| pr_err("DC_CHG: vchg=%d, ichg=%d status=%d\n", |
| vchg, ichg, status); |
| |
| return 0; |
| } |
| |
| static void gcpm_pps_dc_charging(struct gcpm_drv *gcpm) |
| { |
| struct pd_pps_data *pps_data = &gcpm->pps_data; |
| struct power_supply *pps_psy = gcpm->tcpm_psy; |
| const int pre_out_ua = pps_data->op_ua; |
| const int pre_out_uv = pps_data->out_uv; |
| int ret, pps_ui = -ENODEV; |
| |
| if (gcpm->dc_state == DC_ENABLE) { |
| struct pd_pps_data *pps_data = &gcpm->pps_data; |
| bool pwr_ok; |
| |
| /* must run at the end of PPS negotiation */ |
| if (gcpm->out_ua == -1) |
| gcpm->out_ua = min(gcpm->cc_max, pps_data->max_ua); |
| if (gcpm->out_uv == -1) { |
| struct power_supply *chg_psy = |
| gcpm_chg_get_active(gcpm); |
| unsigned long ta_max_v, value; |
| int vbatt = -1; |
| |
| ta_max_v = pps_data->max_ua * pps_data->max_uv; |
| ta_max_v /= gcpm->out_ua; |
| if (ta_max_v > DC_TA_VMAX_MV) |
| ta_max_v = DC_TA_VMAX_MV; |
| |
| if (chg_psy) |
| vbatt = GPSY_GET_PROP(chg_psy, |
| POWER_SUPPLY_PROP_VOLTAGE_NOW); |
| if (vbatt < 0) |
| vbatt = gcpm->fv_uv; |
| if (vbatt < 0) |
| vbatt = 0; |
| |
| /* good for pca9468 */ |
| value = 2 * vbatt + DC_VBATT_HEADROOM_MV; |
| if (value < DC_TA_VMIN_MV) |
| value = DC_TA_VMIN_MV; |
| |
| /* PPS voltage in 20mV steps */ |
| gcpm->out_uv = value - value % 20000; |
| } |
| |
| pr_info("CHG_CHK: max_uv=%d,max_ua=%d out_uv=%d,out_ua=%d\n", |
| pps_data->max_uv, pps_data->max_ua, |
| gcpm->out_uv, gcpm->out_ua); |
| |
| pps_ui = pps_update_adapter(pps_data, gcpm->out_uv, |
| gcpm->out_ua, pps_psy); |
| if (pps_ui < 0) |
| pps_ui = PPS_ERROR_RETRY_MS; |
| |
| /* wait until adapter is at or over request */ |
| pwr_ok = pps_data->out_uv == gcpm->out_uv && |
| pps_data->op_ua == gcpm->out_ua; |
| if (pwr_ok) { |
| ret = gcpm_chg_offline(gcpm); |
| if (ret == 0) |
| ret = gcpm_dc_start(gcpm, gcpm->dc_index); |
| if (ret == 0) { |
| gcpm->dc_state = DC_RUNNING; |
| pps_ui = DC_RUN_DELAY_MS; |
| } else if (pps_ui > DC_ERROR_RETRY_MS) { |
| pps_ui = DC_ERROR_RETRY_MS; |
| } |
| } |
| |
| /* |
| * TODO: add retries and switch to DC_ENABLE again or to |
| * DC_DISABLED on timeout. |
| */ |
| |
| pr_info("PPS_DC: dc_state=%d out_uv=%d %d->%d, out_ua=%d %d->%d\n", |
| gcpm->dc_state, |
| pps_data->out_uv, pre_out_uv, gcpm->out_uv, |
| pps_data->op_ua, pre_out_ua, gcpm->out_ua); |
| } else if (gcpm->dc_state == DC_RUNNING) { |
| |
| ret = gcpm_chg_ping(gcpm, 0, 0); |
| if (ret < 0) |
| pr_err("PPS_DC: ping failed with %d\n", ret); |
| |
| /* update gcpm->out_uv, gcpm->out_ua */ |
| pr_info("PPS_DC: dc_state=%d out_uv=%d %d->%d out_ua=%d %d->%d\n", |
| gcpm->dc_state, |
| pps_data->out_uv, pre_out_uv, gcpm->out_uv, |
| pps_data->op_ua, pre_out_ua, gcpm->out_ua); |
| |
| ret = gcpm_dc_charging(gcpm); |
| if (ret < 0) |
| pps_ui = DC_ERROR_RETRY_MS; |
| |
| ret = pps_update_adapter(&gcpm->pps_data, |
| gcpm->out_uv, gcpm->out_ua, |
| pps_psy); |
| if (ret < 0) |
| pps_ui = PPS_ERROR_RETRY_MS; |
| } |
| |
| return pps_ui; |
| } |
| #endif |