[PATCH RFC 2/8] power: supply: Add driver for Qualcomm PMI8998 fuel gauge

David Heidelberg via B4 Relay posted 8 patches 2 months, 2 weeks ago
[PATCH RFC 2/8] power: supply: Add driver for Qualcomm PMI8998 fuel gauge
Posted by David Heidelberg via B4 Relay 2 months, 2 weeks ago
From: Joel Selvaraj <foss@joelselvaraj.com>

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 <casey@connolly.tech>
Co-developed-by: Barnabás Czémán <barnabas.czeman@mainlining.org>
Signed-off-by: Barnabás Czémán <barnabas.czeman@mainlining.org>
Co-developed-by: Yassine Oudjana <y.oudjana@protonmail.com>
Signed-off-by: Yassine Oudjana <y.oudjana@protonmail.com>
Signed-off-by: David Heidelberg <david@ixit.cz>
---
 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, &reg, addr, 1);
+	if (ret)
+		return ret;
+
+	reg &= ~mask;
+	reg |= val & mask;
+
+	return pmi8998_fg_write(chip, &reg, 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 <casey@connolly.tech>");
+MODULE_AUTHOR("Joel Selvaraj <foss@joelselvaraj.com>");
+MODULE_AUTHOR("Yassine Oudjana <y.oudjana@protonmail.com>");
+MODULE_DESCRIPTION("Qualcomm PMI8998 Fuel Gauge Driver");
+MODULE_LICENSE("GPL");

-- 
2.51.0


Re: [PATCH RFC 2/8] power: supply: Add driver for Qualcomm PMI8998 fuel gauge
Posted by Konrad Dybcio 2 months, 2 weeks ago
On 11/24/25 10:53 PM, David Heidelberg via B4 Relay wrote:
> From: Joel Selvaraj <foss@joelselvaraj.com>
> 
> 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 <casey@connolly.tech>
> Co-developed-by: Barnabás Czémán <barnabas.czeman@mainlining.org>
> Signed-off-by: Barnabás Czémán <barnabas.czeman@mainlining.org>
> Co-developed-by: Yassine Oudjana <y.oudjana@protonmail.com>
> Signed-off-by: Yassine Oudjana <y.oudjana@protonmail.com>
> Signed-off-by: David Heidelberg <david@ixit.cz>
> ---

[...]

> +/**
> + * @brief pmi8998_fg_read() - Read multiple registers with regmap_bulk_read

I think this won't pass kerneldoc checks (make W=1)

[...]

> +static int pmi8998_fg_write(struct pmi8998_fg_chip *chip, u8 *val, u16 addr, int len)
> +{
> +	bool sec_access = (addr & 0xff) > 0xd0;

Downstream checks if the address is > 0xBA which is what you want
at least for pmi8998

You can de-abbreviate this to 'secure_access' (not to be confused
with 'secondary' or so). There's a locking mechanism which needs a
0xa5 byte written to the base+0xd0 register (applies to all FG
peripherals with the 'last non-secure register' value possibly
varying).

[...]

> +	u8 sec_addr_val = 0xa5;
> +	int ret;
> +
> +	if (((chip->base + addr) & 0xff00) == 0)

The 'fuel gauge' consists of:

FG_BATT_SOC @ 0x4000 (state of charge monitor)
FG_BATT_INFO @ 0x4100 ("general fg minus SoC")
FG_BCL @ 0x4200 (battery current limiter)
FG_LMH @ 0x4300 (limits management hardware)
FG_MEM_IF @ 0x4400 (DMA engine)
RRADC @ 0x4500 (today handled by its own driver)

and a couple other peripherals that Linux doesn't need to worry about

Each one of them should have its own 'reg' entry (which is assumed
to be 0x100-long), which will let you skip such interesting checks
and rely on the regmap framework disallowing address spillover (or
you can just then make the addr argument a u8)

It would be good to keep in mind their relationship and think about how
to model them together. I don't think they must all necessarily be part
of a single big "fg" dt node, particularly the LMH/BCL part seems to be
rather self-contained

[...]

> +		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, &reg, addr, 1);
> +	if (ret)
> +		return ret;
> +
> +	reg &= ~mask;
> +	reg |= val & mask;
> +
> +	return pmi8998_fg_write(chip, &reg, 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;
> +	}

Downstream tries for 5 times to get this (raw) pair of values and fails if
they don't match - 0x400a is a shadow register of 0x4009 and this is very
much intended

> +	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;

98 comes from "FULL_CAPACITY (100) - 2", 0xff denotes "FULL_SOC_RAW", i.e. the
raw value of this register that corresponds to 100% (again not sure where the
minus2 comes from - perhaps some rounding fixups)

> +
> +	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 */

PMI8998v1 has flipped the order of the registers and I would guesstimate
that it wouldn't actually be present in the wild

> +	temp = (s16)(readval[1] << 8 | readval[0]);
> +	*val = div_s64((s64)temp * 488281, 1000);

This is a funny way to say that this is a 2s complement-encoded
16b value, where 5 bits are reserved for the integer portion

[...]

> +		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.
> +			 */

On downstream, it's the charger counterpart that signals PSY_STATUS_(DIS)CHARGING

Konrad
Re: [PATCH RFC 2/8] power: supply: Add driver for Qualcomm PMI8998 fuel gauge
Posted by David Heidelberg 2 months ago
On 27/11/2025 16:28, Konrad Dybcio wrote:
> On 11/24/25 10:53 PM, David Heidelberg via B4 Relay wrote:
>> From: Joel Selvaraj <foss@joelselvaraj.com>
>>
>> Ths driver supports the fuel gauge hardware available on PMICs known as
>> 3rd generation fuel gauge hardware available on PMI8998.
>>
[...]

> Downstream checks if the address is > 0xBA which is what you want
> at least for pmi8998

My downstream [1] checks this value.

[1] 
https://github.com/LineageOS/android_kernel_xiaomi_sdm845/blob/lineage-22.2/drivers/power/supply/qcom/qpnp-fg.c#L760> 

> You can de-abbreviate this to 'secure_access' (not to be confused
> with 'secondary' or so). There's a locking mechanism which needs a
> 0xa5 byte written to the base+0xd0 register (applies to all FG
> peripherals with the 'last non-secure register' value possibly
> varying).
> 
> [...]
> 
>> +	u8 sec_addr_val = 0xa5;
>> +	int ret;
>> +
>> +	if (((chip->base + addr) & 0xff00) == 0)
> 
> The 'fuel gauge' consists of:
> 
> FG_BATT_SOC @ 0x4000 (state of charge monitor)
> FG_BATT_INFO @ 0x4100 ("general fg minus SoC")
> FG_BCL @ 0x4200 (battery current limiter)
> FG_LMH @ 0x4300 (limits management hardware)
> FG_MEM_IF @ 0x4400 (DMA engine)
> RRADC @ 0x4500 (today handled by its own driver)
> 
> and a couple other peripherals that Linux doesn't need to worry about
> 
> Each one of them should have its own 'reg' entry (which is assumed
> to be 0x100-long), which will let you skip such interesting checks
> and rely on the regmap framework disallowing address spillover (or
> you can just then make the addr argument a u8)

Sounds good.

> 
> It would be good to keep in mind their relationship and think about how
> to model them together. I don't think they must all necessarily be part
> of a single big "fg" dt node, particularly the LMH/BCL part seems to be
> rather self-contained

Would you recommend some readings to prepare for this task?

I see the FG_BATT* + FG_MEM_IF seems to be pretty relying on each other, 
so I assume I need to take good care of that relation, when spliting 
pieces up.

Thanks
David


[...]
Re: [PATCH RFC 2/8] power: supply: Add driver for Qualcomm PMI8998 fuel gauge
Posted by Konrad Dybcio 1 month, 3 weeks ago
On 12/12/25 4:22 PM, David Heidelberg wrote:
> On 27/11/2025 16:28, Konrad Dybcio wrote:
>> On 11/24/25 10:53 PM, David Heidelberg via B4 Relay wrote:
>>> From: Joel Selvaraj <foss@joelselvaraj.com>
>>>
>>> Ths driver supports the fuel gauge hardware available on PMICs known as
>>> 3rd generation fuel gauge hardware available on PMI8998.
>>>
> [...]
> 
>> Downstream checks if the address is > 0xBA which is what you want
>> at least for pmi8998
> 
> My downstream [1] checks this value.

https://github.com/LineageOS/android_kernel_xiaomi_sdm845/blob/lineage-22.2/drivers/power/supply/qcom/fg-util.c#L434

I can quite surely tell you "> 0xBA" is the correct condition to check
for.. both my work laptop and a downstream clone on my SSD concur that

> 
> [1] https://github.com/LineageOS/android_kernel_xiaomi_sdm845/blob/lineage-22.2/drivers/power/supply/qcom/qpnp-fg.c#L760>
>> You can de-abbreviate this to 'secure_access' (not to be confused
>> with 'secondary' or so). There's a locking mechanism which needs a
>> 0xa5 byte written to the base+0xd0 register (applies to all FG
>> peripherals with the 'last non-secure register' value possibly
>> varying).
>>
>> [...]
>>
>>> +    u8 sec_addr_val = 0xa5;
>>> +    int ret;
>>> +
>>> +    if (((chip->base + addr) & 0xff00) == 0)
>>
>> The 'fuel gauge' consists of:
>>
>> FG_BATT_SOC @ 0x4000 (state of charge monitor)
>> FG_BATT_INFO @ 0x4100 ("general fg minus SoC")
>> FG_BCL @ 0x4200 (battery current limiter)
>> FG_LMH @ 0x4300 (limits management hardware)
>> FG_MEM_IF @ 0x4400 (DMA engine)
>> RRADC @ 0x4500 (today handled by its own driver)
>>
>> and a couple other peripherals that Linux doesn't need to worry about
>>
>> Each one of them should have its own 'reg' entry (which is assumed
>> to be 0x100-long), which will let you skip such interesting checks
>> and rely on the regmap framework disallowing address spillover (or
>> you can just then make the addr argument a u8)
> 
> Sounds good.
> 
>>
>> It would be good to keep in mind their relationship and think about how
>> to model them together. I don't think they must all necessarily be part
>> of a single big "fg" dt node, particularly the LMH/BCL part seems to be
>> rather self-contained
> 
> Would you recommend some readings to prepare for this task?
> 
> I see the FG_BATT* + FG_MEM_IF seems to be pretty relying on each other, so I assume I need to take good care of that relation, when spliting pieces up.

It may be that a single "fuel-gauge@" device node may make the most
sense to represent all three of batt_soc, batt_info and mem_if.

You'll unfortunately find out whether this is a good idea as you
write/port the drivers. Downstream has some hacks around plumbing
various PMIC peripherals with just function exports between drivers,
so don't read too hard into that, just try to understand what each
piece/function is doing and how that fits into the power_supply
framework.

I'm a little unsure about bcl and lmh.. but downstream keeps them in
a single node and it doesn't seem to be that bad of an idea. There's some
LMH support today in the kernel.. but not for the PMIC peripheral. That
said, the downstream driver seems to just make the same secure calls
anyway (which may be incomplete for some platforms, I don't remember the
details: <20230113031401.2336157-3-konrad.dybcio@linaro.org>)

Konrad
Re: [PATCH RFC 2/8] power: supply: Add driver for Qualcomm PMI8998 fuel gauge
Posted by Dmitry Baryshkov 2 months, 2 weeks ago
On Mon, Nov 24, 2025 at 10:53:35PM +0100, David Heidelberg via B4 Relay wrote:
> From: Joel Selvaraj <foss@joelselvaraj.com>
> 
> 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 <casey@connolly.tech>
> Co-developed-by: Barnabás Czémán <barnabas.czeman@mainlining.org>
> Signed-off-by: Barnabás Czémán <barnabas.czeman@mainlining.org>
> Co-developed-by: Yassine Oudjana <y.oudjana@protonmail.com>
> Signed-off-by: Yassine Oudjana <y.oudjana@protonmail.com>
> Signed-off-by: David Heidelberg <david@ixit.cz>

This can't be applied, there is no author's (Joel Selvaraj) SoB.

> ---
>  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

Please see Documentation/doc-guide and reforman kerneldoc
appropriately.

> + *
> + * @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;

A comment would be apprciated

> +
> +	dev_vdbg(chip->dev, "%s: Reading 0x%x bytes from 0x%x", __func__, len, addr);

No need to, regmap already has debugging / tracing functionality.

> +
> +	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);

