nRF52 Pulse Width Modulation

Introduction

In this article, you will learn about the Pulse Width Modulation (PWM) module of Nordic nRF52 and the nrf_drv_pwm library in nRF5 SDK to control it.

To compile and run your code on real nRF52 hardware, it is recommended that you have a nRF52 development kit such as

Affiliate Disclosure: When you click on links in this section and make a purchase, this may result in this site earning a commission at no extra cost to you.

The nRF52 PWM peripheral

In this section, we take a look at the nRF52832 PWM peripheral and understand how it works. It is important to know about the PWM hardware because it helps to understand the nrf_drv_pwm library that we are going to use.

Instances

In nRF52, one PWM module can control 4 PWM channels. Each channel can generate PWM signal on a GPIO pin. In other words, one PWM module can generate signals on up to 4 GPIO pins. nRF52832 has 3 PWM modules, which means it can generate 12 PWM signals on 12 GPIO pins. 3 PWM modules are named PWM0, PWM1 and PWM2 and has the following base addresses

InstanceBase address
PWM00x4001C000
PWM10x40021000
PWM20x40022000

Frequency

Each PWM module has a fixed base clock frequency (= 16 MHz). The frequency of the PWM clock signal can be divided by using a prescaler register (offset address 0x50C). The table below listed possible values of prescaler register

PrescalerPWM clock frequency
016 MHz
18 MHz
24 MHz
32 MHz
41 MHz
5500 kHz
6250 kHz
7125 kHz

Internally, the PWM module uses a 15-bit counter which can increase its value by 1 every clock interval. For example, if prescaler = 4, the clock frequency is 1 MHz which is equivalent to 1 us period. Once PWM module is running, the internal counter register value will increase by 1 every 1 microsecond.

Output generation

For each PWM instance, output can be generated on 4 pins defined by PSEL.OUT[0], PSEL.OUT[1], PSEL.OUT[2] and PSEL.OUT[3] registers. The waveform generated on output pins depends on several parameters:

  • COUNTERTOP: this value stores the maximum value that the internal counter register can reach. Once reached the value stored in COUNTERTOP register, the internal counter register can reset its value to 0 immediately or start counting down to 0, depending on the MODE register. The COUNTERTOP and MODE values determine the PWM frequency. For example, if MODE is set to Up, PWM clock frequency is 1 MHz (by using prescaler value of 4 as mentioned above) and COUNTERTOP = 10000, the period of the generated signal is 10000 * 1 us = 10 ms.
  • COMPARE[0], COMPARE[1], COMPARE[2], COMPARE[3]: these compare registers store the values for internal counter to compare to. When reaching the values stored in compare registers, the signal on the output pin is inverted (from low to high or from high to low). There are 4 compare registers correspond to 4 output pins. These diagrams below illustrate the operation:
PWM Up mode diagram. Source: nRF52832 specification
PWM Up and Down mode. Source: nRF52832 specification.

For example, let’s say you want to generate a PWM signal with a frequency of 1 kHz and 50% duty cycle on GPIO pin 15, you would need to set the following parameters:

  • PSEL.OUT[0] = 15
  • PRESCALER = 4
  • MODE = UP
  • COUNTERTOP = 1000
  • COMPARE[0] = 500

When the PWM is running, its internal counter starts counting from 0. When it reaches 500, the output is inverted. It will keep counting to 1000 and then reset to 0.

Loading PWM parameters from RAM

The PWM module does not allow accessing compare registers directly. The compare values are stored in RAM instead. The PWM module uses EasyDMA and DECODER blocks to load those values from RAM to compare registers. As a reminder, EasyDMA is a block in nRF52 that allow accessing RAM directly. The DECODER module, as the name suggests, decodes the meaning of the data stored in RAM and put them to correct compare registers. To understand its operation, let’s take a look at the block diagram:

PWM block diagram. Source: nRF52832 specification.

As can be seen from the above diagram, data in RAM can be stored as Sequence 0 and Sequence 1. Data in these sequences are stored in a specific order so that the DECODER module can interpret and load them into compare registers. The DECODER register (offset address 0x510) has a LOAD field that tells how it will read data in RAM as shown in the below diagram

PWM Decoder load mode. Source: nRF52832 specification.

If DECODER.LOAD = Common, a half word (16-bit) value is loaded to all compare registers. In other words, all output pins will have the same pwm signal. If DECODER.LOAD = Single, there will be 4 different 16-bit values for 4 pwm channels.

Now let’s take an example to understand how everything works. Supposed you want to generate a PWM signal to drive a LED with a frequency of 1 kHz and duty cycle of 50%, how would you set it up? You would need to calculate the values of COUNTERTOP and COMPARE registers, for instance, you can use the previous example values of PRESCALER = 4, MODE = UP, COUNTERTOP = 1000, COMPARE[0] = 500. Since we are driving only a single LED, we can use Common mode and define a sequence in RAM storing a single value of 500. When the PWM is running, it will load this value from RAM to compare register 0 and start counting and generating a PWM signal with 1 kHz and 50% duty cycle to drive the LED.

