diff options
Diffstat (limited to 'drivers/power/supply/axp288_charger.c')
| -rw-r--r-- | drivers/power/supply/axp288_charger.c | 338 |
1 files changed, 225 insertions, 113 deletions
diff --git a/drivers/power/supply/axp288_charger.c b/drivers/power/supply/axp288_charger.c index f8c6da9277b3..ac05942e4e6a 100644 --- a/drivers/power/supply/axp288_charger.c +++ b/drivers/power/supply/axp288_charger.c @@ -1,18 +1,10 @@ +// SPDX-License-Identifier: GPL-2.0-only /* * axp288_charger.c - X-power AXP288 PMIC Charger driver * * Copyright (C) 2016-2017 Hans de Goede <hdegoede@redhat.com> * Copyright (C) 2014 Intel Corporation * Author: Ramakrishna Pallala <ramakrishna.pallala@intel.com> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 as - * published by the Free Software Foundation. - * - * 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. */ #include <linux/acpi.h> @@ -29,6 +21,8 @@ #include <linux/property.h> #include <linux/mfd/axp20x.h> #include <linux/extcon.h> +#include <linux/dmi.h> +#include <asm/iosf_mbi.h> #define PS_STAT_VBUS_TRIGGER BIT(0) #define PS_STAT_BAT_CHRG_DIR BIT(2) @@ -48,11 +42,11 @@ #define VBUS_ISPOUT_CUR_LIM_1500MA 0x1 /* 1500mA */ #define VBUS_ISPOUT_CUR_LIM_2000MA 0x2 /* 2000mA */ #define VBUS_ISPOUT_CUR_NO_LIM 0x3 /* 2500mA */ -#define VBUS_ISPOUT_VHOLD_SET_MASK 0x31 +#define VBUS_ISPOUT_VHOLD_SET_MASK 0x38 #define VBUS_ISPOUT_VHOLD_SET_BIT_POS 0x3 #define VBUS_ISPOUT_VHOLD_SET_OFFSET 4000 /* 4000mV */ #define VBUS_ISPOUT_VHOLD_SET_LSB_RES 100 /* 100mV */ -#define VBUS_ISPOUT_VHOLD_SET_4300MV 0x3 /* 4300mV */ +#define VBUS_ISPOUT_VHOLD_SET_4400MV 0x4 /* 4400mV */ #define VBUS_ISPOUT_VBUS_PATH_DIS BIT(7) #define CHRG_CCCV_CC_MASK 0xf /* 4 bits */ @@ -102,6 +96,8 @@ #define CV_4200MV 4200 /* 4200mV */ #define CV_4350MV 4350 /* 4350mV */ +#define AXP288_REG_UPDATE_INTERVAL (60 * HZ) + #define AXP288_EXTCON_DEV_NAME "axp288_extcon" #define USB_HOST_EXTCON_HID "INT3496" #define USB_HOST_EXTCON_NAME "INT3496:00" @@ -125,6 +121,7 @@ struct axp288_chrg_info { struct regmap_irq_chip_data *regmap_irqc; int irq[CHRG_INTR_END]; struct power_supply *psy_usb; + struct mutex lock; /* OTG/Host mode */ struct { @@ -145,6 +142,12 @@ struct axp288_chrg_info { int cv; int max_cc; int max_cv; + + unsigned long last_updated; + unsigned int input_status; + unsigned int op_mode; + unsigned int backend_control; + bool valid; }; static inline int axp288_charger_set_cc(struct axp288_chrg_info *info, int cc) @@ -175,18 +178,18 @@ static inline int axp288_charger_set_cv(struct axp288_chrg_info *info, int cv) u8 reg_val; int ret; - if (cv <= CV_4100MV) { - reg_val = CHRG_CCCV_CV_4100MV; - cv = CV_4100MV; - } else if (cv <= CV_4150MV) { - reg_val = CHRG_CCCV_CV_4150MV; - cv = CV_4150MV; - } else if (cv <= CV_4200MV) { + if (cv >= CV_4350MV) { + reg_val = CHRG_CCCV_CV_4350MV; + cv = CV_4350MV; + } else if (cv >= CV_4200MV) { reg_val = CHRG_CCCV_CV_4200MV; cv = CV_4200MV; + } else if (cv >= CV_4150MV) { + reg_val = CHRG_CCCV_CV_4150MV; + cv = CV_4150MV; } else { - reg_val = CHRG_CCCV_CV_4350MV; - cv = CV_4350MV; + reg_val = CHRG_CCCV_CV_4100MV; + cv = CV_4100MV; } reg_val = reg_val << CHRG_CCCV_CV_BIT_POS; @@ -204,11 +207,8 @@ static inline int axp288_charger_set_cv(struct axp288_chrg_info *info, int cv) static int axp288_charger_get_vbus_inlmt(struct axp288_chrg_info *info) { unsigned int val; - int ret; - ret = regmap_read(info->regmap, AXP20X_CHRG_BAK_CTRL, &val); - if (ret < 0) - return ret; + val = info->backend_control; val >>= CHRG_VBUS_ILIM_BIT_POS; switch (val) { @@ -302,63 +302,19 @@ static int axp288_charger_enable_charger(struct axp288_chrg_info *info, return ret; } -static int axp288_charger_is_present(struct axp288_chrg_info *info) -{ - int ret, present = 0; - unsigned int val; - - ret = regmap_read(info->regmap, AXP20X_PWR_INPUT_STATUS, &val); - if (ret < 0) - return ret; - - if (val & PS_STAT_VBUS_PRESENT) - present = 1; - return present; -} - -static int axp288_charger_is_online(struct axp288_chrg_info *info) -{ - int ret, online = 0; - unsigned int val; - - ret = regmap_read(info->regmap, AXP20X_PWR_INPUT_STATUS, &val); - if (ret < 0) - return ret; - - if (val & PS_STAT_VBUS_VALID) - online = 1; - return online; -} - static int axp288_get_charger_health(struct axp288_chrg_info *info) { - int ret, pwr_stat, chrg_stat; - int health = POWER_SUPPLY_HEALTH_UNKNOWN; - unsigned int val; - - ret = regmap_read(info->regmap, AXP20X_PWR_INPUT_STATUS, &val); - if ((ret < 0) || !(val & PS_STAT_VBUS_PRESENT)) - goto health_read_fail; - else - pwr_stat = val; - - ret = regmap_read(info->regmap, AXP20X_PWR_OP_MODE, &val); - if (ret < 0) - goto health_read_fail; + if (!(info->input_status & PS_STAT_VBUS_PRESENT)) + return POWER_SUPPLY_HEALTH_UNKNOWN; + + if (!(info->input_status & PS_STAT_VBUS_VALID)) + return POWER_SUPPLY_HEALTH_DEAD; + else if (info->op_mode & CHRG_STAT_PMIC_OTP) + return POWER_SUPPLY_HEALTH_OVERHEAT; + else if (info->op_mode & CHRG_STAT_BAT_SAFE_MODE) + return POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE; else - chrg_stat = val; - - if (!(pwr_stat & PS_STAT_VBUS_VALID)) - health = POWER_SUPPLY_HEALTH_DEAD; - else if (chrg_stat & CHRG_STAT_PMIC_OTP) - health = POWER_SUPPLY_HEALTH_OVERHEAT; - else if (chrg_stat & CHRG_STAT_BAT_SAFE_MODE) - health = POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE; - else - health = POWER_SUPPLY_HEALTH_GOOD; - -health_read_fail: - return health; + return POWER_SUPPLY_HEALTH_GOOD; } static int axp288_charger_usb_set_property(struct power_supply *psy, @@ -369,30 +325,86 @@ static int axp288_charger_usb_set_property(struct power_supply *psy, int ret = 0; int scaled_val; + mutex_lock(&info->lock); switch (psp) { case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: scaled_val = min(val->intval, info->max_cc); scaled_val = DIV_ROUND_CLOSEST(scaled_val, 1000); ret = axp288_charger_set_cc(info, scaled_val); - if (ret < 0) + if (ret < 0) { dev_warn(&info->pdev->dev, "set charge current failed\n"); + goto out; + } break; case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: - scaled_val = min(val->intval, info->max_cv); - scaled_val = DIV_ROUND_CLOSEST(scaled_val, 1000); + scaled_val = DIV_ROUND_CLOSEST(val->intval, 1000); + scaled_val = min(scaled_val, info->max_cv); ret = axp288_charger_set_cv(info, scaled_val); - if (ret < 0) + if (ret < 0) { dev_warn(&info->pdev->dev, "set charge voltage failed\n"); + goto out; + } break; case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: ret = axp288_charger_set_vbus_inlmt(info, val->intval); - if (ret < 0) + if (ret < 0) { dev_warn(&info->pdev->dev, "set input current limit failed\n"); + goto out; + } + info->valid = false; break; default: ret = -EINVAL; } +out: + mutex_unlock(&info->lock); + return ret; +} + +static int axp288_charger_reg_readb(struct axp288_chrg_info *info, int reg, unsigned int *ret_val) +{ + int ret; + + ret = regmap_read(info->regmap, reg, ret_val); + if (ret < 0) { + dev_err(&info->pdev->dev, "Error %d on reading value from register 0x%04x\n", + ret, + reg); + return ret; + } + return 0; +} + +static int axp288_charger_usb_update_property(struct axp288_chrg_info *info) +{ + int ret = 0; + + if (info->valid && time_before(jiffies, info->last_updated + AXP288_REG_UPDATE_INTERVAL)) + return 0; + + dev_dbg(&info->pdev->dev, "Charger updating register values...\n"); + + ret = iosf_mbi_block_punit_i2c_access(); + if (ret < 0) + return ret; + + ret = axp288_charger_reg_readb(info, AXP20X_PWR_INPUT_STATUS, &info->input_status); + if (ret < 0) + goto out; + + ret = axp288_charger_reg_readb(info, AXP20X_PWR_OP_MODE, &info->op_mode); + if (ret < 0) + goto out; + + ret = axp288_charger_reg_readb(info, AXP20X_CHRG_BAK_CTRL, &info->backend_control); + if (ret < 0) + goto out; + + info->last_updated = jiffies; + info->valid = true; +out: + iosf_mbi_unblock_punit_i2c_access(); return ret; } @@ -403,6 +415,11 @@ static int axp288_charger_usb_get_property(struct power_supply *psy, struct axp288_chrg_info *info = power_supply_get_drvdata(psy); int ret; + mutex_lock(&info->lock); + ret = axp288_charger_usb_update_property(info); + if (ret < 0) + goto out; + switch (psp) { case POWER_SUPPLY_PROP_PRESENT: /* Check for OTG case first */ @@ -410,10 +427,7 @@ static int axp288_charger_usb_get_property(struct power_supply *psy, val->intval = 0; break; } - ret = axp288_charger_is_present(info); - if (ret < 0) - return ret; - val->intval = ret; + val->intval = (info->input_status & PS_STAT_VBUS_PRESENT) ? 1 : 0; break; case POWER_SUPPLY_PROP_ONLINE: /* Check for OTG case first */ @@ -421,10 +435,7 @@ static int axp288_charger_usb_get_property(struct power_supply *psy, val->intval = 0; break; } - ret = axp288_charger_is_online(info); - if (ret < 0) - return ret; - val->intval = ret; + val->intval = (info->input_status & PS_STAT_VBUS_VALID) ? 1 : 0; break; case POWER_SUPPLY_PROP_HEALTH: val->intval = axp288_get_charger_health(info); @@ -442,16 +453,15 @@ static int axp288_charger_usb_get_property(struct power_supply *psy, val->intval = info->max_cv * 1000; break; case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: - ret = axp288_charger_get_vbus_inlmt(info); - if (ret < 0) - return ret; - val->intval = ret; + val->intval = axp288_charger_get_vbus_inlmt(info); break; default: - return -EINVAL; + ret = -EINVAL; } - return 0; +out: + mutex_unlock(&info->lock); + return ret; } static int axp288_charger_property_is_writeable(struct power_supply *psy, @@ -547,12 +557,63 @@ static irqreturn_t axp288_charger_irq_thread_handler(int irq, void *dev) dev_warn(&info->pdev->dev, "Spurious Interrupt!!!\n"); goto out; } - + mutex_lock(&info->lock); + info->valid = false; + mutex_unlock(&info->lock); power_supply_changed(info->psy_usb); out: return IRQ_HANDLED; } +/* + * The HP Pavilion x2 10 series comes in a number of variants: + * Bay Trail SoC + AXP288 PMIC, Micro-USB, DMI_BOARD_NAME: "8021" + * Bay Trail SoC + AXP288 PMIC, Type-C, DMI_BOARD_NAME: "815D" + * Cherry Trail SoC + AXP288 PMIC, Type-C, DMI_BOARD_NAME: "813E" + * Cherry Trail SoC + TI PMIC, Type-C, DMI_BOARD_NAME: "827C" or "82F4" + * + * The variants with the AXP288 + Type-C connector are all kinds of special: + * + * 1. They use a Type-C connector which the AXP288 does not support, so when + * using a Type-C charger it is not recognized. Unlike most AXP288 devices, + * this model actually has mostly working ACPI AC / Battery code, the ACPI code + * "solves" this by simply setting the input_current_limit to 3A. + * There are still some issues with the ACPI code, so we use this native driver, + * and to solve the charging not working (500mA is not enough) issue we hardcode + * the 3A input_current_limit like the ACPI code does. + * + * 2. If no charger is connected the machine boots with the vbus-path disabled. + * Normally this is done when a 5V boost converter is active to avoid the PMIC + * trying to charge from the 5V boost converter's output. This is done when + * an OTG host cable is inserted and the ID pin on the micro-B receptacle is + * pulled low and the ID pin has an ACPI event handler associated with it + * which re-enables the vbus-path when the ID pin is pulled high when the + * OTG host cable is removed. The Type-C connector has no ID pin, there is + * no ID pin handler and there appears to be no 5V boost converter, so we + * end up not charging because the vbus-path is disabled, until we unplug + * the charger which automatically clears the vbus-path disable bit and then + * on the second plug-in of the adapter we start charging. To solve the not + * charging on first charger plugin we unconditionally enable the vbus-path at + * probe on this model, which is safe since there is no 5V boost converter. + */ +static const struct dmi_system_id axp288_hp_x2_dmi_ids[] = { + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Hewlett-Packard"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "HP Pavilion x2 Detachable"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "815D"), + }, + }, + { + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "HP"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "HP Pavilion x2 Detachable"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "813E"), + }, + }, + {} /* Terminating entry */ +}; + static void axp288_charger_extcon_evt_worker(struct work_struct *work) { struct axp288_chrg_info *info = @@ -571,12 +632,19 @@ static void axp288_charger_extcon_evt_worker(struct work_struct *work) if (!(val & PS_STAT_VBUS_VALID)) { dev_dbg(&info->pdev->dev, "USB charger disconnected\n"); axp288_charger_enable_charger(info, false); + mutex_lock(&info->lock); + info->valid = false; + mutex_unlock(&info->lock); power_supply_changed(info->psy_usb); return; } /* Determine cable/charger type */ - if (extcon_get_state(edev, EXTCON_CHG_USB_SDP) > 0) { + if (dmi_check_system(axp288_hp_x2_dmi_ids)) { + /* See comment above axp288_hp_x2_dmi_ids declaration */ + dev_dbg(&info->pdev->dev, "HP X2 with Type-C, setting inlmt to 3A\n"); + current_limit = 3000000; + } else if (extcon_get_state(edev, EXTCON_CHG_USB_SDP) > 0) { dev_dbg(&info->pdev->dev, "USB SDP charger is connected\n"); current_limit = 500000; } else if (extcon_get_state(edev, EXTCON_CHG_USB_CDP) > 0) { @@ -598,6 +666,9 @@ static void axp288_charger_extcon_evt_worker(struct work_struct *work) dev_err(&info->pdev->dev, "error setting current limit (%d)\n", ret); + mutex_lock(&info->lock); + info->valid = false; + mutex_unlock(&info->lock); power_supply_changed(info->psy_usb); } @@ -693,6 +764,23 @@ static int charger_init_hw_regs(struct axp288_chrg_info *info) return ret; } + if (dmi_check_system(axp288_hp_x2_dmi_ids)) { + /* See comment above axp288_hp_x2_dmi_ids declaration */ + ret = axp288_charger_vbus_path_select(info, true); + if (ret < 0) + return ret; + } else { + /* Set Vhold to the factory default / recommended 4.4V */ + val = VBUS_ISPOUT_VHOLD_SET_4400MV << VBUS_ISPOUT_VHOLD_SET_BIT_POS; + ret = regmap_update_bits(info->regmap, AXP20X_VBUS_IPSOUT_MGMT, + VBUS_ISPOUT_VHOLD_SET_MASK, val); + if (ret < 0) { + dev_err(&info->pdev->dev, "register(%x) write error(%d)\n", + AXP20X_VBUS_IPSOUT_MGMT, ret); + return ret; + } + } + /* Read current charge voltage and current limit */ ret = regmap_read(info->regmap, AXP20X_CHRG_CTRL1, &val); if (ret < 0) { @@ -748,9 +836,17 @@ static int axp288_charger_probe(struct platform_device *pdev) struct device *dev = &pdev->dev; struct axp20x_dev *axp20x = dev_get_drvdata(pdev->dev.parent); struct power_supply_config charger_cfg = {}; + const char *extcon_name = NULL; unsigned int val; /* + * Normally the native AXP288 fg/charger drivers are preferred but + * on some devices the ACPI drivers should be used instead. + */ + if (!acpi_quirk_skip_acpi_ac_and_battery()) + return -ENODEV; + + /* * On some devices the fuelgauge and charger parts of the axp288 are * not used, check that the fuelgauge is enabled (CC_CTRL != 0). */ @@ -760,29 +856,42 @@ static int axp288_charger_probe(struct platform_device *pdev) if (val == 0) return -ENODEV; - info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); + info = devm_kzalloc(dev, sizeof(*info), GFP_KERNEL); if (!info) return -ENOMEM; + mutex_init(&info->lock); info->pdev = pdev; info->regmap = axp20x->regmap; info->regmap_irqc = axp20x->regmap_irqc; info->cable.edev = extcon_get_extcon_dev(AXP288_EXTCON_DEV_NAME); - if (info->cable.edev == NULL) { - dev_dbg(&pdev->dev, "%s is not ready, probe deferred\n", - AXP288_EXTCON_DEV_NAME); - return -EPROBE_DEFER; + if (IS_ERR(info->cable.edev)) { + dev_err_probe(dev, PTR_ERR(info->cable.edev), + "extcon_get_extcon_dev(%s) failed\n", + AXP288_EXTCON_DEV_NAME); + return PTR_ERR(info->cable.edev); } - if (acpi_dev_present(USB_HOST_EXTCON_HID, NULL, -1)) { - info->otg.cable = extcon_get_extcon_dev(USB_HOST_EXTCON_NAME); - if (info->otg.cable == NULL) { - dev_dbg(dev, "EXTCON_USB_HOST is not ready, probe deferred\n"); - return -EPROBE_DEFER; + /* + * On devices with broken ACPI GPIO event handlers there also is no ACPI + * "INT3496" (USB_HOST_EXTCON_HID) device. x86-android-tablets.ko + * instantiates an "intel-int3496" extcon on these devs as a workaround. + */ + if (acpi_quirk_skip_gpio_event_handlers()) + extcon_name = "intel-int3496"; + else if (acpi_dev_present(USB_HOST_EXTCON_HID, NULL, -1)) + extcon_name = USB_HOST_EXTCON_NAME; + + if (extcon_name) { + info->otg.cable = extcon_get_extcon_dev(extcon_name); + if (IS_ERR(info->otg.cable)) { + dev_err_probe(dev, PTR_ERR(info->otg.cable), + "extcon_get_extcon_dev(%s) failed\n", + USB_HOST_EXTCON_NAME); + return PTR_ERR(info->otg.cable); } - dev_info(&pdev->dev, - "Using " USB_HOST_EXTCON_HID " extcon for usb-id\n"); + dev_info(dev, "Using " USB_HOST_EXTCON_HID " extcon for usb-id\n"); } platform_set_drvdata(pdev, info); @@ -821,7 +930,7 @@ static int axp288_charger_probe(struct platform_device *pdev) INIT_WORK(&info->otg.work, axp288_charger_otg_evt_worker); info->otg.id_nb.notifier_call = axp288_charger_handle_otg_evt; if (info->otg.cable) { - ret = devm_extcon_register_notifier(&pdev->dev, info->otg.cable, + ret = devm_extcon_register_notifier(dev, info->otg.cable, EXTCON_USB_HOST, &info->otg.id_nb); if (ret) { dev_err(dev, "failed to register EXTCON_USB_HOST notifier\n"); @@ -833,6 +942,9 @@ static int axp288_charger_probe(struct platform_device *pdev) /* Register charger interrupts */ for (i = 0; i < CHRG_INTR_END; i++) { pirq = platform_get_irq(info->pdev, i); + if (pirq < 0) + return pirq; + info->irq[i] = regmap_irq_get_virq(info->regmap_irqc, pirq); if (info->irq[i] < 0) { dev_warn(&info->pdev->dev, @@ -843,7 +955,7 @@ static int axp288_charger_probe(struct platform_device *pdev) NULL, axp288_charger_irq_thread_handler, IRQF_ONESHOT, info->pdev->name, info); if (ret) { - dev_err(&pdev->dev, "failed to request interrupt=%d\n", + dev_err(dev, "failed to request interrupt=%d\n", info->irq[i]); return ret; } |