Isn't it required to lock it again?

> +		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, &reg, addr, 1);
> +	if (ret)
> +		return ret;
> +
> +	reg &= ~mask;
> +	reg |= val & mask;
> +
> +	return pmi8998_fg_write(chip, &reg, addr, 1);

regmap_update_bits? Or at least a lock around?

> +}
> +
> +/*
> + * 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;

return 0;

drop else

> +		} 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);

return pmi8998_fg_get_capacity();


etc.

> +		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))) {

Wrong indentation. Please #define the mask if possible.

> +		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 */

DMA?

> +	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 <casey@connolly.tech>");
> +MODULE_AUTHOR("Joel Selvaraj <foss@joelselvaraj.com>");
> +MODULE_AUTHOR("Yassine Oudjana <y.oudjana@protonmail.com>");
> +MODULE_DESCRIPTION("Qualcomm PMI8998 Fuel Gauge Driver");
> +MODULE_LICENSE("GPL");
> 
> -- 
> 2.51.0
> 
> 

-- 
With best wishes
Dmitry
Re: [PATCH RFC 2/8] power: supply: Add driver for Qualcomm PMI8998 fuel gauge
Posted by Joel Selvaraj 2 months, 2 weeks ago
Hi David,

On 11/25/25 5:13 PM, Dmitry Baryshkov wrote:
> On Mon, Nov 24, 2025 at 10:53:35PM +0100, David Heidelberg via B4 Relay wrote:
>> From: Joel Selvaraj <foss@joelselvaraj.com>
>>
>> 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 <casey@connolly.tech>
>> Co-developed-by: Barnabás Czémán <barnabas.czeman@mainlining.org>
>> Signed-off-by: Barnabás Czémán <barnabas.czeman@mainlining.org>
>> Co-developed-by: Yassine Oudjana <y.oudjana@protonmail.com>
>> Signed-off-by: Yassine Oudjana <y.oudjana@protonmail.com>
>> Signed-off-by: David Heidelberg <david@ixit.cz>
> 
> This can't be applied, there is no author's (Joel Selvaraj) SoB.

