All of lore.kernel.org
 help / color / mirror / Atom feed
From: Alistair Bell <dev@alistairbell.org>
To: linux-leds@vger.kernel.org
Cc: lee@kernel.org, pavel@kernel.org, Alistair Bell <dev@alistairbell.org>
Subject: [PATCH 1/2] leds: Texas Instruments LP5816 driver
Date: Sun, 12 Jul 2026 19:43:17 +0100	[thread overview]
Message-ID: <20260712184318.78852-2-dev@alistairbell.org> (raw)
In-Reply-To: <20260712184318.78852-1-dev@alistairbell.org>

Add support for Texas Instruments LP5816 4-channel I2C device,
the driver provides:

- Independent 4-channel control via the multicolor sysfs class
- Configurable fade effects, duration, fade profile and maximum
  operating current via sysfs

Signed-off-by: Alistair Bell <dev@alistairbell.org>
---
 MAINTAINERS                |   8 +
 drivers/leds/Kconfig       |  12 +
 drivers/leds/Makefile      |   1 +
 drivers/leds/leds-lp5816.c | 443 +++++++++++++++++++++++++++++++++++++
 4 files changed, 464 insertions(+)
 create mode 100644 drivers/leds/leds-lp5816.c

diff --git a/MAINTAINERS b/MAINTAINERS
index f37a81950..b6a8c812a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -26761,6 +26761,14 @@ F:	drivers/leds/rgb/Makefile
 F:	drivers/leds/rgb/leds-lp5812.c
 F:	drivers/leds/rgb/leds-lp5812.h
 
+TEXAS INSTRUMENTS' LP5816 RGBW LED DRIVER
+M:	Alistair Bell <dev@alistairbell.org>
+L:	linux-leds@vger.kernel.org
+S:	Maintained
+F:	drivers/leds/Kconfig
+F:	drivers/leds/Makefile
+F:	drivers/leds/leds-lp5816.c
+
 TEXAS INSTRUMENTS' LB8864 LED BACKLIGHT DRIVER
 M:	Alexander Sverdlin <alexander.sverdlin@siemens.com>
 L:	linux-leds@vger.kernel.org
diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig
index f4a0a3c8c..ce3776adf 100644
--- a/drivers/leds/Kconfig
+++ b/drivers/leds/Kconfig
@@ -541,6 +541,18 @@ config LEDS_LP8864
 	  To compile this driver as a module, choose M here: the
 	  module will be called leds-lp8864.
 
+config LEDS_LP5816
+    tristate "LED driver for LP5816 chip"
+    depends on I2C
+    depends on LEDS_CLASS && LEDS_CLASS_MULTICOLOR
+	select REGMAP_I2C
+    help
+      Say Y to enable support for the Texas Instruments LP5816
+      RGBW LED connected via I2C.
+
+      To compile this driver as a module, choose M here:
+      the module will be called lp5816.
+
 config LEDS_CLEVO_MAIL
 	tristate "Mail LED on Clevo notebook"
 	depends on LEDS_CLASS && BROKEN
diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile
index 7db376891..5ba7de099 100644
--- a/drivers/leds/Makefile
+++ b/drivers/leds/Makefile
@@ -56,6 +56,7 @@ obj-$(CONFIG_LEDS_LP5523)		+= leds-lp5523.o
 obj-$(CONFIG_LEDS_LP5562)		+= leds-lp5562.o
 obj-$(CONFIG_LEDS_LP5569)		+= leds-lp5569.o
 obj-$(CONFIG_LEDS_LP55XX_COMMON)	+= leds-lp55xx-common.o
+obj-$(CONFIG_LEDS_LP5816)   	+= leds-lp5816.o
 obj-$(CONFIG_LEDS_LP8501)		+= leds-lp8501.o
 obj-$(CONFIG_LEDS_LP8788)		+= leds-lp8788.o
 obj-$(CONFIG_LEDS_LP8860)		+= leds-lp8860.o
