From ba26aed107ed01b25fd588faace804bbe2f949ef Mon Sep 17 00:00:00 2001 From: Marian Buschsieweke Date: Tue, 12 Nov 2019 20:15:59 +0100 Subject: [PATCH 1/4] cpu/atmega_common: Restructured code Moved macros and static inline helper functions needed to access ATmega GPIOs to cpu/atmega_common/include/atmega_gpio.h in order to reuse them for the platform specific low level part of the Neopixel driver. --- cpu/atmega_common/include/atmega_gpio.h | 102 ++++++++++++++++++++++++ cpu/atmega_common/periph/gpio.c | 83 ++++--------------- 2 files changed, 116 insertions(+), 69 deletions(-) create mode 100644 cpu/atmega_common/include/atmega_gpio.h diff --git a/cpu/atmega_common/include/atmega_gpio.h b/cpu/atmega_common/include/atmega_gpio.h new file mode 100644 index 0000000000..f4befd7719 --- /dev/null +++ b/cpu/atmega_common/include/atmega_gpio.h @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2015 HAW Hamburg + * 2016 INRIA + + * + * 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 cpu_atmega_common + * @ingroup drivers_periph_gpio + * @{ + * + * @file + * @brief Macros and inline functions for accessing GPIOs of the ATmega + * family + * + * @author René Herthel + * @author Francisco Acosta + * @author Laurent Navet + */ + +#ifndef ATMEGA_GPIO_H +#define ATMEGA_GPIO_H +#include + +#include + +#include "cpu.h" +#include "board.h" +#include "periph/gpio.h" +#include "periph_conf.h" +#include "periph_cpu.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define ATMEGA_GPIO_BASE_PORT_A (0x20) +#define ATMEGA_GPIO_OFFSET_PORT_H (0xCB) +#define ATMEGA_GPIO_OFFSET_PIN_PORT (0x02) +#define ATMEGA_GPIO_OFFSET_PIN_PIN (0x03) + +/** + * @brief Extract the pin number of the given pin + */ +static inline uint8_t atmega_pin_num(gpio_t pin) +{ + return (pin & 0x0f); +} + +/** + * @brief Extract the port number of the given pin + */ +static inline uint8_t atmega_port_num(gpio_t pin) +{ + return (pin >> 4) & 0x0f; +} + +/** + * @brief Generate the PORTx address of the give pin. + */ +static inline uint16_t atmega_port_addr(gpio_t pin) +{ + uint8_t port_num = atmega_port_num(pin); + uint16_t port_addr = port_num * ATMEGA_GPIO_OFFSET_PIN_PIN; + + port_addr += ATMEGA_GPIO_BASE_PORT_A; + port_addr += ATMEGA_GPIO_OFFSET_PIN_PORT; + +#if defined (PORTG) + if (port_num > PORT_G) { + port_addr += ATMEGA_GPIO_OFFSET_PORT_H; + } +#endif + return port_addr; +} + +/** + * @brief Generate the DDRx address of the given pin + */ +static inline uint16_t atmega_ddr_addr(gpio_t pin) +{ + return (atmega_port_addr(pin) - 0x01); +} + +/** + * @brief Generate the PINx address of the given pin. + */ +static inline uint16_t atmega_pin_addr(gpio_t pin) +{ + return (atmega_port_addr(pin) - 0x02); +} + +#ifdef __cplusplus +} +#endif + +#endif /* ATMEGA_GPIO_H */ +/** @} */ diff --git a/cpu/atmega_common/periph/gpio.c b/cpu/atmega_common/periph/gpio.c index b5046005a2..1a5066e64f 100644 --- a/cpu/atmega_common/periph/gpio.c +++ b/cpu/atmega_common/periph/gpio.c @@ -35,15 +35,11 @@ #include "periph/gpio.h" #include "periph_conf.h" #include "periph_cpu.h" +#include "atmega_gpio.h" #define ENABLE_DEBUG (0) #include "debug.h" -#define GPIO_BASE_PORT_A (0x20) -#define GPIO_OFFSET_PORT_H (0xCB) -#define GPIO_OFFSET_PIN_PORT (0x02) -#define GPIO_OFFSET_PIN_PIN (0x03) - #ifdef MODULE_PERIPH_GPIO_IRQ /* * @brief Define GPIO interruptions for an specific atmega CPU, by default @@ -167,72 +163,21 @@ static gpio_isr_ctx_pcint_t pcint_config[8 * PCINT_NUM_BANKS]; #endif /* MODULE_PERIPH_GPIO_IRQ */ -/** - * @brief Extract the pin number of the given pin - */ -static inline uint8_t _pin_num(gpio_t pin) -{ - return (pin & 0x0f); -} - -/** - * @brief Extract the port number of the given pin - */ -static inline uint8_t _port_num(gpio_t pin) -{ - return (pin >> 4) & 0x0f; -} - -/** - * @brief Generate the PORTx address of the give pin. - */ -static inline uint16_t _port_addr(gpio_t pin) -{ - uint8_t port_num = _port_num(pin); - uint16_t port_addr = port_num * GPIO_OFFSET_PIN_PIN; - - port_addr += GPIO_BASE_PORT_A; - port_addr += GPIO_OFFSET_PIN_PORT; - -#if defined (PORTG) - if (port_num > PORT_G) { - port_addr += GPIO_OFFSET_PORT_H; - } -#endif - return port_addr; -} - -/** - * @brief Generate the DDRx address of the given pin - */ -static inline uint16_t _ddr_addr(gpio_t pin) -{ - return (_port_addr(pin) - 0x01); -} - -/** - * @brief Generate the PINx address of the given pin. - */ -static inline uint16_t _pin_addr(gpio_t pin) -{ - return (_port_addr(pin) - 0x02); -} - int gpio_init(gpio_t pin, gpio_mode_t mode) { - uint8_t pin_mask = (1 << _pin_num(pin)); + uint8_t pin_mask = (1 << atmega_pin_num(pin)); switch (mode) { case GPIO_OUT: - _SFR_MEM8(_ddr_addr(pin)) |= pin_mask; + _SFR_MEM8(atmega_ddr_addr(pin)) |= pin_mask; break; case GPIO_IN: - _SFR_MEM8(_ddr_addr(pin)) &= ~pin_mask; - _SFR_MEM8(_port_addr(pin)) &= ~pin_mask; + _SFR_MEM8(atmega_ddr_addr(pin)) &= ~pin_mask; + _SFR_MEM8(atmega_port_addr(pin)) &= ~pin_mask; break; case GPIO_IN_PU: - _SFR_MEM8(_ddr_addr(pin)) &= ~pin_mask; - _SFR_MEM8(_port_addr(pin)) |= pin_mask; + _SFR_MEM8(atmega_ddr_addr(pin)) &= ~pin_mask; + _SFR_MEM8(atmega_port_addr(pin)) |= pin_mask; break; default: return -1; @@ -243,17 +188,17 @@ int gpio_init(gpio_t pin, gpio_mode_t mode) int gpio_read(gpio_t pin) { - return (_SFR_MEM8(_pin_addr(pin)) & (1 << _pin_num(pin))); + return (_SFR_MEM8(atmega_pin_addr(pin)) & (1 << atmega_pin_num(pin))); } void gpio_set(gpio_t pin) { - _SFR_MEM8(_port_addr(pin)) |= (1 << _pin_num(pin)); + _SFR_MEM8(atmega_port_addr(pin)) |= (1 << atmega_pin_num(pin)); } void gpio_clear(gpio_t pin) { - _SFR_MEM8(_port_addr(pin)) &= ~(1 << _pin_num(pin)); + _SFR_MEM8(atmega_port_addr(pin)) &= ~(1 << atmega_pin_num(pin)); } void gpio_toggle(gpio_t pin) @@ -307,7 +252,7 @@ int gpio_init_int(gpio_t pin, gpio_mode_t mode, gpio_flank_t flank, /* If pin change interrupts are enabled, enable mask and interrupt */ #ifdef PCINT_NUM_BANKS int8_t offset = -1; - uint8_t pin_num = _pin_num(pin); + uint8_t pin_num = atmega_pin_num(pin); for (unsigned i = 0; i < ARRAY_SIZE(pcint_mapping); i++) { if (pin != GPIO_UNDEF && pin == pcint_mapping[i]) { @@ -365,7 +310,7 @@ int gpio_init_int(gpio_t pin, gpio_mode_t mode, gpio_flank_t flank, break; } /* As ports are mixed in a bank (e.g. PCINT0), we can only save a single bit here! */ - uint8_t port_value = (_SFR_MEM8(_pin_addr( pin ))); + uint8_t port_value = (_SFR_MEM8(atmega_pin_addr( pin ))); uint8_t pin_mask = (1 << pin_num); uint8_t pin_value = ((port_value & pin_mask) != 0); if (pin_value) { @@ -448,9 +393,9 @@ static inline void pcint_handler(uint8_t bank, uint8_t enabled_pcints) /* get pin from mapping (assumes 8 entries per bank!) */ gpio_t pin = pcint_mapping[bank * 8 + idx]; /* re-construct mask from pin */ - uint8_t pin_mask = (1 << (_pin_num(pin))); + uint8_t pin_mask = (1 << (atmega_pin_num(pin))); uint8_t idx_mask = (1 << idx); - uint8_t port_value = (_SFR_MEM8(_pin_addr( pin ))); + uint8_t port_value = (_SFR_MEM8(atmega_pin_addr( pin ))); uint8_t pin_value = ((port_value & pin_mask) != 0); uint8_t old_state = ((pcint_state[bank] & idx_mask) != 0); gpio_isr_ctx_pcint_t *conf = &pcint_config[bank * 8 + idx]; From 1ed19060239fd944adf6f794d1cc0dd875268a9f Mon Sep 17 00:00:00 2001 From: Marian Buschsieweke Date: Tue, 12 Nov 2019 20:16:25 +0100 Subject: [PATCH 2/4] 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. --- drivers/Makefile.dep | 17 ++ drivers/Makefile.include | 4 + drivers/include/ws281x.h | 217 ++++++++++++++++++++++ drivers/ws281x/Makefile | 3 + drivers/ws281x/atmega.c | 205 ++++++++++++++++++++ drivers/ws281x/include/ws281x_constants.h | 62 +++++++ drivers/ws281x/include/ws281x_params.h | 72 +++++++ drivers/ws281x/ws281x.c | 59 ++++++ makefiles/pseudomodules.inc.mk | 3 + 9 files changed, 642 insertions(+) create mode 100644 drivers/include/ws281x.h create mode 100644 drivers/ws281x/Makefile create mode 100644 drivers/ws281x/atmega.c create mode 100644 drivers/ws281x/include/ws281x_constants.h create mode 100644 drivers/ws281x/include/ws281x_params.h create mode 100644 drivers/ws281x/ws281x.c diff --git a/drivers/Makefile.dep b/drivers/Makefile.dep index 4607c76281..9359dce9f1 100644 --- a/drivers/Makefile.dep +++ b/drivers/Makefile.dep @@ -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 diff --git a/drivers/Makefile.include b/drivers/Makefile.include index 3b8ea0fe1d..eda11cdcc7 100644 --- a/drivers/Makefile.include +++ b/drivers/Makefile.include @@ -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 diff --git a/drivers/include/ws281x.h b/drivers/include/ws281x.h new file mode 100644 index 0000000000..5d1b19ca5c --- /dev/null +++ b/drivers/include/ws281x.h @@ -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 + */ + +#ifndef WS281X_H +#define WS281X_H + +#include + +#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 */ +/** @} */ diff --git a/drivers/ws281x/Makefile b/drivers/ws281x/Makefile new file mode 100644 index 0000000000..de0e17ec3b --- /dev/null +++ b/drivers/ws281x/Makefile @@ -0,0 +1,3 @@ +SRC := ws281x.c # All other sources files provide ws281x_write as submodule +SUBMODULES := 1 +include $(RIOTBASE)/Makefile.base diff --git a/drivers/ws281x/atmega.c b/drivers/ws281x/atmega.c new file mode 100644 index 0000000000..f9b633466a --- /dev/null +++ b/drivers/ws281x/atmega.c @@ -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 + * + * @} + */ +#include +#include +#include +#include + +#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 +} diff --git a/drivers/ws281x/include/ws281x_constants.h b/drivers/ws281x/include/ws281x_constants.h new file mode 100644 index 0000000000..b80243c268 --- /dev/null +++ b/drivers/ws281x/include/ws281x_constants.h @@ -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 + */ + +#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 */ +/** @} */ diff --git a/drivers/ws281x/include/ws281x_params.h b/drivers/ws281x/include/ws281x_params.h new file mode 100644 index 0000000000..d668a0e170 --- /dev/null +++ b/drivers/ws281x/include/ws281x_params.h @@ -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 + */ + +#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 */ +/** @} */ diff --git a/drivers/ws281x/ws281x.c b/drivers/ws281x/ws281x.c new file mode 100644 index 0000000000..9624d2395f --- /dev/null +++ b/drivers/ws281x/ws281x.c @@ -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 + * + * @} + */ +#include +#include +#include +#include + +#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; +} diff --git a/makefiles/pseudomodules.inc.mk b/makefiles/pseudomodules.inc.mk index f54c5a2993..f7cafadfa6 100644 --- a/makefiles/pseudomodules.inc.mk +++ b/makefiles/pseudomodules.inc.mk @@ -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 From 74db50032f5aaf1f0cce324d52514a97e19ee99b Mon Sep 17 00:00:00 2001 From: Marian Buschsieweke Date: Tue, 12 Nov 2019 20:19:05 +0100 Subject: [PATCH 3/4] tests: Added test for the ws281x driver --- tests/driver_ws281x/Makefile | 19 ++++++ tests/driver_ws281x/main.c | 119 +++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 tests/driver_ws281x/Makefile create mode 100644 tests/driver_ws281x/main.c diff --git a/tests/driver_ws281x/Makefile b/tests/driver_ws281x/Makefile new file mode 100644 index 0000000000..2af7b097b5 --- /dev/null +++ b/tests/driver_ws281x/Makefile @@ -0,0 +1,19 @@ +BOARD ?= atmega328p +include ../Makefile.tests_common + +# Update this to your needs +PIN ?= GPIO_PIN(0, 0) +N ?= 8 + +USEMODULE += ws281x + +FEATURES_REQUIRED := arch_avr8 # <-- Currently only backend for AVR boards + +# Only AVR boards CPU clocked at 8MHz or 16 MHz are supported. The Waspmote Pro +# is clocked at 14.7456 MHz :-/ +BOARD_BLACKLIST := waspmote-pro + +include $(RIOTBASE)/Makefile.include + +CFLAGS += '-DWS281X_PARAM_PIN=$(PIN)' +CFLAGS += '-DWS281X_PARAM_NUMOF=$(N)' diff --git a/tests/driver_ws281x/main.c b/tests/driver_ws281x/main.c new file mode 100644 index 0000000000..21575846ab --- /dev/null +++ b/tests/driver_ws281x/main.c @@ -0,0 +1,119 @@ +/* + * 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 tests + * @{ + * + * @file + * @brief Test application for the WS281x RGB LED driver + * + * @author Marian Buschsieweke + * + * @} + */ + +#include + +#include "ws281x.h" +#include "ws281x_params.h" +#include "xtimer.h" + +static const color_rgb_t rainbow[] = { + {.r = 0x94, .g = 0x00, .b = 0xd3}, + {.r = 0x4b, .g = 0x00, .b = 0x82}, + {.r = 0x00, .g = 0x00, .b = 0xff}, + {.r = 0x00, .g = 0xff, .b = 0x00}, + {.r = 0xff, .g = 0xff, .b = 0x00}, + {.r = 0xff, .g = 0x7f, .b = 0xd3}, + {.r = 0xff, .g = 0x00, .b = 0x00}, +}; + +#define RAINBOW_LEN ARRAY_SIZE(rainbow) + +int main(void) +{ + ws281x_t dev; + int retval; + + puts( + "WS281x Test Application\n" + "=========================\n" + "\n" + "If you see an animated rainbow, the driver works as expected.\n" + "If the LEDs are flickering, check if the power supply is sufficient\n" + "(at least 4V). Also: The logic level has to be at least 0.7 * VDD,\n" + "so 3.3V logic with a 5V power supply is out of spec, but might work\n" + "OK.\n" + ); + + if (0 != (retval = ws281x_init(&dev, &ws281x_params[0]))) { + printf("Initialization failed with error code %d\n", retval); + return retval; + } + + while (1) { + unsigned offset = 0; + puts("Animation: Moving rainbow..."); + xtimer_ticks32_t last_wakeup = xtimer_now(); + for (unsigned i = 0; i < 100; i++) { + for (uint16_t j = 0; j < dev.params.numof; j++) { + ws281x_set(&dev, j, rainbow[(j + offset) % RAINBOW_LEN]); + } + offset++; + ws281x_write(&dev); + xtimer_periodic_wakeup(&last_wakeup, 100 * US_PER_MS); + } + + puts("Animation: Fading rainbow..."); + last_wakeup = xtimer_now(); + for (unsigned i = 0; i < RAINBOW_LEN; i++) { + for (unsigned j = 0; j < 100; j++) { + color_rgb_t col = { + .r = (uint8_t)(((unsigned)rainbow[i].r * j + 50) / 100), + .g = (uint8_t)(((unsigned)rainbow[i].g * j + 50) / 100), + .b = (uint8_t)(((unsigned)rainbow[i].b * j + 50) / 100), + }; + for (uint16_t k = 0; k < dev.params.numof; k++) { + ws281x_set(&dev, k, col); + } + ws281x_write(&dev); + xtimer_periodic_wakeup(&last_wakeup, 10 * US_PER_MS); + } + for (unsigned j = 100; j > 0; j--) { + color_rgb_t col = { + .r = (uint8_t)(((unsigned)rainbow[i].r * j + 50) / 100), + .g = (uint8_t)(((unsigned)rainbow[i].g * j + 50) / 100), + .b = (uint8_t)(((unsigned)rainbow[i].b * j + 50) / 100), + }; + for (uint16_t k = 0; k < dev.params.numof; k++) { + ws281x_set(&dev, k, col); + } + ws281x_write(&dev); + xtimer_periodic_wakeup(&last_wakeup, 10 * US_PER_MS); + } + } + + puts("Animation: 100 rainbows. (You'll need a long chain for this)"); + uint8_t buf[RAINBOW_LEN * WS281X_BYTES_PER_DEVICE]; + for (unsigned i = 0; i < RAINBOW_LEN; i++) { + ws281x_set_buffer(buf, i, rainbow[i]); + } + + ws281x_prepare_transmission(&dev); + for (unsigned i = 0; i < 100; i++) { + ws281x_write_buffer(&dev, buf, sizeof(buf)); + } + ws281x_end_transmission(&dev); + + /* wait some time to allow testers to verify the result */ + xtimer_sleep(5); + } + + return 0; +} From 8d0a9ead7b474d41efcee66925b7700b3942ca99 Mon Sep 17 00:00:00 2001 From: Marian Buschsieweke Date: Sat, 16 Nov 2019 19:09:29 +0100 Subject: [PATCH 4/4] cpu/atmega_common/periph: Fixed typo in gpio.c --- cpu/atmega_common/periph/gpio.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpu/atmega_common/periph/gpio.c b/cpu/atmega_common/periph/gpio.c index 1a5066e64f..b278407489 100644 --- a/cpu/atmega_common/periph/gpio.c +++ b/cpu/atmega_common/periph/gpio.c @@ -65,7 +65,7 @@ static gpio_isr_ctx_t config[GPIO_EXT_INT_NUMOF]; /** - * @brief detects ammount of possible PCINTs + * @brief detects amount of possible PCINTs */ #if defined(MODULE_ATMEGA_PCINT0) || defined(MODULE_ATMEGA_PCINT1) || \ defined(MODULE_ATMEGA_PCINT2) || defined(MODULE_ATMEGA_PCINT3)