Lab 5: Pulse-width modulation
1. A new way to blink an LED
A pulse-width modulated signal is one way to make an LED blink. The dedicated pulse-width hardware will take over the task, letting the microcontroller do other things once the initial configuration is done.
-
Create a new application.
-
Edit
prj.conf
to enable both logging and pulse-width modulation:CONFIG_LOG=y CONFIG_PWM=y
-
Generate a build configuration and create an overlay.
-
Edit the overlay file. This requires more work than we have seen previously in the overlay so the additions you need to make will be described in several steps. The first step is to create the node identifier related to the external LED.
/{ pwmleds { (1) compatible = "pwm-leds"; (2) pwm_ext_led: pwm_ext_led { (3) pwms = <&pwm0 0 PWM_MSEC(1) PWM_POLARITY_NORMAL>; (4) }; }; };
1 First, we create an new entry in a section that already exists called pwmleds. 2 A hardware category known as pwm-leds
already exists so we can use that.3 The PWM-controlled LED is given the node identifier (and node label) pwm_ext_led
.4 There are four separate PWM controllers on the nRF52840 and each controller supports up to four channels. This adds a channel to the first PWM controller ( pwm0
) for an external LED. The pin correspond to this channel will be set later. A default period of 1 ms is set and the channel has normal polarity (the pulse width specifies the time at a high voltage). -
Continuing to edit the overlay, create custom settings for the PWM controller so it knows what to do during normal power mode and during power-saving sleep mode.
&pwm0 { status = "okay"; (1) pinctrl-0 = <&pwm0_my_default>; pinctrl-1 = <&pwm0_my_sleep>; pinctrl-names = "default", "sleep"; (2) }; &pinctrl { pwm0_my_default: pwm0_my_default { (3) group1 { psels = <NRF_PSEL(PWM_OUT0, 0, 3)>; (4) }; }; pwm0_my_sleep: pwm0_my_sleep { group1 { psels = <NRF_PSEL(PWM_OUT0, 0, 3)>; low-power-enable; (5) }; }; };
1 A status of okay
turns on a peripheral. The off status is calleddisabled
.2 The standard names for the two operational modes are default
andsleep
. We stick with those for compatibility with other systems.3 The name of our custom default setting is pwm0_my_default
.4 A link is made between channel 0 (specified by PWM_OUT0
) and P0.03. If you wanted to define additional channels (up to four are allowed per PWM controller), they would follow this.5 Modifies the behavior for low power mode. -
Enter Program 1 into
main.c
.Program 1. Blink an external LED using pulse-width modulation.#include <zephyr/kernel.h> #include <zephyr/logging/log.h> #include <zephyr/drivers/pwm.h> LOG_MODULE_REGISTER(Lab05_Exercise1, LOG_LEVEL_INF); #define PWM_LED_NI DT_NODELABEL(pwm_ext_led) const struct pwm_dt_spec pwm_led = PWM_DT_SPEC_GET(PWM_LED_NI); (1) int main(void) { int err; if (!pwm_is_ready_dt(&pwm_led)) { (2) LOG_ERR("Error: PWM device is not ready"); return -1; } pwm_set_dt(&pwm_led, PWM_MSEC(250), PWM_MSEC(125)); (3) }
1 The process of getting the information for a PWM output is similar to that for a GPIO. 2 PWM also has its own ready-reporting function. 3 The period and the pulse width are set. The convenience macro PWM_MSEC
converts a time in milliseconds to one in nanoseconds (the unit of time that PWM functions use). -
Connect GND on the development board to the ground bus on a breadboard. Connect the short leg of a red LED to the ground bus and the long leg to a socket in a terminal strip (the main section of the breadboard). Connect a 330 Ω resistor in series with the LED. The other end of the resistor should be connected to P0.03 on the development board.
-
Build your application and flash it to the development board.
-
You should observe a rapidly flashing LED. Unfortunately, the PWM controllers on the nRF52840 have a maximum period of about 260 ms so this method cannot be used to create an LED that has a slower blink rate.
2. Observing PWM on the oscilloscope
Your goal is to observe the output of your pulse-width-modulated output on the oscilloscope and compare to what is expected from the code.
-
Begin by connecting 1+ to junction between P0.03 and the resistor on the breadboard, 1- to ground, and
also to ground.
-
Set the Time base to 50 ms/div and the Trigger level to 2 V. Choose appropriate Channel 1 settings so the PWM trace fills most of the display.
-
Open the Measurements tab and add the horizontal measurements Period, PosDuty, and PosWidth.
-
Acquire a single acquisition and compare the measured period, positive pulse width, and duty cycle to that expected from the code.
-
Change the code to produce a positive pulse width of 25 ms (a duty cycle of 10% when the period is 250 ms) and verify with the oscilloscope.
-
Change the code to produce a positive pulse width of 225 ms (a duty cycle of 90% when the period is 250 ms) and verify with the oscilloscope.
3. Averaging produces output with more than two states
If the period is decreased below the ability of your eye to see individual flashes you instead perceive the average brightness. In other words, a low duty cycle will look dim and a high duty cycle will look bright. You will use the same program to vary the brightness of an external LED. In this case your eye is doing the averaging.
-
Modify Program 1 by:
-
adding the following
define
statements before themain
function:#define PWM_PERIOD_MS 1 #define PWM_PERIOD_NS PWM_PERIOD_MS*1000000
-
replace the code after the
if
statement checking that the PWM controller is ready with the following:err = pwm_set_dt(&pwm_led, PWM_PERIOD_NS, PWM_PERIOD_NS); if (err) { LOG_ERR("Error %d in pwm_set_dt()", err); return -1; } LOG_INF("PWM period is %d ms",PWM_PERIOD_MS); k_msleep(1000); while (true) { for (int i = 0; i <= 10; ++i) { err = pwm_set_dt(&pwm_led, PWM_PERIOD_NS, i*PWM_PERIOD_NS/10); if (err) { LOG_ERR("Error %d in pwm_set_pulse_dt()", err); return -1; } else { LOG_INF("Duty cycle = %d%%",i*10); } k_msleep(2000); } } }
-
-
Build and flash the application. If you are a typical human being you should not be able to detect that the LED is actually turning off and on very rapidly.
-
Open a terminal connection (from Connected Devices) and observe the logger output.
-
Repeat with periods of 10 ms, 20 ms, 50 ms, 100 ms, and 200 ms. When do you first notice the flicker?
When you have finished your observations, discuss the results with the instructor. |
You will use the same program to produce a voltage between 0 and and about 3 V using a resistor and a capacitor. The results will be observed with the oscilloscope.
-
Remove the LED and 330 Ω resistor from the breadboard.
-
Form an RC low-pass filter by connecting the short leg of a 10 µF capacitor to the ground bus. This is a polarized capacitor and will be damaged if you use it backwards. Connect the long leg of the capacitor to a socket in a terminals strip.
-
Connect a 10 kΩ resistor (brown-black-orange) to the same terminal strip as the capacitor. The other leg of the resistor should be connected to P0.03 (via a jumper wire).
-
Set the period in the program to 10 ms.
-
Use the oscilloscope to observe the output of the low-pass filter (1+ to the junction between the resistor and capacitor, 1- to ground, and
to ground). Use a time base of 2 s/div, a time position of 10 s, a channel 1 offset of -2 V, and a range of 500 mV/div.
-
Now observe what happens when you change the resistance. Replace the 10 kΩ resistor with a 1 kΩ resistor (brown-black-red). What differences do you observe on the oscilloscope?
-
Replace the resistor with a 330 Ω one. What differences do you observe on the oscilloscope?
When you have finished your observations, discuss the results with the instructor. |
4. Controlling a servo
The position of a servo is controlled using pulse-width modulation. It expects a period of 20 ms and then the positive pulse width determines the position. For the Hitec HS-422 servo a pulse width of 1500 µs sends it 0°. Changing that pulse width by 10 µs changes the angle by 1°. This means -90° is produced with a pulse width of 600 µs and +90° with 2400 µs. This servo should not be driven outside of those ranges.
-
Create a new application.
-
Create a new folder named
dts
at the top-level of your application (not inside any folder other than the one holding application itself). Inside of thedts
folder create another folderbindings
. A binding is the name used in Zephyr for a file that provides a high-level description of a type of hardware. Zephyr looks for these in this particular folder. -
Create a file named
pwm-servo.yaml
inside of thebindings
folder. Add the following to that file:description: PWM-driven servo compatible: "pwm-servo" (1) include: base.yaml (2) properties: pwms: (3) required: true type: phandle-array description: PWM specifier driving the servo min-pulse: (4) required: true type: int description: Minimum pulse width (nanoseconds) max-pulse: (5) required: true type: int description: Maximum pulse width (nanoseconds) deg-to-pw: (6) required: true type: int description: Conversion factor from degrees to pulse width (nanoseconds) center-pw: (7) required: true type: int description: Pulse width for center position (nanoseconds)
1 This is the name we will use in the devicetree overlay to indicate that this binding should be used. 2 Bindings can be layered on top of others. In this case this one is built on the base
binding which provides properties expected for all bindings.3 The servo requires a PWM specifier, the same as the pwm-leds
used earlier.4 A new property to hold the minimum pulse width is added. Because servos may be damaged if driven outside of their operating range it is a required property. 5 The maximum pulse width is also required. 6 The conversion factor from degrees to pulse width should be specified in the servo description. 7 The pulse width for the center position is the final property needed to describe operation of the servo. -
Edit
prj.conf
to enable both pulse-width modulation and logging. We will also set the logging level for the PWM module so that only errors will be displayed.CONFIG_PWM=y CONFIG_PWM_LOG_LEVEL_ERR=y CONFIG_LOG=y
-
Generate a build configuration and create an overlay.
-
Edit the overlay file. We are going to add the servo using the binding just created.
/{ servo: hs422_servo { compatible = "pwm-servo"; (1) pwms = <&pwm0 0 PWM_MSEC(20) PWM_POLARITY_NORMAL>; (2) min-pulse = <PWM_USEC(600)>; (3) max-pulse = <PWM_USEC(2400)>; center-pw = <PWM_USEC(1500)>; deg-to-pw = <PWM_USEC(10)>; }; };
1 The compatible
property indicates the binding that should be used.2 The default period for a servo is set. 3 A property holding the minimum allowed pulse width is created and set to 600 µs using one of the convenience macros (which actually converts this to a value in nanoseconds). -
Continuing to edit the overlay, create custom settings for the PWM controller so it knows what to do during normal power mode and during power-saving sleep mode. These also define which pin is controlled by the PWM controller.
&pwm0 { status = "okay"; pinctrl-0 = <&pwm0_my_default>; pinctrl-1 = <&pwm0_my_sleep>; pinctrl-names = "default", "sleep"; }; &pinctrl { pwm0_my_default: pwm0_my_default { group1 { psels = <NRF_PSEL(PWM_OUT0, 0, 3)>; }; }; pwm0_my_sleep: pwm0_my_sleep { group1 { psels = <NRF_PSEL(PWM_OUT0, 0, 3)>; low-power-enable; (5) }; }; };
-
You are now ready for the actual application code in
main.c
.Program 2. Send a servo to a series of pre-defined angles.#include <zephyr/kernel.h> #include <zephyr/logging/log.h> #include <zephyr/drivers/pwm.h> LOG_MODULE_REGISTER(Lab05_Servo, LOG_LEVEL_INF); #define SERVO DT_NODELABEL(servo) const struct pwm_dt_spec servo = PWM_DT_SPEC_GET(SERVO); /* Use DT_PROP() to get servo properties from overlay */ #define SERVO_MIN_PULSE_WIDTH DT_PROP(SERVO, min_pulse) (1) #define SERVO_MAX_PULSE_WIDTH DT_PROP(SERVO, max_pulse) #define SERVO_CENTER DT_PROP(SERVO, center_pw) #define SERVO_DEG_CONV DT_PROP(SERVO, deg_to_pw) /** (2) * @brief Convert angle to PWM pulse width * * @param angle Servo angle (integer degrees) * @return int representing the pulse width (in nanoseconds) */ int angle_to_pulsewidth(int angle) { (3) int pw = SERVO_CENTER + angle*SERVO_DEG_CONV; if (pw < SERVO_MIN_PULSE_WIDTH) { (4) pw = SERVO_MIN_PULSE_WIDTH; LOG_WRN("Out of servo range. Attempted to set to %d", angle); } if (pw > SERVO_MAX_PULSE_WIDTH) { pw = SERVO_MAX_PULSE_WIDTH; LOG_WRN("Out of servo range. Attempted to set to %d", angle); } return pw; } int main(void) { int angles[] = {0, +30, -30, +60, -60, +90, -90}; (5) int num_angles = 7; if (!pwm_is_ready_dt(&servo)) { LOG_ERR("PWM controller is not ready"); return -1; } while (true) { for (int i = 0; i < num_angles; i++) { LOG_INF("Angle set to %d deg", angles[i]); (6) pwm_set_pulse_dt(&servo, angle_to_pulsewidth(angles[i])); k_msleep(2000); } } }
1 Access the servo-specific properties from the hardware overlay. Notice that the -
found in the devicetree property name becomes_
when referring to devicetree property names in the C code. Notice that the C code is now independent of the specific servo used. If you wanted to use a different servo you would only need to change the overlay file.2 It is good practice to provide a comment block describing the inputs and output of a function as well as what it does. This one is formatted in the Doxygen style. 3 A function is defined that takes one input argument (an integer representing an angle in degrees) and the output is also an integer (the number of nanoseconds the pulse width should be for the servo to go to that angle). 4 The minimum and maximum pulse widths are not automatically enforced. The code you write needs to do that. In this case, if the pulse width is too small, the value is set to the minimum and an error message is sent to the logger module. 5 Seven angles are stored in an array of integers. 6 The angles stored in the array are accessed by their index. C starts counting from 0 so the first angle is accessed through angles[0]
and the seventh angle throughangles[6]
. -
The Hitec HS-422 servo requires more power than can supplied directly by the nRF52840 DK development board. An external battery pack is required, but it must be used carefully to avoid damaging your development board.
An alternative approach is to use a micro servo such as the TowerPro SG92R. This less powerful servo can be driven directly from voltages supplied by the development board. -
Connect the ground bus of a breadboard to a GND socket on the development board.
-
Connect the black lead from a 6 V battery pack to the same ground bus.
-
Connect the black lead of the servo to the ground bus. It is important that all parts of the system agree on the ground voltage.
The power bus on the breadboard should have no connections other than those to be described. Connecting the 6 V of the battery pack (directly or indirectly) to the development board will damage it. -
Connect the red lead of the battery pack to the power bus.
-
Connect the red lead of the servo to the power bus.
-
Connect the yellow lead of the servo to P0.03.
-
-
Build your application and flash it to the development board.
-
Start a terminal connection so you can observe logging messages.
-
You should observe the servo rotating through a sequence of angles.
5. Your Turn
5.1. Servo controller
In this exercise you will move the servo to a location selected by button presses.
-
BUTTON 1: decreases the current angle by 10°
-
BUTTON 2: increases the current angle by 10°
-
BUTTON 3: sets the angle to 0°
-
BUTTON 4: sets the angle to 90° if the current angle is positive or -90° if the current angle is negative
-
Access the GitHub Classroom link for this assignment on Blackboard.
-
Follow the usual steps for getting started with a repository from GitHub Classroom.
-
Write your code to control the servo. A button should only perform the specified behavior when it has been pressed and then released.
-
Test your program.
-
Update the
README.md
.
-
When your program and circuit are working successfully, remember to push the commits to the remote repository. Also, take a video of its successful operation (along with your reflection) and upload this to Blackboard. |
5.2. Dimmer control
In this assignment you will use three buttons to control the brightness of an external LED. The LED should have brightness levels ranging from 0% (off) to 100% (maximum brightness), adjustable in increments of 10%.
-
BUTTON 1 increases the brightness (duty cycle) by 10%.
-
BUTTON 2 decreases the brightness (duty cycle) by 10%.
-
BUTTON 3 toggles the light off or on. The brightness setting should be remembered when it is toggled off so that when toggled on again the previous brightness setting is restored.
Each button press and release should trigger the corresponding action just once.
-
Create the repository using the GitHub Classroom link on Blackboard.
-
Update
main.c
andREADME.md
. -
Test your program.
When your program and circuit are working successfully, remember to push the commits to the remote repository. Also, take a video of its successful operation (along with your reflection) and upload this to Blackboard. |