drivers: bcl-pmic5: Add a BCL driver for the BCL Peripheral

The new version 5 of the PMIC, has a new BCL peripheral, where the same
hardware with limited functionality can be found in different PMIC's.
For example, one peripheral can monitor only vbat, while other
peripheral can monitor both vbat and ibat.

This is a common driver, that can be used to configure both the PMIC
versions. This driver can monitor vbat and ibat interrupts and notify
the thermal framework when a trip threshold is reached. This driver can
configure the thresholds for only ibat monitoring. vbat is
pre-configured in hardware.

Change-Id: I69fa0ed1250b51c42764e1b243ddcb284bb0db29
Signed-off-by: Ram Chandrasekar <rkumbako@codeaurora.org>
Signed-off-by: Manaf Meethalavalappu Pallikunhi <manafm@codeaurora.org>
diff --git a/Documentation/devicetree/bindings/thermal/qcom-bcl-pmic5.txt b/Documentation/devicetree/bindings/thermal/qcom-bcl-pmic5.txt
new file mode 100644
index 0000000..5322d89
--- /dev/null
+++ b/Documentation/devicetree/bindings/thermal/qcom-bcl-pmic5.txt
@@ -0,0 +1,37 @@
+===============================================================================
+BCL Peripheral driver for PMIC5:
+===============================================================================
+Qualcomm Technologies, Inc's PMIC has battery current limiting peripheral,
+which can monitor for high battery current and low battery voltage in the
+hardware. The BCL peripheral driver interacts with the PMIC peripheral using
+the SPMI driver interface. The hardware can take threshold for notifying for
+high battery current or low battery voltage events. This driver works only
+with PMIC version 5, where the same BCL peripheral can be found in multiple
+PMIC's that are used in a device, with limited functionalities. For example,
+one PMIC can have only vbat monitoring, while the other PMIC can have both
+vbat and ibat monitoring. This is a common driver, that can interact
+with the multiple BCL peripherals.
+
+Required Parameters:
+- compatible: must be
+	'qcom,msm-bcl-pmic5' for bcl peripheral in PMIC version 5.
+- reg: <a b> where 'a' is the starting register address of the PMIC
+	peripheral and 'b' is the size of the peripheral address space.
+- interrupts: <a b c> Where 'a' is the SLAVE ID of the PMIC, 'b' is
+		the peripheral ID and 'c' is the interrupt number in PMIC.
+- interrupt-names: user defined names for the interrupts. These
+		interrupt names will be used by the drivers to identify the
+		interrupts, instead of specifying the ID's. bcl driver will
+		accept these standard interrupts.
+		"bcl-low-vbat"
+		"bcl-high-ibat"
+
+Example:
+		bcl@4200 {
+			compatible = "qcom,msm-bcl-pmic5";
+			reg = <0x4200 0xFF>;
+			interrupts = <0x2 0x42 0x0>,
+					<0x2 0x42 0x1>;
+			interrupt-names = "bcl-high-ibat",
+						"bcl-low-vbat";
+		};
diff --git a/drivers/thermal/qcom/Kconfig b/drivers/thermal/qcom/Kconfig
index f334a1d..c3c662d 100644
--- a/drivers/thermal/qcom/Kconfig
+++ b/drivers/thermal/qcom/Kconfig
@@ -72,6 +72,17 @@
 
 	  If you want this support, you should say Y here.
 
+config MSM_BCL_PMIC5
+	bool "BCL driver for BCL peripherals in PMIC5"
+	depends on SPMI && THERMAL_OF
+	help
+	  Say Y here to enable this BCL driver for PMIC5. This driver
+	  provides routines to configure and monitor the BCL
+	  PMIC peripheral. This driver registers the battery current and
+	  voltage sensors with the thermal core framework and can take
+	  threshold input and notify the thermal core when the threshold is
+	  reached.
+
 config QTI_BCL_SOC_DRIVER
 	bool "QTI Battery state of charge sensor driver"
 	depends on THERMAL_OF
