All of lore.kernel.org
 help / color / mirror / Atom feed
From: Chuanhong Guo <gch981213@gmail.com>
To: linux-leds@vger.kernel.org
Cc: Chuanhong Guo <gch981213@gmail.com>, Pavel Machek <pavel@ucw.cz>,
	Rob Herring <robh+dt@kernel.org>,
	Krzysztof Kozlowski <krzysztof.kozlowski+dt@linaro.org>,
	Stanislav Jakubek <stano.jakubek@gmail.com>,
	Linus Walleij <linus.walleij@linaro.org>,
	Shawn Guo <shawnguo@kernel.org>,
	Johan Hovold <johan+linaro@kernel.org>,
	Bjorn Andersson <andersson@kernel.org>,
	Marijn Suijten <marijn.suijten@somainline.org>,
	Sven Schwermer <sven.schwermer@disruptive-technologies.com>,
	devicetree@vger.kernel.org (open list:OPEN FIRMWARE AND
	FLATTENED DEVICE TREE BINDINGS),
	linux-kernel@vger.kernel.org (open list)
Subject: [PATCH v4 3/3] leds: add driver for SPI driven WorldSemi WS2812B RGB LEDs
Date: Mon, 12 Dec 2022 12:55:58 +0800	[thread overview]
Message-ID: <20221212045558.69602-4-gch981213@gmail.com> (raw)
In-Reply-To: <20221212045558.69602-1-gch981213@gmail.com>

This patch adds support for driving a chain of WS2812B LED chips
using SPI bus.

WorldSemi WS2812B is a individually addressable LED chip that
can be chained together and controlled individually using a
single wire. The chip recognize a long pulse as a bit of 1 and
a short pulse as a bit of 0. Host sends a continuous stream
of 24-bits color values, each LED chip takes the first 3 byte
it receives as its color value and passes the leftover bytes to
the next LED on the chain.

This driver simulates this protocol using SPI bus by sending
a long pulse as 3'b110 and a short pulse as 3'b100. The SPI
frequency needs to be 2.105MHz~2.85MHz for the timing to be
correct and the controller needs to transfer all the bytes
continuously.

Signed-off-by: Chuanhong Guo <gch981213@gmail.com>
---
Changes since v1:
rename the driver to drop -spi suffix
add support for default-brightness
use fwnode apis for properties

Changes since v2:
drop default-brightness and default-intensity

Changes since v3:
1. add more comments
2. rename reg to cascade
3. redo some line breaking
4. move duplicated pointer calculation into ws2812b_set_byte
5. reword error message
6. get ws2812b_priv from led cdev->dev->parent

 drivers/leds/rgb/Kconfig        |  11 ++
 drivers/leds/rgb/Makefile       |   1 +
 drivers/leds/rgb/leds-ws2812b.c | 231 ++++++++++++++++++++++++++++++++
 3 files changed, 243 insertions(+)
 create mode 100644 drivers/leds/rgb/leds-ws2812b.c

diff --git a/drivers/leds/rgb/Kconfig b/drivers/leds/rgb/Kconfig
index 204cf470beae..5c2081852f01 100644
--- a/drivers/leds/rgb/Kconfig
+++ b/drivers/leds/rgb/Kconfig
@@ -26,4 +26,15 @@ config LEDS_QCOM_LPG
 
 	  If compiled as a module, the module will be named leds-qcom-lpg.
 
+config LEDS_WS2812B
+	tristate "SPI driven WS2812B RGB LED support"
+	depends on OF
+	depends on SPI
+	help
+	  This option enables support for driving daisy-chained WS2812B RGB
+	  LED chips using SPI bus. This driver simulates the single-wire
+	  protocol by sending bits over the SPI MOSI pin. For this to work,
+	  the SPI frequency should be 2.105MHz~2.85MHz and the controller
+	  needs to transfer all the bytes continuously.
+
 endif # LEDS_CLASS_MULTICOLOR
diff --git a/drivers/leds/rgb/Makefile b/drivers/leds/rgb/Makefile
index 0675bc0f6e18..a6f855eaeb14 100644
--- a/drivers/leds/rgb/Makefile
+++ b/drivers/leds/rgb/Makefile
@@ -2,3 +2,4 @@
 
 obj-$(CONFIG_LEDS_PWM_MULTICOLOR)	+= leds-pwm-multicolor.o
 obj-$(CONFIG_LEDS_QCOM_LPG)		+= leds-qcom-lpg.o
