Using a PWM Device in Zephyr

Mark Zachmann
Oct 24, 2019 · 5 min read

Using devices in Zephyr is tricky because there are so many options and settings at first that it’s just not clear from documentation and samples how to do even easy things.

My Simple Example

I have a board based on the Nordic Nrf52840 that uses one pin (Pin 33) to play simple tones on a speaker. I’d like to use the PWM facility to drive the speaker as easily as possible.

There are a few Zephyr samples that use PWM. The simplest is samples/basic/blink_led. There aren’t any usable comments in the sample but it does work.

The Device Tree

We start by looking at the existing device tree that we’ll use/override.


The nrf52840 is an arm-based board so inside of the {zephyr}/boards/arm folder contains the folder for my board — for example the folder nrf52840dk_nrf52840 for the dev kit. Inside of that folder, the myboard.dts file has this line to include the base CPU definition:

#include <nordic/nrf52840_qiaa.dtsi>

The file dts/arm/nordic/nrf52840_qiaa.dts defines the qiaa rev of the nrf52840 chip (the one with 1MB of flash and 256KB of ram). That file includes the base nordic devicetree file at dts\arm\nordic\nrf52840.dtsi. Looking at that tree for pwm we find:

pwm0: pwm@4001c000 {
compatible = "nordic,nrf-pwm";
reg = <0x4001c000 0x1000>;
interrupts = <28 1>;
status = "disabled";
label = "PWM_0";
#pwm-cells = <1>;

The nrf52840 has 4 pwm modules with 4 channels each. These entries partially define devices pwm0, pwm1, … as parts of the nrf52840 SOC (system on chip).

In words: pwm0 — is a nordic,nrf-pwm device, it resides in the 4K (0x1000) block at 0x4001c00, is named “PWM_0”, and uses interrupt 28. It is disabled by default. They are nice enough to show the required pwm-cells commented out.

Examine the yaml file at dts/bindings/pwm/nordic,nrf-pwm.yaml to see the meta-definition of the nrf-pwm device.

Our Defines

Now for the higher level board definition (in myboard.dtsi) add something like:

&pwm0 {
status = "okay";
ch0-pin = <33>;

The & indicates that we’re adding/changing the base pwm0 definition to

  • status = “okay” enables it (required)
  • set the channel 0 pin to 33 (required)
  • say that the channel 0 pin is inverted (optional)

We could add more channels and pins but here I’m just using one pin/channel.

Now add one line to the myboard.yaml file in the supported: section

- pwm

Now we have a pwm defined but we’re not yet easily using it in the board. To encapsulate it comfortably we want to give the pwm/channel pair a name. The simplest approach is to note that pwm-leds has a high-level pwm definition that isolates one pwm pin. So we can add to the myboard.dtsi file (in the device area at the top):

aliases {
pwmsound = &pwm_dev0;

This defines the pwmdevs device group as having one pwm-leds device named pwm_dev0 that uses pin 33 (hence channel 0). It has a usable name (alias) of pwmsound. Personally the use of the pin instead of channel here is odd.

This is a lot of abstraction to be able to refer to channel 0 of pwm0 by name pwmsound.

To ensure that the pwm code from the zephyr source is included, we have to add a line


to the prj.conf file in the project. The project would compile without this line but then it would fail to find any pwm devices.

In Source Code

To use it in code it helps to know what DEFINEs this produced. Take a look at the generated file build/zephyr/include/generated/devicetree_unfixed.h and look for pwmsound. Here’s a piece of the file ->

* Devicetree node: /pwmdevs/pwm_dev_0
* Node identifier: DT_N_S_pwmdevs_S_pwm_dev_0
* Binding (compatible = pwm-leds):
* $ZEPHYR_BASE\dts\bindings\led\pwm-leds.yaml
* Description:
* PWM LED child node

Finally we can use the generated defines in source code. DT_ALIAS(pwmsound) returns the very long define with pwm_dev_0 and then we can extract the label and channel

#if DT_NODE_HAS_STATUS(DT_ALIAS(pwmsound), okay)
#error "Choose a supported PWM driver"

which defines PWM_DRIVER ("PWM_0") and PWM_CHANNEL (at pin 33) for the rest of the code

struct device *pwm_dev;
u64_t cycles;
pwm_dev = device_get_binding(PWM_DRIVER);
if (!pwm_dev) {
printk("Cannot find %s!\n", PWM_DRIVER);

Do I Need to Do All This?

Not really. Just define and enable at least one pwm channel (pin) - see the below dtsi code and add the aforesaid .yaml lines.

&pwm0 {
status = "okay";
ch0-pin = <33>;

Then refer to pwm0 and the pin more directly. Look at the devicetree_unfixed.h file to find the right defines.


The Zephyr Pwm interface is incredibly primitive and poorly documented. There’s no on or off, just set a pwm period (cycle-time) and pulse-width(on-time). Setting both positive turns on the pwm and setting pulse-width to zero (0) turns it off in the nrf52840 pwm api.

Other soc’s are different (some don’t let you turn off the pwm). Also, the nrf52840 pwm api always declares a cycle frequency of 16MHz, but when you set the period/turn it on it sets the prescaler automatically. To change the prescaler value you have to turn the pwm off, which produces opaque — but working — code.

if (pwm_pin_set_usec(pwm_dev, PWM_CHANNEL, timeus, timeus / 2U)) {
printk("pwm pin set fails\n");

Home Wireless

Home automation in the wireless IOT era

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store