Skip to content
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
1 change: 1 addition & 0 deletions drivers/led_strip/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_GPIO ws2812_gpio.c)
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_SPI ws2812_spi.c)
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_I2S ws2812_i2s.c)
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_RPI_PICO_PIO ws2812_rpi_pico_pio.c)
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_STM32_TIMER ws2812_stm32_timer.c)
zephyr_library_sources_ifdef(CONFIG_TLC5971_STRIP tlc5971.c)
zephyr_library_sources_ifdef(CONFIG_TLC59731_STRIP tlc59731.c)
12 changes: 12 additions & 0 deletions drivers/led_strip/Kconfig.ws2812
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,15 @@ config WS2812_STRIP_RPI_PICO_PIO
help
Enable driver for WS2812 (and compatibles) LED strip using
the RaspberryPi Pico's PIO.

config WS2812_STRIP_STM32_TIMER
bool "WS2812 LED strip STM32 TIMER driver"
default y
depends on DT_HAS_WORLDSEMI_WS2812_STM32_TIMER_ENABLED
select PWM
select PWM_WITH_DMA
select DMA
help
Enable driver for WS2812 (and compatibles) LED strip using
the STM32 TIMER with DMA

269 changes: 269 additions & 0 deletions drivers/led_strip/ws2812_stm32_timer.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
/*
* Copyright (c) 2026 Peter Johanson
*
* SPDX-License-Identifier: Apache-2.0
*/

#define DT_DRV_COMPAT worldsemi_ws2812_stm32_timer

#include <zephyr/drivers/led_strip.h>

#define LOG_LEVEL CONFIG_LED_STRIP_LOG_LEVEL
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(ws2812_stm32_timer);

#include <zephyr/device.h>
#include <zephyr/drivers/pwm.h>
#include <zephyr/drivers/dma.h>
#include <zephyr/drivers/dma/dma_stm32.h>
#include <zephyr/dt-bindings/led/led.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/util.h>

struct ws2812_stm32_timer_cfg {
struct pwm_dt_spec pwm;
struct device const *dma_dev;
TIM_TypeDef *timer_base;
uint32_t dma_channel;
size_t tx_buf_bytes;
struct k_mem_slab *mem_slab;
uint8_t num_colors;
size_t length;
const uint8_t *color_mapping;
uint16_t code_period_ns;
uint16_t zero_high_ns;
uint16_t one_high_ns;
uint16_t reset_codes;
};

struct ws2812_stm32_timer_data {
struct dma_config dma_config;
struct dma_block_config dma_block_config;

uint16_t zero_high_cycles;
uint16_t one_high_cycles;
struct k_sem sem;
void *mem_block;
int64_t last_dma_done;
};

static void ws2812_strip_dma_callback(const struct device *dev, void *user_data, uint32_t channel,
int status)
{
const struct device *ws_dev = (const struct device *)user_data;
const struct ws2812_stm32_timer_cfg *cfg = ws_dev->config;
struct ws2812_stm32_timer_data *data = ws_dev->data;

k_mem_slab_free(cfg->mem_slab, data->mem_block);
data->mem_block = NULL;

int ret = pwm_set_dt(&cfg->pwm, cfg->code_period_ns, 0);
if (ret) {

Check warning on line 61 in drivers/led_strip/ws2812_stm32_timer.c

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

LINE_SPACING

drivers/led_strip/ws2812_stm32_timer.c:61 Missing a blank line after declarations
LOG_ERR("Failed to set the period (%d)", ret);
}

data->last_dma_done = k_uptime_get();

k_sem_give(&data->sem);
}

static int ws2812_strip_update_rgb(const struct device *dev, struct led_rgb *pixels,
size_t num_pixels)
{
const struct ws2812_stm32_timer_cfg *cfg = dev->config;
struct ws2812_stm32_timer_data *data = dev->data;
uint16_t *tx_buf;
void *mem_block;
uint16_t zero_high_cycles = data->zero_high_cycles;
uint16_t one_high_cycles = data->one_high_cycles;
int ret;

/* Acquire memory for the compare value sequence, which we can prepare
* while an existing DMA burst is running. */

Check warning on line 82 in drivers/led_strip/ws2812_stm32_timer.c

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

BLOCK_COMMENT_STYLE

drivers/led_strip/ws2812_stm32_timer.c:82 Block comments use a trailing */ on a separate line
ret = k_mem_slab_alloc(cfg->mem_slab, &mem_block, K_SECONDS(10));
if (ret < 0) {
LOG_ERR("Unable to allocate mem slab for TX (err %d)", ret);
return -ENOMEM;
}

tx_buf = (uint16_t *)mem_block;

/* Add a pre-data reset, so the first pixel isn't skipped by the strip. */
for (uint16_t i = 0; i < cfg->reset_codes; i++) {
*tx_buf = 0x00;
tx_buf++;
}

for (uint16_t i = 0; i < num_pixels; i++) {
for (uint16_t j = 0; j < cfg->num_colors; j++) {
uint8_t pixel;

switch (cfg->color_mapping[j]) {
/* White channel is not supported by LED strip API. */
case LED_COLOR_ID_WHITE:
pixel = 0;
break;
case LED_COLOR_ID_RED:
pixel = pixels[i].r;
break;
case LED_COLOR_ID_GREEN:
pixel = pixels[i].g;
break;
case LED_COLOR_ID_BLUE:
pixel = pixels[i].b;
break;
default:
return -EINVAL;
}

/* High bits are sent first */
for (int8_t bit = 7; bit >= 0; bit--) {
*tx_buf =
IS_BIT_SET(pixel, bit) ? one_high_cycles : zero_high_cycles;
tx_buf++;
}
}
}

k_sem_take(&data->sem, K_FOREVER);

data->mem_block = mem_block;
data->dma_block_config.source_address = (uint32_t)mem_block;

if (dma_config(cfg->dma_dev, cfg->dma_channel, &data->dma_config) != 0) {
LOG_ERR("DMA config failed");
k_mem_slab_free(cfg->mem_slab, data->mem_block);
data->mem_block = NULL;
return -EIO;
}
if (dma_start(cfg->dma_dev, cfg->dma_channel) != 0) {
LOG_ERR("DMA start failed");
k_mem_slab_free(cfg->mem_slab, data->mem_block);
data->mem_block = NULL;
return -EIO;
}

return 0;
}

