Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion cpu/sam0_common/periph/spi.c
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,16 @@ static void _spi_acquire(spi_t bus, spi_mode_t mode, spi_clk_t clk)
* equation is modified to
* BAUD.reg = ((f_ref + f_bus) / (2 * f_bus) - 1) */
const uint8_t baud = (gclk_src + clk) / (2 * clk) - 1;

#if ENABLE_DEBUG
/* compute actual SPI clock */
uint32_t spi_actual = gclk_src / (2U * (baud + 1U));
DEBUG("SPI bus %u: requested %lu Hz, gclk %lu Hz, BAUD %u -> actual %lu Hz\n",
bus,
(unsigned long)clk,
(unsigned long)gclk_src,
baud,
(unsigned long)spi_actual);
#endif
/* configure device to be master and set mode and pads,
*
* NOTE: we could configure the pads already during spi_init, but for
Expand Down
17 changes: 16 additions & 1 deletion drivers/ws281x/Makefile.dep
Original file line number Diff line number Diff line change
@@ -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_timer_poll|periph_spi
Comment thread
fabian18 marked this conversation as resolved.

ifeq (,$(filter ws281x_%,$(USEMODULE)))
ifneq (,$(filter cpu_core_atmega,$(FEATURES_USED)))
Expand All @@ -15,6 +15,15 @@ ifeq (,$(filter ws281x_%,$(USEMODULE)))
ifeq (-periph_timer_poll,$(filter ws281x_%,$(USEMODULE))-$(filter periph_timer_poll,$(FEATURES_USED)))
USEMODULE += ws281x_timer_gpio_ll
endif
ifeq (,$(filter ws281x_%,$(USEMODULE)))
# No backend found yet.
# Select SPI backend as last resort but fail feature check for missing features later.
# If testing for all necessary features, CI will fail because support is claimed
# when only periph_spi is available.
ifneq (,$(filter periph_spi,$(FEATURES_USED)))
USEMODULE += ws281x_spi
endif
endif
endif

ifneq (,$(filter ws281x_atmega,$(USEMODULE)))
Expand All @@ -38,5 +47,11 @@ ifneq (,$(filter ws281x_timer_gpio_ll,$(USEMODULE)))
FEATURES_REQUIRED += periph_gpio_ll periph_timer periph_timer_poll
endif

ifneq (,$(filter ws281x_spi,$(USEMODULE)))
FEATURES_REQUIRED += periph_spi
FEATURES_REQUIRED += periph_spi_reconfigure
FEATURES_REQUIRED += periph_dma
Comment thread
maribu marked this conversation as resolved.
endif

# It would seem xtimer is always required as it is used in the header...
USEMODULE += xtimer
21 changes: 16 additions & 5 deletions drivers/ws281x/include/ws281x_backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ extern "C" {
* @{
*/
#ifdef MODULE_WS281X_ATMEGA
#define WS281X_HAVE_INIT (1)
# define WS281X_HAVE_INIT (1)
#endif
/** @} */

