aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLeon Rinkel <[email protected]>2023-11-01 19:53:13 +0100
committerLeon Rinkel <[email protected]>2023-11-01 19:53:13 +0100
commit9dc5b296672299eadbbc631a84df122390bfd386 (patch)
treebb7ad6eb2780fad6ee44dc70c88db184cd8df344
parent7b00a083dcceebb4c7a4efe023716498888e8ae3 (diff)
Add ws2812 driver clone with RGB to RGBW conversion
-rw-r--r--app/prj.conf2
-rw-r--r--drivers/CMakeLists.txt2
-rw-r--r--drivers/Kconfig5
-rw-r--r--drivers/ws2812/CMakeLists.txt7
-rw-r--r--drivers/ws2812/Kconfig50
-rw-r--r--drivers/ws2812/ws2812_gpio.c310
-rw-r--r--drivers/ws2812/ws2812_i2s.c317
-rw-r--r--drivers/ws2812/ws2812_spi.c300
8 files changed, 992 insertions, 1 deletions
diff --git a/app/prj.conf b/app/prj.conf
index cb8cd82..7c6a8c8 100644
--- a/app/prj.conf
+++ b/app/prj.conf
@@ -6,7 +6,7 @@
CONFIG_SPI=y
CONFIG_LED_STRIP=y
-CONFIG_WS2812_STRIP=y
+CONFIG_LUMEN_WS2812_STRIP=y
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
diff --git a/drivers/CMakeLists.txt b/drivers/CMakeLists.txt
index 919c7a5..809d705 100644
--- a/drivers/CMakeLists.txt
+++ b/drivers/CMakeLists.txt
@@ -1,3 +1,5 @@
# Copyright (c) 2021 Nordic Semiconductor ASA
# Copyright (c) 2023 Leon Rinkel
# SPDX-License-Identifier: Apache-2.0
+
+add_subdirectory_ifdef(CONFIG_LUMEN_WS2812_STRIP ws2812)
diff --git a/drivers/Kconfig b/drivers/Kconfig
index cd16edb..857009a 100644
--- a/drivers/Kconfig
+++ b/drivers/Kconfig
@@ -3,4 +3,9 @@
# SPDX-License-Identifier: Apache-2.0
menu "Drivers"
+
+if LED_STRIP
+rsource "ws2812/Kconfig"
+endif # LED_STRIP
+
endmenu
diff --git a/drivers/ws2812/CMakeLists.txt b/drivers/ws2812/CMakeLists.txt
new file mode 100644
index 0000000..dfafd93
--- /dev/null
+++ b/drivers/ws2812/CMakeLists.txt
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: Apache-2.0
+
+zephyr_library()
+
+zephyr_library_sources_ifdef(CONFIG_LUMEN_WS2812_STRIP_GPIO ws2812_gpio.c)
+zephyr_library_sources_ifdef(CONFIG_LUMEN_WS2812_STRIP_SPI ws2812_spi.c)
+zephyr_library_sources_ifdef(CONFIG_LUMEN_WS2812_STRIP_I2S ws2812_i2s.c)
diff --git a/drivers/ws2812/Kconfig b/drivers/ws2812/Kconfig
new file mode 100644
index 0000000..11a6c9e
--- /dev/null
+++ b/drivers/ws2812/Kconfig
@@ -0,0 +1,50 @@
+# Copyright (c) 2017 Linaro Limited
+# Copyright (c) 2019 Nordic Semiconductor ASA
+# Copyright (c) 2023 Leon Rinkel
+#
+# SPDX-License-Identifier: Apache-2.0
+
+# The following blog post is an excellent resource about pulse timing:
+#
+# https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/
+
+menuconfig LUMEN_WS2812_STRIP
+ bool "WS2812 (and compatible) LED strip driver"
+ select LED_STRIP_RGB_SCRATCH
+ help
+ Enable LED strip driver for daisy chains of WS2812-ish (or WS2812B,
+ WS2813, SK6812, Everlight B1414, or compatible) devices.
+
+choice LUMEN_WS2812_STRIP_DRIVER
+ prompt "Driver backend"
+ default LUMEN_WS2812_STRIP_SPI
+ depends on LUMEN_WS2812_STRIP
+
+config LUMEN_WS2812_STRIP_SPI
+ bool "SPI driver"
+ depends on SPI
+ help
+ The SPI driver is portable, but requires significantly more
+ memory (1 byte of overhead per bit of pixel data).
+
+config LUMEN_WS2812_STRIP_I2S
+ bool "I2S driver"
+ depends on I2S
+ help
+ Uses the I2S peripheral, memory usage is 4 bytes per color,
+ times the number of pixels. A few more for the start and end
+ delay. The reset delay has a coarse resolution of ~20us.
+
+config LUMEN_WS2812_STRIP_GPIO
+ bool "GPIO driver"
+ # Only an Cortex-M0 inline assembly implementation for the nRF51
+ # is supported currently.
+ depends on SOC_SERIES_NRF51X
+ help
+ The GPIO driver does bit-banging with inline assembly,
+ and is not available on all SoCs.
+
+ Note that this driver is not compatible with the Everlight B1414
+ controller.
+
+endchoice
diff --git a/drivers/ws2812/ws2812_gpio.c b/drivers/ws2812/ws2812_gpio.c
new file mode 100644
index 0000000..d5837d4
--- /dev/null
+++ b/drivers/ws2812/ws2812_gpio.c
@@ -0,0 +1,310 @@
+/*
+ * Copyright (c) 2018 Intel Corporation
+ * Copyright (c) 2019 Nordic Semiconductor ASA
+ * Copyright (c) 2021 Seagate Technology LLC
+ * Copyright (c) 2023 Leon Rinkel
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#define DT_DRV_COMPAT worldsemi_ws2812_gpio
+
+#include <zephyr/drivers/led_strip.h>
+
+#include <string.h>
+#include <math.h>
+
+#define LOG_LEVEL CONFIG_LED_STRIP_LOG_LEVEL
+#include <zephyr/logging/log.h>
+LOG_MODULE_REGISTER(ws2812_gpio);
+
+#include <zephyr/kernel.h>
+#include <soc.h>
+#include <zephyr/drivers/gpio.h>
+#include <zephyr/device.h>
+#include <zephyr/drivers/clock_control.h>
+#include <zephyr/drivers/clock_control/nrf_clock_control.h>
+#include <zephyr/dt-bindings/led/led.h>
+
+struct ws2812_gpio_cfg {
+ struct gpio_dt_spec in_gpio;
+ uint8_t num_colors;
+ const uint8_t *color_mapping;
+};
+
+/*
+ * This is hard-coded to nRF51 in two ways:
+ *
+ * 1. The assembly delays T1H, T0H, TxL
+ * 2. GPIO set/clear
+ */
+
+/*
+ * T1H: 1 bit high pulse delay: 12 cycles == .75 usec
+ * T0H: 0 bit high pulse delay: 4 cycles == .25 usec
+ * TxL: inter-bit low pulse delay: 8 cycles == .5 usec
+ *
+ * We can't use k_busy_wait() here: its argument is in microseconds,
+ * and we need roughly .05 microsecond resolution.
+ */
+#define DELAY_T1H "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
+#define DELAY_T0H "nop\nnop\nnop\nnop\n"
+#define DELAY_TxL "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n"
+
+/*
+ * GPIO set/clear (these make assumptions about assembly details
+ * below).
+ *
+ * This uses OUTCLR == OUTSET+4.
+ *
+ * We should be able to make this portable using the results of
+ * https://github.com/zephyrproject-rtos/zephyr/issues/11917.
+ *
+ * We already have the GPIO device stashed in ws2812_gpio_config, so
+ * this driver can be used as a test case for the optimized API.
+ *
+ * Per Arm docs, both Rd and Rn must be r0-r7, so we use the "l"
+ * constraint in the below assembly.
+ */
+#define SET_HIGH "str %[p], [%[r], #0]\n" /* OUTSET = BIT(LED_PIN) */
+#define SET_LOW "str %[p], [%[r], #4]\n" /* OUTCLR = BIT(LED_PIN) */
+
+/* Send out a 1 bit's pulse */
+#define ONE_BIT(base, pin) do { \
+ __asm volatile (SET_HIGH \
+ DELAY_T1H \
+ SET_LOW \
+ DELAY_TxL \
+ :: \
+ [r] "l" (base), \
+ [p] "l" (pin)); } while (false)
+
+/* Send out a 0 bit's pulse */
+#define ZERO_BIT(base, pin) do { \
+ __asm volatile (SET_HIGH \
+ DELAY_T0H \
+ SET_LOW \
+ DELAY_TxL \
+ :: \
+ [r] "l" (base), \
+ [p] "l" (pin)); } while (false)
+
+static int send_buf(const struct device *dev, uint8_t *buf, size_t len)
+{
+ const struct ws2812_gpio_cfg *config = dev->config;
+ volatile uint32_t *base = (uint32_t *)&NRF_GPIO->OUTSET;
+ const uint32_t val = BIT(config->in_gpio.pin);
+ struct onoff_manager *mgr =
+ z_nrf_clock_control_get_onoff(CLOCK_CONTROL_NRF_SUBSYS_HF);
+ struct onoff_client cli;
+ unsigned int key;
+ int rc;
+
+ sys_notify_init_spinwait(&cli.notify);
+ rc = onoff_request(mgr, &cli);
+ if (rc < 0) {
+ return rc;
+ }
+
+ while (sys_notify_fetch_result(&cli.notify, &rc)) {
+ /* pend until clock is up and running */
+ }
+
+ key = irq_lock();
+
+ while (len--) {
+ uint32_t b = *buf++;
+ int32_t i;
+
+ /*
+ * Generate signal out of the bits, MSbit first.
+ *
+ * Accumulator maintenance and branching mean the
+ * inter-bit time will be longer than TxL, but the
+ * wp.josh.com blog post says we have at least 5 usec
+ * of slack time between bits before we risk the
+ * signal getting latched, so this will be fine as
+ * long as the compiler does something minimally
+ * reasonable.
+ */
+ for (i = 7; i >= 0; i--) {
+ if (b & BIT(i)) {
+ ONE_BIT(base, val);
+ } else {
+ ZERO_BIT(base, val);
+ }
+ }
+ }
+
+ irq_unlock(key);
+
+ rc = onoff_release(mgr);
+ /* Returns non-negative value on success. Cap to 0 as API states. */
+ rc = MIN(rc, 0);
+
+ return rc;
+}
+
+/** RGB to RGBW conversion according to Wang et al. */
+void do_rgbw_conversion(
+ uint8_t* ro, uint8_t* go, uint8_t* bo, uint8_t* wo,
+ uint8_t ri, uint8_t gi, uint8_t bi,
+ uint8_t algo
+)
+{
+ float m = fmin(ri, fmin(gi, bi));
+ float M = fmax(ri, fmax(gi, bi));
+
+ float w;
+ switch (algo)
+ {
+ case 1:
+ w = m;
+ break;
+ case 2:
+ w = pow(m, 2);
+ break;
+ case 3:
+ w = -pow(m, 3) + pow(m, 2) + m;
+ break;
+ case 4:
+ w = (m / M >= 0.5) ? M :
+ (m * M) / (M - m);
+ break;
+
+ default:
+ return;
+ }
+
+ float k = (w + M) / M;
+
+ float r = k * ri - w;
+ float g = k * gi - w;
+ float b = k * bi - w;
+
+ *wo = fmax(fmin(floor(w), 255), 0);
+ *ro = fmax(fmin(floor(r), 255), 0);
+ *go = fmax(fmin(floor(g), 255), 0);
+ *bo = fmax(fmin(floor(b), 255), 0);
+}
+
+static int ws2812_gpio_update_rgb(const struct device *dev,
+ struct led_rgb *pixels,
+ size_t num_pixels)
+{
+ const struct ws2812_gpio_cfg *config = dev->config;
+ uint8_t *ptr = (uint8_t *)pixels;
+ size_t i;
+
+ /* Convert from RGB to on-wire format (e.g. GRB, GRBW, RGB, etc) */
+ for (i = 0; i < num_pixels; i++) {
+ uint8_t j;
+
+ uint8_t ro, go, bo, wo;
+ do_rgbw_conversion(
+ &ro, &go, &bo, &wo,
+ pixels[i].r, pixels[i].g, pixels[i].b,
+ 1
+ );
+
+ for (j = 0; j < config->num_colors; j++) {
+ switch (config->color_mapping[j]) {
+ /* White channel is not supported by LED strip API. */
+ case LED_COLOR_ID_WHITE:
+ *ptr++ = wo;
+ break;
+ case LED_COLOR_ID_RED:
+ *ptr++ = ro;
+ break;
+ case LED_COLOR_ID_GREEN:
+ *ptr++ = go;
+ break;
+ case LED_COLOR_ID_BLUE:
+ *ptr++ = bo;
+ break;
+ default:
+ return -EINVAL;
+ }
+ }
+ }
+
+ return send_buf(dev, (uint8_t *)pixels, num_pixels * config->num_colors);
+}
+
+static int ws2812_gpio_update_channels(const struct device *dev,
+ uint8_t *channels,
+ size_t num_channels)
+{
+ LOG_ERR("update_channels not implemented");
+ return -ENOTSUP;
+}
+
+static const struct led_strip_driver_api ws2812_gpio_api = {
+ .update_rgb = ws2812_gpio_update_rgb,
+ .update_channels = ws2812_gpio_update_channels,
+};
+
+/*
+ * Retrieve the channel to color mapping (e.g. RGB, BGR, GRB, ...) from the
+ * "color-mapping" DT property.
+ */
+#define WS2812_COLOR_MAPPING(idx) \
+static const uint8_t ws2812_gpio_##idx##_color_mapping[] = \
+ DT_INST_PROP(idx, color_mapping)
+
+#define WS2812_NUM_COLORS(idx) (DT_INST_PROP_LEN(idx, color_mapping))
+
+/*
+ * The inline assembly above is designed to work on nRF51 devices with
+ * the 16 MHz clock enabled.
+ *
+ * TODO: try to make this portable, or at least port to more devices.
+ */
+
+#define WS2812_GPIO_DEVICE(idx) \
+ \
+ static int ws2812_gpio_##idx##_init(const struct device *dev) \
+ { \
+ const struct ws2812_gpio_cfg *cfg = dev->config; \
+ uint8_t i; \
+ \
+ if (!gpio_is_ready_dt(&cfg->in_gpio)) { \
+ LOG_ERR("GPIO device not ready"); \
+ return -ENODEV; \
+ } \
+ \
+ for (i = 0; i < cfg->num_colors; i++) { \
+ switch (cfg->color_mapping[i]) { \
+ case LED_COLOR_ID_WHITE: \
+ case LED_COLOR_ID_RED: \
+ case LED_COLOR_ID_GREEN: \
+ case LED_COLOR_ID_BLUE: \
+ break; \
+ default: \
+ LOG_ERR("%s: invalid channel to color mapping." \
+ " Check the color-mapping DT property", \
+ dev->name); \
+ return -EINVAL; \
+ } \
+ } \
+ \
+ return gpio_pin_configure_dt(&cfg->in_gpio, GPIO_OUTPUT); \
+ } \
+ \
+ WS2812_COLOR_MAPPING(idx); \
+ \
+ static const struct ws2812_gpio_cfg ws2812_gpio_##idx##_cfg = { \
+ .in_gpio = GPIO_DT_SPEC_INST_GET(idx, in_gpios), \
+ .num_colors = WS2812_NUM_COLORS(idx), \
+ .color_mapping = ws2812_gpio_##idx##_color_mapping, \
+ }; \
+ \
+ DEVICE_DT_INST_DEFINE(idx, \
+ ws2812_gpio_##idx##_init, \
+ NULL, \
+ NULL, \
+ &ws2812_gpio_##idx##_cfg, POST_KERNEL, \
+ CONFIG_LED_STRIP_INIT_PRIORITY, \
+ &ws2812_gpio_api);
+
+DT_INST_FOREACH_STATUS_OKAY(WS2812_GPIO_DEVICE)
diff --git a/drivers/ws2812/ws2812_i2s.c b/drivers/ws2812/ws2812_i2s.c
new file mode 100644
index 0000000..25d33f8
--- /dev/null
+++ b/drivers/ws2812/ws2812_i2s.c
@@ -0,0 +1,317 @@
+/*
+ * Copyright (c) 2022 Jonathan Rico
+ * Copyright (c) 2023 Leon Rinkel
+ *
+ * Adapted from the SPI driver, using the procedure in this blog post:
+ * https://electronut.in/nrf52-i2s-ws2812/
+ *
+ * Note: the word "word" refers to a 32-bit integer unless otherwise stated.
+ *
+ * WS/LRCK frequency:
+ * This refers to the "I2S word or channel select" clock.
+ * The I2C peripheral sends two 16-bit channel values for each clock period.
+ * A single LED color (8 data bits) will take up one 32-bit word or one LRCK
+ * period. This means a standard RGB led will take 3 LRCK periods to transmit.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#define DT_DRV_COMPAT worldsemi_ws2812_i2s
+
+#include <string.h>
+#include <math.h>
+
+#include <zephyr/drivers/led_strip.h>
+
+#define LOG_LEVEL CONFIG_LED_STRIP_LOG_LEVEL
+#include <zephyr/logging/log.h>
+LOG_MODULE_REGISTER(ws2812_i2s);
+
+#include <zephyr/device.h>
+#include <zephyr/drivers/i2s.h>
+#include <zephyr/dt-bindings/led/led.h>
+#include <zephyr/kernel.h>
+#include <zephyr/sys/util.h>
+
+#define WS2812_I2S_PRE_DELAY_WORDS 1
+
+struct ws2812_i2s_cfg {
+ struct device const *dev;
+ size_t tx_buf_bytes;
+ struct k_mem_slab *mem_slab;
+ uint8_t num_colors;
+ const uint8_t *color_mapping;
+ uint16_t reset_words;
+ uint32_t lrck_period;
+ uint32_t extra_wait_time_us;
+ bool active_low;
+ uint8_t nibble_one;
+ uint8_t nibble_zero;
+};
+
+/* Serialize an 8-bit color channel value into two 16-bit I2S values (or 1 32-bit
+ * word).
+ */
+static inline void ws2812_i2s_ser(uint32_t *word, uint8_t color, const uint8_t sym_one,
+ const uint8_t sym_zero)
+{
+ *word = 0;
+ for (uint16_t i = 0; i < 8; i++) {
+ if ((1 << i) & color) {
+ *word |= sym_one << (i * 4);
+ } else {
+ *word |= sym_zero << (i * 4);
+ }
+ }
+
+ /* Swap the two I2S values due to the (audio) channel TX order. */
+ *word = (*word >> 16) | (*word << 16);
+}
+
+/** RGB to RGBW conversion according to Wang et al. */
+void do_rgbw_conversion(
+ uint8_t* ro, uint8_t* go, uint8_t* bo, uint8_t* wo,
+ uint8_t ri, uint8_t gi, uint8_t bi,
+ uint8_t algo
+)
+{
+ float m = fmin(ri, fmin(gi, bi));
+ float M = fmax(ri, fmax(gi, bi));
+
+ float w;
+ switch (algo)
+ {
+ case 1:
+ w = m;
+ break;
+ case 2:
+ w = pow(m, 2);
+ break;
+ case 3:
+ w = -pow(m, 3) + pow(m, 2) + m;
+ break;
+ case 4:
+ w = (m / M >= 0.5) ? M :
+ (m * M) / (M - m);
+ break;
+
+ default:
+ return;
+ }
+
+ float k = (w + M) / M;
+
+ float r = k * ri - w;
+ float g = k * gi - w;
+ float b = k * bi - w;
+
+ *wo = fmax(fmin(floor(w), 255), 0);
+ *ro = fmax(fmin(floor(r), 255), 0);
+ *go = fmax(fmin(floor(g), 255), 0);
+ *bo = fmax(fmin(floor(b), 255), 0);
+}
+
+static int ws2812_strip_update_rgb(const struct device *dev, struct led_rgb *pixels,
+ size_t num_pixels)
+{
+ const struct ws2812_i2s_cfg *cfg = dev->config;
+ uint8_t sym_one, sym_zero;
+ uint32_t reset_word;
+ uint32_t *tx_buf;
+ uint32_t flush_time_us;
+ void *mem_block;
+ int ret;
+
+ if (cfg->active_low) {
+ sym_one = (~cfg->nibble_one) & 0x0F;
+ sym_zero = (~cfg->nibble_zero) & 0x0F;
+ reset_word = 0xFFFFFFFF;
+ } else {
+ sym_one = cfg->nibble_one & 0x0F;
+ sym_zero = cfg->nibble_zero & 0x0F;
+ reset_word = 0;
+ }
+
+ /* Acquire memory for the I2S payload. */
+ ret = k_mem_slab_alloc(cfg->mem_slab, &mem_block, K_SECONDS(10));
+ if (ret < 0) {
+ LOG_ERR("Unable to allocate mem slab for TX (err %d)", ret);
+ return -ENOMEM;
+ }
+ tx_buf = (uint32_t *)mem_block;
+
+ /* Add a pre-data reset, so the first pixel isn't skipped by the strip. */
+ for (uint16_t i = 0; i < WS2812_I2S_PRE_DELAY_WORDS; i++) {
+ *tx_buf = reset_word;
+ tx_buf++;
+ }
+
+ /*
+ * Convert pixel data into I2S frames. Each frame has pixel data
+ * in color mapping on-wire format (e.g. GRB, GRBW, RGB, etc).
+ */
+ for (uint16_t i = 0; i < num_pixels; i++) {
+ uint8_t ro, go, bo, wo;
+ do_rgbw_conversion(
+ &ro, &go, &bo, &wo,
+ pixels[i].r, pixels[i].g, pixels[i].b,
+ 1
+ );
+
+ for (uint16_t j = 0; j < cfg->num_colors; j++) {
+ uint8_t pixel;
+
+ switch (cfg->color_mapping[j]) {
+ /* White channel is not supported by LED strip API. */
+ case LED_COLOR_ID_WHITE:
+ pixel = wo;
+ break;
+ case LED_COLOR_ID_RED:
+ pixel = ro;
+ break;
+ case LED_COLOR_ID_GREEN:
+ pixel = go;
+ break;
+ case LED_COLOR_ID_BLUE:
+ pixel = bo;
+ break;
+ default:
+ return -EINVAL;
+ }
+ ws2812_i2s_ser(tx_buf, pixel, sym_one, sym_zero);
+ tx_buf++;
+ }
+ }
+
+ for (uint16_t i = 0; i < cfg->reset_words; i++) {
+ *tx_buf = reset_word;
+ tx_buf++;
+ }
+
+ /* Flush the buffer on the wire. */
+ ret = i2s_write(cfg->dev, mem_block, cfg->tx_buf_bytes);
+ if (ret < 0) {
+ k_mem_slab_free(cfg->mem_slab, mem_block);
+ LOG_ERR("Failed to write data: %d", ret);
+ return ret;
+ }
+
+ ret = i2s_trigger(cfg->dev, I2S_DIR_TX, I2S_TRIGGER_START);
+ if (ret < 0) {
+ LOG_ERR("Failed to trigger command %d on TX: %d", I2S_TRIGGER_START, ret);
+ return ret;
+ }
+
+ ret = i2s_trigger(cfg->dev, I2S_DIR_TX, I2S_TRIGGER_DRAIN);
+ if (ret < 0) {
+ LOG_ERR("Failed to trigger command %d on TX: %d", I2S_TRIGGER_DRAIN, ret);
+ return ret;
+ }
+
+ /* Wait until transaction is over */
+ flush_time_us = cfg->lrck_period * cfg->tx_buf_bytes / sizeof(uint32_t);
+ k_usleep(flush_time_us + cfg->extra_wait_time_us);
+
+ return ret;
+}
+
+static int ws2812_strip_update_channels(const struct device *dev, uint8_t *channels,
+ size_t num_channels)
+{
+ LOG_ERR("update_channels not implemented");
+ return -ENOTSUP;
+}
+
+static int ws2812_i2s_init(const struct device *dev)
+{
+ const struct ws2812_i2s_cfg *cfg = dev->config;
+ struct i2s_config config;
+ uint32_t lrck_hz;
+ int ret;
+
+ lrck_hz = USEC_PER_SEC / cfg->lrck_period;
+ LOG_DBG("Word clock: freq %u Hz period %u us",
+ lrck_hz, cfg->lrck_period);
+
+ /* 16-bit stereo, 100kHz LCLK */
+ config.word_size = 16;
+ config.channels = 2;
+ config.format = I2S_FMT_DATA_FORMAT_I2S;
+ config.options = I2S_OPT_BIT_CLK_MASTER | I2S_OPT_FRAME_CLK_MASTER;
+ config.frame_clk_freq = lrck_hz; /* WS (or LRCK) */
+ config.mem_slab = cfg->mem_slab;
+ config.block_size = cfg->tx_buf_bytes;
+ config.timeout = 1000;
+
+ ret = i2s_configure(cfg->dev, I2S_DIR_TX, &config);
+ if (ret < 0) {
+ LOG_ERR("Failed to configure I2S device: %d\n", ret);
+ return ret;
+ }
+
+ for (uint16_t i = 0; i < cfg->num_colors; i++) {
+ switch (cfg->color_mapping[i]) {
+ case LED_COLOR_ID_WHITE:
+ case LED_COLOR_ID_RED:
+ case LED_COLOR_ID_GREEN:
+ case LED_COLOR_ID_BLUE:
+ break;
+ default:
+ LOG_ERR("%s: invalid channel to color mapping."
+ "Check the color-mapping DT property",
+ dev->name);
+ return -EINVAL;
+ }
+ }
+
+ return 0;
+}
+
+static const struct led_strip_driver_api ws2812_i2s_api = {
+ .update_rgb = ws2812_strip_update_rgb,
+ .update_channels = ws2812_strip_update_channels,
+};
+
+/* Integer division, but always rounds up: e.g. 10/3 = 4 */
+#define WS2812_ROUNDED_DIVISION(x, y) ((x + (y - 1)) / y)
+
+#define WS2812_I2S_LRCK_PERIOD_US(idx) DT_INST_PROP(idx, lrck_period)
+
+#define WS2812_RESET_DELAY_US(idx) DT_INST_PROP(idx, reset_delay)
+/* Rounds up to the next 20us. */
+#define WS2812_RESET_DELAY_WORDS(idx) WS2812_ROUNDED_DIVISION(WS2812_RESET_DELAY_US(idx), \
+ WS2812_I2S_LRCK_PERIOD_US(idx))
+
+#define WS2812_NUM_COLORS(idx) (DT_INST_PROP_LEN(idx, color_mapping))
+
+#define WS2812_I2S_NUM_PIXELS(idx) (DT_INST_PROP(idx, chain_length))
+
+#define WS2812_I2S_BUFSIZE(idx) \
+ (((WS2812_NUM_COLORS(idx) * WS2812_I2S_NUM_PIXELS(idx)) + \
+ WS2812_I2S_PRE_DELAY_WORDS + WS2812_RESET_DELAY_WORDS(idx)) * 4)
+
+#define WS2812_I2S_DEVICE(idx) \
+ \
+ K_MEM_SLAB_DEFINE_STATIC(ws2812_i2s_##idx##_slab, WS2812_I2S_BUFSIZE(idx), 2, 4); \
+ \
+ static const uint8_t ws2812_i2s_##idx##_color_mapping[] = \
+ DT_INST_PROP(idx, color_mapping); \
+ \
+ static const struct ws2812_i2s_cfg ws2812_i2s_##idx##_cfg = { \
+ .dev = DEVICE_DT_GET(DT_INST_PROP(idx, i2s_dev)), \
+ .tx_buf_bytes = WS2812_I2S_BUFSIZE(idx), \
+ .mem_slab = &ws2812_i2s_##idx##_slab, \
+ .num_colors = WS2812_NUM_COLORS(idx), \
+ .color_mapping = ws2812_i2s_##idx##_color_mapping, \
+ .lrck_period = WS2812_I2S_LRCK_PERIOD_US(idx), \
+ .extra_wait_time_us = DT_INST_PROP(idx, extra_wait_time), \
+ .reset_words = WS2812_RESET_DELAY_WORDS(idx), \
+ .active_low = DT_INST_PROP(idx, out_active_low), \
+ .nibble_one = DT_INST_PROP(idx, nibble_one), \
+ .nibble_zero = DT_INST_PROP(idx, nibble_zero), \
+ }; \
+ \
+ DEVICE_DT_INST_DEFINE(idx, ws2812_i2s_init, NULL, NULL, &ws2812_i2s_##idx##_cfg, \
+ POST_KERNEL, CONFIG_LED_STRIP_INIT_PRIORITY, &ws2812_i2s_api);
+
+DT_INST_FOREACH_STATUS_OKAY(WS2812_I2S_DEVICE)
diff --git a/drivers/ws2812/ws2812_spi.c b/drivers/ws2812/ws2812_spi.c
new file mode 100644
index 0000000..9bebb13
--- /dev/null
+++ b/drivers/ws2812/ws2812_spi.c
@@ -0,0 +1,300 @@
+/*
+ * Copyright (c) 2017 Linaro Limited
+ * Copyright (c) 2019, Nordic Semiconductor ASA
+ * Copyright (c) 2021 Seagate Technology LLC
+ * Copyright (c) 2023 Leon Rinkel
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#define DT_DRV_COMPAT worldsemi_ws2812_spi
+
+#include <zephyr/drivers/led_strip.h>
+
+#include <string.h>
+#include <math.h>
+
+#define LOG_LEVEL CONFIG_LED_STRIP_LOG_LEVEL
+#include <zephyr/logging/log.h>
+LOG_MODULE_REGISTER(ws2812_spi);
+
+#include <zephyr/kernel.h>
+#include <zephyr/device.h>
+#include <zephyr/drivers/spi.h>
+#include <zephyr/sys/math_extras.h>
+#include <zephyr/sys/util.h>
+#include <zephyr/dt-bindings/led/led.h>
+
+/* spi-one-frame and spi-zero-frame in DT are for 8-bit frames. */
+#define SPI_FRAME_BITS 8
+
+/*
+ * SPI master configuration:
+ *
+ * - mode 0 (the default), 8 bit, MSB first (arbitrary), one-line SPI
+ * - no shenanigans (don't hold CS, don't hold the device lock, this
+ * isn't an EEPROM)
+ */
+#define SPI_OPER(idx) (SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB | \
+ COND_CODE_1(DT_INST_PROP(idx, spi_cpol), (SPI_MODE_CPOL), (0)) | \
+ COND_CODE_1(DT_INST_PROP(idx, spi_cpha), (SPI_MODE_CPHA), (0)) | \
+ SPI_WORD_SET(SPI_FRAME_BITS))
+
+struct ws2812_spi_cfg {
+ struct spi_dt_spec bus;
+ uint8_t *px_buf;
+ size_t px_buf_size;
+ uint8_t one_frame;
+ uint8_t zero_frame;
+ uint8_t num_colors;
+ const uint8_t *color_mapping;
+ uint16_t reset_delay;
+};
+
+static const struct ws2812_spi_cfg *dev_cfg(const struct device *dev)
+{
+ return dev->config;
+}
+
+/*
+ * Serialize an 8-bit color channel value into an equivalent sequence
+ * of SPI frames, MSbit first, where a one bit becomes SPI frame
+ * one_frame, and zero bit becomes zero_frame.
+ */
+static inline void ws2812_spi_ser(uint8_t buf[8], uint8_t color,
+ const uint8_t one_frame, const uint8_t zero_frame)
+{
+ int i;
+
+ for (i = 0; i < 8; i++) {
+ buf[i] = color & BIT(7 - i) ? one_frame : zero_frame;
+ }
+}
+
+/*
+ * Returns true if and only if cfg->px_buf is big enough to convert
+ * num_pixels RGB color values into SPI frames.
+ */
+static inline bool num_pixels_ok(const struct ws2812_spi_cfg *cfg,
+ size_t num_pixels)
+{
+ size_t nbytes;
+ bool overflow;
+
+ overflow = size_mul_overflow(num_pixels, cfg->num_colors * 8, &nbytes);
+ return !overflow && (nbytes <= cfg->px_buf_size);
+}
+
+/*
+ * Latch current color values on strip and reset its state machines.
+ */
+static inline void ws2812_reset_delay(uint16_t delay)
+{
+ k_usleep(delay);
+}
+
+/** RGB to RGBW conversion according to Wang et al. */
+void do_rgbw_conversion(
+ uint8_t* ro, uint8_t* go, uint8_t* bo, uint8_t* wo,
+ uint8_t ri, uint8_t gi, uint8_t bi,
+ uint8_t algo
+)
+{
+ float m = fmin(ri, fmin(gi, bi));
+ float M = fmax(ri, fmax(gi, bi));
+
+ float w;
+ switch (algo)
+ {
+ case 1:
+ w = m;
+ break;
+ case 2:
+ w = pow(m, 2);
+ break;
+ case 3:
+ w = -pow(m, 3) + pow(m, 2) + m;
+ break;
+ case 4:
+ w = (m / M >= 0.5) ? M :
+ (m * M) / (M - m);
+ break;
+
+ default:
+ return;
+ }
+
+ float k = (w + M) / M;
+
+ float r = k * ri - w;
+ float g = k * gi - w;
+ float b = k * bi - w;
+
+ *wo = fmax(fmin(floor(w), 255), 0);
+ *ro = fmax(fmin(floor(r), 255), 0);
+ *go = fmax(fmin(floor(g), 255), 0);
+ *bo = fmax(fmin(floor(b), 255), 0);
+}
+
+static int ws2812_strip_update_rgb(const struct device *dev,
+ struct led_rgb *pixels,
+ size_t num_pixels)
+{
+ const struct ws2812_spi_cfg *cfg = dev_cfg(dev);
+ const uint8_t one = cfg->one_frame, zero = cfg->zero_frame;
+ struct spi_buf buf = {
+ .buf = cfg->px_buf,
+ .len = cfg->px_buf_size,
+ };
+ const struct spi_buf_set tx = {
+ .buffers = &buf,
+ .count = 1
+ };
+ uint8_t *px_buf = cfg->px_buf;
+ size_t i;
+ int rc;
+
+ if (!num_pixels_ok(cfg, num_pixels)) {
+ return -ENOMEM;
+ }
+
+ /*
+ * Convert pixel data into SPI frames. Each frame has pixel data
+ * in color mapping on-wire format (e.g. GRB, GRBW, RGB, etc).
+ */
+ for (i = 0; i < num_pixels; i++) {
+ uint8_t j;
+
+ uint8_t ro, go, bo, wo;
+ do_rgbw_conversion(
+ &ro, &go, &bo, &wo,
+ pixels[i].r, pixels[i].g, pixels[i].b,
+ 1
+ );
+
+ for (j = 0; j < cfg->num_colors; j++) {
+ uint8_t pixel;
+
+ switch (cfg->color_mapping[j]) {
+ /* White channel is not supported by LED strip API. */
+ case LED_COLOR_ID_WHITE:
+ pixel = wo;
+ break;
+ case LED_COLOR_ID_RED:
+ pixel = ro;
+ break;
+ case LED_COLOR_ID_GREEN:
+ pixel = go;
+ break;
+ case LED_COLOR_ID_BLUE:
+ pixel = bo;
+ break;
+ default:
+ return -EINVAL;
+ }
+ ws2812_spi_ser(px_buf, pixel, one, zero);
+ px_buf += 8;
+ }
+ }
+
+ /*
+ * Display the pixel data.
+ */
+ rc = spi_write_dt(&cfg->bus, &tx);
+ ws2812_reset_delay(cfg->reset_delay);
+
+ return rc;
+}
+
+static int ws2812_strip_update_channels(const struct device *dev,
+ uint8_t *channels,
+ size_t num_channels)
+{
+ LOG_ERR("update_channels not implemented");
+ return -ENOTSUP;
+}
+
+static int ws2812_spi_init(const struct device *dev)
+{
+ const struct ws2812_spi_cfg *cfg = dev_cfg(dev);
+ uint8_t i;
+
+ if (!spi_is_ready_dt(&cfg->bus)) {
+ LOG_ERR("SPI device %s not ready", cfg->bus.bus->name);
+ return -ENODEV;
+ }
+
+ for (i = 0; i < cfg->num_colors; i++) {
+ switch (cfg->color_mapping[i]) {
+ case LED_COLOR_ID_WHITE:
+ case LED_COLOR_ID_RED:
+ case LED_COLOR_ID_GREEN:
+ case LED_COLOR_ID_BLUE:
+ break;
+ default:
+ LOG_ERR("%s: invalid channel to color mapping."
+ "Check the color-mapping DT property",
+ dev->name);
+ return -EINVAL;
+ }
+ }
+
+ return 0;
+}
+
+static const struct led_strip_driver_api ws2812_spi_api = {
+ .update_rgb = ws2812_strip_update_rgb,
+ .update_channels = ws2812_strip_update_channels,
+};
+
+#define WS2812_SPI_NUM_PIXELS(idx) \
+ (DT_INST_PROP(idx, chain_length))
+#define WS2812_SPI_HAS_WHITE(idx) \
+ (DT_INST_PROP(idx, has_white_channel) == 1)
+#define WS2812_SPI_ONE_FRAME(idx) \
+ (DT_INST_PROP(idx, spi_one_frame))
+#define WS2812_SPI_ZERO_FRAME(idx) \
+ (DT_INST_PROP(idx, spi_zero_frame))
+#define WS2812_SPI_BUFSZ(idx) \
+ (WS2812_NUM_COLORS(idx) * 8 * WS2812_SPI_NUM_PIXELS(idx))
+
+/*
+ * Retrieve the channel to color mapping (e.g. RGB, BGR, GRB, ...) from the
+ * "color-mapping" DT property.
+ */
+#define WS2812_COLOR_MAPPING(idx) \
+ static const uint8_t ws2812_spi_##idx##_color_mapping[] = \
+ DT_INST_PROP(idx, color_mapping)
+
+#define WS2812_NUM_COLORS(idx) (DT_INST_PROP_LEN(idx, color_mapping))
+
+/* Get the latch/reset delay from the "reset-delay" DT property. */
+#define WS2812_RESET_DELAY(idx) DT_INST_PROP(idx, reset_delay)
+
+#define WS2812_SPI_DEVICE(idx) \
+ \
+ static uint8_t ws2812_spi_##idx##_px_buf[WS2812_SPI_BUFSZ(idx)]; \
+ \
+ WS2812_COLOR_MAPPING(idx); \
+ \
+ static const struct ws2812_spi_cfg ws2812_spi_##idx##_cfg = { \
+ .bus = SPI_DT_SPEC_INST_GET(idx, SPI_OPER(idx), 0), \
+ .px_buf = ws2812_spi_##idx##_px_buf, \
+ .px_buf_size = WS2812_SPI_BUFSZ(idx), \
+ .one_frame = WS2812_SPI_ONE_FRAME(idx), \
+ .zero_frame = WS2812_SPI_ZERO_FRAME(idx), \
+ .num_colors = WS2812_NUM_COLORS(idx), \
+ .color_mapping = ws2812_spi_##idx##_color_mapping, \
+ .reset_delay = WS2812_RESET_DELAY(idx), \
+ }; \
+ \
+ DEVICE_DT_INST_DEFINE(idx, \
+ ws2812_spi_init, \
+ NULL, \
+ NULL, \
+ &ws2812_spi_##idx##_cfg, \
+ POST_KERNEL, \
+ CONFIG_LED_STRIP_INIT_PRIORITY, \
+ &ws2812_spi_api);
+
+DT_INST_FOREACH_STATUS_OKAY(WS2812_SPI_DEVICE)