1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2025-12-26 23:11:19 +01:00

drivers/ws281x: Added driver for RGB LEDs

Added driver for the WS2812/SK6812 RGB LEDs often sold as NeoPixels, which due
to their integrated RGB controller can be chained to arbitrary length and
controlled with a single GPIO.
This commit is contained in:
Marian Buschsieweke 2019-11-12 20:16:25 +01:00
parent ba26aed107
commit 1ed1906023
No known key found for this signature in database
GPG Key ID: 61F64C6599B1539F
9 changed files with 642 additions and 0 deletions

View File

@ -608,6 +608,23 @@ ifneq (,$(filter w5100,$(USEMODULE)))
USEMODULE += luid
endif
ifneq (,$(filter ws281x_%,$(USEMODULE)))
USEMODULE += ws281x
endif
ifneq (,$(filter ws281x,$(USEMODULE)))
FEATURES_OPTIONAL += arch_avr8
ifeq (,$(filter ws281x_%,$(USEMODULE)))
ifneq (,$(filter arch_avr8,$(FEATURES_USED)))
USEMODULE += ws281x_atmega
endif
endif
ifneq (,$(filter ws281x_atmega,$(USEMODULE)))
FEATURES_REQUIRED += arch_avr8
endif
USEMODULE += xtimer
endif
ifneq (,$(filter xbee,$(USEMODULE)))
FEATURES_REQUIRED += periph_uart
FEATURES_REQUIRED += periph_gpio

View File

@ -314,6 +314,10 @@ ifneq (,$(filter w5100,$(USEMODULE)))
USEMODULE_INCLUDES += $(RIOTBASE)/drivers/w5100/include
endif
ifneq (,$(filter ws281x,$(USEMODULE)))
USEMODULE_INCLUDES += $(RIOTBASE)/drivers/ws281x/include
endif
ifneq (,$(filter xbee,$(USEMODULE)))
USEMODULE_INCLUDES += $(RIOTBASE)/drivers/xbee/include
endif

217
drivers/include/ws281x.h Normal file
View File

