drivers: led_strip: add WS2812-ish driver
The WS2812 LED driver IC has a one-wire interface which encodes bit values as pulse widths. The ICs themselves are basically shift registers. Roughly speaking, a "short" pulse shifts in a zero bit, a "long" pulse shifts in a one bit, and an inter-pulse gap exceeding a reset time threshold causes a pixel to latch the shifted-in color values. Each chip has an output pin for daisy chaining. Refer to the chip datsheets and comments in Kconfig.ws2812 for more details. To meet timing without hogging the core, this driver generates pulses using SPI. To work, this requires the MOSI line to stay low between SPI frames, and for inter-frame delays to be less than the reset pulse time. There are other ways do it (PWM + DMA on some SoCs, GPIO bit-banging if no other tasks need the core), but this is a reasonably general-purpose implementation. Signed-off-by: Marti Bolivar <marti.bolivar@linaro.org>
This commit is contained in:
parent
658b673a83
commit
669327137b
|
@ -44,4 +44,6 @@ config LED_STRIP_RGB_SCRATCH
|
|||
|
||||
source "drivers/led_strip/Kconfig.lpd880x"
|
||||
|
||||
source "drivers/led_strip/Kconfig.ws2812"
|
||||
|
||||
endif # LED_STRIP
|
||||
|
|
122
drivers/led_strip/Kconfig.ws2812
Normal file
122
drivers/led_strip/Kconfig.ws2812
Normal file
|
@ -0,0 +1,122 @@
|
|||
#
|
||||
# Copyright (c) 2017 Linaro Limited
|
||||
#
|
||||
# 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 WS2812_STRIP
|
||||
bool "Enable WS2812 (and compatible) LED strip driver"
|
||||
depends on SPI
|
||||
depends on !SPI_LEGACY_API
|
||||
default n
|
||||
help
|
||||
Enable LED strip driver for daisy chains of WS2812-ish
|
||||
(or WS2812B, WS2813, SK6812, or compatible) devices.
|
||||
These devices have a one-wire communications interface
|
||||
which encodes bits using pulses. Short pulses indicate
|
||||
zero bits, and long pulses indicate ones; refer to the
|
||||
chip datsheets for precise specifications. To implement
|
||||
this in a multitasking operating system, this driver
|
||||
generates the pulses using a SPI peripheral.
|
||||
|
||||
if WS2812_STRIP
|
||||
|
||||
config WS2812_STRIP_NAME
|
||||
string "Driver name"
|
||||
default "ws2812_strip"
|
||||
help
|
||||
Device name for WS2812 LED strip.
|
||||
|
||||
config WS2812_STRIP_SPI_DEV_NAME
|
||||
string "SPI master to use to drive the strip"
|
||||
default ""
|
||||
help
|
||||
Specify the device name of the SPI master which the
|
||||
WS2812 driver should use to control the LED strip.
|
||||
The MOSI pin of this SPI peripheral should be connected
|
||||
to the signal pin for the first chip in the strip.
|
||||
Examples: SPI_0, SPI_1, etc.
|
||||
|
||||
config WS2812_STRIP_SPI_BAUD_RATE
|
||||
int "Baud rate to use to drive LED strip"
|
||||
default 5250000
|
||||
help
|
||||
SPI clock rate, in Hz, to use while driving the strip.
|
||||
The baud rate must be chosen carefully together with the
|
||||
WS2812_STRIP_SPI_ONE_FRAME and WS2812_STRIP_SPI_ZERO_FRAME
|
||||
values so that the transmitted frames meet the chipset
|
||||
pulse widths for one and zero bits. If unsure, keep the default,
|
||||
but enable SPI debug logging for your device and make sure the
|
||||
configuration is matched exactly at runtime.
|
||||
|
||||
config WS2812_STRIP_ONE_FRAME
|
||||
hex "SPI frame to shift out to signal a one bit"
|
||||
default 0x7c
|
||||
help
|
||||
When shifted out at the configured clock frequency,
|
||||
this must generate a pulse whose width fits within the chipset
|
||||
specifications for T1H, and whose interpulse timing meets low
|
||||
times. It is recommended that the first and last bits in the
|
||||
frame be zero; this "encourages" SPI IPs to leave MOSI low
|
||||
between frames.
|
||||
|
||||
config WS2812_STRIP_ZERO_FRAME
|
||||
hex "SPI frame to shift out to signal a zero bit"
|
||||
default 0x60
|
||||
help
|
||||
When shifted out at the configured clock frequency,
|
||||
this must generate a pulse whose width fits within the chipset
|
||||
specifications for T0H, and whose interpulse timing meets low
|
||||
times. It is recommended that the first and last bits in the
|
||||
frame be zero; this "encourages" SPI IPs to leave MOSI low
|
||||
between frames.
|
||||
|
||||
# By default, we use GRBW [sic] (and ignore W).
|
||||
comment "The following options determine channel data order on the wire."
|
||||
|
||||
config WS2812_RED_ORDER
|
||||
int "Order in which a red pixel should be shifted out"
|
||||
default 1
|
||||
range 0 3
|
||||
help
|
||||
If the red channel is shifted out first, specify 0.
|
||||
If second, specify 1, and so on.
|
||||
|
||||
config WS2812_GRN_ORDER
|
||||
int "Order in which a green pixel should be shifted out"
|
||||
default 0
|
||||
range 0 3
|
||||
help
|
||||
If the green channel is shifted out first, specify 0.
|
||||
If second, specify 1, and so on.
|
||||
|
||||
config WS2812_BLU_ORDER
|
||||
int "Order in which a blue pixel should be shifted out"
|
||||
default 2
|
||||
range 0 3
|
||||
help
|
||||
If the blue channel is shifted out first, specify 0.
|
||||
If second, specify 1, and so on.
|
||||
|
||||
config WS2812_HAS_WHITE_CHANNEL
|
||||
bool "Does the chip have a white channel on wire?"
|
||||
default y
|
||||
help
|
||||
If the chipset has a white channel, say y. White channels
|
||||
are not used by the driver, but must be declared if expected
|
||||
by the chip.
|
||||
|
||||
config WS2812_WHT_ORDER
|
||||
int "Order in which a white pixel should be shifted out"
|
||||
default 3
|
||||
range 0 3
|
||||
depends on WS2812_HAS_WHITE_CHANNEL
|
||||
help
|
||||
If the blue channel is shifted out first, specify 0.
|
||||
If second, specify 1, and so on.
|
||||
|
||||
endif # WS2812_STRIP
|
|
@ -1 +1,2 @@
|
|||
obj-$(CONFIG_LPD880X_STRIP) += lpd880x.o
|
||||
obj-$(CONFIG_WS2812_STRIP) += ws2812.o
|
||||
|
|
191
drivers/led_strip/ws2812.c
Normal file
191
drivers/led_strip/ws2812.c
Normal file
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* Copyright (c) 2017 Linaro Limited
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <led_strip.h>
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#define SYS_LOG_LEVEL CONFIG_SYS_LOG_LED_STRIP_LEVEL
|
||||
#include <logging/sys_log.h>
|
||||
|
||||
#include <zephyr.h>
|
||||
#include <device.h>
|
||||
#include <spi.h>
|
||||
#include <misc/util.h>
|
||||
|
||||
/*
|
||||
* WS2812-ish 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 (SPI_OP_MODE_MASTER | \
|
||||
SPI_TRANSFER_MSB | \
|
||||
SPI_WORD_SET(8) | \
|
||||
SPI_LINES_SINGLE)
|
||||
|
||||
#define SPI_FREQ CONFIG_WS2812_STRIP_SPI_BAUD_RATE
|
||||
#define ONE_FRAME CONFIG_WS2812_STRIP_ONE_FRAME
|
||||
#define ZERO_FRAME CONFIG_WS2812_STRIP_ZERO_FRAME
|
||||
#define RED_OFFSET (8 * sizeof(u8_t) * CONFIG_WS2812_RED_ORDER)
|
||||
#define GRN_OFFSET (8 * sizeof(u8_t) * CONFIG_WS2812_GRN_ORDER)
|
||||
#define BLU_OFFSET (8 * sizeof(u8_t) * CONFIG_WS2812_BLU_ORDER)
|
||||
#ifdef CONFIG_WS2812_HAS_WHITE_CHANNEL
|
||||
#define WHT_OFFSET (8 * sizeof(u8_t) * CONFIG_WS2812_WHT_ORDER)
|
||||
#else
|
||||
#define WHT_OFFSET -1
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Despite datasheet claims (see blog post link in Kconfig.ws2812), a
|
||||
* 6 microsecond pulse is enough to reset the strip. Convert that into
|
||||
* a number of 8 bit SPI frames, adding another just to be safe.
|
||||
*/
|
||||
#define RESET_NFRAMES ((size_t)ceiling_fraction(3 * SPI_FREQ, 4000000) + 1)
|
||||
|
||||
struct ws2812_data {
|
||||
struct spi_config config;
|
||||
};
|
||||
|
||||
/*
|
||||
* Convert a color channel's bits into a sequence of SPI frames (with
|
||||
* the proper pulse and inter-pulse widths) to shift out.
|
||||
*/
|
||||
static inline void ws2812_serialize_color(u8_t buf[8], u8_t color)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < 8; i++) {
|
||||
buf[i] = color & BIT(7 - i) ? ONE_FRAME : ZERO_FRAME;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert a pixel into SPI frames, returning the number of bytes used.
|
||||
*/
|
||||
static size_t ws2812_serialize_pixel(u8_t px[32], struct led_rgb *pixel)
|
||||
{
|
||||
ws2812_serialize_color(px + RED_OFFSET, pixel->r);
|
||||
ws2812_serialize_color(px + GRN_OFFSET, pixel->g);
|
||||
ws2812_serialize_color(px + BLU_OFFSET, pixel->b);
|
||||
if (IS_ENABLED(CONFIG_WS2812_HAS_WHITE_CHANNEL)) {
|
||||
ws2812_serialize_color(px + WHT_OFFSET, 0); /* unused */
|
||||
return 32;
|
||||
}
|
||||
return 24;
|
||||
}
|
||||
|
||||
/*
|
||||
* Latch current color values on strip and reset its state machines.
|
||||
*/
|
||||
static int ws2812_reset_strip(struct spi_config *config)
|
||||
{
|
||||
u8_t reset_buf[RESET_NFRAMES];
|
||||
struct spi_buf reset = {
|
||||
.buf = reset_buf,
|
||||
.len = sizeof(reset_buf),
|
||||
};
|
||||
|
||||
memset(reset_buf, 0x00, sizeof(reset_buf));
|
||||
|
||||
return spi_write(config, &reset, 1);
|
||||
}
|
||||
|
||||
static int ws2812_strip_update_rgb(struct device *dev, struct led_rgb *pixels,
|
||||
size_t num_pixels)
|
||||
{
|
||||
struct ws2812_data *drv_data = dev->driver_data;
|
||||
struct spi_config *config = &drv_data->config;
|
||||
u8_t px_buf[32]; /* 32 are needed when a white channel is present. */
|
||||
struct spi_buf buf = {
|
||||
.buf = px_buf,
|
||||
};
|
||||
size_t i;
|
||||
int rc;
|
||||
|
||||
for (i = 0; i < num_pixels; i++) {
|
||||
buf.len = ws2812_serialize_pixel(px_buf, &pixels[i]);
|
||||
rc = spi_write(config, &buf, 1);
|
||||
if (rc) {
|
||||
/*
|
||||
* Latch anything we've shifted out first, to
|
||||
* call visual attention to the problematic
|
||||
* pixel.
|
||||
*/
|
||||
(void)ws2812_reset_strip(config);
|
||||
SYS_LOG_ERR("can't set pixel %u: %d", i, rc);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
|
||||
return ws2812_reset_strip(config);
|
||||
}
|
||||
|
||||
static int ws2812_strip_update_channels(struct device *dev, u8_t *channels,
|
||||
size_t num_channels)
|
||||
{
|
||||
struct ws2812_data *drv_data = dev->driver_data;
|
||||
struct spi_config *config = &drv_data->config;
|
||||
u8_t px_buf[8]; /* one byte per bit */
|
||||
struct spi_buf buf = {
|
||||
.buf = px_buf,
|
||||
.len = sizeof(px_buf),
|
||||
};
|
||||
size_t i;
|
||||
int rc;
|
||||
|
||||
for (i = 0; i < num_channels; i++) {
|
||||
ws2812_serialize_color(px_buf, channels[i]);
|
||||
rc = spi_write(config, &buf, 1);
|
||||
if (rc) {
|
||||
/*
|
||||
* Latch anything we've shifted out first, to
|
||||
* call visual attention to the problematic
|
||||
* pixel.
|
||||
*/
|
||||
(void)ws2812_reset_strip(config);
|
||||
SYS_LOG_ERR("can't set channel %u: %d", i, rc);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
|
||||
return ws2812_reset_strip(config);
|
||||
}
|
||||
|
||||
static int ws2812_strip_init(struct device *dev)
|
||||
{
|
||||
struct ws2812_data *data = dev->driver_data;
|
||||
struct spi_config *config = &data->config;
|
||||
struct device *spi;
|
||||
|
||||
spi = device_get_binding(CONFIG_WS2812_STRIP_SPI_DEV_NAME);
|
||||
if (!spi) {
|
||||
SYS_LOG_ERR("SPI device %s not found",
|
||||
CONFIG_WS2812_STRIP_SPI_DEV_NAME);
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
config->dev = spi;
|
||||
config->frequency = SPI_FREQ;
|
||||
config->operation = SPI_OPER;
|
||||
config->slave = 0; /* MOSI only. */
|
||||
config->cs = NULL;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static struct ws2812_data ws2812_strip_data;
|
||||
|
||||
static const struct led_strip_driver_api ws2812_strip_api = {
|
||||
.update_rgb = ws2812_strip_update_rgb,
|
||||
.update_channels = ws2812_strip_update_channels,
|
||||
};
|
||||
|
||||
DEVICE_AND_API_INIT(ws2812_strip, CONFIG_WS2812_STRIP_NAME,
|
||||
ws2812_strip_init, &ws2812_strip_data,
|
||||
NULL, POST_KERNEL, CONFIG_LED_STRIP_INIT_PRIORITY,
|
||||
&ws2812_strip_api);
|
Loading…
Reference in a new issue