Skip to content

drivers/ws281x: simple DMA implementation through SPI #20218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
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
16 changes: 16 additions & 0 deletions cpu/esp8266/include/periph_cpu.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2019 Gunar Schorcht
* 2024 Hugues Larrive
*
* 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
Expand All @@ -14,6 +15,7 @@
* @brief CPU specific definitions and functions for peripheral handling
*
* @author Gunar Schorcht <gunar@schorcht.net>
* Hugues Larrive <hlarrive@pm.me>
*/

#ifndef PERIPH_CPU_H
Expand Down Expand Up @@ -283,6 +285,20 @@ typedef struct {
#define PERIPH_SPI_NEEDS_TRANSFER_BYTE /**< requires function spi_transfer_byte */
#define PERIPH_SPI_NEEDS_TRANSFER_REG /**< requires function spi_transfer_reg */
#define PERIPH_SPI_NEEDS_TRANSFER_REGS /**< requires function spi_transfer_regs */

/**
* @brief Override SPI clock configuration
*/
#define HAVE_SPI_CLK_T
typedef enum {
SPI_CLK_100KHZ = 0, /**< drive the SPI bus with 100KHz */
SPI_CLK_400KHZ, /**< drive the SPI bus with 400KHz */
SPI_CLK_1MHZ, /**< drive the SPI bus with 1MHz */
SPI_CLK_5MHZ, /**< drive the SPI bus with 5MHz */
SPI_CLK_10MHZ, /**< drive the SPI bus with 10MHz */
SPI_CLK_WS281X /**< drive the SPI bus with 2.5MHz */
} spi_clk_t;

/** @} */

/**
Expand Down
11 changes: 9 additions & 2 deletions cpu/esp8266/periph/spi.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2022 Gunar Schorcht
* 2024 Hugues Larrive
*
* 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
Expand All @@ -15,6 +16,7 @@
* @brief Low-level SPI driver implementation for ESP8266
*
* @author Gunar Schorcht <gunar@schorcht.net>
* @author Hugues Larrive <hlarrive@pm.me>
*
* @}
*/
Expand Down Expand Up @@ -42,7 +44,9 @@

#define SPI_DOUTDIN (BIT(0))

#ifndef SPI_BLOCK_SIZE
#define SPI_BLOCK_SIZE 64 /* number of bytes per SPI transfer */
#endif

/** structure which describes all properties of one SPI bus */
struct _spi_bus_t {
Expand Down Expand Up @@ -279,10 +283,13 @@ void IRAM_ATTR spi_acquire(spi_t bus, spi_cs_t cs, spi_mode_t mode, spi_clk_t cl
spi_clkcnt_N = 10; /* 10 cycles results into 400 kHz */
break;
case SPI_CLK_100KHZ: spi_clkdiv_pre = 20; /* predivides 80 MHz to 4 MHz */
spi_clkcnt_N = 40; /* 20 cycles results into 100 kHz */
spi_clkcnt_N = 40; /* 40 cycles results into 100 kHz */
break;
case SPI_CLK_WS281X: spi_clkdiv_pre = 2; /* predivides 80 MHz to 40 MHz */
spi_clkcnt_N = 16; /* 16 cycles results into 2.5 MHz */
break;
default: spi_clkdiv_pre = 20; /* predivides 80 MHz to 4 MHz */
spi_clkcnt_N = 40; /* 20 cycles results into 100 kHz */
spi_clkcnt_N = 40; /* 40 cycles results into 100 kHz */
}

/* register values are set to deviders-1 */
Expand Down
41 changes: 41 additions & 0 deletions drivers/include/ws281x.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* Copyright 2019 Marian Buschsieweke
* 2024 Hugues Larrive
*
* 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
Expand Down Expand Up @@ -42,6 +43,30 @@
* The ESP32 implementation is frequency independent, as frequencies above 80MHz
* are high enough to support bit banging without assembly.
*
* ## SPI
*
* The primary aim of this implementation was to enable the use of DMA on STM32.
* Its major drawback is that it uses binary patterns sent on the MOSI pin,
* which implies a large additional buffer at least three times the size of
* `ws281x_buf`. Its advantage is that it should work on a wide variety of
* devices.
*
* @warning The SPI clock frequency required is equal to 800KHz multiplied by
* the pattern length, so the optimum SPI clock frequency is 2.4 MHz. In
* practice, I've successfully tested frequencies ranging from 0.5 MHz (4 / 8)
* to 1.11 MHz (4.44 / 4 and 3.33 / 3).
* You should also bear in mind that the clock frequency actually obtained
* depends greatly on the hardware implementation of the SPI device and the
* support of an arbitrary speed by the software driver. For example, on
* ESP8266 you could have used an 8-bit long pattern with SPI_CLK_5MHZ which
* gives a frequency of 0.63 MHz, but it's better to use the 3-bit long
* pattern by modifying the driver to use `spi_clkdiv_pre = 2` and
* `spi_clkcnt_N = 16` to obtain a frequency of 2.5 MHz. The size of the SPI
* buffer can also be a problem if the cpu takes too long to reload it, as
* on AVR. The ESP 8266 has a 64-byte buffer and takes 5µs to reload, which
* is a problem unless it occurs "between 2 leds" or 24 patterns (8-bit
* pattern: 24 - 48; 3-bit pattern: 9 .. 63).
*
* ## Native/VT100
*
* The native (VT100) implementation writes the LED state to the console.
Expand Down Expand Up @@ -70,6 +95,21 @@
* USEMODULE += ws281x_esp32
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* * the SPI backend:
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Makefile
* USEMODULE += ws281x_spi
*
* # Optionally
* FEATURES_REQUIRED += periph_dma
* CFLAGS += '-DWS281X_SPI_DEV=0'
* CFLAGS += '-DWS281X_SPI_PATTERN_LENGTH=3'
* CFLAGS += '-DWS281X_SPI_CLK=2400000'
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* The driver supports 3 pattern sizes: 3, 4 and 8 bits. The default
* WS281X_SPI_CLK is KHZ(800 * WS281X_SPI_PATTERN_LENGTH), but this only works
* for drivers that support an arbitrary speed, such as stm32. With others,
* you'll need to set it to SPI_CLK_5MHZ.
*
* * the native/VT100 backend:
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Makefile
* USEMODULE += ws281x_vt100
Expand All @@ -81,6 +121,7 @@
* @brief WS2812/SK6812 RGB LED Driver
*
* @author Marian Buschsieweke <marian.buschsieweke@ovgu.de>
* @author Hugues Larrive <hlarrive@pm.me>
*/

#ifndef WS281X_H
Expand Down
15 changes: 14 additions & 1 deletion drivers/ws281x/Makefile.dep
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# 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

ifeq (,$(filter ws281x_%,$(USEMODULE)))
ifneq (,$(filter periph_spi,$(FEATURES_USED)))
USEMODULE += ws281x_spi
endif
ifneq (,$(filter cpu_core_atmega,$(FEATURES_USED)))
USEMODULE += ws281x_atmega
endif
Expand Down Expand Up @@ -34,6 +37,16 @@ ifneq (,$(filter ws281x_esp32%,$(USEMODULE)))
endif
endif

ifneq (,$(filter ws281x_spi,$(USEMODULE)))
FEATURES_REQUIRED += periph_spi
USEMODULE += periph_spi
ifneq (,$(filter arch_esp8266,$(FEATURES_USED)))
CFLAGS += '-DSPI_BLOCK_SIZE=63'
CFLAGS += '-DWS281X_SPI_PATTERN_LENGTH=3'
CFLAGS += '-DWS281X_SPI_CLK=SPI_CLK_WS281X'
endif
endif

ifneq (,$(filter ws281x_timer_gpio_ll,$(USEMODULE)))
FEATURES_REQUIRED += periph_gpio_ll periph_timer periph_timer_poll
endif
13 changes: 13 additions & 0 deletions drivers/ws281x/include/ws281x_backend.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2019 Marian Buschsieweke
* 2024 Hugues Larrive <hlarrive@pm.me>
*
* 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
Expand All @@ -14,6 +15,7 @@
* @brief Backend configuration for WS2812/SK6812 RGB LEDs
*
* @author Marian Buschsieweke <marian.buschsieweke@ovgu.de>
* @author Hugues Larrive <hlarrive@pm.me>
*/

#ifndef WS281X_BACKEND_H
Expand Down Expand Up @@ -41,6 +43,17 @@ extern "C" {
#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
/** @} */

/**
* @name Properties of the VT100 terminal backend.
* @{
Expand Down
115 changes: 115 additions & 0 deletions drivers/ws281x/spi.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright 2024 Hugues Larrive <hlarrive@pm.me>
*
* 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_buffer()` through SPI
*
* @author Hugues Larrive <hlarrive@pm.me>
*
* @}
*/
#include <assert.h>
#include <errno.h>
#include <stdint.h>
#include <string.h>

#include "macros/units.h"
#include "periph/spi.h"
#include "ws281x.h"
#include "ws281x_constants.h"
#include "ws281x_params.h"

#ifndef WS281X_SPI_DEV
#define WS281X_SPI_DEV (0)
#endif
#ifndef WS281X_SPI_PATTERN_LENGTH
#define WS281X_SPI_PATTERN_LENGTH (3)
#endif
#ifndef WS281X_SPI_CLK
#define WS281X_SPI_CLK KHZ(800 * WS281X_SPI_PATTERN_LENGTH)
#endif

#if (WS281X_SPI_PATTERN_LENGTH == 3)
/* The LSB of the 2nd byte (always 1) is the MSB of the pattern for bit 2,
* which is encoded in the 3rd byte, so we will use a 4-bit pattern. */
#define WS281X_SPI_DATA_0_MASK (0b00001001)
#define WS281X_SPI_DATA_1_MASK (0b00001101)
/* 1u01u01u 01u01u01 u01u01u0 */
#define WS281X_SPI_SHIFTS {4,1,-2,3,0,5,2,-1}
#elif (WS281X_SPI_PATTERN_LENGTH == 4)
#define WS281X_SPI_DATA_0_MASK (0b00001000)
#define WS281X_SPI_DATA_1_MASK (0b00001110)
#define WS281X_SPI_SHIFTS {4,0,4,0,4,0,4,0}
#elif (WS281X_SPI_PATTERN_LENGTH == 8)
#define WS281X_SPI_DATA_0_MASK (0b11000000)
#define WS281X_SPI_DATA_1_MASK (0b11111100)
#define WS281X_SPI_SHIFTS {0,0,0,0,0,0,0,0}
#endif

static uint8_t spi_buf[WS281X_PARAM_NUMOF * WS281X_BYTES_PER_DEVICE * WS281X_SPI_PATTERN_LENGTH];

int 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;

return 0;
}

void ws281x_prepare_transmission(ws281x_t *dev)
{
(void)dev;
spi_acquire(SPI_DEV(WS281X_SPI_DEV), SPI_CS_UNDEF, SPI_MODE_0, WS281X_SPI_CLK);
}

void ws281x_end_transmission(ws281x_t *dev)
{
(void)dev;
spi_release(SPI_DEV(WS281X_SPI_DEV));
xtimer_usleep(WS281X_T_END_US);
}

void ws281x_write_buffer(ws281x_t *dev, const void *_buf, size_t size)
{
assert(dev);

const uint8_t *buf = _buf;
const int8_t shift[8] = WS281X_SPI_SHIFTS;

for (int i = 0; i < (int)size; ++i) {
uint8_t byte = buf[i];
uint8_t offset = 0;
spi_buf[i * WS281X_SPI_PATTERN_LENGTH] = 0;
for (uint8_t cnt = 0; cnt < 8; ++cnt) {
spi_buf[i * WS281X_SPI_PATTERN_LENGTH + offset]
|= (byte & 0b10000000)
? (shift[cnt] >= 0)
? WS281X_SPI_DATA_1_MASK << shift[cnt]
: WS281X_SPI_DATA_1_MASK >> -shift[cnt]
: (shift[cnt] >= 0)
? WS281X_SPI_DATA_0_MASK << shift[cnt]
: WS281X_SPI_DATA_0_MASK >> -shift[cnt];
byte <<= 1;
if (shift[cnt] <= 0) {
++offset;
spi_buf[i * WS281X_SPI_PATTERN_LENGTH + offset] = 0;
}
}
}

spi_transfer_bytes(SPI_DEV(WS281X_SPI_DEV), SPI_CS_UNDEF, false, spi_buf, NULL, sizeof(spi_buf));
}
24 changes: 22 additions & 2 deletions tests/drivers/ws281x/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ include ../Makefile.drivers_common
# TIMER ?= 2
# FREQ ?= 16000000

# For SPI implementation only
# DEV ?= 0
# DMA ?= 0
# LEN ?= 3
# CLK ?= 2400000

USEMODULE += ws281x
USEMODULE += xtimer

Expand All @@ -18,17 +24,31 @@ BOARD_BLACKLIST := waspmote-pro

EXTERNAL_BOARD_DIRS += $(RIOTBASE)/tests/build_system/external_board_dirs/esp-ci-boards

include $(RIOTBASE)/Makefile.include

ifneq (, $(PIN))
CFLAGS += '-DWS281X_PARAM_PIN=$(PIN)'
endif
ifneq (, $(N))
CFLAGS += '-DWS281X_PARAM_NUMOF=$(N)'
endif

ifneq (, $(DEV))
CFLAGS += '-DWS281X_SPI_DEV=$(DEV)'
endif
ifeq (1, $(DMA))
FEATURES_REQUIRED += periph_dma
endif
ifneq (, $(LEN))
CFLAGS += '-DWS281X_SPI_PATTERN_LENGTH=$(LEN)'
endif
ifneq (, $(CLK))
CFLAGS += '-DWS281X_SPI_CLK=$(CLK)'
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

include $(RIOTBASE)/Makefile.include