mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Val Packett <val@packett.cool>
To: Sebastian Reichel <sre@kernel.org>, Val Packett <val@packett.cool>
Cc: "Marc Lainez" <marc.lainez@gmail.com>,
	"Luca Weiss" <luca.weiss@fairphone.com>,
	"Barnabás Czémán" <barnabas.czeman@mainlining.org>,
	"Danila Tikhonov" <danila@jiaxyga.com>,
	"Rob Herring" <robh@kernel.org>,
	"Krzysztof Kozlowski" <krzk+dt@kernel.org>,
	"Conor Dooley" <conor+dt@kernel.org>,
	"Bjorn Andersson" <andersson@kernel.org>,
	"Konrad Dybcio" <konradybcio@kernel.org>,
	"Abel Vesa" <abelvesa@kernel.org>,
	"Jonathan Cameron" <jic23@kernel.org>,
	"David Lechner" <dlechner@baylibre.com>,
	"Nuno Sá" <nuno.sa@analog.com>,
	"Andy Shevchenko" <andy@kernel.org>,
	linux-arm-msm@vger.kernel.org, linux-pm@vger.kernel.org,
	devicetree@vger.kernel.org, phone-devel@vger.kernel.org,
	linux-iio@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [PATCH 2/5] power: supply: add Qualcomm QGauge (qcom_qg) fuel gauge driver
Date: Thu, 12 Nov 2026 14:07:28 -0300	[thread overview]
Message-ID: <20261112170841.2189322-3-val@packett.cool> (raw)
In-Reply-To: <20261112170841.2189322-1-val@packett.cool>

From: Marc Lainez <marc.lainez@gmail.com>

Add a driver for the QGauge (QG) fuel-gauge block found in Qualcomm SPMI
PMICs such as PMI632 and PM6150. The QG hardware provides FIFO-based
voltage/current sampling, sleep-time open-circuit-voltage measurement and
coulomb counting; state-of-charge is computed in-kernel from the
monitored-battery OCV-to-capacity tables. Coulomb counting is not
implemented yet.

The driver exposes present/status/health/technology, instantaneous and
running-average voltage and current, OCV, capacity, temperature,
and battery design limits via the power_supply class.

Signed-off-by: Marc Lainez <marc.lainez@gmail.com>
[val: cleanup, implement SDAM full charge, fix batt_info leak]
Signed-off-by: Val Packett <val@packett.cool>
---
Assisted-by: LLM
(as declared by Marc in the GitHub thread)
---
 MAINTAINERS                    |   1 +
 drivers/power/supply/Kconfig   |  13 +
 drivers/power/supply/Makefile  |   1 +
 drivers/power/supply/qcom_qg.c | 807 +++++++++++++++++++++++++++++++++
 4 files changed, 822 insertions(+)
 create mode 100644 drivers/power/supply/qcom_qg.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 7b76b07ed66f..111e56340be8 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -23333,6 +23333,7 @@ M:	Val Packett <val@packett.cool>
 L:	linux-arm-msm@vger.kernel.org
 S:	Maintained
 F:	Documentation/devicetree/bindings/power/supply/qcom,qg.yaml
+F:	drivers/power/supply/qcom_qg.c
 
 QUALCOMM SMB CHARGER DRIVER
 M:	Casey Connolly <casey.connolly@linaro.org>
diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig
index 16d0a38b41b7..ad235a9057d1 100644
--- a/drivers/power/supply/Kconfig
+++ b/drivers/power/supply/Kconfig
@@ -228,6 +228,19 @@ config BATTERY_QCOM_BATTMGR
 	  which is used on modern Qualcomm platforms to provide battery and
 	  power supply information.
 
+config BATTERY_QCOM_QG
+	tristate "Qualcomm QGauge fuel gauge support"
+	depends on MFD_SPMI_PMIC || COMPILE_TEST
+	depends on IIO
+	help
+	  Say Y to add support for the QGauge (QG) fuel gauge block found
+	  in Qualcomm PMICs such as PMI632 and PM6150. This hardware provides
+	  battery voltage and current FIFO sampling, open circuit voltage
+	  (OCV) measurement, and coulomb counting for battery state estimation.
+
+	  To compile this driver as module, choose M here: the
+	  module will be called qcom_qg.
+
 config BATTERY_OLPC
 	tristate "One Laptop Per Child battery"
 	depends on OLPC_EC
diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile
index cd3640f4b748..07296f3f7914 100644
--- a/drivers/power/supply/Makefile
+++ b/drivers/power/supply/Makefile
@@ -134,6 +134,7 @@ obj-$(CONFIG_CHARGER_SURFACE)	+= surface_charger.o
 obj-$(CONFIG_BATTERY_CHARGER_SURFACE_RT) += surface-rt-ec.o
 obj-$(CONFIG_BATTERY_UG3105)	+= ug3105_battery.o
 obj-$(CONFIG_CHARGER_QCOM_SMB2)	+= qcom_smbx.o
+obj-$(CONFIG_BATTERY_QCOM_QG)	+= qcom_qg.o
 obj-$(CONFIG_FUEL_GAUGE_MM8013)	+= mm8013.o
 obj-$(CONFIG_MACSMC_POWER)	+= macsmc-power.o
 obj-$(CONFIG_CHARGER_MAX77759)	+= max77759_charger.o
diff --git a/drivers/power/supply/qcom_qg.c b/drivers/power/supply/qcom_qg.c
new file mode 100644
index 000000000000..071e29d4c299
--- /dev/null
+++ b/drivers/power/supply/qcom_qg.c
@@ -0,0 +1,807 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2025, Marc Lainez <marc.lainez@gmail.com>
+ * Copyright (c) 2026, Val Packett <val@packett.cool>
+ *
+ * Qualcomm QGauge (QG) fuel gauge driver (PMI632, PM6150, ...).
+ *
+ * The QG hardware block provides battery voltage and current measurement
+ * via FIFO-based sampling, open circuit voltage (OCV) measurement during
+ * sleep states, and coulomb counting through V/I accumulators.
+ *
+ * This driver exposes battery state via the power_supply subsystem.
+ * SOC is computed in-kernel using OCV-to-capacity lookup tables from
+ * the monitored-battery simple-battery node.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/cleanup.h>
+#include <linux/errno.h>
+#include <linux/iio/consumer.h>
+#include <linux/interrupt.h>
+#include <linux/math64.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/nvmem-consumer.h>
+#include <linux/platform_device.h>
+#include <linux/power_supply.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/timekeeping.h>
+
+/* Peripheral type register */
+#define QG_PERPH_TYPE			0x04
+#define QG_PERPH_TYPE_VALUE		0x0D
+
+/* Status registers */
+#define QG_STATUS1			0x08
+#define  QG_STATUS1_BATT_PRESENT	BIT(0)
+
+#define QG_STATUS2			0x09
+#define  QG_STATUS2_GOOD_OCV		BIT(1)
+
+#define QG_STATUS3			0x0A
+#define  QG_STATUS3_FIFO_RT_COUNT	GENMASK(3, 0)
+
+/* Data control */
+#define QG_DATA_CTL1			0x41
+#define  QG_DATA_CTL1_MASTER_HOLD	BIT(0)
+
+#define QG_DATA_CTL2			0x42
+#define  QG_DATA_CTL2_BURST_AVG_HOLD	BIT(0)
+
+/* OCV data registers (16-bit little-endian) */
+#define QG_S7_PON_OCV_V		0x70
+#define QG_S3_GOOD_OCV_V		0x74
+
+/* S2 (normal-mode) running-average data registers */
+#define QG_S2_NORMAL_AVG_V		0x80
+#define QG_S2_NORMAL_AVG_I		0x82
+
+/* FIFO data registers */
+#define QG_V_FIFO0			0x90
+#define QG_I_FIFO0			0xA0
+
+/* Last ADC data registers */
+#define QG_LAST_ADC_V			0xC0
+#define QG_LAST_ADC_I			0xC2
+#define QG_LAST_BURST_AVG_I		0xC6
+
+/* Sentinel value indicating FIFO entry is not yet written */
+#define QG_FIFO_RESET_VAL		0x8000
+
+/* Maximum hardware FIFO depth */
+#define QG_MAX_FIFO_LENGTH		8
+
+/* SDAM (shared direct access memory) offsets */
+#define QG_SDAM_VALID_OFFSET		0x46	/* 1-byte validity marker */
+#define QG_SDAM_LEARNED_CAPACITY_OFFSET	0x68	/* 2-byte, mAh */
+
+#define QG_OCV_STALE_SECS		180
+
+struct qcom_qg {
+	struct device *dev;
+	struct regmap *regmap;
+	unsigned int base;
+
+	struct power_supply *psy;
+	struct power_supply_battery_info *batt_info;
+
+	struct iio_channel *batt_therm;
+	struct nvmem_device *sdam;
+
+	struct mutex lock; /* protects the FIFO/burst hold register sequences */
+
+	/* Cached measurements (updated from IRQs and on-demand reads) */
+	unsigned int vbat_uv;
+	int ibat_ua;
+	unsigned int ocv_uv;
+	time64_t ocv_time;
+	bool batt_present;
+};
+
+static inline unsigned int qg_raw_to_uv(u16 raw)
+{
+	return div_u64(194637ULL * raw, 1000);
+}
+
+static inline int qg_raw_to_ua(s16 raw)
+{
+	/*
+	 * Hardware convention: positive = discharge, negative = charge.
+	 * Power supply class convention (ABI): positive = charge.
+	 */
+	return -div_s64(152588LL * raw, 1000);
+}
+
+static int qg_read16(struct qcom_qg *qg, unsigned int reg, u16 *val)
+{
+	int ret;
+	__le16 raw;
+
+	ret = regmap_bulk_read(qg->regmap, qg->base + reg, &raw, sizeof(raw));
+	if (ret)
+		return ret;
+
+	*val = le16_to_cpu(raw);
+	return 0;
+}
+
+static int qg_master_hold(struct qcom_qg *qg, bool hold)
+{
+	int ret;
+
+	/* Clear first (required for 0->1 transition to latch) */
+	ret = regmap_update_bits(qg->regmap, qg->base + QG_DATA_CTL1,
+				 QG_DATA_CTL1_MASTER_HOLD, 0);
+	if (ret)
+		return ret;
+
+	if (hold)
+		ret = regmap_update_bits(qg->regmap, qg->base + QG_DATA_CTL1,
+					 QG_DATA_CTL1_MASTER_HOLD,
+					 QG_DATA_CTL1_MASTER_HOLD);
+
+	return ret;
+}
+
+static int qg_read_battery_present(struct qcom_qg *qg, bool *present)
+{
+	unsigned int val;
+	int ret;
+
+	ret = regmap_read(qg->regmap, qg->base + QG_STATUS1, &val);
+	if (ret)
+		return ret;
+
+	*present = !!(val & QG_STATUS1_BATT_PRESENT);
+	return 0;
+}
+
+static int qg_read_vbat(struct qcom_qg *qg, unsigned int *vbat_uv)
+{
+	u16 raw;
+	int ret;
+
+	ret = qg_read16(qg, QG_LAST_ADC_V, &raw);
+	if (ret)
+		return ret;
+
+	*vbat_uv = qg_raw_to_uv(raw);
+	return 0;
+}
+
+static int qg_read_ibat(struct qcom_qg *qg, int *ibat_ua)
+{
+	u16 raw;
+	int ret;
+
+	guard(mutex)(&qg->lock);
+
+	/* Hold the burst average register to get a coherent reading */
+	ret = regmap_update_bits(qg->regmap, qg->base + QG_DATA_CTL2,
+				 QG_DATA_CTL2_BURST_AVG_HOLD,
+				 QG_DATA_CTL2_BURST_AVG_HOLD);
+	if (ret)
+		return ret;
+
+	ret = qg_read16(qg, QG_LAST_BURST_AVG_I, &raw);
+
+	/* Always release the hold, even if the read failed */
+	regmap_update_bits(qg->regmap, qg->base + QG_DATA_CTL2,
+			   QG_DATA_CTL2_BURST_AVG_HOLD, 0);
+
+	if (ret)
+		return ret;
+
+	*ibat_ua = qg_raw_to_ua((s16)raw);
+	return 0;
+}
+
+static int qg_read_ocv(struct qcom_qg *qg, unsigned int reg,
+		       unsigned int *ocv_uv)
+{
+	u16 raw;
+	int ret;
+
+	ret = qg_read16(qg, reg, &raw);
+	if (ret)
+		return ret;
+
+	*ocv_uv = qg_raw_to_uv(raw);
+	return 0;
+}
+
+static int qg_read_temperature(struct qcom_qg *qg, int *temp_decidegc)
+{
+	int ret, val;
+
+	lockdep_assert_not_held(&qg->lock);
+
+	if (!qg->batt_therm)
+		return -ENODEV;
+
+	ret = iio_read_channel_processed(qg->batt_therm, &val);
+	if (ret < 0)
+		return ret;
+
+	/* SPMI ADC5 bat_therm channel returns millidegrees */
+	*temp_decidegc = DIV_ROUND_CLOSEST(val, 100);
+
+	return 0;
+}
+
+static bool qg_ocv_valid(struct qcom_qg *qg)
+{
+	return qg->ocv_uv &&
+	       ktime_get_seconds() - qg->ocv_time <= QG_OCV_STALE_SECS;
+}
+
+static int qg_read_fifo_vbat(struct qcom_qg *qg)
+{
+	unsigned int count_reg;
+	unsigned int fifo_count;
+	u64 vbat_sum = 0;
+	unsigned int valid = 0;
+	u16 raw;
+	int ret, i;
+
+	guard(mutex)(&qg->lock);
+
+	ret = qg_master_hold(qg, true);
+	if (ret)
+		return ret;
+
+	ret = regmap_read(qg->regmap, qg->base + QG_STATUS3, &count_reg);
+	if (ret)
+		goto release;
+
+	fifo_count = FIELD_GET(QG_STATUS3_FIFO_RT_COUNT, count_reg);
+	fifo_count = min_t(unsigned int, fifo_count, QG_MAX_FIFO_LENGTH);
+
+	for (i = 0; i < fifo_count; i++) {
+		ret = qg_read16(qg, QG_V_FIFO0 + i * 2, &raw);
+		if (ret)
+			goto release;
+
+		if (raw == QG_FIFO_RESET_VAL)
+			continue;
+
+		vbat_sum += qg_raw_to_uv(raw);
+		valid++;
+	}
+
+	if (valid)
+		qg->vbat_uv = div_u64(vbat_sum, valid);
+
+release:
+	qg_master_hold(qg, false);
+	return ret;
+}
+
+static int qg_update_ocv(struct qcom_qg *qg)
+{
+	unsigned int status2;
+	unsigned int ocv_uv;
+	int ret;
+
+	lockdep_assert_held(&qg->lock);
+
+	ret = regmap_read(qg->regmap, qg->base + QG_STATUS2, &status2);
+	if (ret)
+		return ret;
+
+	/* Clear the sticky register by writing back */
+	regmap_write(qg->regmap, qg->base + QG_STATUS2, 0);
+
+	if (!(status2 & QG_STATUS2_GOOD_OCV))
+		return 0;
+
+	ret = qg_read_ocv(qg, QG_S3_GOOD_OCV_V, &ocv_uv);
+	if (ret)
+		return ret;
+
+	qg->ocv_uv = ocv_uv;
+	qg->ocv_time = ktime_get_seconds();
+
+	return 0;
+}
+
+static int qg_get_capacity(struct qcom_qg *qg, int *capacity)
+{
+	int temp_decidegc;
+	int cap, ret;
+
+	if (!qg->batt_info)
+		return -ENODATA;
+
+	/*
+	 * Without OCV, it's impossible to calculate state-of-charge.
+	 * Well, you could try, but you'd get *massive* load swings.
+	 */
+	if (!qg->ocv_uv)
+		return -ENODATA;
+
+	ret = qg_read_temperature(qg, &temp_decidegc);
+	if (ret)
+		temp_decidegc = 250; /* default 25.0 degC */
+
+	cap = power_supply_batinfo_ocv2cap(qg->batt_info, qg->ocv_uv,
+					   temp_decidegc);
+	if (cap < 0)
+		return cap;
+
+	*capacity = clamp(cap, 0, 100);
+
+	return 0;
+}
+
+static int qg_get_charge_full(struct qcom_qg *qg, int *charge_uah)
+{
+	u8 valid;
+	__le16 raw;
+	int ret;
+
+	if (!qg->sdam)
+		return -ENODATA;
+
+	ret = nvmem_device_read(qg->sdam, QG_SDAM_VALID_OFFSET, 1, &valid);
+	if (ret < 0)
+		return ret;
+	if (ret != 1 || !valid)
+		return -ENODATA;
+
+	ret = nvmem_device_read(qg->sdam, QG_SDAM_LEARNED_CAPACITY_OFFSET,
+				sizeof(raw), &raw);
+	if (ret < 0)
+		return ret;
+	if (ret != sizeof(raw))
+		return -EIO;
+
+	/* 0 mAh means no full-charge learning cycle has completed yet */
+	if (!le16_to_cpu(raw))
+		return -ENODATA;
+
+	*charge_uah = le16_to_cpu(raw) * 1000;
+
+	return 0;
+}
+
+static int qg_get_health(struct qcom_qg *qg, int *health)
+{
+	struct power_supply_battery_info *info = qg->batt_info;
+	int temp_decidegc;
+	int ret;
+
+	if (!qg->batt_present) {
+		*health = POWER_SUPPLY_HEALTH_UNKNOWN;
+		return 0;
+	}
+
+	if (info) {
+		ret = qg_read_temperature(qg, &temp_decidegc);
+
+		/* No thermistor is not a health fault */
+		if (ret && ret != -ENODEV)
+			return ret;
+
+		if (!ret) {
+			/* Unset thresholds stay 0; 0 degC is a valid reading. */
+			if (info->temp_alert_max > 0 &&
+			    temp_decidegc >= info->temp_alert_max) {
+				*health = POWER_SUPPLY_HEALTH_HOT;
+				return 0;
+			}
+			if (info->temp_alert_min > 0 &&
+			    temp_decidegc <= info->temp_alert_min) {
+				*health = POWER_SUPPLY_HEALTH_COLD;
+				return 0;
+			}
+			if (info->temp_max > 0 &&
+			    temp_decidegc >= info->temp_max) {
+				*health = POWER_SUPPLY_HEALTH_WARM;
+				return 0;
+			}
+			if (info->temp_min > 0 &&
+			    temp_decidegc <= info->temp_min) {
+				*health = POWER_SUPPLY_HEALTH_COOL;
+				return 0;
+			}
+		}
+	}
+
+	*health = POWER_SUPPLY_HEALTH_GOOD;
+
+	if (info && qg->vbat_uv) {
+		if (qg->vbat_uv < info->voltage_min_design_uv)
+			*health = POWER_SUPPLY_HEALTH_DEAD;
+		else if (qg->vbat_uv > info->voltage_max_design_uv)
+			*health = POWER_SUPPLY_HEALTH_OVERVOLTAGE;
+	}
+
+	return 0;
+}
+
+static irqreturn_t qg_fifo_done_irq(int irq, void *data)
+{
+	struct qcom_qg *qg = data;
+	int ret;
+
+	ret = qg_read_fifo_vbat(qg);
+	if (ret)
+		dev_warn_once(qg->dev, "Failed to read FIFO: %d\n", ret);
+
+	power_supply_changed(qg->psy);
+
+	return IRQ_HANDLED;
+}
+
+static irqreturn_t qg_good_ocv_irq(int irq, void *data)
+{
+	struct qcom_qg *qg = data;
+
+	scoped_guard(mutex, &qg->lock)
+		qg_update_ocv(qg);
+
+	power_supply_changed(qg->psy);
+
+	return IRQ_HANDLED;
+}
+
+static irqreturn_t qg_batt_missing_irq(int irq, void *data)
+{
+	struct qcom_qg *qg = data;
+	bool present;
+
+	if (!qg_read_battery_present(qg, &present))
+		qg->batt_present = present;
+
+	power_supply_changed(qg->psy);
+
+	return IRQ_HANDLED;
+}
+
+static int qg_get_property(struct power_supply *psy,
+			   enum power_supply_property psp,
+			   union power_supply_propval *val)
+{
+	struct qcom_qg *qg = power_supply_get_drvdata(psy);
+	int ret;
+
+	/*
+	 * Not locking the mutex as we can't hold locks across the
+	 * temperature ADC read. No writes here, should be fine.
+	 */
+
+	switch (psp) {
+	case POWER_SUPPLY_PROP_PRESENT:
+		val->intval = qg->batt_present;
+		return 0;
+
+	case POWER_SUPPLY_PROP_STATUS:
+		if (!qg->batt_present) {
+			val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
+			return 0;
+		}
+		ret = power_supply_am_i_supplied(psy);
+		if (ret < 0 && ret != -ENODEV)
+			return ret;
+		if (ret > 0)
+			val->intval = POWER_SUPPLY_STATUS_CHARGING;
+		else
+			val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
+		return 0;
+
+	case POWER_SUPPLY_PROP_HEALTH:
+		return qg_get_health(qg, &val->intval);
+
+	case POWER_SUPPLY_PROP_VOLTAGE_NOW:
+		if (!qg->batt_present)
+			return -ENODATA;
+		/* Refresh from last ADC register */
+		ret = qg_read_vbat(qg, &qg->vbat_uv);
+		if (ret)
+			return ret;
+		val->intval = qg->vbat_uv;
+		return 0;
+
+	case POWER_SUPPLY_PROP_CURRENT_NOW:
+		if (!qg->batt_present)
+			return -ENODATA;
+		ret = qg_read_ibat(qg, &qg->ibat_ua);
+		if (ret)
+			return ret;
+		val->intval = qg->ibat_ua;
+		return 0;
+
+	case POWER_SUPPLY_PROP_VOLTAGE_OCV:
+		if (!qg->batt_present)
+			return -ENODATA;
+		if (!qg_ocv_valid(qg))
+			return -ENODATA;
+		val->intval = qg->ocv_uv;
+		return 0;
+
+	case POWER_SUPPLY_PROP_CAPACITY:
+		if (!qg->batt_present)
+			return -ENODATA;
+		return qg_get_capacity(qg, &val->intval);
+
+	case POWER_SUPPLY_PROP_TEMP:
+		if (!qg->batt_present)
+			return -ENODATA;
+		return qg_read_temperature(qg, &val->intval);
+
+	case POWER_SUPPLY_PROP_TECHNOLOGY:
+		val->intval = POWER_SUPPLY_TECHNOLOGY_LION;
+		return 0;
+
+	case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN:
+		if (!qg->batt_info)
+			return -ENODATA;
+		val->intval = qg->batt_info->voltage_max_design_uv;
+		return 0;
+
+	case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN:
+		if (!qg->batt_info)
+			return -ENODATA;
+		val->intval = qg->batt_info->voltage_min_design_uv;
+		return 0;
+
+	case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN:
+		if (!qg->batt_info ||
+		    qg->batt_info->charge_full_design_uah < 0)
+			return -ENODATA;
+		val->intval = qg->batt_info->charge_full_design_uah;
+		return 0;
+
+	case POWER_SUPPLY_PROP_CHARGE_FULL:
+		return qg_get_charge_full(qg, &val->intval);
+
+	case POWER_SUPPLY_PROP_VOLTAGE_AVG: {
+		u16 raw;
+
+		if (!qg->batt_present)
+			return -ENODATA;
+		ret = qg_read16(qg, QG_S2_NORMAL_AVG_V, &raw);
+		if (ret)
+			return ret;
+		val->intval = qg_raw_to_uv(raw);
+		return 0;
+	}
+
+	case POWER_SUPPLY_PROP_CURRENT_AVG: {
+		u16 raw;
+
+		if (!qg->batt_present)
+			return -ENODATA;
+		ret = qg_read16(qg, QG_S2_NORMAL_AVG_I, &raw);
+		if (ret)
+			return ret;
+		val->intval = qg_raw_to_ua((s16)raw);
+		return 0;
+	}
+
+	default:
+		return -EINVAL;
+	}
+}
+
+static enum power_supply_property qg_properties[] = {
+	POWER_SUPPLY_PROP_PRESENT,
+	POWER_SUPPLY_PROP_STATUS,
+	POWER_SUPPLY_PROP_HEALTH,
+	POWER_SUPPLY_PROP_TECHNOLOGY,
+	POWER_SUPPLY_PROP_VOLTAGE_NOW,
+	POWER_SUPPLY_PROP_VOLTAGE_AVG,
+	POWER_SUPPLY_PROP_VOLTAGE_OCV,
+	POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN,
+	POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN,
+	POWER_SUPPLY_PROP_CURRENT_NOW,
+	POWER_SUPPLY_PROP_CURRENT_AVG,
+	POWER_SUPPLY_PROP_CAPACITY,
+	POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN,
+	POWER_SUPPLY_PROP_CHARGE_FULL,
+	POWER_SUPPLY_PROP_TEMP,
+};
+
+static const struct power_supply_desc qg_psy_desc = {
+	.name = "qcom-qg",
+	.type = POWER_SUPPLY_TYPE_BATTERY,
+	.properties = qg_properties,
+	.num_properties = ARRAY_SIZE(qg_properties),
+	.get_property = qg_get_property,
+};
+
+static int qg_get_optional_irq(struct qcom_qg *qg, struct platform_device *pdev,
+			       const char *name, irq_handler_t handler)
+{
+	int irq, ret;
+
+	irq = platform_get_irq_byname_optional(pdev, name);
+	if (irq < 0)
+		return irq;
+
+	ret = devm_request_threaded_irq(qg->dev, irq, NULL, handler,
+					IRQF_ONESHOT, name, qg);
+	if (ret)
+		return dev_err_probe(qg->dev, ret,
+				     "Failed to request %s IRQ", name);
+
+	return 0;
+}
+
+static void qg_batt_info_cleanup(void *data)
+{
+	struct qcom_qg *qg = data;
+
+	power_supply_put_battery_info(qg->psy, qg->batt_info);
+}
+
+static int qcom_qg_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct qcom_qg *qg;
+	struct power_supply_config psy_cfg = {};
+	unsigned int perph_type;
+	int ret;
+
+	qg = devm_kzalloc_obj(dev, struct qcom_qg);
+	if (!qg)
+		return -ENOMEM;
+
+	qg->dev = dev;
+	mutex_init(&qg->lock);
+
+	qg->regmap = dev_get_regmap(dev->parent, NULL);
+	if (!qg->regmap)
+		return dev_err_probe(dev, -ENODEV,
+				     "Failed to get parent regmap");
+
+	ret = device_property_read_u32(dev, "reg", &qg->base);
+	if (ret)
+		return dev_err_probe(dev, ret, "Failed to read reg property");
+
+	ret = regmap_read(qg->regmap, qg->base + QG_PERPH_TYPE, &perph_type);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "Failed to read peripheral type");
+
+	if (perph_type != QG_PERPH_TYPE_VALUE)
+		return dev_err_probe(dev, -ENODEV,
+				     "Unexpected peripheral type: 0x%02x",
+				     perph_type);
+
+	ret = qg_read_battery_present(qg, &qg->batt_present);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "Failed to read battery presence");
+
+	ret = qg_read_ocv(qg, QG_S7_PON_OCV_V, &qg->ocv_uv);
+	if (ret)
+		return dev_err_probe(dev, ret, "Failed to read PON OCV");
+
+	qg->ocv_time = ktime_get_seconds();
+
+	ret = qg_read_vbat(qg, &qg->vbat_uv);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "Failed to read battery voltage");
+
+	qg->batt_therm = devm_iio_channel_get(dev, "batt-therm");
+	if (IS_ERR(qg->batt_therm)) {
+		ret = PTR_ERR(qg->batt_therm);
+		if (ret == -EPROBE_DEFER)
+			return ret;
+		dev_dbg(dev, "No batt-therm IIO channel: %d\n", ret);
+		qg->batt_therm = NULL;
+	}
+
+	qg->sdam = devm_nvmem_device_get(dev, NULL);
+	if (IS_ERR(qg->sdam)) {
+		ret = PTR_ERR(qg->sdam);
+		if (ret == -EPROBE_DEFER)
+			return ret;
+		dev_dbg(dev, "No SDAM nvmem device: %d\n", ret);
+		qg->sdam = NULL;
+	}
+
+	psy_cfg.drv_data = qg;
+	psy_cfg.fwnode = dev_fwnode(dev);
+
+	qg->psy = devm_power_supply_register(dev, &qg_psy_desc, &psy_cfg);
+	if (IS_ERR(qg->psy))
+		return dev_err_probe(dev, PTR_ERR(qg->psy),
+				     "Failed to register power supply");
+
+	ret = power_supply_get_battery_info(qg->psy, &qg->batt_info);
+	if (ret)
+		return dev_err_probe(dev, ret, "Failed to get battery info");
+
+	ret = devm_add_action_or_reset(dev, qg_batt_info_cleanup, qg);
+	if (ret)
+		return ret;
+
+	ret = qg_get_optional_irq(qg, pdev, "fifo-done", qg_fifo_done_irq);
+	if (ret && ret != -ENXIO)
+		return ret;
+
+	ret = qg_get_optional_irq(qg, pdev, "good-ocv", qg_good_ocv_irq);
+	if (ret && ret != -ENXIO)
+		return ret;
+
+	ret = qg_get_optional_irq(qg, pdev, "batt-missing",
+				  qg_batt_missing_irq);
+	if (ret && ret != -ENXIO)
+		return ret;
+
+	platform_set_drvdata(pdev, qg);
+
+	dev_dbg(dev, "QG: batt=%s OCV=%u uV VBAT=%u uV\n",
+		qg->batt_present ? "present" : "absent",
+		qg->ocv_uv, qg->vbat_uv);
+
+	return 0;
+}
+
+static int qcom_qg_suspend(struct device *dev)
+{
+	struct qcom_qg *qg = dev_get_drvdata(dev);
+
+	/*
+	 * The QG hardware FSM will automatically transition to sleep
+	 * states (S3) and measure OCV. Nothing special is needed here
+	 * beyond ensuring we pick up the new OCV on resume.
+	 */
+	dev_dbg(qg->dev, "Suspending, last OCV=%u uV\n", qg->ocv_uv);
+
+	return 0;
+}
+
+static int qcom_qg_resume(struct device *dev)
+{
+	struct qcom_qg *qg = dev_get_drvdata(dev);
+
+	/*
+	 * After resume, the hardware may have measured a good OCV during
+	 * the sleep state. Check for it and update our cached value.
+	 * Also refresh the battery voltage.
+	 */
+	scoped_guard(mutex, &qg->lock)
+		qg_update_ocv(qg);
+
+	qg_read_vbat(qg, &qg->vbat_uv);
+
+	dev_dbg(qg->dev, "Resumed, OCV=%u uV VBAT=%u uV\n",
+		qg->ocv_uv, qg->vbat_uv);
+
+	power_supply_changed(qg->psy);
+
+	return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(qcom_qg_pm_ops,
+				qcom_qg_suspend, qcom_qg_resume);
+
+static const struct of_device_id qcom_qg_of_match[] = {
+	{ .compatible = "qcom,pm6150-qg" },
+	{ .compatible = "qcom,pmi632-qg" },
+	{}
+};
+MODULE_DEVICE_TABLE(of, qcom_qg_of_match);
+
+static struct platform_driver qcom_qg_driver = {
+	.driver = {
+		.name = "qcom-qg",
+		.of_match_table = qcom_qg_of_match,
+		.pm = pm_sleep_ptr(&qcom_qg_pm_ops),
+	},
+	.probe = qcom_qg_probe,
+};
+module_platform_driver(qcom_qg_driver);
+
+MODULE_DESCRIPTION("Qualcomm QGauge fuel gauge driver");
+MODULE_AUTHOR("Marc Lainez <marc.lainez@gmail.com>");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("IIO_CONSUMER");
-- 
2.56.0


  parent reply	other threads:[~2026-11-12 17:09 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-12 17:07 [PATCH 0/5] Add driver for Qualcomm QGauge (qcom_qg) fuel gauge Val Packett
2026-11-12 17:07 ` [PATCH 1/5] dt-bindings: power: supply: add Qualcomm QGauge " Val Packett
2026-11-12 17:07 ` Val Packett [this message]
2026-11-12 17:07 ` [PATCH 3/5] iio: adc: qcom-spmi-adc5: Add ADC5_BAT_THERM_100K_PU Val Packett
2026-11-12 17:07 ` [PATCH 4/5] arm64: dts: qcom: pmi632: Add missing bat_therm ADC channel Val Packett
2026-11-12 17:07 ` [PATCH 5/5] arm64: dts: qcom: pmi632: Add battery fuel gauge node Val Packett

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261112170841.2189322-3-val@packett.cool \
    --to=val@packett.cool \
    --cc=abelvesa@kernel.org \
    --cc=andersson@kernel.org \
    --cc=andy@kernel.org \
    --cc=barnabas.czeman@mainlining.org \
    --cc=conor+dt@kernel.org \
    --cc=danila@jiaxyga.com \
    --cc=devicetree@vger.kernel.org \
    --cc=dlechner@baylibre.com \
    --cc=jic23@kernel.org \
    --cc=konradybcio@kernel.org \
    --cc=krzk+dt@kernel.org \
    --cc=linux-arm-msm@vger.kernel.org \
    --cc=linux-iio@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-pm@vger.kernel.org \
    --cc=luca.weiss@fairphone.com \
    --cc=marc.lainez@gmail.com \
    --cc=nuno.sa@analog.com \
    --cc=phone-devel@vger.kernel.org \
    --cc=robh@kernel.org \
    --cc=sre@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®