Expand All @@ -36,7 +36,7 @@ extern "C" {
* @{
*/
#ifdef MODULE_WS281X_ESP32
#define WS281X_HAVE_INIT (1)
# define WS281X_HAVE_INIT (1)
#endif
/** @} */

Expand All @@ -45,8 +45,8 @@ extern "C" {
* @{
*/
#ifdef MODULE_WS281X_VT100
#define WS281X_HAVE_PREPARE_TRANSMISSION (1)
#define WS281X_HAVE_END_TRANSMISSION (1)
# define WS281X_HAVE_PREPARE_TRANSMISSION (1)
# define WS281X_HAVE_END_TRANSMISSION (1)
#endif
/** @} */

Expand All @@ -55,7 +55,18 @@ extern "C" {
* @{
*/
#ifdef MODULE_WS281X_TIMER_GPIO_LL
#define WS281X_HAVE_INIT (1)
# define WS281X_HAVE_INIT (1)
#endif
/** @} */

/**
* @name Properties of the SPI backend.
* @{
*/
#ifdef MODULE_WS281X_SPI
# define WS281X_HAVE_INIT (1)
# define WS281X_HAVE_PREPARE_TRANSMISSION (1)
# define WS281X_HAVE_END_TRANSMISSION (1)
#endif
/** @} */

Expand Down
8 changes: 6 additions & 2 deletions drivers/ws281x/include/ws281x_constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,12 @@ extern "C" {
* @brief The high-times in nanoseconds.
* @{
*/
#define WS281X_T_DATA_ONE_NS (650U)
#define WS281X_T_DATA_ZERO_NS (325U)
#ifndef WS281X_T_DATA_ONE_NS
# define WS281X_T_DATA_ONE_NS (650U)
#endif
#ifndef WS281X_T_DATA_ZERO_NS
# define WS281X_T_DATA_ZERO_NS (325U)
#endif
Comment thread
maribu marked this conversation as resolved.
/**@}*/

/**
Expand Down
34 changes: 26 additions & 8 deletions drivers/ws281x/include/ws281x_params.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,24 @@
* @{
*/
#ifndef WS281X_PARAM_PIN
#define WS281X_PARAM_PIN (GPIO_PIN(0, 0)) /**< GPIO pin connected to the data pin of the first LED */
# define WS281X_PARAM_PIN (GPIO_PIN(0, 0)) /**< GPIO pin connected to the data pin of the first LED */

Check warning on line 37 in drivers/ws281x/include/ws281x_params.h

View workflow job for this annotation

GitHub Actions / static-tests

line is longer than 100 characters
#endif
#ifndef WS281X_PARAM_NUMOF
#define WS281X_PARAM_NUMOF (8U) /**< Number of LEDs chained */
# 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 */
# define WS281X_PARAM_BUF (ws281x_buf) /**< Data buffer holding LED states */
#endif

#ifndef WS281X_PARAMS
/**
* @brief WS281x initialization parameters
*/
#define WS281X_PARAMS { \
# define WS281X_PARAMS { \
.pin = WS281X_PARAM_PIN, \
.numof = WS281X_PARAM_NUMOF, \
.buf = WS281X_PARAM_BUF, \
Expand Down Expand Up @@ -87,15 +87,15 @@
* as the default may change without notice.
* */
#if !defined(WS281X_TIMER_DEV) || defined(DOXYGEN)
#define WS281X_TIMER_DEV TIMER_DEV(2)
# 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 maximum value of @ref WS281X_TIMER_DEV.
* */
#ifndef WS281X_TIMER_MAX_VALUE
#define WS281X_TIMER_MAX_VALUE UINT_MAX
# define WS281X_TIMER_MAX_VALUE UINT_MAX
#endif

/** @brief Frequency for the timer used for WS281x (by the timer_gpio_ll implementation)
Expand All @@ -104,14 +104,32 @@
* depending on the precise low and high times. A value of 16MHz works well.
* */
#ifndef WS281X_TIMER_FREQ
#define WS281X_TIMER_FREQ 16000000
# define WS281X_TIMER_FREQ 16000000
#endif

/**
* @brief SPI device to use for WS281x RGB LED data transmission
*
* This SPI must support DMA.
*/
#ifndef WS281X_SPI_DEV
# define WS281X_SPI_DEV SPI_DEV(0)
#endif

/**
* @brief SPI clock speed: 3.2 MHz → 312.5 ns per SPI bit
*
* 4 SPI bits add up to 1.25 µs period, which is the time to transmit one WS281x bit.
*/
#ifndef WS281X_SPI_CLK
# define WS281X_SPI_CLK 3200000
#endif

/**
* @brief SAUL info
*/
#ifndef WS281X_SAUL_INFO
#define WS281X_SAUL_INFO { .name = "WS281X RGB LED" }
# define WS281X_SAUL_INFO { .name = "WS281X RGB LED" }
#endif

/**
Expand Down
199 changes: 199 additions & 0 deletions drivers/ws281x/spi.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
* SPDX-FileCopyrightText: 2026 ML!PA Consulting GmbH.
* SPDX-License-Identifier: LGPL-2.1-only
*/

/**
* @ingroup drivers_ws281x
* @{
*
* @file
* @brief Implementation of WS281x interface through SPI
*
* @author Hugues Larrive <hlarrive@pm.me>
* @author Fabian Hüßler <fabian.huessler@ml-pa.com>
*
* @}
*/

#include <assert.h>
#include <errno.h>
#include <stdint.h>
#include <string.h>

#include "compiler_hints.h"
#include "macros/math.h"
#include "od.h"
#include "periph/gpio.h"
#include "periph/spi.h"
#include "string_utils.h"
#include "time_units.h"
#include "ztimer.h"

#include "ws281x.h"
#include "ws281x_constants.h"
#include "ws281x_params.h"

#define ENABLE_DEBUG 0
#include "debug.h"

/* Trying to reduce the number of parameters, but you can still overwrite them */
/* Duration of one SPI bit in nanoseconds */
#define _SPI_NS_PER_BIT (NS_PER_SEC / WS281X_SPI_CLK)
/* Total SPI bits per WS281x bit (rounded) */
#define _SPI_BITS_PER_WS DIV_ROUND(WS281X_T_DATA_NS, _SPI_NS_PER_BIT)
/* Number of HIGH SPI bits for WS 0 */
#define _SPI_BITS_ZERO DIV_ROUND(WS281X_T_DATA_ZERO_NS, _SPI_NS_PER_BIT)
/* Number of HIGH SPI bits for WS 1 */
#define _SPI_BITS_ONE DIV_ROUND_UP(WS281X_T_DATA_ONE_NS, _SPI_NS_PER_BIT)
/* MSB aligned bit pattern for WS 0 */
#define _SPI_NIBBLE_0 (((1U << _SPI_BITS_ZERO) - 1) \
<< (_SPI_BITS_PER_WS - _SPI_BITS_ZERO))
/* MSB aligned bit pattern for WS 1 */
#define _SPI_NIBBLE_1 (((1U << _SPI_BITS_ONE) - 1) \
<< (_SPI_BITS_PER_WS - _SPI_BITS_ONE))
/* Number of SPI 0-bytes to trigger a reset */
#define _SPI_RESET_BYTES ((WS281X_T_END_US * NS_PER_US) / _SPI_NS_PER_BIT / 8U)

MAYBE_UNUSED
static const uint8_t _WS281X_SPI_NIBBLE_0 = (uint8_t)_SPI_NIBBLE_0;
MAYBE_UNUSED
static const uint8_t _WS281X_SPI_NIBBLE_1 = (uint8_t)_SPI_NIBBLE_1;

#ifndef WS281X_SPI_NIBBLE_0
# define WS281X_SPI_NIBBLE_0 _WS281X_SPI_NIBBLE_0 /* e.g., 0x08: 1000: short high */
#endif

#ifndef WS281X_SPI_NIBBLE_1
# define WS281X_SPI_NIBBLE_1 _WS281X_SPI_NIBBLE_1 /* e.g., 0x0E: 1110: long high */
#endif

#ifndef WS281X_SPI_BITS_PER_WS_BIT
# define WS281X_SPI_BITS_PER_WS_BIT _SPI_BITS_PER_WS /* e.g., 4: 4 SPI bits per WS bit */
#endif
Comment thread
crasbe marked this conversation as resolved.

typedef struct ws281x_spi_data {
uint8_t data[WS281X_BYTES_PER_DEVICE * WS281X_SPI_BITS_PER_WS_BIT];
} ws281x_spi_data_t;

/* Add space for one RESET pulse to eat phantom bit */
static uint8_t _spi_buf[_SPI_RESET_BYTES + WS281X_PARAM_NUMOF * sizeof(ws281x_spi_data_t)];
static size_t _spi_size;

int ws281x_init(ws281x_t *dev, const ws281x_params_t *params)
{
if (!dev || !params || !params->buf) {
return -EINVAL;
}
memset(dev, 0, sizeof(*dev));
dev->params = *params;
gpio_clear(dev->params.pin);
gpio_init(dev->params.pin, GPIO_OUT);
DEBUG("WS281x SPI encoding: %u bits per WS bit (0: 0x%02X, 1: 0x%02X)\n",
(unsigned)WS281X_SPI_BITS_PER_WS_BIT,
WS281X_SPI_NIBBLE_0,
WS281X_SPI_NIBBLE_1);
return 0;
}

void ws281x_prepare_transmission(ws281x_t *dev)
{
(void)dev;
memset(_spi_buf, 0, sizeof(_spi_buf));
_spi_size = _SPI_RESET_BYTES;
}

void ws281x_end_transmission(ws281x_t *dev)
{
(void)dev;
/* spi_periph_reconfigure is required to keep MOSI line stable low before and after transfer */
spi_init_pins(WS281X_SPI_DEV);
Comment thread
maribu marked this conversation as resolved.
spi_acquire(WS281X_SPI_DEV, SPI_CS_UNDEF, SPI_MODE_0, WS281X_SPI_CLK);
spi_transfer_bytes(WS281X_SPI_DEV, SPI_CS_UNDEF, false,
_spi_buf, _spi_buf, _spi_size);
ztimer_sleep(ZTIMER_USEC, WS281X_T_END_US);
spi_release(WS281X_SPI_DEV);
gpio_clear(dev->params.pin);
spi_deinit_pins(WS281X_SPI_DEV);
Comment thread
benpicco marked this conversation as resolved.
gpio_init(dev->params.pin, GPIO_OUT);
/* Can get the size when LED chain tail is connected to MISO */
const uint8_t *non_zero_spi_byte = memchk(_spi_buf, 0x00, sizeof(_spi_buf));
if (non_zero_spi_byte) {
size_t led_numof = (non_zero_spi_byte - _spi_buf) / sizeof(ws281x_spi_data_t);
led_numof -= (_SPI_RESET_BYTES / sizeof(ws281x_spi_data_t));
dev->params.numof = led_numof;
DEBUG("Detected %u LEDs in chain\n", (unsigned)dev->params.numof);
}
#if ENABLE_DEBUG && MODULE_OD
DEBUG("Received SPI data (%u bytes):\n", (unsigned)_spi_size);
od_hex_dump(_spi_buf, _spi_size, sizeof(ws281x_spi_data_t));
#endif
}

static void _ws281x_write_buffer_unaligned(const void *_buf, size_t size)
{
const uint8_t *buf = _buf;
uint8_t *out = _spi_buf + _spi_size;
size_t bit_pos = 0;
for (size_t i = 0; i < size; i++) {
uint8_t byte = buf[i];

for (int b = 7; b >= 0; b--) {
uint8_t nibble = (byte & (1u << b)) ? WS281X_SPI_NIBBLE_1
: WS281X_SPI_NIBBLE_0;
/* Pack _SPI_BITS_PER_WS bits into _spi_buf */
for (int k = WS281X_SPI_BITS_PER_WS_BIT - 1; k >= 0; k--) {
size_t byte_index = bit_pos / 8;
size_t bit_index = 7 - (bit_pos % 8);

if (nibble & (1u << k)) {
out[byte_index] |= (1u << bit_index);
}
else {
out[byte_index] &= ~(1u << bit_index);
}
bit_pos++;
}
}
}
_spi_size += (bit_pos + 7) / 8;
}

static void _ws281x_write_buffer_aligned(const void *_buf, size_t size)
{
const uint8_t *buf = _buf;
uint8_t *out = _spi_buf + _spi_size;

const unsigned ws_bits_per_spi_byte = 8 / WS281X_SPI_BITS_PER_WS_BIT;

for (size_t i = 0; i < size; i++) {
uint8_t byte = buf[i];

/* Encode multiple WS bits per SPI byte */
for (unsigned bit = 0; bit < 8; bit += ws_bits_per_spi_byte) {
uint8_t spi_byte = 0;

for (unsigned j = 0; j < ws_bits_per_spi_byte; j++) {
spi_byte <<= WS281X_SPI_BITS_PER_WS_BIT;
spi_byte |= (byte & 0x80) ? WS281X_SPI_NIBBLE_1 : WS281X_SPI_NIBBLE_0;
byte <<= 1;
}
*out++ = spi_byte;
}
}
_spi_size = out - _spi_buf;
}

void ws281x_write_buffer(ws281x_t *dev, const void *_buf, size_t size)
{
assert(dev);
assert(size % WS281X_BYTES_PER_DEVICE == 0);
assert(size * WS281X_SPI_BITS_PER_WS_BIT <= sizeof(_spi_buf));

if (8 % WS281X_SPI_BITS_PER_WS_BIT) {
_ws281x_write_buffer_unaligned(_buf, size);
}
else {
_ws281x_write_buffer_aligned(_buf, size);
}
}
Loading