Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 81101090 authored by David Keitel's avatar David Keitel
Browse files

power: add qpnp fuel gauge driver



This adds the driver to support the
fuel gauge peripheral on PMI8994.

The initial driver is able to supply userspace
with the following properties via the power supply
framework.

  * POWER_SUPPLY_PROP_CAPACITY:
    - The state of charge of the battery in percent.
  * POWER_SUPPLY_PROP_CURRENT_NOW:
    - The battery current with 32 mA resolution.
  * POWER_SUPPLY_PROP_VOLTAGE_NOW:
    - The battery voltage with 32 mV resolution.

The fuel gauge driver wakes up the system if
either FULL (100%) or EMPTY (0%) state of charge (SOC)
has been reached to allow userspace to take appropriate
action.

In addition to this there are also non-wakeable interrupts
for each SOC change via the DELTA_SOC interrupt.

Each SOC change is conveyed to consumers via the power_supply
framework. If a given kernel driver needs to subscribe to
these events, one must expand the fg_supplicants string array.

Change-Id: I633cf48b4b3c0b1389327a8272b6df974fa03808
Signed-off-by: default avatarDavid Keitel <dkeitel@codeaurora.org>
parent f98fc1b2
Loading
Loading
Loading
Loading
+80 −0
Original line number Diff line number Diff line
QTI's QPNP PMIC Fuel Gauge Device

QPNP PMIC FG provides interface to clients to read properties related
to the battery. Its main function is to retrieve the State of Charge (SOC),
a 0-100 percentage representing the amount of charge left in the battery.

There are two required peripherals in the FG driver, both implemented as
subnodes in the example. These peripherals must not be disabled if the FG
device is to enabled:

- qcom,fg-soc : The main FG device. Supports battery fuel gauge controls and
		sensors.

Optionally ADC nodes can be added
- qcom,fg-adc-vbat : A subnode with a register address for the FG_ADC_USR
		peripheral which is used mainly for battery current limiting (BCL).
		This node maps out the VBAT reading register which allows to have
		a +/- 32 mV accurate reading of VBAT.
- qcom,fg-adc-ibat : A subnode with a register address for the FG_ADC_USR
		peripheral which is used mainly for battery current limiting (BCL).
		This node maps out the IBAT current reading register which allows
		to have a +/- 32 mA accurate reading of IBAT.

Parent node required properties:
- compatible : should be "qcom,qpnp-fg" for the FG driver.

qcom,fg-soc node required properties:
- reg : offset and length of the PMIC peripheral register map.
- interrupts : the interrupt mappings.
		The format should be
		<slave-id peripheral-id interrupt-number>.
- interrupt-names : names for the mapped fg soc interrupts
		The following interrupts are required:
		0: high-soc
		1: low-soc
		2: full-soc
		3: empty-soc
		4: delta-soc
		5: first-est-done
		6: sw-fallbk-ocv
		7: sw-fallbk-new-battrt-sts

Example:
pmi8994_fg: qcom,fg {
	spmi-dev-container;
	compatible = "qcom,qpnp-fg";
	#address-cells = <1>;
	#size-cells = <1>;
	status = "disabled";

	qcom,fg-soc@4000 {
		reg = <0x4000 0x100>;
		interrupts =	<0x2 0x40 0x0>,
				<0x2 0x40 0x1>,
				<0x2 0x40 0x2>,
				<0x2 0x40 0x3>,
				<0x2 0x40 0x4>,
				<0x2 0x40 0x5>,
				<0x2 0x40 0x6>,
				<0x2 0x40 0x7>;

		interrupt-names =	"high-soc",
					"low-soc",
					"full-soc",
					"empty-soc",
					"delta-soc",
					"first-est-done",
					"sw-fallbk-ocv",
					"sw-fallbk-new-battrt-sts",
					"fg-soc-irq-count";
	};

	qcom,fg-adc-vbat@4254 {
		reg = <0x4254 0x1>;
	};

	qcom,fg-adc-ibat@4255 {
		reg = <0x4255 0x1>;
	};
};
+10 −0
Original line number Diff line number Diff line
@@ -455,6 +455,16 @@ config QPNP_SMBCHARGER
	  The driver also offers relevant information to userspace via the
	  power supply framework.

config QPNP_FG
	tristate "QPNP fuel gauge driver"
	depends on SPMI
	depends on OF_SPMI
	help
	  Say Y here to enable the Fuel Gauge driver. This adds support for
	  battery fuel gauging and state of charge of battery connected to the
	  fuel gauge. The state of charge is reported through a BMS power
	  supply property and also sends uevents when the capacity is updated.

config PM8921_BMS
	tristate "PM8921 Battery Monitoring System driver"
	depends on MFD_PM8921_CORE
