Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions Documentation/devicetree/bindings/leds/worldsemi,ws2812b.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
%YAML 1.2
---
$id: http://devicetree.org/schemas/leds/worldsemi,ws2812b.yaml#
$schema: http://devicetree.org/meta-schemas/core.yaml#

title: WS2812B LEDs driven using SPI

maintainers:
- Chuanhong Guo <gch981213@gmail.com>

description: |
WorldSemi WS2812B is a individually addressable LED chip that can be chained
together and controlled individually using a single wire.
This binding describes a chain of WS2812B LEDs connected to the SPI MOSI pin.
Typical setups includes connecting the data pin of the LED chain to MOSI as
the only device or using CS and MOSI with a tri-state voltage-level shifter
for the data pin.
The SPI frequency needs to be 2.105MHz~2.85MHz for the timing to be correct
and the controller needs to send all the bytes continuously.

allOf:
- $ref: /schemas/spi/spi-peripheral-props.yaml#

properties:
compatible:
const: worldsemi,ws2812b

reg:
maxItems: 1

spi-max-frequency:
minimum: 2105000
maximum: 2850000

"#address-cells":
const: 1

"#size-cells":
const: 0

patternProperties:
"^multi-led@[0-9a-f]+$":
type: object
$ref: leds-class-multicolor.yaml#
unevaluatedProperties: false

properties:
color-index:
description: |
A 3-item array specifying color of each components in this LED. It
should be one of the LED_COLOR_ID_* prefixed definitions from the
header include/dt-bindings/leds/common.h. Defaults to
<LED_COLOR_ID_GREEN LED_COLOR_ID_RED LED_COLOR_ID_BLUE>
if unspecified.
$ref: /schemas/types.yaml#/definitions/uint32-array
maxItems: 3

reg:
description: |
Which LED this node represents. The reg of the first LED on the chain
is 0.
maxItems: 1

required:
- reg
- color
- function

required:
- compatible

additionalProperties: false

examples:
- |
#include <dt-bindings/leds/common.h>
spi {
#address-cells = <1>;
#size-cells = <0>;

led-controller@0 {
compatible = "worldsemi,ws2812b";
reg = <0>;
#address-cells = <1>;
#size-cells = <0>;
spi-max-frequency = <2850000>;
multi-led@0 {
reg = <0>;
color-index = <LED_COLOR_ID_RED LED_COLOR_ID_GREEN LED_COLOR_ID_BLUE>;
color = <LED_COLOR_ID_RGB>;
function = LED_FUNCTION_STATUS;
function-enumerator = <0>;
};
multi-led@1 {
reg = <1>;
color = <LED_COLOR_ID_RGB>;
function = LED_FUNCTION_STATUS;
function-enumerator = <1>;
};
multi-led@2 {
reg = <2>;
color = <LED_COLOR_ID_RGB>;
function = LED_FUNCTION_STATUS;
function-enumerator = <2>;
};
multi-led@3 {
reg = <3>;
color = <LED_COLOR_ID_RGB>;
function = LED_FUNCTION_STATUS;
function-enumerator = <3>;
};
};
};

...
2 changes: 2 additions & 0 deletions Documentation/devicetree/bindings/vendor-prefixes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1845,6 +1845,8 @@ patternProperties:
description: Wondermedia Technologies, Inc.
"^wobo,.*":
description: Wobo
"^worldsemi,.*":
description: WorldSemi Co., Limited
"^wolfvision,.*":
description: WolfVision GmbH
"^x-powers,.*":
Expand Down
11 changes: 11 additions & 0 deletions drivers/leds/rgb/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,15 @@ config LEDS_MT6370_RGB
This driver can also be built as a module. If so, the module
will be called "leds-mt6370-rgb".

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
1 change: 1 addition & 0 deletions drivers/leds/rgb/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ obj-$(CONFIG_LEDS_NCP5623) += leds-ncp5623.o
obj-$(CONFIG_LEDS_PWM_MULTICOLOR) += leds-pwm-multicolor.o
obj-$(CONFIG_LEDS_QCOM_LPG) += leds-qcom-lpg.o
obj-$(CONFIG_LEDS_MT6370_RGB) += leds-mt6370-rgb.o
obj-$(CONFIG_LEDS_WS2812B) += leds-ws2812b.o
231 changes: 231 additions & 0 deletions drivers/leds/rgb/leds-ws2812b.c
Original file line number Diff line number Diff line change
@@ -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");