Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]
Groups > linux.kernel > #1607212 > unrolled thread
| Started by | Bjorn Andersson <bjorn.andersson@linaro.org> |
|---|---|
| First post | 2017-03-23 08:50 +0100 |
| Last post | 2017-03-27 07:00 +0200 |
| Articles | 3 — 2 participants |
Back to article view | Back to linux.kernel
[PATCH 1/2] leds: Add driver for Qualcomm LPG Bjorn Andersson <bjorn.andersson@linaro.org> - 2017-03-23 08:50 +0100
Re: [PATCH 1/2] leds: Add driver for Qualcomm LPG Pavel Machek <pavel@ucw.cz> - 2017-03-23 21:40 +0100
Re: [PATCH 1/2] leds: Add driver for Qualcomm LPG Bjorn Andersson <bjorn.andersson@linaro.org> - 2017-03-27 07:00 +0200
| From | Bjorn Andersson <bjorn.andersson@linaro.org> |
|---|---|
| Date | 2017-03-23 08:50 +0100 |
| Subject | [PATCH 1/2] leds: Add driver for Qualcomm LPG |
| Message-ID | <to5vK-7s8-47@gated-at.bofh.it> |
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
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");
--
2.12.0
[toc] | [next] | [standalone]
| From | Pavel Machek <pavel@ucw.cz> |
|---|---|
| Date | 2017-03-23 21:40 +0100 |
| Message-ID | <tohGy-7w7-9@gated-at.bofh.it> |
| In reply to | #1607212 |
[Multipart message — attachments visible in raw view] — view raw
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 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
...
Which probably means finding subset that makes sense for everyone.
Hmm. What is difference between "ping_pong" and "reverse"? And do we
really want it? That seems little .. too specialized.
How are different channels on RGB LED synchronized?
> +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.
We normally do "space separated" in sysfs.
Can your engine do "smooth transitions"? For example if you want to
slowly turn on the LED on, can you do something more clever than
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
...
496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509,
510, 511
? What is the maximum length of the pattern?
Could we do patterns in form of "delay brightness delay brightness"
.... ?
> +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;
> +}
Does this return something reasonable when pattern is running?
Pavel
--
(english) http://www.livejournal.com/~pavelmachek
(cesky, pictures) http://atrey.karlin.mff.cuni.cz/~pavel/picture/horses/blog.html
[toc] | [prev] | [next] | [standalone]
| From | Bjorn Andersson <bjorn.andersson@linaro.org> |
|---|---|
| Date | 2017-03-27 07:00 +0200 |
| Message-ID | <tpuV3-2w9-1@gated-at.bofh.it> |
| In reply to | #1607882 |
On Thu 23 Mar 13:37 PDT 2017, 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
>
I have not worked with the LP55xx chips before, they look quite capable!
> 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
> ...
>
> Which probably means finding subset that makes sense for everyone.
>
While I share your concern of userspace differences I'm not sure how to
expose the advanced features of the LP55xx series and the LPG's limited
pattern-controlled PWM.
> Hmm. What is difference between "ping_pong" and "reverse"? And do we
> really want it? That seems little .. too specialized.
>
Writing the appropriate bit in RAMP_CONTROL_REG of the LUT block
qcom_lpg_lut_sync() resets the ramp-walker; ping-pong causes the
ramp-walker to make one round-trip run over the pattern, while reverse
means that the ramp-walker should start from the hi-index.
I.e. with the pattern [1,2,3] we get:
ping-pong: [1,2,3,2,1]
reverse: [3,2,1]
ping-pong and reverse: [3,2,1,2,3]
> How are different channels on RGB LED synchronized?
>
You can reset multiple ramp-generators simultaneously, which would cause
the channels to be synchronized. I have not implemented this though.
> > +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.
>
> We normally do "space separated" in sysfs.
>
Okay, I'm fine with that.
> Can your engine do "smooth transitions"? For example if you want to
> slowly turn on the LED on, can you do something more clever than
>
> 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
> 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
> ...
> 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509,
> 510, 511
>
There's nothing beyond "run the pattern", so this is exactly what you
would have to do if you need a perfectly smooth transition.
> ? What is the maximum length of the pattern?
>
It differs between the various PMICs implementing this. I've seen cases
of 24 slots and 64 slots.
> Could we do patterns in form of "delay brightness delay brightness"
> .... ?
>
The ramp-walker is configured to tick with a fixed clock (in
milliseconds) and the PWM will be configured with the duty-cycle of the
current element of the ramp.
So you can do this, given that all your "delay" is the same fixed delay,
which is max 511 milliseconds.
> > +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;
> > +}
>
> Does this return something reasonable when pattern is running?
>
No, I'll fix that. I treat brightness as a boolean if a pattern is
provided, but I'm concerned about modifying max_brightness to reflect
this.
As you can see from these answers, the hardware is quite limited in
comparison to the LP55xx series. It would make some sense to feed e.g. a
mathematical formula to the kernel and have the driver map that to
patterns for the LPG or program code in the case of LP55xx.
But this would add quite a bit of complexity and with hardware as
limited as the 24-slot LPG we likely need that direct control of the
patterns.
Regards,
Bjorn
[toc] | [prev] | [standalone]
Back to top | Article view | linux.kernel
csiph-web