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:
Marti Bolivar 2017-10-16 23:28:15 -04:00 committed by Anas Nashif
parent 658b673a83
commit 669327137b
4 changed files with 316 additions and 0 deletions

View file

@ -44,4 +44,6 @@ config LED_STRIP_RGB_SCRATCH
source "drivers/led_strip/Kconfig.lpd880x"
source "drivers/led_strip/Kconfig.ws2812"
endif # LED_STRIP

View 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

View file

@ -1 +1,2 @@
obj-$(CONFIG_LPD880X_STRIP) += lpd880x.o
obj-$(CONFIG_WS2812_STRIP) += ws2812.o

191
drivers/led_strip/ws2812.c Normal file
View 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);