+1 −0
Original line number Diff line number Diff line
@@ -61,6 +61,7 @@ obj-$(CONFIG_BATTERY_BQ28400) += bq28400_battery.o
obj-$(CONFIG_SMB137C_CHARGER)	+= smb137c-charger.o
obj-$(CONFIG_QPNP_BMS)		+= qpnp-bms.o batterydata-lib.o
obj-$(CONFIG_QPNP_VM_BMS)	+= qpnp-vm-bms.o batterydata-lib.o batterydata-interface.o
obj-$(CONFIG_QPNP_FG)		+= qpnp-fg.o
obj-$(CONFIG_QPNP_CHARGER)	+= qpnp-charger.o
obj-$(CONFIG_QPNP_LINEAR_CHARGER)	+= qpnp-linear-charger.o
obj-$(CONFIG_QPNP_SMBCHARGER)	+= qpnp-smbcharger.o
+435 −0
Original line number Diff line number Diff line
/* Copyright (c) 2014, The Linux Foundation. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 and
 * only version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

#define pr_fmt(fmt)	"FG: %s: " fmt, __func__

#include <linux/kernel.h>
#include <linux/of.h>
#include <linux/err.h>
#include <linux/init.h>
#include <linux/spmi.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/bitops.h>
#include <linux/types.h>
#include <linux/module.h>
#include <linux/power_supply.h>

/* Register offsets */
#define SOC_MONOTONIC_SOC	0x09

#define REG_OFFSET_PERP_SUBTYPE	0x05

/* Bit/Mask definitions */
#define FULL_PERCENT		0xFF
#define MAX_TRIES_SOC		5
#define MA_MV_BIT_RES		39
#define MSB_SIGN		BIT(7)
#define IBAT_VBAT_MASK		0x7F

/* SUBTYPE definitions */
#define FG_SOC			0x9
#define FG_BATT			0xA
#define FG_MEMIF		0xD

#define QPNP_FG_DEV_NAME "qcom,qpnp-fg"

struct fg_irq {
	int			irq;
	unsigned long		disabled;
};

enum fg_soc_irq {
	HIGH_SOC,
	LOW_SOC,
	FULL_SOC,
	EMPTY_SOC,
	DELTA_SOC,
	FIRST_EST_DONE,
	SW_FALLBK_OCV,
	SW_FALLBK_NEW_BATTRT_STS,
	FG_SOC_IRQ_COUNT,
};

enum fg_batt_irq {
	JEITA_SOFT_COLD,
	JEITA_SOFT_HOT,
	VBATT_LOW,
	BATT_IDENTIFIED,
	BATT_ID_REQ,
	BATT_UNKNOWN,
	BATT_MISSING,
	BATT_MATCH,
	FG_BATT_IRQ_COUNT,
};

enum fg_mem_if_irq {
	FG_MEM_AVAIL,
	TA_RCVRY_SUG,
	FG_MEM_IF_IRQ_COUNT,
};

struct fg_chip {
	struct device		*dev;
	struct dentry		*dent;
	struct spmi_device	*spmi;
	u16			soc_base;
	u16			batt_base;
	u16			mem_base;
	u16			vbat_adc_addr;
	u16			ibat_adc_addr;
	struct fg_irq		soc_irq[FG_SOC_IRQ_COUNT];
	struct fg_irq		batt_irq[FG_BATT_IRQ_COUNT];
	struct fg_irq		mem_irq[FG_MEM_IF_IRQ_COUNT];
	struct fg_mem_if_chip	*memif;
	struct power_supply	bms_psy;
};

static const struct of_device_id fg_match_table[] = {
	{	.compatible = QPNP_FG_DEV_NAME, },
	{}
};

static char *fg_supplicants[] = {
	"battery"
};

static int fg_read(struct fg_chip *chip, u8 *val, u16 base, int count)
{
	int rc = 0;
	struct spmi_device *spmi = chip->spmi;

	if (base == 0) {
		pr_err("base cannot be zero base=0x%02x sid=0x%02x rc=%d\n",
			base, spmi->sid, rc);
		return -EINVAL;
	}

	rc = spmi_ext_register_readl(spmi->ctrl, spmi->sid, base, val, count);
	if (rc) {
		pr_err("SPMI read failed base=0x%02x sid=0x%02x rc=%d\n", base,
				spmi->sid, rc);
		return rc;
	}

	return 0;
}

static int get_prop_capacity(struct fg_chip *chip)
{
	u8 cap[2];
	int rc, capacity = 0, tries = 0;

	while (tries < MAX_TRIES_SOC) {
		rc = fg_read(chip, cap,
				chip->soc_base + SOC_MONOTONIC_SOC, 2);
		if (rc) {
			pr_err("spmi read failed: addr=%03x, rc=%d\n",
				chip->soc_base + SOC_MONOTONIC_SOC, rc);
			return rc;
		}

		if (cap[0] == cap[1])
			break;

		tries++;
	}

	if (tries == MAX_TRIES_SOC) {
		pr_err("shadow registers do not match\n");
		return -EINVAL;
	}

	if (cap[0] > 0)
		capacity = (cap[0] * 100 / FULL_PERCENT);

	pr_debug("capacity: %d, raw: 0x%02x\n", capacity, cap[0]);
	return capacity;
}

