drivers: led_strip: add WS2812 I2S-based driver

Add a driver implementation that uses the I2S peripheral.
Based off this blog post:
https://electronut.in/nrf52-i2s-ws2812/

Should help with #33505, #29877 and maybe #47780, as there is no garbage
data at the end of transmissions on nRF52832, and no gaps.

Signed-off-by: Jonathan Rico <jonathan@rico.live>
This commit is contained in:
Jonathan Rico 2022-11-07 21:42:43 +01:00 committed by Marti Bolivar
parent c51cf4f08d
commit f8e5e17246
7 changed files with 406 additions and 8 deletions

View file

@ -6,4 +6,5 @@ zephyr_library_sources_ifdef(CONFIG_APA102_STRIP apa102.c)
zephyr_library_sources_ifdef(CONFIG_LPD880X_STRIP lpd880x.c) zephyr_library_sources_ifdef(CONFIG_LPD880X_STRIP lpd880x.c)
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_GPIO ws2812_gpio.c) zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_GPIO ws2812_gpio.c)
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_SPI ws2812_spi.c) zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_SPI ws2812_spi.c)
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_I2S ws2812_i2s.c)
zephyr_library_sources_ifdef(CONFIG_TLC5971_STRIP tlc5971.c) zephyr_library_sources_ifdef(CONFIG_TLC5971_STRIP tlc5971.c)

View file

@ -26,6 +26,14 @@ config WS2812_STRIP_SPI
The SPI driver is portable, but requires significantly more The SPI driver is portable, but requires significantly more
memory (1 byte of overhead per bit of pixel data). memory (1 byte of overhead per bit of pixel data).
config 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 WS2812_STRIP_GPIO config WS2812_STRIP_GPIO
bool "GPIO driver" bool "GPIO driver"
# Only an Cortex-M0 inline assembly implementation for the nRF51 # Only an Cortex-M0 inline assembly implementation for the nRF51

View file