diff --git a/drivers/thermal/qcom/Makefile b/drivers/thermal/qcom/Makefile
index 784ef21..b5b56c7 100644
--- a/drivers/thermal/qcom/Makefile
+++ b/drivers/thermal/qcom/Makefile
@@ -6,4 +6,5 @@
 obj-$(CONFIG_QTI_AOP_REG_COOLING_DEVICE) += regulator_aop_cdev.o
 obj-$(CONFIG_REGULATOR_COOLING_DEVICE) += regulator_cdev.o
 obj-$(CONFIG_QTI_QMI_COOLING_DEVICE) += thermal_mitigation_device_service_v01.o qmi_cooling.o
+obj-$(CONFIG_MSM_BCL_PMIC5) += bcl_pmic5.o
 obj-$(CONFIG_QTI_BCL_SOC_DRIVER) += bcl_soc.o
diff --git a/drivers/thermal/qcom/bcl_pmic5.c b/drivers/thermal/qcom/bcl_pmic5.c
new file mode 100644
index 0000000..9c35c79
--- /dev/null
+++ b/drivers/thermal/qcom/bcl_pmic5.c
@@ -0,0 +1,529 @@
+/*
+ * Copyright (c) 2018, 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) "%s:%s " fmt, KBUILD_MODNAME, __func__
+
+#include <linux/module.h>
+#include <linux/interrupt.h>
+#include <linux/workqueue.h>
+#include <linux/kernel.h>
+#include <linux/regmap.h>
+#include <linux/io.h>
+#include <linux/err.h>
+#include <linux/of.h>
+#include <linux/of_address.h>
+#include <linux/spmi.h>
+#include <linux/platform_device.h>
+#include <linux/mutex.h>
+#include <linux/thermal.h>
+
+#include "../thermal_core.h"
+
+#define BCL_DRIVER_NAME       "bcl_pmic5"
+#define BCL_MONITOR_EN        0x46
+
+#define BCL_IBAT_HIGH         0x4B
+#define BCL_IBAT_TOO_HIGH     0x4C
+#define BCL_IBAT_READ         0x86
+#define BCL_IBAT_SCALING_UA   78127
+
+#define BCL_VBAT_READ         0x76
+#define BCL_VBAT_ADC_LOW      0x48
+#define BCL_VBAT_COMP_LOW     0x49
+#define BCL_VBAT_COMP_TLOW    0x4A
+#define BCL_VBAT_SCALING_UV   49827
+#define BCL_VBAT_NO_READING   127
+#define BCL_VBAT_BASE_MV      2000
+#define BCL_VBAT_INC_MV       25
+#define BCL_VBAT_MAX_MV       3600
+
+enum bcl_dev_type {
+	BCL_HIGH_IBAT,
+	BCL_VHIGH_IBAT,
+	BCL_LOW_VBAT,
+	BCL_VLOW_VBAT,
+	BCL_CLOW_VBAT,
+	BCL_TYPE_MAX,
+};
+
+static char bcl_int_names[BCL_TYPE_MAX][25] = {
+	"bcl-high-ibat",
+	"bcl-very-high-ibat",
+	"bcl-low-vbat",
+	"bcl-very-low-vbat",
+	"bcl-crit-low-vbat"
+};
+
+struct bcl_peripheral_data {
+	int                     irq_num;
+	long int		trip_thresh;
+	int                     last_val;
+	struct mutex            state_trans_lock;
+	bool			irq_enabled;
+	struct thermal_zone_of_device_ops ops;
+	struct thermal_zone_device *tz_dev;
+};
+
+struct bcl_device {
+	struct regmap			*regmap;
+	uint16_t			fg_bcl_addr;
+	struct bcl_peripheral_data	param[BCL_TYPE_MAX];
+};
+
+static struct bcl_device *bcl_perph;
+
+static int bcl_read_register(int16_t reg_offset, unsigned int *data)
+{
+	int ret = 0;
+
+	if (!bcl_perph) {
+		pr_err("BCL device not initialized\n");
+		return -EINVAL;
+	}
+	ret = regmap_read(bcl_perph->regmap,
+			       (bcl_perph->fg_bcl_addr + reg_offset),
+			       data);
+	if (ret < 0) {
+		pr_err("Error reading register %d. err:%d", reg_offset, ret);
+		return ret;
+	}
+
+	return ret;
+}
+
+static int bcl_write_general_register(int16_t reg_offset,
+					uint16_t base, uint8_t data)
+{
+	int  ret = 0;
+	uint8_t *write_buf = &data;
+
+	if (!bcl_perph) {
+		pr_err("BCL device not initialized\n");
+		return -EINVAL;
+	}
+	ret = regmap_write(bcl_perph->regmap, (base + reg_offset), *write_buf);
+	if (ret < 0) {
+		pr_err("Error reading register %d. err:%d", reg_offset, ret);
+		return ret;
+	}
+	pr_debug("wrote 0x%02x to 0x%04x\n", data, base + reg_offset);
+
+	return ret;
+}
+
+static int bcl_write_register(int16_t reg_offset, uint8_t data)
+{
+	return bcl_write_general_register(reg_offset,
+			bcl_perph->fg_bcl_addr, data);
+}
+
+static void convert_adc_to_vbat_thresh_val(int *val)
+{
+	/*
+	 * Threshold register is bit shifted from ADC MSB.
+	 * So the scaling factor is half.
+	 */
+	*val = (*val * BCL_VBAT_SCALING_UV) / 2000;
+}
+
+static void convert_adc_to_vbat_val(int *val)
+{
+	*val = (*val * BCL_VBAT_SCALING_UV) / 1000;
+}
+
+static void convert_ibat_to_adc_val(int *val)
+{
+	/*
+	 * Threshold register is bit shifted from ADC MSB.
+	 * So the scaling factor is half.
+	 */
+	*val = (*val * 2000) / BCL_IBAT_SCALING_UA;
+}
+
+static void convert_adc_to_ibat_val(int *val)
+{
+	*val = (*val * BCL_IBAT_SCALING_UA) / 1000;
+}
+
+static int bcl_set_ibat(void *data, int low, int high)
+{
+	int ret = 0, ibat_ua, thresh_value;
+	int8_t val = 0;
+	int16_t addr;
+	struct bcl_peripheral_data *bat_data =
+		(struct bcl_peripheral_data *)data;
+
+	mutex_lock(&bat_data->state_trans_lock);
+	thresh_value = high;
+	if (bat_data->trip_thresh == thresh_value)
+		goto set_trip_exit;
+
+	if (bat_data->irq_num && bat_data->irq_enabled) {
+		disable_irq_nosync(bat_data->irq_num);
+		bat_data->irq_enabled = false;
+	}
+	if (thresh_value == INT_MAX) {
+		bat_data->trip_thresh = thresh_value;
+		goto set_trip_exit;
+	}
+
+	ibat_ua = thresh_value;
+	convert_ibat_to_adc_val(&thresh_value);
+	val = (int8_t)thresh_value;
+	if (&bcl_perph->param[BCL_HIGH_IBAT] == bat_data) {
+		addr = BCL_IBAT_HIGH;
+		pr_debug("ibat high threshold:%d mA ADC:0x%02x\n",
+				ibat_ua, val);
+	} else if (&bcl_perph->param[BCL_VHIGH_IBAT] == bat_data) {
+		addr = BCL_IBAT_TOO_HIGH;
+		pr_debug("ibat too high threshold:%d mA ADC:0x%02x\n",
+				ibat_ua, val);
+	} else {
+		goto set_trip_exit;
+	}
+	ret = bcl_write_register(addr, val);
+	if (ret) {
+		pr_err("Error accessing BCL peripheral. err:%d\n", ret);
+		goto set_trip_exit;
+	}
+	bat_data->trip_thresh = ibat_ua;
+
+	if (bat_data->irq_num && !bat_data->irq_enabled) {
+		enable_irq(bat_data->irq_num);
+		bat_data->irq_enabled = true;
+	}
+
+set_trip_exit:
+	mutex_unlock(&bat_data->state_trans_lock);
+
+	return ret;
+}
+
+static int bcl_read_ibat(void *data, int *adc_value)
+{
+	int ret = 0;
+	unsigned int val = 0;
+	struct bcl_peripheral_data *bat_data =
+		(struct bcl_peripheral_data *)data;
+
+	*adc_value = val;
+	ret = bcl_read_register(BCL_IBAT_READ, &val);
+	if (ret) {
+		pr_err("BCL register read error. err:%d\n", ret);
+		goto bcl_read_exit;
+	}
+	*adc_value = val;
+	if (*adc_value == 0) {
+		/*
+		 * The sensor sometime can read a value 0 if there is
+		 * consequtive reads
+		 */
+		*adc_value = bat_data->last_val;
+	} else {
+		convert_adc_to_ibat_val(adc_value);
+		bat_data->last_val = *adc_value;
+	}
+	pr_debug("ibat:%d mA\n", bat_data->last_val);
+
+bcl_read_exit:
+	return ret;
+}
+
+static int bcl_get_vbat_trip(void *data, int type, int *trip)
+{
+	int ret = 0;
+	unsigned int val = 0;
+	struct bcl_peripheral_data *bat_data =
+		(struct bcl_peripheral_data *)data;
+	int16_t addr;
+
+	*trip = 0;
+	if (&bcl_perph->param[BCL_LOW_VBAT] == bat_data)
+		addr = BCL_VBAT_ADC_LOW;
+	else if (&bcl_perph->param[BCL_VLOW_VBAT] == bat_data)
+		addr = BCL_VBAT_COMP_LOW;
+	else if (&bcl_perph->param[BCL_CLOW_VBAT] == bat_data)
+		addr = BCL_VBAT_COMP_TLOW;
+	else
+		return -ENODEV;
+
+	ret = bcl_read_register(addr, &val);
+	if (ret) {
+		pr_err("BCL register read error. err:%d\n", ret);
+		return ret;
+	}
+
+	if (addr == BCL_VBAT_ADC_LOW) {
+		*trip = val;
+		convert_adc_to_vbat_thresh_val(trip);
+		pr_debug("vbat trip: %d mV\n", val);
+	} else {
+		*trip = 2250 + val * 25;
+		if (*trip > BCL_VBAT_MAX_MV)
+			*trip = BCL_VBAT_MAX_MV;
+		pr_debug("vbat-%s-low trip: %d mV\n",
+				(addr == BCL_VBAT_COMP_LOW) ?
+				"too" : "critical",
+				bat_data->irq_num);
+	}
+
+	return 0;
+}
+
+static int bcl_set_vbat(void *data, int low, int high)
+{
+	struct bcl_peripheral_data *bat_data =
+		(struct bcl_peripheral_data *)data;
+
+	mutex_lock(&bat_data->state_trans_lock);
+
+	if (low == INT_MIN &&
+		bat_data->irq_num && bat_data->irq_enabled) {
+		disable_irq_nosync(bat_data->irq_num);
+		bat_data->irq_enabled = false;
+		pr_debug("vbat: disable irq:%d\n", bat_data->irq_num);
+	} else if (low != INT_MIN &&
+		 bat_data->irq_num && !bat_data->irq_enabled) {
+		enable_irq(bat_data->irq_num);
+		bat_data->irq_enabled = true;
+		pr_debug("vbat: enable irq:%d\n", bat_data->irq_num);
+	}
+
+	/*
+	 * Vbat threshold's are pre-configured and cant be
+	 * programmed.
+	 */
+	mutex_unlock(&bat_data->state_trans_lock);
+
+	return 0;
+}
+
+static int bcl_read_vbat(void *data, int *adc_value)
+{
+	int ret = 0;
+	unsigned int val = 0;
+	struct bcl_peripheral_data *bat_data =
+		(struct bcl_peripheral_data *)data;
+
+	*adc_value = val;
+	ret = bcl_read_register(BCL_VBAT_READ, &val);
+	if (ret) {
+		pr_err("BCL register read error. err:%d\n", ret);
+		goto bcl_read_exit;
+	}
+	*adc_value = val;
+	if (*adc_value == BCL_VBAT_NO_READING) {
+		*adc_value = bat_data->last_val;
+	} else {
+		convert_adc_to_vbat_val(adc_value);
+		bat_data->last_val = *adc_value;
+	}
+	pr_debug("vbat:%d mv\n", bat_data->last_val);
+
+bcl_read_exit:
+	return ret;
+}
+
+static irqreturn_t bcl_handle_irq(int irq, void *data)
+{
+	struct bcl_peripheral_data *perph_data =
+		(struct bcl_peripheral_data *)data;
+
+	mutex_lock(&perph_data->state_trans_lock);
+	if (!perph_data->irq_enabled) {
+		WARN_ON(1);
+		disable_irq_nosync(irq);
+		perph_data->irq_enabled = false;
+		goto exit_intr;
+	}
+	mutex_unlock(&perph_data->state_trans_lock);
+	of_thermal_handle_trip(perph_data->tz_dev);
+
+	return IRQ_HANDLED;
+
+exit_intr:
+	mutex_unlock(&perph_data->state_trans_lock);
+	return IRQ_HANDLED;
+}
+
+static int bcl_get_devicetree_data(struct platform_device *pdev)
+{
+	int ret = 0;
+	const __be32 *prop = NULL;
+	struct device_node *dev_node = pdev->dev.of_node;
+
+	prop = of_get_address(dev_node, 0, NULL, NULL);
+	if (prop) {
+		bcl_perph->fg_bcl_addr = be32_to_cpu(*prop);
+		pr_debug("fg_user_adc@%04x\n", bcl_perph->fg_bcl_addr);
+	} else {
+		dev_err(&pdev->dev, "No fg_user_adc registers found\n");
+		return -ENODEV;
+	}
+
+	return ret;
+}
+
+static void bcl_fetch_trip(struct platform_device *pdev, const char *int_name,
+		struct bcl_peripheral_data *data,
+		irqreturn_t (*handle)(int, void *))
+{
+	int ret = 0, irq_num = 0;
+
+	mutex_lock(&data->state_trans_lock);
+	data->irq_num = 0;
+	data->irq_enabled = false;
+	if (!handle) {
+		mutex_unlock(&data->state_trans_lock);
+		return;
+	}
+
+	irq_num = platform_get_irq_byname(pdev, int_name);
+	if (irq_num) {
+		ret = devm_request_threaded_irq(&pdev->dev,
+				irq_num, NULL, handle,
+				IRQF_TRIGGER_RISING | IRQF_ONESHOT,
+				int_name, data);
+		if (ret) {
+			dev_err(&pdev->dev,
+				"Error requesting trip irq. err:%d",
+				ret);
+			mutex_unlock(&data->state_trans_lock);
+			return;
+		}
+		disable_irq_nosync(irq_num);
+		data->irq_num = irq_num;
+	}
+	mutex_unlock(&data->state_trans_lock);
+}
+
+static void bcl_vbat_init(struct platform_device *pdev,
+		enum bcl_dev_type type)
+{
+	struct bcl_peripheral_data *vbat = &bcl_perph->param[type];
+	irqreturn_t (*handle)(int, void *) = (type == BCL_LOW_VBAT) ?
+						bcl_handle_irq : NULL;
+
+	mutex_init(&vbat->state_trans_lock);
+	bcl_fetch_trip(pdev, bcl_int_names[type], vbat, handle);
+	vbat->ops.get_temp = bcl_read_vbat;
+	vbat->ops.set_trips = bcl_set_vbat;
+	vbat->ops.get_trip_temp = bcl_get_vbat_trip;
+	vbat->tz_dev = thermal_zone_of_sensor_register(&pdev->dev,
+				type, vbat, &vbat->ops);
+	if (IS_ERR(vbat->tz_dev)) {
+		pr_debug("vbat[%s] register failed. err:%ld\n",
+				bcl_int_names[type],
+				PTR_ERR(vbat->tz_dev));
+		vbat->tz_dev = NULL;
+		return;
+	}
+	thermal_zone_device_update(vbat->tz_dev, THERMAL_DEVICE_UP);
+}
+
+static void bcl_probe_vbat(struct platform_device *pdev)
+{
+	bcl_vbat_init(pdev, BCL_LOW_VBAT);
+	bcl_vbat_init(pdev, BCL_VLOW_VBAT);
+	bcl_vbat_init(pdev, BCL_CLOW_VBAT);
+}
+
+static void bcl_ibat_init(struct platform_device *pdev,
+				enum bcl_dev_type type)
+{
+	struct bcl_peripheral_data *ibat = &bcl_perph->param[type];
+	irqreturn_t (*handle)(int, void *) = (type == BCL_HIGH_IBAT) ?
+						bcl_handle_irq : NULL;
+
+	mutex_init(&ibat->state_trans_lock);
+	bcl_fetch_trip(pdev, bcl_int_names[type], ibat, handle);
+	ibat->ops.get_temp = bcl_read_ibat;
+	ibat->ops.set_trips = bcl_set_ibat;
+	ibat->tz_dev = thermal_zone_of_sensor_register(&pdev->dev,
+				type, ibat, &ibat->ops);
+	if (IS_ERR(ibat->tz_dev)) {
+		pr_debug("ibat:[%s] register failed. err:%ld\n",
+				bcl_int_names[type],
+				PTR_ERR(ibat->tz_dev));
+		ibat->tz_dev = NULL;
+		return;
+	}
+	thermal_zone_device_update(ibat->tz_dev, THERMAL_DEVICE_UP);
+}
+
+static void bcl_probe_ibat(struct platform_device *pdev)
+{
+	bcl_ibat_init(pdev, BCL_HIGH_IBAT);
+	bcl_ibat_init(pdev, BCL_VHIGH_IBAT);
+}
+
+static void bcl_configure_bcl_peripheral(void)
+{
+	bcl_write_register(BCL_MONITOR_EN, BIT(7));
+}
+
+static int bcl_remove(struct platform_device *pdev)
+{
+	int i = 0;
+
+	for (; i < BCL_TYPE_MAX; i++) {
+		if (!bcl_perph->param[i].tz_dev)
+			continue;
+		thermal_zone_of_sensor_unregister(&pdev->dev,
+				bcl_perph->param[i].tz_dev);
+	}
+	bcl_perph = NULL;
+
+	return 0;
+}
+
+static int bcl_probe(struct platform_device *pdev)
+{
+	bcl_perph = devm_kzalloc(&pdev->dev, sizeof(*bcl_perph), GFP_KERNEL);
+	if (!bcl_perph)
+		return -ENOMEM;
+
+	bcl_perph->regmap = dev_get_regmap(pdev->dev.parent, NULL);
+	if (!bcl_perph->regmap) {
+		dev_err(&pdev->dev, "Couldn't get parent's regmap\n");
+		return -EINVAL;
+	}
+
+	bcl_get_devicetree_data(pdev);
+	bcl_probe_ibat(pdev);
+	bcl_probe_vbat(pdev);
+	bcl_configure_bcl_peripheral();
+
+	dev_set_drvdata(&pdev->dev, bcl_perph);
+
+	return 0;
+}
+
+static const struct of_device_id bcl_match[] = {
+	{
+		.compatible = "qcom,msm-bcl-pmic5",
+	},
+	{},
+};
+
+static struct platform_driver bcl_driver = {
+	.probe  = bcl_probe,
+	.remove = bcl_remove,
+	.driver = {
+		.name           = BCL_DRIVER_NAME,
+		.owner          = THIS_MODULE,
+		.of_match_table = bcl_match,
+	},
+};
+
+builtin_platform_driver(bcl_driver);