static int get_prop_current_now(struct fg_chip *chip)
{
	u8 ibat_ma;
	int rc, current_now_ua;

	if (!chip->ibat_adc_addr)
		return 0;

	rc = fg_read(chip, &ibat_ma, chip->ibat_adc_addr, 1);
	if (rc) {
		pr_err("spmi read failed: addr=%03x, rc=%d\n",
			chip->ibat_adc_addr, rc);
		return rc;
	}

	/* convert to uA */
	current_now_ua = MA_MV_BIT_RES * (ibat_ma & IBAT_VBAT_MASK) * 1000;

	if (ibat_ma & MSB_SIGN)
		current_now_ua *= -1;

	pr_debug("current %d uA\n", current_now_ua);
	return current_now_ua;
}

static int get_prop_voltage_now(struct fg_chip *chip)
{
	u8 vbat_mv;
	int rc, voltage_now_uv;

	if (!chip->vbat_adc_addr)
		return 0;

	rc = fg_read(chip, &vbat_mv, chip->vbat_adc_addr, 1);
	if (rc) {
		pr_err("spmi read failed: addr=%03x, rc=%d\n",
			chip->vbat_adc_addr, rc);
		return rc;
	}

	/* convert to uV */
	voltage_now_uv = MA_MV_BIT_RES * (vbat_mv & IBAT_VBAT_MASK) * 1000;

	if (vbat_mv & MSB_SIGN)
		voltage_now_uv *= -1;

	pr_debug("voltage %d uV\n", voltage_now_uv);

	return voltage_now_uv;
}

static enum power_supply_property fg_power_props[] = {
	POWER_SUPPLY_PROP_CAPACITY,
	POWER_SUPPLY_PROP_CURRENT_NOW,
	POWER_SUPPLY_PROP_VOLTAGE_NOW,
};

static int fg_power_get_property(struct power_supply *psy,
				       enum power_supply_property psp,
				       union power_supply_propval *val)
{
	struct fg_chip *chip = container_of(psy, struct fg_chip, bms_psy);

	switch (psp) {
	case POWER_SUPPLY_PROP_CAPACITY:
		val->intval = get_prop_capacity(chip);
		break;
	case POWER_SUPPLY_PROP_CURRENT_NOW:
		val->intval = get_prop_current_now(chip);
		break;
	case POWER_SUPPLY_PROP_VOLTAGE_NOW:
		val->intval = get_prop_voltage_now(chip);
		break;
	default:
		return -EINVAL;
	}

	return 0;
}

static irqreturn_t fg_soc_irq_handler(int irq, void *_chip)
{
	struct fg_chip *chip = _chip;

	power_supply_changed(&chip->bms_psy);
	return IRQ_HANDLED;
}

static int fg_init_irqs(struct fg_chip *chip,
		struct spmi_resource *spmi_resource, u8 subtype)
{
	int rc = 0;

	switch (subtype) {
	case FG_SOC:
		chip->soc_irq[FULL_SOC].irq = spmi_get_irq_byname(chip->spmi,
					spmi_resource, "full-soc");
		if (chip->soc_irq[FULL_SOC].irq < 0) {
			pr_err("Unable to get full-soc irq\n");
			return rc;
		}
		chip->soc_irq[EMPTY_SOC].irq = spmi_get_irq_byname(chip->spmi,
					spmi_resource, "empty-soc");
		if (chip->soc_irq[EMPTY_SOC].irq < 0) {
			pr_err("Unable to get low-soc irq\n");
			return rc;
		}
		chip->soc_irq[DELTA_SOC].irq = spmi_get_irq_byname(chip->spmi,
					spmi_resource, "delta-soc");
		if (chip->soc_irq[DELTA_SOC].irq < 0) {
			pr_err("Unable to get delta-soc irq\n");
			return rc;
		}

		rc |= devm_request_irq(chip->dev, chip->soc_irq[FULL_SOC].irq,
			fg_soc_irq_handler, IRQF_TRIGGER_RISING,
			"full-soc", chip);
		if (rc < 0) {
			pr_err("Can't request %d full-soc: %d\n",
					chip->soc_irq[FULL_SOC].irq, rc);
			return rc;
		}
		rc |= devm_request_irq(chip->dev, chip->soc_irq[EMPTY_SOC].irq,
				fg_soc_irq_handler, IRQF_TRIGGER_RISING,
				"empty-soc", chip);
		if (rc < 0) {
			pr_err("Can't request %d empty-soc: %d\n",
					chip->soc_irq[EMPTY_SOC].irq, rc);
			return rc;
		}
		rc |= devm_request_irq(chip->dev, chip->soc_irq[DELTA_SOC].irq,
				fg_soc_irq_handler, IRQF_TRIGGER_RISING,
				"delta-soc", chip);
		if (rc < 0) {
			pr_err("Can't request %d delta-soc: %d\n",
					chip->soc_irq[DELTA_SOC].irq, rc);
			return rc;
		}

		enable_irq_wake(chip->soc_irq[FULL_SOC].irq);
		enable_irq_wake(chip->soc_irq[EMPTY_SOC].irq);
		break;
	case FG_BATT:
		break;
	case FG_MEMIF:
		break;
	default:
		return -EINVAL;
	}