@ -0,0 +1,265 @@
/*
* Copyright (c) 2022 Jonathan Rico
*
* 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 <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);
}
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++) {
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 = 0;
break;
case LED_COLOR_ID_RED:
pixel = pixels[i].r;
break;
case LED_COLOR_ID_GREEN:
pixel = pixels[i].g;
break;
case LED_COLOR_ID_BLUE:
pixel = pixels[i].b;
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)

View file

@ -0,0 +1,43 @@
# Copyright (c) 2022 Jonathan Rico
# SPDX-License-Identifier: Apache-2.0
description: |
Worldsemi WS2812 LED strip, I2S binding
Driver bindings for controlling a WS2812 or compatible LED
strip with an I2S master.
compatible: "worldsemi,ws2812-i2s"
include: [base.yaml, ws2812.yaml]
properties:
i2s-dev:
type: phandle
required: true
description: Pointer to the I2S instance.
out-active-low:
type: boolean
description: True if the output pin is active low.
nibble-one:
type: int
default: 0x0E
description: 4-bit value to shift out for a 1 pulse.
nibble-zero:
type: int
default: 0x08
description: 4-bit value to shift out for a 0 pulse.
lrck-period:
type: int
default: 10
description: LRCK (left/right clock) period in microseconds.
extra-wait-time:
type: int
default: 300
description: Extra microseconds to wait for the driver to flush its I2S queue.

View file

@ -30,13 +30,21 @@ Wiring
****** ******
#. Ensure your Zephyr board, and the LED strip share a common ground. #. Ensure your Zephyr board, and the LED strip share a common ground.
#. Connect the LED strip control pin (either SPI MOSI or GPIO) from your board #. Connect the LED strip control pin (either I2S SDOUT, SPI MOSI or GPIO) from
to the data input pin of the first WS2812 IC in the strip. your board to the data input pin of the first WS2812 IC in the strip.
#. Power the LED strip at an I/O level compatible with the control pin signals. #. Power the LED strip at an I/O level compatible with the control pin signals.
Building and Running Wiring on a thingy52
******************** ********************
The thingy52 has integrated NMOS transistors, that can be used instead of a level shifter.
The I2S driver supports inverting the output to suit this scheme, using the ``out-active-low`` dts
property. See the overlay file
:zephyr_file:`samples/drivers/led_ws2812/boards/thingy52_nrf52832.overlay` for more detail.
Building and Running
*********************
.. _blog post on WS2812 timing: https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/ .. _blog post on WS2812 timing: https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/
This sample's source directory is :zephyr_file:`samples/drivers/led_ws2812/`. This sample's source directory is :zephyr_file:`samples/drivers/led_ws2812/`.
@ -46,14 +54,19 @@ To make sure the sample is set up properly for building, you must:
- select the correct WS2812 driver backend for your SoC. This currently should - select the correct WS2812 driver backend for your SoC. This currently should
be :kconfig:option:`CONFIG_WS2812_STRIP_SPI` unless you are using an nRF51 SoC, in be :kconfig:option:`CONFIG_WS2812_STRIP_SPI` unless you are using an nRF51 SoC, in
which case it will be :kconfig:option:`CONFIG_WS2812_STRIP_GPIO`. which case it will be :kconfig:option:`CONFIG_WS2812_STRIP_GPIO`.
For the nRF52832, the SPI peripheral might output some garbage at the end of
transmissions, and that might confuse older WS2812 strips. Use the I2S driver
in those cases.
- create a ``led-strip`` :ref:`devicetree alias <dt-alias-chosen>`, which - create a ``led-strip`` :ref:`devicetree alias <dt-alias-chosen>`, which refers
refers to a node in your :ref:`devicetree <dt-guide>` with a to a node in your :ref:`devicetree <dt-guide>` with a
``worldsemi,ws2812-spi`` or ``worldsemi,ws2812-gpio`` compatible. The node ``worldsemi,ws2812-i2s``, ``worldsemi,ws2812-spi`` or
must be properly configured for the driver backend (SPI or GPIO) and daisy ``worldsemi,ws2812-gpio`` compatible. The node must be properly configured for
chain length (number of WS2812 chips). the driver backend (I2S, SPI or GPIO) and daisy chain length (number of WS2812
chips).
For example devicetree configurations for each compatible, see For example devicetree configurations for each compatible, see
:zephyr_file:`samples/drivers/led_ws2812/boards/thingy52_nrf52832.overlay`,
:zephyr_file:`samples/drivers/led_ws2812/boards/nrf52dk_nrf52832.overlay` and :zephyr_file:`samples/drivers/led_ws2812/boards/nrf52dk_nrf52832.overlay` and
:zephyr_file:`samples/drivers/led_ws2812/boards/nrf51dk_nrf51422.overlay`. :zephyr_file:`samples/drivers/led_ws2812/boards/nrf51dk_nrf51422.overlay`.
@ -77,6 +90,27 @@ following output:
[00:00:00.005,920] <inf> main: Found LED strip device WS2812 [00:00:00.005,920] <inf> main: Found LED strip device WS2812
[00:00:00.005,950] <inf> main: Displaying pattern on strip [00:00:00.005,950] <inf> main: Displaying pattern on strip
Supported drivers
*****************
This sample uses different drivers depending on the selected board:
I2S driver:
- thingy52_nrf52832
SPI driver:
- mimxrt1050_evk
- mimxrt1050_evk_qspi
- nrf52dk_nrf52832
- nucleo_f070rb
- nucleo_g071rb
- nucleo_h743zi
- nucleo_l476rg
GPIO driver (cortex-M0 only):
- bbc_microbit
- nrf51dk_nrf51422
References References
********** **********

View file

@ -0,0 +1,5 @@
CONFIG_SPI=n
CONFIG_I2S=y
CONFIG_WS2812_STRIP=y
CONFIG_WS2812_STRIP_I2S=y

View file

@ -0,0 +1,42 @@
#include <zephyr/dt-bindings/led/led.h>
/* Wiring:
* - M1.S connected to GND
* - SDOUT connected to M1.D
* - ~300 ohm resistor between M1.D and TP5 (5V / Vbus)
*/
&pinctrl {
i2s0_default_alt: i2s0_default_alt {
group1 {
psels = <NRF_PSEL(I2S_SCK_M, 0, 20)>,
<NRF_PSEL(I2S_LRCK_M, 0, 19)>,
<NRF_PSEL(I2S_SDOUT, 0, 18)>,
<NRF_PSEL(I2S_SDIN, 0, 21)>;
};
};
};
i2s_led: &i2s0 {
status = "okay";
pinctrl-0 = <&i2s0_default_alt>;
pinctrl-names = "default";
};
/ {
led_strip: ws2812 {
compatible = "worldsemi,ws2812-i2s";
i2s-dev = < &i2s_led >;
chain-length = <10>; /* arbitrary; change at will */
color-mapping = <LED_COLOR_ID_GREEN
LED_COLOR_ID_RED
LED_COLOR_ID_BLUE>;
out-active-low;
reset-delay = <120>;
};
aliases {
led-strip = &led_strip;
};
};