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
Instance | Base address |
---|---|
PWM0 | 0x4001C000 |
PWM1 | 0x40021000 |
PWM2 | 0x40022000 |
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
Prescaler | PWM clock frequency |
---|---|
0 | 16 MHz |
1 | 8 MHz |
2 | 4 MHz |
3 | 2 MHz |
4 | 1 MHz |
5 | 500 kHz |
6 | 250 kHz |
7 | 125 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 inCOUNTERTOP
register, the internal counter register can reset its value to 0 immediately or start counting down to 0, depending on theMODE
register. TheCOUNTERTOP
andMODE
values determine the PWM frequency. For example, ifMODE
is set toUp
, PWM clock frequency is 1 MHz (by using prescaler value of 4 as mentioned above) andCOUNTERTOP = 10000
, the period of the generated signal is10000 * 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:
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:
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
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.