Contributors: 4
Author Tokens Token Proportion Commits Commit Proportion
Adam Thomson 2145 99.31% 1 25.00%
Christophe Jaillet 11 0.51% 1 25.00%
Thomas Gleixner 2 0.09% 1 25.00%
Gustavo A. R. Silva 2 0.09% 1 25.00%
Total 2160 4


// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * DA9150 Fuel-Gauge Driver
 *
 * Copyright (c) 2015 Dialog Semiconductor
 *
 * Author: Adam Thomson <Adam.Thomson.Opensource@diasemi.com>
 */

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/slab.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/power_supply.h>
#include <linux/list.h>
#include <asm/div64.h>
#include <linux/mfd/da9150/core.h>
#include <linux/mfd/da9150/registers.h>
#include <linux/devm-helpers.h>

/* Core2Wire */
#define DA9150_QIF_READ		(0x0 << 7)
#define DA9150_QIF_WRITE	(0x1 << 7)
#define DA9150_QIF_CODE_MASK	0x7F

#define DA9150_QIF_BYTE_SIZE	8
#define DA9150_QIF_BYTE_MASK	0xFF
#define DA9150_QIF_SHORT_SIZE	2
#define DA9150_QIF_LONG_SIZE	4

/* QIF Codes */
#define DA9150_QIF_UAVG			6
#define DA9150_QIF_UAVG_SIZE		DA9150_QIF_LONG_SIZE
#define DA9150_QIF_IAVG			8
#define DA9150_QIF_IAVG_SIZE		DA9150_QIF_LONG_SIZE
#define DA9150_QIF_NTCAVG		12
#define DA9150_QIF_NTCAVG_SIZE		DA9150_QIF_LONG_SIZE
#define DA9150_QIF_SHUNT_VAL		36
#define DA9150_QIF_SHUNT_VAL_SIZE	DA9150_QIF_SHORT_SIZE
#define DA9150_QIF_SD_GAIN		38
#define DA9150_QIF_SD_GAIN_SIZE		DA9150_QIF_LONG_SIZE
#define DA9150_QIF_FCC_MAH		40
#define DA9150_QIF_FCC_MAH_SIZE		DA9150_QIF_SHORT_SIZE
#define DA9150_QIF_SOC_PCT		43
#define DA9150_QIF_SOC_PCT_SIZE		DA9150_QIF_SHORT_SIZE
#define DA9150_QIF_CHARGE_LIMIT		44
#define DA9150_QIF_CHARGE_LIMIT_SIZE	DA9150_QIF_SHORT_SIZE
#define DA9150_QIF_DISCHARGE_LIMIT	45
#define DA9150_QIF_DISCHARGE_LIMIT_SIZE	DA9150_QIF_SHORT_SIZE
#define DA9150_QIF_FW_MAIN_VER		118
#define DA9150_QIF_FW_MAIN_VER_SIZE	DA9150_QIF_SHORT_SIZE
#define DA9150_QIF_E_FG_STATUS		126
#define DA9150_QIF_E_FG_STATUS_SIZE	DA9150_QIF_SHORT_SIZE
#define DA9150_QIF_SYNC			127
#define DA9150_QIF_SYNC_SIZE		DA9150_QIF_SHORT_SIZE
#define DA9150_QIF_MAX_CODES		128

/* QIF Sync Timeout */
#define DA9150_QIF_SYNC_TIMEOUT		1000
#define DA9150_QIF_SYNC_RETRIES		10

/* QIF E_FG_STATUS */
#define DA9150_FG_IRQ_LOW_SOC_MASK	(1 << 0)
#define DA9150_FG_IRQ_HIGH_SOC_MASK	(1 << 1)
#define DA9150_FG_IRQ_SOC_MASK	\
	(DA9150_FG_IRQ_LOW_SOC_MASK | DA9150_FG_IRQ_HIGH_SOC_MASK)

/* Private data */
struct da9150_fg {
	struct da9150 *da9150;
	struct device *dev;

	struct mutex io_lock;

	struct power_supply *battery;
	struct delayed_work work;
	u32 interval;

	int warn_soc;
	int crit_soc;
	int soc;
};

