[PATCH 1/2] leds: tlc5928: Driver for the TI 16 Channel spi LED driver

From: Corentin Guillevic
Date: Wed Mar 26 2025 - 11:37:38 EST


The TLC59928 is an SPI-connected bus controlled 16-channel LED driver.
A single 16-bit register handles the whole LEDs. Following a write, a
latch GPIO applies the new LED configuration. An "enable" GPIO (blank
in the TLC59928 datasheet) turns off the whole LEDs when active/high.

This driver is able to handle a daisy-chain case, so when several
TLC59928 controllers are connected in serie.

Signed-off-by: Corentin Guillevic <corentin.guillevic@xxxxxxxx>
---
drivers/leds/Kconfig | 9 ++
drivers/leds/Makefile | 1 +
drivers/leds/leds-tlc5928.c | 248 ++++++++++++++++++++++++++++++++++++
3 files changed, 258 insertions(+)
create mode 100644 drivers/leds/leds-tlc5928.c

diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig
index b784bb74a837..f429214dd9c2 100644
--- a/drivers/leds/Kconfig
+++ b/drivers/leds/Kconfig
@@ -747,6 +747,15 @@ config LEDS_TLC591XX
This option enables support for Texas Instruments TLC59108
and TLC59116 LED controllers.

+config LEDS_TLC5928
+ tristate "LED driver for TLC5928 controller"
+ depends on LEDS_CLASS && SPI
+ depends on GPIOLIB
+ select REGMAP_SPI
+ help
+ This option enables support for Texas Instruments TLC5928
+ LED controller.
+
config LEDS_MAX77650
tristate "LED support for Maxim MAX77650 PMIC"
depends on LEDS_CLASS && MFD_MAX77650
diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile
index 18afbb5a23ee..085d4917232a 100644
--- a/drivers/leds/Makefile
+++ b/drivers/leds/Makefile
@@ -87,6 +87,7 @@ obj-$(CONFIG_LEDS_SYSCON) += leds-syscon.o
obj-$(CONFIG_LEDS_TCA6507) += leds-tca6507.o
obj-$(CONFIG_LEDS_TI_LMU_COMMON) += leds-ti-lmu-common.o
obj-$(CONFIG_LEDS_TLC591XX) += leds-tlc591xx.o
+obj-$(CONFIG_LEDS_TLC5928) += leds-tlc5928.o
obj-$(CONFIG_LEDS_TPS6105X) += leds-tps6105x.o
obj-$(CONFIG_LEDS_TURRIS_OMNIA) += leds-turris-omnia.o
obj-$(CONFIG_LEDS_WM831X_STATUS) += leds-wm831x-status.o
diff --git a/drivers/leds/leds-tlc5928.c b/drivers/leds/leds-tlc5928.c
new file mode 100644
index 000000000000..5d6663d5d3f3
--- /dev/null
+++ b/drivers/leds/leds-tlc5928.c
@@ -0,0 +1,248 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright 2025 Corentin Guillevic <corentin.guillevic@xxxxxxxx>
+ */
+
+#include <linux/gpio/consumer.h>
+#include <linux/delay.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/slab.h>
+#include <linux/spi/spi.h>
+
+#define TLC5928_MAX_LEDS 16
+
+#define ldev_to_led(c) container_of(c, struct tlc5928_led, ldev)
+
+struct tlc5928_led {
+ bool active;
+ unsigned int led_no;
+ struct led_classdev ldev;
+ struct tlc5928_chip *chip;
+};
+
+struct tlc5928_chip {
+ struct gpio_desc *enable_gpio;
+ struct tlc5928_led leds[TLC5928_MAX_LEDS];
+ struct list_head list;
+ struct tlc5928_priv *priv;
+ u16 leds_state;
+};
+
+struct tlc5928_priv {
+ struct spi_device *spi;
+ struct gpio_desc *latch_gpio;
+ struct list_head chips_list;
+ struct mutex lock;
+};
+
+static int
+tlc5928_set_ledout(struct tlc5928_led *led, bool val)
+{
+ struct tlc5928_chip *chip;
+ struct tlc5928_chip *chip_owner = led->chip;
+ struct tlc5928_priv *priv = chip_owner->priv;
+ int ret;
+
+ mutex_lock(&priv->lock);
+
+ if (val)
+ chip_owner->leds_state |= (1 << led->led_no);
+ else
+ chip_owner->leds_state &= ~(1 << led->led_no);
+
+ list_for_each_entry_reverse(chip, &priv->chips_list, list) {
+ u16 leds_state = cpu_to_be16(chip->leds_state);
+
+ ret = spi_write(priv->spi, &(leds_state), sizeof(leds_state));
+
+ if (ret)
+ return ret;
+ }
+
+ gpiod_set_value(priv->latch_gpio, 0);
+ udelay(1);
+ gpiod_set_value(priv->latch_gpio, 1);
+
+ mutex_unlock(&priv->lock);
+
+ return 0;
+}
+
+static int
+tlc5928_brightness_set(struct led_classdev *led_cdev,
+ enum led_brightness brightness)
+{
+ struct tlc5928_led *led = ldev_to_led(led_cdev);
+
+ /* TLC5928 only allows on/off, no brightness */
+ return tlc5928_set_ledout(led, !!brightness);
+}
+
+static const struct of_device_id of_tlc5928_leds_match[] __maybe_unused = {
+ { .compatible = "ti,tlc5928" },
+ {},
+};
+MODULE_DEVICE_TABLE(of, of_tlc5928_leds_match);
+
+static int tlc5928_probe_chip_dt(struct device *dev, struct device_node *node,
+ struct tlc5928_chip *chip)
+{
+ struct device_node *child;
+ int count, err, reg;
+
+ count = of_get_available_child_count(node);
+ if (!count)
+ return -EINVAL;
+
+ chip->leds_state = 0;
+
+ for_each_available_child_of_node(node, child) {
+ struct tlc5928_led *led;
+ struct led_init_data init_data = {};
+
+ init_data.fwnode = of_fwnode_handle(child);
+
+ err = of_property_read_u32(child, "reg", &reg);
+ if (err) {
+ dev_err(dev, "%pOF: failed to read reg\n", child);
+ of_node_put(child);
+ return err;
+ }
+
+ if (reg < 0 || reg >= TLC5928_MAX_LEDS ||
+ chip->leds[reg].active) {
+ of_node_put(child);
+ return -EINVAL;
+ }
+
+ led = &chip->leds[reg];
+
+ led->active = true;
+ led->chip = chip;
+ led->led_no = reg;
+ led->ldev.brightness_set_blocking = tlc5928_brightness_set;
+ err = devm_led_classdev_register_ext(dev, &led->ldev,
+ &init_data);
+ if (err < 0) {
+ of_node_put(child);
+ dev_err(dev, "Failed to register LED for node %pfw\n",
+ init_data.fwnode);
+ return err;
+ }
+ }
+
+ return 0;
+}
+
+static int tlc5928_probe(struct spi_device *spi)
+{
+ struct device_node *node, *child;
+ struct device *dev = &spi->dev;
+ struct list_head *pos;
+ struct tlc5928_chip *chip;
+ struct tlc5928_priv *priv;
+ int count, err, i;
+
+ node = dev_of_node(dev);
+ if (!node)
+ return -ENODEV;
+
+ count = of_get_available_child_count(node);
+ if (!count)
+ return -EINVAL;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ priv->spi = spi;
+ priv->latch_gpio = devm_gpiod_get(dev, "latch", GPIOD_OUT_HIGH);
+ if (IS_ERR(priv->latch_gpio))
+ return dev_err_probe(dev, PTR_ERR(priv->latch_gpio),
+ "Failed to get latch GPIO\n");
+
+ mutex_init(&priv->lock);
+ INIT_LIST_HEAD(&priv->chips_list);
+
+ i = 0;
+ for_each_available_child_of_node(node, child) {
+ chip = devm_kzalloc(dev, sizeof(*chip), GFP_KERNEL);
+ if (!chip)
+ return -ENOMEM;
+
+ list_add_tail(&chip->list, &priv->chips_list);
+ chip->priv = priv;
+ chip->enable_gpio = devm_gpiod_get_index_optional(dev, "enable", i,
+ GPIOD_OUT_HIGH);
+ if (IS_ERR(chip->enable_gpio)) {
+ dev_err(dev, "Error getting enable GPIO %i property: %ld\n", i,
+ PTR_ERR(chip->enable_gpio));
+ return PTR_ERR(chip->enable_gpio);
+ }
+
+ err = tlc5928_probe_chip_dt(dev, child, chip);
+ if (err)
+ return err;
+
+ i++;
+ }
+
+ list_for_each(pos, &priv->chips_list) {
+ chip = container_of(pos, struct tlc5928_chip, list);
+ if (chip->enable_gpio)
+ gpiod_set_value(chip->enable_gpio, 0);
+ }
+
+ spi_set_drvdata(spi, priv);
+
+ return 0;
+}
+
+static int tlc5928_remove(struct spi_device *spi)
+{
+ struct list_head *pos;
+ struct tlc5928_priv *priv = spi_get_drvdata(spi);
+ int i;
+
+ list_for_each(pos, &priv->chips_list) {
+ struct tlc5928_chip *chip = container_of(pos, struct tlc5928_chip,
+ list);
+
+ for (i = 0; i < TLC5928_MAX_LEDS; i++) {
+ if (chip->leds[i].active)
+ devm_led_classdev_unregister(&spi->dev,
+ &chip->leds[i].ldev);
+ }
+
+ if (chip->enable_gpio) {
+ gpiod_set_value(chip->enable_gpio, 1);
+ gpiod_put(chip->enable_gpio);
+ }
+ }
+
+ return 0;
+}
+
+static const struct spi_device_id tlc5928_id[] = {
+ { "tlc5928" },
+ {},
+};
+MODULE_DEVICE_TABLE(spi, tlc5928_id);
+
+static struct spi_driver tlc5928_driver = {
+ .driver = {
+ .name = "tlc5928",
+ .of_match_table = of_match_ptr(of_tlc5928_leds_match),
+ },
+ .probe = tlc5928_probe,
+ .remove = tlc5928_remove,
+ .id_table = tlc5928_id,
+};
+
+module_spi_driver(tlc5928_driver);
+
+MODULE_AUTHOR("Corentin Guillevic <corentin.guillevic@xxxxxxxx>");
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("TLC5928 LED driver");
--
2.45.2