You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
250 lines
8.3 KiB
250 lines
8.3 KiB
/* |
|
* 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 I2S 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 <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; |
|
size_t length; |
|
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 uint32_t ws2812_i2s_ser(uint8_t color, const uint8_t sym_one, const uint8_t sym_zero) |
|
{ |
|
uint32_t word = 0; |
|
|
|
for (uint_fast8_t mask = 0x80; mask != 0; mask >>= 1) { |
|
word <<= 4; |
|
word |= (color & mask) ? sym_one : sym_zero; |
|
} |
|
|
|
/* Swap the two I2S values due to the (audio) channel TX order. */ |
|
return (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; |
|
const uint8_t sym_one = cfg->nibble_one; |
|
const uint8_t sym_zero = cfg->nibble_zero; |
|
const uint32_t reset_word = cfg->active_low ? ~0 : 0; |
|
uint32_t *tx_buf; |
|
uint32_t flush_time_us; |
|
void *mem_block; |
|
int ret; |
|
|
|
/* 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; |
|
} |
|
*tx_buf = ws2812_i2s_ser(pixel, sym_one, sym_zero) ^ reset_word; |
|
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 size_t ws2812_strip_length(const struct device *dev) |
|
{ |
|
const struct ws2812_i2s_cfg *cfg = dev->config; |
|
|
|
return cfg->length; |
|
} |
|
|
|
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 DEVICE_API(led_strip, ws2812_i2s_api) = { |
|
.update_rgb = ws2812_strip_update_rgb, |
|
.length = ws2812_strip_length, |
|
}; |
|
|
|
#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) \ |
|
DIV_ROUND_UP(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_BUS(idx)), \ |
|
.tx_buf_bytes = WS2812_I2S_BUFSIZE(idx), \ |
|
.mem_slab = &ws2812_i2s_##idx##_slab, \ |
|
.num_colors = WS2812_NUM_COLORS(idx), \ |
|
.length = DT_INST_PROP(idx, chain_length), \ |
|
.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)
|
|
|