/* Battery Properties */
static u32 da9150_fg_read_attr(struct da9150_fg *fg, u8 code, u8 size)

{
	u8 buf[DA9150_QIF_LONG_SIZE];
	u8 read_addr;
	u32 res = 0;
	int i;

	/* Set QIF code (READ mode) */
	read_addr = (code & DA9150_QIF_CODE_MASK) | DA9150_QIF_READ;

	da9150_read_qif(fg->da9150, read_addr, size, buf);
	for (i = 0; i < size; ++i)
		res |= (buf[i] << (i * DA9150_QIF_BYTE_SIZE));

	return res;
}

static void da9150_fg_write_attr(struct da9150_fg *fg, u8 code, u8 size,
				 u32 val)

{
	u8 buf[DA9150_QIF_LONG_SIZE];
	u8 write_addr;
	int i;

	/* Set QIF code (WRITE mode) */
	write_addr = (code & DA9150_QIF_CODE_MASK) | DA9150_QIF_WRITE;

	for (i = 0; i < size; ++i) {
		buf[i] = (val >> (i * DA9150_QIF_BYTE_SIZE)) &
			 DA9150_QIF_BYTE_MASK;
	}
	da9150_write_qif(fg->da9150, write_addr, size, buf);
}

/* Trigger QIF Sync to update QIF readable data */
static void da9150_fg_read_sync_start(struct da9150_fg *fg)
{
	int i = 0;
	u32 res = 0;

	mutex_lock(&fg->io_lock);

	/* Check if QIF sync already requested, and write to sync if not */
	res = da9150_fg_read_attr(fg, DA9150_QIF_SYNC,
				  DA9150_QIF_SYNC_SIZE);
	if (res > 0)
		da9150_fg_write_attr(fg, DA9150_QIF_SYNC,
				     DA9150_QIF_SYNC_SIZE, 0);

	/* Wait for sync to complete */
	res = 0;
	while ((res == 0) && (i++ < DA9150_QIF_SYNC_RETRIES)) {
		usleep_range(DA9150_QIF_SYNC_TIMEOUT,
			     DA9150_QIF_SYNC_TIMEOUT * 2);
		res = da9150_fg_read_attr(fg, DA9150_QIF_SYNC,
					  DA9150_QIF_SYNC_SIZE);
	}

	/* Check if sync completed */
	if (res == 0)
		dev_err(fg->dev, "Failed to perform QIF read sync!\n");
}

/*
 * Should always be called after QIF sync read has been performed, and all
 * attributes required have been accessed.
 */
static inline void da9150_fg_read_sync_end(struct da9150_fg *fg)
{
	mutex_unlock(&fg->io_lock);
}

/* Sync read of single QIF attribute */
static u32 da9150_fg_read_attr_sync(struct da9150_fg *fg, u8 code, u8 size)
{
	u32 val;

	da9150_fg_read_sync_start(fg);
	val = da9150_fg_read_attr(fg, code, size);
	da9150_fg_read_sync_end(fg);

	return val;
}

/* Wait for QIF Sync, write QIF data and wait for ack */
static void da9150_fg_write_attr_sync(struct da9150_fg *fg, u8 code, u8 size,
				      u32 val)
{
	int i = 0;
	u32 res = 0, sync_val;

	mutex_lock(&fg->io_lock);

	/* Check if QIF sync already requested */
	res = da9150_fg_read_attr(fg, DA9150_QIF_SYNC,
				  DA9150_QIF_SYNC_SIZE);

	/* Wait for an existing sync to complete */
	while ((res == 0) && (i++ < DA9150_QIF_SYNC_RETRIES)) {
		usleep_range(DA9150_QIF_SYNC_TIMEOUT,
			     DA9150_QIF_SYNC_TIMEOUT * 2);
		res = da9150_fg_read_attr(fg, DA9150_QIF_SYNC,
					  DA9150_QIF_SYNC_SIZE);
	}

	if (res == 0) {
		dev_err(fg->dev, "Timeout waiting for existing QIF sync!\n");
		mutex_unlock(&fg->io_lock);
		return;
	}

	/* Write value for QIF code */
	da9150_fg_write_attr(fg, code, size, val);

	/* Wait for write acknowledgment */
	i = 0;
	sync_val = res;
	while ((res == sync_val) && (i++ < DA9150_QIF_SYNC_RETRIES)) {
		usleep_range(DA9150_QIF_SYNC_TIMEOUT,
			     DA9150_QIF_SYNC_TIMEOUT * 2);
		res = da9150_fg_read_attr(fg, DA9150_QIF_SYNC,
					  DA9150_QIF_SYNC_SIZE);
	}

	mutex_unlock(&fg->io_lock);

	/* Check write was actually successful */
	if (res != (sync_val + 1))
		dev_err(fg->dev, "Error performing QIF sync write for code %d\n",
			code);
}