@ -0,0 +1,217 @@
/*
* Copyright 2019 Marian Buschsieweke
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @defgroup drivers_ws281x WS2812/SK6812 RGB LED (NeoPixel)
* @ingroup drivers_actuators
* @brief Driver for the WS2812 or the SK6812 RGB LEDs sold as NeoPixel
*
* # Summary
*
* The WS2812 or SK6812 RGB LEDs, or more commonly known as NeoPixels, can be
* chained so that a single data pin of the MCU can control an arbitrary number
* of RGB LEDs.
*
* # Support
*
* The protocol to communicate with the WS281x is custom, so no hardware
* implementations can be used. Hence, the protocol needs to be bit banged in
* software. As the timing requirements are to strict to do this using
* the platform independent APIs for accessing @ref drivers_periph_gpio and
* @ref sys_xtimer, platform specific implementations of @ref ws281x_write are
* needed.
*
* ## ATmega
*
* A bit banging implementation for ATmegas clocked at 8MHz and at 16MHz is
* provided. Boards clocked at any other core frequency are not supported.
* (But keep in mind that most (all?) ATmega MCUs do have an internal 8 MHz
* oscillator, that could be enabled by changing the fuse settings.)
*
* @warning On 8MHz ATmegas, only pins at GPIO ports B, C, and D are supported.
* (On 16MHz ATmegas, any pin is fine.)
*
* ### Usage
*
* Add the following to your `Makefile` to use the ATmega backend:
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Makefile
* USEMODULE += ws281x_atmega
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* @{
*
* @file
* @brief WS2812/SK6812 RGB LED Driver
*
* @author Marian Buschsieweke <marian.buschsieweke@ovgu.de>
*/
#ifndef WS281X_H
#define WS281X_H
#include <stdint.h>
#include "color.h"
#include "periph/gpio.h"
#include "ws281x_constants.h"
#include "xtimer.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief The number of bytes to allocate in the data buffer per LED
*/
#define WS281X_BYTES_PER_DEVICE (3U)
/**
* @brief Struct to hold initialization parameters for a WS281x RGB LED
*/
typedef struct {
/**
* @brief A statically allocated data buffer storing the state of the LEDs
*
* @pre Must be sized `numof * WS281X_BYTES_PER_DEVICE` bytes
*/
uint8_t *buf;
uint16_t numof; /**< Number of chained RGB LEDs */
gpio_t pin; /**< GPIO connected to the data pin of the first LED */
} ws281x_params_t;
/**
* @brief Device descriptor of a WS281x RGB LED chain
*/
typedef struct {
ws281x_params_t params; /**< Parameters of the LED chain */
} ws281x_t;
/**
* @brief Initialize an WS281x RGB LED chain
*
* @param dev Device descriptor to initialize
* @param params Parameters to initialize the device with
*
* @retval 0 Success
* @retval -EINVAL Invalid argument
* @retval -EIO Failed to initialize the data GPIO pin
*/
int ws281x_init(ws281x_t *dev, const ws281x_params_t *params);
/**
* @brief Writes the color data of the user supplied buffer
*
* @param dev Device descriptor of the LED chain to write to
* @param buf Buffer to write
* @param size Size of the buffer in bytes
*
* @pre Before the transmission starts @ref ws281x_prepare_transmission is
* called
* @post At the end of the transmission @ref ws281x_end_transmission is
* called
*
* This function can be used to drive a huge number of LEDs with small data
* buffers. However, after the return of this function the next chunk should
* be send within a few microseconds to avoid accidentally sending the end of
* transmission signal.
*
* Usage:
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ {.c}
* uint8_t chunk[CHUNK_SIZE];
* ws281x_prepare_transmission(ws281x_dev);
* while (more_chunks_available()) {
* prepare_chunk(chunk);
* ws281x_write_buffer(ws281x_dev, chunk, sizeof(chunk));
* }
* ws281x_end_transmission(dev);
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
void ws281x_write_buffer(ws281x_t *dev, const void *buf, size_t size);
#if defined(WS281X_HAVE_PREPARE_TRANSMISSION) || defined(DOXYGEN)
/**
* @brief Sets up everything needed to write data to the WS281X LED chain
*
* @param dev Device descriptor of the LED chain to write to
*/
void ws281x_prepare_transmission(ws281x_t *dev);
#else
static inline void ws281x_prepare_transmission(ws281x_t *dev)
{
(void)dev;
}
#endif
#if defined(WS281X_HAVE_END_TRANSMISSION) || defined(DOXYGEN)
/**
* @brief Ends the transmission to the WS2812/SK6812 LED chain
*
* @param dev Device descriptor of the LED chain to write to
*
* Does any cleanup the backend needs after sending data. In the simplest case
* it simply waits for 80µs to send the end of transmission signal.
*/
void ws281x_end_transmission(ws281x_t *dev);
#else
static inline void ws281x_end_transmission(ws281x_t *dev)
{
(void)dev;
xtimer_usleep(WS281X_T_END_US);
}
#endif
/**
* @brief Sets the color of an LED in the given data buffer
*
* @param dest Buffer to set the color in
* @param index The index of the LED to set the color of
* @param color The new color to apply
*
* @warning This change will not become active until @ref ws281x_write is
* called
*/
void ws281x_set_buffer(void *dest, uint16_t index, color_rgb_t color);
/**
* @brief Sets the color of an LED in the chain in the internal buffer
*
* @param dev Device descriptor of the LED chain to modify
* @param index The index of the LED to set the color of
* @param color The new color to apply
*
* @warning This change will not become active until @ref ws281x_write is
* called
*/
static inline void ws281x_set(ws281x_t *dev, uint16_t index, color_rgb_t color)
{
ws281x_set_buffer(dev->params.buf, index, color);
}
/**
* @brief Writes the internal buffer to the LED chain
*
* @param dev Device descriptor of the LED chain to write to
*
* @note This function implicitly calls @ref ws281x_end_transmission
* @see ws281x_set
*/
static inline void ws281x_write(ws281x_t *dev)
{
ws281x_prepare_transmission(dev);
ws281x_write_buffer(dev, dev->params.buf,
(size_t)dev->params.numof * WS281X_BYTES_PER_DEVICE);
ws281x_end_transmission(dev);
}
#ifdef __cplusplus
}
#endif
#endif /* WS281X_H */
/** @} */

3
drivers/ws281x/Makefile Normal file
View File

@ -0,0 +1,3 @@
SRC := ws281x.c # All other sources files provide ws281x_write as submodule
SUBMODULES := 1
include $(RIOTBASE)/Makefile.base

205
drivers/ws281x/atmega.c Normal file
View File

