Skip to content

drivers: pwm: add support for enabling DMA requests#88671

Merged
aescolar merged 1 commit intozephyrproject-rtos:mainfrom
SiViSur:main
Mar 11, 2026
Merged

drivers: pwm: add support for enabling DMA requests#88671
aescolar merged 1 commit intozephyrproject-rtos:mainfrom
SiViSur:main

Conversation

@SiViSur
Copy link
Copy Markdown
Contributor

@SiViSur SiViSur commented Apr 15, 2025

Extends the PWM API with optional API functions for enabling DMA requests triggered by a given PWM channel.

Possible solution for #88670

Tested and verified on NUCLEO-WB55RG development board

@github-actions
Copy link
Copy Markdown

Hello @SiViSur, and thank you very much for your first pull request to the Zephyr project!
Our Continuous Integration pipeline will execute a series of checks on your Pull Request commit messages and code, and you are expected to address any failures by updating the PR. Please take a look at our commit message guidelines to find out how to format your commit messages, and at our contribution workflow to understand how to update your Pull Request. If you haven't already, please make sure to review the project's Contributor Expectations and update (by amending and force-pushing the commits) your pull request if necessary.
If you are stuck or need help please join us on Discord and ask your question there. Additionally, you can escalate the review when applicable. 😊

@SiViSur SiViSur force-pushed the main branch 3 times, most recently from fa368da to 39dd7af Compare April 17, 2025 16:42
@SiViSur SiViSur changed the title drivers:pwm: add support for enabling DMA requests drivers: pwm: add support for enabling DMA requests Apr 17, 2025
@SiViSur
Copy link
Copy Markdown
Contributor Author

SiViSur commented May 22, 2025

I've decided to add a small example app using the added API. Please find all relevant files in this zip

Here's some snippets of the source code:
Main.c

#include <zephyr/kernel.h>  // printk

#include "audio_data.h"
#include "led_blinky.h"
#include "pwm_setup.h"

int main(void) {
  printk("Zephyr Example Application\n");

  /************************ Speaker PWM setup begin ************************/
  if (start_pwm()) {
    return 0;
  }
  /************************* Speaker PWM setup end *************************/

  /************************ Speaker DMA setup begin ************************/
  if (start_dma_for_pwm()) {
    return 0;
  }
  /************************* Speaker DMA setup end *************************/

  /*********************** LED blinky example begin ************************/
  if (setup_blinky_led()) {
    printk("Failed to setup blinky LED\n");
    return 0;
  }
  /************************ LED blinky example end *************************/

  while (1) {
    if (toggle_led()) {
      return 0;
    }

    k_msleep(SLEEP_TIME_MS);
  }

  return 0;
}

pwm_setup.h

#ifndef PWM_SETUP_H
#define PWM_SETUP_H

#include <zephyr/device.h>
#include <zephyr/drivers/clock_control/stm32_clock_control.h>
#include <zephyr/drivers/dma.h>
#include <zephyr/drivers/pwm.h>
#include <zephyr/kernel.h>  // printk

#include "stm32wb55xx.h"
#include "stm32wbxx_ll_tim.h"

/* Define the buzzer PWM device */
#define BUZZER_PWM_NODE DT_ALIAS(pwm_speaker)
#if !DT_NODE_HAS_STATUS(BUZZER_PWM_NODE, okay)
#error "Speaker PWM device not found or not enabled in device tree"
#endif

/* Static PWM device reference */
static const struct device *pwm_dev = DEVICE_DT_GET(BUZZER_PWM_NODE);

/* Define the DMA device node */
#define DMA2_DEVICE_NODE DT_ALIAS(dma_speaker)
#if !DT_NODE_HAS_STATUS(DMA2_DEVICE_NODE, okay)
#error "DMA device not found or not enabled in device tree"
#endif

/* Static DMA device reference */
static const struct device *dma2_dev = DEVICE_DT_GET(DMA2_DEVICE_NODE);
static uint32_t dma_callback_count = 0;

int start_pwm() {
  // Retrieve PWM device node for speaker pins
  if (!device_is_ready(pwm_dev)) {
    printk("Error: PWM device not ready\n");
    return -ENODEV;
  }

  printk("Enabling PWM\n");
  // Start PWM on TIM2 channel 1 (PA15)
  if (pwm_set(pwm_dev, 1, PWM_USEC(23), PWM_USEC(11), 0)) {
    printk("PWM channel 1 (PA15) start failed\n");
    return -1;
  }

  return 0;
}

