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

Commit 49f89de3 authored by Linux Build Service Account's avatar Linux Build Service Account Committed by Gerrit - the friendly Code Review server
Browse files

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

parents 1c9ce044 5e51193a
Loading
Loading
Loading
Loading
+37 −0
Original line number Diff line number Diff line
===============================================================================
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";
		};
+11 −0
Original line number Diff line number Diff line
@@ -72,6 +72,17 @@ config REGULATOR_COOLING_DEVICE

	  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
+1 −0
Original line number Diff line number Diff line
@@ -6,4 +6,5 @@ obj-$(CONFIG_QTI_VIRTUAL_SENSOR) += qti_virtual_sensor.o
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
+529 −0
Original line number Diff line number Diff line
/*
 * 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);