Here is my signed-off-by. You can include them in future revisions.

Signed-off-by: Joel Selvaraj <foss@joelselvaraj.com>

Regards,
Joel
Re: [PATCH RFC 2/8] power: supply: Add driver for Qualcomm PMI8998 fuel gauge
Posted by Dmitry Baryshkov 2 months, 1 week ago
On Wed, Nov 26, 2025 at 04:35:09PM +0000, Joel Selvaraj wrote:
> Hi David,
> 
> On 11/25/25 5:13 PM, Dmitry Baryshkov wrote:
> > On Mon, Nov 24, 2025 at 10:53:35PM +0100, David Heidelberg via B4 Relay wrote:
> >> From: Joel Selvaraj <foss@joelselvaraj.com>
> >>
> >> 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 <casey@connolly.tech>
> >> Co-developed-by: Barnabás Czémán <barnabas.czeman@mainlining.org>
> >> Signed-off-by: Barnabás Czémán <barnabas.czeman@mainlining.org>
> >> Co-developed-by: Yassine Oudjana <y.oudjana@protonmail.com>
> >> Signed-off-by: Yassine Oudjana <y.oudjana@protonmail.com>
> >> Signed-off-by: David Heidelberg <david@ixit.cz>
> > 
> > This can't be applied, there is no author's (Joel Selvaraj) SoB.
> 
> Here is my signed-off-by. You can include them in future revisions.
> 
> Signed-off-by: Joel Selvaraj <foss@joelselvaraj.com>

Thanks!

-- 
With best wishes
Dmitry