/* Power Supply attributes */
static int da9150_fg_capacity(struct da9150_fg *fg,
			      union power_supply_propval *val)
{
	val->intval = da9150_fg_read_attr_sync(fg, DA9150_QIF_SOC_PCT,
					       DA9150_QIF_SOC_PCT_SIZE);

	if (val->intval > 100)
		val->intval = 100;

	return 0;
}

static int da9150_fg_current_avg(struct da9150_fg *fg,
				 union power_supply_propval *val)
{
	u32 iavg, sd_gain, shunt_val;
	u64 div, res;

	da9150_fg_read_sync_start(fg);
	iavg = da9150_fg_read_attr(fg, DA9150_QIF_IAVG,
				   DA9150_QIF_IAVG_SIZE);
	shunt_val = da9150_fg_read_attr(fg, DA9150_QIF_SHUNT_VAL,
					DA9150_QIF_SHUNT_VAL_SIZE);
	sd_gain = da9150_fg_read_attr(fg, DA9150_QIF_SD_GAIN,
				      DA9150_QIF_SD_GAIN_SIZE);
	da9150_fg_read_sync_end(fg);

	div = (u64) (sd_gain * shunt_val * 65536ULL);
	do_div(div, 1000000);
	res = (u64) (iavg * 1000000ULL);
	do_div(res, div);

	val->intval = (int) res;

	return 0;
}

static int da9150_fg_voltage_avg(struct da9150_fg *fg,
				 union power_supply_propval *val)
{
	u64 res;

	val->intval = da9150_fg_read_attr_sync(fg, DA9150_QIF_UAVG,
					       DA9150_QIF_UAVG_SIZE);

	res = (u64) (val->intval * 186ULL);
	do_div(res, 10000);
	val->intval = (int) res;

	return 0;
}

static int da9150_fg_charge_full(struct da9150_fg *fg,
				 union power_supply_propval *val)
{
	val->intval = da9150_fg_read_attr_sync(fg, DA9150_QIF_FCC_MAH,
					       DA9150_QIF_FCC_MAH_SIZE);

	val->intval = val->intval * 1000;

	return 0;
}

/*
 * Temperature reading from device is only valid if battery/system provides
 * valid NTC to associated pin of DA9150 chip.
 */
static int da9150_fg_temp(struct da9150_fg *fg,
			  union power_supply_propval *val)
{
	val->intval = da9150_fg_read_attr_sync(fg, DA9150_QIF_NTCAVG,
					       DA9150_QIF_NTCAVG_SIZE);

	val->intval = (val->intval * 10) / 1048576;

	return 0;
}

static enum power_supply_property da9150_fg_props[] = {
	POWER_SUPPLY_PROP_CAPACITY,
	POWER_SUPPLY_PROP_CURRENT_AVG,
	POWER_SUPPLY_PROP_VOLTAGE_AVG,
	POWER_SUPPLY_PROP_CHARGE_FULL,
	POWER_SUPPLY_PROP_TEMP,
};

static int da9150_fg_get_prop(struct power_supply *psy,
			      enum power_supply_property psp,
			      union power_supply_propval *val)
{
	struct da9150_fg *fg = dev_get_drvdata(psy->dev.parent);
	int ret;