+obj-$(CONFIG_LEDS_WS2812B)		+= leds-ws2812b.o
diff --git a/drivers/leds/rgb/leds-ws2812b.c b/drivers/leds/rgb/leds-ws2812b.c
new file mode 100644
index 000000000000..ca9c5caabe48
--- /dev/null
+++ b/drivers/leds/rgb/leds-ws2812b.c
@@ -0,0 +1,231 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * WorldSemi WS2812B individually-addressable LED driver using SPI
+ *
+ * Copyright 2022 Chuanhong Guo <gch981213@gmail.com>
+ *
+ * This driver simulates WS2812B protocol using SPI MOSI pin. A one pulse
+ * is transferred as 3'b110 and a zero pulse is 3'b100. For this driver to
+ * work properly, the SPI frequency should be 2.105MHz~2.85MHz and it needs
+ * to transfer all the bytes continuously.
+ */
+
+#include <linux/led-class-multicolor.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/of_device.h>
+#include <linux/property.h>
+#include <linux/spi/spi.h>
+#include <linux/mutex.h>
+
+#define WS2812B_BYTES_PER_COLOR 3
+#define WS2812B_NUM_COLORS 3
+/* A continuous 0 for 50us+ as the 'reset' signal */
+#define WS2812B_RESET_LEN 18
+
+struct ws2812b_led {
+	struct led_classdev_mc mc_cdev;
+	struct mc_subled subled[WS2812B_NUM_COLORS];
+	int cascade;
+};
+
+struct ws2812b_priv {
+	struct led_classdev ldev;
+	struct spi_device *spi;
+	struct mutex mutex;
+	int num_leds;
+	size_t data_len;
+	u8 *data_buf;
+	struct ws2812b_led leds[];
+};
+
+/**
+ * ws2812b_set_byte - convert a byte of data to 3-byte SPI data for pulses
+ * @priv: pointer to the private data structure
+ * @offset: offset of the target byte in the data stream
+ * @val: 1-byte data to be set
+ *
+ * WS2812B receives a stream of bytes from DI, takes the first 3 byte as LED
+ * brightness and pases the rest to the next LED through the DO pin.
+ * This function assembles a single byte of data to the LED:
+ * A bit is represented with a pulse of specific length. A long pulse is a 1
+ * and a short pulse is a 0.
+ * SPI transfers data continuously, MSB first. We can send 3'b100 to create a
+ * 0 pulse and 3'b110 for a 1 pulse. In this way, a byte of data takes up 3
+ * bytes in a SPI transfer:
+ *  1x0 1x0 1x0 1x0 1x0 1x0 1x0 1x0
+ * Let's rearrange it in 8 bits:
+ *  1x01x01x 01x01x01 x01x01x0
+ * The higher 3 bits, middle 2 bits and lower 3 bits are represented with the
+ * 1st, 2nd and 3rd byte in the SPI transfer respectively.
+ * There are only 8 combinations for 3 bits and 4 for 2 bits, so we can create
+ * a lookup table for the 3 bytes.
+ * e.g. For 0x6b -> 2'b01101011:
+ *  Bit 7-5: 3'b011 -> 10011011 -> 0x9b
+ *  Bit 4-3: 2'b01  -> 01001101 -> 0x4d
+ *  Bit 2-0: 3'b011 -> 00110110 -> 0x36
+ */
+static void ws2812b_set_byte(struct ws2812b_priv *priv, size_t offset, u8 val)
+{
+	/* The lookup table for Bit 7-5 4-3 2-0 */
+	const u8 h3b[] = { 0x92, 0x93, 0x9a, 0x9b, 0xd2, 0xd3, 0xda, 0xdb };
+	const u8 m2b[] = { 0x49, 0x4d, 0x69, 0x6d };
+	const u8 l3b[] = { 0x24, 0x26, 0x34, 0x36, 0xa4, 0xa6, 0xb4, 0xb6 };
+	u8 *p = priv->data_buf + WS2812B_RESET_LEN + (offset * WS2812B_BYTES_PER_COLOR);
+
+	p[0] = h3b[val >> 5]; /* Bit 7-5 */
+	p[1] = m2b[(val >> 3) & 0x3]; /* Bit 4-3 */
+	p[2] = l3b[val & 0x7]; /* Bit 2-0 */
+}
+
+static int ws2812b_set(struct led_classdev *cdev,
+		       enum led_brightness brightness)
+{
+	struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev);
+	struct ws2812b_led *led =
+		container_of(mc_cdev, struct ws2812b_led, mc_cdev);
+	struct ws2812b_priv *priv = dev_get_drvdata(cdev->dev->parent);
+	int ret;
+	int i;
+
+	led_mc_calc_color_components(mc_cdev, brightness);
+
+	mutex_lock(&priv->mutex);
+	for (i = 0; i < WS2812B_NUM_COLORS; i++)
+		ws2812b_set_byte(priv, led->cascade * WS2812B_NUM_COLORS + i,
+				 led->subled[i].brightness);
+	ret = spi_write(priv->spi, priv->data_buf, priv->data_len);
+	mutex_unlock(&priv->mutex);
+
+	return ret;
+}
+
+static int ws2812b_probe(struct spi_device *spi)
+{
+	struct device *dev = &spi->dev;
+	int cur_led = 0;
+	struct ws2812b_priv *priv;
+	struct fwnode_handle *led_node;
+	int num_leds, i, cnt, ret;
+
+	num_leds = device_get_child_node_count(dev);
+
+	priv = devm_kzalloc(dev, struct_size(priv, leds, num_leds), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+	priv->data_len =
+		num_leds * WS2812B_BYTES_PER_COLOR * WS2812B_NUM_COLORS +
+		WS2812B_RESET_LEN;
+	priv->data_buf = kzalloc(priv->data_len, GFP_KERNEL);
+	if (!priv->data_buf)
+		return -ENOMEM;
+
+	for (i = 0; i < num_leds * WS2812B_NUM_COLORS; i++)
+		ws2812b_set_byte(priv, i, 0);
+
+	mutex_init(&priv->mutex);
+	priv->num_leds = num_leds;
+	priv->spi = spi;
+
+	device_for_each_child_node(dev, led_node) {
+		struct led_init_data init_data = {
+			.fwnode = led_node,
+		};
+		/* WS2812B LEDs usually come with GRB color */
+		u32 color_idx[WS2812B_NUM_COLORS] = {
+			LED_COLOR_ID_GREEN,
+			LED_COLOR_ID_RED,
+			LED_COLOR_ID_BLUE,
+		};
+		u32 cascade;
+
+		ret = fwnode_property_read_u32(led_node, "reg", &cascade);
+		if (ret) {
+			dev_err(dev, "failed to obtain numerical LED index for %s",
+				fwnode_get_name(led_node));
+			goto ERR_UNREG_LEDS;
+		}
+		if (cascade >= num_leds) {
+			dev_err(dev, "LED index of %s is larger than the number of LEDs.",
+				fwnode_get_name(led_node));
+			ret = -EINVAL;
+			goto ERR_UNREG_LEDS;
+		}
+
+		cnt = fwnode_property_count_u32(led_node, "color-index");
+		if (cnt > 0 && cnt <= WS2812B_NUM_COLORS)
+			fwnode_property_read_u32_array(led_node, "color-index",
+						       color_idx, (size_t)cnt);
+
+		priv->leds[cur_led].mc_cdev.subled_info =
+			priv->leds[cur_led].subled;
+		priv->leds[cur_led].mc_cdev.num_colors = WS2812B_NUM_COLORS;
+		priv->leds[cur_led].mc_cdev.led_cdev.max_brightness = 255;
+		priv->leds[cur_led].mc_cdev.led_cdev.brightness_set_blocking = ws2812b_set;
+
+		for (i = 0; i < WS2812B_NUM_COLORS; i++) {
+			priv->leds[cur_led].subled[i].color_index = color_idx[i];
+			priv->leds[cur_led].subled[i].intensity = 255;
+		}
+
+		priv->leds[cur_led].cascade = cascade;
+
+		ret = led_classdev_multicolor_register_ext(
+			dev, &priv->leds[cur_led].mc_cdev, &init_data);
+		if (ret) {
+			dev_err(dev, "registration of %s failed.",
+				fwnode_get_name(led_node));
+			goto ERR_UNREG_LEDS;
+		}
+		cur_led++;
+	}
+
+	spi_set_drvdata(spi, priv);
+
+	return 0;
+ERR_UNREG_LEDS:
+	for (; cur_led >= 0; cur_led--)
+		led_classdev_multicolor_unregister(&priv->leds[cur_led].mc_cdev);
+	mutex_destroy(&priv->mutex);
+	kfree(priv->data_buf);
+	return ret;
+}
+
+static void ws2812b_remove(struct spi_device *spi)
+{
+	struct ws2812b_priv *priv = spi_get_drvdata(spi);
+	int cur_led;
+
+	for (cur_led = priv->num_leds - 1; cur_led >= 0; cur_led--)
+		led_classdev_multicolor_unregister(&priv->leds[cur_led].mc_cdev);
+	kfree(priv->data_buf);
+	mutex_destroy(&priv->mutex);
+}
+
+static const struct spi_device_id ws2812b_spi_ids[] = {
+	{ "ws2812b" },
+	{},
+};
+MODULE_DEVICE_TABLE(spi, ws2812b_spi_ids);
+
+static const struct of_device_id ws2812b_dt_ids[] = {
+	{ .compatible = "worldsemi,ws2812b" },
+	{},
+};
+MODULE_DEVICE_TABLE(of, ws2812b_dt_ids);
+
+static struct spi_driver ws2812b_driver = {
+	.probe		= ws2812b_probe,
+	.remove		= ws2812b_remove,
+	.id_table	= ws2812b_spi_ids,
+	.driver = {
+		.name		= KBUILD_MODNAME,
+		.of_match_table	= ws2812b_dt_ids,
+	},
+};
+
+module_spi_driver(ws2812b_driver);
+
+MODULE_AUTHOR("Chuanhong Guo <gch981213@gmail.com>");
+MODULE_DESCRIPTION("WS2812B LED driver using SPI");
+MODULE_LICENSE("GPL");
-- 
2.38.1


      parent reply	other threads:[~2022-12-12  4:57 UTC|newest]

Thread overview: 17+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-12-12  4:55 [PATCH v4 0/3] leds: add driver for SPI driven WorldSemi WS2812B RGB LEDs Chuanhong Guo
2022-12-12  4:55 ` [PATCH v4 1/3] dt-bindings: vendor-prefixes: add an entry for WorldSemi Chuanhong Guo
2022-12-12  4:55 ` [PATCH v4 2/3] dt-bindings: leds: add worldsemi,ws2812b Chuanhong Guo
2022-12-12  8:56   ` Krzysztof Kozlowski
2022-12-23 12:48     ` Lee Jones
2022-12-23 17:19     ` Pavel Machek
2022-12-24  5:52       ` Chuanhong Guo
2022-12-24 12:53       ` Krzysztof Kozlowski
2023-01-09 16:52         ` Lee Jones
2023-01-10  9:24           ` Krzysztof Kozlowski
2023-01-10 10:21             ` Lee Jones
2023-01-11 18:53               ` Chuanhong Guo
2023-01-12  9:16                 ` Krzysztof Kozlowski
2023-01-13 14:56                 ` Lee Jones
2023-01-14 12:29                   ` Chuanhong Guo
2023-01-19 14:46                     ` Lee Jones
2022-12-12  4:55 ` Chuanhong Guo [this message]

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=20221212045558.69602-4-gch981213@gmail.com \
    --to=gch981213@gmail.com \
    --cc=andersson@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=johan+linaro@kernel.org \
    --cc=krzysztof.kozlowski+dt@linaro.org \
    --cc=linus.walleij@linaro.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-leds@vger.kernel.org \
    --cc=marijn.suijten@somainline.org \
    --cc=pavel@ucw.cz \
    --cc=robh+dt@kernel.org \
    --cc=shawnguo@kernel.org \
    --cc=stano.jakubek@gmail.com \
    --cc=sven.schwermer@disruptive-technologies.com \
    /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.