For this exercise, we will use the Tower Pro Micro Servo SG90 servo motor, but you can also follow along without any additional hardware by using the LED’s to simulate a motor similar to what we did in exercise 1.
The nRF91x1 DKs and the nRF7002 DK only have 1.8 V output, which is not enough to control the servo motor used in this course. If you are using one of these boards, you can follow along by using one of the LEDs to simulate a motor or use a level shifter, which we will not cover in this exercise.
The nRF54L15DK has 1.8 V output by default, which is not enough to control the servo motor used in this course. If you are using this board, you should use the nRF Connect Board Configurator app to change VDD to 3.3V.
This exercise assumes that you’ve set VDD to 3.3V using the Board Configurator app.
The first thing we will do is use the pwm_led
instance we used in the first exercise and create a custom variation of this that we will use for our PWM signal that will drive another GPIO other than LED 1. If you don’t have a motor available, follow along with the steps in this exercise, but don’t change the GPIO that this PWM instance will drive.
The data sheet for the servo motor we will use can be found here: http://www.ee.ic.ac.uk/pcheung/teaching/DE1_EE/stores/sg90_datasheet.pdf
The datasheet shows that the PWM period is 20ms (50Hz), and the duty cycle is between 1ms and 2ms. Recall that these are the same values we used in the previous exercise when we tested the PWM peripheral for controlling LED1, and we observed that these values only resulted in a dimly lit LED. For the specific motor used in this exercise the values must be within the parameters to ensure that the motor only moves within the range it is physically able to move within.
The servo can rotate approximately 90 degrees in each direction. 1ms pulse corresponds to -90 degrees, 1.5ms corresponds to 0 degrees and 2ms corresponds to 90 degrees rotation.
In Exercise 1, we used LED1 as our PWM LED, which is connected to P0.13 for the nRF52840 DK. We need to use one of the available GPIOs for our motor. To do this, we need to make some modifications to the device tree.
Open the code base of the exercise by navigating to Create a new application in the nRF Connect for VS Code extension, select Copy a sample, and search for Lesson 4 – Exercise 2. Make sure to use the version directory that matches the SDK version you are using.
Alternatively, in the GitHub repository for this course, go to the base code for this exercise, found in l4/l4_e2
of whichever version directory you are using.
You must build the application before beginning for some of the VS Code devicetree functionality to work properly.
1. Creating a custom PWM device
A useful resource when learning about devicetree is to inspect a board’s devicetree to see what nodes/devices are predefined on a board. For this exercise, we will use the devicetree for the nRF52840DK, nRF52840dk_nrf52840.dts
, which we will open and inspect in the following sections to illustrate the peripherals and devices we need to use.. If you’re using the VS Code extension directly, you may find the .dts file under “Config files-> Devicetree” in the details view of the extension or in the relative path <install_path>\ncs\zephyr\boards\arm
.
1.1 Create an overlay for your board
In the template of this exercise we’ve created a folder named boards
which contains an .overlay file. Open this directory, l4_e2/boards
, and rename the overlay file to the name of the build target for the board you are using, for example nrf52840dk_nrf52840.overlay
. If you’ve created your own project from scratch instead of using the template, create a new overlay file located in l4_e2/boards
and follow the same naming convention as just mentioned.
Open the .dts for your board, for instance nRF52840dk_nrf52840.dts
for the nRF52840DK, or locate the board file for your board as stated in the introduction for step 1. When opening the .dts in the extension, you should be able to locate compatibles such as the predefined LEDs and buttons as well as the predefined node pwmleds
.
The .dts contains the default definitions of what the DKs nodes/compatibles/devices should do and what GPIOs they are connected to. Every project in this version of the SDK that uses one of these boards will use this exact .dts file, so we will not make our modifications here. Instead, we will make the modifications in an overlay file that is specific to this project.
In the image above, you can see that the overlay file we have created will place itself in the devicetree folder located under the Config files item in the extension.
1.2 Add a pwm_led
instance and change the polarity.
We want to create a custom device that should drive a GPIO with a PWM signal. From the nrf52840dk_nrf52840.dts, L:48-53
, we know that we have a predefined pwm_led
instance, which we can modify to fit our needs.
We will copy the pwm_led
instance from the .dts and paste it into your project’s overlay file under step 1.2. Regardless of which board you’re using, you can copy this step as long as it has a PWM peripheral. You can verify this by reading the product specification for your specific board.
In addition to adding this node, we want to change the polarity to normal. This means that the PWM signal will have a high output for the duty cycle instead of a low.
Add the following code snippet to the overlay file
/{
pwmleds {
compatible = "pwm-leds";
pwm_led0: pwm_led_0 {
pwms = <&pwm0 0 PWM_MSEC(20) PWM_POLARITY_NORMAL>;
};
};
};
DevicetreeThis snippet means that we now have a node in our overlay that will overwrite what is defined in the .dts file. This node is a pwmleds
node that uses channel 0 on the pwm0
peripheral of the board, and has an alias named “pwm_led0”. For now,
this is not any different from what is already defined in the .dts file other than that the polarity has been changed, but we will further modify this node in the next two steps.
We will use the alias pwm_led0
to showcase that this alias is just a name and that the properties can easily be modified to drive other devices on another GPIO.
1.3 Add your own custom pwm0
instance.
In your overlay file, right-click on “&pwm0
” in the instance of pwmleds
in and click Go to Definition. This will take you to the definition in the devicetree file we had a look at earlier, and you will see something like this:
This will only work if you have tried to build the application before this step.
Notice that the pwm0
instance has two states, pwm0_default
and pwm0_sleep
. This is what is sent out on the GPIO connected to this pin control instance. We want to copy the &pwm0
instance into your overlay file, but create our own custom states by changing the names to pwm0_custom
and pwm0_csleep
.
Add the following code snippet in the overlay file
&pwm0 {
status = "okay";
pinctrl-0 = <&pwm0_custom>;
pinctrl-1 = <&pwm0_csleep>;
pinctrl-names = "default", "sleep";
};
DevicetreeIf you’re using the nRF54L15 DK you must replace “pwm0” with “pwm20”. This is because the PWM peripheral is defined in the nRF54L15 as the three pwm20, pwm 21 and pwm22 devices.
&pwm20 {
status = “okay”;
pinctrl-0 = <&pwm20_custom>;
pinctrl-1 = <&pwm20_csleep>;
pinctrl-names = “default”, “sleep”;
};
1.4 Configure which pins your custom pwm0
instance should use through pinctrl
Inspect pwm0_default
and pwm0_sleep
in your boards devicetree file by right-clicking on one of them and clicking Go to Definition again.
It should look something like this:
We will use the same format showcased in line 77-89 in the image above in our overlay file but change the names to pwm0_custom
and pwm0_csleep
, see the following code snippet as an example
&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 13)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 13)>;
low-power-enable;
};
};
};
DevicetreeUp until this step, we’ve created a custom pwm0
instance that does exactly the same as the pwm_led0
instance, only with different state names. For the nRF52840 DK, our new custom pwm0
is set to use P0.13 (port 0, pin 13) for its output. Since pin 13 is used by LED 1 on the nRF52840 DK, we need to change the output pin if we want to control anything other than LED1.
For instance, set <NRF_PSEL(PWM_OUT0, 0, 14)>;
to have the PWM output go to GPIO 14, i.e LED 2, instead of GPIO 13, i.e LED 1. If you don’t have a motor available, then this step is sufficient to showcase how to drive a GPIO with a PWM signal.
If you’re using a motor, we instead want to use a free available GPIO on the DK. This means that if you wish to use anything other than what we’ve described here, you need to consult with the product specification of the device you’re using and check if that GPIO is available for use. A good starting point is to inspect the product specification for your development kit to find a GPIO that is not used for anything else. For instance, in the User Guide for the nRF52840 DK, in section “8.6 Connector interface”, you can see that all the GPIOs in P6 and P24 are already in use for the items listed in the figure.
The solution in this exercise will use the following GPIOs for the 3V boards and the GPIO connected to LED2 for the case of the 1.8V boards (nRF7002 DK and nRF91x1 DK)
Board | GPIOs in the overlay solution |
nRF52 DK | P0.03 |
nRF52833 DK | P0.03 |
nRF52840 DK | P0.03 |
nRF5340 DK | P0.05 |
nRF7002 DK | P0.07 (LED2) |
nRF9160 DK | P0.10 |
nRF91x1 DK | P0.01 (LED2) |
nRF54L15 DK | P1.11 |
Add the snippet corresponding to your board into your overlay file.
&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 5)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 5)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm20_custom: pwm20_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 11)>;
nordic,invert;
};
};
pwm20_csleep: pwm20_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 11)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 7)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 7)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 10)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 10)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 1)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 1)>;
low-power-enable;
};
};
};
Devicetree2. Control the motor angle of the servo.
We have now set up our device to have a PWM output on pin 3, but we still need to decide how to set the pin. Let’s create the function set_motor_angle()
that sets the motor angle based on the input parameter duty_cycle_ns
.
This input parameter will be decided based on the motor’s datasheet. For the SG90 servo motor, you can see that here.
From the datasheet we can see that the duty cycle ranges from 1-2 ms while the PWM period is 20 ms (50Hz).
2.1 Define the function set_motor_angle()
to set the motor angle.
Define a function to set the motor angle. This function should take in the PWM duty cycle or an angle between 0 and 180 degrees as the input parameter and use pwm_set_dt()
to set it accordingly.
Add the following code snippet to the main.c file
int set_motor_angle(uint32_t duty_cycle_ns)
{
int err;
err = pwm_set_dt(&pwm_led0, PWM_PERIOD, duty_cycle_ns);
if (err) {
LOG_ERR("pwm_set_dt_returned %d", err);
}
return err;
}
C2.2 Define a maximum and minimum duty cycle
Inspect the motor’s datasheet and define the macros PWM_MIN_DUTY_CYCLE
and PWM_MAX_DUTY_CYCLE
based on this.
Note that you may need to experiment as what corresponds to the minimum and maximum angle may vary from motor to motor due to wear and tear.
We will set the following as our parameters:
#define PWM_MIN_DUTY_CYCLE 1000000
#define PWM_MAX_DUTY_CYCLE 2000000
C2.3 Check if the device is ready and set it to an initial value:
Use the API-specific pwm_is_ready_dt()
to check if the device is ready and set the initial value with pwm_set_dt()
.
Add the following code
if (!pwm_is_ready_dt(&pwm_led0)) {
LOG_ERR("Error: PWM device %s is not ready", pwm_led0.dev->name);
return 0;
}
err = pwm_set_dt(&pwm_led0, PWM_PERIOD, PWM_MIN_DUTY_CYCLE);
if (err) {
LOG_ERR("pwm_set_dt returned %d", err);
return 0;
}
C2.4 Change motor angle when a button is pressed.
Now we want to change the button_handler()
, to call set_motor_angle()
when button 1 or button 2 is pressed.
Add the following code snippet in button_handler()
case DK_BTN1_MSK:
LOG_INF("Button 1 pressed");
err = set_motor_angle(PWM_MIN_DUTY_CYCLE);
break;
case DK_BTN2_MSK:
LOG_INF("Button 2 pressed");
err = set_motor_angle(PWM_MAX_DUTY_CYCLE);
break;
C3. Testing the code.
3.1 Connect the wiring on the motor
Connect the motor’s ground to ground, the VCC to a voltage source on the DK, and the PWM wire to the GPIO pin you set in step 1.4.
Wire color | Servo | DK |
---|---|---|
Brown | Ground | Ground |
Red | VCC | Voltage source |
Yellow | PWM | GPIO pin |
This is how the wiring looks if you’ve used the GPIO on port 0, pin 3
For the nRF9160 DK, you must set SW9 on the board to 3V for the servo motor to work.
3.2 Build and flash the application to your board.
You should now be able to change the motor’s angle by pressing button 1 or button 2.
If you’ve followed this exercise without the motor and instead configured everything for the PWM LED from exercise 1, you should now be able to make the LED blink with two different frequencies. Note that the parameters chosen for the motor control may not work for an LED.
4. Configure pwm0
to drive LED 1.
Before we can add another PMW instance, we need to revert the PWM instance we’ve modified from driving a GPIO that controls an LED to a general GPIO, back to driving an LED.
4.1 Configure pwm0
to drive the GPIO pin for LED 1
We want to configure pwm0
to drive one of the LEDs on the board. We will change our previous overlay by changing NRF_PSEL
to the GPIO connected to LED1.
Replace the pin control for pwm0_custom
and pwm0_csleep
code with the following code snippet, changing the pin number back to the GPIO corresponding to LED1
&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 17)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 17)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 13)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 13)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 13)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 13)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 28)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 28)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm20_custom: pwm20_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 10)>;
nordic,invert;
};
};
pwm20_csleep: pwm20_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 10)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 6)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 6)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 2)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 2)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 0)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 0)>;
low-power-enable;
};
};
};
Devicetree4.2 Change the duty cycles for the LED
Now that the PWM will drive an LED we should change the duty cycles
Add the following
#define PWM_MIN_DUTY_CYCLE 20000000
#define PWM_MAX_DUTY_CYCLE 50000000
C4.3 Create the function set_led_blink()
to set the duty cycle of the PWM LED.
Create a function that takes in the period and duty cycle and uses pwm_set_dt()
to configure the PWM driving the LED.
Add the following
int set_led_blink(uint32_t period, uint32_t duty_cycle_ns){
int err;
err = pwm_set_dt(&pwm_led0, period, duty_cycle_ns);
if (err) {
LOG_ERR("pwm_set_dt_returned %d", err);
}
return err;
}
C4.4 Change the LED when a button is pressed.
Now we want to change button_handler()
, to call set_led_blink()
when buttons 3 or 4 are pressed.
Add the following code
case DK_BTN3_MSK:
LOG_INF("Button 3 pressed");
err = set_led_blink(2*PWM_PERIOD, PWM_MIN_DUTY_CYCLE);
break;
case DK_BTN4_MSK:
LOG_INF("Button 4 pressed");
err = set_led_blink(4*PWM_PERIOD, PWM_MAX_DUTY_CYCLE);
break;
CThis is doing the same as set_led_blink()
did initially. The integer we multiply PWM_PERIOD
with is selected as a semi-arbitrary value to showcase how the LED blinking frequency varies with different input.
The nRF7002 DK only has two buttons on the board, meaning the button handler can only have action on two buttons. In the solution, it controls pwm0
, i.e LED 2.
The nRF9160DK only has two buttons, but it has two slide switches that will act similarly to the buttons in the button handler callback. To make the application work, ensure the switches are in the GND position and then switch to the right and back to the left position to simulate a button press.
5. Add another PWM instance to drive the servo.
Now, we want to add another PWM instance. While doing this, we will also show you how to create a custom device that will act as a servo instead of modifying the existing node, which is, by default, enabled to control pwm0
.
5.1 Create a device binding for the servo, called pwm-servo
.
Since there is no preexisting device binding for the device we want to use, we will create our own generic device binding in the application directory called pwm-servo
.
This device binding will have the properties
pwms
: The PWM specifier driving the servo motor, in our case &pwm1
min-pulse
: The minimum pulse width in nanosecondsmax-pulse
: The maximum pulse width in nanoseconds.The pwms
property is of type phandle-array
, which is a property type used to specify a resource that is owned by another node.
In the folder dts/bindings
, open the file called pwm-servo.yaml
, and add the following code snippet
description: PWM-driven servo motor.
compatible: "pwm-servo"
include: base.yaml
properties:
pwms:
required: true
type: phandle-array
description: PWM specifier driving the servo motor.
min-pulse:
required: true
type: int
description: Minimum pulse width (nanoseconds).
max-pulse:
required: true
type: int
description: Maximum pulse width (nanoseconds).
5.2 Add the servo device to your overlay.
Now that we have a devicetree binding to describe it, let’s add the servo motor as a node in the devicetree. We want our servo motor to be driven by a signal generated from PWM instance pwm1
.
Add the following to the overlay file
/ {
servo: servo {
compatible = "pwm-servo";
pwms = <&pwm1 0 PWM_MSEC(20) PWM_POLARITY_NORMAL>;
min-pulse = <PWM_USEC(1000)>;
max-pulse = <PWM_USEC(2000)>;
};
};
DevicetreeIf you’re using the nRF54L15 DK add the following instead:
/ {
servo: servo {
compatible = "pwm-servo";
pwms = <&pwm21 0 PWM_MSEC(20) PWM_POLARITY_NORMAL>;
min-pulse = <PWM_USEC(1000)>;
max-pulse = <PWM_USEC(2000)>;
};
};
DevicetreeWe’ve now defined our device with the nodelabel “servo
”, it’s a compatible of “pwm-servo
” which is driven by the pwm1
instance, period of 20 ms and a normal polarity. In addition, we have defined it to have a minimum and maximum duty cycle of 1 ms and 2 ms, respectively
5.3 Configure which pins pwm1
should use
Similar to what we did previously, we need to use pin control to select what GPIO pins pwm1
should drive. First by defining the pin control states pwm1_custom_motor
and pwm1_csleep_motor
, then defining these states and which GPIO pins they are associated with.
Select your board and add the following in the overlay file
&pwm1 {
status = "okay";
pinctrl-0 = <&pwm1_custom_motor>;
pinctrl-1 = <&pwm1_csleep_motor>;
pinctrl-names = "default", "sleep";
};
&pinctrl {
pwm1_custom_motor: pwm1_custom_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
nordic,invert;
};
};
pwm1_csleep_motor: pwm1_csleep_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
low-power-enable;
};
};
};
Devicetree&pwm1 {
status = "okay";
pinctrl-0 = <&pwm1_custom_motor>;
pinctrl-1 = <&pwm1_csleep_motor>;
pinctrl-names = "default", "sleep";
};
&pinctrl {
pwm1_custom_motor: pwm1_custom_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
nordic,invert;
};
};
pwm1_csleep_motor: pwm1_csleep_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
low-power-enable;
};
};
};
Devicetree&pwm1 {
status = "okay";
pinctrl-0 = <&pwm1_custom_motor>;
pinctrl-1 = <&pwm1_csleep_motor>;
pinctrl-names = "default", "sleep";
};
&pinctrl {
pwm1_custom_motor: pwm1_custom_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
nordic,invert;
};
};
pwm1_csleep_motor: pwm1_csleep_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
low-power-enable;
};
};
};
Devicetree&pwm1 {
status = "okay";
pinctrl-0 = <&pwm1_custom_motor>;
pinctrl-1 = <&pwm1_csleep_motor>;
pinctrl-names = "default", "sleep";
};
&pinctrl {
pwm1_custom_motor: pwm1_custom_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 5)>;
nordic,invert;
};
};
pwm1_csleep_motor: pwm1_csleep_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 5)>;
low-power-enable;
};
};
};
Devicetree&pwm21 {
status = "okay";
pinctrl-0 = <&pwm21_custom_motor>;
pinctrl-1 = <&pwm21_csleep_motor>;
pinctrl-names = "default", "sleep";
};
&pinctrl {
pwm21_custom_motor: pwm21_custom_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 11)>;
nordic,invert;
};
};
pwm21_csleep_motor: pwm21_csleep_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 11)>;
low-power-enable;
};
};
};
Devicetree&pwm1 {
status = "okay";
pinctrl-0 = <&pwm1_custom_motor>;
pinctrl-1 = <&pwm1_csleep_motor>;
pinctrl-names = "default", "sleep";
};
&pinctrl {
pwm1_custom_motor: pwm1_custom_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 7)>;
nordic,invert;
};
};
pwm1_csleep_motor: pwm1_csleep_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 7)>;
low-power-enable;
};
};
};
Devicetree&pwm1 {
status = "okay";
pinctrl-0 = <&pwm1_custom_motor>;
pinctrl-1 = <&pwm1_csleep_motor>;
pinctrl-names = "default", "sleep";
};
&pinctrl {
pwm1_custom_motor: pwm1_custom_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 10)>;
nordic,invert;
};
};
pwm1_csleep_motor: pwm1_csleep_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 10)>;
low-power-enable;
};
};
};
Devicetree&pwm1 {
status = "okay";
pinctrl-0 = <&pwm1_custom_motor>;
pinctrl-1 = <&pwm1_csleep_motor>;
pinctrl-names = "default", "sleep";
};
&pinctrl {
pwm1_custom_motor: pwm1_custom_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 10)>;
nordic,invert;
};
};
pwm1_csleep_motor: pwm1_csleep_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 10)>;
low-power-enable;
};
};
};
Devicetree5.4 Retrieve the device structure for the servo motor
Initialize and populate struct
pwm_dt_spec
by using PWM_DT_SPEC_GET()
, and DT_NODELABEL()
with the node label servo
, that we defined in the overlay file.
Add the following
#define SERVO_MOTOR DT_NODELABEL(servo)
static const struct pwm_dt_spec pwm_servo = PWM_DT_SPEC_GET(SERVO_MOTOR);
C5.5 Define the minimum and maximum duty cycle from the defined device.
Use DT_PROP()
to obtain the minimum and maximum duty cycle from the defined device.
Add the following in the code
#define PWM_SERVO_MIN_DUTY_CYCLE DT_PROP(SERVO_MOTOR, min_pulse)
#define PWM_SERVO_MAX_DUTY_CYCLE DT_PROP(SERVO_MOTOR, max_pulse)
C5.6 Update the button handler with the new duty cycle
Replace the first two button handles with the following
case DK_BTN1_MSK:
LOG_INF("Button 1 pressed");
err = set_motor_angle(PWM_SERVO_MIN_DUTY_CYCLE);
break;
case DK_BTN2_MSK:
LOG_INF("Button 2 pressed");
err = set_motor_angle(PWM_SERVO_MAX_DUTY_CYCLE);
break;
C5.7 Check if the motor device is ready and set its initial value.
Add the following code
if (!pwm_is_ready_dt(&pwm_servo)) {
LOG_ERR("Error: PWM device %s is not ready", pwm_servo.dev->name);
return 0;
}
err = pwm_set_dt(&pwm_servo, PWM_PERIOD, PWM_SERVO_MIN_DUTY_CYCLE);
if (err) {
LOG_ERR("pwm_set_dt returned %d", err);
return 0;
}
C5.8 Change set_motor_angle()
to use the pwm_servo
device.
Replace the set_motor_angle()
function with the following code snippet
int set_motor_angle(uint32_t duty_cycle_ns)
{
int err;
err = pwm_set_dt(&pwm_servo, PWM_PERIOD, duty_cycle_ns);
if (err) {
LOG_ERR("pwm_set_dt_returned %d", err);
}
return err;
}
C6. Build and test your code
After programming your device, you should now observe that LED1 starts blinking, and if your motor is not already in the initial position, it should move there. When you press the various buttons, you should now observe that the frequency of the LED changes or that the motor changes position depending on which button.
What we’ve now created is a device that has two PWM instances that each drive its own GPIO.
The solution for this exercise can be found in the GitHub repository, l4/l4_e2_sol
.
For this exercise, we will use the Tower Pro Micro Servo SG90 servo motor, but you can also follow along without any additional hardware by using the LED’s to simulate a motor similar to what we did in exercise 1.
The nRF91x1 DKs and the nRF7002 DK only have 1.8 V output, which is not enough to control the servo motor used in this course. If you are using one of these boards, you can follow along by using one of the LEDs to simulate a motor or use a level shifter, which we will not cover in this exercise.
The first thing we will do is use the pwm_led
instance we used in the first exercise and create a custom variation of this that we will use for our PWM signal that will drive another GPIO other than LED 1. If you don’t have a motor available, follow along with the steps in this exercise, but don’t change the GPIO that this PWM instance will drive.
The data sheet for the servo motor we will use can be found here: http://www.ee.ic.ac.uk/pcheung/teaching/DE1_EE/stores/sg90_datasheet.pdf
The datasheet shows that the PWM period is 20ms (50Hz), and the duty cycle is between 1ms and 2ms. Recall that these are the same values we used in the previous exercise when we tested the PWM peripheral for controlling LED1, and we observed that these values only resulted in a dimly lit LED. For the specific motor used in this exercise the values must be within the parameters to ensure that the motor only moves within the range it is physically able to move within.
The servo can rotate approximately 90 degrees in each direction. 1ms pulse corresponds to -90 degrees, 1.5ms corresponds to 0 degrees and 2ms corresponds to 90 degrees rotation.
In Exercise 1, we used LED1 as our PWM LED, which is connected to P0.13 for the nRF52840 DK. We need to use one of the available GPIOs for our motor. To do this, we need to make some modifications to the device tree.
Open the code base of the exercise by navigating to Create a new application in the nRF Connect for VS Code extension, select Copy a sample, and search for Lesson 4 – Exercise 2. Make sure to use the version directory that matches the SDK version you are using.
Alternatively, in the GitHub repository for this course, go to the base code for this exercise, found in l4/l4_e2
of whichever version directory you are using.
You must build the application before beginning for some of the VS Code devicetree functionality to work properly.
1. Creating a custom PWM device
A useful resource when learning about devicetree is to inspect a board’s devicetree to see what nodes/devices are predefined on a board. For this exercise, we will use the devicetree for the nRF52840DK, nRF52840dk_nrf52840.dts
, which we will open and inspect in the following sections to illustrate the peripherals and devices we need to use.. If you’re using the VS Code extension directly, you may find the .dts file under “Config files-> Devicetree” in the details view of the extension or in the relative path <install_path>\ncs\zephyr\boards\arm
.
1.1 Create an overlay for your board
In the template of this exercise we’ve created a folder named boards
which contains an .overlay file. Open this directory, l4_e2/boards
, and rename the overlay file to the name of the build target for the board you are using, for example nrf52840dk_nrf52840.overlay
. If you’ve created your own project from scratch instead of using the template, create a new overlay file located in l4_e2/boards
and follow the same naming convention as just mentioned.
Open the .dts for your board, for instance nRF52840dk_nrf52840.dts
for the nRF52840DK, or locate the board file for your board as stated in the introduction for step 1. When opening the .dts in the extension, you should be able to locate compatibles such as the predefined LEDs and buttons as well as the predefined node pwmleds
.
The .dts contains the default definitions of what the DKs nodes/compatibles/devices should do and what GPIOs they are connected to. Every project in this version of the SDK that uses one of these boards will use this exact .dts file, so we will not make our modifications here. Instead, we will make the modifications in an overlay file that is specific to this project.
In the image above, you can see that the overlay file we have created will place itself in the devicetree folder located under the Config files item in the extension.
1.2 Add a pwm_led
instance and change the polarity.
We want to create a custom device that should drive a GPIO with a PWM signal. From the nrf52840dk_nrf52840.dts, L:48-53
, we know that we have a predefined pwm_led
instance, which we can modify to fit our needs.
We will copy the pwm_led
instance from the .dts and paste it into your project’s overlay file under step 1.2. Regardless of which board you’re using, you can copy this step as long as it has a PWM peripheral. You can verify this by reading the product specification for your specific board.
In addition to adding this node, we want to change the polarity to normal. This means that the PWM signal will have a high output for the duty cycle instead of a low.
Add the following code snippet to the overlay file
/{
pwmleds {
compatible = "pwm-leds";
pwm_led0: pwm_led_0 {
pwms = <&pwm0 0 PWM_MSEC(20) PWM_POLARITY_NORMAL>;
};
};
};
DevicetreeThis snippet means that we now have a node in our overlay that will overwrite what is defined in the .dts file. This node is a pwmleds
node that uses channel 0 on the pwm0
peripheral of the board, and has an alias named “pwm_led0”. For now,
this is not any different from what is already defined in the .dts file other than that the polarity has been changed, but we will further modify this node in the next two steps.
We will use the alias pwm_led0
to showcase that this alias is just a name and that the properties can easily be modified to drive other devices on another GPIO.
1.3 Add your own custom pwm0
instance.
In your overlay file, right-click on “&pwm0
” in the instance of pwmleds
in and click Go to Definition. This will take you to the definition in the devicetree file we had a look at earlier, and you will see something like this:
This will only work if you have tried to build the application before this step.
Notice that the pwm0
instance has two states, pwm0_default
and pwm0_sleep
. This is what is sent out on the GPIO connected to this pin control instance. We want to copy the &pwm0
instance into your overlay file, but create our own custom states by changing the names to pwm0_custom
and pwm0_csleep
.
Add the following code snippet in the overlay file
&pwm0 {
status = "okay";
pinctrl-0 = <&pwm0_custom>;
pinctrl-1 = <&pwm0_csleep>;
pinctrl-names = "default", "sleep";
};
Devicetree1.4 Configure which pins your custom pwm0
instance should use through pinctrl
Inspect pwm0_default
and pwm0_sleep
in your boards devicetree file by right-clicking on one of them and clicking Go to Definition again.
It should look something like this:
We will use the same format showcased in line 77-89 in the image above in our overlay file but change the names to pwm0_custom
and pwm0_csleep
, see the following code snippet as an example
&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 13)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 13)>;
low-power-enable;
};
};
};
DevicetreeUp until this step, we’ve created a custom pwm0
instance that does exactly the same as the pwm_led0
instance, only with different state names. For the nRF52840 DK, our new custom pwm0
is set to use P0.13 (port 0, pin 13) for its output. Since pin 13 is used by LED 1 on the nRF52840 DK, we need to change the output pin if we want to control anything other than LED1.
For instance, set <NRF_PSEL(PWM_OUT0, 0, 14)>;
to have the PWM output go to GPIO 14, i.e LED 2, instead of GPIO 13, i.e LED 1. If you don’t have a motor available, then this step is sufficient to showcase how to drive a GPIO with a PWM signal.
If you’re using a motor, we instead want to use a free available GPIO on the DK. This means that if you wish to use anything other than what we’ve described here, you need to consult with the product specification of the device you’re using and check if that GPIO is available for use. A good starting point is to inspect the product specification for your development kit to find a GPIO that is not used for anything else. For instance, in the User Guide for the nRF52840 DK, in section “8.6 Connector interface”, you can see that all the GPIOs in P6 and P24 are already in use for the items listed in the figure.
The solution in this exercise will use the following GPIOs for the 3V boards and the GPIO connected to LED2 for the case of the 1.8V boards (nRF7002 DK and nRF91x1 DK)
Board | GPIOs in the overlay solution |
nRF52 DK | P0.03 |
nRF52833 DK | P0.03 |
nRF52840 DK | P0.03 |
nRF5340 DK | P0.05 |
nRF7002 DK | P0.07 (LED2) |
nRF9160 DK | P0.10 |
nRF91x1 DK | P0.01 (LED2) |
Add the snippet corresponding to your board into your overlay file.
&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 5)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 5)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 7)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 7)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 10)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 10)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 1)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 1)>;
low-power-enable;
};
};
};
Devicetree2. Control the motor angle of the servo.
We have now set up our device to have a PWM output on pin 3, but we still need to decide how to set the pin. Let’s create the function set_motor_angle()
that sets the motor angle based on the input parameter duty_cycle_ns
.
This input parameter will be decided based on the motor’s datasheet. For the SG90 servo motor, you can see that here.
From the datasheet we can see that the duty cycle ranges from 1-2 ms while the PWM period is 20 ms (50Hz).
2.1 Define the function set_motor_angle()
to set the motor angle.
Define a function to set the motor angle. This function should take in the PWM duty cycle or an angle between 0 and 180 degrees as the input parameter and use pwm_set_dt()
to set it accordingly.
Add the following code snippet to the main.c file
int set_motor_angle(uint32_t duty_cycle_ns)
{
int err;
err = pwm_set_dt(&pwm_led0, PWM_PERIOD, duty_cycle_ns);
if (err) {
LOG_ERR("pwm_set_dt_returned %d", err);
}
return err;
}
C2.2 Define a maximum and minimum duty cycle
Inspect the motor’s datasheet and define the macros PWM_MIN_DUTY_CYCLE
and PWM_MAX_DUTY_CYCLE
based on this.
Note that you may need to experiment as what corresponds to the minimum and maximum angle may vary from motor to motor due to wear and tear.
We will set the following as our parameters:
#define PWM_MIN_DUTY_CYCLE 1000000
#define PWM_MAX_DUTY_CYCLE 2000000
C2.3 Check if the device is ready and set it to an initial value:
Use the API-specific pwm_is_ready_dt()
to check if the device is ready and set the initial value with pwm_set_dt()
.
Add the following code
if (!pwm_is_ready_dt(&pwm_led0)) {
LOG_ERR("Error: PWM device %s is not ready", pwm_led0.dev->name);
return 0;
}
err = pwm_set_dt(&pwm_led0, PWM_PERIOD, PWM_MIN_DUTY_CYCLE);
if (err) {
LOG_ERR("pwm_set_dt returned %d", err);
return 0;
}
C2.4 Change motor angle when a button is pressed.
Now we want to change the button_handler()
, to call set_motor_angle()
when button 1 or button 2 is pressed.
Add the following code snippet in button_handler()
case DK_BTN1_MSK:
LOG_INF("Button 1 pressed");
err = set_motor_angle(PWM_MIN_DUTY_CYCLE);
break;
case DK_BTN2_MSK:
LOG_INF("Button 2 pressed");
err = set_motor_angle(PWM_MAX_DUTY_CYCLE);
break;
C3. Testing the code.
3.1 Connect the wiring on the motor
Connect the motor’s ground to ground, the VCC to a voltage source on the DK, and the PWM wire to the GPIO pin you set in step 1.4.
Wire color | Servo | DK |
---|---|---|
Brown | Ground | Ground |
Red | VCC | Voltage source |
Yellow | PWM | GPIO pin |
This is how the wiring looks if you’ve used the GPIO on port 0, pin 3
For the nRF9160 DK, you must set SW9 on the board to 3V for the servo motor to work.
3.2 Build and flash the application to your board.
You should now be able to change the motor’s angle by pressing button 1 or button 2.
If you’ve followed this exercise without the motor and instead configured everything for the PWM LED from exercise 1, you should now be able to make the LED blink with two different frequencies. Note that the parameters chosen for the motor control may not work for an LED.
4. Configure pwm0
to drive LED 1.
Before we can add another PMW instance, we need to revert the PWM instance we’ve modified from driving a GPIO that controls an LED to a general GPIO, back to driving an LED.
4.1 Configure pwm0
to drive the GPIO pin for LED 1
We want to configure pwm0
to drive one of the LEDs on the board. We will change our previous overlay by changing NRF_PSEL
to the GPIO connected to LED1.
Replace the pin control for pwm0_custom
and pwm0_csleep
code with the following code snippet, changing the pin number back to the GPIO corresponding to LED1
&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 17)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 17)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 13)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 13)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 13)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 13)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 28)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 28)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 6)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 6)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 2)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 2)>;
low-power-enable;
};
};
};
Devicetree&pinctrl {
pwm0_custom: pwm0_custom {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 0)>;
nordic,invert;
};
};
pwm0_csleep: pwm0_csleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 0)>;
low-power-enable;
};
};
};
Devicetree4.2 Change the duty cycles for the LED
Now that the PWM will drive an LED we should change the duty cycles
Add the following
#define PWM_MIN_DUTY_CYCLE 20000000
#define PWM_MAX_DUTY_CYCLE 50000000
C4.3 Create the function set_led_blink()
to set the duty cycle of the PWM LED.
Create a function that takes in the period and duty cycle and uses pwm_set_dt()
to configure the PWM driving the LED.
Add the following
int set_led_blink(uint32_t period, uint32_t duty_cycle_ns){
int err;
err = pwm_set_dt(&pwm_led0, period, duty_cycle_ns);
if (err) {
LOG_ERR("pwm_set_dt_returned %d", err);
}
return err;
}
C4.4 Change the LED when a button is pressed.
Now we want to change button_handler()
, to call set_led_blink()
when buttons 3 or 4 are pressed.
Add the following code
case DK_BTN3_MSK:
LOG_INF("Button 3 pressed");
err = set_led_blink(2*PWM_PERIOD, PWM_MIN_DUTY_CYCLE);
break;
case DK_BTN4_MSK:
LOG_INF("Button 4 pressed");
err = set_led_blink(4*PWM_PERIOD, PWM_MAX_DUTY_CYCLE);
break;
CThis is doing the same as set_led_blink()
did initially. The integer we multiply PWM_PERIOD
with is selected as a semi-arbitrary value to showcase how the LED blinking frequency varies with different input.
The nRF7002 DK only has two buttons on the board, meaning the button handler can only have action on two buttons. In the solution, it controls pwm0
, i.e LED 2.
The nRF9160DK only has two buttons, but it has two slide switches that will act similarly to the buttons in the button handler callback. To make the application work, ensure the switches are in the GND position and then switch to the right and back to the left position to simulate a button press.
5. Add another PWM instance to drive the servo.
Now, we want to add another PWM instance. While doing this, we will also show you how to create a custom device that will act as a servo instead of modifying the existing node, which is, by default, enabled to control pwm0
.
5.1 Create a device binding for the servo, called pwm-servo
.
Since there is no preexisting device binding for the device we want to use, we will create our own generic device binding in the application directory called pwm-servo
.
This device binding will have the properties
pwms
: The PWM specifier driving the servo motor, in our case &pwm1
min-pulse
: The minimum pulse width in nanosecondsmax-pulse
: The maximum pulse width in nanoseconds.The pwms
property is of type phandle-array
, which is a property type used to specify a resource that is owned by another node.
In the folder dts/bindings
, open the file called pwm-servo.yaml
, and add the following code snippet
description: PWM-driven servo motor.
compatible: "pwm-servo"
include: base.yaml
properties:
pwms:
required: true
type: phandle-array
description: PWM specifier driving the servo motor.
min-pulse:
required: true
type: int
description: Minimum pulse width (nanoseconds).
max-pulse:
required: true
type: int
description: Maximum pulse width (nanoseconds).
5.2 Add the servo device to your overlay.
Now that we have a devicetree binding to describe it, let’s add the servo motor as a node in the devicetree. We want our servo motor to be driven by a signal generated from PWM instance pwm1
.
Add the following to the overlay file
/ {
servo: servo {
compatible = "pwm-servo";
pwms = <&pwm1 0 PWM_MSEC(20) PWM_POLARITY_NORMAL>;
min-pulse = <PWM_USEC(1000)>;
max-pulse = <PWM_USEC(2000)>;
};
};
DevicetreeWe’ve now defined our device with the nodelabel “servo
”, it’s a compatible of “pwm-servo
” which is driven by the pwm1
instance, period of 20 ms and a normal polarity. In addition, we have defined it to have a minimum and maximum duty cycle of 1 ms and 2 ms, respectively
5.3 Configure which pins pwm1
should use
Similar to what we did previously, we need to use pin control to select what GPIO pins pwm1
should drive. First by defining the pin control states pwm1_custom_motor
and pwm1_csleep_motor
, then defining these states and which GPIO pins they are associated with.
Select your board and add the following in the overlay file
&pwm1 {
status = "okay";
pinctrl-0 = <&pwm1_custom_motor>;
pinctrl-1 = <&pwm1_csleep_motor>;
pinctrl-names = "default", "sleep";
};
&pinctrl {
pwm1_custom_motor: pwm1_custom_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
nordic,invert;
};
};
pwm1_csleep_motor: pwm1_csleep_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
low-power-enable;
};
};
};
Devicetree&pwm1 {
status = "okay";
pinctrl-0 = <&pwm1_custom_motor>;
pinctrl-1 = <&pwm1_csleep_motor>;
pinctrl-names = "default", "sleep";
};
&pinctrl {
pwm1_custom_motor: pwm1_custom_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
nordic,invert;
};
};
pwm1_csleep_motor: pwm1_csleep_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
low-power-enable;
};
};
};
Devicetree&pwm1 {
status = "okay";
pinctrl-0 = <&pwm1_custom_motor>;
pinctrl-1 = <&pwm1_csleep_motor>;
pinctrl-names = "default", "sleep";
};
&pinctrl {
pwm1_custom_motor: pwm1_custom_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
nordic,invert;
};
};
pwm1_csleep_motor: pwm1_csleep_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 3)>;
low-power-enable;
};
};
};
Devicetree&pwm1 {
status = "okay";
pinctrl-0 = <&pwm1_custom_motor>;
pinctrl-1 = <&pwm1_csleep_motor>;
pinctrl-names = "default", "sleep";
};
&pinctrl {
pwm1_custom_motor: pwm1_custom_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 5)>;
nordic,invert;
};
};
pwm1_csleep_motor: pwm1_csleep_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 5)>;
low-power-enable;
};
};
};
Devicetree&pwm1 {
status = "okay";
pinctrl-0 = <&pwm1_custom_motor>;
pinctrl-1 = <&pwm1_csleep_motor>;
pinctrl-names = "default", "sleep";
};
&pinctrl {
pwm1_custom_motor: pwm1_custom_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 7)>;
nordic,invert;
};
};
pwm1_csleep_motor: pwm1_csleep_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 7)>;
low-power-enable;
};
};
};
Devicetree&pwm1 {
status = "okay";
pinctrl-0 = <&pwm1_custom_motor>;
pinctrl-1 = <&pwm1_csleep_motor>;
pinctrl-names = "default", "sleep";
};
&pinctrl {
pwm1_custom_motor: pwm1_custom_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 10)>;
nordic,invert;
};
};
pwm1_csleep_motor: pwm1_csleep_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 10)>;
low-power-enable;
};
};
};
Devicetree&pwm1 {
status = "okay";
pinctrl-0 = <&pwm1_custom_motor>;
pinctrl-1 = <&pwm1_csleep_motor>;
pinctrl-names = "default", "sleep";
};
&pinctrl {
pwm1_custom_motor: pwm1_custom_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 10)>;
nordic,invert;
};
};
pwm1_csleep_motor: pwm1_csleep_motor {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 10)>;
low-power-enable;
};
};
};
Devicetree5.4 Retrieve the device structure for the servo motor
Initialize and populate struct
pwm_dt_spec
by using PWM_DT_SPEC_GET()
, and DT_NODELABEL()
with the node label servo
, that we defined in the overlay file.
Add the following
#define SERVO_MOTOR DT_NODELABEL(servo)
static const struct pwm_dt_spec pwm_servo = PWM_DT_SPEC_GET(SERVO_MOTOR);
C5.5 Define the minimum and maximum duty cycle from the defined device.
Use DT_PROP()
to obtain the minimum and maximum duty cycle from the defined device.
Add the following in the code
#define PWM_SERVO_MIN_DUTY_CYCLE DT_PROP(SERVO_MOTOR, min_pulse)
#define PWM_SERVO_MAX_DUTY_CYCLE DT_PROP(SERVO_MOTOR, max_pulse)
C5.6 Update the button handler with the new duty cycle
Replace the first two button handles with the following
case DK_BTN1_MSK:
LOG_INF("Button 1 pressed");
err = set_motor_angle(PWM_SERVO_MIN_DUTY_CYCLE);
break;
case DK_BTN2_MSK:
LOG_INF("Button 2 pressed");
err = set_motor_angle(PWM_SERVO_MAX_DUTY_CYCLE);
break;
C5.7 Check if the motor device is ready and set its initial value.
Add the following code
if (!pwm_is_ready_dt(&pwm_servo)) {
LOG_ERR("Error: PWM device %s is not ready", pwm_servo.dev->name);
return 0;
}
err = pwm_set_dt(&pwm_servo, PWM_PERIOD, PWM_SERVO_MIN_DUTY_CYCLE);
if (err) {
LOG_ERR("pwm_set_dt returned %d", err);
return 0;
}
C5.8 Change set_motor_angle()
to use the pwm_servo
device.
Replace the set_motor_angle()
function with the following code snippet
int set_motor_angle(uint32_t duty_cycle_ns)
{
int err;
err = pwm_set_dt(&pwm_servo, PWM_PERIOD, duty_cycle_ns);
if (err) {
LOG_ERR("pwm_set_dt_returned %d", err);
}
return err;
}
C6. Build and test your code
After programming your device, you should now observe that LED1 starts blinking, and if your motor is not already in the initial position, it should move there. When you press the various buttons, you should now observe that the frequency of the LED changes or that the motor changes position depending on which button.
What we’ve now created is a device that has two PWM instances that each drive its own GPIO.
The solution for this exercise can be found in the GitHub repository, l4/l4_e2_sol
.