	switch (psp) {
	case POWER_SUPPLY_PROP_CAPACITY:
		ret = da9150_fg_capacity(fg, val);
		break;
	case POWER_SUPPLY_PROP_CURRENT_AVG:
		ret = da9150_fg_current_avg(fg, val);
		break;
	case POWER_SUPPLY_PROP_VOLTAGE_AVG:
		ret = da9150_fg_voltage_avg(fg, val);
		break;
	case POWER_SUPPLY_PROP_CHARGE_FULL:
		ret = da9150_fg_charge_full(fg, val);
		break;
	case POWER_SUPPLY_PROP_TEMP:
		ret = da9150_fg_temp(fg, val);
		break;
	default:
		ret = -EINVAL;
		break;
	}

	return ret;
}

/* Repeated SOC check */
static bool da9150_fg_soc_changed(struct da9150_fg *fg)
{
	union power_supply_propval val;

	da9150_fg_capacity(fg, &val);
	if (val.intval != fg->soc) {
		fg->soc = val.intval;
		return true;
	}

	return false;
}

static void da9150_fg_work(struct work_struct *work)
{
	struct da9150_fg *fg = container_of(work, struct da9150_fg, work.work);

	/* Report if SOC has changed */
	if (da9150_fg_soc_changed(fg))
		power_supply_changed(fg->battery);

	schedule_delayed_work(&fg->work, msecs_to_jiffies(fg->interval));
}

/* SOC level event configuration */
static void da9150_fg_soc_event_config(struct da9150_fg *fg)
{
	int soc;

	soc = da9150_fg_read_attr_sync(fg, DA9150_QIF_SOC_PCT,
				       DA9150_QIF_SOC_PCT_SIZE);

	if (soc > fg->warn_soc) {
		/* If SOC > warn level, set discharge warn level event */
		da9150_fg_write_attr_sync(fg, DA9150_QIF_DISCHARGE_LIMIT,
					  DA9150_QIF_DISCHARGE_LIMIT_SIZE,
					  fg->warn_soc + 1);
	} else if ((soc <= fg->warn_soc) && (soc > fg->crit_soc)) {
		/*
		 * If SOC <= warn level, set discharge crit level event,
		 * and set charge warn level event.
		 */
		da9150_fg_write_attr_sync(fg, DA9150_QIF_DISCHARGE_LIMIT,
					  DA9150_QIF_DISCHARGE_LIMIT_SIZE,
					  fg->crit_soc + 1);

		da9150_fg_write_attr_sync(fg, DA9150_QIF_CHARGE_LIMIT,
					  DA9150_QIF_CHARGE_LIMIT_SIZE,
					  fg->warn_soc);
	} else if (soc <= fg->crit_soc) {
		/* If SOC <= crit level, set charge crit level event */
		da9150_fg_write_attr_sync(fg, DA9150_QIF_CHARGE_LIMIT,
					  DA9150_QIF_CHARGE_LIMIT_SIZE,
					  fg->crit_soc);
	}
}

static irqreturn_t da9150_fg_irq(int irq, void *data)
{
	struct da9150_fg *fg = data;
	u32 e_fg_status;

	/* Read FG IRQ status info */
	e_fg_status = da9150_fg_read_attr(fg, DA9150_QIF_E_FG_STATUS,
					  DA9150_QIF_E_FG_STATUS_SIZE);

	/* Handle warning/critical threhold events */
	if (e_fg_status & DA9150_FG_IRQ_SOC_MASK)
		da9150_fg_soc_event_config(fg);

	/* Clear any FG IRQs */
	da9150_fg_write_attr(fg, DA9150_QIF_E_FG_STATUS,
			     DA9150_QIF_E_FG_STATUS_SIZE, e_fg_status);

	return IRQ_HANDLED;
}

static struct da9150_fg_pdata *da9150_fg_dt_pdata(struct device *dev)
{
	struct device_node *fg_node = dev->of_node;
	struct da9150_fg_pdata *pdata;

	pdata = devm_kzalloc(dev, sizeof(struct da9150_fg_pdata), GFP_KERNEL);
	if (!pdata)
		return NULL;

	of_property_read_u32(fg_node, "dlg,update-interval",
			     &pdata->update_interval);
	of_property_read_u8(fg_node, "dlg,warn-soc-level",
			    &pdata->warn_soc_lvl);
	of_property_read_u8(fg_node, "dlg,crit-soc-level",
			    &pdata->crit_soc_lvl);

	return pdata;
}

static const struct power_supply_desc fg_desc = {
	.name		= "da9150-fg",
	.type		= POWER_SUPPLY_TYPE_BATTERY,
	.properties	= da9150_fg_props,
	.num_properties	= ARRAY_SIZE(da9150_fg_props),
	.get_property	= da9150_fg_get_prop,
};

static int da9150_fg_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct da9150 *da9150 = dev_get_drvdata(dev->parent);
	struct da9150_fg_pdata *fg_pdata = dev_get_platdata(dev);
	struct da9150_fg *fg;
	int ver, irq, ret = 0;

	fg = devm_kzalloc(dev, sizeof(*fg), GFP_KERNEL);
	if (fg == NULL)
		return -ENOMEM;

	platform_set_drvdata(pdev, fg);
	fg->da9150 = da9150;
	fg->dev = dev;

	mutex_init(&fg->io_lock);

	/* Enable QIF */
	da9150_set_bits(da9150, DA9150_CORE2WIRE_CTRL_A, DA9150_FG_QIF_EN_MASK,
			DA9150_FG_QIF_EN_MASK);

	fg->battery = devm_power_supply_register(dev, &fg_desc, NULL);
	if (IS_ERR(fg->battery)) {
		ret = PTR_ERR(fg->battery);
		return ret;
	}

	ver = da9150_fg_read_attr(fg, DA9150_QIF_FW_MAIN_VER,
				  DA9150_QIF_FW_MAIN_VER_SIZE);
	dev_info(dev, "Version: 0x%x\n", ver);

	/* Handle DT data if provided */
	if (dev->of_node) {
		fg_pdata = da9150_fg_dt_pdata(dev);
		dev->platform_data = fg_pdata;
	}

	/* Handle any pdata provided */
	if (fg_pdata) {
		fg->interval = fg_pdata->update_interval;

		if (fg_pdata->warn_soc_lvl > 100)
			dev_warn(dev, "Invalid SOC warning level provided, Ignoring");
		else
			fg->warn_soc = fg_pdata->warn_soc_lvl;

		if ((fg_pdata->crit_soc_lvl > 100) ||
		    (fg_pdata->crit_soc_lvl >= fg_pdata->warn_soc_lvl))
			dev_warn(dev, "Invalid SOC critical level provided, Ignoring");
		else
			fg->crit_soc = fg_pdata->crit_soc_lvl;


	}

	/* Configure initial SOC level events */
	da9150_fg_soc_event_config(fg);

	/*
	 * If an interval period has been provided then setup repeating
	 * work for reporting data updates.
	 */
	if (fg->interval) {
		ret = devm_delayed_work_autocancel(dev, &fg->work,
						   da9150_fg_work);
		if (ret) {
			dev_err(dev, "Failed to init work\n");
			return ret;
		}

		schedule_delayed_work(&fg->work,
				      msecs_to_jiffies(fg->interval));
	}

	/* Register IRQ */
	irq = platform_get_irq_byname(pdev, "FG");
	if (irq < 0)
		return irq;

	ret = devm_request_threaded_irq(dev, irq, NULL, da9150_fg_irq,
					IRQF_ONESHOT, "FG", fg);
	if (ret) {
		dev_err(dev, "Failed to request IRQ %d: %d\n", irq, ret);
		return ret;
	}

	return 0;
}

static int da9150_fg_resume(struct platform_device *pdev)
{
	struct da9150_fg *fg = platform_get_drvdata(pdev);

	/*
	 * Trigger SOC check to happen now so as to indicate any value change
	 * since last check before suspend.
	 */
	if (fg->interval)
		flush_delayed_work(&fg->work);

	return 0;
}

static struct platform_driver da9150_fg_driver = {
	.driver = {
		.name = "da9150-fuel-gauge",
	},
	.probe = da9150_fg_probe,
	.resume = da9150_fg_resume,
};

module_platform_driver(da9150_fg_driver);

MODULE_DESCRIPTION("Fuel-Gauge Driver for DA9150");
MODULE_AUTHOR("Adam Thomson <Adam.Thomson.Opensource@diasemi.com>");
MODULE_LICENSE("GPL");