A more complicated example is to drive two LEDs with different duty cycles: LED 1 with 50% duty cycle and LED 2 with 20% duty cycle. In this case, you can use Single mode and store an array in RAM as follow

{ 500, 200, 0, 0 }

When running, the DECODER module will load 500 to compare register 0 and 200 to compare register 1 to drive LED 1 and LED 2, respectively.

The nrf_drv_pwm library

In this section, we will look at the nrf_drv_pwm library in nRF5 SDK to control the PWM peripheral.

Including the library

To use the library, you need to include it in your source file list

SRC_FILES += \
  $(SDK_ROOT)/modules/nrfx/drivers/src/nrfx_pwm.c \

and include the header file in your application

#include "nrf_drv_pwm.h"

Enabling PWM in sdk_config

Next, remember to enable the PWM module in sdk_config.h

#define PWM_ENABLED 1

and enable the PWM instance you want to use. For instance, if you want to use PWM0:

#define PWM0_ENABLED 1

Declare an instance variable

To use a specific PWM instance, you’ll need to use declare an instance variable of type nrf_drv_pwm_t and use macro NRF_DRV_PWM_INSTANCE() to initialise it with instance index number. For example,

static nrf_drv_pwm_t m_pwm0 = NRF_DRV_PWM_INSTANCE(0);

Initialise PWM

To initialise PWM module, you use the API nrf_drv_pwm_init(). This API is the same as nrfx_pwm_init() used internally

nrfx_err_t nrfx_pwm_init(nrfx_pwm_t const * const  p_instance,
                         nrfx_pwm_config_t const * p_config,
                         nrfx_pwm_handler_t        handler);

This function takes a pointer to the instance variable, a pointer to a configuration structure and a handler function. The configuration structure tells how to configure the PWM instance. For example

static nrf_drv_pwm_config_t const config0 =
        {
            .output_pins =
            {
                15,                                   // channel 0
                NRF_DRV_PWM_PIN_NOT_USED,             // channel 1
                NRF_DRV_PWM_PIN_NOT_USED,             // channel 2
                NRF_DRV_PWM_PIN_NOT_USED,             // channel 3
            },
            .irq_priority = APP_IRQ_PRIORITY_LOWEST,
            .base_clock   = NRF_PWM_CLK_1MHz,         // 1 us period
            .count_mode   = NRF_PWM_MODE_UP,
            .top_value    = 1000,
            .load_mode    = NRF_PWM_LOAD_COMMON,
            .step_mode    = NRF_PWM_STEP_AUTO
        };

In this example, we are declaring a configuration structure of type nrf_drv_pwm_config_t and tells using pin 15 as the pwm channel 0 output pin, use base clock of 1 MHz (corresponding to prescaler = 4), using UP mode and set the COUNTERTOP value to 1000 so that the generated signal’s frequency is 1 kHz. The NRF_PWM_LOAD_COMMON in load_mode field configures the DECODER module to use Common mode. After defining this configuration variable, you can initialise the PWM module as

APP_ERROR_CHECK(nrf_drv_pwm_init(&m_pwm0, &config0, NULL));

Defining sequence values in RAM

Since the PWM module use EasyDMA to load sequence values directly from RAM, you would need to define an array storing values to load to compare registers.

static uint16_t /*const*/ seq_values[] =
        {
            500,
        };
nrf_pwm_sequence_t const seq =
        {
            .values.p_common = seq_values,
            .length          = NRF_PWM_VALUES_LENGTH(seq_values),
            .repeats         = 0,
            .end_delay       = 0
        };

Starting the PWM

To start the PWM, you can use the following API nrf_drv_pwm_simple_playback() which is internally equivalent to nrfx_pwm_simple_playback

uint32_t nrfx_pwm_simple_playback(nrfx_pwm_t const * const   p_instance,
                                  nrf_pwm_sequence_t const * p_sequence,
                                  uint16_t                   playback_count,
                                  uint32_t                   flags);

You pass in the pointer to instance variable and pointer to sequence data in RAM in the first and second arguments. The third argument playback_count tells how many times it would need to play the sequence. The last flags argument is used to indicate whether to stop or continue playing the sequence in a loop. If flags = NRFX_PWM_FLAG_STOP, the PWM is stopped after playing the sequence. If flags = NRFX_PWM_FLAG_LOOP, it will continue playing in a loop by using SHORTS register. For example, you can use

nrf_drv_pwm_simple_playback(&m_pwm0, &seq, 1, NRFX_PWM_FLAG_LOOP);

Wrapping Up

In this article, you have learnt about the PWM peripheral in Nordic nRF52 and the nrf_drv_pwm library in nRF5 SDK. From here, you can explore more complex usages of the PWM module using API reference from Nordic Infocenter.

Leave a Comment