Feedback
Feedback

If you are having issues with the exercises, please create a ticket on DevZone: devzone.nordicsemi.com
Click or drag files to this area to upload. You can upload up to 2 files.

Exercise 2 – Using PWM to control a servo motor

v2.8.x – v2.7.0

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.

Important

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.

Important

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.

Exercise steps

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.

Note

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.

nRF52840 DK default devicetree

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

This 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:

Note

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

Note

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

Up 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)

BoardGPIOs in the overlay solution
nRF52 DKP0.03
nRF52833 DKP0.03
nRF52840 DKP0.03
nRF5340 DKP0.05
nRF7002 DKP0.07 (LED2)
nRF9160 DKP0.10
nRF91x1 DKP0.01 (LED2)
nRF54L15 DKP1.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;
        };
    };
};
Devicetree

2. 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;
}
C

2.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
C

2.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;
}
C

2.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;
C

3. 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 ServoDK
BrownGroundGround
RedVCCVoltage source
YellowPWMGPIO pin

This is how the wiring looks if you’ve used the GPIO on port 0, pin 3

Connecting to nRF52840 DK

Important

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

4.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
C

4.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;
}
C

4.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;
C

This 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.

Note

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 nanoseconds
  • max-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)>;
    };
};
Devicetree

If 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)>;
    };
};
Devicetree

We’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;
        };
    };
};
Devicetree

5.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);
C

5.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)
C

5.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;
C

5.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;
}
C

5.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;
}
C

6. 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.

v2.6.2 -v2.5.2

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.

Important

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.

Exercise steps

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.

Note

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.

nRF52840 DK default devicetree

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

This 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:

Note

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

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

Up 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)

BoardGPIOs in the overlay solution
nRF52 DKP0.03
nRF52833 DKP0.03
nRF52840 DKP0.03
nRF5340 DKP0.05
nRF7002 DKP0.07 (LED2)
nRF9160 DKP0.10
nRF91x1 DKP0.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;
        };
    };
};
Devicetree

2. 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;
}
C

2.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
C

2.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;
}
C

2.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;
C

3. 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 ServoDK
BrownGroundGround
RedVCCVoltage source
YellowPWMGPIO pin

This is how the wiring looks if you’ve used the GPIO on port 0, pin 3

Connecting to nRF52840 DK

Important

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

4.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
C

4.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;
}
C

4.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;
C

This 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.

Note

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 nanoseconds
  • max-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)>;
    };
};
Devicetree

We’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;
        };
    };
};
Devicetree

5.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);
C

5.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)
C

5.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;
C

5.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;
}
C

5.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;
}
C

6. 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.

Register an account
Already have an account? Log in
(All fields are required unless specified optional)

  • 8 or more characters
  • Upper and lower case letters
  • At least one number or special character

Forgot your password?
Enter the email associated with your account, and we will send you a link to reset your password.