1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2024-12-29 04:50:03 +01:00

Merge pull request #19891 from chrysn-pull-requests/ws2181x_timer

drivers/ws281x: Add gpio_ll and timer based driver
This commit is contained in:
Marian Buschsieweke 2024-01-03 09:22:26 +00:00 committed by GitHub
commit f860d96a25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 419 additions and 3 deletions

View File

@ -56,6 +56,14 @@ extern "C" {
#define BTN1_MODE GPIO_IN_PU
/** @} */
/**
* @name WS281x RGB LED configuration
* @{
*/
#define WS281X_TIMER_DEV TIMER_DEV(1) /**< Timer device */
#define WS281X_TIMER_MAX_VALUE TIMER_1_MAX_VALUE /**< Timer max value */
/** @} */
#ifdef __cplusplus
}
#endif

View File

@ -49,6 +49,11 @@ static const timer_conf_t timer_config[] = {
#define TIMER_0_ISR isr_timer0
#define TIMER_1_ISR isr_timer1
/** See @ref timer_init */
#define TIMER_0_MAX_VALUE 0xffffffff
/** See @ref timer_init */
#define TIMER_1_MAX_VALUE 0xffffffff
#define TIMER_NUMOF ARRAY_SIZE(timer_config)
/** @} */

View File

@ -56,6 +56,13 @@ static const timer_conf_t timer_config[] = {
#define TIMER_1_ISR isr_timer1
#define TIMER_2_ISR isr_timer2
/** See @ref timer_init */
#define TIMER_0_MAX_VALUE 0xffffffff
/** See @ref timer_init */
#define TIMER_1_MAX_VALUE 0xffffffff
/** See @ref timer_init */
#define TIMER_2_MAX_VALUE 0xffffffff
#define TIMER_NUMOF ARRAY_SIZE(timer_config)
/** @} */

View File

@ -80,6 +80,21 @@ static const timer_conf_t timer_config[] = {
#define TIMER_2_ISR isr_timer3
#define TIMER_3_ISR isr_timer4
/** See @ref timer_init */
#define TIMER_0_MAX_VALUE 0xffffffff
/** See @ref timer_init */
#define TIMER_1_MAX_VALUE 0xffffffff
#ifdef NRF_TIMER3
/** See @ref timer_init */
#define TIMER_2_MAX_VALUE 0xffffffff
#endif
/* If there is no NRF_TIMER3 this should be TIMER_2 because the index shifts
* up, but there is only a TIMER4 if there is a TIMER3 too. */
#ifdef NRF_TIMER4
/** See @ref timer_init */
#define TIMER_3_MAX_VALUE 0xffffffff
#endif
#define TIMER_NUMOF ARRAY_SIZE(timer_config)
/** @} */

View File

@ -87,6 +87,14 @@ extern "C" {
#define BTN3_MODE GPIO_IN /**< BTN3 default mode */
/** @} */
/**
* @name WS281x RGB LED configuration
* @{
*/
#define WS281X_TIMER_DEV TIMER_DEV(1) /**< Timer device */
#define WS281X_TIMER_MAX_VALUE TIMER_1_MAX_VALUE /**< Timer max value */
/** @} */
#ifdef __cplusplus
}
#endif

View File

@ -83,6 +83,11 @@ static const timer_conf_t timer_config[] = {
#define TIMER_0_ISR isr_timer0 /**< Timer0 IRQ*/
#define TIMER_1_ISR isr_timer1 /**< Timer1 IRQ */
/** See @ref timer_init */
#define TIMER_0_MAX_VALUE 0xffffffff
/** See @ref timer_init */
#define TIMER_1_MAX_VALUE 0xffffffff
#define TIMER_NUMOF ARRAY_SIZE(timer_config) /**< Timer configuration NUMOF */
/** @} */

View File

@ -16,6 +16,7 @@ config CPU_FAM_NRF53
select HAS_PERIPH_GPIO
select HAS_PERIPH_GPIO_IRQ
select HAS_PERIPH_TIMER_PERIODIC
select HAS_PERIPH_TIMER_POLL
select HAS_PERIPH_TIMER_QUERY_FREQS
select HAS_PERIPH_UART_MODECFG
select HAS_PERIPH_WDT

View File

@ -20,6 +20,7 @@ depends on !CPU_FAM_NRF53
select HAS_PERIPH_HWRNG
select HAS_PERIPH_TEMPERATURE
select HAS_PERIPH_TIMER_PERIODIC
select HAS_PERIPH_TIMER_POLL
select HAS_PERIPH_TIMER_QUERY_FREQS
select HAS_PERIPH_RTT_OVERFLOW
select HAS_PERIPH_UART_MODECFG

View File

@ -5,6 +5,7 @@ FEATURES_PROVIDED += periph_flashpage_in_address_space
FEATURES_PROVIDED += periph_flashpage_pagewise
FEATURES_PROVIDED += periph_gpio periph_gpio_irq
FEATURES_PROVIDED += periph_timer_periodic
FEATURES_PROVIDED += periph_timer_poll
FEATURES_PROVIDED += periph_timer_query_freqs
FEATURES_PROVIDED += periph_uart_modecfg
FEATURES_PROVIDED += periph_wdt periph_wdt_cb

View File

@ -0,0 +1,42 @@
/*
* Copyright (C) 2015 Jan Wagner <mail@jwagner.eu>
* 2015-2016 Freie Universität Berlin
* 2019 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_nrf5x_common
* @ingroup drivers_periph_timer
* @{
*
* @file
* @brief CPU specific part of the timer API
*
* @author Christian Amsüss <chrysn@fsfe.org>
*/
#ifndef TIMER_ARCH_H
#define TIMER_ARCH_H
#ifdef __cplusplus
extern "C" {
#endif
#ifndef DOXYGEN /* hide implementation specific details from Doxygen */
static inline bool timer_poll_channel(tim_t tim, int channel)
{
return timer_config[tim].dev->EVENTS_COMPARE[channel];
}
#endif /* DOXYGEN */
#ifdef __cplusplus
}
#endif
#endif /* TIMER_ARCH_H */
/** @} */

View File

@ -111,7 +111,9 @@ int timer_init(tim_t tim, uint32_t freq, timer_cb_t cb, void *arg)
}
/* enable interrupts */
NVIC_EnableIRQ(timer_config[tim].irqn);
if (cb != NULL) {
NVIC_EnableIRQ(timer_config[tim].irqn);
}
/* start the timer */
dev(tim)->TASKS_START = 1;

View File

@ -17,6 +17,7 @@ config CPU_FAM_NRF9160
select HAS_PERIPH_GPIO_LL_IRQ
select HAS_PERIPH_GPIO_LL_IRQ_UNMASK
select HAS_PERIPH_TIMER_PERIODIC
select HAS_PERIPH_TIMER_POLL
select HAS_PERIPH_TIMER_QUERY_FREQS
select HAS_PERIPH_UART_MODECFG
select HAS_PERIPH_SPI_GPIO_MODE

View File

@ -35,6 +35,7 @@
#include <limits.h>
#include <stdint.h>
#include <stdbool.h>
#include "architecture.h"
#include "periph_cpu.h"
@ -295,6 +296,39 @@ uword_t timer_query_channel_numof(tim_t dev);
*/
uint32_t timer_query_freqs(tim_t dev, uword_t index);
#if defined(DOXYGEN)
/**
* @brief Check whether a compare channel has matched
*
* @return true once after the channel has matched.
*
* It is currently not defined whether this keeps returning true after a
* channel has been polled until that channel is set, or whether later calls
* return false.
*
* This is typically used in spin loops that wait for a timer's completion:
*
* ~~~
* while (!timer_poll_channel(tim, chan)) {};
* ~~~
*
* This function is only available on platforms that implement the
* `periph_timer_poll` peripheral in addition to `periph_timer`.
*
*/
/* As this function is polled, it needs to be inlined, so it is typically
* provided through timer_arch.h. If a platform ever does not need to go
* through static inline here, this declaration's condition can be extended to
* be `(defined(MODULE_PERIPH_TIMER_POLL) &&
* !defined(PERIPH_TIMER_PROVIDES_INLINE_POLL_CHANNEL) || defined(DOXYGEN)` or
* similar. */
bool timer_poll_channel(tim_t dev, int channel);
#endif
#if defined(MODULE_PERIPH_TIMER_POLL)
#include "timer_arch.h"
#endif
#ifdef __cplusplus
}
#endif

View File

@ -78,6 +78,8 @@ rsource "Kconfig.flashpage"
rsource "Kconfig.gpio"
rsource "Kconfig.gpio_ll"
config MODULE_PERIPH_HWRNG
bool "HWRNG peripheral driver"
depends on HAS_PERIPH_HWRNG
@ -157,6 +159,12 @@ config MODULE_PERIPH_INIT_RTT
default y if MODULE_PERIPH_INIT
depends on MODULE_PERIPH_RTT
config MODULE_PERIPH_TIMER_POLL
bool "Timer poll"
depends on HAS_PERIPH_TIMER_POLL
help
Enables the timer_poll_channel function.
rsource "Kconfig.sdmmc"
rsource "Kconfig.spi"

View File

@ -0,0 +1,22 @@
# Copyright (c) 2023 HAW Hamburg
#
# 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.
#
menuconfig MODULE_PERIPH_GPIO_LL
bool "Low-level GPIO peripheral driver"
depends on HAS_PERIPH_GPIO_LL
if MODULE_PERIPH_GPIO_LL
config MODULE_PERIPH_GPIO_LL_IRQ_UNMASK
bool "Unmask GPIO peripheral interrupts"
default y
depends on HAS_PERIPH_GPIO_LL_IRQ_UNMASK
help
Enables GPIO peripheral unmasking interrupts without
clearing pending IRQs that came in while masked.
endif # MODULE_PERIPH_GPIO_LL

View File

@ -7,12 +7,13 @@
config MODULE_WS281X
bool "WS2812/SK6812 RGB LED (NeoPixel)"
depends on HAS_CPU_CORE_ATMEGA || HAS_ARCH_ESP32 || HAS_ARCH_NATIVE
depends on HAS_CPU_CORE_ATMEGA || HAS_ARCH_ESP32 || HAS_ARCH_NATIVE || HAS_PERIPH_TIMER_POLL
depends on TEST_KCONFIG
select MODULE_XTIMER
select MODULE_WS281X_ATMEGA if HAS_CPU_CORE_ATMEGA
select MODULE_WS281X_VT100 if HAS_ARCH_NATIVE
select MODULE_WS281X_ESP32 if HAS_ARCH_ESP32
select MODULE_WS281X_TIMER_GPIO_LL if HAS_PERIPH_TIMER_POLL
config MODULE_WS281X_ATMEGA
bool
@ -45,6 +46,16 @@ config MODULE_WS281X_ESP32_SW
Use the bit-banging software implementation to generate the RGB LED
signal.
config MODULE_WS281X_TIMER_GPIO_LL
bool
depends on HAS_PERIPH_TIMER_POLL
select MODULE_PERIPH_TIMER_POLL
depends on HAS_PERIPH_GPIO_LL
select MODULE_PERIPH_GPIO_LL
help
Use a platform independent bit-banging software implementation to
generate the RGB LED signal.
config HAVE_WS281X
bool
help

View File

@ -1,4 +1,5 @@
FEATURES_REQUIRED_ANY += cpu_core_atmega|arch_esp32|arch_native
# 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
ifeq (,$(filter ws281x_%,$(USEMODULE)))
ifneq (,$(filter cpu_core_atmega,$(FEATURES_USED)))
@ -10,6 +11,10 @@ ifeq (,$(filter ws281x_%,$(USEMODULE)))
ifneq (,$(filter arch_esp32,$(FEATURES_USED)))
USEMODULE += ws281x_esp32
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
endif
endif
ifneq (,$(filter ws281x_atmega,$(USEMODULE)))
@ -28,3 +33,7 @@ ifneq (,$(filter ws281x_esp32%,$(USEMODULE)))
USEMODULE += ws281x_esp32_hw
endif
endif
ifneq (,$(filter ws281x_timer_gpio_ll,$(USEMODULE)))
FEATURES_REQUIRED += periph_gpio_ll periph_timer periph_timer_poll
endif

View File

@ -51,6 +51,15 @@ extern "C" {
#endif
/** @} */
/**
* @name Properties of the timer_gpio_ll backend.
* @{
*/
#ifdef MODULE_WS281X_TIMER_GPIO_LL
#define WS281X_HAVE_INIT (1)
#endif
/** @} */
#ifdef __cplusplus
}
#endif

View File

@ -64,6 +64,47 @@ static const ws281x_params_t ws281x_params[] =
WS281X_PARAMS
};
/** @brief Timer used for WS281x (by the timer_gpio_ll implementation)
*
* A single timer is configured for any number of WS281x strands, so this does
* not need to be part of params.
*
* It is required that the timer has at least 2 channels. (Future versions may
* require a 3rd channel).
*
* It is required that the timer's MAX_VALUE is 2^n-1, which is a trivial but
* not explicitly stated case.
*
* This timer is configured at WS281x initialization time, and kept stopped
* outside of transmissions.
*
* The default value of 2 is chosen because the only platform on which the
* module is usable is nRF5x, where TIMER_DEV(1) is in use by the radio module.
* It is strongly advised to explicitly set this timer to a known free timer,
* as the default may change without notice.
* */
#if !defined(WS281X_TIMER_DEV) || defined(DOXYGEN)
#define WS281X_TIMER_DEV TIMER_DEV(2)
#endif
/** @brief Maximum value of the timer used for WS281x (by the timer_gpio_ll implementation)
*
* This macro needs to be defined to the `TIMER_x_MAX_VALUE` corresponding to
* the `TIMER_DEV(x)` in @ref WS281X_TIMER_DEV.
* */
#ifndef WS281X_TIMER_MAX_VALUE
#define WS281X_TIMER_MAX_VALUE TIMER_2_MAX_VALUE
#endif
/** @brief Frequency for the timer used for WS281x (by the timer_gpio_ll implementation)
*
* This should be set to a frequency that is a close multiple of 3MHz,
* depending on the precise low and high times. A value of 16MHz works well.
* */
#ifndef WS281X_TIMER_FREQ
#define WS281X_TIMER_FREQ 16000000
#endif
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,169 @@
/*
* Copyright 2023 Christian Amsüss
*
* 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
*
* When this is used, the whole @ref ws281x_write_buffer operation is conducted
* in a single interrupts-disabled run. As it takes about 1us per bit (eg.
* 0.3ms on a 100-LED strip), the use of this module needs to be carefully
* evaluated against other real-time requirements.
*
* (Letting the trailing pause run with interrupts enabled, or even enabling
* interrupts between bits, is an option that is being considered for future
* expansion).
*
* @author Christian Amsüss <chrysn@fsfe.org>
*
* @}
*/
#include <assert.h>
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include "irq.h"
#include "time_units.h"
#include "periph/timer.h"
#include "periph/gpio_ll.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;
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;
/* The logical sequence for a cycle would be to set high, sleep, set low,
* sleep. But the critical time is the high time, so we do something
* different: We go low, set up the timer, and then go high and back low
* both as controlled by the timer. That means that our logical cycles are
* the previous bit's low time followed by the current bit's high time -- so we store the previous bit here.
*
* It is initialized to 1 because the 1 has the longer high time, so the
* lower low time -- we just spend 325 instead of 600ns before actually
* sending the message. (This wait-before-we-start time is also part of how
* this implementation can get away without compensation for the time it
* takes to set up timers: it sets them up and then waits for some time
* that is known to be long enough, because that time also works
* mid-transmission). */
bool last_bit = 1;
gpio_port_t port = gpio_get_port(dev->params.pin);
uword_t mask = 1 << gpio_get_pin_num(dev->params.pin);
/* If a too-slow channel is used, then the critical section could be done
* away with: If an interrupt fires (or a higher priority thread runs;
* really: runs long enough to matter, but that'd need very fast switching
* and hundreds of MHz to be the case), the function will return with an
* error (or ignore the error), and the worst thing that'd happen is that
* if it hits the last bit of a LED, that might be flipped to a value 1.
* */
unsigned int irq_state = irq_disable();
while (pos < end) {
uint8_t data = *pos;
for (uint8_t cnt = 8; cnt > 0; cnt--) {
bool bit = data >> 7;
data <<= 1;
int time_to_set = last_bit ? (ticks_data - ticks_one) : (ticks_data - ticks_zero);
int time_to_clear = time_to_set + (bit ? ticks_one : ticks_zero);
/* I'd prefer to just zero the timer, but we don't have API for that */
int now = timer_read(WS281X_TIMER_DEV);
time_to_set = (time_to_set + now) & WS281X_TIMER_MAX_VALUE;
time_to_clear = (time_to_clear + now) & WS281X_TIMER_MAX_VALUE;
timer_set_absolute(WS281X_TIMER_DEV, 0, time_to_set);
timer_set_absolute(WS281X_TIMER_DEV, 1, time_to_clear);
timer_start(WS281X_TIMER_DEV);
/* A
* ~~~
* while ((int)timer_read(WS281X_TIMER_DEV) < time_to_set) {};
* ~~~
* would be way too slow, plus it'd impose additional requirements
* on the timer to avoid wrapping */
while (!timer_poll_channel(WS281X_TIMER_DEV, 0)) {}
gpio_ll_set(port, mask);
while (!timer_poll_channel(WS281X_TIMER_DEV, 1)) {}
gpio_ll_clear(port, mask);
timer_stop(WS281X_TIMER_DEV);
last_bit = bit;
}
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, params->pin, err);
if (err != 0) {
return -EIO;
}
err = timer_init(WS281X_TIMER_DEV, WS281X_TIMER_FREQ, NULL, NULL);
DEBUG("Initialized timer to %d Hz: %d\n", WS281X_TIMER_FREQ, err);
if (err != 0) {
return -EIO;
}
timer_stop(WS281X_TIMER_DEV);
/* We're not trying to make an assessment on whether that means we can
* manage or not, for that also depends on the number of instructions in
* the loop, which adds to the granularity. (Really the relevant time is
* the duration converted back from ticks to time, rounded up or down
* randomly to the duration of the loop). */
DEBUG("At this speed, ones are %d ticks long and zeros %d ticks of a %d "
"tick transmission.\n", ticks_one, ticks_zero, ticks_data);
return 0;
}

