[PATCH] drivers: led: Add WS28XX compatible LED driver
Andri Yngvason
andri at yngvason.is
Tue Aug 12 17:30:18 CEST 2025
This is a new LED driver for the WS28XX family of LED controllers from
Worldsemi and compatible devices from other manufacturers.
The LED chain must be connected to an SPI output. This driver uses SPI
to emulate the required digital signal.
Each LED gets a fixed colour assigned to it in the device tree.
Signed-off-by: Andri Yngvason <andri at yngvason.is>
---
doc/device-tree-bindings/leds/leds-ws28xx.txt | 48 ++++
drivers/led/Kconfig | 8 +
drivers/led/Makefile | 1 +
drivers/led/led_ws28xx.c | 233 ++++++++++++++++++
4 files changed, 290 insertions(+)
create mode 100644 doc/device-tree-bindings/leds/leds-ws28xx.txt
create mode 100644 drivers/led/led_ws28xx.c
diff --git a/doc/device-tree-bindings/leds/leds-ws28xx.txt b/doc/device-tree-bindings/leds/leds-ws28xx.txt
new file mode 100644
index 00000000000..8c72c81fd55
--- /dev/null
+++ b/doc/device-tree-bindings/leds/leds-ws28xx.txt
@@ -0,0 +1,48 @@
+WS28XX compatible LEDs connected to SPI
+
+Required properties:
+- compatible: Must be "ws28xx-leds".
+- reg: SPI chip-select is unused, but should be set to <0>.
+
+Optional properties:
+- red-offset: The offset of the red colour component (default 0).
+- green-offset: The offset of the green colour component (default 8).
+- blue-offset: The offset of the blue colour component (default 16).
+- white-offset: The offset of the white colour component (disabled by default).
+- reset-period: Communication reset period in microseconds (default 50).
+
+The offsets are expressed in bits as they are sent out on the wire. E.g. if
+red-offset = <0> and green-offset = <8>, then red is sent first and then green.
+
+Each LED is represented as a sub-node of the ws28xx-leds device. Each node's
+name represents the name of the corresponding LED. Multiple LEDs can be chained
+and each LED is represented by a sub-node in the device tree in the same order
+as in the chain.
+
+Optional LED sub-node properties:
+- red: Brightness of the red colour component (0 - 255, default 0).
+- green: Brightness of the green colour component (0 - 255, default 0).
+- blue: Brightness of the blue colour component (0 - 255, default 0).
+- white: Brightness of the white colour component (0 - 255, default 0).
+
+Example:
+
+&spi0 {
+ status = "okay";
+
+ leds {
+ red-offset = <0>;
+ green-offset = <16>;
+ blue-offset = <8>;
+
+ red-led {
+ red = <127>;
+ default-state = "on";
+ };
+
+ green-led {
+ green = <127>;
+ default-state = "on";
+ };
+ };
+};
diff --git a/drivers/led/Kconfig b/drivers/led/Kconfig
index c98cbf92fab..4f711657ae0 100644
--- a/drivers/led/Kconfig
+++ b/drivers/led/Kconfig
@@ -88,6 +88,13 @@ config LED_PWM
Enable support for LEDs connected to PWM.
Linux compatible ofdata.
+config LED_WS28XX
+ bool "LED WS28XX"
+ depends on LED && DM_SPI
+ help
+ Enable support for WS2811, WS2812 and other compatible LEDS
+ controlled over SPI.
+
config LED_BLINK
bool "Support hardware LED blinking"
depends on LED
@@ -142,6 +149,7 @@ config LED_STATUS
Allows common u-boot commands to use a board's leds to
provide status for activities like booting and downloading files.
+
if LED_STATUS
# Hidden constants
diff --git a/drivers/led/Makefile b/drivers/led/Makefile
index 996753b88ae..2b42b6eb9f7 100644
--- a/drivers/led/Makefile
+++ b/drivers/led/Makefile
@@ -13,3 +13,4 @@ obj-$(CONFIG_LED_PWM) += led_pwm.o
obj-$(CONFIG_$(PHASE_)LED_GPIO) += led_gpio.o
obj-$(CONFIG_LED_CORTINA) += led_cortina.o
obj-$(CONFIG_LED_LP5562) += led_lp5562.o
+obj-$(CONFIG_LED_WS28XX) += led_ws28xx.o
diff --git a/drivers/led/led_ws28xx.c b/drivers/led/led_ws28xx.c
new file mode 100644
index 00000000000..a311a35dc88
--- /dev/null
+++ b/drivers/led/led_ws28xx.c
@@ -0,0 +1,233 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (c) App Dynamic ehf.
+ * Written by Andri Yngvason <andri at yngvason.is>
+ *
+ * LED controllers in the WS28XX family from Worldsemi are controlled using a
+ * digital signal transmitted via a single wire. Multiple devices can be
+ * chained, in which case the first device will pass on extraneous data to the
+ * second device, second to third, and so on.
+ *
+ * Each bit in the digital signal is represented by a particular wave form. The
+ * bit period is 2.5 µs +/- 0.3 µs (400 kb/s).
+ *
+ * To represent 0, the signal goes high for 0.5 µs and then back low for the
+ * remainder of the bit period.
+ *
+ * To represent 1, the signal goes high for 1.2 µs and then back low for the
+ * remainder of the bit period.
+ *
+ * If the signal remains low for 50 µs or more, the device will reset
+ * communication and start receiving the next signal as if it were the first
+ * signal received.
+ *
+ * Bit-banging this waveform would be sub-optimal, but there is enough
+ * precision within half a byte of data transmitted over SPI to generate these
+ * waveforms so that they meet the specified timing constraints.
+ *
+ * The required data rate is 4 * 400 kb/s = 1.6 Mb/s, but the SPI clock
+ * frequency is twice that, or 3.2 MHz.
+ *
+ * Now, 0 can be represented using 8, which yields a duty period of 2.5 µs / 4
+ * = 0.6 µs, which is close enough, and 1 can be represented using 0xc, which
+ * yields a duty period of 2.5 µs / 2 = 1.25 µs, which is fits within the error
+ * margin.
+ */
+
+#include <dm.h>
+#include <errno.h>
+#include <led.h>
+#include <log.h>
+#include <malloc.h>
+#include <spi.h>
+#include <linux/delay.h>
+
+#define LEDS_WS28XX_DRIVER_NAME "led_ws28xx"
+#define WS28XX_SPI_CLOCK 3200000
+#define LEDS_WS28XX_MIN_RESET_PERIOD_DEFAULT 50 /* µs */
+
+struct led_ws28xx_priv {
+ bool on;
+};
+
+struct led_ws28xx_plat {
+ u8 r, g, b, w;
+};
+
+struct led_ws28xx_wrap_plat {
+ bool have_white;
+ u8 r_off, g_off, b_off, w_off;
+ u32 reset_period;
+};
+
+static void byte_pattern_from_value(u8 *dst, u8 value)
+{
+ for (int i = 0; i < 8; i += 2) {
+ dst[i / 2] = (value & BIT(7 - i)) ? 0xc0 : 0x80;
+ dst[i / 2] |= (value & BIT(6 - i)) ? 0xc : 0x8;
+ }
+}
+
+static void ws28xx_spi_payload_from_led(u8 *payload,
+ struct led_ws28xx_wrap_plat *wrap,
+ struct udevice *dev)
+{
+ struct led_ws28xx_priv *priv = dev_get_priv(dev);
+ struct led_ws28xx_plat *plat = dev_get_plat(dev);
+ u8 r = 0, g = 0, b = 0, w = 0;
+
+ if (priv && priv->on) {
+ r = plat->r;
+ g = plat->g;
+ b = plat->b;
+ w = plat->w;
+ }
+
+ byte_pattern_from_value(payload + wrap->r_off / 2, r);
+ byte_pattern_from_value(payload + wrap->g_off / 2, g);
+ byte_pattern_from_value(payload + wrap->b_off / 2, b);
+
+ if (wrap->have_white)
+ byte_pattern_from_value(payload + wrap->w_off / 2, w);
+}
+
+static int led_ws28xx_commit(struct udevice *dev)
+{
+ struct led_ws28xx_wrap_plat *plat = dev_get_plat(dev);
+ int n_leds = device_get_child_count(dev);
+ int bytes_per_bit = 2;
+ int bits_per_led = plat->have_white ? 32 : 24;
+ int bytes_per_led = bits_per_led / bytes_per_bit;
+ int payload_size = n_leds * bytes_per_led;
+ struct udevice *child;
+ u8 *payload;
+ int pos = 0;
+ int ret;
+
+ payload = malloc(payload_size);
+ if (!payload)
+ return -ENOMEM;
+
+ device_foreach_child(child, dev) {
+ ws28xx_spi_payload_from_led(payload + pos, plat, child);
+ pos += bytes_per_led;
+ }
+
+ dm_spi_claim_bus(dev);
+ ret = dm_spi_xfer(dev, payload_size * 8, payload, NULL, SPI_XFER_ONCE);
+ dm_spi_release_bus(dev);
+
+ free(payload);
+
+ udelay(plat->reset_period);
+
+ return ret;
+}
+
+static int led_ws28xx_set_state(struct udevice *dev, enum led_state_t state)
+{
+ struct led_ws28xx_priv *priv = dev_get_priv(dev);
+
+ switch (state) {
+ case LEDST_OFF:
+ priv->on = false;
+ break;
+ case LEDST_ON:
+ priv->on = true;
+ break;
+ case LEDST_TOGGLE:
+ priv->on = !priv->on;
+ break;
+ default:
+ return -ENOSYS;
+ }
+
+ return led_ws28xx_commit(dev_get_parent(dev));
+}
+
+static enum led_state_t led_ws28xx_get_state(struct udevice *dev)
+{
+ struct led_ws28xx_priv *priv = dev_get_priv(dev);
+
+ return priv->on ? LEDST_ON : LEDST_OFF;
+}
+
+static int led_ws28xx_of_to_plat(struct udevice *dev)
+{
+ struct led_ws28xx_plat *plat = dev_get_plat(dev);
+
+ plat->r = dev_read_u32_default(dev, "red", 0);
+ plat->g = dev_read_u32_default(dev, "green", 0);
+ plat->b = dev_read_u32_default(dev, "blue", 0);
+ plat->w = dev_read_u32_default(dev, "white", 0);
+
+ return 0;
+}
+
+static const struct led_ops led_ws28xx_ops = {
+ .set_state = led_ws28xx_set_state,
+ .get_state = led_ws28xx_get_state,
+};
+
+U_BOOT_DRIVER(led_ws28xx) = {
+ .name = LEDS_WS28XX_DRIVER_NAME,
+ .id = UCLASS_LED,
+ .ops = &led_ws28xx_ops,
+ .plat_auto = sizeof(struct led_ws28xx_plat),
+ .priv_auto = sizeof(struct led_ws28xx_priv),
+ .of_to_plat = led_ws28xx_of_to_plat,
+};
+
+static int led_ws28xx_wrap_of_to_plat(struct udevice *dev)
+{
+ struct led_ws28xx_wrap_plat *plat = dev_get_plat(dev);
+ u32 w_off;
+
+ plat->r_off = dev_read_u32_default(dev, "red-offset", 0);
+ plat->g_off = dev_read_u32_default(dev, "green-offset", 8);
+ plat->b_off = dev_read_u32_default(dev, "blue-offset", 16);
+ plat->have_white = dev_read_u32(dev, "white-offset", &w_off) == 0;
+ plat->w_off = w_off;
+ plat->reset_period = dev_read_u32_default(dev, "reset-period",
+ LEDS_WS28XX_MIN_RESET_PERIOD_DEFAULT);
+
+ return 0;
+}
+
+static int led_ws28xx_wrap_probe(struct udevice *dev)
+{
+ struct spi_slave *spi;
+ struct udevice *bus = dev->parent;
+
+ if (bus->uclass->uc_drv->id != UCLASS_SPI) {
+ log_err(LEDS_WS28XX_DRIVER_NAME " must be an SPI slave\n");
+ return -EOPNOTSUPP;
+ }
+
+ spi = dev_get_parent_priv(dev);
+ spi->max_hz = WS28XX_SPI_CLOCK;
+ spi->mode = SPI_MODE_0;
+ spi->wordlen = 8;
+
+ return 0;
+}
+
+static int led_ws28xx_bind(struct udevice *parent)
+{
+ return led_bind_generic(parent, LEDS_WS28XX_DRIVER_NAME);
+}
+
+static const struct udevice_id led_ws28xx_ids[] = {
+ { .compatible = "ws28xx-leds" },
+ { }
+};
+
+U_BOOT_DRIVER(led_pwm_wrap) = {
+ .name = LEDS_WS28XX_DRIVER_NAME "_wrap",
+ .id = UCLASS_NOP,
+ .of_match = led_ws28xx_ids,
+ .plat_auto = sizeof(struct led_ws28xx_wrap_plat),
+ .of_to_plat = led_ws28xx_wrap_of_to_plat,
+ .probe = led_ws28xx_wrap_probe,
+ .bind = led_ws28xx_bind,
+};
--
2.50.0
More information about the U-Boot
mailing list