@ -0,0 +1,205 @@
/*
* Copyright 2019 Marian Buschsieweke
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup drivers_ws281x
*
* @{
*
* @file
* @brief Implementation of `ws281x_write()` for ATmega MCUs
*
* @author Marian Buschsieweke <marian.buschsieweke@ovgu.de>
*
* @}
*/
#include <assert.h>
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include "atmega_gpio.h"
#include "ws281x.h"
#include "ws281x_params.h"
#include "ws281x_constants.h"
#include "periph_cpu.h"
#include "xtimer.h"
/*
* Data encoding according to the datasheets of the WS2812 and the SK6812:
* - Encoding of zero bit:
* 1. High for 325ns ± 125ns (average of WS2812 and SK6812)
* 2. Low for 850ns ± 100ns (average of WS2812 and SK6812)
* - Encoding of one bit:
* 1. High for 650ns ± 100ns (average of WS2812 and SK6812)
* 2. Low for 600ns ± 150ns (for both WS2812 and SK6812)
*
* While the data sheet claims the opposite, the precision requirement for
* the duration of the second phase (the low period) is in reality super
* lax. Therefore, we bit bang the high period precisely in inline assembly,
* and completely ignore the timing of the low phase. This works just fine.
*
* Further: For an 8MHz clock, we need a single CPU cycle access to the
* GPIO port. The is only possible with the `out` instruction, which takes
* the port to write to as immediate. Thus, it has to be known at compile
* time. We therefore simply provide 3 implementations for each of the
* three most commonly used GPIO ports. For the 16MHz version accessing the
* GPIO port via memory using the st instruction works fine, so no limitations
* on the GPIO por there.
*
* On final trick: A relative jump to the next instructions takes two
* CPU cycles; this, its 2 NOPs for the price of one (in terms of ROM
* consumption).
*
* High phase timings
*
* +------------+-------------------+-------------------+-------------------+
* | Data | Device/Frequency | Min | Max |
* +------------+-------------------+-------------------+-------------------+
* | 0 bit | WS2812 | 200ns | 500ns |
* | 1 bit | WS2812 | 550ns | 850ns (*) |
* +------------+-------------------+-------------------+-------------------+
* | 0 bit | SK6812 | 150ns | 450ns |
* | 1 bit | SK6812 | 450ns | 750ns |
* +------------+-------------------+-------------------+-------------------+
* | 0 bit | WS2812/SK6812 | 200ns | 450ns |
* | 1 bit | WS2812/SK6812 | 550ns | 750ns |
* +------------+-------------------+-------------------+-------------------+
* | 0 bit | 8 MHz | 2 Cycles (250ns) | 3 Cycles (375ns) |
* | 1 bit | 8 MHz | 5 Cycles (625ns) | 6 Cycles (750ns) |
* +------------+-------------------+-------------------+-------------------+
* | 0 bit | 16 MHz | 4 Cycles (250ns) | 7 Cycles (438ns) |
* | 1 bit | 16 MHz | 9 Cycles (563ns) | 12 Cycles (750ns) |
* +------------+-------------------+-------------------+-------------------+
*
* (*) The high time for encoding a 1 bit on the WS2812 has no upper bound in
* practise; A high period of 5 seconds has been reported to work reliable.
*/
void ws281x_write_buffer(ws281x_t *dev, const void *buf, size_t size)
{
assert(dev);
const uint8_t *pos = buf;
const uint8_t *end = pos + size;
uint16_t port_addr = atmega_port_addr(dev->params.pin);
uint8_t mask_on, mask_off;
{
uint8_t port_state = _SFR_MEM8(port_addr);
mask_on = port_state | (1 << atmega_pin_num(dev->params.pin));
mask_off = port_state & ~(1 << atmega_pin_num(dev->params.pin));
}
#if (CLOCK_CORECLOCK >= 7500000U) && (CLOCK_CORECLOCK <= 8500000U)
const uint8_t port_num = atmega_port_num(dev->params.pin);
switch (port_num) {
case PORT_B:
while (pos < end) {
uint8_t cnt = 8;
uint8_t data = *pos;
while (cnt > 0) {
__asm__ volatile( /* Cycles | 1 | 0 */
"out %[port], %[on]" "\n\t" /* 1 | x | x */
"sbrs %[data], 7" "\n\t" /* 1 | x | x */
"out %[port], %[off]" "\n\t" /* 1 | - | x */
"rjmp .+0" "\n\t" /* 2 | x | x */
"out %[port], %[off]" "\n\t" /* 1 | x | x */
/* Total CPU cycles for high period:
* 5 cycles for bit 1, 2 cycles for bit 0 */
: [data] "+r" (data)
: [port] "I" (_SFR_IO_ADDR(PORTB)),
[on] "r" (mask_on),
[off] "r" (mask_off)
);
cnt--;
data <<= 1;
}
pos++;
}
break;
case PORT_C:
while (pos < end) {
uint8_t cnt = 8;
uint8_t data = *pos;
while (cnt > 0) {
__asm__ volatile( /* Cycles | 1 | 0 */
"out %[port], %[on]" "\n\t" /* 1 | x | x */
"sbrs %[data], 7" "\n\t" /* 1 | x | x */
"out %[port], %[off]" "\n\t" /* 1 | - | x */
"rjmp .+0" "\n\t" /* 2 | x | x */
"out %[port], %[off]" "\n\t" /* 1 | x | x */
/* Total CPU cycles for high period:
* 5 cycles for bit 1, 2 cycles for bit 0 */
: [data] "+r" (data)
: [port] "I" (_SFR_IO_ADDR(PORTC)),
[on] "r" (mask_on),
[off] "r" (mask_off)
);
cnt--;
data <<= 1;
}
pos++;
}
break;
case PORT_D:
while (pos < end) {
uint8_t cnt = 8;
uint8_t data = *pos;
while (cnt > 0) {
__asm__ volatile( /* Cycles | 1 | 0 */
"out %[port], %[on]" "\n\t" /* 1 | x | x */
"sbrs %[data], 7" "\n\t" /* 1 | x | x */
"out %[port], %[off]" "\n\t" /* 1 | - | x */
"rjmp .+0" "\n\t" /* 2 | x | x */
"out %[port], %[off]" "\n\t" /* 1 | x | x */
/* Total CPU cycles for high period:
* 5 cycles for bit 1, 2 cycles for bit 0 */
: [data] "+r" (data)
: [port] "I" (_SFR_IO_ADDR(PORTD)),
[on] "r" (mask_on),
[off] "r" (mask_off)
);
cnt--;
data <<= 1;
}
pos++;
}
break;
default:
assert(0);
break;
}
#elif (CLOCK_CORECLOCK >= 15500000U) && (CLOCK_CORECLOCK <= 16500000U)
while (pos < end) {
uint8_t cnt = 8;
uint8_t data = *pos;
while (cnt > 0) {
__asm__ volatile( /* CPU Cycles | 1 | 0 */
"st %a[port], %[on]" "\n\t" /* 2 | x | x */
"sbrc %[data], 7" "\n\t" /* 1 | x | x */
"rjmp .+0" "\n\t" /* 2 | x | - */
"sbrc %[data], 7" "\n\t" /* 1 | x | x */
"rjmp .+0" "\n\t" /* 2 | x | - */
"sbrc %[data], 7" "\n\t" /* 1 | x | x */
"nop" "\n\t" /* 1 | x | - */
"st %a[port], %[off]" "\n\t" /* 2 | x | x */
/* Total CPU cycles for high period:
* 10 cycles for bit 1, 5 cycles for bit 0 */
: [data] "+r" (data)
: [on] "r" (mask_on),
[port] "e" (port_addr),
[off] "r" (mask_off)
);
cnt--;
data <<= 1;
}
pos++;
}
#else
#error "No low level WS281x implementation for ATmega CPUs for your CPU clock"
#endif
}

