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@xxxxxxxxxxxx> --- .../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@xxxxxxx> + +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@xxxxxxx> + * + * Inspired by (C) Martin Sperl <kernel@xxxxxxxxxxxxxxxx> + * + */ +#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@xxxxxxx>"); +MODULE_DESCRIPTION("WS2812B LED driver powered by SPI"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("spi:ws2812b"); -- 2.25.1