View File

@ -572,6 +572,12 @@ config HAS_PERIPH_TIMER_PERIODIC
Indicates that the Timer peripheral provides the periodic timeout
functionality.
config HAS_PERIPH_TIMER_POLL
bool
help
Indicates that the Timer peripheral supports the timer_poll_channel
function.
config HAS_PERIPH_TIMER_QUERY_FREQS
bool
help

View File

@ -47,6 +47,7 @@ PERIPH_IGNORE_MODULES := \
periph_rtt_hw_rtc \
periph_rtt_hw_sys \
periph_spi_on_qspi \
periph_timer_poll \
periph_timer_query_freqs \
periph_uart_collision \
periph_uart_rxstart_irq \

View File

@ -4,6 +4,10 @@ include ../Makefile.drivers_common
# Update this to your needs
# PIN ?= GPIO_PIN(0, 0)
# N ?= 8
# When using the ws281x_timer_gpio_ll module, you'll also need to define those:
# (Example values are suitable for nRF52 devices)
# TIMER ?= 2
# FREQ ?= 16000000
USEMODULE += ws281x
USEMODULE += xtimer
@ -22,3 +26,9 @@ endif
ifneq (, $(N))
CFLAGS += '-DWS281X_PARAM_NUMOF=$(N)'
endif
ifneq (, $(TIMER))
CFLAGS += '-DWS281X_TIMER_DEV=TIMER_DEV($(TIMER))' '-DWS281X_TIMER_MAX_VALUE=TIMER_$(TIMER)_MAX_VALUE'
endif
ifneq (, $(FREQ))
CFLAGS += '-DWS281X_TIMER_FREQ=$(FREQ)'
endif