View File

@ -0,0 +1,62 @@
/*
* Copyright (C) 2019 Marian Buschsieweke
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup drivers_ws281x
*
* @{
* @file
* @brief Constants for WS2812/SK6812 RGB LEDs
*
* @author Marian Buschsieweke <marian.buschsieweke@ovgu.de>
*/
#ifndef WS281X_CONSTANTS_H
#define WS281X_CONSTANTS_H
#ifdef __cplusplus
extern "C" {
#endif
/**
* @name Timing parameters for WS2812/SK6812 RGB LEDs
* @{
*/
/**
* @brief Time in microseconds to pull the data line low to signal end of data
*
* For the WS2812 it is 50µs, for the SK6812 it is 80µs. We choose 80µs to
* be compatible with both.
*/
#define WS281X_T_END_US (80U)
/**@}*/
/**
* @name Data encoding parameters for WS2812/SK6812 RGB LEDs
* @{
*/
/**
* @brief Offset for the red color component
*/
#define WS281X_OFFSET_R (1U)
/**
* @brief Offset for the green color component
*/
#define WS281X_OFFSET_G (0U)
/**
* @brief Offset for the blue color component
*/
#define WS281X_OFFSET_B (2U)
/**@}*/
#ifdef __cplusplus
}
#endif
#endif /* WS281X_CONSTANTS_H */
/** @} */

