All of lore.kernel.org
 help / color / mirror / Atom feed
From: Ivan Vozvakhov <i.vozvakhov@corp.mail.ru>
To: pavel@ucw.cz, linux-leds@vger.kernel.org
Cc: Ivan Vozvakhov <i.vozvakhov@corp.mail.ru>
Subject: [PATCH] leds: add LED driver for Worldsemi WS2812B
Date: Mon, 14 Mar 2022 15:12:21 +0300	[thread overview]
Message-ID: <20220314121221.1175437-1-i.vozvakhov@corp.mail.ru> (raw)

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

diff --git a/Documentation/devicetree/bindings/leds/leds-ws2812b.yaml b/Documentation/devicetree/bindings/leds/leds-ws2812b.yaml
new file mode 100644
index 000000000000..a71f37f51e2a
--- /dev/null
+++ b/Documentation/devicetree/bindings/leds/leds-ws2812b.yaml
@@ -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>;
+   };
+
+...
diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig
index 6090e647daee..4eda92a2c0b2 100644
--- a/drivers/leds/Kconfig
+++ b/drivers/leds/Kconfig
@@ -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
diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile
index e58ecb36360f..6eef9b731884 100644
--- a/drivers/leds/Makefile
+++ b/drivers/leds/Makefile
@@ -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
diff --git a/drivers/leds/leds-ws2812b.c b/drivers/leds/leds-ws2812b.c
new file mode 100644
index 000000000000..daef470e073e
--- /dev/null
+++ b/drivers/leds/leds-ws2812b.c
@@ -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");
-- 
2.25.1


             reply	other threads:[~2022-03-14 12:20 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-03-14 12:12 Ivan Vozvakhov [this message]
2022-03-16 15:28 ` [PATCH] leds: add LED driver for Worldsemi WS2812B Andy Shevchenko

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=20220314121221.1175437-1-i.vozvakhov@corp.mail.ru \
    --to=i.vozvakhov@corp.mail.ru \
    --cc=linux-leds@vger.kernel.org \
    --cc=pavel@ucw.cz \
    /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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.