static size_t ws2812_strip_length(const struct device *dev)
{
const struct ws2812_stm32_timer_cfg *cfg = dev->config;

return cfg->length;
}

static int ws2812_stm32_timer_init(const struct device *dev)
{
const struct ws2812_stm32_timer_cfg *cfg = dev->config;
struct ws2812_stm32_timer_data *data = dev->data;
int ret;
uint64_t cycle_per_sec;

k_sem_init(&data->sem, 1, 1);

data->dma_config.head_block = &data->dma_block_config;
data->dma_config.user_data = (void *)dev;
data->dma_block_config.dest_address =
(uint32_t)(&cfg->timer_base->CCR1 + (cfg->pwm.channel - 1));

ret = pwm_get_cycles_per_sec(cfg->pwm.dev, cfg->pwm.channel, &cycle_per_sec);
if (ret) {
LOG_ERR("Failed to get cycles-per-sec for the channel (%d)", ret);
return -ENODEV;
}

uint64_t zero_high_cycles = DIV_ROUND_UP(cycle_per_sec * cfg->zero_high_ns, 1000000000);
uint64_t one_high_cycles = DIV_ROUND_UP(cycle_per_sec * cfg->one_high_ns, 1000000000);
data->zero_high_cycles = (uint16_t)zero_high_cycles;

Check warning on line 178 in drivers/led_strip/ws2812_stm32_timer.c

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

LINE_SPACING

drivers/led_strip/ws2812_stm32_timer.c:178 Missing a blank line after declarations
data->one_high_cycles = (uint16_t)one_high_cycles;
ret = pwm_set_dt(&cfg->pwm, cfg->code_period_ns, 0);
if (ret) {
LOG_ERR("Failed to set the period (%d)", ret);
return -ENODEV;
}

ret = pwm_enable_dma(cfg->pwm.dev, cfg->pwm.channel);
if (ret) {
LOG_ERR("Failed to enable DMA (%d)", ret);
return -ENODEV;
}

return 0;
}

static DEVICE_API(led_strip, ws2812_stm32_timer_api) = {
.update_rgb = ws2812_strip_update_rgb,
.length = ws2812_strip_length,
};

#define WS2812_RESET_DELAY_WORDS(idx) DT_INST_PROP(idx, reset_codes)
#define WS2812_NUM_COLORS(idx) (DT_INST_PROP_LEN(idx, color_mapping))
#define WS2812_STM32_TIMER_NUM_PIXELS(idx) (DT_INST_PROP(idx, chain_length))

#define PWM_TIMER_BASE(inst) \
((TIM_TypeDef *)DT_REG_ADDR(DT_PARENT(DT_PWMS_CTLR(DT_DRV_INST(inst)))))

#define WS2812_STM32_TIMER_BUFSIZE(idx) \
((((WS2812_NUM_COLORS(idx) * WS2812_STM32_TIMER_NUM_PIXELS(idx) * 8) + \
WS2812_RESET_DELAY_WORDS(idx)) * \
2))

