diff mbox series

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

Message ID 20250326153535.158137-1-corentin.guillevic@smile.fr
State New
Headers show
Series [1/2] leds: tlc5928: Driver for the TI 16 Channel spi LED driver | expand

Commit Message

Corentin Guillevic March 26, 2025, 3:35 p.m. UTC
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@smile.fr>
---
 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 mbox series

Patch

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@smile.fr>
+ */
+
+#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@smile.fr>");
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("TLC5928 LED driver");