Using a PWM Device in Zephyr

Mark Zachmann
Oct 24 · 4 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.

Parents

The myboard.dts file has this line:

#include <nordic/nrf52840_qiaa.dtsi>

This includes dts/arm/nordic/nrf52840_qiaa.dts, which defines the qiaa rev of the nrf52840 chip as 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>;
};
pwm1: pwm@40021000 {
compatible = "nordic,nrf-pwm";
reg = <0x40021000 0x1000>;
interrupts = <33 1>;
status = "disabled";
label = "PWM_1";
#pwm-cells = <1>;
};
... (pwm2 and pwm3 entries as well)

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 — uses the nordic,nrf-pwm device definition, resides at 4001c00, is named “PWM_0”, uses interrupt 28, and requires a pwm-cell definition to describe the pin mappings. It is disabled by default.

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>;
ch0-inverted;
};

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

This, paired with adding this to Kconfig.defconfig:

if PWMconfig PWM_0
default y
endif # PWM

Ensures that the pwm_0 is enabled.

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. So we can add to the myboard.dtsi file (in the device area at the top):

aliases {
pwmsound = &pwm_dev0;
};
pwmdevs {
compatible = "pwm-leds";
pwm_dev0: pwm_dev_0 {
pwms = <&pwm0 33>;
};
};

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

This is a lot of abstraction to be able to refer to the device under the alias pwmsound.

In Source Code

To use it in code we have to know what DEFINEs this produced. Take a look at the generated file build/zephyr/include/generated/generated_dts_board.conf and look for pwmsound :

# Devicetree node: /pwmdevs/pwm_dev_0
# Binding (compatible = pwm-leds): ZEPHYR_BASE\dts\bindings\led\pwm-leds.yaml
# Binding description: PWM LED child node
DT_PWM_LEDS_PWM_DEV_0_PWMS_CONTROLLER="PWM_0"
DT_ALIAS_PWMSOUND_PWMS_CONTROLLER="PWM_0"
DT_PWM_LEDS_PWMSOUND_PWMS_CONTROLLER="PWM_0"
DT_PWM_LEDS_PWM_DEV_0_PWMS_CHANNEL=33
DT_ALIAS_PWMSOUND_PWMS_CHANNEL=33
DT_PWM_LEDS_PWMSOUND_PWMS_CHANNEL=33

Finally we can use the generated defines in source code:

#if defined(DT_ALIAS_PWMSOUND_PWMS_CONTROLLER) && defined(DT_ALIAS_PWMSOUND_PWMS_CHANNEL)
/* get the defines from dt (based on alias 'pwm-led0') */
#define PWM_DRIVER DT_ALIAS_PWMSOUND_PWMS_CONTROLLER
#define PWM_CHANNEL DT_ALIAS_PWMSOUND_PWMS_CHANNEL
#else
#error "Choose supported PWM driver"
#endif

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);
return;
}
pwm_get_cycles_per_sec(pwm_dev, PWM_CHANNEL, &cycles);
....

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. Use (here) DT_ALIAS_PWM_0_LABEL for the controller name and DT_ALIAS_PWM_0_CH0_PIN for the pin number. The code isn’t as portable but it works.

Caveats

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");
return;
}
k_sleep(MSEC_PER_SEC * 1U); // play for 1 second// turn it off
if (pwm_pin_set_usec(pwm_dev, PWM_CHANNEL, timeus, 0)) {
printk("pwm off fails\n");
return;
}
Mark Zachmann

Written by

Entrepreneur, software architect, electrical engineer. Ex-academic.

Home Wireless

Home automation in the wireless IOT era

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade