[1/2] leds: Add driver for Qualcomm LPG

Message ID 20170323055435.29197-1-bjorn.andersson@linaro.org
State New
Headers show
Series
  • [1/2] leds: Add driver for Qualcomm LPG
Related show

Commit Message

Bjorn Andersson March 23, 2017, 5:54 a.m.
The Light Pulse Generator (LPG) is a PWM-block found in a wide range of
PMICs from Qualcomm. It can operate on fixed parameters or based on a
lookup-table, altering the duty cycle over time - which provides the
means for e.g. hardware assisted transitions of LED brightness.

Signed-off-by: Bjorn Andersson <bjorn.andersson@linaro.org>

---
 .../ABI/testing/sysfs-class-led-driver-qcom-lpg    |  47 ++
 drivers/leds/Kconfig                               |   7 +
 drivers/leds/Makefile                              |   3 +
 drivers/leds/leds-qcom-lpg-lut.c                   | 299 ++++++++
 drivers/leds/leds-qcom-lpg.c                       | 779 +++++++++++++++++++++
 drivers/leds/leds-qcom-lpg.h                       |  30 +
 drivers/leds/leds-qcom-triled.c                    | 193 +++++
 7 files changed, 1358 insertions(+)
 create mode 100644 Documentation/ABI/testing/sysfs-class-led-driver-qcom-lpg
 create mode 100644 drivers/leds/leds-qcom-lpg-lut.c
 create mode 100644 drivers/leds/leds-qcom-lpg.c
 create mode 100644 drivers/leds/leds-qcom-lpg.h
 create mode 100644 drivers/leds/leds-qcom-triled.c

-- 
2.12.0

Comments

Bjorn Andersson March 30, 2017, 12:09 a.m. | #1
On Wed 29 Mar 15:23 PDT 2017, Pavel Machek wrote:

> On Wed 2017-03-29 12:07:25, Bjorn Andersson wrote:

> > On Tue 28 Mar 19:17 PDT 2017, Rob Herring wrote:

> > 

> > > On Thu, Mar 23, 2017 at 09:37:49PM +0100, Pavel Machek wrote:

> > > > Hi!

> > > > 

> > > > > The Light Pulse Generator (LPG) is a PWM-block found in a wide range of

> > > > > PMICs from Qualcomm. It can operate on fixed parameters or based on a

> > > > > lookup-table, altering the duty cycle over time - which provides the

> > > > > means for e.g. hardware assisted transitions of LED brightness.

> > > > 

> > > > Ok, this is not first hardware that supports something like this. We

> > > > have similar hardware that can do blinking on Nokia N900 -- please

> > > > take a look at leds-lp55*.c

> > > 

> > > And perhaps some alignment on the bindings too if the N900 has bindings.

> > > 

> > 

> > There is a binding for ti,lp55xx, but there's nothing I can reuse from

> > that binding...because it's completely different hardware.

> 

> Agreed, if you drop the pattern stuff from the binding, at least for now. 

> 


I do not have a strong preference to expose these knobs in devicetree
and I do fear that finding some common "pattern" bindings that suits
everyone will be very difficult.

So I'll drop them from the binding for now.

> > > > And it would be really good to provide hardware abstraction. We really

> > > > don't want to have different userspace for LPG and for N900 and for

> > > 

> > > I'm interested in what this looks like as several AOSP platforms do 

> > > tri-color LEDs with custom sysfs extensions.

> > 

> > How to model RGB LEDs has been discussed many times before and I was

> > hoping for that discussion to come to some conclusion during the last 2

> > years, but now I couldn't wait more - we need this driver for

> > db820c.

> 

> If you want driver merged quickly, I believe the best way would be to

> leave out pattern support for now. We can merge the basic driver

> easily to 4.12.

> 


I'm not that much in a hurry and would rather see that we resolve any
outstanding issues with the implementation of the pattern handling.


But regardless of this we still have the problem that the typical
Qualcomm PMIC has 8 LPG-blocks and any triple could be driving a
RGB-LED. So we would have to create some sort of in-driver-wrapper
around any three instances exposing them as a single LED to the user.

I rather expose the individual channels and make sure that when we
trigger a blink operation or enable a pattern (i.e. the two operations
that do require synchronization) we will perform that synchronization
under the hood.

> > With this driver, as with many existing, you will have 3 LEDs that you

> > set independently.

> > 

> > I did implement blinking by using the PWM straight off, so you can't set

> > brightness or synchronize the multiple channels. Perhaps this should be

> > changed to use the ramp generator.

> > 

> > To synchronize patterns I suggest that we extend the LUT binding to

> > describe groups and when any LPG trigger a restart of the pattern-walker

> > we trigger all that are grouped.

> > 

> > These two changes combined allows you to set brightness and blink with a

> > RGB-LED.

> > 

> > 

> > But I will have to dig up some hardware that uses the LPG for driving a

> > RGB-LED to be able to test this (and I do prefer that to be done with

> > some incremental patches at some later time, if acceptable).

> 

> Incremental patches sound like a good idea, yes.

> 

> I'd say that testing with actual RGB LED is not a requirement... as

> long as we design reasonable interface where the synchronizaction will

> be easy.

> 


As this relates to the board layout (which LPG-channels are hooked to a
RGB) I think it makes sense to expose a mechanism in devicetree to
indicate which channels should have their pattern/blink synchronized.

We should be able to extend the LUT (the hardware that actually
implements the pattern-walker logic) with a DT-property like:

  qcom,synchronize-group-0 = <1, 2, 3>;
  qcom,synchronize-group-1 = <5, 6, 7>;

And whenever we configure a pattern involving one of the affected LEDs
from a group we start all of them.

I'll implement this in a separate patch and include in version 2 as
well.

Regards,
Bjorn

Patch hide | download patch | download mbox

diff --git a/Documentation/ABI/testing/sysfs-class-led-driver-qcom-lpg b/Documentation/ABI/testing/sysfs-class-led-driver-qcom-lpg
new file mode 100644
index 000000000000..35bfe6e8e148
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-class-led-driver-qcom-lpg
@@ -0,0 +1,47 @@ 
+What:		/sys/class/leds/<led>/duration
+Date:		March 2017
+KernelVersion:	4.12
+Contact:	Bjorn Andersson <bjorn.andersson@linaro.org>
+Description:
+		Duration of one cycle through the pattern in the ramp
+		generator.
+
+What:		/sys/class/leds/<led>/oneshot
+Date:		March 2017
+KernelVersion:	4.12
+Contact:	Bjorn Andersson <bjorn.andersson@linaro.org>
+Description:
+		Stop the ramp generator after a single pass.
+
+What:		/sys/class/leds/<led>/pattern
+Date:		March 2017
+KernelVersion:	4.12
+Contact:	Bjorn Andersson <bjorn.andersson@linaro.org>
+Description:
+		Comma-separated list of duty cycle values to output from
+		the ramp generator. Values should be in the range of 0
+		to 511.
+
+What:		/sys/class/leds/<led>/pause_lo
+What:		/sys/class/leds/<led>/pause_hi
+Date:		March 2017
+KernelVersion:	4.12
+Contact:	Bjorn Andersson <bjorn.andersson@linaro.org>
+Description:
+		Pause time, in milliseconds, before and after one run
+		over the pattern.
+
+What:		/sys/class/leds/<led>/ping_pong
+Date:		March 2017
+KernelVersion:	4.12
+Contact:	Bjorn Andersson <bjorn.andersson@linaro.org>
+Description:
+		Reverse direction when the ramp generator reaches the
+		end of the pattern, rather than wrapping to the start.
+
+What:		/sys/class/leds/<led>/reverse
+Date:		March 2017
+KernelVersion:	4.12
+Contact:	Bjorn Andersson <bjorn.andersson@linaro.org>
+Description:
+		Run the ramp generator backwards over the pattern.
diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig
index 275f467956ee..4b08d9802d5b 100644
--- a/drivers/leds/Kconfig
+++ b/drivers/leds/Kconfig
@@ -634,6 +634,13 @@  config LEDS_POWERNV
 	  To compile this driver as a module, choose 'm' here: the module
 	  will be called leds-powernv.
 
+config LEDS_QCOM_LPG
+	tristate "LED support for Qualcomm LPG"
+	depends on LEDS_CLASS
+	help
+	  This option enables support for the Light Pulse Generator found in a
+	  wide variety of Qualcomm PMICs.
+
 config LEDS_SYSCON
 	bool "LED support for LEDs on system controllers"
 	depends on LEDS_CLASS=y
diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile
index 6b8273736478..6390c1a36b13 100644
--- a/drivers/leds/Makefile
+++ b/drivers/leds/Makefile
@@ -61,6 +61,9 @@  obj-$(CONFIG_LEDS_MAX77693)		+= leds-max77693.o
 obj-$(CONFIG_LEDS_MAX8997)		+= leds-max8997.o
 obj-$(CONFIG_LEDS_LM355x)		+= leds-lm355x.o
 obj-$(CONFIG_LEDS_BLINKM)		+= leds-blinkm.o
+obj-$(CONFIG_LEDS_QCOM_LPG)		+= leds-qcom-lpg.o
+obj-$(CONFIG_LEDS_QCOM_LPG)		+= leds-qcom-lpg-lut.o
+obj-$(CONFIG_LEDS_QCOM_LPG)		+= leds-qcom-triled.o
 obj-$(CONFIG_LEDS_SYSCON)		+= leds-syscon.o
 obj-$(CONFIG_LEDS_VERSATILE)		+= leds-versatile.o
 obj-$(CONFIG_LEDS_MENF21BMC)		+= leds-menf21bmc.o
diff --git a/drivers/leds/leds-qcom-lpg-lut.c b/drivers/leds/leds-qcom-lpg-lut.c
new file mode 100644
index 000000000000..020f9cefda23
--- /dev/null
+++ b/drivers/leds/leds-qcom-lpg-lut.c
@@ -0,0 +1,299 @@ 
+/* Copyright (c) 2017 Linaro Ltd
+ *
+ * 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.
+ */
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_device.h>
+#include <linux/platform_device.h>
+#include <linux/pm.h>
+#include <linux/pwm.h>
+#include <linux/regmap.h>
+#include <linux/slab.h>
+
+#include "leds-qcom-lpg.h"
+
+#define LPG_LUT_REG(x)		(0x40 + (x) * 2)
+#define RAMP_CONTROL_REG	0xc8
+
+static struct platform_driver lpg_lut_driver;
+
+/*
+ * lpg_lut_dev - LUT device context
+ * @dev:	struct device for the LUT device
+ * @map:	regmap for register access
+ * @reg:	base address for the LUT block
+ * @size:	number of LUT entries in LUT block
+ * @bitmap:	bitmap tracking occupied LUT entries
+ */
+struct lpg_lut_dev {
+	struct device *dev;
+	struct regmap *map;
+
+	u32 reg;
+	u32 size;
+
+	unsigned long bitmap[];
+};
+
+/*
+ * qcom_lpg_lut - context for a client and LUT device pair
+ * @ldev:	reference to a LUT device
+ * @start_mask:	mask of bits to use for synchronizing ramp generators
+ */
+struct qcom_lpg_lut {
+	struct lpg_lut_dev *ldev;
+	int start_mask;
+};
+
+static void lpg_lut_release(struct device *dev, void *res)
+{
+	struct qcom_lpg_lut *lut = res;
+
+	put_device(lut->ldev->dev);
+}
+
+/**
+ * qcom_lpg_lut_get() - acquire a handle to the LUT implementation
+ * @dev:	struct device reference of the client
+ *
+ * Returns a LUT context, or ERR_PTR on failure.
+ */
+struct qcom_lpg_lut *qcom_lpg_lut_get(struct device *dev)
+{
+	struct platform_device *pdev;
+	struct device_node *lut_node;
+	struct qcom_lpg_lut *lut;
+	u32 cell;
+	int ret;
+
+	lut_node = of_parse_phandle(dev->of_node, "qcom,lut", 0);
+	if (!lut_node)
+		return NULL;
+
+	ret = of_property_read_u32(dev->of_node, "cell-index", &cell);
+	if (ret) {
+		dev_err(dev, "lpg without cell-index\n");
+		return ERR_PTR(ret);
+	}
+
+	pdev = of_find_device_by_node(lut_node);
+	of_node_put(lut_node);
+	if (!pdev || !pdev->dev.driver)
+		return ERR_PTR(-EPROBE_DEFER);
+
+	if (pdev->dev.driver != &lpg_lut_driver.driver) {
+		dev_err(dev, "referenced node is not a lpg lut\n");
+		return ERR_PTR(-EINVAL);
+	}
+
+	lut = devres_alloc(lpg_lut_release, sizeof(*lut), GFP_KERNEL);
+	if (!lut)
+		return ERR_PTR(-ENOMEM);
+
+	lut->ldev = platform_get_drvdata(pdev);
+	lut->start_mask = BIT(cell - 1);
+
+	devres_add(dev, lut);
+
+	return lut;
+}
+EXPORT_SYMBOL_GPL(qcom_lpg_lut_get);
+
+/**
+ * qcom_lpg_lut_store() - store a sequence of levels in the LUT
+ * @lut:	LUT context acquired from qcom_lpg_lut_get()
+ * @values:	an array of values, in the range 0 <= x < 512
+ * @len:	length of the @values array
+ *
+ * Returns a qcom_lpg_pattern object, or ERR_PTR on failure.
+ *
+ * Patterns must be freed by calling qcom_lpg_lut_free()
+ */
+struct qcom_lpg_pattern *qcom_lpg_lut_store(struct qcom_lpg_lut *lut,
+					    const u16 *values, size_t len)
+{
+	struct qcom_lpg_pattern *pattern;
+	struct lpg_lut_dev *ldev = lut->ldev;
+	unsigned long lo_idx;
+	u8 val[2];
+	int i;
+
+	/* Hardware does not behave when LO_IDX == HI_IDX */
+	if (len == 1)
+		return ERR_PTR(-EINVAL);
+
+	lo_idx = bitmap_find_next_zero_area(ldev->bitmap, ldev->size, 0, len, 0);
+	if (lo_idx >= ldev->size)
+		return ERR_PTR(-ENOMEM);
+
+	pattern = kzalloc(sizeof(*pattern), GFP_KERNEL);
+	if (!pattern)
+		return ERR_PTR(-ENOMEM);
+
+	pattern->lut = lut;
+	pattern->lo_idx = lo_idx;
+	pattern->hi_idx = lo_idx + len - 1;
+
+	for (i = 0; i < len; i++) {
+		val[0] = values[i] & 0xff;
+		val[1] = values[i] >> 8;
+
+		regmap_bulk_write(ldev->map,
+				  ldev->reg + LPG_LUT_REG(lo_idx + i), val, 2);
+	}
+
+	bitmap_set(ldev->bitmap, lo_idx, len);
+
+	return pattern;
+}
+EXPORT_SYMBOL_GPL(qcom_lpg_lut_store);
+
+ssize_t qcom_lpg_lut_show(struct qcom_lpg_pattern *pattern, char *buf)
+{
+	struct qcom_lpg_lut *lut;
+	struct lpg_lut_dev *ldev;
+	unsigned long lo_idx;
+	char chunk[6]; /* 3 digits, a comma, a space and NUL */
+	char *bp = buf;
+	int len;
+	u8 val[2];
+	int ret;
+	int i;
+	int n;
+
+	if (!pattern)
+		return 0;
+
+	lut = pattern->lut;
+	ldev = lut->ldev;
+	lo_idx = pattern->lo_idx;
+
+	len = pattern->hi_idx - pattern->lo_idx + 1;
+	for (i = 0; i < len; i++) {
+		ret = regmap_bulk_read(ldev->map,
+				       ldev->reg + LPG_LUT_REG(lo_idx + i),
+				       &val, 2);
+		if (ret)
+			return ret;
+
+		n = snprintf(chunk, sizeof(chunk), "%d", val[0] | val[1] << 8);
+
+		/* ensure we have space for value, comma and NUL */
+		if (bp + n + 2 >= buf + PAGE_SIZE)
+			return -E2BIG;
+
+		memcpy(bp, chunk, n);
+		bp += n;
+
+		if (i < len - 1)
+			*bp++ = ',';
+		else
+			*bp++ = '\n';
+	}
+
+	*bp = '\0';
+
+	return bp - buf;
+}
+EXPORT_SYMBOL_GPL(qcom_lpg_lut_show);
+
+/**
+ * qcom_lpg_lut_free() - release LUT pattern and free entries
+ * @pattern:	reference to pattern to release
+ */
+void qcom_lpg_lut_free(struct qcom_lpg_pattern *pattern)
+{
+	struct qcom_lpg_lut *lut;
+	struct lpg_lut_dev *ldev;
+	int len;
+
+	if (!pattern)
+		return;
+
+	lut = pattern->lut;
+	ldev = lut->ldev;
+
+	len = pattern->hi_idx - pattern->lo_idx + 1;
+	bitmap_clear(ldev->bitmap, pattern->lo_idx, len);
+}
+EXPORT_SYMBOL_GPL(qcom_lpg_lut_free);
+
+/**
+ * qcom_lpg_lut_sync() - (re)start the ramp generator, to sync pattern
+ * @lut:	LUT device reference, to sync
+ */
+int qcom_lpg_lut_sync(struct qcom_lpg_lut *lut)
+{
+	struct lpg_lut_dev *ldev = lut->ldev;
+
+	return regmap_update_bits(ldev->map, ldev->reg + RAMP_CONTROL_REG,
+				  lut->start_mask, 0xff);
+}
+EXPORT_SYMBOL_GPL(qcom_lpg_lut_sync);
+
+static int lpg_lut_probe(struct platform_device *pdev)
+{
+	struct device_node *np = pdev->dev.of_node;
+	struct lpg_lut_dev *ldev;
+	size_t bitmap_size;
+	u32 size;
+	int ret;
+
+	ret = of_property_read_u32(np, "qcom,lut-size", &size);
+	if (ret) {
+		dev_err(&pdev->dev, "invalid LUT size\n");
+		return -EINVAL;
+	}
+
+	bitmap_size = BITS_TO_LONGS(size) / sizeof(unsigned long);
+	ldev = devm_kzalloc(&pdev->dev, sizeof(*ldev) + bitmap_size, GFP_KERNEL);
+	if (!ldev)
+		return -ENOMEM;
+
+	ldev->dev = &pdev->dev;
+	ldev->size = size;
+
+	ldev->map = dev_get_regmap(pdev->dev.parent, NULL);
+	if (!ldev->map) {
+		dev_err(&pdev->dev, "parent regmap unavailable\n");
+		return -ENXIO;
+	}
+
+	ret = of_property_read_u32(np, "reg", &ldev->reg);
+	if (ret) {
+		dev_err(&pdev->dev, "no register offset specified\n");
+		return -EINVAL;
+	}
+
+	platform_set_drvdata(pdev, ldev);
+
+	return 0;
+}
+
+static const struct of_device_id lpg_lut_of_table[] = {
+	{ .compatible = "qcom,spmi-lpg-lut" },
+	{},
+};
+MODULE_DEVICE_TABLE(of, lpg_lut_of_table);
+
+static struct platform_driver lpg_lut_driver = {
+	.probe = lpg_lut_probe,
+	.driver = {
+		.name = "qcom_lpg_lut",
+		.of_match_table = lpg_lut_of_table,
+	},
+};
+module_platform_driver(lpg_lut_driver);
+
+MODULE_DESCRIPTION("Qualcomm TRI LED driver");
+MODULE_LICENSE("GPL v2");
+
diff --git a/drivers/leds/leds-qcom-lpg.c b/drivers/leds/leds-qcom-lpg.c
new file mode 100644
index 000000000000..b60a446c5e8b
--- /dev/null
+++ b/drivers/leds/leds-qcom-lpg.c
@@ -0,0 +1,779 @@ 
+/*
+ * Copyright (c) 2017 Linaro Ltd
+ * Copyright (c) 2010-2012, 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.
+ */
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_device.h>
+#include <linux/platform_device.h>
+#include <linux/pm.h>
+#include <linux/pwm.h>
+#include <linux/regmap.h>
+#include <linux/slab.h>
+
+#include "leds-qcom-lpg.h"
+
+#define LPG_PATTERN_CONFIG_REG	0x40
+#define LPG_SIZE_CLK_REG	0x41
+#define LPG_PREDIV_CLK_REG	0x42
+#define PWM_TYPE_CONFIG_REG	0x43
+#define PWM_VALUE_REG		0x44
+#define PWM_ENABLE_CONTROL_REG	0x46
+#define PWM_SYNC_REG		0x47
+#define LPG_RAMP_DURATION_REG	0x50
+#define LPG_HI_PAUSE_REG	0x52
+#define LPG_LO_PAUSE_REG	0x54
+#define LPG_HI_IDX_REG		0x56
+#define LPG_LO_IDX_REG		0x57
+#define PWM_SEC_ACCESS_REG	0xd0
+#define PWM_DTEST_REG(x)	(0xe2 + (x) - 1)
+
+/*
+ * lpg - LPG device context
+ * @dev:	struct device for LPG device
+ * @map:	regmap for register access
+ * @reg:	base address of the LPG device
+ * @dtest_line:	DTEST line for output, or 0 if disabled
+ * @dtest_value: DTEST line configuration
+ * @is_lpg:	operating as LPG, in contrast to simple PWM
+ * @cdev:	LED class object
+ * @tri_led:	reference to TRILED color object, optional
+ * @chip:	PWM-chip object, if operating in PWM mode
+ * @period_us:	period (in microseconds) of the generated pulses
+ * @pwm_value:	duty (in microseconds) of the generated pulses, overriden by LUT
+ * @enabled:	output enabled?
+ * @pwm_size:	resolution of the @pwm_value, 6 or 9 bits
+ * @clk:	base frequency of the clock generator
+ * @pre_div:	divider of @clk
+ * @pre_div_exp: exponential divider of @clk
+ * @ramp_enabled: duty cycle is driven by iterating over lookup table
+ * @ramp_ping_pong: reverse through pattern, rather than wrapping to start
+ * @ramp_oneshot: perform only a single pass over the pattern
+ * @ramp_reverse: iterate over pattern backwards
+ * @ramp_duration_ms: length (in milliseconds) of one pattern run
+ * @ramp_lo_pause_ms: pause (in milliseconds) before iterating over pattern
+ * @ramp_hi_pause_ms: pause (in milliseconds) after iterating over pattern
+ * @lut:	LUT context reference
+ * @pattern:	reference to allocated pattern with LUT
+ */
+
+struct lpg {
+	struct device *dev;
+	struct regmap *map;
+
+	u32 reg;
+	int dtest_line;
+	int dtest_value;
+
+	bool is_lpg;
+
+	struct led_classdev cdev;
+
+	struct qcom_tri_led *tri_led;
+
+	struct pwm_chip chip;
+
+	unsigned int period_us;
+
+	u16 pwm_value;
+	bool enabled;
+
+	unsigned int pwm_size;
+	unsigned int clk;
+	unsigned int pre_div;
+	unsigned int pre_div_exp;
+
+	bool ramp_enabled;
+	bool ramp_ping_pong;
+	bool ramp_oneshot;
+	bool ramp_reverse;
+	unsigned long ramp_duration_ms;
+	unsigned long ramp_lo_pause_ms;
+	unsigned long ramp_hi_pause_ms;
+
+	struct qcom_lpg_lut *lut;
+	struct qcom_lpg_pattern *pattern;
+};
+
+#define NUM_PWM_PREDIV	4
+#define NUM_PWM_CLK	3
+#define NUM_EXP		7
+
+static const unsigned int lpg_clk_table[NUM_PWM_PREDIV][NUM_PWM_CLK] = {
+	{
+		1 * (NSEC_PER_SEC / 1024),
+		1 * (NSEC_PER_SEC / 32768),
+		1 * (NSEC_PER_SEC / 19200000),
+	},
+	{
+		3 * (NSEC_PER_SEC / 1024),
+		3 * (NSEC_PER_SEC / 32768),
+		3 * (NSEC_PER_SEC / 19200000),
+	},
+	{
+		5 * (NSEC_PER_SEC / 1024),
+		5 * (NSEC_PER_SEC / 32768),
+		5 * (NSEC_PER_SEC / 19200000),
+	},
+	{
+		6 * (NSEC_PER_SEC / 1024),
+		6 * (NSEC_PER_SEC / 32768),
+		6 * (NSEC_PER_SEC / 19200000),
+	},
+};
+
+/*
+ * PWM Frequency = Clock Frequency / (N * T)
+ *      or
+ * PWM Period = Clock Period * (N * T)
+ *      where
+ * N = 2^9 or 2^6 for 9-bit or 6-bit PWM size
+ * T = Pre-divide * 2^m, where m = 0..7 (exponent)
+ *
+ * This is the formula to figure out m for the best pre-divide and clock:
+ * (PWM Period / N) = (Pre-divide * Clock Period) * 2^m
+ */
+static void lpg_calc_freq(struct lpg *lpg, unsigned int period_us)
+{
+	int             n, m, clk, div;
+	int             best_m, best_div, best_clk;
+	unsigned int    last_err, cur_err, min_err;
+	unsigned int    tmp_p, period_n;
+
+	if (period_us == lpg->period_us)
+		return;
+
+	/* PWM Period / N */
+	if (period_us < ((unsigned int)(-1) / NSEC_PER_USEC)) {
+		period_n = (period_us * NSEC_PER_USEC) >> 6;
+		n = 6;
+	} else {
+		period_n = (period_us >> 9) * NSEC_PER_USEC;
+		n = 9;
+	}
+
+	min_err = last_err = (unsigned int)(-1);
+	best_m = 0;
+	best_clk = 0;
+	best_div = 0;
+	for (clk = 0; clk < NUM_PWM_CLK; clk++) {
+		for (div = 0; div < NUM_PWM_PREDIV; div++) {
+			/* period_n = (PWM Period / N) */
+			/* tmp_p = (Pre-divide * Clock Period) * 2^m */
+			tmp_p = lpg_clk_table[div][clk];
+			for (m = 0; m <= NUM_EXP; m++) {
+				if (period_n > tmp_p)
+					cur_err = period_n - tmp_p;
+				else
+					cur_err = tmp_p - period_n;
+
+				if (cur_err < min_err) {
+					min_err = cur_err;
+					best_m = m;
+					best_clk = clk;
+					best_div = div;
+				}
+
+				if (m && cur_err > last_err)
+					/* Break for bigger cur_err */
+					break;
+
+				last_err = cur_err;
+				tmp_p <<= 1;
+			}
+		}
+	}
+
+	/* Use higher resolution */
+	if (best_m >= 3 && n == 6) {
+		n += 3;
+		best_m -= 3;
+	}
+
+	lpg->clk = best_clk;
+	lpg->pre_div = best_div;
+	lpg->pre_div_exp = best_m;
+	lpg->pwm_size = n;
+
+	lpg->period_us = period_us;
+}
+
+static void lpg_calc_duty(struct lpg *lpg, unsigned long duty_us)
+{
+	unsigned long max = (1 << lpg->pwm_size) - 1;
+	unsigned long val;
+
+	/* Figure out pwm_value with overflow handling */
+	if (duty_us < 1 << (sizeof(val) * 8 - lpg->pwm_size))
+		val = (duty_us << lpg->pwm_size) / lpg->period_us;
+	else
+		val = duty_us / (lpg->period_us >> lpg->pwm_size);
+
+	if (val > max)
+		val = max;
+
+	lpg->pwm_value = val;
+}
+
+#define LPG_RESOLUTION_9BIT	BIT(4)
+
+static void lpg_apply_freq(struct lpg *lpg)
+{
+	unsigned long val;
+
+	if (!lpg->enabled)
+		return;
+
+	/* Clock register values are off-by-one from lpg_clk_table */
+	val = lpg->clk + 1;
+
+	if (lpg->pwm_size == 9)
+		val |= LPG_RESOLUTION_9BIT;
+	regmap_write(lpg->map, lpg->reg + LPG_SIZE_CLK_REG, val);
+
+	val = lpg->pre_div << 5 | lpg->pre_div_exp;
+	regmap_write(lpg->map, lpg->reg + LPG_PREDIV_CLK_REG, val);
+}
+
+#define LPG_ENABLE_GLITCH_REMOVAL	BIT(5)
+
+static void lpg_enable_glitch(struct lpg *lpg)
+{
+	regmap_update_bits(lpg->map, lpg->reg + PWM_TYPE_CONFIG_REG,
+			   LPG_ENABLE_GLITCH_REMOVAL, 0);
+}
+
+static void lpg_disable_glitch(struct lpg *lpg)
+{
+	regmap_update_bits(lpg->map, lpg->reg + PWM_TYPE_CONFIG_REG,
+			   LPG_ENABLE_GLITCH_REMOVAL,
+			   LPG_ENABLE_GLITCH_REMOVAL);
+}
+
+static void lpg_apply_pwm_value(struct lpg *lpg)
+{
+	u8 val[] = { lpg->pwm_value & 0xff, lpg->pwm_value >> 8 };
+
+	if (!lpg->enabled)
+		return;
+
+	regmap_bulk_write(lpg->map, lpg->reg + PWM_VALUE_REG, val, 2);
+}
+
+#define LPG_PATTERN_CONFIG_LO_TO_HI	BIT(4)
+#define LPG_PATTERN_CONFIG_REPEAT	BIT(3)
+#define LPG_PATTERN_CONFIG_TOGGLE	BIT(2)
+#define LPG_PATTERN_CONFIG_PAUSE_HI	BIT(1)
+#define LPG_PATTERN_CONFIG_PAUSE_LO	BIT(0)
+
+static void lpg_apply_lut_control(struct lpg *lpg)
+{
+	struct qcom_lpg_pattern *pattern = lpg->pattern;
+	unsigned int hi_pause;
+	unsigned int lo_pause;
+	unsigned int step;
+	unsigned int conf = 0;
+	int pattern_len;
+
+	if (!lpg->ramp_enabled || !pattern)
+		return;
+
+	pattern_len = pattern->hi_idx - pattern->lo_idx + 1;
+
+	step = DIV_ROUND_UP(lpg->ramp_duration_ms, pattern_len);
+	hi_pause = DIV_ROUND_UP(lpg->ramp_hi_pause_ms, step);
+	lo_pause = DIV_ROUND_UP(lpg->ramp_lo_pause_ms, step);
+
+	if (!lpg->ramp_reverse)
+		conf |= LPG_PATTERN_CONFIG_LO_TO_HI;
+	if (!lpg->ramp_oneshot)
+		conf |= LPG_PATTERN_CONFIG_REPEAT;
+	if (lpg->ramp_ping_pong)
+		conf |= LPG_PATTERN_CONFIG_TOGGLE;
+	if (lpg->ramp_hi_pause_ms)
+		conf |= LPG_PATTERN_CONFIG_PAUSE_HI;
+	if (lpg->ramp_lo_pause_ms)
+		conf |= LPG_PATTERN_CONFIG_PAUSE_LO;
+
+	regmap_write(lpg->map, lpg->reg + LPG_PATTERN_CONFIG_REG, conf);
+	regmap_write(lpg->map, lpg->reg + LPG_HI_IDX_REG, pattern->hi_idx);
+	regmap_write(lpg->map, lpg->reg + LPG_LO_IDX_REG, pattern->lo_idx);
+
+	regmap_write(lpg->map, lpg->reg + LPG_RAMP_DURATION_REG, step);
+	regmap_write(lpg->map, lpg->reg + LPG_HI_PAUSE_REG, hi_pause);
+	regmap_write(lpg->map, lpg->reg + LPG_LO_PAUSE_REG, lo_pause);
+
+	/* Trigger start of ramp generator(s) */
+	qcom_lpg_lut_sync(lpg->lut);
+}
+
+#define LPG_ENABLE_CONTROL_OUTPUT		BIT(7)
+#define LPG_ENABLE_CONTROL_BUFFER_TRISTATE	BIT(5)
+#define LPG_ENABLE_CONTROL_SRC_PWM		BIT(2)
+#define LPG_ENABLE_CONTROL_RAMP_GEN		BIT(1)
+
+static void lpg_apply_control(struct lpg *lpg)
+{
+	unsigned int ctrl;
+
+	ctrl = LPG_ENABLE_CONTROL_BUFFER_TRISTATE;
+
+	if (lpg->enabled)
+		ctrl |= LPG_ENABLE_CONTROL_OUTPUT;
+
+	if (lpg->pattern)
+		ctrl |= LPG_ENABLE_CONTROL_RAMP_GEN;
+	else
+		ctrl |= LPG_ENABLE_CONTROL_SRC_PWM;
+
+	regmap_write(lpg->map, lpg->reg + PWM_ENABLE_CONTROL_REG, ctrl);
+
+	/*
+	 * Due to LPG hardware bug, in the PWM mode, having enabled PWM,
+	 * We have to write PWM values one more time.
+	 */
+	if (lpg->enabled)
+		lpg_apply_pwm_value(lpg);
+}
+
+#define LPG_SYNC_PWM	BIT(0)
+
+static void lpg_apply_sync(struct lpg *lpg)
+{
+	regmap_write(lpg->map, lpg->reg + PWM_SYNC_REG, LPG_SYNC_PWM);
+}
+
+static void lpg_apply_dtest(struct lpg *lpg)
+{
+	if (!lpg->dtest_line)
+		return;
+
+	regmap_write(lpg->map, lpg->reg + PWM_SEC_ACCESS_REG, 0xa5);
+	regmap_write(lpg->map, lpg->reg + PWM_DTEST_REG(lpg->dtest_line),
+		     lpg->dtest_value);
+}
+
+static void lpg_apply(struct lpg *lpg)
+{
+	lpg_disable_glitch(lpg);
+	lpg_apply_freq(lpg);
+	lpg_apply_pwm_value(lpg);
+	lpg_apply_control(lpg);
+	lpg_apply_sync(lpg);
+	lpg_apply_lut_control(lpg);
+	lpg_enable_glitch(lpg);
+
+	if (lpg->tri_led)
+		qcom_tri_led_set(lpg->tri_led, lpg->enabled);
+}
+
+static void lpg_brightnes_set(struct led_classdev *cdev,
+			      enum led_brightness value)
+{
+	struct lpg *lpg = container_of(cdev, struct lpg, cdev);
+	unsigned int duty_us;
+
+	if (value == LED_OFF) {
+		lpg->enabled = false;
+		lpg->ramp_enabled = false;
+	} else if (lpg->pattern) {
+		lpg_calc_freq(lpg, NSEC_PER_USEC);
+
+		lpg->enabled = true;
+		lpg->ramp_enabled = true;
+	} else {
+		lpg_calc_freq(lpg, NSEC_PER_USEC);
+
+		duty_us = value * lpg->period_us / cdev->max_brightness;
+		lpg_calc_duty(lpg, duty_us);
+		lpg->enabled = true;
+		lpg->ramp_enabled = false;
+	}
+
+	lpg_apply(lpg);
+}
+
+static int lpg_blink_set(struct led_classdev *cdev,
+			 unsigned long *delay_on, unsigned long *delay_off)
+{
+	struct lpg *lpg = container_of(cdev, struct lpg, cdev);
+	unsigned int period_us;
+	unsigned int duty_us;
+
+	if (!*delay_on && !*delay_off) {
+		*delay_on = 500;
+		*delay_off = 500;
+	}
+
+	duty_us = *delay_on * USEC_PER_MSEC;
+	period_us = (*delay_on + *delay_off) * USEC_PER_MSEC;
+
+	lpg_calc_freq(lpg, period_us);
+	lpg_calc_duty(lpg, duty_us);
+
+	lpg->enabled = true;
+	lpg->ramp_enabled = false;
+
+	lpg_apply(lpg);
+
+	return 0;
+}
+
+static enum led_brightness lpg_brightnes_get(struct led_classdev *cdev)
+{
+	struct lpg *lpg = container_of(cdev, struct lpg, cdev);
+	unsigned long max = (1 << lpg->pwm_size) - 1;
+
+	if (!lpg->enabled)
+		return LED_OFF;
+
+	return lpg->pwm_value * cdev->max_brightness / max;
+}
+
+static int lpg_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm,
+			 struct pwm_state *state)
+{
+	struct lpg *lpg = container_of(chip, struct lpg, chip);
+
+	lpg_calc_freq(lpg, state->period / NSEC_PER_USEC);
+	lpg_calc_duty(lpg, state->duty_cycle / NSEC_PER_USEC);
+	lpg->enabled = state->enabled;
+
+	lpg_apply(lpg);
+
+	state->polarity = PWM_POLARITY_NORMAL;
+	state->period = lpg->period_us * NSEC_PER_USEC;
+
+	return 0;
+}
+
+static const struct pwm_ops lpg_pwm_ops = {
+	.apply = lpg_pwm_apply,
+	.owner = THIS_MODULE,
+};
+
+static ssize_t lpg_attr_get(struct device *dev,
+			    struct device_attribute *attr,
+			    char *buf);
+static ssize_t lpg_attr_set(struct device *dev,
+			    struct device_attribute *attr,
+			    const char *buf, size_t count);
+
+static DEVICE_ATTR(ping_pong,	0600, lpg_attr_get, lpg_attr_set);
+static DEVICE_ATTR(oneshot,	0600, lpg_attr_get, lpg_attr_set);
+static DEVICE_ATTR(reverse,	0600, lpg_attr_get, lpg_attr_set);
+static DEVICE_ATTR(pattern,	0600, lpg_attr_get, lpg_attr_set);
+static DEVICE_ATTR(duration,	0600, lpg_attr_get, lpg_attr_set);
+static DEVICE_ATTR(pause_lo,	0600, lpg_attr_get, lpg_attr_set);
+static DEVICE_ATTR(pause_hi,	0600, lpg_attr_get, lpg_attr_set);
+
+static ssize_t lpg_pattern_store(struct lpg *lpg, const char *buf, size_t count)
+{
+	struct qcom_lpg_pattern *new_pattern;
+	unsigned long val;
+	char *sbegin;
+	u16 *pattern;
+	char *elem;
+	char *s;
+	int len = 0;
+	int ret = 0;
+
+	s = sbegin = kstrndup(buf, count, GFP_KERNEL);
+	if (!s)
+		return -ENOMEM;
+
+	pattern = kcalloc(count, sizeof(u16), GFP_KERNEL);
+	if (!pattern) {
+		ret = -ENOMEM;
+		goto out;
+	}
+
+	if (s[0] == '\0' || (s[0] == '\n' && s[1] == '\0')) {
+		qcom_lpg_lut_free(lpg->pattern);
+		lpg->pattern = NULL;
+	} else {
+		while ((elem = strsep(&s, " ,")) != NULL) {
+			ret = kstrtoul(elem, 10, &val);
+			if (ret)
+				goto out;
+
+			pattern[len++] = val;
+		}
+
+		new_pattern = qcom_lpg_lut_store(lpg->lut, pattern, len);
+		if (IS_ERR(new_pattern)) {
+			ret = PTR_ERR(new_pattern);
+			goto out;
+		}
+
+		qcom_lpg_lut_free(lpg->pattern);
+		lpg->pattern = new_pattern;
+	}
+
+out:
+	kfree(pattern);
+	kfree(sbegin);
+	return ret < 0 ? ret : count;
+}
+
+static ssize_t lpg_attr_get(struct device *dev,
+			    struct device_attribute *attr,
+			    char *buf)
+{
+	struct lpg *lpg = dev_get_drvdata(dev);
+
+	if (attr == &dev_attr_ping_pong)
+		return sprintf(buf, "%d\n", lpg->ramp_ping_pong);
+	else if (attr == &dev_attr_oneshot)
+		return sprintf(buf, "%d\n", lpg->ramp_oneshot);
+	else if (attr == &dev_attr_reverse)
+		return sprintf(buf, "%d\n", lpg->ramp_reverse);
+	else if (attr == &dev_attr_duration)
+		return sprintf(buf, "%ld\n", lpg->ramp_duration_ms);
+	else if (attr == &dev_attr_pause_lo)
+		return sprintf(buf, "%ld\n", lpg->ramp_lo_pause_ms);
+	else if (attr == &dev_attr_pause_hi)
+		return sprintf(buf, "%ld\n", lpg->ramp_hi_pause_ms);
+	else if (attr == &dev_attr_pattern)
+		return qcom_lpg_lut_show(lpg->pattern, buf);
+
+	return -EINVAL;
+}
+
+static ssize_t lpg_attr_set(struct device *dev,
+			    struct device_attribute *attr,
+			    const char *buf, size_t count)
+{
+	struct lpg *lpg = dev_get_drvdata(dev);
+	int ret = -EINVAL;
+
+	if (attr == &dev_attr_ping_pong)
+		ret = strtobool(buf, &lpg->ramp_ping_pong);
+	else if (attr == &dev_attr_oneshot)
+		ret = strtobool(buf, &lpg->ramp_oneshot);
+	else if (attr == &dev_attr_reverse)
+		ret = strtobool(buf, &lpg->ramp_reverse);
+	else if (attr == &dev_attr_duration)
+		ret = kstrtoul(buf, 10, &lpg->ramp_duration_ms);
+	else if (attr == &dev_attr_pause_lo)
+		ret = kstrtoul(buf, 10, &lpg->ramp_lo_pause_ms);
+	else if (attr == &dev_attr_pause_hi)
+		ret = kstrtoul(buf, 10, &lpg->ramp_hi_pause_ms);
+	else if (attr == &dev_attr_pattern)
+		ret = lpg_pattern_store(lpg, buf, count);
+
+	if (ret < 0)
+		return -EINVAL;
+
+	lpg_apply(lpg);
+	return count;
+}
+
+static struct attribute *lpg_attributes[] = {
+	&dev_attr_ping_pong.attr,
+	&dev_attr_oneshot.attr,
+	&dev_attr_reverse.attr,
+	&dev_attr_pattern.attr,
+	&dev_attr_duration.attr,
+	&dev_attr_pause_lo.attr,
+	&dev_attr_pause_hi.attr,
+	NULL
+};
+
+static const struct attribute_group lpg_attr_group = {
+	.attrs = lpg_attributes,
+};
+
+static const struct attribute_group *lpg_attr_groups[] = {
+	&lpg_attr_group,
+	NULL
+};
+
+static int lpg_register_pwm(struct lpg *lpg)
+{
+	int ret;
+
+	lpg->chip.base = -1;
+	lpg->chip.dev = lpg->dev;
+	lpg->chip.npwm = 1;
+	lpg->chip.ops = &lpg_pwm_ops;
+
+	ret = pwmchip_add(&lpg->chip);
+	if (ret)
+		dev_err(lpg->dev, "failed to add PWM chip: ret %d\n", ret);
+
+	return ret;
+}
+
+static int lpg_parse_lut(struct lpg *lpg)
+{
+	struct device_node *np = lpg->dev->of_node;
+	u16 *pattern;
+	u32 val;
+	int len;
+
+	lpg->lut = qcom_lpg_lut_get(lpg->dev);
+	if (IS_ERR_OR_NULL(lpg->lut))
+		return PTR_ERR(lpg->lut);
+
+	if (!of_find_property(np, "qcom,pattern", NULL))
+		return 0;
+
+	len = of_property_count_elems_of_size(np, "qcom,pattern", sizeof(u16));
+	if (len < 0)
+		return -EINVAL;
+
+	pattern = kcalloc(len, sizeof(u16), GFP_KERNEL);
+	if (!pattern)
+		return -ENOMEM;
+
+	of_property_read_u16_array(np, "qcom,pattern", pattern, len);
+
+	lpg->pattern = qcom_lpg_lut_store(lpg->lut, pattern, len);
+	kfree(pattern);
+	if (IS_ERR(lpg->pattern))
+		return PTR_ERR(lpg->pattern);
+
+	if (!of_property_read_u32(np, "qcom,pattern-length-ms", &val))
+		lpg->ramp_duration_ms = val;
+	if (!of_property_read_u32(np, "qcom,pattern-pause-lo-ms", &val))
+		lpg->ramp_lo_pause_ms = val;
+	if (!of_property_read_u32(np, "qcom,pattern-pause-hi-ms", &val))
+		lpg->ramp_hi_pause_ms = val;
+
+	lpg->ramp_ping_pong = of_property_read_bool(np, "qcom,pattern-ping-pong");
+	lpg->ramp_oneshot = of_property_read_bool(np, "qcom,pattern-oneshot");
+	lpg->ramp_reverse = of_property_read_bool(np, "qcom,pattern-reverse");
+
+	return 0;
+}
+
+static int lpg_register_led(struct lpg *lpg)
+{
+	struct device_node *np = lpg->dev->of_node;
+	const char *state;
+	int ret;
+
+	ret = lpg_parse_lut(lpg);
+	if (ret)
+		return ret;
+
+	/* Use label else node name */
+	lpg->cdev.name = of_get_property(np, "label", NULL) ? : np->name;
+	lpg->cdev.default_trigger = of_get_property(np, "linux,default-trigger", NULL);
+	lpg->cdev.brightness_set = lpg_brightnes_set;
+	lpg->cdev.brightness_get = lpg_brightnes_get;
+	lpg->cdev.blink_set = lpg_blink_set;
+	lpg->cdev.max_brightness = 255;
+	lpg->cdev.groups = lpg_attr_groups;
+
+	if (!of_property_read_string(np, "default-state", &state) &&
+	    !strcmp(state, "on"))
+		lpg->cdev.brightness = LED_FULL;
+	else
+		lpg->cdev.brightness = LED_OFF;
+
+	lpg_brightnes_set(&lpg->cdev, lpg->cdev.brightness);
+
+	ret = devm_led_classdev_register(lpg->dev, &lpg->cdev);
+	if (ret)
+		dev_err(lpg->dev, "unable to register \"%s\"\n", lpg->cdev.name);
+
+	return ret;
+}
+
+static int lpg_probe(struct platform_device *pdev)
+{
+	struct device_node *np = pdev->dev.of_node;
+	struct lpg *lpg;
+	u32 dtest[2];
+	int ret;
+
+	lpg = devm_kzalloc(&pdev->dev, sizeof(*lpg), GFP_KERNEL);
+	if (!lpg)
+		return -ENOMEM;
+
+	lpg->dev = &pdev->dev;
+
+	lpg->map = dev_get_regmap(pdev->dev.parent, NULL);
+	if (!lpg->map) {
+		dev_err(&pdev->dev, "parent regmap unavailable\n");
+		return -ENXIO;
+	}
+
+	ret = of_property_read_u32(np, "reg", &lpg->reg);
+	if (ret) {
+		dev_err(&pdev->dev, "no register offset specified\n");
+		return -EINVAL;
+	}
+
+	if (!of_find_property(np, "#pwm-cells", NULL))
+		lpg->is_lpg = true;
+
+	lpg->tri_led = qcom_tri_led_get(&pdev->dev);
+	if (IS_ERR(lpg->tri_led))
+		return PTR_ERR(lpg->tri_led);
+
+	ret = of_property_read_u32_array(np, "qcom,dtest", dtest, 2);
+	if (!ret) {
+		lpg->dtest_line = dtest[0];
+		lpg->dtest_value = dtest[1];
+	}
+
+	if (lpg->is_lpg) {
+		ret = lpg_register_led(lpg);
+		if (ret)
+			return ret;
+	} else {
+		ret = lpg_register_pwm(lpg);
+		if (ret)
+			return ret;
+	}
+
+	lpg_apply_dtest(lpg);
+
+	platform_set_drvdata(pdev, lpg);
+
+	return 0;
+}
+
+static int lpg_remove(struct platform_device *pdev)
+{
+	struct lpg *lpg = platform_get_drvdata(pdev);
+
+	if (!lpg->is_lpg)
+		pwmchip_remove(&lpg->chip);
+
+	qcom_lpg_lut_free(lpg->pattern);
+
+	return 0;
+}
+
+static const struct of_device_id lpg_of_table[] = {
+	{ .compatible = "qcom,spmi-lpg" },
+	{},
+};
+MODULE_DEVICE_TABLE(of, lpg_of_table);
+
+static struct platform_driver lpg_driver = {
+	.probe = lpg_probe,
+	.remove = lpg_remove,
+	.driver = {
+		.name = "qcom-spmi-lpg",
+		.of_match_table = lpg_of_table,
+	},
+};
+module_platform_driver(lpg_driver);
+
+MODULE_DESCRIPTION("Qualcomm TRI LED driver");
+MODULE_LICENSE("GPL v2");
diff --git a/drivers/leds/leds-qcom-lpg.h b/drivers/leds/leds-qcom-lpg.h
new file mode 100644
index 000000000000..f2abb106133d
--- /dev/null
+++ b/drivers/leds/leds-qcom-lpg.h
@@ -0,0 +1,30 @@ 
+#ifndef __LEDS_QCOM_LPG_H__
+#define __LEDS_QCOM_LPG_H__
+
+struct qcom_tri_led;
+struct qcom_lpg_lut;
+
+/*
+ * qcom_lpg_pattern - object tracking allocated LUT entries
+ * @lut:	reference to the client & LUT device context
+ * @lo_idx:	index of first entry in the LUT used by pattern
+ * @hi_idx:	index of the last entry in the LUT used by pattern
+ */
+struct qcom_lpg_pattern {
+	struct qcom_lpg_lut *lut;
+
+	unsigned int lo_idx;
+	unsigned int hi_idx;
+};
+
+struct qcom_tri_led *qcom_tri_led_get(struct device *dev);
+int qcom_tri_led_set(struct qcom_tri_led *tri, bool enabled);
+
+struct qcom_lpg_lut *qcom_lpg_lut_get(struct device *dev);
+struct qcom_lpg_pattern *qcom_lpg_lut_store(struct qcom_lpg_lut *lut,
+					    const u16 *values, size_t len);
+ssize_t qcom_lpg_lut_show(struct qcom_lpg_pattern *pattern, char *buf);
+void qcom_lpg_lut_free(struct qcom_lpg_pattern *pattern);
+int qcom_lpg_lut_sync(struct qcom_lpg_lut *lut);
+
+#endif
diff --git a/drivers/leds/leds-qcom-triled.c b/drivers/leds/leds-qcom-triled.c
new file mode 100644
index 000000000000..ce3de613be5b
--- /dev/null
+++ b/drivers/leds/leds-qcom-triled.c
@@ -0,0 +1,193 @@ 
+/* Copyright (c) 2017 Linaro Ltd
+ *
+ * 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.
+ */
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_device.h>
+#include <linux/platform_device.h>
+#include <linux/pm.h>
+#include <linux/pwm.h>
+#include <linux/regmap.h>
+#include <linux/slab.h>
+
+#include "leds-qcom-lpg.h"
+
+#define TRI_LED_SRC_SEL	0x45
+#define TRI_LED_EN_CTL	0x46
+#define TRI_LED_ATC_CTL	0x47
+
+#define TRI_LED_COUNT	3
+
+static struct platform_driver tri_led_driver;
+
+/*
+ * tri_led_dev - TRILED device context
+ * @dev:	struct device reference
+ * @map:	regmap for register access
+ * @reg:	base address of TRILED block
+ */
+struct tri_led_dev {
+	struct device *dev;
+	struct regmap *map;
+
+	u32 reg;
+};
+
+/*
+ * qcom_tri_led - representation of a single color
+ * @tdev:	TRILED device reference
+ * @color:	color of this object 0 <= color < 3
+ */
+struct qcom_tri_led {
+	struct tri_led_dev *tdev;
+	u8 color;
+};
+
+static void tri_led_release(struct device *dev, void *res)
+{
+	struct qcom_tri_led *tri = res;
+	struct tri_led_dev *tdev = tri->tdev;
+
+	put_device(tdev->dev);
+}
+
+/**
+ * qcom_tri_led_get() - acquire a reference to a single color of the TRILED
+ * @dev:	struct device of the client
+ *
+ * Returned devres allocated TRILED color object, NULL if client lacks TRILED
+ * reference or ERR_PTR on failure.
+ */
+struct qcom_tri_led *qcom_tri_led_get(struct device *dev)
+{
+	struct platform_device *pdev;
+	struct of_phandle_args args;
+	struct qcom_tri_led *tri;
+	int ret;
+
+	ret = of_parse_phandle_with_fixed_args(dev->of_node,
+					       "qcom,tri-led", 1, 0, &args);
+	if (ret)
+		return NULL;
+
+	pdev = of_find_device_by_node(args.np);
+	of_node_put(args.np);
+	if (!pdev || !pdev->dev.driver)
+		return ERR_PTR(-EPROBE_DEFER);
+
+	if (pdev->dev.driver != &tri_led_driver.driver) {
+		dev_err(dev, "referenced node is not a tri-led\n");
+		return ERR_PTR(-EINVAL);
+	}
+
+	if (args.args[0] >= TRI_LED_COUNT) {
+		dev_err(dev, "invalid color\n");
+		return ERR_PTR(-EINVAL);
+	}
+
+	tri = devres_alloc(tri_led_release, sizeof(*tri), GFP_KERNEL);
+	if (!tri)
+		return ERR_PTR(-ENOMEM);
+
+	tri->tdev = platform_get_drvdata(pdev);
+	tri->color = args.args[0];
+
+	devres_add(dev, tri);
+
+	return tri;
+}
+EXPORT_SYMBOL_GPL(qcom_tri_led_get);
+
+/**
+ * qcom_tri_led_set() - enable/disable a TRILED output
+ * @tri:	TRILED color object reference
+ * @enable:	new state of the output
+ *
+ * Returns 0 on success, negative errno on failure.
+ */
+int qcom_tri_led_set(struct qcom_tri_led *tri, bool enable)
+{
+	struct tri_led_dev *tdev = tri->tdev;
+	unsigned int mask;
+	unsigned int val;
+
+	/* red, green, blue are mapped to bits 7, 6 and 5 respectively */
+	mask = BIT(7 - tri->color);
+	val = enable ? mask : 0;
+
+	return regmap_update_bits(tdev->map, tdev->reg + TRI_LED_EN_CTL,
+				  mask, val);
+}
+EXPORT_SYMBOL_GPL(qcom_tri_led_set);
+
+static int tri_led_probe(struct platform_device *pdev)
+{
+	struct device_node *np = pdev->dev.of_node;
+	struct tri_led_dev *tri;
+	u32 src_sel;
+	int ret;
+
+	tri = devm_kzalloc(&pdev->dev, sizeof(*tri), GFP_KERNEL);
+	if (!tri)
+		return -ENOMEM;
+
+	tri->dev = &pdev->dev;
+
+	tri->map = dev_get_regmap(pdev->dev.parent, NULL);
+	if (!tri->map) {
+		dev_err(&pdev->dev, "parent regmap unavailable\n");
+		return -ENXIO;
+	}
+
+	ret = of_property_read_u32(np, "reg", &tri->reg);
+	if (ret) {
+		dev_err(&pdev->dev, "no register offset specified\n");
+		return -EINVAL;
+	}
+
+	ret = of_property_read_u32(np, "qcom,power-source", &src_sel);
+	if (ret || src_sel == 2 || src_sel > 3) {
+		dev_err(&pdev->dev, "invalid power source\n");
+		return -EINVAL;
+	}
+
+	/* Disable automatic trickle charge LED */
+	regmap_write(tri->map, tri->reg + TRI_LED_ATC_CTL, 0);
+
+	/* Configure power source */
+	regmap_write(tri->map, tri->reg + TRI_LED_SRC_SEL, src_sel);
+
+	/* Default all outputs to off */
+	regmap_write(tri->map, tri->reg + TRI_LED_EN_CTL, 0);
+
+	platform_set_drvdata(pdev, tri);
+
+	return 0;
+}
+
+static const struct of_device_id tri_led_of_table[] = {
+	{ .compatible = "qcom,spmi-tri-led" },
+	{},
+};
+MODULE_DEVICE_TABLE(of, tri_led_of_table);
+
+static struct platform_driver tri_led_driver = {
+	.probe = tri_led_probe,
+	.driver = {
+		.name = "qcom_tri_led",
+		.of_match_table = tri_led_of_table,
+	},
+};
+module_platform_driver(tri_led_driver);
+
+MODULE_DESCRIPTION("Qualcomm TRI LED driver");
+MODULE_LICENSE("GPL v2");