diff --git a/drivers/led_strip/CMakeLists.txt b/drivers/led_strip/CMakeLists.txt index b293782e620fb..6974b56de9f8a 100644 --- a/drivers/led_strip/CMakeLists.txt +++ b/drivers/led_strip/CMakeLists.txt @@ -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) diff --git a/drivers/led_strip/Kconfig.ws2812 b/drivers/led_strip/Kconfig.ws2812 index c9125b06d3e64..652ac21ee542f 100644 --- a/drivers/led_strip/Kconfig.ws2812 +++ b/drivers/led_strip/Kconfig.ws2812 @@ -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 + diff --git a/drivers/led_strip/ws2812_stm32_timer.c b/drivers/led_strip/ws2812_stm32_timer.c new file mode 100644 index 0000000000000..3739ddb593a87 --- /dev/null +++ b/drivers/led_strip/ws2812_stm32_timer.c @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2026 Peter Johanson + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT worldsemi_ws2812_stm32_timer + +#include + +#define LOG_LEVEL CONFIG_LED_STRIP_LOG_LEVEL +#include +LOG_MODULE_REGISTER(ws2812_stm32_timer); + +#include +#include +#include +#include +#include +#include +#include + +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) { + 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. */ + 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; + 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) diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig index 0dd835a8973c2..3eeafa60ff438 100644 --- a/drivers/pwm/Kconfig +++ b/drivers/pwm/Kconfig @@ -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 @@ -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" diff --git a/drivers/pwm/pwm_handlers.c b/drivers/pwm/pwm_handlers.c index 5c342a89068fc..1426f9616f287 100644 --- a/drivers/pwm/pwm_handlers.c +++ b/drivers/pwm/pwm_handlers.c @@ -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 */ @@ -78,3 +79,22 @@ static inline int z_vrfy_pwm_capture_cycles(const struct device *dev, #include #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 + +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 + +#endif /* CONFIG_PWM_WITH_DMA */ diff --git a/drivers/pwm/pwm_stm32.c b/drivers/pwm/pwm_stm32.c index 708709bf2ef35..5a34fa5c90420 100644 --- a/drivers/pwm/pwm_stm32.c +++ b/drivers/pwm/pwm_stm32.c @@ -2,6 +2,7 @@ * Copyright (c) 2016 Linaro Limited. * Copyright (c) 2020 Teslabs Engineering S.L. * Copyright (c) 2023 Nobleo Technology + * Copyright (c) 2025 Siemens SA * * SPDX-License-Identifier: Apache-2.0 */ @@ -182,6 +183,18 @@ static void __maybe_unused (*const clear_capture_interrupt[])(TIM_TypeDef *) = { LL_TIM_ClearFlag_CC3, LL_TIM_ClearFlag_CC4 }; +/* Channel to enable DMA request flag mapping. */ +static void __maybe_unused (*const enable_dma_interrupt[])(TIM_TypeDef *) = { + LL_TIM_EnableDMAReq_CC1, LL_TIM_EnableDMAReq_CC2, + LL_TIM_EnableDMAReq_CC3, LL_TIM_EnableDMAReq_CC4 +}; + +/* Channel to disable DMA request flag mapping. */ +static void __maybe_unused (*const disable_dma_interrupt[])(TIM_TypeDef *) = { + LL_TIM_DisableDMAReq_CC1, LL_TIM_DisableDMAReq_CC2, + LL_TIM_DisableDMAReq_CC3, LL_TIM_DisableDMAReq_CC4 +}; + /** * Obtain LL polarity from PWM flags. * @@ -756,6 +769,51 @@ static void pwm_stm32_isr(const struct device *dev) #endif /* CONFIG_PWM_CAPTURE */ +#ifdef CONFIG_PWM_WITH_DMA +static int pwm_stm32_enable_dma(const struct device *dev, + uint32_t channel) +{ + const struct pwm_stm32_config *cfg = dev->config; + + /* DMA requests are only supported on Capture/Compare channels. + * However, these DMA request can also be used in PWM output mode to + * dynamically update the duty cycle once per period. Therefore, enabling + * or disabling DMA requests on PWM channels should not be limited to + * Capture/Compare driver functions. + */ + if ((channel < 1u) || (channel > 4u)) { + LOG_ERR("DMA for PWM only exists on channels 1, 2, 3 and 4."); + return -ENOTSUP; + } + + enable_dma_interrupt[channel - 1](cfg->timer); + + return 0; +} + +static int pwm_stm32_disable_dma(const struct device *dev, + uint32_t channel) +{ + const struct pwm_stm32_config *cfg = dev->config; + + /* DMA requests are only supported on Capture/Compare channels. + * However, these DMA requests can also be used in PWM output mode to + * dynamically update the duty cycle once per period. Therefore, enabling + * or disabling DMA requests on PWM channels should not be limited to + * Capture/Compare driver functions. + */ + if ((channel < 1u) || (channel > 4u)) { + LOG_ERR("DMA for PWM only exists on channels 1, 2, 3 and 4."); + return -ENOTSUP; + } + + disable_dma_interrupt[channel - 1](cfg->timer); + + return 0; +} + +#endif /* CONFIG_PWM_WITH_DMA */ + static int pwm_stm32_get_cycles_per_sec(const struct device *dev, uint32_t channel, uint64_t *cycles) { @@ -775,6 +833,10 @@ static DEVICE_API(pwm, pwm_stm32_driver_api) = { .enable_capture = pwm_stm32_enable_capture, .disable_capture = pwm_stm32_disable_capture, #endif /* CONFIG_PWM_CAPTURE */ +#ifdef CONFIG_PWM_WITH_DMA + .enable_dma = pwm_stm32_enable_dma, + .disable_dma = pwm_stm32_disable_dma, +#endif /* CONFIG_PWM_WITH_DMA */ }; static int pwm_stm32_init(const struct device *dev) diff --git a/dts/bindings/led_strip/worldsemi,ws2812-stm32-timer.yaml b/dts/bindings/led_strip/worldsemi,ws2812-stm32-timer.yaml new file mode 100644 index 0000000000000..0ebdc17dc2298 --- /dev/null +++ b/dts/bindings/led_strip/worldsemi,ws2812-stm32-timer.yaml @@ -0,0 +1,58 @@ +# Copyright (c) 2026 Peter Johanson +# SPDX-License-Identifier: Apache-2.0 + +description: | + Worldsemi WS2812 LED strip, STM32 timer/PWM via DMA + + Driver bindings for controlling a WS2812 or compatible LED + strip with an STM32 timer in PWM mode with DMA to load compare + values for sending the correct data + +compatible: "worldsemi,ws2812-stm32-timer" + +include: [ws2812.yaml, base.yaml] + +properties: + + pwms: + type: phandle-array + + dmas: + description: The DMA device and channel to use for the compare sequence + dmas: + description: | + Required TX dma specifiers. Each specifier will have a phandle + reference to the dmac controller, the channel number, and peripheral + trigger source. + + For example + dmas = <&dmac 0 0xb>; + + dma-names: + description: | + Required if the dmas property exists. This should be "tx" + to match the dmas property. + + For example + dma-names = "tx"; + + reset-codes: + type: int + required: true + description: Duration of a reset, as a multiple of the code period ns. + + code-period-ns: + type: int + required: true + description: Length of a given 0/1 code in nanoseconds. + + zero-high-ns: + type: int + required: true + description: Length of a 0 code high time in nanoseconds. + + one-high-ns: + type: int + required: true + description: Length of a 1 code high time in nanoseconds. + diff --git a/include/zephyr/drivers/pwm.h b/include/zephyr/drivers/pwm.h index 4f21f41e87617..3450eb2d71668 100644 --- a/include/zephyr/drivers/pwm.h +++ b/include/zephyr/drivers/pwm.h @@ -1,6 +1,8 @@ /* * Copyright (c) 2016 Intel Corporation. * Copyright (c) 2020-2021 Vestas Wind Systems A/S + * Copyright (c) 2025 Basalte bv + * Copyright (c) 2025 Siemens SA * * SPDX-License-Identifier: Apache-2.0 */ @@ -17,7 +19,7 @@ * @brief PWM Interface * @defgroup pwm_interface PWM Interface * @since 1.0 - * @version 1.0.0 + * @version 1.1.0 * @ingroup io_interfaces * @{ */ @@ -434,6 +436,21 @@ typedef int (*pwm_disable_capture_t)(const struct device *dev, uint32_t channel); #endif /* CONFIG_PWM_CAPTURE */ + +#ifdef CONFIG_PWM_WITH_DMA +/** + * @brief PWM driver API call to enable PWM DMA requests. + * @see pwm_enable_dma() for argument description + */ +typedef int (*pwm_enable_dma_t)(const struct device *dev, uint32_t channel); + +/** + * @brief PWM driver API call to disable PWM DMA requests. + * @see pwm_disable_dma() for argument description + */ +typedef int (*pwm_disable_dma_t)(const struct device *dev, uint32_t channel); +#endif /* CONFIG_PWM_WITH_DMA */ + /** @brief PWM driver API definition. */ __subsystem struct pwm_driver_api { pwm_set_cycles_t set_cycles; @@ -443,6 +460,10 @@ __subsystem struct pwm_driver_api { pwm_enable_capture_t enable_capture; pwm_disable_capture_t disable_capture; #endif /* CONFIG_PWM_CAPTURE */ +#ifdef CONFIG_PWM_WITH_DMA + pwm_enable_dma_t enable_dma; + pwm_disable_dma_t disable_dma; +#endif /* CONFIG_PWM_WITH_DMA */ }; /** @endcond */ @@ -786,6 +807,56 @@ static inline int z_impl_pwm_disable_capture(const struct device *dev, } #endif /* CONFIG_PWM_CAPTURE */ +#if defined(CONFIG_PWM_WITH_DMA) || defined(__DOXYGEN__) +/** + * @brief Enable DMA requests triggered by PWM cycles for a single PWM channel. + * + * @param[in] dev PWM device instance. + * @param channel PWM channel. + * + * @retval 0 If successful. + * @retval -EINVAL if invalid function parameters were given + * @retval -ENOSYS if DMA for PWM is not supported + * @retval -ENOTSUP if the PWM channel does not support DMA + */ +__syscall int pwm_enable_dma(const struct device *dev, uint32_t channel); + +static inline int z_impl_pwm_enable_dma(const struct device *dev, uint32_t channel) +{ + const struct pwm_driver_api *api = (const struct pwm_driver_api *)dev->api; + + if (api->enable_dma == NULL) { + return -ENOSYS; + } + + return api->enable_dma(dev, channel); +} + +/** + * @brief Disable DMA requests triggered by PWM cycles for a single PWM channel. + * + * @param[in] dev PWM device instance. + * @param channel PWM channel. + * + * @retval 0 If successful. + * @retval -EINVAL if invalid function parameters were given + * @retval -ENOSYS if DMA for PWM is not supported + * @retval -ENOTSUP if the PWM channel does not support DMA + */ +__syscall int pwm_disable_dma(const struct device *dev, uint32_t channel); + +static inline int z_impl_pwm_disable_dma(const struct device *dev, uint32_t channel) +{ + const struct pwm_driver_api *api = (const struct pwm_driver_api *)dev->api; + + if (api->disable_dma == NULL) { + return -ENOSYS; + } + + return api->disable_dma(dev, channel); +} +#endif /* CONFIG_PWM_WITH_DMA */ + /** * @brief Capture a single PWM period/pulse width in clock cycles for a single * PWM input.