#define WS2812_STM32_TIMER_DEVICE(idx) \
\
K_MEM_SLAB_DEFINE_STATIC(ws2812_stm32_timer_##idx##_slab, WS2812_STM32_TIMER_BUFSIZE(idx), \
2, 2); \
\
static const uint8_t ws2812_stm32_timer_##idx##_color_mapping[] = \
DT_INST_PROP(idx, color_mapping); \
\
static struct ws2812_stm32_timer_data ws2812_stm32_timer_##idx##_data = { \
.dma_config = \
{ \
.dma_slot = STM32_DMA_SLOT(idx, tx, slot), \
.channel_direction = STM32_DMA_CONFIG_DIRECTION( \
STM32_DMA_CHANNEL_CONFIG(idx, tx)), \
.channel_priority = STM32_DMA_CONFIG_PRIORITY( \
STM32_DMA_CHANNEL_CONFIG(idx, tx)), \
.source_data_size = STM32_DMA_CONFIG_MEMORY_DATA_SIZE( \
STM32_DMA_CHANNEL_CONFIG(idx, tx)), \
.dest_data_size = STM32_DMA_CONFIG_PERIPHERAL_DATA_SIZE( \
STM32_DMA_CHANNEL_CONFIG(idx, tx)), \
.source_burst_length = 1, /* SINGLE transfer */ \
.dest_burst_length = 1, /* SINGLE transfer */ \
.block_count = 1, \
.complete_callback_en = true, \
.error_callback_dis = false, \
.dma_callback = ws2812_strip_dma_callback, \
}, \
.dma_block_config = \
{ \
.source_addr_adj = DMA_ADDR_ADJ_INCREMENT, \
.dest_addr_adj = DMA_ADDR_ADJ_NO_CHANGE, \
.block_size = WS2812_STM32_TIMER_BUFSIZE(idx), \
.source_reload_en = false, /* circular mode */ \
.dest_reload_en = false, /* circular mode */ \
}, \
}; \
static const struct ws2812_stm32_timer_cfg ws2812_stm32_timer_##idx##_cfg = { \
.pwm = PWM_DT_SPEC_INST_GET(idx), \
.dma_dev = DEVICE_DT_GET(DT_INST_PHANDLE(idx, dmas)), \
.timer_base = PWM_TIMER_BASE(idx), \
.dma_channel = DT_INST_DMAS_CELL_BY_NAME(idx, tx, channel), \
.tx_buf_bytes = WS2812_STM32_TIMER_BUFSIZE(idx), \
.mem_slab = &ws2812_stm32_timer_##idx##_slab, \
.num_colors = WS2812_NUM_COLORS(idx), \
.length = DT_INST_PROP(idx, chain_length), \
.color_mapping = ws2812_stm32_timer_##idx##_color_mapping, \
.reset_codes = DT_INST_PROP(idx, reset_codes), \
.code_period_ns = DT_INST_PROP(idx, code_period_ns), \
.zero_high_ns = DT_INST_PROP(idx, zero_high_ns), \
.one_high_ns = DT_INST_PROP(idx, one_high_ns), \
}; \
\
DEVICE_DT_INST_DEFINE(idx, ws2812_stm32_timer_init, NULL, \
&ws2812_stm32_timer_##idx##_data, &ws2812_stm32_timer_##idx##_cfg, \
POST_KERNEL, CONFIG_LED_STRIP_INIT_PRIORITY, \
&ws2812_stm32_timer_api);

DT_INST_FOREACH_STATUS_OKAY(WS2812_STM32_TIMER_DEVICE)
8 changes: 8 additions & 0 deletions drivers/pwm/Kconfig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# PWM configuration options

# Copyright (c) 2015 Intel Corporation
# Copyright (c) 2025 Siemens SA
# SPDX-License-Identifier: Apache-2.0

menuconfig PWM
Expand Down Expand Up @@ -32,6 +33,13 @@ config PWM_CAPTURE
This option extends the Zephyr PWM API with the ability to capture PWM
period/pulse widths.


config PWM_WITH_DMA
bool "Provide API for using PWM with DMA"
help
This option extends the Zephyr PWM API with the ability to trigger DMA
requests from PWM channels.

source "drivers/pwm/Kconfig.b91"

source "drivers/pwm/Kconfig.cc13xx_cc26xx_timer"
Expand Down
20 changes: 20 additions & 0 deletions drivers/pwm/pwm_handlers.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* Copyright (c) 2017 Intel Corporation
* Copyright (c) 2020-2021 Vestas Wind Systems A/S
* Copyright (c) 2025 Siemens SA
*
* SPDX-License-Identifier: Apache-2.0
*/
Expand Down Expand Up @@ -78,3 +79,22 @@ static inline int z_vrfy_pwm_capture_cycles(const struct device *dev,
#include <zephyr/syscalls/pwm_capture_cycles_mrsh.c>

#endif /* CONFIG_PWM_CAPTURE */

#ifdef CONFIG_PWM_WITH_DMA
static inline int z_vrfy_pwm_enable_dma(const struct device *dev,
uint32_t channel)
{
K_OOPS(K_SYSCALL_DRIVER_PWM(dev, enable_dma));
return z_impl_pwm_enable_dma((const struct device *)dev, channel);
}
#include <zephyr/syscalls/pwm_enable_dma_mrsh.c>

static inline int z_vrfy_pwm_disable_dma(const struct device *dev,
uint32_t channel)
{
K_OOPS(K_SYSCALL_DRIVER_PWM(dev, disable_dma));
return z_impl_pwm_disable_dma((const struct device *)dev, channel);
}
#include <zephyr/syscalls/pwm_disable_dma_mrsh.c>

#endif /* CONFIG_PWM_WITH_DMA */
Loading
Loading