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:
parent
c51cf4f08d
commit
f8e5e17246
|
@ -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_WS2812_STRIP_GPIO ws2812_gpio.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)
|
||||
|
|
|
@ -26,6 +26,14 @@ config WS2812_STRIP_SPI
|
|||
The SPI driver is portable, but requires significantly more
|
||||
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
|
||||
bool "GPIO driver"
|
||||
# Only an Cortex-M0 inline assembly implementation for the nRF51
|
||||
|
|
265
drivers/led_strip/ws2812_i2s.c
Normal file
265
drivers/led_strip/ws2812_i2s.c
Normal 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)
|
43
dts/bindings/led_strip/worldsemi,ws2812-i2s.yaml
Normal file
43
dts/bindings/led_strip/worldsemi,ws2812-i2s.yaml
Normal 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.
|
|
@ -30,13 +30,21 @@ Wiring
|
|||
******
|
||||
|
||||
#. 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
|
||||
to the data input pin of the first WS2812 IC in the strip.
|
||||
#. Connect the LED strip control pin (either I2S SDOUT, SPI MOSI or GPIO) from
|
||||
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.
|
||||
|
||||
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/
|
||||
|
||||
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
|
||||
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`.
|
||||
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
|
||||
refers to a node in your :ref:`devicetree <dt-guide>` with a
|
||||
``worldsemi,ws2812-spi`` or ``worldsemi,ws2812-gpio`` compatible. The node
|
||||
must be properly configured for the driver backend (SPI or GPIO) and daisy
|
||||
chain length (number of WS2812 chips).
|
||||
- create a ``led-strip`` :ref:`devicetree alias <dt-alias-chosen>`, which refers
|
||||
to a node in your :ref:`devicetree <dt-guide>` with a
|
||||
``worldsemi,ws2812-i2s``, ``worldsemi,ws2812-spi`` or
|
||||
``worldsemi,ws2812-gpio`` compatible. The node must be properly configured for
|
||||
the driver backend (I2S, SPI or GPIO) and daisy chain length (number of WS2812
|
||||
chips).
|
||||
|
||||
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/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,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
|
||||
**********
|
||||
|
||||
|
|
5
samples/drivers/led_ws2812/boards/thingy52_nrf52832.conf
Normal file
5
samples/drivers/led_ws2812/boards/thingy52_nrf52832.conf
Normal file
|
@ -0,0 +1,5 @@
|
|||
CONFIG_SPI=n
|
||||
|
||||
CONFIG_I2S=y
|
||||
CONFIG_WS2812_STRIP=y
|
||||
CONFIG_WS2812_STRIP_I2S=y
|
42
samples/drivers/led_ws2812/boards/thingy52_nrf52832.overlay
Normal file
42
samples/drivers/led_ws2812/boards/thingy52_nrf52832.overlay
Normal 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;
|
||||
};
|
||||
};
|
Loading…
Reference in a new issue