View File

@ -0,0 +1,72 @@
/*
* Copyright (C) 2019 Marian Buschsieweke
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup drivers_ws281x
*
* @{
* @file
* @brief Default configuration for WS2812/SK6812 RGB LEDs
*
* @author Marian Buschsieweke <marian.buschsieweke@ovgu.de>
*/
#ifndef WS281X_PARAMS_H
#define WS281X_PARAMS_H
#include "board.h"
#include "ws281x.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @name Default configuration parameters for WS281x RGB LEDs
* @{
*/
#ifndef WS281X_PARAM_PIN
#define WS281X_PARAM_PIN (GPIO_PIN(0,0)) /**< GPIO pin connected to the data pin of the first LED */
#endif
#ifndef WS281X_PARAM_NUMOF
#define WS281X_PARAM_NUMOF (8U) /**< Number of LEDs chained */
#endif
#ifndef WS281X_PARAM_BUF
/**
* @brief Data buffer holding the LED states
*/
extern uint8_t ws281x_buf[WS281X_PARAM_NUMOF * WS281X_BYTES_PER_DEVICE];
#define WS281X_PARAM_BUF (ws281x_buf) /**< Data buffer holding LED states */
#endif
#ifndef WS281X_PARAMS
/**
* @brief WS281x initialization parameters
*/
#define WS281X_PARAMS { \
.pin = WS281X_PARAM_PIN, \
.numof = WS281X_PARAM_NUMOF, \
.buf = WS281X_PARAM_BUF, \
}
#endif
/**@}*/
/**
* @brief Initialization parameters for WS281x devices
*/
static const ws281x_params_t ws281x_params[] =
{
WS281X_PARAMS
};
#ifdef __cplusplus
}
#endif
#endif /* WS281X_PARAMS_H */
/** @} */

59
drivers/ws281x/ws281x.c Normal file
View File

@ -0,0 +1,59 @@
/*
* Copyright 2019 Marian Buschsieweke
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup drivers_ws281x
*
* @{
*
* @file
* @brief Driver for the WS2812 or the SK6812 RGB LEDs sold as NeoPixel
*
* @author Marian Buschsieweke <marian.buschsieweke@ovgu.de>
*
* @}
*/
#include <assert.h>
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include "ws281x.h"
#include "ws281x_constants.h"
#include "ws281x_params.h"
#include "periph/gpio.h"
/* Default buffer used in ws281x_params.h. Will be optimized out if unused */
uint8_t ws281x_buf[WS281X_PARAM_NUMOF * WS281X_BYTES_PER_DEVICE];
/* Some backend will need a custom init function. Declaring this as weak symbol
* allows them to provide their own. */
int __attribute__((weak)) ws281x_init(ws281x_t *dev,
const ws281x_params_t *params)
{
if (!dev || !params || !params->buf) {
return -EINVAL;
}
memset(dev, 0, sizeof(ws281x_t));
dev->params = *params;
if (gpio_init(dev->params.pin, GPIO_OUT)) {
return -EIO;
}
return 0;
}
void ws281x_set_buffer(void *_dest, uint16_t n, color_rgb_t c)
{
uint8_t *dest = _dest;
dest[WS281X_BYTES_PER_DEVICE * n + WS281X_OFFSET_R] = c.r;
dest[WS281X_BYTES_PER_DEVICE * n + WS281X_OFFSET_G] = c.g;
dest[WS281X_BYTES_PER_DEVICE * n + WS281X_OFFSET_B] = c.b;
}

View File

@ -156,6 +156,9 @@ PSEUDOMODULES += vcnl4010
PSEUDOMODULES += vcnl4020
PSEUDOMODULES += vcnl4040
# implementations of ws281x_write as submodules of ws281x:
PSEUDOMODULES += ws281x_%
# include variants of lpsxxx drivers as pseudo modules
PSEUDOMODULES += lps331ap
PSEUDOMODULES += lps22hb