	return rc;
}

static int fg_remove(struct spmi_device *spmi)
{
	struct fg_chip *chip = dev_get_drvdata(&spmi->dev);

	power_supply_unregister(&chip->bms_psy);
	dev_set_drvdata(&spmi->dev, NULL);
	return 0;
}

static int fg_probe(struct spmi_device *spmi)
{
	struct device *dev = &spmi->dev;
	struct fg_chip *chip;
	struct spmi_resource *spmi_resource;
	struct resource *resource;
	u8 subtype;
	int rc = 0;

	if (!spmi->dev.of_node) {
		pr_err("device node missing\n");
		return -ENODEV;
	}

	chip = devm_kzalloc(dev, sizeof(struct fg_chip), GFP_KERNEL);
	if (!chip) {
		pr_err("Can't allocate fg_chip\n");
		return -ENOMEM;
	}

	chip->spmi = spmi;
	chip->dev = &(spmi->dev);

	spmi_for_each_container_dev(spmi_resource, spmi) {
		if (!spmi_resource) {
			pr_err("qpnp_chg: spmi resource absent\n");
			rc = -ENXIO;
			return rc;
		}

		resource = spmi_get_resource(spmi, spmi_resource,
						IORESOURCE_MEM, 0);
		if (!(resource && resource->start)) {
			pr_err("node %s IO resource absent!\n",
				spmi->dev.of_node->full_name);
			rc = -ENXIO;
			return rc;
		}

		if (strcmp("qcom,fg-adc-vbat",
					spmi_resource->of_node->name) == 0) {
			chip->vbat_adc_addr = resource->start;
			continue;
		} else if (strcmp("qcom,fg-adc-ibat",
					spmi_resource->of_node->name) == 0) {
			chip->ibat_adc_addr = resource->start;
			continue;
		}

		rc = fg_read(chip, &subtype,
				resource->start + REG_OFFSET_PERP_SUBTYPE, 1);
		if (rc) {
			pr_err("Peripheral subtype read failed rc=%d\n", rc);
			return rc;
		}

		switch (subtype) {
		case FG_SOC:
			chip->soc_base = resource->start;
			rc = fg_init_irqs(chip, spmi_resource, subtype);
			if (rc) {
				pr_err("IRQ init failed, rc=%d\n", rc);
				return rc;
			}
			break;
		default:
			pr_err("Invalid peripheral subtype=0x%x\n", subtype);
			rc = -EINVAL;
		}
	}

	chip->bms_psy.name = "bms";
	chip->bms_psy.type = POWER_SUPPLY_TYPE_BMS;
	chip->bms_psy.properties = fg_power_props;
	chip->bms_psy.num_properties = ARRAY_SIZE(fg_power_props);
	chip->bms_psy.get_property = fg_power_get_property;
	chip->bms_psy.supplied_to = fg_supplicants;
	chip->bms_psy.num_supplicants = ARRAY_SIZE(fg_supplicants);

	rc = power_supply_register(chip->dev, &chip->bms_psy);
	if (rc < 0) {
		pr_err("batt failed to register rc = %d\n", rc);
		return rc;
	}

	pr_info("probe success SOC %d\n", get_prop_capacity(chip));

	return rc;
}

static struct spmi_driver fg_driver = {
	.driver		= {
		.name	= QPNP_FG_DEV_NAME,
		.of_match_table	= fg_match_table,
	},
	.probe		= fg_probe,
	.remove		= fg_remove,
};

static int __init fg_init(void)
{
	return spmi_driver_register(&fg_driver);
}

static void __exit fg_exit(void)
{
	return spmi_driver_unregister(&fg_driver);
}

module_init(fg_init);
module_exit(fg_exit);

MODULE_DESCRIPTION("QPNP Fuel Gauge Driver");
MODULE_LICENSE("GPL v2");
MODULE_ALIAS("platform:" QPNP_FG_DEV_NAME);