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

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 nRF9161 DK and 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.

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 focus on the devicetree for the nRF52840DK, nRF52840dk_nrf52840.dts, which we will open and inspect in the following sections. If you’re using the VS Code extension directly, you may find the .dts file under “Input files” in 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, inter_less4_exer2/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 inter_less4_exer2/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 see 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

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

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

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 nRF9161 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
nRF9161 DKP0.01 (LED2)

Add the snippet corresponding to your board into your overlay file.

nRF52 DK
nRF52833 DK
nRF52840 DK
nRF5340 DK
nRF7002 DK
nRF9160 DK
nRF9161 DK

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: http://www.ee.ic.ac.uk/pcheung/teaching/DE1_EE/stores/sg90_datasheet.pdf

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

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:

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

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

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

nRF52 DK
nRF52833 DK
nRF52840 DK
nRF5340 DK
nRF7002 DK
nRF9160 DK
nRF9161 DK

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

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

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

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

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

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

nRF52 DK
nRF52833 DK
nRF52840 DK
nRF5340 DK
nRF7002 DK
nRF9160 DK
nRF9161 DK

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

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

5.6 Update the button handler with the new duty cycle

Replace the first two button handles with the following

5.7 Check if the motor device is ready and set its initial value.

Add the following code

5.8 Change set_motor_angle() to use the pwm_servo device.

Replace the set_motor_angle() function with the following code snippet

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, lesson4/inter_less4_exer2_solution.

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.