Linux LED subsystem development
 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox