Home Wireless
Published in

Home Wireless

Using a PWM Device in Zephyr

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 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>;
};
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 — 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>;
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

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

CONFIG_PWM=y

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
*/
/* Node's dependency ordinal: */
#define DT_N_S_pwmdevs_S_pwm_dev_0_ORD 17
/* Ordinals for what this node depends on directly: */
#define DT_N_S_pwmdevs_S_pwm_dev_0_REQUIRES_ORDS \
14, /* /pwmdevs */ \
16, /* /soc/pwm@4001c000 */
/* Ordinals for what depends directly on this node: */
#define DT_N_S_pwmdevs_S_pwm_dev_0_SUPPORTS_ORDS /* nothing */
/* Existence and alternate IDs: */
#define DT_N_S_pwmdevs_S_pwm_dev_0_EXISTS 1
#define DT_N_ALIAS_pwmsound DT_N_S_pwmdevs_S_pwm_dev_0
#define DT_N_NODELABEL_pwm_dev0 DT_N_S_pwmdevs_S_pwm_dev_0
/* Special property macros: */
#define DT_N_S_pwmdevs_S_pwm_dev_0_STATUS_okay 1
/* Generic property macros: */
#define DT_N_S_pwmdevs_S_pwm_dev_0_P_pwms_IDX_0_VAL_channel 26

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)
#define PWM_DRIVER DT_PWMS_LABEL(DT_ALIAS(pwmsound))
#define PWM_CHANNEL DT_PWMS_CHANNEL(DT_ALIAS(pwmsound))
#else
#error "Choose a 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. Look at the devicetree_unfixed.h file to find the right defines.

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

Home automation in the wireless IOT era

Recommended from Medium

Obsidian Code Snippets

Our xJOE Degenbox strategy!

Android Unidirectional Data Flow — Local Unit Testing

Application Programming Interface

Deploying Database Migrations with Spinnaker and Kubernetes

Importance of Logging + Best Practices

Node.js Certifications and Training Sale + New Preview of Testing Environment

The Seen and The Unseen Part 2

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
Mark Zachmann

Mark Zachmann

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

More from Medium

Getting Hands Dirty with Intel Cache Allocation Technology

Mathematical Miracles in the Holy Quran

How to use a Spindle Camera and SpindleView

Syllabifying toki pona with a Regular Expression