new file mode 100644
@@ -0,0 +1,76 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/leds/leds-ws2812b.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Worldsemi WS2812B LED's driver powered by SPI
+
+maintainers:
+ - Ivan Vozvakhov <i.vozvakhov@vk.team>
+
+description: |
+ Bindings for the Worldsemi WS2812B LED's powered by SPI.
+ Used SPI-MOSI only.
+
+ For more product information please see the link below:
+ http://www.world-semi.com/Certifications/WS2812B.html
+
+properties:
+ compatible:
+ const: worldsemi,ws2812b
+
+ reg:
+ maxItems: 1
+
+ spi-max-frequency:
+ const: 2500000
+
+ device-name:
+ type: string
+
+patternProperties:
+ "(^led[0-9a-f]$|led)":
+ type: object
+ $ref: common.yaml#
+
+required:
+ - compatible
+ - reg
+ - spi-max-frequency
+
+additionalProperties: false
+
+examples:
+ - |
+ &spi0 {
+ status = "okay";
+ pinctrl-0 = <&spi0_mosi>;
+
+ ws2812b@00 {
+ compatible = "worldsemi,ws2812b";
+ reg = <0x00>;
+ spi-max-frequency = <2500000>;
+
+ led1 {
+ label = "top-led1";
+ color = <LED_COLOR_ID_GREEN>;
+ };
+
+ led2 {
+ label = "top-led2";
+ color = <LED_COLOR_ID_RED>;
+ };
+
+ led3 {
+ label = "top-led3";
+ color = <LED_COLOR_ID_BLUE>;
+ };
+ };
+ };
+
+ &spi0_mosi_hs {
+ rockchip,pins = <2 RK_PA1 2 &pcfg_pull_down>;
+ };
+
+...
@@ -157,6 +157,18 @@ config LEDS_EL15203000
To compile this driver as a module, choose M here: the module
will be called leds-el15203000.
+config LEDS_WS2812B
+ tristate "LED Support for Worldsemi WS2812B"
+ depends on LEDS_CLASS
+ depends on SPI
+ depends on OF
+ help
+ This option enables support for WS2812B LED's
+ through SPI.
+
+ To compile this driver as a module, choose M here: the module
+ will be called leds-ws2812b.
+
config LEDS_TURRIS_OMNIA
tristate "LED support for CZ.NIC's Turris Omnia"
depends on LEDS_CLASS_MULTICOLOR
@@ -92,6 +92,7 @@ obj-$(CONFIG_LEDS_CR0014114) += leds-cr0014114.o
obj-$(CONFIG_LEDS_DAC124S085) += leds-dac124s085.o
obj-$(CONFIG_LEDS_EL15203000) += leds-el15203000.o
obj-$(CONFIG_LEDS_SPI_BYTE) += leds-spi-byte.o
+obj-$(CONFIG_LEDS_WS2812B) += leds-ws2812b.o
# LED Userspace Drivers
obj-$(CONFIG_LEDS_USER) += uleds.o
new file mode 100644
@@ -0,0 +1,420 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * LEDs driver for Worldsemi WS2812B through SPI
+ * SPI-MOSI for data transfer
+ * Required DMA transfers
+ *
+ * Copyright (C) 2022 Ivan Vozvakhov <i.vozvakhov@vk.team>
+ *
+ * Inspired by (C) Martin Sperl <kernel@martin.sperl.org>
+ *
+ */
+#include <linux/leds.h>
+#include <linux/of.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/workqueue.h>
+#include <linux/spi/spi.h>
+#include <linux/uaccess.h>
+#include <linux/miscdevice.h>
+
+/*
+ * WS2812B timings:
+ * TH + TL = 1.25us +-600us
+ * T0H: 0.4us +-150ns
+ * T1H: 0.8us +-150ns
+ * T0L: 0.85us +-150ns
+ * T1L: 0.45us +-150ns
+ * RESL: >50us
+ *
+ * Each bit led's state coding by 3 real bits (see tables above):
+ * T0H and T0L as 1 bit, T1H and T1L as 2 bits.
+ *
+ * And let's assume SPI bus freq. to 2.5MHz.
+ * By that:
+ * T0H: 0.4us
+ * T1H: 0.8us
+ * T0L: 0.8us
+ * T1L: 0.4us
+ * RESL: > (50 / 0.4 = 125) bit (16 bytes)
+ */
+#define SPI_BUS_SPEED_HZ 2500000
+#define RESET_BYTES 16
+/*
+ * Basically, SPI pull-up MOSI line, but for correct state it should be pull-down
+ * (RES is detected by low signal).
+ * SPI-MOSI for some controllers could have z-state with pull-down for MOSI
+ * before first SPI-CLK edges.
+ * To eliminate it, send RES sequence before first bit's.
+ */
+#define DELAY_BEFORE_FIRST_DATA RESET_BYTES
+#define DEFAULT_DEVICE_NAME "ws2812b"
+
+/*
+ * Ioctl interface for set's several led's at one time.
+ *
+ * [start_led, stop_led)
+ */
+struct ws2812b_multi_set {
+ int start_led;
+ int stop_led;
+ uint8_t *brightnesses;
+};
+
+#define LEDS_WS2812B_IOCTL_MAGIC 'z'
+#define LEDS_WS2812B_IOCTL_MULTI_SET \
+ _IOW(LEDS_WS2812B_IOCTL_MAGIC, 0x01, struct ws2812b_multi_set)
+#define LEDS_WS2812B_IOCTL_GET_LEDS_NUMBER \
+ _IOR(LEDS_WS2812B_IOCTL_MAGIC, 0x02, int)
+
+/*
+ * Each led's state bits coded by 3 bits,
+ * 8 led's one-color state (actual LED) would take 24 real-bits.
+ * That 24 bits divided into high, medium, low groups.
+ * All possible states defined there (see brightess_encode func. for masks).
+ */
+const char byte2encoding_h[] = {
+ 0x92, 0x93, 0x9a, 0x9b,
+ 0xd2, 0xd3, 0xda, 0xdb
+};
+
+const char byte2encoding_m[] = {
+ 0x49, 0x4d, 0x69, 0x6d
+};
+
+const char byte2encoding_l[] = {
+ 0x24, 0x26, 0x34, 0x36,
+ 0xa4, 0xa6, 0xb4, 0xb6
+};
+
+struct ws2812b_encoding {
+ uint8_t h, m, l;
+};
+
+static void brightess_encode(
+ struct ws2812b_encoding *enc,
+ const uint8_t val)
+{
+ enc->h = byte2encoding_h[(val >> 5) & 0x07];
+ enc->m = byte2encoding_m[(val >> 3) & 0x03];
+ enc->l = byte2encoding_l[(val >> 0) & 0x07];
+}
+
+struct ws2812b_led {
+ struct led_classdev ldev;
+ spinlock_t led_data_lock;
+
+ uint8_t brightness;
+ int num;
+
+ struct device *dev;
+ struct device_node *child;
+
+ struct work_struct work;
+ struct ws2812b_priv *priv;
+};
+
+struct ws2812b_priv {
+ struct mutex ws2812b_mutex;
+
+ struct spi_device *spi;
+ struct spi_message spi_msg;
+ struct spi_transfer spi_xfer;
+ struct ws2812b_encoding *spi_data;
+
+ struct miscdevice mdev;
+ struct work_struct work_update_all;
+ int num_leds;
+
+ struct ws2812b_led *leds;
+};
+
+static void ws2812b_all_leds_update_work(struct work_struct *work)
+{
+ struct ws2812b_priv *priv = container_of(work, struct ws2812b_priv, work_update_all);
+ struct ws2812b_encoding *led_enc = priv->spi_data;
+ struct ws2812b_led *led = priv->leds;
+ int i;
+
+ led_enc = (struct ws2812b_encoding *)((uint8_t *)led_enc + DELAY_BEFORE_FIRST_DATA);
+
+ mutex_lock(&priv->ws2812b_mutex);
+ for (i = 0; i < priv->num_leds; i++, led_enc++, led++)
+ brightess_encode(led_enc, led->brightness);
+ spi_sync(priv->spi, &priv->spi_msg);
+ mutex_unlock(&priv->ws2812b_mutex);
+}
+
+static void ws2812b_led_work(struct work_struct *work)
+{
+ struct ws2812b_led *led = container_of(work, struct ws2812b_led, work);
+ struct ws2812b_priv *priv = led->priv;
+ struct ws2812b_encoding *led_enc = &priv->spi_data[led->num];
+
+ led_enc = (struct ws2812b_encoding *)((uint8_t *)led_enc + DELAY_BEFORE_FIRST_DATA);
+
+ mutex_lock(&priv->ws2812b_mutex);
+ brightess_encode(led_enc, led->brightness);
+ spi_sync(priv->spi, &priv->spi_msg);
+ mutex_unlock(&priv->ws2812b_mutex);
+}
+
+static void ws2812b_led_set_brightness(struct led_classdev *ldev,
+ enum led_brightness brightness)
+{
+ struct ws2812b_led *led = container_of(ldev, struct ws2812b_led, ldev);
+
+ spin_lock(&led->led_data_lock);
+ led->brightness = (uint8_t) brightness;
+ schedule_work(&led->work);
+ spin_unlock(&led->led_data_lock);
+}
+
+static int ws2812b_open(struct inode *inode, struct file *file)
+{
+ return 0;
+}
+
+static int ws2812b_release(struct inode *inode, struct file *file)
+{
+ return 0;
+}
+
+static long ws2812b_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
+{
+ struct miscdevice *mdev = file->private_data;
+ struct ws2812b_priv *priv = container_of(mdev, struct ws2812b_priv, mdev);
+ struct ws2812b_led *led;
+ struct ws2812b_multi_set ms;
+ uint8_t *brightness;
+ int i = 0, ret = 0, leds_to_change;
+
+ switch (cmd) {
+ case LEDS_WS2812B_IOCTL_MULTI_SET:
+ {
+ if (copy_from_user(&ms, (void __user *)arg,
+ sizeof(struct ws2812b_multi_set))) {
+ ret = -EFAULT;
+ break;
+ }
+
+ leds_to_change = ms.stop_led - ms.start_led;
+ if (ms.start_led < 0
+ || leds_to_change > priv->num_leds
+ || leds_to_change < 1) {
+ ret = -EINVAL;
+ break;
+ }
+
+ brightness = kmalloc(sizeof(uint8_t) * leds_to_change, GFP_KERNEL);
+ if (!brightness)
+ return -ENOMEM;
+
+ if (copy_from_user(brightness, ms.brightnesses,
+ sizeof(uint8_t) * leds_to_change)) {
+ ret = -EFAULT;
+ break;
+ }
+
+ for (i = ms.start_led, led = priv->leds+ms.start_led;
+ i < ms.stop_led;
+ i++, led++, brightness++) {
+ spin_lock(&led->led_data_lock);
+ led->brightness = *brightness;
+ }
+ schedule_work(&priv->work_update_all);
+
+ for (i = ms.start_led, led = priv->leds+ms.start_led;
+ i < ms.stop_led;
+ i++, led++) {
+ spin_unlock(&led->led_data_lock);
+ }
+ kfree(brightness-leds_to_change);
+ break;
+ }
+ case LEDS_WS2812B_IOCTL_GET_LEDS_NUMBER:
+ {
+ int __user *p = (int __user *)arg;
+
+ ret = put_user(priv->num_leds, p);
+ break;
+ }
+ default:
+ break;
+ }
+
+ return ret;
+}
+
+static const struct file_operations ws2812b_ops = {
+ .owner = THIS_MODULE,
+ .open = ws2812b_open,
+ .release = ws2812b_release,
+ .unlocked_ioctl = ws2812b_ioctl,
+#ifdef CONFIG_COMPAT
+ .compat_ioctl = ws2812b_ioctl,
+#endif
+};
+
+static int ws2812b_parse_child_dt(const struct device *dev,
+ struct device_node *child,
+ struct ws2812b_led *led)
+{
+ struct led_classdev *ldev = &led->ldev;
+ const char *state;
+
+ if (of_property_read_string(child, "label", &ldev->name))
+ ldev->name = child->name;
+
+ state = of_get_property(child, "default-state", NULL);
+ if (state) {
+ if (!strcmp(state, "on")) {
+ ldev->brightness = LED_FULL;
+ } else if (strcmp(state, "off")) {
+ dev_err(dev, "default-state can only be 'on' or 'off'");
+ return -EINVAL;
+ }
+ ldev->brightness = LED_OFF;
+ }
+
+ ldev->brightness_set = ws2812b_led_set_brightness;
+
+ INIT_WORK(&led->work, ws2812b_led_work);
+
+ return 0;
+}
+
+static int ws2812b_parse_dt(struct device *dev,
+ struct ws2812b_priv *priv)
+{
+ struct device_node *child;
+ int ret = 0, i = 0;
+
+ for_each_child_of_node(dev->of_node, child) {
+ struct ws2812b_led *led = &priv->leds[i];
+
+ led->priv = priv;
+ led->dev = dev;
+ led->child = child;
+ led->num = i;
+
+ spin_lock_init(&led->led_data_lock);
+
+ ret = ws2812b_parse_child_dt(dev, child, led);
+
+ if (ret)
+ goto err;
+
+ ret = devm_led_classdev_register(dev, &led->ldev);
+ if (ret) {
+ dev_err(dev, "failed to register led for %s: %d\n", led->ldev.name, ret);
+ goto err;
+ }
+
+ led->ldev.dev->of_node = child;
+ i++;
+ }
+
+ return 0;
+err:
+ of_node_put(child);
+ return ret;
+}
+
+static const struct of_device_id ws2812b_driver_ids[] = {
+ { .compatible = "worldsemi,ws2812b" },
+ {},
+};
+MODULE_DEVICE_TABLE(of, ws2812b_driver_ids);
+
+static int ws2812b_probe(struct spi_device *spi)
+{
+ struct device *dev = &spi->dev;
+ struct ws2812b_priv *priv;
+ struct ws2812b_encoding *spi_data;
+ int ret, len, count_leds;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ count_leds = of_get_child_count(dev->of_node);
+ if (!count_leds) {
+ dev_err(dev, "should define at least one led\n");
+ return -EINVAL;
+ }
+
+ priv->num_leds = count_leds;
+ priv->leds = devm_kzalloc(dev, sizeof(struct ws2812b_led) * count_leds, GFP_KERNEL);
+
+ mutex_init(&priv->ws2812b_mutex);
+
+ len = DELAY_BEFORE_FIRST_DATA + count_leds * sizeof(struct ws2812b_encoding) + RESET_BYTES;
+ spi_data = devm_kzalloc(dev, len, GFP_KERNEL);
+ if (!spi_data)
+ return -ENOMEM;
+ priv->spi_data = spi_data;
+
+ priv->spi = spi;
+ spi_message_init(&priv->spi_msg);
+ priv->spi_xfer.len = len;
+ priv->spi_xfer.tx_buf = spi_data;
+ priv->spi_xfer.speed_hz = SPI_BUS_SPEED_HZ;
+ spi_message_add_tail(&priv->spi_xfer, &priv->spi_msg);
+
+ priv->mdev.minor = MISC_DYNAMIC_MINOR;
+ priv->mdev.fops = &ws2812b_ops;
+ priv->mdev.parent = NULL;
+
+ if (of_property_read_string(dev->of_node, "device-name", &priv->mdev.name))
+ priv->mdev.name = DEFAULT_DEVICE_NAME;
+
+ ret = misc_register(&priv->mdev);
+ if (ret) {
+ dev_err(dev, "can't register %s device\n", priv->mdev.name);
+ return ret;
+ }
+
+ INIT_WORK(&priv->work_update_all, ws2812b_all_leds_update_work);
+
+ spi_set_drvdata(spi, priv);
+
+ ret = ws2812b_parse_dt(dev, priv);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static int ws2812b_remove(struct spi_device *spi)
+{
+ struct ws2812b_priv *priv = spi_get_drvdata(spi);
+ int i;
+
+ for (i = 0; i < priv->num_leds; i++) {
+ led_classdev_unregister(&priv->leds[i].ldev);
+ cancel_work_sync(&priv->leds[i].work);
+ }
+ cancel_work_sync(&priv->work_update_all);
+
+ return 0;
+}
+
+static struct spi_driver ws2812b_driver = {
+ .probe = ws2812b_probe,
+ .remove = ws2812b_remove,
+ .driver = {
+ .name = KBUILD_MODNAME,
+ .owner = THIS_MODULE,
+ .of_match_table = ws2812b_driver_ids,
+ },
+};
+
+module_spi_driver(ws2812b_driver);
+
+MODULE_AUTHOR("Ivan Vozvakhov <i.vozvakhov@vk.team>");
+MODULE_DESCRIPTION("WS2812B LED driver powered by SPI");
+MODULE_LICENSE("GPL v2");
+MODULE_ALIAS("spi:ws2812b");
This patch adds a LED class driver (powered by SPI) for the WS2812B LEDs that's is widely used in consumer electronic devices and DIY. Signed-off-by: Ivan Vozvakhov <i.vozvakhov@corp.mail.ru> --- .../bindings/leds/leds-ws2812b.yaml | 76 ++++ drivers/leds/Kconfig | 12 + drivers/leds/Makefile | 1 + drivers/leds/leds-ws2812b.c | 420 ++++++++++++++++++ 4 files changed, 509 insertions(+) create mode 100644 Documentation/devicetree/bindings/leds/leds-ws2812b.yaml create mode 100644 drivers/leds/leds-ws2812b.c