Skip to content

Commit

Permalink
drivers: led_strip: add WS2812 I2S-based driver
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
narvalotech authored and mbolivar-nordic committed Feb 24, 2023
1 parent c51cf4f commit f8e5e17
Show file tree
Hide file tree
Showing 7 changed files with 406 additions and 8 deletions.
1 change: 1 addition & 0 deletions drivers/led_strip/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
8 changes: 8 additions & 0 deletions drivers/led_strip/Kconfig.ws2812
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
265 changes: 265 additions & 0 deletions drivers/led_strip/ws2812_i2s.c
Original file line number Diff line number Diff line change
@@ -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 changes: 43 additions & 0 deletions dts/bindings/led_strip/worldsemi,ws2812-i2s.yaml
Original file line number Diff line number Diff line change
@@ -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.
Loading

3 comments on commit f8e5e17

@parksj10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@narvalotech or @jori-nordic, where does nibble_zero and nibble_one get set in any config? I'm having trouble following that part of the example, and it seems that defines the 0/1 of the datastream in 4 i2s bits... but what/where are they lol?

@droidecahedron
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For posterity,

  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.

@parksj10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For posterity,

  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.

Found here: https:/zephyrproject-rtos/zephyr/blob/main/dts/bindings/led_strip/worldsemi%2Cws2812-i2s.yaml 😄

Please sign in to comment.