| /* SPDX-License-Identifier: GPL-2.0 */ |
| /* |
| * Copyright 2020 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 ": %s " fmt, __func__ |
| |
| #include <linux/ctype.h> |
| #include <linux/i2c.h> |
| #include <linux/interrupt.h> |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/pm_runtime.h> |
| #include <linux/of.h> |
| #include <linux/of_gpio.h> |
| #include <linux/regmap.h> |
| #include <linux/debugfs.h> |
| #include <linux/seq_file.h> |
| #include <misc/gvotable.h> |
| #include "gbms_power_supply.h" |
| #include "google_bms.h" |
| #include "max_m5.h" |
| #include "max77759.h" |
| |
| /* Hardware modes */ |
| enum max77759_charger_modes { |
| MAX77759_CHGR_MODE_ALL_OFF = 0x00, |
| MAX77759_CHGR_MODE_BUCK_ON = 0x04, |
| MAX77759_CHGR_MODE_CHGR_BUCK_ON = 0x05, |
| MAX77759_CHGR_MODE_BOOST_UNO_ON = 0x08, |
| MAX77759_CHGR_MODE_BOOST_ON = 0x09, |
| MAX77759_CHGR_MODE_OTG_BOOST_ON = 0x0a, |
| MAX77759_CHGR_MODE_BUCK_BOOST_UNO_ON = 0x0c, |
| MAX77759_CHGR_MODE_CHGR_BUCK_BOOST_UNO_ON = 0x0d, |
| MAX77759_CHGR_MODE_OTG_BUCK_BOOST_ON = 0x0e, |
| MAX77759_CHGR_MODE_CHGR_OTG_BUCK_BOOST_ON = 0x0f, |
| }; |
| |
| /* internal system values */ |
| enum { |
| GBMS_CHGR_MODE_STBY_ON = 0x10 + MAX77759_CHGR_MODE_ALL_OFF, |
| GBMS_CHGR_MODE_CHGR_BUCK_ON = 0x10 + MAX77759_CHGR_MODE_CHGR_BUCK_ON, |
| GBMS_CHGR_MODE_BOOST_UNO_ON = 0x10 + MAX77759_CHGR_MODE_BOOST_UNO_ON, |
| }; |
| |
| #define MAX77759_DEFAULT_MODE MAX77759_CHGR_MODE_ALL_OFF |
| |
| |
| /* CHG_DETAILS_01:CHG_DTLS */ |
| #define CHGR_DTLS_DEAD_BATTERY_MODE 0x00 |
| #define CHGR_DTLS_FAST_CHARGE_CONST_CURRENT_MODE 0x01 |
| #define CHGR_DTLS_FAST_CHARGE_CONST_VOLTAGE_MODE 0x02 |
| #define CHGR_DTLS_TOP_OFF_MODE 0x03 |
| #define CHGR_DTLS_DONE_MODE 0x04 |
| #define CHGR_DTLS_TIMER_FAULT_MODE 0x06 |
| #define CHGR_DTLS_DETBAT_HIGH_SUSPEND_MODE 0x07 |
| #define CHGR_DTLS_OFF_MODE 0x08 |
| #define CHGR_DTLS_OFF_HIGH_TEMP_MODE 0x0a |
| #define CHGR_DTLS_OFF_WATCHDOG_MODE 0x0b |
| #define CHGR_DTLS_OFF_JEITA 0x0c |
| #define CHGR_DTLS_OFF_TEMP 0x0d |
| |
| /* for usecases */ |
| struct max77759_usecase_data { |
| int bst_on; /* */ |
| int bst_sel; /* */ |
| int ext_bst_ctl; /* MW VENDOR_EXTBST_CTRL */ |
| |
| bool init_done; |
| }; |
| |
| struct max77759_chgr_data { |
| struct device *dev; |
| struct power_supply *psy; |
| struct power_supply *wcin_psy; |
| struct power_supply *chgin_psy; |
| |
| struct power_supply *fg_psy; |
| struct power_supply *wlc_psy; |
| struct regmap *regmap; |
| |
| struct gvotable_election *mode_votable; |
| enum gbms_charger_modes last_mode; |
| struct max77759_usecase_data uc_data; |
| |
| struct gvotable_election *dc_icl_votable; |
| struct gvotable_election *dc_suspend_votable; |
| |
| bool chgin_input_suspend; |
| bool wcin_input_suspend; |
| |
| int irq_gpio; |
| |
| struct i2c_client *fg_i2c_client; |
| struct i2c_client *pmic_i2c_client; |
| |
| struct dentry *de; |
| atomic_t sysuvlo1_cnt; |
| atomic_t sysuvlo2_cnt; |
| |
| struct mutex io_lock; |
| bool resume_complete; |
| bool init_complete; |
| |
| int use_case; |
| |
| int fship_dtls; |
| bool online; |
| bool wden; |
| }; |
| |
| static inline int max77759_reg_read(struct regmap *regmap, uint8_t reg, |
| uint8_t *val) |
| { |
| int ret, ival; |
| |
| ret = regmap_read(regmap, reg, &ival); |
| if (ret == 0) |
| *val = 0xFF & ival; |
| |
| return ret; |
| } |
| |
| static inline int max77759_reg_write(struct regmap *regmap, uint8_t reg, |
| uint8_t val) |
| { |
| return regmap_write(regmap, reg, val); |
| } |
| |
| static inline int max77759_readn(struct regmap *regmap, uint8_t reg, |
| uint8_t *val, int count) |
| { |
| return regmap_bulk_read(regmap, reg, val, count); |
| } |
| |
| static inline int max77759_writen(struct regmap *regmap, uint8_t reg, |
| const uint8_t *val, int count) |
| { |
| return regmap_bulk_write(regmap, reg, val, count); |
| } |
| |
| static inline int max77759_reg_update(struct max77759_chgr_data *data, |
| uint8_t reg, uint8_t msk, uint8_t val) |
| { |
| int ret; |
| unsigned tmp; |
| |
| mutex_lock(&data->io_lock); |
| ret = regmap_read(data->regmap, reg, &tmp); |
| if (!ret) { |
| tmp &= ~msk; |
| tmp |= val; |
| ret = regmap_write(data->regmap, reg, tmp); |
| } |
| mutex_unlock(&data->io_lock); |
| |
| return ret; |
| } |
| |
| /* set WDTEN in CHG_CNFG_18 (0xCB), tWD = 80s */ |
| static int max77759_wdt_enable(struct max77759_chgr_data *data, bool enable) |
| { |
| int ret; |
| u8 reg; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_CNFG_18, ®); |
| if (ret < 0) |
| return -EIO; |
| |
| if ((!!_chg_cnfg_18_wdten_get(reg)) == enable) |
| return 0; |
| |
| /* this register is protected, read back to check if it worked */ |
| reg = _chg_cnfg_18_wdten_set(reg, enable); |
| ret = max77759_reg_write(data->regmap, MAX77759_CHG_CNFG_18, reg); |
| if (ret < 0) |
| return -EIO; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_CNFG_18, ®); |
| if (ret < 0) |
| return -EIO; |
| |
| return (ret == 0 && (!!_chg_cnfg_18_wdten_get(reg)) == enable) ? |
| 0 : -EINVAL; |
| } |
| |
| |
| struct max77759_foreach_cb_data { |
| struct gvotable_election *el; |
| |
| const char *reason; |
| |
| bool chgr_on; /* CC_MAX != 0 */ |
| bool stby_on; /* on disconnect */ |
| |
| bool buck_on; /* wired power in (chgin_on) from TCPCI */ |
| |
| bool otg_on; /* power out, usually external */ |
| bool frs_on; /* fast role swap */ |
| |
| bool wlc_on; /* charging wireless */ |
| bool wlc_tx; /* battery share */ |
| |
| bool pps_dc; /* PPS enabled - wired */ |
| bool wlc_dc; /* PPS enabled - wireless */ |
| |
| bool boost_on; /* old for WLC program */ |
| bool uno_on; /* old for WLC program */ |
| |
| u8 raw_value; /* hard override */ |
| bool use_raw; |
| |
| u8 reg; |
| }; |
| |
| /* First step to convert votes to a usecase and a setting for mode */ |
| static int max77759_foreach_callback(void *data, const char *reason, |
| void *vote) |
| { |
| struct max77759_foreach_cb_data *cb_data = data; |
| int mode = (int)vote; /* max77759_mode is an int election */ |
| |
| pr_info("%s: %s : reason=%s mode=%x\n", __func__, |
| cb_data->reason, reason ? reason : "", mode); |
| |
| switch (mode) { |
| /* Direct raw modes last come fist served */ |
| case MAX77759_CHGR_MODE_ALL_OFF: |
| case MAX77759_CHGR_MODE_BUCK_ON: |
| case MAX77759_CHGR_MODE_CHGR_BUCK_ON: |
| case MAX77759_CHGR_MODE_BOOST_UNO_ON: |
| case MAX77759_CHGR_MODE_BOOST_ON: |
| case MAX77759_CHGR_MODE_OTG_BOOST_ON: |
| case MAX77759_CHGR_MODE_BUCK_BOOST_UNO_ON: |
| case MAX77759_CHGR_MODE_CHGR_BUCK_BOOST_UNO_ON: |
| case MAX77759_CHGR_MODE_OTG_BUCK_BOOST_ON: |
| case MAX77759_CHGR_MODE_CHGR_OTG_BUCK_BOOST_ON: |
| if (cb_data->use_raw) |
| break; |
| pr_info("%s:%d RAW vote=%x\n", __func__, __LINE__, mode); |
| cb_data->raw_value = mode; |
| cb_data->reason = reason; |
| cb_data->use_raw = true; |
| break; |
| |
| /* temporary, can be used to program the WLC chip, remove */ |
| case GBMS_CHGR_MODE_BOOST_UNO_ON: |
| if (!cb_data->boost_on || !cb_data->uno_on) |
| cb_data->reason = reason; |
| pr_info("%s:%d BOOST_UNO vote=%x\n", __func__, __LINE__, mode); |
| cb_data->boost_on += 1; |
| cb_data->uno_on += 1; |
| break; |
| |
| /* SYSTEM modes can add complex transactions */ |
| |
| /* MAX77759: on disconnect */ |
| case GBMS_CHGR_MODE_STBY_ON: |
| if (!cb_data->stby_on) |
| cb_data->reason = reason; |
| pr_info("%s:%d FORCE_OFF vote=%x\n", __func__, __LINE__, mode); |
| cb_data->stby_on += 1; |
| break; |
| |
| /* MAX77759: charging on via CC_MAX (needs inflow, buck_on on) */ |
| case GBMS_CHGR_MODE_CHGR_BUCK_ON: |
| if (!cb_data->chgr_on) |
| cb_data->reason = reason; |
| pr_info("%s:%d CHGR_BUCK_ON vote=%x\n", __func__, __LINE__, mode); |
| cb_data->chgr_on += 1; |
| break; |
| |
| /* USB: inflow, actual charging controlled via BUCK_ON */ |
| case GBMS_USB_BUCK_ON: |
| if (!cb_data->buck_on) |
| cb_data->reason = reason; |
| pr_info("%s:%d BUCK_ON vote=%x\n", __func__, __LINE__, mode); |
| cb_data->buck_on += 1; |
| break; |
| /* USB: OTG, source, fast role swap case */ |
| case GBMS_USB_OTG_FRS_ON: |
| if (!cb_data->frs_on) |
| cb_data->reason = reason; |
| pr_info("%s:%d FRS_ON vote=%x\n", __func__, __LINE__, mode); |
| cb_data->frs_on += 1; |
| break; |
| /* USB: boost mode, source, normally external boost */ |
| case GBMS_USB_OTG_ON: |
| if (!cb_data->otg_on) |
| cb_data->reason = reason; |
| pr_info("%s:%d OTG_ON vote=%x\n", __func__, __LINE__, mode); |
| cb_data->otg_on += 1; |
| break; |
| /* DC Charging: mode=0, set CP_EN */ |
| case GBMS_CHGR_MODE_CHGR_DC: |
| if (!cb_data->pps_dc) |
| cb_data->reason = reason; |
| pr_info("%s:%d DC_ON vote=%x\n", __func__, __LINE__, mode); |
| cb_data->pps_dc += 1; |
| break; |
| |
| default: |
| pr_info("mode=%x not supported\n", mode); |
| break; |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * Use cases, these are platform specific and need to be outside the driver. |
| * Case USB_chg USB_otg WLC_chg WLC_TX PMIC_Charger Ext_B LSx Name |
| * ---------------------------------------------------------------------------- |
| * 1-1 1 0 x 0 IF-PMIC-VBUS 0 0/0 USB_CHG |
| * 1-2 2 0 x 0 DC VBUS 0 0/0 USB_DC |
| * 2-1 1 0 0 1 IF-PMIC-VBUS 2 0/1 USB_CHG_WLC_TX |
| * 2-2 2 0 0 1 DC CHG 2 0/1 USB_DC_WLC_TX |
| * 3-1 0 0 1 0 IF-PMIC-WCIN 0 0/0 WLC_RX |
| * 3-2 0 0 2 0 DC WCIN 0 0/0 WLC_DC |
| * 4-1 0 1 1 0 IF-PMIC-WCIN 1 1/0 USB_OTG_WLC_RX |
| * 4-2 0 1 2 0 DC WCIN 1 1/0 USB_OTG_WLC_DC |
| * 5-1 0 1 0 0 0 1 1/0 USB_OTG |
| * 5-2 0 1 0 0 OTG 5V 0 0/0 USB_OTG_FRS |
| * 6-2 0 0 0 1 0 2 0/1 WLC_TX |
| * 7-2 0 1 0 1 MW OTG 5V 2 0/1 USB_OTG_WLC_TX |
| * 8 0 0 0 0 0 0 0/0 IDLE |
| * ---------------------------------------------------------------------------- |
| * |
| * Ext_Boost = 0 off, 1 = OTG 5V, 2 = WTX 7.5 |
| * USB_chg = 0 off, 1 = on, 2 = PPS |
| * WLC_chg = 0 off, 1 = on, 2 = PPS |
| */ |
| |
| enum gsu_usecases { |
| GSU_RAW_MODE = -1, /* raw mode, default, */ |
| |
| GSU_MODE_STANDBY = 0, /* 8, PMIC mode 0 */ |
| GSU_MODE_USB_CHG = 1, /* 1-1 wired mode 0x4, mode 0x5 */ |
| GSU_MODE_USB_DC = 2, /* 1-2 wired mode 0x0 */ |
| GSU_MODE_USB_CHG_WLC_TX = 3, /* 2-1, 1041, */ |
| GSU_MODE_USB_DC_WLC_TX = 4, /* 2-2 1042, */ |
| |
| GSU_MODE_WLC_RX = 5, /* 3-1, mode 0x4, mode 0x5 */ |
| GSU_MODE_WLC_DC = 6, /* 3-2, mode 0x0 */ |
| |
| GSU_MODE_USB_OTG_WLC_RX = 7, /* 4-1, 524, */ |
| GSU_MODE_USB_OTG_WLC_DC = 8, /* 4-2, 532, */ |
| GSU_MODE_USB_OTG = 9, /* 5-1, 516,*/ |
| GSU_MODE_USB_OTG_FRS = 10, /* 5-2, PMIC mode 0x0a */ |
| |
| GSU_MODE_WLC_TX = 11, /* 6-2, 1056, */ |
| GSU_MODE_USB_OTG_WLC_TX = 12, /* 7-2, 1060, */ |
| }; |
| |
| |
| /* control VENDOR_EXTBST_CTRL (from TCPCI module) */ |
| static int max77759_ls_mode(struct max77759_chgr_data *data, int mode) |
| { |
| int ret = 0; |
| |
| pr_info("%s: mode=%d on=%d sel=%d\n", __func__, mode, |
| data->uc_data.bst_on, data->uc_data.bst_sel); |
| |
| if (data->uc_data.ext_bst_ctl < 0) |
| return 0; |
| |
| switch (mode) { |
| case 0: |
| gpio_set_value_cansleep(data->uc_data.ext_bst_ctl, 0); |
| break; |
| case 1: |
| gpio_set_value_cansleep(data->uc_data.ext_bst_ctl, 1); |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return ret; |
| } |
| |
| /* control external boost mode |
| * can be done controlling ls1, ls2 |
| */ |
| #define EXT_MODE_OFF 0 |
| #define EXT_MODE_OTG_5_0V 1 |
| #define EXT_MODE_OTG_7_5V 2 |
| |
| /* GPIO5 on Max77759 on canopy and on all whitefins */ |
| static int max77759_ext_mode(struct max77759_chgr_data *data, int mode) |
| { |
| int ret = 0; |
| |
| pr_info("%s: mode=%d on=%d sel=%d\n", __func__, mode, |
| data->uc_data.bst_on, data->uc_data.bst_sel); |
| |
| if (data->uc_data.bst_on < 0 || data->uc_data.bst_sel < 0) |
| return 0; |
| |
| switch (mode) { |
| case EXT_MODE_OFF: |
| gpio_set_value_cansleep(data->uc_data.bst_on, 0); |
| break; |
| case EXT_MODE_OTG_5_0V: |
| gpio_set_value_cansleep(data->uc_data.bst_sel, 0); |
| mdelay(100); |
| gpio_set_value_cansleep(data->uc_data.bst_on, 1); |
| break; |
| case EXT_MODE_OTG_7_5V: /* TODO: verify this */ |
| gpio_set_value_cansleep(data->uc_data.bst_sel, 1); |
| mdelay(100); |
| gpio_set_value_cansleep(data->uc_data.bst_on, 1); |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return ret; |
| } |
| |
| /* |
| * Transition to standby (if needed) at the beginning of the sequences |
| * @return <0 on error, 0 on success. ->use_case becomes GSU_MODE_STANDBY |
| * if the transition is necessary (and successful). |
| */ |
| static int max77759_to_standby(struct max77759_chgr_data *data, int use_case) |
| { |
| bool need_stby = false; |
| int ret; |
| |
| switch (data->use_case) { |
| case GSU_MODE_USB_CHG: |
| need_stby = use_case != GSU_MODE_USB_CHG_WLC_TX && |
| use_case != GSU_MODE_WLC_RX && |
| use_case != GSU_MODE_USB_DC; |
| break; |
| case GSU_MODE_WLC_RX: |
| need_stby = use_case != GSU_MODE_USB_OTG_WLC_RX && |
| use_case != GSU_MODE_WLC_DC; |
| break; |
| case GSU_MODE_WLC_TX: |
| need_stby = use_case != GSU_MODE_USB_OTG_WLC_TX && |
| use_case != GSU_MODE_USB_CHG_WLC_TX && |
| use_case != GSU_MODE_USB_DC_WLC_TX; |
| break; |
| case GSU_MODE_USB_CHG_WLC_TX: |
| need_stby = use_case != GSU_MODE_USB_CHG; |
| break; |
| case GSU_MODE_USB_OTG: /* From 5. USB OTG to 8. standby */ |
| |
| |
| if (use_case == GSU_MODE_USB_OTG_FRS) |
| break; |
| |
| /* missing setting EXT_BST_EN in MW (TCPM) */ |
| |
| ret = max77759_ext_mode(data, 0); |
| if (ret < 0) { |
| dev_err(data->dev, "cannot turn off ext (%d)\n", |
| ret); |
| return -EIO; |
| } |
| |
| /* missing Discharge IN/OUT nodes with AO37 */ |
| mdelay(100); |
| |
| need_stby = true; |
| break; |
| |
| case GSU_MODE_USB_OTG_FRS: |
| |
| if (use_case == GSU_MODE_USB_OTG) |
| break; |
| |
| need_stby = true; |
| break; |
| |
| /* no need to transition to stby for these modes */ |
| case GSU_MODE_USB_OTG_WLC_RX: |
| case GSU_MODE_USB_OTG_WLC_TX: |
| case GSU_MODE_STANDBY: |
| default: |
| need_stby = false; |
| break; |
| } |
| |
| pr_info("%s: use_case=%d->%d need_stby=%x\n", __func__, |
| data->use_case, use_case, need_stby); |
| |
| if (!need_stby) |
| return 0; |
| |
| if (data->use_case == GSU_MODE_WLC_TX) { |
| /* not yet */ |
| } |
| |
| /* transition to STBY (might need to be up) */ |
| ret = max77759_reg_write(data->regmap, MAX77759_CHG_CNFG_00, 0); |
| if (ret < 0) { |
| dev_err(data->dev, "cannot reset mode (%d)\n", ret); |
| return -EIO; |
| } |
| |
| data->use_case = GSU_MODE_STANDBY; |
| return 0; |
| } |
| |
| /* |
| * Case USB_chg USB_otg WLC_chg WLC_TX PMIC_Charger Ext_B LSxx Name |
| * ------------------------------------------------------------------------------------- |
| * 4-1 0 1 10 0 IF-PMIC-WCIN 1 1/0 USB_OTG_WLC_RX |
| * 4-2 0 1 01 0 DC WCIN 1 1/0 USB_OTG_WLC_DC |
| * 5-1 0 1 0 0 0 1 1/0 USB_OTG |
| * 5-2 0 1 0 0 OTG_5V 0 0/0 USB_OTG_FRS |
| * 7-2 0 1 0 1 OTG_5V 2 0/1 USB_OTG_WLC_TX |
| * ------------------------------------------------------------------------------------- |
| * WLC_chg = 0 off, 1 = on, 2 = PPS |
| * Ext_Boost = 0 off, 1 = OTG 5V, 2 = WTX 7.5 |
| * |
| * 5-1: mode=0x0 in MW, EXT_B=1, LS1=1, LS2=0, IDLE <-> OTG (ext) |
| * 5-2: mode=0xa in MW, EXT_B=0, LS1=0, LS2=0, IDLE <-> OTG_FRS |
| * 7-2: mode=0xa in MW, EXT_B=2, LS1=0, LS2=1 |
| * |
| * AO37 + GPIO5 MW (canopy 3, whitev2p2) |
| * . AO_ls1 <&max20339_gpio 0 GPIO_ACTIVE_HIGH> - bit0 |
| * . AO_ls2 <&max20339_gpio 1 GPIO_ACTIVE_HIGH> - bit4 |
| * |
| * ls1 can be controlled poking the AO37 OR using a MW_GPIO _> EXT_BST_EN |
| * |
| * max77759,bst_on = <&max777x9_gpio 4 GPIO_ACTIVE_HIGH> |
| * max77759,bst-sel = <&gpp27 3 GPIO_ACTIVE_HIGH> |
| * max77759,bst_on=0, max77759,bst_sel=x => OFF |
| * max77759,bst_on=1, max77759,bst_sel=0 => 5V |
| * max77759,bst_on=1, max77759,bst_sel=1 => 7.5V |
| * |
| * Ext_Boost = 0 off |
| * MW_gpio5 : Ext_B = 0, MW_gpio5 -> LOW |
| * AO_ls1/ls2 : 0/0 |
| * |
| * Ext_Boost = 1 = OTG 5V |
| * MW_gpio5 : Ext_B = 1, MW_gpio5 -> HIGH |
| * AO_ls1/ls2 : 1/0 |
| * |
| * Ext_Boost = 2 WTX 7.5 |
| * MW_gpio5 : Ext_B = 2, MW_gpio5 -> HIGH |
| * AO_ls1/ls2 : 0/1 |
| * |
| * NOTE: do not call with (cb_data->wlc_on && cb_data->wlc_tx) |
| */ |
| static int max77759_to_otg_usecase(struct max77759_chgr_data *data, |
| int use_case, u8 reg) |
| { |
| int ret = 0; |
| |
| if (!data->uc_data.init_done) |
| return -EPROBE_DEFER; |
| |
| if (data->use_case == GSU_MODE_STANDBY) { |
| /* 5-1: #3: stby to USB OTG, mode = 1 */ |
| /* 5-2: #3: stby to USB OTG_FRS, mode = 0 */ |
| const int mode = use_case == GSU_MODE_USB_OTG_FRS ? |
| EXT_MODE_OFF : |
| EXT_MODE_OTG_5_0V; |
| |
| /* Write 0b11 to IN_CTR(0x10).INSwEn[1:0] */ |
| /* Write 0b1 to AO37 SwCntl (0xA).LSw1En */ |
| /* TCPCM controls EXT_BST_EN? */ |
| ret = max77759_ls_mode(data, 1); |
| if (ret == 0) |
| ret = max77759_ext_mode(data, mode); |
| |
| } else if (data->use_case == GSU_MODE_USB_OTG) { |
| /* |
| * OTG source handover: OTG -> OTG_FRS |
| * from IF-PMIC OTG (FRS OTG) to EXT_BST (regular OTG) |
| */ |
| } else if (data->use_case == GSU_MODE_USB_OTG_FRS) { |
| /* |
| * OTG source handover: OTG_FRS -> OTG |
| * from EXT_BST (Regular OTG) to IF-PMIC OTG (FRS OTG) |
| */ |
| } |
| |
| return ret; |
| } |
| |
| /* handles the transition data->use_case ==> use_case */ |
| static int max77759_to_usecase(struct max77759_chgr_data *data, |
| int use_case, u8 reg) |
| { |
| int ret = 0; |
| |
| pr_info("%s: use_case=%d->%d reg=%x\n", __func__, |
| data->use_case, use_case, reg); |
| |
| switch (use_case) { |
| case GSU_MODE_USB_OTG: |
| case GSU_MODE_USB_OTG_FRS: |
| ret = max77759_to_otg_usecase(data, use_case, reg); |
| if (ret < 0) |
| return ret; |
| break; |
| case GSU_MODE_WLC_TX: |
| break; |
| default: |
| break; |
| } |
| |
| ret = max77759_reg_write(data->regmap, MAX77759_CHG_CNFG_00, reg); |
| if (ret < 0) { |
| dev_err(data->dev, "cannot set CNFG_00 (%d)\n", ret); |
| return -EIO; |
| } |
| |
| return ret; |
| |
| } |
| |
| /* |
| * Case USB_chg USB_otg WLC_chg WLC_TX PMIC_Charger Ext_B LSxx Name |
| * ------------------------------------------------------------------------------------- |
| * 4-1 0 1 10 0 IF-PMIC-WCIN 1 1/0 USB_OTG_WLC_RX |
| * 4-2 0 1 01 0 DC WCIN 1 1/0 USB_OTG_WLC_DC |
| * 5-1 0 1 0 0 0 1 1/0 USB_OTG |
| * 5-2 0 1 0 0 OTG_5V 0 0/0 USB_OTG_FRS |
| * 7-2 0 1 0 1 OTG_5V 2 0/1 USB_OTG_WLC_TX |
| * ------------------------------------------------------------------------------------- |
| * Ext_Boost = 0 off, 1 = OTG 5V, 2 = WTX 7.5 |
| * WLC_chg = 0 off, 1 = on, 2 = PPS |
| * |
| * NOTE: do not call with (cb_data->wlc_on && cb_data->wlc_tx) |
| */ |
| static int max77759_get_otg_usecase(struct max77759_foreach_cb_data *cb_data) |
| { |
| int usecase; |
| u8 mode; |
| |
| /* invalid, not with USB power */ |
| if (cb_data->buck_on) { |
| pr_err("%s: buck_on with OTG\n", __func__); |
| return -EINVAL; |
| } |
| |
| /* pure OTG default to FRS */ |
| if (!cb_data->wlc_on && !cb_data->wlc_tx) { |
| /* 5-1: USB_OTG or 5-2: USB_OTG_FRS */ |
| |
| /* HACK: force FRS */ |
| //cb_data->frs_on = 1; |
| |
| if (cb_data->frs_on) { |
| usecase = GSU_MODE_USB_OTG_FRS; |
| mode = MAX77759_CHGR_MODE_OTG_BOOST_ON; |
| } else { |
| usecase = GSU_MODE_USB_OTG; |
| mode = MAX77759_CHGR_MODE_ALL_OFF; |
| } |
| |
| if (cb_data->pps_dc) |
| pr_err("%s: charge pump on with OTG\n", __func__); |
| cb_data->pps_dc = 0; |
| } else if (cb_data->wlc_tx) { |
| /* 7-2 */ |
| usecase = GSU_MODE_USB_OTG_WLC_TX; |
| pr_err("%s: GSU_MODE_WLC_TX not yet\n", __func__); |
| |
| return -EINVAL; |
| } else if (cb_data->wlc_dc) { |
| /* 4-2, maybe use pps_dc */ |
| |
| pr_err("%s: GSU_MODE_WLC_TX not yet\n", __func__); |
| |
| usecase = GSU_MODE_USB_OTG_WLC_DC; |
| mode = MAX77759_CHGR_MODE_ALL_OFF; |
| |
| /* TODO: enable Ext_B 5V */ |
| return -EINVAL; |
| } else { |
| |
| usecase = GSU_MODE_USB_OTG_WLC_RX; |
| mode = MAX77759_CHGR_MODE_ALL_OFF; |
| |
| pr_debug("%s: chgr_on with OTG\n", __func__); |
| |
| } |
| |
| cb_data->reg = _chg_cnfg_00_cp_en_set(cb_data->reg, cb_data->pps_dc); |
| cb_data->reg = _chg_cnfg_00_mode_set(cb_data->reg, mode); |
| return usecase; |
| } |
| |
| /* |
| * Determines the use case to switch to. This is device/system dependent and |
| * will likely be factored to a separate file (compile module). |
| */ |
| static int max77759_get_usecase(struct max77759_foreach_cb_data *cb_data) |
| { |
| int usecase; |
| u8 mode; |
| |
| /* Raw mode, just use it TODO: transition to stby first */ |
| if (cb_data->use_raw) { |
| cb_data->reg = cb_data->raw_value; |
| return GSU_RAW_MODE; |
| } |
| |
| /* consistency check, TOD: add more */ |
| if (cb_data->wlc_tx && cb_data->wlc_on) { |
| pr_err("%s: wlc_tx and wlc_rx\n", __func__); |
| return -EINVAL; |
| } |
| |
| /* OTG modes override the others */ |
| if (cb_data->otg_on || cb_data->frs_on) |
| return max77759_get_otg_usecase(cb_data); |
| |
| /* buck_on is wired, wlc_on is wireless */ |
| if (!cb_data->buck_on && !cb_data->wlc_on) { |
| mode = MAX77759_CHGR_MODE_ALL_OFF; |
| usecase = GSU_MODE_STANDBY; |
| } else { |
| /* MODE_BUCK_ON is inflow */ |
| if (cb_data->chgr_on) { |
| mode = MAX77759_CHGR_MODE_CHGR_BUCK_ON; |
| usecase = GSU_MODE_USB_CHG; |
| } else { |
| mode = MAX77759_CHGR_MODE_BUCK_ON; |
| usecase = GSU_MODE_USB_CHG; |
| } |
| |
| /* direct charging or off mode */ |
| if (cb_data->pps_dc) { |
| mode = MAX77759_CHGR_MODE_ALL_OFF; |
| usecase = GSU_MODE_USB_DC; |
| } else if (cb_data->wlc_dc) { |
| mode = MAX77759_CHGR_MODE_ALL_OFF; |
| usecase = GSU_MODE_WLC_DC; |
| } else if (cb_data->stby_on) { |
| mode = MAX77759_CHGR_MODE_ALL_OFF; |
| usecase = GSU_MODE_STANDBY; |
| } |
| |
| } |
| |
| /* reg might be ignored later */ |
| cb_data->reg = _chg_cnfg_00_cp_en_set(cb_data->reg, cb_data->pps_dc); |
| cb_data->reg = _chg_cnfg_00_mode_set(cb_data->reg, mode); |
| |
| return usecase; |
| } |
| |
| /* switch to a use case, handle the transitions */ |
| static int max77759_set_usecase(struct max77759_chgr_data *data, u8 reg, |
| int use_case) |
| { |
| int ret; |
| |
| /* raw mode doesn't do any transition: this minimal implementation */ |
| if (use_case == GSU_RAW_MODE) |
| return max77759_reg_write(data->regmap, MAX77759_CHG_CNFG_00, |
| reg); |
| |
| /* transition to STBY if requested from the use case. */ |
| ret = max77759_to_standby(data, use_case); |
| if (ret < 0) |
| return ret; |
| |
| /* transition from data->use_case to use_case */ |
| ret = max77759_to_usecase(data, use_case, reg); |
| if (ret < 0) |
| return ret; |
| |
| return 0; |
| } |
| |
| static int max77759_wcin_is_online(struct max77759_chgr_data *data); |
| |
| /* lazy init on the */ |
| static bool max77759_setup_usecases(struct max77759_usecase_data *uc_data, |
| struct device_node *node) |
| { |
| |
| if (!node) { |
| uc_data->init_done = false; |
| uc_data->bst_on = -EPROBE_DEFER; |
| uc_data->bst_sel = -EPROBE_DEFER; |
| uc_data->ext_bst_ctl = -EPROBE_DEFER; |
| return 0; |
| } |
| |
| if (uc_data->bst_on == -EPROBE_DEFER) |
| uc_data->bst_on = of_get_named_gpio(node, "max77759,bst-on", 0); |
| |
| if (uc_data->bst_sel == -EPROBE_DEFER) |
| uc_data->bst_sel = of_get_named_gpio(node, "max77759,bst-sel", 0); |
| |
| if (uc_data->ext_bst_ctl == -EPROBE_DEFER) |
| uc_data->ext_bst_ctl = of_get_named_gpio(node, "max77759,extbst-ctl", 0); |
| |
| return uc_data->bst_on != -EPROBE_DEFER && |
| uc_data->bst_sel != -EPROBE_DEFER && |
| uc_data->ext_bst_ctl != -EPROBE_DEFER; |
| } |
| |
| /* |
| * I am using a the comparator_none, need scan all the votes to determine |
| * the actual. |
| */ |
| static void max77759_mode_callback(struct gvotable_election *el, |
| const char *reason, void *value) |
| { |
| struct max77759_chgr_data *data = gvotable_get_data(el); |
| struct max77759_usecase_data *uc_data = &data->uc_data; |
| struct max77759_foreach_cb_data cb_data = { 0 }; |
| int use_case, ret; |
| u8 reg; |
| |
| /* reason and value are the last voted on */ |
| pr_debug("%s: current reason=%s, value=0x%x\n", __func__, |
| reason ? reason : "<>", (int)value); |
| |
| mutex_lock(&data->io_lock); |
| |
| /* wait for usecases */ |
| if (!uc_data->init_done) { |
| uc_data->init_done = max77759_setup_usecases(uc_data, data->dev->of_node); |
| dev_info(data->dev, "bst_on:%d, bst_sel:%d, ext_bst_ctl:%d", |
| uc_data->bst_on, uc_data->bst_sel, uc_data->ext_bst_ctl); |
| } |
| |
| /* no caching */ |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_CNFG_00, ®); |
| if (ret < 0) { |
| dev_err(data->dev, "cannot read CNFG_00 (%d)\n", ret); |
| goto unlock_done; |
| } |
| |
| /* this is the last vote of the election */ |
| cb_data.reg = reg; /* current */ |
| cb_data.el = el; /* election */ |
| cb_data.wlc_on = max77759_wcin_is_online(data); |
| |
| /* now scan all the reasons, accumulate in cb_data */ |
| gvotable_election_for_each(el, max77759_foreach_callback, &cb_data); |
| |
| dev_info(data->dev, "max77759_charger: CHARGER_MODE=%d reason=%s reg:%x\n", |
| cb_data.raw_value, cb_data.reason ? cb_data.reason : "", |
| reg); |
| |
| dev_info(data->dev, "%s: stby_on=%d, pps_dc=%d, chgr_on=%d, buck_on=%d, " |
| "boost_on=%d, otg_on=%d, uno_on=%d\n", __func__, |
| cb_data.stby_on, cb_data.pps_dc, cb_data.chgr_on, |
| cb_data.buck_on, cb_data.boost_on, cb_data.otg_on, |
| cb_data.uno_on); |
| |
| /* figure out next use case */ |
| use_case = max77759_get_usecase(&cb_data); |
| if (use_case < 0) { |
| dev_err(data->dev, "max77759_charger: no valid use case %d\n", |
| use_case); |
| goto unlock_done; |
| } |
| |
| dev_info(data->dev, "%s: use_case=%x->%x reg=%x->%x\n", __func__, |
| data->use_case, use_case, reg, cb_data.reg); |
| |
| /* TODO: state machine that handle transition between states */ |
| ret = max77759_set_usecase(data, cb_data.reg, use_case); |
| if (ret < 0) { |
| dev_err(data->dev, "%s: use_case=%x->%x reg=%x->%x ret=%d\n", |
| __func__, data->use_case, use_case, reg, cb_data.reg, |
| ret); |
| goto unlock_done; |
| } |
| |
| /* the election is an int election */ |
| if (cb_data.reason) |
| reason = cb_data.reason; |
| if (!reason) |
| reason = "<>"; |
| |
| ret = gvotable_election_set_result(el, reason, |
| (void*)(uintptr_t)cb_data.reg); |
| if (ret < 0) { |
| dev_err(data->dev, "max77759_charger: cannot update election %d\n", |
| ret); |
| goto unlock_done; |
| } |
| |
| /* mode */ |
| data->use_case = use_case; |
| |
| unlock_done: |
| mutex_unlock(&data->io_lock); |
| } |
| |
| static int max77759_get_charge_enabled(struct max77759_chgr_data *data, |
| int *enabled) |
| { |
| int ret; |
| const void *vote = (const void *)0; |
| |
| ret = gvotable_get_current_vote(data->mode_votable, &vote); |
| if (ret < 0) |
| return ret; |
| |
| switch ((uintptr_t)vote) { |
| case MAX77759_CHGR_MODE_CHGR_BUCK_ON: |
| case MAX77759_CHGR_MODE_CHGR_BUCK_BOOST_UNO_ON: |
| case MAX77759_CHGR_MODE_CHGR_OTG_BUCK_BOOST_ON: |
| *enabled = 1; |
| break; |
| default: |
| *enabled = 0; |
| break; |
| } |
| |
| return ret; |
| } |
| |
| /* called from gcpm, DC_SUSPEND and for CC_MAX == 0 */ |
| static int max77759_set_charge_enabled(struct max77759_chgr_data *data, |
| int enabled, const char *reason) |
| { |
| return gvotable_cast_vote(data->mode_votable, reason, |
| (void*)GBMS_CHGR_MODE_CHGR_BUCK_ON, |
| enabled); |
| } |
| |
| /* google_charger on disconnect */ |
| static int max77759_set_charge_disable(struct max77759_chgr_data *data, |
| int enabled, const char *reason) |
| { |
| return gvotable_cast_vote(data->mode_votable, reason, |
| (void*)GBMS_CHGR_MODE_STBY_ON, |
| enabled); |
| } |
| |
| /* turn off CHGIN_INSEL: works when max77559 registers are not protected */ |
| static int max77759_chgin_input_suspend(struct max77759_chgr_data *data, |
| bool enabled, const char *reason) |
| { |
| const u8 value = (!enabled) << MAX77759_CHG_CNFG_12_CHGINSEL_SHIFT; |
| |
| data->chgin_input_suspend = enabled; /* cache */ |
| |
| return max77759_reg_update(data, MAX77759_CHG_CNFG_12, |
| MAX77759_CHG_CNFG_12_CHGINSEL_MASK, |
| value); |
| } |
| |
| /* turn off WCIN_INSEL: works when max77559 registers are not protected */ |
| static int max77759_wcin_input_suspend(struct max77759_chgr_data *data, |
| bool enabled, const char *reason) |
| { |
| const u8 value = (!enabled) << MAX77759_CHG_CNFG_12_WCINSEL_SHIFT; |
| |
| data->wcin_input_suspend = enabled; /* cache */ |
| |
| return max77759_reg_update(data, MAX77759_CHG_CNFG_12, |
| MAX77759_CHG_CNFG_12_WCINSEL_MASK, |
| value); |
| } |
| |
| static int max77759_set_regulation_voltage(struct max77759_chgr_data *data, |
| int voltage_uv) |
| { |
| u8 value; |
| |
| if (voltage_uv >= 4500000) |
| value = 0x32; |
| else if (voltage_uv < 4000000) |
| value = 0x38 + (voltage_uv - 3800000) / 100000; |
| else |
| value = (voltage_uv - 4000000) / 10000; |
| |
| value = VALUE2FIELD(MAX77759_CHG_CNFG_04_CHG_CV_PRM, value); |
| return max77759_reg_update(data, MAX77759_CHG_CNFG_04, |
| MAX77759_CHG_CNFG_04_CHG_CV_PRM_MASK, |
| value); |
| } |
| |
| static int max77759_get_regulation_voltage_uv(struct max77759_chgr_data *data, |
| int *voltage_uv) |
| { |
| u8 value; |
| int ret; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_CNFG_04, &value); |
| if (ret < 0) |
| return ret; |
| |
| if (value < 0x33) |
| *voltage_uv = (4000 + value * 10) * 1000; |
| else if (value == 0x38) |
| *voltage_uv = 3800 * 1000; |
| else if (value == 0x39) |
| *voltage_uv = 3900 * 1000; |
| else |
| return -EINVAL; |
| |
| return 0; |
| } |
| |
| /* set charging current to 0 to disable charging (MODE=0) */ |
| static int max77759_set_charger_current_max_ua(struct max77759_chgr_data *data, |
| int current_ua) |
| { |
| const int disabled = current_ua == 0; |
| u8 value; |
| int ret; |
| |
| if (current_ua < 0) |
| return 0; |
| |
| /* ilim=0 -> switch to mode 0 and suspend charging */ |
| if (current_ua == 0) |
| value = 0x0; |
| else if (current_ua <= 200000) |
| value = 0x03; |
| else if (current_ua > 4000000) |
| value = 0x3F; |
| else |
| value = 0x3 + (current_ua - 200000) / 66670; |
| |
| value = VALUE2FIELD(MAX77759_CHG_CNFG_02_CHGCC, value); |
| ret = max77759_reg_update(data, MAX77759_CHG_CNFG_02, |
| MAX77759_CHG_CNFG_02_CHGCC_MASK, |
| value); |
| if (ret == 0) |
| ret = max77759_set_charge_enabled(data, !disabled, "CC_MAX"); |
| |
| return ret; |
| } |
| |
| static int max77759_get_charger_current_max_ua(struct max77759_chgr_data *data, |
| int *current_ua) |
| { |
| u8 value; |
| int ret; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_CNFG_02, |
| &value); |
| if (ret < 0) |
| return ret; |
| |
| /* TODO: fix the rounding */ |
| value = VALUE2FIELD(MAX77759_CHG_CNFG_02_CHGCC, value); |
| |
| /* ilim=0 -> mode 0 with charging suspended */ |
| if (value == 0) |
| *current_ua = 0; |
| else if (value < 3) |
| *current_ua = 133 * 1000; |
| else if (value >= 0x3C) |
| *current_ua = 4000 * 1000; |
| else |
| *current_ua = 133000 + (value - 2) * 66670; |
| |
| return 0; |
| } |
| |
| /* enable autoibus and charger mode */ |
| static int max77759_chgin_set_ilim_max_ua(struct max77759_chgr_data *data, |
| int ilim_ua) |
| { |
| const bool suspend = ilim_ua == 0; |
| u8 value; |
| int ret; |
| |
| /* TODO: disable charging */ |
| if (ilim_ua < 0) |
| return 0; |
| |
| if (ilim_ua == 0) |
| value = 0x00; |
| else if (ilim_ua > 3200000) |
| value = 0x7f; |
| else |
| value = 0x04 + (ilim_ua - 125000) / 25000; |
| |
| value = VALUE2FIELD(MAX77759_CHG_CNFG_09_NO_AUTOIBUS, 1) | |
| VALUE2FIELD(MAX77759_CHG_CNFG_09_CHGIN_ILIM, value); |
| ret = max77759_reg_update(data, MAX77759_CHG_CNFG_09, |
| MAX77759_CHG_CNFG_09_NO_AUTOIBUS | |
| MAX77759_CHG_CNFG_09_CHGIN_ILIM_MASK, |
| value); |
| if (ret == 0) |
| ret = max77759_chgin_input_suspend(data, suspend, "ILIM"); |
| |
| return ret; |
| } |
| |
| static int max77759_chgin_get_ilim_max_ua(struct max77759_chgr_data *data, |
| int *ilim_ua) |
| { |
| int icl, ret; |
| u8 value; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_CNFG_09, &value); |
| if (ret < 0) |
| return ret; |
| |
| value = FIELD2VALUE(MAX77759_CHG_CNFG_09_CHGIN_ILIM, value); |
| if (value == 0) |
| icl = 0; |
| else if (value > 3) |
| icl = 100 + (value - 3) * 25; |
| else |
| icl = 100; |
| |
| *ilim_ua = icl * 1000; |
| |
| if (data->chgin_input_suspend) |
| *ilim_ua = 0; |
| |
| return 0; |
| } |
| |
| static int max77759_wcin_set_ilim_max_ua(struct max77759_chgr_data *data, |
| int ilim_ua) |
| { |
| const bool suspend = ilim_ua == 0; |
| u8 value; |
| int ret; |
| |
| if (ilim_ua < 0) |
| return -EINVAL; |
| |
| if (ilim_ua == 0) |
| value = 0x00; |
| else if (ilim_ua <= 125000) |
| value = 0x01; |
| else |
| value = 0x3 + (ilim_ua - 125000) / 31250; |
| |
| value = VALUE2FIELD(MAX77759_CHG_CNFG_10_WCIN_ILIM, value); |
| ret = max77759_reg_update(data, MAX77759_CHG_CNFG_10, |
| MAX77759_CHG_CNFG_10_WCIN_ILIM_MASK, |
| value); |
| |
| if (ret == 0) |
| ret = max77759_wcin_input_suspend(data, suspend, "DC_ICL"); |
| |
| return ret; |
| } |
| |
| static int max77759_wcin_get_ilim_max_ua(struct max77759_chgr_data *data, |
| int *ilim_ua) |
| { |
| int ret; |
| u8 value; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_CNFG_10, &value); |
| if (ret < 0) |
| return ret; |
| |
| value = FIELD2VALUE(MAX77759_CHG_CNFG_10_WCIN_ILIM, value); |
| if (value == 0) |
| *ilim_ua = 0; |
| else if (value < 4) |
| *ilim_ua = 125000; |
| else |
| *ilim_ua = 125000 + (value - 3) * 31250; |
| |
| if (data->wcin_input_suspend) |
| *ilim_ua = 0; |
| |
| return 0; |
| } |
| |
| /* default is no suspend, any valid vote will suspend */ |
| static void max77759_dc_suspend_vote_callback(struct gvotable_election *el, |
| const char *reason, void *value) |
| { |
| struct max77759_chgr_data *data = gvotable_get_data(el); |
| int ret, suspend = (int)value > 0; |
| |
| ret = max77759_wcin_input_suspend(data, suspend, "DC_SUSPEND"); |
| if (ret < 0) |
| return; |
| |
| /* enable charging when DC_SUSPEND is not set */ |
| ret = max77759_set_charge_enabled(data, !suspend, "DC_SUSPEND"); |
| |
| dev_info(data->dev, "DC_SUSPEND reason=%s, value=%d suspend=%d (%d)\n", |
| reason ? reason : "", (int)value, suspend, ret); |
| } |
| |
| static void max77759_dcicl_callback(struct gvotable_election *el, |
| const char *reason, |
| void *value) |
| { |
| struct max77759_chgr_data *data = gvotable_get_data(el); |
| int dc_icl = (int)value; |
| const bool suspend = dc_icl == 0; |
| int ret; |
| |
| pr_debug("%s: dc_icl=%d suspend=%d (%d)\n", __func__, |
| dc_icl, suspend, ret); |
| |
| ret = max77759_wcin_set_ilim_max_ua(data, dc_icl); |
| if (ret < 0) |
| dev_err(data->dev, "cannot set DC_ICL (%d)\n", ret); |
| |
| ret = gvotable_cast_vote(data->dc_suspend_votable, "DC_ICL", |
| (void *)1, |
| suspend); |
| |
| if (ret < 0) |
| dev_err(data->dev, "cannot set DC_SUSPEND=%d (%d)\n", |
| suspend, ret); |
| } |
| |
| /************************* |
| * WCIN PSY REGISTRATION * |
| *************************/ |
| static enum power_supply_property max77759_wcin_props[] = { |
| POWER_SUPPLY_PROP_PRESENT, |
| POWER_SUPPLY_PROP_ONLINE, |
| POWER_SUPPLY_PROP_VOLTAGE_NOW, |
| POWER_SUPPLY_PROP_CURRENT_MAX, |
| POWER_SUPPLY_PROP_VOLTAGE_MAX, |
| }; |
| |
| static int max77759_wcin_is_present(struct max77759_chgr_data *data) |
| { |
| uint8_t int_ok; |
| int ret; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_INT_OK, &int_ok); |
| return (ret == 0) && _chg_int_ok_wcin_ok_get(int_ok); |
| } |
| |
| static int max77759_wcin_is_online(struct max77759_chgr_data *data) |
| { |
| uint8_t val; |
| int ret; |
| |
| ret = max77759_wcin_is_present(data) && |
| max77759_reg_read(data->regmap, MAX77759_CHG_DETAILS_02, &val); |
| return (ret == 0) && _chg_details_02_wcin_sts_get(val); |
| } |
| |
| static int max77759_wcin_voltage_max(struct max77759_chgr_data *chg, |
| union power_supply_propval *val) |
| { |
| int rc; |
| |
| if (!chg->wlc_psy) { |
| chg->wlc_psy = power_supply_get_by_name("wireless"); |
| if (!chg->wlc_psy) |
| return -ENODEV; |
| } |
| |
| if (!max77759_wcin_is_present(chg)) { |
| val->intval = 0; |
| return 0; |
| } |
| |
| rc = power_supply_get_property(chg->wlc_psy, |
| POWER_SUPPLY_PROP_VOLTAGE_MAX, val); |
| if (rc < 0) { |
| dev_err(chg->dev, "Couldn't get VOLTAGE_MAX, rc=%d\n", rc); |
| return rc; |
| } |
| |
| return rc; |
| } |
| |
| static int max77759_wcin_voltage_now(struct max77759_chgr_data *chg, |
| union power_supply_propval *val) |
| { |
| int rc; |
| |
| if (!chg->wlc_psy) { |
| chg->wlc_psy = power_supply_get_by_name("wireless"); |
| if (!chg->wlc_psy) |
| return -ENODEV; |
| } |
| |
| if (!max77759_wcin_is_present(chg)) { |
| val->intval = 0; |
| return 0; |
| } |
| |
| rc = power_supply_get_property(chg->wlc_psy, |
| POWER_SUPPLY_PROP_VOLTAGE_NOW, val); |
| if (rc < 0) |
| dev_err(chg->dev, "Couldn't get VOLTAGE_NOW, rc=%d\n", rc); |
| |
| return rc; |
| } |
| |
| static int max77759_wcin_get_prop(struct power_supply *psy, |
| enum power_supply_property psp, |
| union power_supply_propval *val) |
| { |
| struct max77759_chgr_data *chgr = power_supply_get_drvdata(psy); |
| int rc = 0; |
| |
| switch (psp) { |
| case POWER_SUPPLY_PROP_PRESENT: |
| val->intval = max77759_wcin_is_present(chgr); |
| break; |
| case POWER_SUPPLY_PROP_ONLINE: |
| val->intval = max77759_wcin_is_online(chgr); |
| break; |
| case POWER_SUPPLY_PROP_VOLTAGE_NOW: |
| rc = max77759_wcin_voltage_now(chgr, val); |
| break; |
| case POWER_SUPPLY_PROP_CURRENT_MAX: |
| rc = max77759_wcin_get_ilim_max_ua(chgr, &val->intval); |
| break; |
| case POWER_SUPPLY_PROP_VOLTAGE_MAX: |
| rc = max77759_wcin_voltage_max(chgr, val); |
| break; |
| default: |
| return -EINVAL; |
| } |
| if (rc < 0) { |
| pr_debug("Couldn't get prop %d rc = %d\n", psp, rc); |
| return -ENODATA; |
| } |
| return 0; |
| } |
| |
| static int max77759_wcin_set_prop(struct power_supply *psy, |
| enum power_supply_property psp, |
| const union power_supply_propval *val) |
| { |
| struct max77759_chgr_data *chgr = power_supply_get_drvdata(psy); |
| int rc = 0; |
| |
| switch (psp) { |
| case POWER_SUPPLY_PROP_CURRENT_MAX: |
| rc = max77759_wcin_set_ilim_max_ua(chgr, val->intval); |
| break; |
| /* called from google_cpm when switching chargers */ |
| case GBMS_PROP_CHARGING_ENABLED: |
| rc = max77759_set_charge_enabled(chgr, val->intval > 0, |
| "DC_PSP_ENABLED"); |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return rc; |
| } |
| |
| static int max77759_wcin_prop_is_writeable(struct power_supply *psy, |
| enum power_supply_property psp) |
| { |
| switch (psp) { |
| case POWER_SUPPLY_PROP_CURRENT_MAX: |
| case GBMS_PROP_CHARGING_ENABLED: |
| return 1; |
| default: |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static const struct power_supply_desc max77759_wcin_psy_desc = { |
| .name = "dc", |
| .type = POWER_SUPPLY_TYPE_WIRELESS_EXT, |
| .properties = max77759_wcin_props, |
| .num_properties = ARRAY_SIZE(max77759_wcin_props), |
| .get_property = max77759_wcin_get_prop, |
| .set_property = max77759_wcin_set_prop, |
| .property_is_writeable = max77759_wcin_prop_is_writeable, |
| }; |
| |
| static int max77759_init_wcin_psy(struct max77759_chgr_data *data) |
| { |
| struct power_supply_config wcin_cfg = {}; |
| |
| wcin_cfg.drv_data = data; |
| wcin_cfg.of_node = data->dev->of_node; |
| data->wcin_psy = devm_power_supply_register( |
| data->dev, &max77759_wcin_psy_desc, &wcin_cfg); |
| if (IS_ERR(data->wcin_psy)) { |
| pr_err("Couldn't register wlc power supply\n"); |
| return PTR_ERR(data->wcin_psy); |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * NOTE: could also check aicl to determine whether the adapter is, in fact, |
| * at fault. Possibly qualify this with battery voltage as subpar adapters |
| * are likely to flag AICL when the battery is at high voltage. |
| */ |
| static int max77759_is_limited(struct max77759_chgr_data *data) |
| { |
| int ret; |
| u8 value; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_INT_OK, &value); |
| return (ret == 0) && _chg_int_ok_inlim_ok_get(value) == 0; |
| } |
| |
| static int max77759_is_present(struct max77759_chgr_data *data) |
| { |
| uint8_t int_ok; |
| int ret; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_INT_OK, &int_ok); |
| return (ret == 0) && (_chg_int_ok_chgin_ok_get(int_ok) || |
| _chg_int_ok_wcin_ok_get(int_ok)); |
| } |
| |
| static int max77759_is_online(struct max77759_chgr_data *data) |
| { |
| uint8_t val; |
| int ret; |
| |
| ret = max77759_is_present(data) && |
| max77759_reg_read(data->regmap, MAX77759_CHG_DETAILS_02, &val); |
| return (ret == 0) && (_chg_details_02_chgin_sts_get(val) || |
| _chg_details_02_wcin_sts_get(val)); |
| } |
| |
| static int max77759_get_charge_type(struct max77759_chgr_data *data) |
| { |
| int ret; |
| uint8_t reg; |
| |
| if (!max77759_is_online(data)) |
| return POWER_SUPPLY_CHARGE_TYPE_NONE; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_DETAILS_01, ®); |
| if (ret < 0) |
| return POWER_SUPPLY_CHARGE_TYPE_UNKNOWN; |
| |
| switch(_chg_details_01_chg_dtls_get(reg)) { |
| case CHGR_DTLS_DEAD_BATTERY_MODE: |
| return POWER_SUPPLY_CHARGE_TYPE_TRICKLE; |
| case CHGR_DTLS_FAST_CHARGE_CONST_CURRENT_MODE: |
| return POWER_SUPPLY_CHARGE_TYPE_FAST; |
| case CHGR_DTLS_FAST_CHARGE_CONST_VOLTAGE_MODE: |
| case CHGR_DTLS_TOP_OFF_MODE: |
| return POWER_SUPPLY_CHARGE_TYPE_TAPER_EXT; |
| |
| case CHGR_DTLS_DONE_MODE: |
| case CHGR_DTLS_TIMER_FAULT_MODE: |
| case CHGR_DTLS_DETBAT_HIGH_SUSPEND_MODE: |
| case CHGR_DTLS_OFF_MODE: |
| case CHGR_DTLS_OFF_HIGH_TEMP_MODE: |
| case CHGR_DTLS_OFF_WATCHDOG_MODE: |
| return POWER_SUPPLY_CHARGE_TYPE_NONE; |
| default: |
| break; |
| } |
| |
| return POWER_SUPPLY_CHARGE_TYPE_UNKNOWN; |
| } |
| |
| static int max77759_get_status(struct max77759_chgr_data *data) |
| { |
| uint8_t val; |
| int ret; |
| |
| if (!max77759_is_online(data)) |
| return POWER_SUPPLY_STATUS_DISCHARGING; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_DETAILS_01, &val); |
| if (ret < 0) |
| return POWER_SUPPLY_STATUS_UNKNOWN; |
| |
| switch (_chg_details_01_chg_dtls_get(val)) { |
| case CHGR_DTLS_DEAD_BATTERY_MODE: |
| case CHGR_DTLS_FAST_CHARGE_CONST_CURRENT_MODE: |
| case CHGR_DTLS_FAST_CHARGE_CONST_VOLTAGE_MODE: |
| return POWER_SUPPLY_STATUS_CHARGING; |
| case CHGR_DTLS_TOP_OFF_MODE: |
| case CHGR_DTLS_DONE_MODE: |
| /* same as POWER_SUPPLY_PROP_CHARGE_DONE */ |
| return POWER_SUPPLY_STATUS_FULL; |
| case CHGR_DTLS_TIMER_FAULT_MODE: |
| case CHGR_DTLS_DETBAT_HIGH_SUSPEND_MODE: |
| case CHGR_DTLS_OFF_MODE: |
| case CHGR_DTLS_OFF_HIGH_TEMP_MODE: |
| case CHGR_DTLS_OFF_WATCHDOG_MODE: |
| return POWER_SUPPLY_STATUS_NOT_CHARGING; |
| default: |
| break; |
| } |
| |
| return POWER_SUPPLY_STATUS_UNKNOWN; |
| } |
| |
| static int max77759_read_from_fg(struct max77759_chgr_data *data, |
| enum power_supply_property psp, |
| union power_supply_propval *pval) |
| { |
| int ret = -ENODEV; |
| |
| if (!data->fg_psy) |
| data->fg_psy = power_supply_get_by_name("maxfg"); |
| if (data->fg_psy) |
| ret = power_supply_get_property(data->fg_psy, psp, pval); |
| if (ret < 0) |
| pval->intval = -1; |
| return 0; |
| } |
| |
| static int max77759_get_chg_chgr_state(struct max77759_chgr_data *data, |
| union gbms_charger_state *chg_state) |
| { |
| int usb_present, usb_valid, dc_present, dc_valid; |
| union power_supply_propval pval; |
| const char *source = ""; |
| uint8_t int_ok, dtls; |
| int icl = 0; |
| int rc; |
| |
| chg_state->v = 0; |
| chg_state->f.chg_status = max77759_get_status(data); |
| chg_state->f.chg_type = max77759_get_charge_type(data); |
| chg_state->f.flags = gbms_gen_chg_flags(chg_state->f.chg_status, |
| chg_state->f.chg_type); |
| |
| rc = max77759_reg_read(data->regmap, MAX77759_CHG_INT_OK, &int_ok); |
| if (rc == 0) |
| rc = max77759_reg_read(data->regmap, MAX77759_CHG_DETAILS_02, |
| &dtls); |
| |
| /* present when connected, valid when FET is closed */ |
| usb_present = (rc == 0) && _chg_int_ok_chgin_ok_get(int_ok); |
| usb_valid = usb_present && _chg_details_02_chgin_sts_get(dtls); |
| |
| /* present if in field, valid when FET is closed */ |
| dc_present = (rc == 0) && _chg_int_ok_wcin_ok_get(int_ok); |
| dc_valid = dc_present && _chg_details_02_wcin_sts_get(dtls); |
| |
| rc = max77759_read_from_fg(data, POWER_SUPPLY_PROP_VOLTAGE_NOW, |
| &pval); |
| if (rc == 0) |
| chg_state->f.vchrg = pval.intval / 1000; |
| |
| if (chg_state->f.chg_status == POWER_SUPPLY_STATUS_DISCHARGING) |
| goto exit_done; |
| |
| rc = max77759_is_limited(data); |
| if (rc > 0) |
| chg_state->f.flags |= GBMS_CS_FLAG_ILIM; |
| |
| /* TODO: b/ handle input MUX corner cases */ |
| if (usb_valid) { |
| max77759_chgin_get_ilim_max_ua(data, &icl); |
| source = dc_present ? "Uw" : "u"; |
| } else if (dc_valid) { |
| max77759_wcin_get_ilim_max_ua(data, &icl); |
| source = usb_present ? "uW" : "w"; |
| } else if (usb_present && dc_present) { |
| source = "-"; |
| } else if (usb_present) { |
| source = "u?"; |
| } else if (dc_present) { |
| source = "w?"; |
| } |
| |
| chg_state->f.icl = icl / 1000; |
| |
| exit_done: |
| pr_debug("MSC_PCS chg_state=%lx [0x%x:%d:%d:%d:%d] chg=%s\n", |
| (unsigned long)chg_state->v, |
| chg_state->f.flags, |
| chg_state->f.chg_type, |
| chg_state->f.chg_status, |
| chg_state->f.vchrg, |
| chg_state->f.icl, |
| source); |
| |
| return 0; |
| } |
| |
| static int max77759_find_pmic(struct max77759_chgr_data *data) |
| { |
| if (!data->pmic_i2c_client) { |
| struct device_node *dn; |
| |
| dn = of_parse_phandle(data->dev->of_node, "max77759,pmic", 0); |
| if (!dn) |
| return -ENXIO; |
| |
| data->pmic_i2c_client = of_find_i2c_device_by_node(dn); |
| if (!data->pmic_i2c_client) |
| return -EAGAIN; |
| } |
| |
| return !!data->pmic_i2c_client; |
| } |
| |
| static int max77759_find_fg(struct max77759_chgr_data *data) |
| { |
| struct device_node *dn; |
| |
| if (data->fg_i2c_client) |
| return 0; |
| |
| dn = of_parse_phandle(data->dev->of_node, "max77759,max_m5", 0); |
| if (!dn) |
| return -ENXIO; |
| |
| data->fg_i2c_client = of_find_i2c_device_by_node(dn); |
| if (!data->fg_i2c_client) |
| return -EAGAIN; |
| |
| return 0; |
| } |
| |
| /* only valid in mode 5, 6, 7, e, f */ |
| static int max77759_read_iic_from_fg(struct max77759_chgr_data *data, int *iic) |
| { |
| int ret; |
| |
| ret = max77759_find_fg(data); |
| if (ret < 0) |
| return ret; |
| |
| return max_m5_read_actual_input_current_ua(data->fg_i2c_client, iic); |
| } |
| |
| static int max77759_wd_tickle(struct max77759_chgr_data *data) |
| { |
| int ret; |
| u8 reg, reg_new; |
| |
| mutex_lock(&data->io_lock); |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_CNFG_00, ®); |
| if (ret == 0) { |
| reg_new = _chg_cnfg_00_wdtclr_set(reg, 0x1); |
| ret = max77759_reg_write(data->regmap, MAX77759_CHG_CNFG_00, |
| reg_new); |
| } |
| |
| if (ret < 0) |
| pr_err("WD Tickle failed %d\n", ret); |
| |
| mutex_unlock(&data->io_lock); |
| return ret; |
| } |
| |
| |
| static int max77759_set_online(struct max77759_chgr_data *data, bool online) |
| { |
| int ret; |
| |
| ret = max77759_wd_tickle(data); |
| if (ret < 0) |
| pr_err("cannot tickle the watchdog\n"); |
| |
| if (data->online != online) { |
| ret = gvotable_cast_vote(data->mode_votable, "OFFLINE", |
| (void *)GBMS_CHGR_MODE_STBY_ON, |
| !online); |
| data->online = online; |
| } |
| |
| return ret; |
| } |
| |
| static int max77759_psy_set_property(struct power_supply *psy, |
| enum power_supply_property psp, |
| const union power_supply_propval *pval) |
| { |
| struct max77759_chgr_data *data = power_supply_get_drvdata(psy); |
| int ret = -EINVAL; |
| |
| pm_runtime_get_sync(data->dev); |
| if (!data->init_complete || !data->resume_complete) { |
| pm_runtime_put_sync(data->dev); |
| return -EAGAIN; |
| } |
| pm_runtime_put_sync(data->dev); |
| |
| switch (psp) { |
| case POWER_SUPPLY_PROP_CURRENT_MAX: |
| ret = max77759_chgin_set_ilim_max_ua(data, pval->intval); |
| pr_debug("%s: icl=%d (%d)\n", __func__, pval->intval, ret); |
| break; |
| case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: |
| ret = max77759_set_charger_current_max_ua(data, pval->intval); |
| pr_debug("%s: charge_current=%d (%d)\n", |
| __func__, pval->intval, ret); |
| break; |
| case POWER_SUPPLY_PROP_VOLTAGE_MAX: |
| case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: |
| ret = max77759_set_regulation_voltage(data, pval->intval); |
| pr_debug("%s: charge_voltage=%d (%d)\n", |
| __func__, pval->intval, ret); |
| break; |
| /* called from google_cpm when switching chargers */ |
| case GBMS_PROP_CHARGING_ENABLED: |
| ret = max77759_set_charge_enabled(data, pval->intval, |
| "PSP_ENABLED"); |
| pr_debug("%s: charging_enabled=%d (%d)\n", |
| __func__, pval->intval, ret); |
| break; |
| /* called from google_charger on disconnect */ |
| case GBMS_PROP_CHARGE_DISABLE: |
| ret = max77759_set_charge_disable(data, pval->intval, |
| "PSP_DISABLE"); |
| pr_debug("%s: charge_disable=%d (%d)\n", |
| __func__, pval->intval, ret); |
| break; |
| case POWER_SUPPLY_PROP_ONLINE: |
| ret = max77759_set_online(data, pval->intval != 0); |
| break; |
| default: |
| break; |
| } |
| |
| if (ret == 0 && data->wden) |
| max77759_wd_tickle(data); |
| |
| |
| return ret; |
| } |
| |
| static int max77759_psy_get_property(struct power_supply *psy, |
| enum power_supply_property psp, |
| union power_supply_propval *pval) |
| { |
| struct max77759_chgr_data *data = power_supply_get_drvdata(psy); |
| union gbms_charger_state chg_state; |
| int rc, ret = 0; |
| |
| pm_runtime_get_sync(data->dev); |
| if (!data->init_complete || !data->resume_complete) { |
| pm_runtime_put_sync(data->dev); |
| return -EAGAIN; |
| } |
| pm_runtime_put_sync(data->dev); |
| |
| switch (psp) { |
| case GBMS_PROP_CHARGE_DISABLE: |
| rc = max77759_get_charge_enabled(data, &pval->intval); |
| if (rc == 0) |
| pval->intval = !pval->intval; |
| else |
| pval->intval = rc; |
| break; |
| case POWER_SUPPLY_PROP_CHARGE_TYPE: |
| pval->intval = max77759_get_charge_type(data); |
| break; |
| case GBMS_PROP_CHARGING_ENABLED: |
| ret = max77759_get_charge_enabled(data, &pval->intval); |
| break; |
| case GBMS_PROP_CHARGE_CHARGER_STATE: |
| rc = max77759_get_chg_chgr_state(data, &chg_state); |
| if (rc == 0) |
| gbms_propval_int64val(pval) = chg_state.v; |
| break; |
| case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: |
| ret = max77759_get_charger_current_max_ua(data, &pval->intval); |
| break; |
| case POWER_SUPPLY_PROP_VOLTAGE_MAX: |
| case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: |
| ret = max77759_get_regulation_voltage_uv(data, &pval->intval); |
| break; |
| case POWER_SUPPLY_PROP_ONLINE: |
| pval->intval = max77759_is_online(data); |
| break; |
| case POWER_SUPPLY_PROP_PRESENT: |
| pval->intval = max77759_is_present(data); |
| break; |
| case POWER_SUPPLY_PROP_CURRENT_MAX: |
| ret = max77759_chgin_get_ilim_max_ua(data, &pval->intval); |
| break; |
| case GBMS_PROP_INPUT_CURRENT_LIMITED: |
| pval->intval = max77759_is_limited(data); |
| break; |
| case POWER_SUPPLY_PROP_STATUS: |
| pval->intval = max77759_get_status(data); |
| break; |
| case POWER_SUPPLY_PROP_VOLTAGE_NOW: |
| rc = max77759_read_from_fg(data, psp, pval); |
| if (rc < 0) |
| dev_err(data->dev, "cannot read voltage now=%d\n", rc); |
| break; |
| case POWER_SUPPLY_PROP_CURRENT_NOW: |
| rc = max77759_read_iic_from_fg(data, &pval->intval); |
| if (rc < 0) |
| pval->intval = -1; |
| break; |
| default: |
| dev_err(data->dev, "property (%d) unsupported.\n", psp); |
| ret = -EINVAL; |
| break; |
| } |
| |
| return ret; |
| } |
| |
| static int max77759_psy_is_writeable(struct power_supply *psy, |
| enum power_supply_property psp) |
| { |
| switch (psp) { |
| case POWER_SUPPLY_PROP_ONLINE: |
| case POWER_SUPPLY_PROP_VOLTAGE_MAX: /* compat, same the next */ |
| case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: |
| case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: |
| case POWER_SUPPLY_PROP_CURRENT_MAX: |
| case GBMS_PROP_CHARGE_DISABLE: |
| return 1; |
| default: |
| break; |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * TODO: POWER_SUPPLY_PROP_RERUN_AICL, POWER_SUPPLY_PROP_TEMP |
| * POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX |
| * POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX |
| */ |
| static enum power_supply_property max77759_psy_props[] = { |
| POWER_SUPPLY_PROP_CHARGE_TYPE, |
| POWER_SUPPLY_PROP_ONLINE, |
| POWER_SUPPLY_PROP_PRESENT, |
| POWER_SUPPLY_PROP_CURRENT_MAX, |
| POWER_SUPPLY_PROP_CURRENT_NOW, |
| POWER_SUPPLY_PROP_VOLTAGE_MAX, /* compat */ |
| POWER_SUPPLY_PROP_VOLTAGE_NOW, |
| POWER_SUPPLY_PROP_STATUS, |
| }; |
| |
| static struct power_supply_desc max77759_psy_desc = { |
| .name = "max77759-charger", |
| .type = POWER_SUPPLY_TYPE_UNKNOWN, |
| .properties = max77759_psy_props, |
| .num_properties = ARRAY_SIZE(max77759_psy_props), |
| .get_property = max77759_psy_get_property, |
| .set_property = max77759_psy_set_property, |
| .property_is_writeable = max77759_psy_is_writeable, |
| }; |
| |
| static ssize_t show_fship_dtls(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| struct max77759_chgr_data *data = dev_get_drvdata(dev); |
| static char *fship_reason[] = {"None", "PWRONB1", "PWRONB1", "PWR"}; |
| u8 pmic_rd; |
| int ret; |
| |
| if (data->fship_dtls != -1) |
| goto exit_done; |
| |
| |
| ret = max77759_find_pmic(data); |
| if (ret < 0) |
| return ret; |
| |
| ret = max777x9_pmic_reg_read(data->pmic_i2c_client, |
| MAX77759_FSHIP_EXIT_DTLS, |
| &pmic_rd, 1); |
| if (ret < 0) |
| return -EIO; |
| |
| if (pmic_rd & MAX77759_FSHIP_EXIT_DTLS_RD) { |
| u8 fship_dtls; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_DETAILS_03, |
| &fship_dtls); |
| if (ret < 0) |
| return -EIO; |
| |
| data->fship_dtls = |
| _chg_details_03_fship_exit_dtls_get(fship_dtls); |
| |
| pmic_rd &= ~MAX77759_FSHIP_EXIT_DTLS_RD; |
| ret = max777x9_pmic_reg_write(data->pmic_i2c_client, |
| MAX77759_FSHIP_EXIT_DTLS, |
| &pmic_rd, 1); |
| if (ret < 0) |
| pr_err("FSHIP: cannot update RD (%d)\n", ret); |
| |
| } else { |
| data->fship_dtls = 0; |
| } |
| |
| exit_done: |
| return scnprintf(buf, PAGE_SIZE, "%d %s\n", data->fship_dtls, |
| fship_reason[data->fship_dtls]); |
| } |
| |
| static DEVICE_ATTR(fship_dtls, 0444, show_fship_dtls, NULL); |
| |
| static ssize_t show_sysuvlo1_cnt(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| struct max77759_chgr_data *data = dev_get_drvdata(dev); |
| |
| return scnprintf(buf, PAGE_SIZE, "%d\n", |
| atomic_read(&data->sysuvlo1_cnt)); |
| } |
| |
| static DEVICE_ATTR(sysuvlo1_cnt, 0444, show_sysuvlo1_cnt, NULL); |
| |
| static ssize_t show_sysuvlo2_cnt(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| struct max77759_chgr_data *data = dev_get_drvdata(dev); |
| |
| return scnprintf(buf, PAGE_SIZE, "%d\n", |
| atomic_read(&data->sysuvlo2_cnt)); |
| } |
| |
| static DEVICE_ATTR(sysuvlo2_cnt, 0444, show_sysuvlo2_cnt, NULL); |
| |
| static int vdroop2_ok_get(void *d, u64 *val) |
| { |
| struct max77759_chgr_data *data = d; |
| int ret = 0; |
| u8 chg_dtls1; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_DETAILS_01, &chg_dtls1); |
| if (ret < 0) |
| return -ENODEV; |
| |
| *val = _chg_details_01_vdroop2_ok_get(chg_dtls1); |
| |
| return 0; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(vdroop2_ok_fops, vdroop2_ok_get, NULL, "%llu\n"); |
| |
| static int vdp1_stp_bst_get(void *d, u64 *val) |
| { |
| struct max77759_chgr_data *data = d; |
| int ret = 0; |
| u8 chg_cnfg18; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_CNFG_18, &chg_cnfg18); |
| if (ret < 0) |
| return -ENODEV; |
| |
| *val = _chg_cnfg_18_vdp1_stp_bst_get(chg_cnfg18); |
| return 0; |
| } |
| |
| static int vdp1_stp_bst_set(void *d, u64 val) |
| { |
| struct max77759_chgr_data *data = d; |
| int ret = 0; |
| u8 chg_cnfg18; |
| const u8 vdp1_stp_bst = (val > 0)? 0x1 : 0x0; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_CNFG_18, &chg_cnfg18); |
| if (ret < 0) |
| return -ENODEV; |
| |
| chg_cnfg18 = _chg_cnfg_18_vdp1_stp_bst_set(chg_cnfg18, vdp1_stp_bst); |
| ret = max77759_reg_write(data->regmap, MAX77759_CHG_CNFG_18, chg_cnfg18); |
| |
| return ret; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(vdp1_stp_bst_fops, vdp1_stp_bst_get, vdp1_stp_bst_set, "%llu\n"); |
| |
| static int vdp2_stp_bst_get(void *d, u64 *val) |
| { |
| struct max77759_chgr_data *data = d; |
| int ret = 0; |
| u8 chg_cnfg18; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_CNFG_18, &chg_cnfg18); |
| if (ret < 0) |
| return -ENODEV; |
| |
| *val = _chg_cnfg_18_vdp2_stp_bst_get(chg_cnfg18); |
| return 0; |
| } |
| |
| static int vdp2_stp_bst_set(void *d, u64 val) |
| { |
| struct max77759_chgr_data *data = d; |
| int ret = 0; |
| u8 chg_cnfg18; |
| const u8 vdp2_stp_bst = (val > 0)? 0x1 : 0x0; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_CNFG_18, &chg_cnfg18); |
| if (ret < 0) |
| return -ENODEV; |
| |
| chg_cnfg18 = _chg_cnfg_18_vdp2_stp_bst_set(chg_cnfg18, vdp2_stp_bst); |
| ret = max77759_reg_write(data->regmap, MAX77759_CHG_CNFG_18, chg_cnfg18); |
| |
| return ret; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(vdp2_stp_bst_fops, vdp2_stp_bst_get, vdp2_stp_bst_set, "%llu\n"); |
| |
| static int sys_uvlo1_get(void *d, u64 *val) |
| { |
| struct max77759_chgr_data *data = d; |
| int ret = 0; |
| u8 chg_cnfg15; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_CNFG_15, &chg_cnfg15); |
| if (ret < 0) |
| return -ENODEV; |
| |
| *val = chg_cnfg15; |
| return 0; |
| } |
| |
| static int sys_uvlo1_set(void *d, u64 val) |
| { |
| struct max77759_chgr_data *data = d; |
| int ret = 0; |
| |
| ret = max77759_reg_write(data->regmap, MAX77759_CHG_CNFG_15, (u8) val); |
| |
| return ret; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(sys_uvlo1_fops, sys_uvlo1_get, sys_uvlo1_set, "0x%x\n"); |
| |
| static int sys_uvlo2_get(void *d, u64 *val) |
| { |
| struct max77759_chgr_data *data = d; |
| int ret = 0; |
| u8 chg_cnfg16; |
| |
| ret = max77759_reg_read(data->regmap, MAX77759_CHG_CNFG_16, &chg_cnfg16); |
| if (ret < 0) |
| return -ENODEV; |
| |
| *val = chg_cnfg16; |
| return 0; |
| } |
| |
| static int sys_uvlo2_set(void *d, u64 val) |
| { |
| struct max77759_chgr_data *data = d; |
| int ret = 0; |
| |
| ret = max77759_reg_write(data->regmap, MAX77759_CHG_CNFG_16, (u8) val); |
| |
| return ret; |
| } |
| |
| DEFINE_SIMPLE_ATTRIBUTE(sys_uvlo2_fops, sys_uvlo2_get, sys_uvlo2_set, "0x%x\n"); |
| |
| static int dbg_init_fs(struct max77759_chgr_data *data) |
| { |
| int ret; |
| |
| ret = device_create_file(data->dev, &dev_attr_fship_dtls); |
| if (ret != 0) |
| pr_err("Failed to create fship_dtls, ret=%d\n", ret); |
| ret = device_create_file(data->dev, &dev_attr_sysuvlo1_cnt); |
| if (ret != 0) |
| pr_err("Failed to create sysuvlo1_cnt, ret=%d\n", ret); |
| ret = device_create_file(data->dev, &dev_attr_sysuvlo2_cnt); |
| if (ret != 0) |
| pr_err("Failed to create sysuvlo2_cnt, ret=%d\n", ret); |
| |
| data->de = debugfs_create_dir("max77759_chg", 0); |
| if (IS_ERR_OR_NULL(data->de)) |
| return -EINVAL; |
| |
| debugfs_create_atomic_t("sysuvlo1_cnt", 0644, data->de, |
| &data->sysuvlo1_cnt); |
| debugfs_create_atomic_t("sysuvlo2_cnt", 0644, data->de, |
| &data->sysuvlo2_cnt); |
| |
| debugfs_create_file("vdroop2_ok", 0400, data->de, data, |
| &vdroop2_ok_fops); |
| debugfs_create_file("vdp1_stp_bst", 0600, data->de, data, |
| &vdp1_stp_bst_fops); |
| debugfs_create_file("vdp2_stp_bst", 0600, data->de, data, |
| &vdp2_stp_bst_fops); |
| debugfs_create_file("sys_uvlo1", 0600, data->de, data, |
| &sys_uvlo1_fops); |
| debugfs_create_file("sys_uvlo2", 0600, data->de, data, |
| &sys_uvlo2_fops); |
| |
| return 0; |
| } |
| |
| static bool max77759_chg_is_reg(struct device *dev, unsigned int reg) |
| { |
| return (reg >= MAX77759_CHG_INT) && (reg <= MAX77759_CHG_CNFG_19); |
| } |
| |
| static const struct regmap_config max77759_chg_regmap_cfg = { |
| .name = "max77759_charger", |
| .reg_bits = 8, |
| .val_bits = 8, |
| .val_format_endian = REGMAP_ENDIAN_NATIVE, |
| .max_register = MAX77759_CHG_CNFG_19, |
| .readable_reg = max77759_chg_is_reg, |
| .volatile_reg = max77759_chg_is_reg, |
| |
| }; |
| |
| static u8 max77759_int_mask[MAX77759_CHG_INT_COUNT] = { |
| ~(MAX77759_CHG_INT_MASK_CHGIN_M | |
| MAX77759_CHG_INT_MASK_WCIN_M | |
| MAX77759_CHG_INT_MASK_CHG_M | |
| MAX77759_CHG_INT_MASK_BAT_M), |
| ~(MAX77759_CHG_INT2_MASK_SYS_UVLO1_M | |
| MAX77759_CHG_INT2_MASK_SYS_UVLO2_M | |
| MAX77759_CHG_INT2_MASK_CHG_STA_CV_M | |
| MAX77759_CHG_INT2_MASK_CHG_STA_TO_M | |
| MAX77759_CHG_INT2_MASK_CHG_STA_DONE_M), |
| }; |
| |
| static irqreturn_t max77759_chgr_irq(int irq, void *client) |
| { |
| struct max77759_chgr_data *data = client; |
| u8 chg_int[MAX77759_CHG_INT_COUNT]; |
| bool broadcast = false; |
| int ret; |
| |
| ret = max77759_readn(data->regmap, MAX77759_CHG_INT, chg_int, |
| sizeof(chg_int)); |
| if (ret < 0) |
| return IRQ_NONE; |
| ret = max77759_writen(data->regmap, MAX77759_CHG_INT, chg_int, |
| sizeof(chg_int)); |
| if (ret < 0) |
| return IRQ_NONE; |
| |
| pr_debug("INT : %x %x\n", chg_int[0], chg_int[1]); |
| |
| if ((chg_int[0] & ~max77759_int_mask[0]) == 0 && |
| (chg_int[1] & ~max77759_int_mask[1]) == 0) |
| return IRQ_NONE; |
| |
| if (chg_int[1] & MAX77759_CHG_INT2_SYS_UVLO1_I) |
| atomic_inc(&data->sysuvlo1_cnt); |
| |
| if (chg_int[1] & MAX77759_CHG_INT2_SYS_UVLO2_I) |
| atomic_inc(&data->sysuvlo2_cnt); |
| |
| if (chg_int[1] & MAX77759_CHG_INT2_MASK_CHG_STA_TO_M) { |
| pr_debug("%s: TOP_OFF\n", __func__); |
| /* |
| * TODO: rewrite to FV_UV when if entering TOP off far |
| * from terminal voltage |
| */ |
| } |
| |
| if (chg_int[1] & MAX77759_CHG_INT2_MASK_CHG_STA_CV_M) |
| pr_debug("%s: CV_MODE\n", __func__); |
| |
| if (chg_int[1] & MAX77759_CHG_INT2_MASK_CHG_STA_DONE_M) { |
| pr_debug("%s: CHARGE DONE\n", __func__); |
| |
| if (data->psy) |
| power_supply_changed(data->psy); |
| } |
| |
| /* wired input is changed */ |
| if (chg_int[0] & MAX77759_CHG_INT_MASK_CHGIN_M) { |
| |
| if (data->chgin_psy) |
| power_supply_changed(data->chgin_psy); |
| else |
| power_supply_changed(data->psy); |
| } |
| |
| /* wireless input is changed */ |
| if (data->wcin_psy && (chg_int[0] & MAX77759_CHG_INT_MASK_WCIN_M)) |
| power_supply_changed(data->wcin_psy); |
| |
| |
| /* someting else is changed */ |
| broadcast = (chg_int[0] & MAX77759_CHG_INT_MASK_CHG_M) | |
| (chg_int[0] & MAX77759_CHG_INT_MASK_BAT_M); |
| if (data->psy && broadcast) |
| power_supply_changed(data->psy); |
| |
| return IRQ_HANDLED; |
| } |
| |
| static int max77759_setup_votables(struct max77759_chgr_data *data) |
| { |
| int ret; |
| |
| /* initialized to -EPROBE_DEFER */ |
| max77759_setup_usecases(&data->uc_data, NULL); |
| |
| /* votes might change mode */ |
| data->mode_votable = gvotable_create_int_election(NULL, NULL, |
| max77759_mode_callback, |
| data); |
| if (IS_ERR_OR_NULL(data->mode_votable)) { |
| ret = PTR_ERR(data->mode_votable); |
| dev_err(data->dev, "no mode votable (%d)\n", ret); |
| return ret; |
| } |
| |
| gvotable_set_vote2str(data->mode_votable, gvotable_v2s_uint); |
| /* will use gvotable_get_default() when available */ |
| gvotable_set_default(data->mode_votable, (void *)GBMS_CHGR_MODE_STBY_ON); |
| gvotable_election_set_name(data->mode_votable, GBMS_MODE_VOTABLE); |
| |
| /* Wireless charging, DC name is for compat */ |
| data->dc_suspend_votable = |
| gvotable_create_bool_election(NULL, |
| max77759_dc_suspend_vote_callback, |
| data); |
| if (IS_ERR_OR_NULL(data->dc_suspend_votable)) { |
| ret = PTR_ERR(data->dc_suspend_votable); |
| dev_err(data->dev, "no dc_suspend votable (%d)\n", ret); |
| return ret; |
| } |
| |
| gvotable_set_vote2str(data->dc_suspend_votable, gvotable_v2s_int); |
| gvotable_election_set_name(data->dc_suspend_votable, "DC_SUSPEND"); |
| |
| data->dc_icl_votable = |
| gvotable_create_int_election(NULL, gvotable_comparator_int_min, |
| max77759_dcicl_callback, |
| data); |
| if (IS_ERR_OR_NULL(data->dc_icl_votable)) { |
| ret = PTR_ERR(data->dc_icl_votable); |
| dev_err(data->dev, "no dc_icl votable (%d)\n", ret); |
| return ret; |
| } |
| |
| gvotable_set_vote2str(data->dc_icl_votable, gvotable_v2s_uint); |
| gvotable_set_default(data->dc_icl_votable, (void *)700000); |
| gvotable_election_set_name(data->dc_icl_votable, "DC_ICL"); |
| gvotable_use_default(data->dc_icl_votable, true); |
| |
| return 0; |
| } |
| |
| static int max77759_charger_probe(struct i2c_client *client, |
| const struct i2c_device_id *id) |
| { |
| struct power_supply_config chgr_psy_cfg = { 0 }; |
| struct device *dev = &client->dev; |
| struct max77759_chgr_data *data; |
| struct regmap *regmap; |
| const char *psy_name; |
| int ret = 0; |
| u8 ping; |
| |
| regmap = devm_regmap_init_i2c(client, &max77759_chg_regmap_cfg); |
| if (IS_ERR(regmap)) { |
| dev_err(dev, "Failed to initialize regmap\n"); |
| return -EINVAL; |
| } |
| |
| ret = max77759_reg_read(regmap, MAX77759_CHG_CNFG_00, &ping); |
| if (ret < 0) |
| return -ENODEV; |
| |
| /* TODO: PING or read HW version from PMIC */ |
| |
| data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); |
| if (!data) |
| return -ENOMEM; |
| |
| data->dev = dev; |
| data->regmap = regmap; |
| data->fship_dtls = -1; |
| data->wden = false; /* TODO: read from DT */ |
| mutex_init(&data->io_lock); |
| atomic_set(&data->sysuvlo1_cnt, 0); |
| atomic_set(&data->sysuvlo2_cnt, 0); |
| i2c_set_clientdata(client, data); |
| |
| ret = of_property_read_string(dev->of_node, "max77759,psy-name", |
| &psy_name); |
| if (ret == 0) |
| max77759_psy_desc.name = devm_kstrdup(dev, psy_name, |
| GFP_KERNEL); |
| |
| chgr_psy_cfg.drv_data = data; |
| chgr_psy_cfg.supplied_to = NULL; |
| chgr_psy_cfg.num_supplicants = 0; |
| data->psy = devm_power_supply_register(dev, &max77759_psy_desc, |
| &chgr_psy_cfg); |
| if (IS_ERR(data->psy)) { |
| dev_err(dev, "Failed to register psy rc = %ld\n", |
| PTR_ERR(data->psy)); |
| return -EINVAL; |
| } |
| |
| /* other drivers (ex tcpci) need this. */ |
| ret = max77759_setup_votables(data); |
| if (ret < 0) |
| return ret; |
| |
| data->irq_gpio = of_get_named_gpio(dev->of_node, "max77759,irq-gpio", 0); |
| if (data->irq_gpio < 0) { |
| dev_err(dev, "failed get irq_gpio\n"); |
| } else { |
| client->irq = gpio_to_irq(data->irq_gpio); |
| |
| ret = devm_request_threaded_irq(data->dev, client->irq, NULL, |
| max77759_chgr_irq, |
| IRQF_TRIGGER_LOW | |
| IRQF_SHARED | |
| IRQF_ONESHOT, |
| "max77759_charger", |
| data); |
| if (ret == 0) { |
| enable_irq_wake(client->irq); |
| |
| /* might cause the isr to be called */ |
| max77759_chgr_irq(-1, data); |
| ret = max77759_writen(regmap, MAX77759_CHG_INT_MASK, |
| max77759_int_mask, |
| sizeof(max77759_int_mask)); |
| if (ret < 0) |
| dev_err(dev, "cannot set irq_mask (%d)\n", ret); |
| } |
| } |
| |
| ret = dbg_init_fs(data); |
| if (ret < 0) |
| dev_err(dev, "Failed to initialize debug fs\n"); |
| |
| mutex_lock(&data->io_lock); |
| ret = max77759_wdt_enable(data, data->wden); |
| if (ret < 0) |
| dev_err(dev, "wd enable=%d failed %d\n", data->wden, ret); |
| mutex_unlock(&data->io_lock); |
| |
| data->init_complete = 1; |
| data->resume_complete = 1; |
| |
| dev_info(dev, "registered as %s\n", max77759_psy_desc.name); |
| max77759_init_wcin_psy(data); |
| return 0; |
| } |
| |
| static int max77759_charger_remove(struct i2c_client *client) |
| { |
| return 0; |
| } |
| |
| |
| static const struct of_device_id max77759_charger_of_match_table[] = { |
| { .compatible = "maxim,max77759chrg"}, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(of, max77759_charger_of_match_table); |
| |
| static const struct i2c_device_id max77759_id[] = { |
| {"max77759_charger", 0}, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(i2c, max77759_id); |
| |
| #if defined CONFIG_PM |
| static int max77759_charger_pm_suspend(struct device *dev) |
| { |
| /* TODO: is there anything to do here? */ |
| return 0; |
| } |
| |
| static int max77759_charger_pm_resume(struct device *dev) |
| { |
| /* TODO: is there anything to do here? */ |
| return 0; |
| } |
| #endif |
| |
| static const struct dev_pm_ops max77759_charger_pm_ops = { |
| SET_LATE_SYSTEM_SLEEP_PM_OPS( |
| max77759_charger_pm_suspend, |
| max77759_charger_pm_resume) |
| }; |
| |
| static struct i2c_driver max77759_charger_i2c_driver = { |
| .driver = { |
| .name = "max77759-charger", |
| .owner = THIS_MODULE, |
| .of_match_table = max77759_charger_of_match_table, |
| #ifdef CONFIG_PM |
| .pm = &max77759_charger_pm_ops, |
| #endif |
| }, |
| .id_table = max77759_id, |
| .probe = max77759_charger_probe, |
| .remove = max77759_charger_remove, |
| }; |
| |
| module_i2c_driver(max77759_charger_i2c_driver); |
| |
| MODULE_DESCRIPTION("Maxim 77759 Charger Driver"); |
| MODULE_AUTHOR("AleX Pelosi <[email protected]>"); |
| MODULE_LICENSE("GPL"); |