diff --git a/drivers/ws281x/Makefile.dep b/drivers/ws281x/Makefile.dep index 31f00f6c33..fb47552808 100644 --- a/drivers/ws281x/Makefile.dep +++ b/drivers/ws281x/Makefile.dep @@ -1,5 +1,5 @@ # Actually |(periph_timer_poll and periph_gpio_ll), but that's too complex for FEATURES_REQUIRED_ANY to express -FEATURES_REQUIRED_ANY += cpu_core_atmega|arch_esp32|arch_native|periph_timer_poll +FEATURES_REQUIRED_ANY += cpu_core_atmega|arch_esp32|arch_native|periph_systick|periph_timer_poll ifeq (,$(filter ws281x_%,$(USEMODULE))) ifneq (,$(filter cpu_core_atmega,$(FEATURES_USED))) @@ -11,6 +11,9 @@ ifeq (,$(filter ws281x_%,$(USEMODULE))) ifneq (,$(filter arch_esp32,$(FEATURES_USED))) USEMODULE += ws281x_esp32 endif + ifneq (,$(filter periph_systick,$(FEATURES_USED))) + USEMODULE += ws281x_systick_gpio_ll + endif # Not only looking for the used feature but also for the absence of any more specific driver ifeq (-periph_timer_poll,$(filter ws281x_%,$(USEMODULE))-$(filter periph_timer_poll,$(FEATURES_USED))) USEMODULE += ws281x_timer_gpio_ll @@ -38,5 +41,9 @@ ifneq (,$(filter ws281x_timer_gpio_ll,$(USEMODULE))) FEATURES_REQUIRED += periph_gpio_ll periph_timer periph_timer_poll endif +ifneq (,$(filter ws281x_systick_gpio_ll,$(USEMODULE))) + FEATURES_REQUIRED += periph_gpio_ll periph_systick +endif + # It would seem xtimer is always required as it is used in the header... USEMODULE += xtimer diff --git a/drivers/ws281x/include/ws281x_backend.h b/drivers/ws281x/include/ws281x_backend.h index 01f74fb877..0a80addbfa 100644 --- a/drivers/ws281x/include/ws281x_backend.h +++ b/drivers/ws281x/include/ws281x_backend.h @@ -60,6 +60,15 @@ extern "C" { #endif /** @} */ +/** + * @name Properties of the systick_gpio_ll backend. + * @{ + */ +#ifdef MODULE_WS281X_SYSTICK_GPIO_LL +#define WS281X_HAVE_INIT (1) +#endif +/** @} */ + #ifdef __cplusplus } #endif diff --git a/drivers/ws281x/systick_gpio_ll.c b/drivers/ws281x/systick_gpio_ll.c new file mode 100644 index 0000000000..3d9a879483 --- /dev/null +++ b/drivers/ws281x/systick_gpio_ll.c @@ -0,0 +1,134 @@ +/* + * Copyright 2024 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 the WS281x abstraction based on GPIO_LL and timers + * + * @author Marian Buschsieweke + * + * @} + */ +#include +#include +#include +#include + +#include "clk.h" +#include "cpu.h" +#include "irq.h" +#include "macros/math.h" +#include "periph/gpio_ll.h" +#include "time_units.h" + +#include "ws281x.h" +#include "ws281x_params.h" +#include "ws281x_constants.h" + +#define ENABLE_DEBUG 0 +#include "debug.h" + +/* (+ NS_PER_SEC - 1): Rounding up, as T1H is the time that needs to distinctly + * longer than T0H. + * + * Then adding +1 extra, because the spin loop adds another layer of jitter. A + * more correct version would be to add the spin loop time before rounding (and + * then rounding up), but as that time is not available, spending one more + * cycle is the next best thing to do. */ +const int ticks_one = ((uint64_t)WS281X_T_DATA_ONE_NS * WS281X_TIMER_FREQ + NS_PER_SEC - 1) + / NS_PER_SEC + 1; +/* Rounding down, zeros are better shorter */ +const int ticks_zero = (uint64_t)WS281X_T_DATA_ZERO_NS * (uint64_t)WS281X_TIMER_FREQ / NS_PER_SEC; +/* No particular known requirements, but we're taking longer than that anyway + * because we don't clock the times between bits. */ +const int ticks_data = (uint64_t)WS281X_T_DATA_NS * (uint64_t)WS281X_TIMER_FREQ / NS_PER_SEC; + +static void _systick_start(uint32_t ticks) +{ + /* disable SysTick, clear value */ + SysTick->CTRL = 0; + SysTick->VAL = 0; + /* prepare value in re-load register */ + SysTick->LOAD = ticks; + /* start and wait for the load value to be applied */ + SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk; + while (SysTick->VAL == 0) { /* Wait for SysTick to start and spin */ } +} + +static void _systick_wait(void) +{ + while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk)) { /* busy wait */ } +} + +void ws281x_write_buffer(ws281x_t *dev, const void *buf, size_t size) +{ + assert(dev); + + /* the high time for one can be as high as 5 seconds in practise, so + * rather be on the high side by adding a few CPU cycles. */ + const uint32_t ticks_one = DIV_ROUND_UP((uint64_t)WS281X_T_DATA_ONE_NS * (uint64_t)coreclk(), NS_PER_SEC) + 16; + /* the low time should rather be on the short side, so rounding down */ + const uint32_t ticks_zero = (uint64_t)(WS281X_T_DATA_ZERO_NS - 50) * (uint64_t)coreclk() / NS_PER_SEC; + /* the remaining time doesn't matter to much, should only be enough for the + * LEDs to detect the low phase. And not way to much to be detected as + * reset */ + const uint32_t ticks_bit = DIV_ROUND((uint64_t)WS281X_T_DATA_NS * (uint64_t)coreclk(), NS_PER_SEC); + + const uint8_t *pos = buf; + const uint8_t *end = pos + size; + + gpio_port_t port = gpio_get_port(dev->params.pin); + uword_t mask = 1U << gpio_get_pin_num(dev->params.pin); + + unsigned irq_state = irq_disable(); + while (pos < end) { + uint8_t data = *pos; + for (uint8_t cnt = 8; cnt > 0; cnt--) { + uint32_t ticks_high = (data & 0x80) ? ticks_one : ticks_zero; + uint32_t ticks_low = ticks_bit - ticks_high; + _systick_start(ticks_high); + gpio_ll_set(port, mask); + _systick_wait(); + gpio_ll_clear(port, mask); + _systick_start(ticks_low); + _systick_wait(); + data <<= 1; + } + pos++; + } + + irq_restore(irq_state); +} + +int ws281x_init(ws281x_t *dev, const ws281x_params_t *params) +{ + int err; + + if (!dev || !params || !params->buf) { + return -EINVAL; + } + + memset(dev, 0, sizeof(ws281x_t)); + dev->params = *params; + + gpio_port_t port = gpio_get_port(dev->params.pin); + uint8_t pin = gpio_get_pin_num(dev->params.pin); + + err = gpio_ll_init(port, pin, gpio_ll_out); + DEBUG("Initializing port %x pin %d (originally %x): %d\n", + port, pin, (unsigned)params->pin, err); + if (err != 0) { + return -EIO; + } + + return 0; +}