mirror of
https://github.com/RIOT-OS/RIOT.git
synced 2024-12-29 04:50:03 +01:00
drivers/ws281x: add SysTick + GPIO LL backend
This backend is largely inspired by the `periph_timer_poll` + GPIO LL driver, but uses the SysTick timer instead of the `periph_timer`. The main advantage is that this (hopefully) works across Cortex M MCUs with no configuration other than the GPIO pin needed.
This commit is contained in:
parent
6f6d93b9a8
commit
51d2b85f1b
@ -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
|
||||
|
@ -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
|
||||
|
134
drivers/ws281x/systick_gpio_ll.c
Normal file
134
drivers/ws281x/systick_gpio_ll.c
Normal file
@ -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 <marian.buschsieweke@posteo.net>
|
||||
*
|
||||
* @}
|
||||
*/
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user