void dma_callback(const struct device *dev, void *user_data, uint32_t channel,
                  int status) {
  dma_callback_count++;
  printk("DMA Callback count: %d\n", dma_callback_count);
}

int start_dma_for_pwm() {
  // Retrieve DMA2 device node for PWM duty cycle DMA requests
  if (!device_is_ready(dma2_dev)) {
    printk("Error: DMA2 device not ready\n");
    return -ENODEV;
  }

  // Retrieve PWM device node for speaker pins
  if (!device_is_ready(pwm_dev)) {
    printk("Error: PWM device not ready\n");
    return -ENODEV;
  }

  printk("Enabling DMA interrupts on TIM2 CH1\n");
  // Enable DMA interrupts on TIM2 CH1
  pwm_enable_dma(pwm_dev, 1);

  // Configure the DMA
  struct dma_config dma_cfg = {0};
  struct dma_block_config dma_block = {0};
  /* Get the base address of Timer 2 */
  uint32_t timer2_base = DT_REG_ADDR(DT_NODELABEL(timers2));
  // #define DMA_REQUEST_TIM2_CH1 0x0000001CU
  dma_cfg.dma_slot = 0x0000001CU;
  dma_cfg.channel_direction = MEMORY_TO_PERIPHERAL;
  dma_cfg.complete_callback_en = true;
  dma_cfg.error_callback_dis = true;
  dma_cfg.channel_priority = 0x2; /* high priority */
  dma_cfg.source_data_size = 4;   /* 32-bit data */
  dma_cfg.dest_data_size = 4;     /* 32-bit data */
  dma_cfg.source_burst_length = 1;
  dma_cfg.dest_burst_length = 1;
  dma_cfg.block_count = 1;
  dma_cfg.head_block = &dma_block;
  dma_cfg.dma_callback = dma_callback;
  // Set up DMA block for circular transfer
  dma_block.block_size = AUDIO_DATA_SIZE * sizeof(uint32_t);
  dma_block.source_address = (uint32_t)audioData_high;
  dma_block.dest_address =
      (uint32_t)timer2_base + 0x34; /* CCR1 offset is 0x34 */
  dma_block.source_addr_adj = DMA_ADDR_ADJ_INCREMENT;
  dma_block.dest_addr_adj = DMA_ADDR_ADJ_NO_CHANGE;
  dma_block.source_reload_en = true; /* circular mode */
  dma_block.dest_reload_en = true;   /* circular mode */

  /* Start the DMA transfer on channel 1 */
  if (dma_config(dma2_dev, 1, &dma_cfg) != 0) {
    printk("DMA2 config failed\n");
    return -EIO;
  }
  if (dma_start(dma2_dev, 1) != 0) {
    printk("DMA2 start failed\n");
    return -EIO;
  }

  return 0;
}

#endif /* PWM_SETUP_H */

@SiViSur SiViSur requested a review from rruuaanng May 22, 2025 10:16
@rruuaanng
Copy link
Copy Markdown
Contributor

I've decided to add a small example app using the added API. Please find all relevant files in this zip

Here's some snippets of the source code:
Main.c

If this sample code is useful, would you add a new commit with them :--)

@SiViSur
Copy link
Copy Markdown
Contributor Author

SiViSur commented May 29, 2025

I've decided to add a small example app using the added API. Please find all relevant files in this zip
Here's some snippets of the source code:
Main.c

If this sample code is useful, would you add a new commit with them :--)

What do you mean? Creating a samples/drivers/pwm directory and adding the sample code there?

@github-actions
Copy link
Copy Markdown

This pull request has been marked as stale because it has been open (more than) 60 days with no activity. Remove the stale label or add a comment saying that you would like to have the label removed otherwise this pull request will automatically be closed in 14 days. Note, that you can always re-open a closed pull request at any time.

@github-actions github-actions bot added the Stale label Jul 29, 2025
@rruuaanng rruuaanng removed the Stale label Jul 29, 2025
@SiViSur
Copy link
Copy Markdown
Contributor Author

SiViSur commented Jul 29, 2025

