From: Joel Selvaraj <[email protected]> Ths driver supports the fuel gauge hardware available on PMICs known as 3rd generation fuel gauge hardware available on PMI8998.
Co-developed-by: Casey Connolly <[email protected]> Co-developed-by: Barnabás Czémán <[email protected]> Signed-off-by: Barnabás Czémán <[email protected]> Co-developed-by: Yassine Oudjana <[email protected]> Signed-off-by: Yassine Oudjana <[email protected]> Signed-off-by: David Heidelberg <[email protected]> --- drivers/power/supply/Kconfig | 8 + drivers/power/supply/Makefile | 1 + drivers/power/supply/pmi8998_fg.c | 687 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 696 insertions(+) diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig index 92f9f7aae92f2..4024c6fe3fef2 100644 --- a/drivers/power/supply/Kconfig +++ b/drivers/power/supply/Kconfig @@ -746,6 +746,14 @@ config CHARGER_PM8916_LBC To compile this driver as module, choose M here: the module will be called pm8916_lbc. +config BATTERY_PMI8998_FG + tristate "Qualcomm PMI8998 PMIC fuel gauge driver" + depends on MFD_SPMI_PMIC + help + Say Y here to enable the Qualcomm PMI8998 PMIC Fuel Gauge driver. + This adds support for battery fuel gauging and state of charge of + battery connected to the fuel gauge. + config CHARGER_BQ2415X tristate "TI BQ2415x battery charger driver" depends on I2C diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile index 4b79d5abc49a7..03584efa2b1b0 100644 --- a/drivers/power/supply/Makefile +++ b/drivers/power/supply/Makefile @@ -96,6 +96,7 @@ obj-$(CONFIG_CHARGER_MT6370) += mt6370-charger.o obj-$(CONFIG_CHARGER_QCOM_SMBB) += qcom_smbb.o obj-$(CONFIG_BATTERY_PM8916_BMS_VM) += pm8916_bms_vm.o obj-$(CONFIG_CHARGER_PM8916_LBC) += pm8916_lbc.o +obj-$(CONFIG_BATTERY_PMI8998_FG) += pmi8998_fg.o obj-$(CONFIG_CHARGER_BQ2415X) += bq2415x_charger.o obj-$(CONFIG_CHARGER_BQ24190) += bq24190_charger.o obj-$(CONFIG_CHARGER_BQ24257) += bq24257_charger.o diff --git a/drivers/power/supply/pmi8998_fg.c b/drivers/power/supply/pmi8998_fg.c new file mode 100644 index 0000000000000..d5fccd16a013b --- /dev/null +++ b/drivers/power/supply/pmi8998_fg.c @@ -0,0 +1,687 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (c) 2020, The Linux Foundation. All rights reserved. */ + +#include <linux/device.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/math64.h> +#include <linux/module.h> +#include <linux/of_address.h> +#include <linux/of_device.h> +#include <linux/of_irq.h> +#include <linux/platform_device.h> +#include <linux/power_supply.h> +#include <linux/regmap.h> +#include <linux/types.h> +#include <linux/workqueue.h> + +/* SOC */ +#define BATT_MONOTONIC_SOC 0x009 + +/* BATT */ +#define PARAM_ADDR_BATT_TEMP 0x150 +#define BATT_INFO_JEITA_COLD 0x162 +#define BATT_INFO_JEITA_COOL 0x163 +#define BATT_INFO_JEITA_WARM 0x164 +#define BATT_INFO_JEITA_HOT 0x165 +#define PARAM_ADDR_BATT_VOLTAGE 0x1a0 +#define PARAM_ADDR_BATT_CURRENT 0x1a2 + +/* MEMIF */ +#define MEM_INTF_IMA_CFG 0x452 +#define MEM_INTF_IMA_EXP_STS 0x455 +#define MEM_INTF_IMA_HW_STS 0x456 +#define MEM_INTF_IMA_ERR_STS 0x45f +#define MEM_INTF_ADDR_LSB 0x461 +#define MEM_INTF_RD_DATA0 0x467 +#define MEM_INTF_WR_DATA0 0x463 +#define MEM_IF_DMA_STS 0x470 +#define MEM_IF_DMA_CTL 0x471 + +#define BATT_TEMP_LSB_MASK GENMASK(7, 0) +#define BATT_TEMP_MSB_MASK GENMASK(2, 0) + +struct pmi8998_fg_chip { + struct device *dev; + unsigned int base; + struct regmap *regmap; + struct notifier_block nb; + + struct power_supply *batt_psy; + struct power_supply *chg_psy; + int status; + struct delayed_work status_changed_work; +}; + +/* + * IO functions + */ + +/** + * @brief pmi8998_fg_read() - Read multiple registers with regmap_bulk_read + * + * @param chip Pointer to chip + * @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_fg_read(struct pmi8998_fg_chip *chip, u8 *val, u16 addr, int len) +{ + if (((chip->base + addr) & 0xff00) == 0) + return -EINVAL; + + dev_vdbg(chip->dev, "%s: Reading 0x%x bytes from 0x%x", __func__, len, addr); + + return regmap_bulk_read(chip->regmap, chip->base + addr, val, len); +} + +/** + * @brief pmi8998_fg_write() - Write multiple registers with regmap_bulk_write + * + * @param chip Pointer to chip + * @param val Pointer to write values from + * @param addr Address to write to + * @param len Number of registers (bytes) to write + * @return int 0 on success, negative errno on error + */ +static int pmi8998_fg_write(struct pmi8998_fg_chip *chip, u8 *val, u16 addr, int len) +{ + bool sec_access = (addr & 0xff) > 0xd0; + u8 sec_addr_val = 0xa5; + int ret; + + if (((chip->base + addr) & 0xff00) == 0) + return -EINVAL; + + dev_vdbg(chip->dev, "%s: Writing 0x%x to 0x%x", __func__, *val, addr); + + if (sec_access) { + ret = regmap_bulk_write(chip->regmap, + ((chip->base + addr) & 0xff00) | 0xd0, + &sec_addr_val, 1); + if (ret) + return ret; + } + + return regmap_bulk_write(chip->regmap, chip->base + addr, val, len); +} + +/** + * @brief pmi8998_fg_masked_write() - like pmi8998_fg_write but applies + * a mask first. + * + * @param chip Pointer to chip + * @param val Pointer to write values from + * @param addr Address to write to + * @param len Number of registers (bytes) to write + * @return int 0 on success, negative errno on error + */ +static int pmi8998_fg_masked_write(struct pmi8998_fg_chip *chip, u16 addr, u8 mask, u8 val) +{ + u8 reg; + int ret; + + ret = pmi8998_fg_read(chip, ®, addr, 1); + if (ret) + return ret; + + reg &= ~mask; + reg |= val & mask; + + return pmi8998_fg_write(chip, ®, addr, 1); +} + +/* + * Battery status + */ + +/** + * @brief pmi8998_fg_get_capacity() - Get remaining capacity of battery + * + * @param chip Pointer to chip + * @param val Pointer to store value at + * @return int 0 on success, negative errno on error + */ +static int pmi8998_fg_get_capacity(struct pmi8998_fg_chip *chip, int *val) +{ + u8 cap[2]; + int ret; + + ret = pmi8998_fg_read(chip, cap, BATT_MONOTONIC_SOC, 2); + if (ret) { + dev_err(chip->dev, "Failed to read capacity: %d", ret); + return ret; + } + + 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; +} + +/** + * @brief pmi8998_fg_get_temperature() - Get temperature of battery + * + * @param chip Pointer to chip + * @param val Pointer to store value at + * @return int 0 on success, negative errno on error + */ +static int pmi8998_fg_get_temperature(struct pmi8998_fg_chip *chip, int *val) +{ + int ret, temp; + u8 readval[2]; + + ret = pmi8998_fg_read(chip, readval, PARAM_ADDR_BATT_TEMP, 2); + if (ret) { + dev_err(chip->dev, "Failed to read temperature: %d\n", ret); + return ret; + } + + 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; +} + +/** + * @brief pmi8998_fg_get_current() - Get current being drawn from battery + * + * @param chip Pointer to chip + * @param val Pointer to store value at + * @return int 0 on success, negative errno on error + */ +static int pmi8998_fg_get_current(struct pmi8998_fg_chip *chip, int *val) +{ + s16 temp; + u8 readval[2]; + int ret; + + ret = pmi8998_fg_read(chip, readval, PARAM_ADDR_BATT_CURRENT, 2); + if (ret) { + dev_err(chip->dev, "Failed to read current: %d\n", ret); + return ret; + } + + /* handle rev 1 too */ + temp = (s16)(readval[1] << 8 | readval[0]); + *val = div_s64((s64)temp * 488281, 1000); + + /* + * PSY API expects charging batteries to report a positive current, which is inverted + * to what the PMIC reports. + */ + *val = -*val; + + return 0; +} + +/** + * @brief pmi8998_fg_get_voltage() - Get voltage of battery + * + * @param chip Pointer to chip + * @param val Pointer to store value at + * @return int 0 on success, negative errno on error + */ +static int pmi8998_fg_get_voltage(struct pmi8998_fg_chip *chip, int *val) +{ + int temp; + u8 readval[2]; + int ret; + + ret = pmi8998_fg_read(chip, readval, PARAM_ADDR_BATT_VOLTAGE, 2); + if (ret) { + dev_err(chip->dev, "Failed to read voltage: %d\n", ret); + return ret; + } + + /* handle rev 1 too */ + temp = readval[1] << 8 | readval[0]; + *val = div_u64((u64)temp * 122070, 1000); + + return 0; +} + +/** + * @brief pmi8998_fg_get_temp_threshold() - Get configured temperature thresholds + * + * @param chip Pointer to chip + * @param psp Power supply property of temperature limit + * @param val Pointer to store value at + * @return int 0 on success, negative errno on error + */ +static int pmi8998_fg_get_temp_threshold(struct pmi8998_fg_chip *chip, + enum power_supply_property psp, int *val) +{ + u8 temp; + u16 reg; + int ret; + + switch (psp) { + case POWER_SUPPLY_PROP_TEMP_MIN: + reg = BATT_INFO_JEITA_COLD; + break; + case POWER_SUPPLY_PROP_TEMP_MAX: + reg = BATT_INFO_JEITA_HOT; + break; + case POWER_SUPPLY_PROP_TEMP_ALERT_MIN: + reg = BATT_INFO_JEITA_COOL; + break; + case POWER_SUPPLY_PROP_TEMP_ALERT_MAX: + reg = BATT_INFO_JEITA_WARM; + break; + default: + return -EINVAL; + } + + ret = pmi8998_fg_read(chip, &temp, reg, 1); + if (ret < 0) { + dev_err(chip->dev, "Failed to read JEITA property %d level: %d\n", psp, ret); + return ret; + } + + /* Resolution is 0.5C. Base is -30C. */ + *val = (((5 * temp) / 10) - 30) * 10; + + return 0; +} + +/* + * Battery power supply + */ + +static enum power_supply_property pmi8998_fg_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TEMP_MIN, + POWER_SUPPLY_PROP_TEMP_MAX, + POWER_SUPPLY_PROP_TEMP_ALERT_MIN, + POWER_SUPPLY_PROP_TEMP_ALERT_MAX, +}; + +static int pmi8998_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 temp, ret = 0; + + dev_dbg(chip->dev, "Getting property: %d", psp); + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + /* Get status from charger if available */ + if (chip->chg_psy && + chip->status != POWER_SUPPLY_STATUS_UNKNOWN) { + val->intval = chip->status; + } else { + /* + * Fall back to capacity and current-based + * status checking + */ + ret = pmi8998_fg_get_capacity(chip, &temp); + if (ret) { + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + break; + } + if (temp == 100) { + val->intval = POWER_SUPPLY_STATUS_FULL; + break; + } + + ret = pmi8998_fg_get_current(chip, &temp); + if (ret) { + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + break; + } + if (temp < 0) + val->intval = POWER_SUPPLY_STATUS_CHARGING; + else if (temp > 0) + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + else + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; + } + + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + case POWER_SUPPLY_PROP_CAPACITY: + ret = pmi8998_fg_get_capacity(chip, &val->intval); + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = pmi8998_fg_get_current(chip, &val->intval); + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = pmi8998_fg_get_voltage(chip, &val->intval); + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = 1; + break; + case POWER_SUPPLY_PROP_TEMP: + ret = pmi8998_fg_get_temperature(chip, &val->intval); + break; + case POWER_SUPPLY_PROP_TEMP_MIN: + case POWER_SUPPLY_PROP_TEMP_MAX: + case POWER_SUPPLY_PROP_TEMP_ALERT_MIN: + case POWER_SUPPLY_PROP_TEMP_ALERT_MAX: + ret = pmi8998_fg_get_temp_threshold(chip, psp, &val->intval); + break; + default: + dev_err(chip->dev, "invalid property: %d\n", psp); + return -EINVAL; + } + + return ret; +} + +static const struct power_supply_desc batt_psy_desc = { + .name = "qcom-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = pmi8998_fg_props, + .num_properties = ARRAY_SIZE(pmi8998_fg_props), + .get_property = pmi8998_fg_get_property, +}; + +/* + * Init functions + */ + +static int pmi8998_fg_iacs_clear_sequence(struct pmi8998_fg_chip *chip) +{ + u8 temp; + int ret; + + /* clear the error */ + ret = pmi8998_fg_masked_write(chip, MEM_INTF_IMA_CFG, BIT(2), BIT(2)); + if (ret) { + dev_err(chip->dev, "Failed to write IMA_CFG: %d\n", ret); + return ret; + } + + temp = 0x4; + ret = pmi8998_fg_write(chip, &temp, MEM_INTF_ADDR_LSB + 1, 1); + if (ret) { + dev_err(chip->dev, "Failed to write MEM_INTF_ADDR_MSB: %d\n", ret); + return ret; + } + + temp = 0x0; + ret = pmi8998_fg_write(chip, &temp, MEM_INTF_WR_DATA0 + 3, 1); + if (ret) { + dev_err(chip->dev, "Failed to write WR_DATA3: %d\n", ret); + return ret; + } + + ret = pmi8998_fg_read(chip, &temp, MEM_INTF_RD_DATA0 + 3, 1); + if (ret) { + dev_err(chip->dev, "Failed to write RD_DATA3: %d\n", ret); + return ret; + } + + ret = pmi8998_fg_masked_write(chip, MEM_INTF_IMA_CFG, BIT(2), 0); + if (ret) { + dev_err(chip->dev, "Failed to write IMA_CFG: %d\n", ret); + return ret; + } + + return 0; +} + +static int pmi8998_fg_clear_ima(struct pmi8998_fg_chip *chip, bool check_hw_sts) +{ + u8 err_sts, exp_sts, hw_sts; + bool run_err_clr_seq = false; + int ret; + + ret = pmi8998_fg_read(chip, &err_sts, MEM_INTF_IMA_ERR_STS, 1); + if (ret) { + dev_err(chip->dev, "Failed to read IMA_ERR_STS: %d\n", ret); + return ret; + } + + ret = pmi8998_fg_read(chip, &exp_sts, + MEM_INTF_IMA_EXP_STS, 1); + if (ret) { + dev_err(chip->dev, "Failed to read IMA_EXP_STS: %d\n", ret); + return ret; + } + + if (check_hw_sts) { + ret = pmi8998_fg_read(chip, &hw_sts, + MEM_INTF_IMA_HW_STS, 1); + if (ret) { + dev_err(chip->dev, "Failed to read IMA_HW_STS: %d\n", ret); + return ret; + } + /* + * Lower nibble should be equal to upper nibble before SRAM + * transactions begins from SW side. + */ + if ((hw_sts & 0x0f) != hw_sts >> 4) { + dev_dbg(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_dbg(chip->dev, "IMA exception bit set, exp_sts=%x\n", exp_sts); + run_err_clr_seq = true; + } + + if (run_err_clr_seq) { + ret = pmi8998_fg_iacs_clear_sequence(chip); + if (!ret) + return -EAGAIN; + } + + return 0; +} + +static irqreturn_t pmi8998_fg_handle_soc_delta(int irq, void *data) +{ + struct pmi8998_fg_chip *chip = data; + + /* Signal change in state of charge */ + power_supply_changed(chip->batt_psy); + dev_dbg(chip->dev, "SOC changed"); + + return IRQ_HANDLED; +} + +static void pmi8998_fg_status_changed_worker(struct work_struct *work) +{ + struct pmi8998_fg_chip *chip = container_of(work, struct pmi8998_fg_chip, + status_changed_work.work); + + power_supply_changed(chip->batt_psy); +} + +static int pmi8998_fg_notifier_call(struct notifier_block *nb, unsigned long val, void *v) +{ + struct pmi8998_fg_chip *chip = container_of(nb, struct pmi8998_fg_chip, nb); + struct power_supply *psy = v; + union power_supply_propval propval; + int ret; + + if (psy == chip->chg_psy) { + ret = power_supply_get_property(psy, + POWER_SUPPLY_PROP_STATUS, &propval); + if (ret) + chip->status = POWER_SUPPLY_STATUS_UNKNOWN; + + chip->status = propval.intval; + + power_supply_changed(chip->batt_psy); + + if (chip->status == POWER_SUPPLY_STATUS_UNKNOWN) { + /* + * REVISIT: Find better solution or remove current-based + * status checking once checking is properly implemented + * in charger drivers + + * Sometimes it take a while for current to stabilize, + * so signal property change again later to make sure + * current-based status is properly detected. + */ + cancel_delayed_work_sync(&chip->status_changed_work); + schedule_delayed_work(&chip->status_changed_work, + msecs_to_jiffies(1000)); + } + } + + return NOTIFY_OK; +} + +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 irq; + u8 dma_status; + bool error_present; + int ret; + + chip = devm_kzalloc(&pdev->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->dev = &pdev->dev; + + 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, "Failed to read SOC base address from dt\n"); + return -EINVAL; + } + chip->base = be32_to_cpu(*prop_addr); + + /* + * 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) + */ + ret = pmi8998_fg_masked_write(chip, MEM_INTF_IMA_CFG, BIT(3), BIT(3)); + if (ret) { + dev_err(chip->dev, + "Failed to configure interrupt sourete: %d\n", ret); + return ret; + } + + ret = pmi8998_fg_clear_ima(chip, true); + if (ret && ret != -EAGAIN) { + dev_err(chip->dev, "Failed to clear IMA exception: %d\n", ret); + return ret; + } + + /* Check and clear DMA errors */ + ret = pmi8998_fg_read(chip, &dma_status, MEM_IF_DMA_STS, 1); + if (ret < 0) { + dev_err(chip->dev, "Failed to read dma_status: %d\n", ret); + return ret; + } + + error_present = dma_status & (BIT(1) | BIT(2)); + ret = pmi8998_fg_masked_write(chip, MEM_IF_DMA_CTL, BIT(0), + error_present ? BIT(0) : 0); + if (ret < 0) { + dev_err(chip->dev, "Failed to write dma_ctl: %d\n", ret); + return ret; + } + + supply_config.drv_data = chip; + supply_config.fwnode = dev_fwnode(&pdev->dev); + + chip->batt_psy = devm_power_supply_register(chip->dev, + &batt_psy_desc, &supply_config); + if (IS_ERR(chip->batt_psy)) { + if (PTR_ERR(chip->batt_psy) != -EPROBE_DEFER) + dev_err(&pdev->dev, "Failed to register battery\n"); + return PTR_ERR(chip->batt_psy); + } + + platform_set_drvdata(pdev, chip); + + /* Get soc-delta IRQ */ + irq = of_irq_get_byname(pdev->dev.of_node, "soc-delta"); + if (irq < 0) { + dev_err(&pdev->dev, "Failed to get irq soc-delta byname: %d\n", + irq); + return irq; + } + + ret = devm_request_threaded_irq(chip->dev, irq, NULL, + pmi8998_fg_handle_soc_delta, + IRQF_ONESHOT, "soc-delta", chip); + if (ret < 0) { + dev_err(&pdev->dev, "Failed to request soc-delta IRQ: %d\n", ret); + return ret; + } + + /* Optional: Get charger power supply for status checking */ + chip->chg_psy = power_supply_get_by_reference(of_fwnode_handle(chip->dev->of_node), + "power-supplies"); + if (IS_ERR(chip->chg_psy)) { + ret = PTR_ERR(chip->chg_psy); + dev_warn(chip->dev, "Failed to get charger supply: %d\n", ret); + chip->chg_psy = NULL; + } + + if (chip->chg_psy) { + INIT_DELAYED_WORK(&chip->status_changed_work, + pmi8998_fg_status_changed_worker); + + chip->nb.notifier_call = pmi8998_fg_notifier_call; + ret = power_supply_reg_notifier(&chip->nb); + if (ret) { + dev_err(chip->dev, + "Failed to register notifier: %d\n", ret); + return ret; + } + } + + return 0; +} + +static const struct of_device_id fg_match_id_table[] = { + { .compatible = "qcom,pmi8998-fg" }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, fg_match_id_table); + +static struct platform_driver pmi8998_fg_driver = { + .probe = pmi8998_fg_probe, + .driver = { + .name = "pmi8998-fg", + .of_match_table = fg_match_id_table, + }, +}; + +module_platform_driver(pmi8998_fg_driver); + +MODULE_AUTHOR("Casey Connolly <[email protected]>"); +MODULE_AUTHOR("Joel Selvaraj <[email protected]>"); +MODULE_AUTHOR("Yassine Oudjana <[email protected]>"); +MODULE_DESCRIPTION("Qualcomm PMI8998 Fuel Gauge Driver"); +MODULE_LICENSE("GPL"); -- 2.51.0