diff --git a/drivers/leds/leds-lp5816.c b/drivers/leds/leds-lp5816.c
new file mode 100644
index 000000000..584c348a7
--- /dev/null
+++ b/drivers/leds/leds-lp5816.c
@@ -0,0 +1,443 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Texas Instruments LP5816 4-Channel I2C Interface RGBW LED Driver
+ * Copyright (C) 2026 Alistair Bell <dev@alistairbell.org>
+ */
+
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/leds.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/minmax.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/regmap.h>
+#include <linux/string.h>
+
+#define REG_CHIP_EN 0x0
+#define CHIP_EN_ENABLE 0x1
+
+#define REG_DEV_CONFIG0 0x1
+#define DEV_CONFIG0_25_5_MA 0x0
+#define DEV_CONFIG0_51_MA 0x1
+
+#define REG_DEV_CONFIG1 0x2
+#define REG_DEV_CONFIG2 0x3
+#define REG_DEV_CONFIG3 0x4
+
+#define REG_RESET_CMD 0xe
+#define RESET_CMD 0xcc
+
+#define REG_UPDATE_CMD 0xf
+#define UPDATE_CMD 0x55
+
+#define REG_OUT0_DC 0x14
+#define REG_OUT1_DC 0x15
+#define REG_OUT2_DC 0x16
+#define REG_OUT3_DC 0x17
+#define REG_OUT0_MANUAL_PWM 0x18
+
+struct lp5816 {
+	struct led_classdev_mc mcdev;
+	struct regmap *regmap;
+	/* For register read/writes. */
+	struct mutex mtx;
+	struct device *dev;
+	struct mc_subled subleds[4];
+};
+
+static const struct regmap_config lp5816_regmap_config = {
+	.max_register = 0x40,
+	.reg_bits = 8,
+	.val_bits = 8,
+};
+
+static ssize_t max_current_show(struct device *dev,
+				struct device_attribute *attr,
+								char *buf);
+static ssize_t max_current_store(struct device *dev,
+				 struct device_attribute *attr,
+								const char *buf, size_t count);
+static DEVICE_ATTR_RW(max_current);
+
+static ssize_t fade_show(struct device *dev, struct device_attribute *attr,
+			 char *buf);
+static ssize_t fade_store(struct device *dev, struct device_attribute *attr,
+			  const char *buf, size_t count);
+static DEVICE_ATTR_RW(fade);
+
+static ssize_t fade_mode_show(struct device *dev, struct device_attribute *attr,
+			      char *buf);
+static ssize_t fade_mode_store(struct device *dev,
+			       struct device_attribute *attr, const char *buf,
+							size_t count);
+static DEVICE_ATTR_RW(fade_mode);
+
+static struct attribute *lp5816_attrs[] = {
+	&dev_attr_max_current.attr,
+	&dev_attr_fade.attr,
+	&dev_attr_fade_mode.attr,
+	NULL,
+};
+
+static struct attribute_group lp5816_group = {
+	.attrs = lp5816_attrs,
+};
+
+static int lp5816_multi_lock_write(struct lp5816 *chip, const struct reg_sequence seq[], int count)
+{
+	int res;
+
+	mutex_lock(&chip->mtx);
+	res = regmap_multi_reg_write(chip->regmap, seq, count);
+	mutex_unlock(&chip->mtx);
+	return res;
+}
+
+static int lp5816_split_string(const char *in, int out[], const int lim)
+{
+	char *tk, *step, *local;
+	int n, res;
+
+	local = kstrdup(in, GFP_KERNEL);
+	if (!local)
+		return -ENOMEM;
+	step = strim(local);
+
+	n = 0;
+	while ((tk = strsep(&step, " ")) != NULL && n < lim) {
+		if (*tk == '\0')
+			continue;
+
+		res = kstrtoint(tk, 0, &out[n]);
+		if (res < 0) {
+			kfree(local);
+			return res;
+		}
+		n++;
+	}
+
+	kfree(local);
+	return n;
+}
+
+static int lp5816_startup(struct lp5816 *chip)
+{
+	int res;
+	const struct reg_sequence sequence[] = {
+		/* Ensure that all registers are the default values. */
+		{ .reg = REG_RESET_CMD, .def = RESET_CMD },
+		/* Moves from standby to normal operating mode. */
+		{ .reg = REG_CHIP_EN, .def = CHIP_EN_ENABLE },
+		/* Sets maximum operating max_current to 51mA. */
+		{ .reg = REG_DEV_CONFIG0, .def = DEV_CONFIG0_51_MA },
+		/* Enables all four outputs. */
+		{ .reg = REG_DEV_CONFIG1, .def = 0xf },
+		/* Sets the red current to 100% of Imax (controlled by CONFIG0). */
+		{ .reg = REG_OUT0_DC, .def = 0xff },
+		/* Sets the green, .def = blue and white to 80% of Imax. */
+		{ .reg = REG_OUT1_DC, .def = 0xcc },
+		{ .reg = REG_OUT2_DC, .def = 0xcc },
+		{ .reg = REG_OUT3_DC, .def = 0xcc },
+		/* Posts the update cmd. */
+		{ .reg = REG_UPDATE_CMD, .def = UPDATE_CMD }
+	};
+
+	res = regmap_multi_reg_write(chip->regmap, sequence, ARRAY_SIZE(sequence));
+	if (res < 0)
+		return dev_err_probe(chip->dev, res, "failed to write startup sequence values\n");
+	return 0;
+}
+
+static ssize_t max_current_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct led_classdev *cdev;
+	struct lp5816 *chip;
+	unsigned int val;
+	int res;
+
+	cdev = dev_get_drvdata(dev);
+	chip = container_of(cdev, struct lp5816, mcdev.led_cdev);
+
+	mutex_lock(&chip->mtx);
+	res = regmap_read(chip->regmap, REG_DEV_CONFIG0, &val);
+	mutex_unlock(&chip->mtx);
+
+	if (res < 0)
+		return res;
+	if (val > 1)
+		return -EPROTO;
+
+	return sysfs_emit(buf, "%d\n", val);
+}
+
+static ssize_t max_current_store(struct device *dev,
+				 struct device_attribute *attr,
+								const char *buf, size_t count)
+{
+	struct led_classdev *cdev;
+	struct lp5816 *chip;
+	int res, val;
+
+	cdev = dev_get_drvdata(dev);
+	chip = container_of(cdev, struct lp5816, mcdev.led_cdev);
+
+	res = kstrtoint(buf, 0, &val);
+	if (res < 0)
+		return res;
+	if (val < 0 || val > 1)
+		return -EINVAL;
+
+	res = lp5816_multi_lock_write(chip, (const struct reg_sequence[]) {
+		{ .reg = REG_DEV_CONFIG0, .def = val },
+		{ .reg = REG_UPDATE_CMD, .def = UPDATE_CMD } }, 2);
+	return (res < 0) ? res : count;
+}
+
+static ssize_t fade_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct led_classdev *cdev;
+	struct lp5816 *chip;
+	unsigned int val;
+	int res;
+
+	cdev = dev_get_drvdata(dev);
+	chip = container_of(cdev, struct lp5816, mcdev.led_cdev);
+
+	mutex_lock(&chip->mtx);
+	res = regmap_read(chip->regmap, REG_DEV_CONFIG2, &val);
+	mutex_unlock(&chip->mtx);
+
+	if (res < 0)
+		return res;
+
+	return sysfs_emit(buf, "%d %d %d %d %d\n",
+			(val >> 0) & 0x1,
+			(val >> 1) & 0x1,
+			(val >> 2) & 0x1,
+			(val >> 3) & 0x1,
+			(val >> 4) & 0xf);
+}
+
+static ssize_t fade_store(struct device *dev, struct device_attribute *attr,
+			  const char *buf, size_t count)
+{
+	struct led_classdev *cdev;
+	struct lp5816 *chip;
+	int vals[5], i, val, res;
+
+	cdev = dev_get_drvdata(dev);
+	chip = container_of(cdev, struct lp5816, mcdev.led_cdev);
+
+	res = lp5816_split_string(buf, vals, ARRAY_SIZE(vals));
+	if (res != ARRAY_SIZE(vals))
+		return (res < 0) ? res : -EINVAL;
+
+	for (i = 0, val = 0; i < ARRAY_SIZE(vals); i++) {
+		if (i < 4) {
+			if (vals[i] < 0 || vals[i] > 1)
+				return -EINVAL;
+			val |= vals[i] << i;
+		} else {
+			if (vals[i] < 0 || vals[i] > 15)
+				return -EINVAL;
+			val |= (vals[i] & 0xf) << 4;
+		}
+	}
+
+	res = lp5816_multi_lock_write(chip, (const struct reg_sequence[]) {
+			{ .reg = REG_DEV_CONFIG2, .def = val },
+			{ .reg = REG_UPDATE_CMD, .def = UPDATE_CMD }}, 2);
+	return (res < 0) ? res : count;
+}
+
+static ssize_t fade_mode_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	struct led_classdev *cdev;
+	struct lp5816 *chip;
+	int res, val;
+
+	cdev = dev_get_drvdata(dev);
+	chip = container_of(cdev, struct lp5816, mcdev.led_cdev);
+
+	mutex_lock(&chip->mtx);
+	res = regmap_read(chip->regmap, REG_DEV_CONFIG3, &val);
+	mutex_unlock(&chip->mtx);
+
+	if (res < 0)
+		return res;
+
+	val >>= 4;
+	return sysfs_emit(buf, "%d %d %d %d\n",
+			val & 0x1,
+			(val >> 1) & 0x1,
+			(val >> 2) & 0x1,
+			(val >> 3) & 0x1);
+}
+
+static ssize_t fade_mode_store(struct device *dev,
+			       struct device_attribute *attr, const char *buf,
+							size_t count)
+{
+	struct led_classdev *cdev;
+	struct lp5816 *chip;
+	int vals[4], res, i;
+	u8 val;
+
+	cdev = dev_get_drvdata(dev);
+	chip = container_of(cdev, struct lp5816, mcdev.led_cdev);
+
+	res = lp5816_split_string(buf, vals, ARRAY_SIZE(vals));
+	if (res != ARRAY_SIZE(vals))
+		return (res < 0) ? res : -EINVAL;
+
+	/*
+	 * Requires 4 values of either 0 or 1,
+	 * which disable/enable exponential fade mode per LED.
+	 */
+	for (i = 0, val = 0; i < ARRAY_SIZE(vals); i++) {
+		if (vals[i] < 0 || vals[i] > 1)
+			return -EINVAL;
+		val |= (vals[i] << i);
+	}
+
+	res = lp5816_multi_lock_write(chip, (const struct reg_sequence[]) {
+		{ .reg = REG_DEV_CONFIG3, .def = val << 4 },
+		{ .reg = REG_UPDATE_CMD, .def = UPDATE_CMD }}, 2);
+	return (res < 0) ? res : count;
+}
+
+static enum led_brightness lp5816_brightness_get(struct led_classdev *cdev)
+{
+	struct mc_subled *leds;
+	struct lp5816 *chip;
+	int intensity;
+
+	chip = container_of(cdev, struct lp5816, mcdev.led_cdev);
+
+	mutex_lock(&chip->mtx);
+	leds = lcdev_to_mccdev(cdev)->subled_info;
+	intensity = MAX(leds[0].intensity, leds[1].intensity);
+	intensity = MAX(intensity, MAX(leds[2].intensity, leds[3].intensity));
+	mutex_unlock(&chip->mtx);
+	return intensity & 0xff;
+}
+
+static int lp5816_brightness_set(struct led_classdev *cdev, enum led_brightness br)
+{
+	struct lp5816 *chip;
+	struct led_classdev_mc *mc;
+	u8 vals[4];
+
+	mc = lcdev_to_mccdev(cdev);
+	chip = container_of(cdev, struct lp5816, mcdev.led_cdev);
+
+	vals[0] = mc->subled_info[0].intensity & 0xff;
+	vals[1] = mc->subled_info[1].intensity & 0xff;
+	vals[2] = mc->subled_info[2].intensity & 0xff;
+	vals[3] = mc->subled_info[3].intensity & 0xff;
+
+	chip = container_of(cdev, struct lp5816, mcdev.led_cdev);
+
+	mutex_lock(&chip->mtx);
+	regmap_bulk_write(chip->regmap, REG_OUT0_MANUAL_PWM, vals, ARRAY_SIZE(vals));
+	mutex_unlock(&chip->mtx);
+	return 0;
+}
+
+static int lp5816_probe(struct i2c_client *client)
+{
+	struct lp5816 *chip;
+	char *name;
+	int res;
+
+	chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL);
+	if (!chip)
+		return dev_err_probe(&client->dev, -ENOMEM,
+			"failed to allocate lp5816 internal structure\n");
+
+	i2c_set_clientdata(client, chip);
+	mutex_init(&chip->mtx);
+
+	(*chip) = (struct lp5816) {
+		.dev = &client->dev,
+		.regmap = devm_regmap_init_i2c(client, &lp5816_regmap_config),
+		.subleds = {
+			{ .color_index = LED_COLOR_ID_RED, .intensity = 0 },
+			{ .color_index = LED_COLOR_ID_GREEN, .intensity = 0 },
+			{ .color_index = LED_COLOR_ID_BLUE, .intensity = 0 },
+			{ .color_index = LED_COLOR_ID_WHITE, .intensity = 0 },
+		},
+	};
+
+	if (IS_ERR(chip->regmap))
+		return dev_err_probe(&client->dev, PTR_ERR(chip->regmap),
+				"failed to allocate register map\n");
+
+	name = devm_kasprintf(&client->dev, GFP_KERNEL, "%s:rgbw", dev_name(&client->dev));
+	if (!name)
+		return dev_err_probe(&client->dev, -ENOMEM,
+				"failed to allocate led-classdev name\n");
+
+	chip->mcdev = (struct led_classdev_mc){
+		.led_cdev = (struct led_classdev){
+			.brightness_set_blocking = lp5816_brightness_set,
+			.brightness_get = lp5816_brightness_get,
+			.name = name,
+		},
+		.num_colors = ARRAY_SIZE(chip->subleds),
+		.subled_info = chip->subleds,
+	};
+
+	res = devm_led_classdev_multicolor_register(&client->dev, &chip->mcdev);
+	if (res < 0)
+		return dev_err_probe(&client->dev, res, "failed to register classdev multicolor\n");
+
+	res = devm_device_add_group(chip->mcdev.led_cdev.dev, &lp5816_group);
+	if (res < 0)
+		return dev_err_probe(&client->dev, res, "failed to add led device group\n");
+
+	res = lp5816_startup(chip);
+	if (res < 0)
+		return dev_err_probe(&client->dev, res,
+				"failed to execute chip startup procedure\n");
+	return 0;
+}
+
+static void lp5816_remove(struct i2c_client *client)
+{
+	struct lp5816 *chip;
+
+	chip = i2c_get_clientdata(client);
+	if (!chip)
+		return;
+
+	mutex_lock(&chip->mtx);
+	regmap_multi_reg_write(chip->regmap, (const struct reg_sequence[]) {
+		{ .reg = REG_RESET_CMD, .def = RESET_CMD },
+		{ .reg = REG_UPDATE_CMD, .def = UPDATE_CMD }}, 2);
+	mutex_unlock(&chip->mtx);
+}
+
+static const struct of_device_id lp5816_of_match[] = {
+	{ .compatible = "ti,lp5816" },
+	{ }
+};
+
+MODULE_DEVICE_TABLE(of, lp5816_of_match);
+
+static struct i2c_driver lp5816_driver = {
+	.driver = {
+		.name = "leds-lp5816",
+		.of_match_table = lp5816_of_match,
+	},
+	.probe = lp5816_probe,
+	.remove = lp5816_remove,
+};
+
+module_i2c_driver(lp5816_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Alistair Bell <dev@alistairbell.org>");
+MODULE_DESCRIPTION("TI LP5816 LED driver");
-- 
2.54.0


  reply	other threads:[~2026-07-12 18:50 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-12 18:43 [PATCH 0/2] leds: Introduce Texas Instruments LP5816 LED driver Alistair Bell
2026-07-12 18:43 ` Alistair Bell [this message]
2026-07-12 18:59   ` [PATCH 1/2] leds: Texas Instruments LP5816 driver sashiko-bot
2026-07-12 22:16   ` Uwe Kleine-König
2026-07-13  6:05   ` Krzysztof Kozlowski
2026-07-12 18:43 ` [PATCH 2/2] dt-bindings: leds: Add binding for ti,lp5816 Alistair Bell
2026-07-12 18:59   ` sashiko-bot
2026-07-13  6:01   ` Krzysztof Kozlowski

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260712184318.78852-2-dev@alistairbell.org \
    --to=dev@alistairbell.org \
    --cc=lee@kernel.org \
    --cc=linux-leds@vger.kernel.org \
    --cc=pavel@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.