@rruuaanng Any advice on how to proceed with this MR? Is it usual to take this much time to get comments?

@erwango
Copy link
Copy Markdown
Member

erwango commented Aug 18, 2025

@anangl Can you comment on the api change ?

@github-actions
Copy link
Copy Markdown

This pull request has been marked as stale because it has been open (more than) 60 days with no activity. Remove the stale label or add a comment saying that you would like to have the label removed otherwise this pull request will automatically be closed in 14 days. Note, that you can always re-open a closed pull request at any time.

Copy link
Copy Markdown
Contributor

@JarmouniA JarmouniA left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API minor version should be incremented.

@SiViSur
Copy link
Copy Markdown
Contributor Author

SiViSur commented Mar 9, 2026

API minor version should be incremented.

What do you mean @JarmouniA? How do I do that?

@JarmouniA
Copy link
Copy Markdown
Contributor

JarmouniA commented Mar 9, 2026

@SiViSur
Copy link
Copy Markdown
Contributor Author

SiViSur commented Mar 9, 2026

What do you mean @JarmouniA? How do I do that?

* @version 1.0.0

https://docs.zephyrproject.org/latest/develop/api/overview.html#api-overview

Thanks! Done.

Extend the PWM API with optional API functions for enabling DMA requests

Possible solution for zephyrproject-rtos#88670

Signed-off-by: Vincent Surkijn <vincent.surkijn@siemens.com>
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

@juickar juickar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit but non-blocking: update copyrights to 2026

@juickar
Copy link
Copy Markdown
Contributor

juickar commented Mar 10, 2026

@anangl PTAL

