diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig index b4c39d16bed9..8ad785d54a4a 100644 --- a/drivers/power/supply/Kconfig +++ b/drivers/power/supply/Kconfig @@ -934,4 +934,14 @@ config CHARGER_QCOM_SMB2 Say Y or M here to enable reporting the charger status and rate on supported platforms such as Snapdragon 845 and 835 based phones. +config BATTERY_PMI8998_FG + tristate "Qualcomm PMI8998 fuel gauge driver" + depends on MFD_SPMI_PMIC + help + Say Y here to enable the Qualcomm PMI8998 Fuel Gauge driver. This + adds support for battery fuel gauging and state of charge of + battery connected tothe fuel gauge. The state of charge is + reported through a BMS power supply property and also sends + uevents when the capacity is updated. + endif # POWER_SUPPLY diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile index edfaa4c08c20..dfa2b0382a46 100644 --- a/drivers/power/supply/Makefile +++ b/drivers/power/supply/Makefile @@ -111,3 +111,4 @@ obj-$(CONFIG_BATTERY_ACER_A500) += acer_a500_battery.o obj-$(CONFIG_BATTERY_SURFACE) += surface_battery.o obj-$(CONFIG_CHARGER_SURFACE) += surface_charger.o obj-$(CONFIG_BATTERY_UG3105) += ug3105_battery.o +obj-$(CONFIG_BATTERY_PMI8998_FG) += pmi8998_fg.o diff --git a/drivers/power/supply/pmi8998_fg.c b/drivers/power/supply/pmi8998_fg.c new file mode 100644 index 000000000000..ba2cf70ecc15 --- /dev/null +++ b/drivers/power/supply/pmi8998_fg.c @@ -0,0 +1,526 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (c) 2020, The Linux Foundation. All rights reserved. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pmi8998_fg.h" + +/************************ + * IO FUNCTIONS + * **********************/ + +/** + * pmi8998_read() - Read multiple registers with regmap_bulk_read + * + * @param map The regmap to read + * @param val Pointer to read values into + * @param addr Address to read from + * @param len Number of registers (bytes) to read + * @return int 0 on success, negative errno on error + */ +static int pmi8998_read(struct regmap *map, u8 *val, u16 addr, int len) +{ + if ((addr & 0xff00) == 0) { + pr_err("base cannot be zero base=0x%02x\n", addr); + return -EINVAL; + } + + pr_info("%s: reading 0x%x bytes from 0x%x", __func__, len, addr); + + return regmap_bulk_read(map, addr, val, len); +} + +/** + * @brief pmi8998_write() - Write multiple registers with regmap_bulk_write + * + * @param map The regmap to write + * @param val Pointer to write values into + * @param addr Address to write from + * @param len Number of registers (bytes) to write + * @return int 0 on success, negative errno on error + */ +static int pmi8998_write(struct regmap *map, u8 *val, u16 addr, int len) +{ + int rc; + bool sec_access = (addr & 0xff) > 0xd0; + u8 sec_addr_val = 0xa5; + + if (sec_access) { + rc = regmap_bulk_write(map, + (addr & 0xff00) | 0xd0, + &sec_addr_val, 1); + } + + if ((addr & 0xff00) == 0) { + pr_err("addr cannot be zero base=0x%02x\n", addr); + return -EINVAL; + } + + return regmap_bulk_write(map, addr, val, len); +} + +/** + * @brief pmi8998_masked_write() - like pmi8998_write but applies + * a mask first. + * + * @param map The regmap to write + * @param val Pointer to write values into + * @param addr Address to write from + * @param len Number of registers (bytes) to write + * @return int 0 on success, negative errno on error + */ +static int pmi8998_masked_write(struct regmap *map, u16 addr, + u8 mask, u8 val) +{ + int error; + u8 reg; + error = pmi8998_read(map, ®, addr, 1); + if (error) + return error; + + reg &= ~mask; + reg |= val & mask; + + error = pmi8998_write(map, ®, addr, 1); + return error; +} + +static int64_t twos_compliment_extend(int64_t val, int nbytes) +{ + int i; + int64_t mask; + + mask = 0x80LL << ((nbytes - 1) * 8); + if (val & mask) { + for (i = 8; i > nbytes; i--) { + mask = 0xFFLL << ((i - 1) * 8); + val |= mask; + } + } + return val; +} + +/************************* + * Battery Status RW + * ***********************/ + +static int pmi8998_fg_get_capacity(struct pmi8998_fg_chip *chip, int *val) +{ + u8 cap[2]; + int error = pmi8998_read(chip->regmap, cap, REG_BASE(chip) + BATT_MONOTONIC_SOC, 2); + if (error) + return error; + if (cap[0] != cap[1]) { + cap[0] = cap[0] < cap[1] ? cap[0] : cap[1]; + } + *val = DIV_ROUND_CLOSEST((cap[0] - 1) * 98, 0xff - 2) + 1; + return 0; +} + +static bool pmi8998_battery_missing(struct pmi8998_fg_chip *chip) +{ + int rc; + u8 fg_batt_sts; + + rc = pmi8998_read(chip->regmap, &fg_batt_sts, + REG_BATT(chip) + INT_RT_STS, 1); + if (rc) { + pr_warn("read read failed: addr=%03X, rc=%d\n", + REG_BATT(chip) + INT_RT_STS, rc); + return false; + } + + // Bit 6 is set if the battery is missing + return (fg_batt_sts & BIT(6)) ? true : false; +} + +static int pmi8998_fg_get_temperature(struct pmi8998_fg_chip *chip, int *val) +{ + int rc, temp; + u8 readval[2]; + + rc = pmi8998_read(chip->regmap, readval, REG_BATT(chip) + PARAM_ADDR_BATT_TEMP, 2); + if (rc) { + pr_err("Failed to read temperature\n"); + return rc; + } + temp = ((readval[1] & BATT_TEMP_MSB_MASK) << 8) | + (readval[0] & BATT_TEMP_LSB_MASK); + temp = DIV_ROUND_CLOSEST(temp * 10, 4); + + *val = temp -2730; + return 0; +} + +static int pmi8998_fg_get_current(struct pmi8998_fg_chip *chip, int *val) +{ + int rc, temp; + u8 readval[2]; + + rc = pmi8998_read(chip->regmap, readval, REG_BATT(chip) + PARAM_ADDR_BATT_CURRENT, 2); + if (rc) { + pr_err("Failed to read current\n"); + return rc; + } + + temp = readval[1] << 8 | readval[0]; + temp = twos_compliment_extend(temp, 2); + *val = div_s64((s64)temp * 488281, + 1000); + return 0; +} + +static int pmi8998_fg_get_voltage(struct pmi8998_fg_chip *chip, int *val) +{ + int rc, temp; + u8 readval[2]; + + rc = pmi8998_read(chip->regmap, readval, REG_BATT(chip) + PARAM_ADDR_BATT_VOLTAGE, 2); + if (rc) { + pr_err("Failed to read voltage\n"); + return rc; + } + + temp = readval[1] << 8 | readval[0]; + temp = twos_compliment_extend(temp, 2); + *val = div_s64((s64)temp * 122070, + 1000); + return 0; +} + +static int pmi8998_fg_get_max_charge_design(struct pmi8998_fg_chip *chip, int *val) +{ + int rc, temp; + u8 readval[2]; + + rc = pmi8998_read(chip->regmap, readval, REG_BATT(chip) + BATT_INFO_CHARGE_MAX_DESIGN, 2); + if (rc) { + pr_err("Failed to read voltage\n"); + return rc; + } + + temp = readval[1] << 8 | readval[0]; + temp = twos_compliment_extend(temp, 2); + *val = div_s64((s64)temp * 122070, + 1000); + return 0; +} + +/******************** + * Init stuff + * ******************/ + +static int pmi8998_iacs_clear_sequence(struct pmi8998_fg_chip *chip) +{ + int rc = 0; + u8 temp; + + /* clear the error */ + rc = pmi8998_masked_write(chip->regmap, REG_MEM(chip) + MEM_INTF_IMA_CFG, + BIT(2), BIT(2)); + if (rc) { + pr_err("Error writing to IMA_CFG, rc=%d\n", rc); + return rc; + } + + temp = 0x4; + rc = pmi8998_write(chip->regmap, &temp, REG_MEM(chip) + MEM_INTF_ADDR_LSB + 1, 1); + if (rc) { + pr_err("Error writing to MEM_INTF_ADDR_MSB, rc=%d\n", rc); + return rc; + } + + temp = 0x0; + rc = pmi8998_write(chip->regmap, &temp, REG_MEM(chip) + MEM_INTF_WR_DATA0 + 3, 1); + if (rc) { + pr_err("Error writing to WR_DATA3, rc=%d\n", rc); + return rc; + } + + rc = pmi8998_read(chip->regmap, &temp, REG_MEM(chip) + MEM_INTF_RD_DATA0 + 3, 1); + if (rc) { + pr_err("Error writing to RD_DATA3, rc=%d\n", rc); + return rc; + } + + rc = pmi8998_masked_write(chip->regmap, REG_MEM(chip) + MEM_INTF_IMA_CFG, + BIT(2), 0); + if (rc) { + pr_err("Error writing to IMA_CFG, rc=%d\n", rc); + return rc; + } + return rc; +} + +static int pmi8998_clear_ima(struct pmi8998_fg_chip *chip, + bool check_hw_sts) +{ + int rc = 0, ret = 0; + u8 err_sts = 0, exp_sts = 0, hw_sts = 0; + bool run_err_clr_seq = false; + + rc = pmi8998_read(chip->regmap, &err_sts, + REG_MEM(chip) + MEM_INTF_IMA_ERR_STS, 1); + if (rc) { + dev_err(chip->dev, "failed to read IMA_ERR_STS, rc=%d\n", rc); + return rc; + } + + rc = pmi8998_read(chip->regmap, &exp_sts, + REG_MEM(chip) + MEM_INTF_IMA_EXP_STS, 1); + if (rc) { + dev_err(chip->dev, "Error in reading IMA_EXP_STS, rc=%d\n", rc); + return rc; + } + + if (check_hw_sts) { + rc = pmi8998_read(chip->regmap, &hw_sts, + REG_MEM(chip) + MEM_INTF_IMA_HW_STS, 1); + if (rc) { + dev_err(chip->dev, "Error in reading IMA_HW_STS, rc=%d\n", rc); + return rc; + } + /* + * Lower nibble should be equal to upper nibble before SRAM + * transactions begins from SW side. + */ + if ((hw_sts & 0x0f) != hw_sts >> 4) { + dev_err(chip->dev, "IMA HW not in correct state, hw_sts=%x\n", + hw_sts); + run_err_clr_seq = true; + } + } + + if (exp_sts & (BIT(0) | BIT(1) | BIT(3) | + BIT(4) | BIT(5) | BIT(6) | + BIT(7))) { + dev_warn(chip->dev, "IMA exception bit set, exp_sts=%x\n", exp_sts); + run_err_clr_seq = true; + } + + if (run_err_clr_seq) { + ret = pmi8998_iacs_clear_sequence(chip); + if (!ret) + return -EAGAIN; + else + dev_err(chip->dev, "Error clearing IMA exception ret=%d\n", ret); + } + + return rc; +} + +static int fg_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct pmi8998_fg_chip *chip = power_supply_get_drvdata(psy); + int error = 0; + + dev_info(chip->dev, "Getting property: %d", psp); + + switch (psp) { + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = "QCOM"; + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = "PMI8998 Battery"; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + case POWER_SUPPLY_PROP_CAPACITY: + error = pmi8998_fg_get_capacity(chip, &val->intval); + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + error = pmi8998_fg_get_current(chip, &val->intval); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + error = pmi8998_fg_get_voltage(chip, &val->intval); + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = chip->batt_info.batt_max_voltage_uv; + break; + case POWER_SUPPLY_PROP_TEMP: + error = pmi8998_fg_get_temperature(chip, &val->intval); + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN: + val->intval = 3370000; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + case POWER_SUPPLY_PROP_CHARGE_FULL: /* TODO: Implement learning */ + val->intval = chip->batt_info.nom_cap_uah; + break; + case POWER_SUPPLY_PROP_STATUS: + val->intval = POWER_SUPPLY_STATUS_CHARGING; + //error = smb2_chg_get_status(chip, &val->intval); + break; + case POWER_SUPPLY_PROP_HEALTH: + val->intval = POWER_SUPPLY_HEALTH_GOOD; + break; + default: + pr_err("invalid property: %d\n", psp); + return -EINVAL; + } + return error; +} + +static const struct power_supply_desc bms_psy_desc = { + .name = "pmi8998-bms", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = fg_properties, + .num_properties = ARRAY_SIZE(fg_properties), + .get_property = fg_get_property, +}; + +static int pmi8998_fg_of_battery_init(struct pmi8998_fg_chip *chip){ + struct device_node *batt_node; + struct device_node *node = chip->dev->of_node; + int rc = 0; + + batt_node = of_find_node_by_name(node, "qcom,battery-data"); + if (!batt_node) { + pr_err("No available batterydata\n"); + return rc; + } + + of_property_read_u32(batt_node, "qcom,max-voltage-uv", + &chip->batt_info.batt_max_voltage_uv_design); + + // Can be read from SRAM, hardcode in DTS for now as reading SRAM is HARD! + of_property_read_u32(batt_node, "qcom,design-capacity", + &chip->batt_info.nom_cap_uah); + + return rc; +} + +static int pmi8998_fg_probe(struct platform_device *pdev) +{ + struct power_supply_config supply_config = {}; + struct pmi8998_fg_chip *chip; + const __be32 *prop_addr; + int rc = 0; + u8 dma_status; + bool error_present; + + chip = devm_kzalloc(&pdev->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) { + return -ENOMEM; + } + + chip->dev = &pdev->dev; + mutex_init(&chip->lock); + + chip->regmap = dev_get_regmap(pdev->dev.parent, NULL); + if (!chip->regmap) { + dev_err(chip->dev, "failed to locate the regmap\n"); + return -ENODEV; + } + + // Get base address + prop_addr = of_get_address(pdev->dev.of_node, 0, NULL, NULL); + if (!prop_addr) { + dev_err(chip->dev, "Couldn't read SOC base address from dt\n"); + return -EINVAL; + } + chip->base = be32_to_cpu(*prop_addr); + + // Init memif fn inlined here (chip hardware info) + rc = pmi8998_read(chip->regmap, chip->revision, REG_MEM(chip) + DIG_MINOR, 4); + if (rc) { + dev_err(chip->dev, "Unable to read FG revision rc=%d\n", rc); + return rc; + } + + dev_dbg(chip->dev, "pmi8998 revision DIG:%d.%d ANA:%d.%d\n", + chip->revision[DIG_MAJOR], chip->revision[DIG_MINOR], + chip->revision[ANA_MAJOR], chip->revision[ANA_MINOR]); + + /* + * Change the FG_MEM_INT interrupt to track IACS_READY + * condition instead of end-of-transaction. This makes sure + * that the next transaction starts only after the hw is ready. + * IACS_INTR_SRC_SLCT is BIT(3) + */ + rc = pmi8998_masked_write(chip->regmap, + REG_MEM(chip) + MEM_INTF_IMA_CFG, BIT(3), BIT(3)); + if (rc) { + dev_err(chip->dev, + "failed to configure interrupt source %d\n", + rc); + return rc; + } + + rc = pmi8998_clear_ima(chip, true); + if (rc && rc != -EAGAIN) { + dev_err(chip->dev, "Error clearing IMA, exception rc=%d", rc); + return rc; + } + + // Check and clear DMA errors + rc = pmi8998_read(chip->regmap, &dma_status, REG_MEM(chip) + 0x70, 1); + if (rc < 0) { + pr_err("failed to read dma_status, rc=%d\n", rc); + return rc; + } + + error_present = dma_status & (BIT(1) | BIT(2)); + rc = pmi8998_masked_write(chip->regmap, REG_MEM(chip) + 0x71, BIT(0), + error_present ? BIT(0) : 0); + if (rc < 0) { + pr_err("failed to write dma_ctl, rc=%d\n", rc); + return rc; + } + + dev_dbg(chip->dev, "probed revision DIG:%d.%d ANA:%d.%d\n", + chip->revision[DIG_MAJOR], chip->revision[DIG_MINOR], + chip->revision[ANA_MAJOR], chip->revision[ANA_MINOR]); + + supply_config.drv_data = chip; + supply_config.of_node = pdev->dev.of_node; + + chip->bms_psy = devm_power_supply_register(chip->dev, + &bms_psy_desc, &supply_config); + if (IS_ERR(chip->bms_psy)) { + dev_err(&pdev->dev, "failed to register battery\n"); + return PTR_ERR(chip->bms_psy); + } + + platform_set_drvdata(pdev, chip); + return 0; +} + +static int pmi8998_fg_remove(struct platform_device *pdev) +{ + return 0; +} + +static const struct of_device_id fg_match_id_table[] = { + { .compatible = "qcom,pmi8998-fg" }, + { /* sentinal */ } +}; +MODULE_DEVICE_TABLE(of, fg_match_id_table); + +static struct platform_driver qcom_fg_driver = { + .probe = pmi8998_fg_probe, + .remove = pmi8998_fg_remove, + .driver = { + .name = "pmi8998-fg", + .of_match_table = fg_match_id_table, + }, +}; + +module_platform_driver(qcom_fg_driver); + +MODULE_AUTHOR("Caleb Connolly "); +MODULE_AUTHOR("Joel Selvaraj "); +MODULE_DESCRIPTION("Qualcomm PMI8998 Fuel Guage Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/power/supply/pmi8998_fg.h b/drivers/power/supply/pmi8998_fg.h new file mode 100644 index 000000000000..20dd2d2fc6e8 --- /dev/null +++ b/drivers/power/supply/pmi8998_fg.h @@ -0,0 +1,138 @@ + +// #define FG_PARAM_MAX 49 + +/**** Registers *****/ + +// pmi8998 v2 specific +#define BATT_INFO_CHARGE_MAX_DESIGN 0x4a +#define MEM_INTF_CFG 0x50 +#define MEM_INTF_ADDR_LSB 0x61 +#define MEM_INTF_RD_DATA0 0x67 +#define MEM_INTF_WR_DATA0 0x63 + +#define SMB2_CABLE_CONNECTED 0x06 + +// pm8950 / pm89988 common +#define MEM_INTF_IMA_CFG 0x52 +#define MEM_INTF_IMA_OPR_STS 0x54 +#define MEM_INTF_IMA_EXP_STS 0x55 +#define MEM_INTF_IMA_HW_STS 0x56 +#define MEM_INTF_BEAT_COUNT 0x57 +#define MEM_INTF_IMA_ERR_STS 0x5f +#define MEM_INTF_IMA_BYTE_EN 0x60 + +#define BATT_INFO_THERM_C1 0x5c +#define BATT_INFO_VBATT_LSB 0xa0 +#define BATT_INFO_VBATT_MSB 0xa1 +#define BATT_INFO_IBATT_LSB 0xa2 +#define BATT_INFO_IBATT_MSB 0xa3 +#define BATT_INFO_BATT_TEMP_LSB 0x50 +#define BATT_INFO_BATT_TEMP_MSB 0x51 +#define BATT_MONOTONIC_SOC 0x09 + +#define BATT_TEMP_LSB_MASK GENMASK(7, 0) +#define BATT_TEMP_MSB_MASK GENMASK(2, 0) + +#define REG_BASE(chip) (chip->base) +#define REG_BATT(chip) (chip->base + 0x100) +#define REG_MEM(chip) (chip->base + 0x400) + +/* Interrupt offsets */ +#define INT_RT_STS 0x10 +#define INT_EN_CLR 0x16 + +// Param addresses +#define PARAM_ADDR_BATT_TEMP 0x50 +#define PARAM_ADDR_BATT_VOLTAGE 0xa0 +#define PARAM_ADDR_BATT_CURRENT 0xa2 + +enum wa_flags { + PMI8998_V1_REV_WA, + PMI8998_V2_REV_WA, +}; + +enum pmi8998_rev_offsets { + DIG_MINOR = 0x0, + DIG_MAJOR = 0x1, + ANA_MINOR = 0x2, + ANA_MAJOR = 0x3, +}; +enum pmi8998_rev { + DIG_REV_1 = 0x1, + DIG_REV_2 = 0x2, + DIG_REV_3 = 0x3, +}; + +enum charger_status{ + TRICKLE_CHARGE = 0, + PRE_CHARGE, + FAST_CHARGE, + FULLON_CHARGE, + TAPER_CHARGE, + TERMINATE_CHARGE, + INHIBIT_CHARGE, + DISABLE_CHARGE, +}; + +static enum power_supply_property fg_properties[] = { + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_MIN, + POWER_SUPPLY_PROP_TEMP, + // POWER_SUPPLY_PROP_CHARGE_NOW, + // POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_STATUS, +}; + +struct fg_learning_data { + struct mutex learning_lock; + bool active; + int64_t cc_uah; + int learned_cc_uah; + int init_cc_pc_val; + int max_start_soc; + int max_increment; + int max_decrement; + int vbat_est_thr_uv; + int max_cap_limit; + int min_cap_limit; + int min_temp; + int max_temp; +}; + +struct battery_info { + const char *manufacturer; + const char *model; + const char *serial_num; + + int nom_cap_uah; + + int batt_max_voltage_uv_design; + int batt_max_voltage_uv; +}; + +struct pmi8998_fg_chip { + struct device *dev; + unsigned int base; + struct regmap *regmap; + struct mutex lock; + + struct power_supply *bms_psy; + + u8 revision[4]; + bool ima_supported; + + struct battery_info batt_info; + + struct fg_learning_data learning_data; + + int health; + int status; +};