uint32_t channel)
{
K_OOPS(K_SYSCALL_DRIVER_PWM(dev, enable_dma));
return z_impl_pwm_enable_dma((const struct device *)dev, channel);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there no indentation here?

Copy link
Copy Markdown
Contributor

@etienne-lms etienne-lms left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor comments, otherwise LGTM.

Comment on lines +87 to +88
K_OOPS(K_SYSCALL_DRIVER_PWM(dev, enable_dma));
return z_impl_pwm_enable_dma((const struct device *)dev, channel);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cast not needed.
Could you indent these instructions and those in z_vrfy_pwm_disable_dma()?

Also fix indentation for the functions 2nd argument, here:

static inline int z_vrfy_pwm_enable_dma(const struct device *dev,
					uint32_t channel)

By the way, it seems the cast to (const struct device *) are not needed, but present above so fair enough for consistency. Maybe to clean is a later change.


#ifdef CONFIG_PWM_WITH_DMA
static int pwm_stm32_enable_dma(const struct device *dev,
uint32_t channel)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* However, these DMA request can also be used in PWM output mode to
* However, these DMA requests can also be used in PWM output mode to

@aescolar aescolar merged commit 311a841 into zephyrproject-rtos:main Mar 11, 2026
28 checks passed
@github-actions
Copy link
Copy Markdown

Hi @SiViSur!
Congratulations on getting your very first Zephyr pull request merged 🎉🥳. This is a fantastic achievement, and we're thrilled to have you as part of our community!

To celebrate this milestone and showcase your contribution, we'd love to award you the Zephyr Technical Contributor badge. If you're interested, please claim your badge by filling out this form: Claim Your Zephyr Badge.

Thank you for your valuable input, and we look forward to seeing more of your contributions in the future! 🪁

@aescolar
Copy link
Copy Markdown
Member

@SiViSur would be so kind as to follow up with a PR fixing the formatting issues?

@SiViSur
Copy link
Copy Markdown
Contributor Author

SiViSur commented Mar 11, 2026

@SiViSur would be so kind as to follow up with a PR fixing the formatting issues?

Sure! I'll try to do so later this week.

@petejohanson
Copy link
Copy Markdown
Contributor

I've decided to add a small example app using the added API. Please find all relevant files in this zip

Here's some snippets of the source code: Main.c

#include <zephyr/kernel.h>  // printk

#include "audio_data.h"
#include "led_blinky.h"
#include "pwm_setup.h"

int main(void) {
  printk("Zephyr Example Application\n");

  /************************ Speaker PWM setup begin ************************/
  if (start_pwm()) {
    return 0;
  }
  /************************* Speaker PWM setup end *************************/

  /************************ Speaker DMA setup begin ************************/
  if (start_dma_for_pwm()) {
    return 0;
  }
  /************************* Speaker DMA setup end *************************/

  /*********************** LED blinky example begin ************************/
  if (setup_blinky_led()) {
    printk("Failed to setup blinky LED\n");
    return 0;
  }
  /************************ LED blinky example end *************************/

  while (1) {
    if (toggle_led()) {
      return 0;
    }

    k_msleep(SLEEP_TIME_MS);
  }

  return 0;
}

pwm_setup.h

#ifndef PWM_SETUP_H
#define PWM_SETUP_H

#include <zephyr/device.h>
#include <zephyr/drivers/clock_control/stm32_clock_control.h>
#include <zephyr/drivers/dma.h>
#include <zephyr/drivers/pwm.h>
#include <zephyr/kernel.h>  // printk

#include "stm32wb55xx.h"
#include "stm32wbxx_ll_tim.h"

/* Define the buzzer PWM device */
#define BUZZER_PWM_NODE DT_ALIAS(pwm_speaker)
#if !DT_NODE_HAS_STATUS(BUZZER_PWM_NODE, okay)
#error "Speaker PWM device not found or not enabled in device tree"
#endif

/* Static PWM device reference */
static const struct device *pwm_dev = DEVICE_DT_GET(BUZZER_PWM_NODE);

/* Define the DMA device node */
#define DMA2_DEVICE_NODE DT_ALIAS(dma_speaker)
#if !DT_NODE_HAS_STATUS(DMA2_DEVICE_NODE, okay)
#error "DMA device not found or not enabled in device tree"
#endif

/* Static DMA device reference */
static const struct device *dma2_dev = DEVICE_DT_GET(DMA2_DEVICE_NODE);
static uint32_t dma_callback_count = 0;

int start_pwm() {
  // Retrieve PWM device node for speaker pins
  if (!device_is_ready(pwm_dev)) {
    printk("Error: PWM device not ready\n");
    return -ENODEV;
  }

  printk("Enabling PWM\n");
  // Start PWM on TIM2 channel 1 (PA15)
  if (pwm_set(pwm_dev, 1, PWM_USEC(23), PWM_USEC(11), 0)) {
    printk("PWM channel 1 (PA15) start failed\n");
    return -1;
  }

  return 0;
}

void dma_callback(const struct device *dev, void *user_data, uint32_t channel,
                  int status) {
  dma_callback_count++;
  printk("DMA Callback count: %d\n", dma_callback_count);
}

int start_dma_for_pwm() {
  // Retrieve DMA2 device node for PWM duty cycle DMA requests
  if (!device_is_ready(dma2_dev)) {
    printk("Error: DMA2 device not ready\n");
    return -ENODEV;
  }

  // Retrieve PWM device node for speaker pins
  if (!device_is_ready(pwm_dev)) {
    printk("Error: PWM device not ready\n");
    return -ENODEV;
  }

  printk("Enabling DMA interrupts on TIM2 CH1\n");
  // Enable DMA interrupts on TIM2 CH1
  pwm_enable_dma(pwm_dev, 1);

  // Configure the DMA
  struct dma_config dma_cfg = {0};
  struct dma_block_config dma_block = {0};
  /* Get the base address of Timer 2 */
  uint32_t timer2_base = DT_REG_ADDR(DT_NODELABEL(timers2));
  // #define DMA_REQUEST_TIM2_CH1 0x0000001CU
  dma_cfg.dma_slot = 0x0000001CU;
  dma_cfg.channel_direction = MEMORY_TO_PERIPHERAL;
  dma_cfg.complete_callback_en = true;
  dma_cfg.error_callback_dis = true;
  dma_cfg.channel_priority = 0x2; /* high priority */
  dma_cfg.source_data_size = 4;   /* 32-bit data */
  dma_cfg.dest_data_size = 4;     /* 32-bit data */
  dma_cfg.source_burst_length = 1;
  dma_cfg.dest_burst_length = 1;
  dma_cfg.block_count = 1;
  dma_cfg.head_block = &dma_block;
  dma_cfg.dma_callback = dma_callback;
  // Set up DMA block for circular transfer
  dma_block.block_size = AUDIO_DATA_SIZE * sizeof(uint32_t);
  dma_block.source_address = (uint32_t)audioData_high;
  dma_block.dest_address =
      (uint32_t)timer2_base + 0x34; /* CCR1 offset is 0x34 */
  dma_block.source_addr_adj = DMA_ADDR_ADJ_INCREMENT;
  dma_block.dest_addr_adj = DMA_ADDR_ADJ_NO_CHANGE;
  dma_block.source_reload_en = true; /* circular mode */
  dma_block.dest_reload_en = true;   /* circular mode */

  /* Start the DMA transfer on channel 1 */
  if (dma_config(dma2_dev, 1, &dma_cfg) != 0) {
    printk("DMA2 config failed\n");
    return -EIO;
  }
  if (dma_start(dma2_dev, 1) != 0) {
    printk("DMA2 start failed\n");
    return -EIO;
  }

  return 0;
}

#endif /* PWM_SETUP_H */

(Little late to this, just stumbled upon this when evaluating how to use the STM32 timers for sending ws2812 data sequences)

It would be really nice to have a sample in the repo that actually exercises this functionality, too, so folks don't need to search for this issue to understand how to use this new feature. Thanks for working on this!

@SiViSur
Copy link
Copy Markdown
Contributor Author

SiViSur commented Mar 13, 2026

(Little late to this, just stumbled upon this when evaluating how to use the STM32 timers for sending ws2812 data sequences)

It would be really nice to have a sample in the repo that actually exercises this functionality, too, so folks don't need to search for this issue to understand how to use this new feature. Thanks for working on this!

@petejohanson You're right. I'll try to include this in the follow-up MR that @aescolar requested. It will take me a bit longer though since I will have to test it on the board, but I think it's worth it.

@mindnever
Copy link
Copy Markdown

It is also shame that enhancement does not give the API to get timer base or at least pointer to CCRn register, cause that is the only thing missing for complete dma setup. Without that api, you either have to guess the timer unit, or to get it as parent of pwm.

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

I've also got to this PR some days ago, also looking for ways to implement compatible = "worldsemi,ws2812-pwm" and I think I got it right. I can also share if interested.

@petejohanson
Copy link
Copy Markdown
Contributor

It is also shame that enhancement does not give the API to get timer base or at least pointer to CCRn register, cause that is the only thing missing for complete dma setup. Without that api, you either have to guess the timer unit, or to get it as parent of pwm.

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

I've also got to this PR some days ago, also looking for ways to implement compatible = "worldsemi,ws2812-pwm" and I think I got it right. I can also share if interested.

I've got a mostly working implementation I was planning to share in a day or two

@mindnever
Copy link
Copy Markdown

mindnever commented Mar 17, 2026

Take a look at this one:

main...mindnever:zephyr:ws2812-pwm

I'm not sure if its good enough for PR. I have tested it with Nucleo H563ZI and there is overlay file for it, but it is also working with F103RE on another proprietary board.

@petejohanson
Copy link
Copy Markdown
Contributor

Take a look at this one:

main...mindnever:zephyr:ws2812-pwm

I'm not sure if its good enough for PR. I have tested it with Nucleo H563ZI and there is overlay file for it, but it is also working with F103RE on another proprietary board.

Looks very reasonable to me. My version, tested so far on stm32g0b1 and stm32wb55rg but will test on a few other targets when I have time: petejohanson@5bda69f

(Note: Mine is in a backport to Zephyr v4.1.0, so beware it's built on an older base with this PR's DMA API change cherry-picked in)

Compared to yours, mine lacks:

  • nocache support for handling series where that's a concern.

Things I've implemented that don't seem to be included with yours:

  • Tweaks to timing in the DTS at a more granular level, so you can specify the bit period, and then period for the high portion of a period for 0/1 values. This timing can very wildly between different LEDs, e.g. SK6803-MINI-E versus WS2812B-2020, so I think being able to specify this is important.
  • I'm using a k_mem_slab with double the memory storage for the compare values, so we can allocate the second chunk and prepare the next sequence while the current one is being transmitted by PWM + DMA.

I'm have no attachment to my implementation "winning", but it would be nice to have the two above features included if/when you PR your version into Zephyr. I would also suggest you adjust the naming of your driver, since it is STM32 specific, and ws2812,pwm indicates a more generic driver than it really is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: PWM Pulse Width Modulation platform: STM32 ST Micro STM32

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants