Pulse-Width Modulation

The examples in this guide are no longer supported and may not work. We are only supporting CircuitPython on our boards. For more information about using PWM with CircuitPython, check out the CircuitPython Essentials: https://learn.adafruit.com/circuitpython-essentials/circuitpython-pwm

Pulse-width modulation (PWM) is a way to use a digital output (i.e. one that is only on or off) to simulate an analog or varying voltage output.  PWM works by turning a digital output on and off very quickly.  So quickly that the component connected to it can't tell the output signal is changing, instead it just sees the 'average' of how long the signal is on vs. off.  If the output is turned on for slightly longer then it's turned off it will appear to be a higher value.  Controlling how long the signal is on vs. off, or the duty cycle, allows you to smoothly control the output values.

One great example of using PWM is to dim a LED between full and no brightness.  If a PWM signal is fast enough (changing more than about 60 times a second) the human eye won't be able to see the LED turning on and off quickly.  Instead the eye will see a brighter light the longer the PWM signal is turned on vs. off.  Using PWM you can control the brightness of a LED so that it pulses and glows in interesting ways instead of just blinking fully on and off.

Note that PWM control in MicroPython is somewhat in flux and varies greatly depending on the board.  This example will only look at MicroPython on the ESP8266 as it has a simple PWM interface, however be sure to consult the documentation for other boards to learn how to use PWM:

Control LED Brightness

For this example you'll learn how to dim a LED using MicroPython on the ESP8266.  To follow this example you'll need the following parts (all available in the Adafruit Parts Pal):

  • MicroPython ESP8266 board, like the Adafruit Feather HUZZAH ESP8266.
  • 1x LED, like one of these 5mm diffused red LEDs.
  • 1x ~560-3k ohm resistor.  You need to use a resistor to limit the current that can flow through the LED (otherwise you might send too much current through the LED and damage it or the MicroPython board!).  The exact value of the resistor usually doesn't matter here, anything in the 560-3,000 ohm range should work great (higher resistance will be lower current and usually dimmer LED).  If unsure grab a 2200 ohm resistor pack.
  • Breadboard & jumper wires.

Wire up the components exactly the same as from the blink a LED guide:

  • Digital GPIO 15 is connected to the anode, or longer leg, of the LED.  It's very important to use the correct leg of the LED otherwise it won't light up as expected!  Note don't use GPIO 16 as it doesn't support PWM output on the ESP8266.
  • The cathode, or shorter leg, of the LED is connected to one side of the resistor (unlike the LED it doesn't matter which way you orient the resistor).
  • The other side of the resistor is connected to the board's ground or GND pin.

Now connect to a serial or other REPL on your board and run the following code to import the machine module and create a PWM controlled pin:

Download: file
import machine
pwm = machine.PWM(machine.Pin(15))

The second line will create an instance of the PWM class from the machine module and assign it to a variable called pwm.  Notice that the initializer takes in a machine Pin class instance, in this case one created for pin 15.

Next you can set the frequency of the PWM signal to 60hz by calling the freq function:

Download: file
pwm.freq(60)

The frequency controls how fast the PWM signal is turned on and off, in this case a frequency of 60 will make the LED turn on and off 60 times a second.  You might need to adjust the frequency depending on what you're trying to control with a PWM output.  For LEDs you don't need a super fast frequency since humans can't really see something changing hundreds or thousands of times a second.

You can also see the current frequency of the PWM output by calling the freq function with no parameter:

Download: file
pwm.freq()

Also note the frequency is global to all PWM instances.  If you create multiple PWM objects and change the frequency on one it will change the frequency for all of them.

Now you can set the 'duty cycle', or percent of time that the signal is on vs. off, with the duty function:

Download: file
pwm.duty(1023)

The value you pass to the duty function should be in the range of 0 to 1023.  A value of 1023 like the above will set the duty cycle to 100% or completely on.  This means the signal never actually turns off--it's always on and the LED is full brightness.

Now try setting the duty cycle to 0:

Download: file
pwm.duty(0)

Notice the LED turns off.  This is because the duty cycle is 0% or completely off--the LED never actually turns on.

Set the duty cycle somewhere inbetween 0 and 1023, like about halfway at 512:

Download: file
pwm.duty(512)

Now you should see the LED light up less bright than before!  

Try setting the duty cycle to different values between 0 and 1023 to see how the brightness changes.  As you increase the duty cycle to 100% the LED will get brighter!  Remember this is because the PWM signal is turned on for a higher percent of time than turned off.  Your eye doesn't see each flash of the LED, instead it just see the 'average' amount of time its on as different brightness levels.

You can also call the duty function without a parameter to see the current duty cycle value:

Download: file
pwm.duty()

You might wonder why the duty cycle is set with a number between 0 and 1023.  Like using the ADC there's a limited 'resolution' or bit accuracy for the PWM output and with the ESP8266 you only have 10 bits available (0 to 1023).

Try making the LED fade or pulse on and off by running the following code:

Download: file
import time
import machine
pwm = machine.PWM(machine.Pin(15))
pwm.freq(60)
while True:
    for i in range(1024):
        pwm.duty(i)
        time.sleep(0.001)
    for i in range(1023, -1, -1):
        pwm.duty(i)
        time.sleep(0.001)

You should see the LED quickly fade or pulse on and off about once a second.  Press Ctrl-c to interrupt and stop the code when finished.

This code will import modules and setup the PWM output as shown previously.  Then it jumps into an infinite loop (the while True loop) that ramps the LED up from 0 to 100% duty cycle (0 to 1023) and then back down from 100% to 0% duty cycle.

Notice the ramp up is done in a for loop over the range of values 0 to 1023.  Inside the loop the duty cycle is set to the appropriate value, then the time.sleep function delays for a short time (1 millisecond).  Another for loop does the ramp down but this time it uses the range of 1023 down to 0 (notice the different range function syntax to count down to 0 instead of up).

To go further on your own try combining reading a potentiometer value with dimming a LED using PWM.  As you saw on the previous page you can use the ESP8266 ADC to read the position of a potentiometer knob.  Then using that position you can use a PWM output to control the intensity of a LED as shown on this page.  Try making the code to read the potentiometer and convert its value into a duty cycle that adjusts the LED intensity!

That's all there is to controlling the brightness of a LED with pulse-width modulation!  PWM is handy for when you need to control the intensity of an analog value and don't have a DAC or other special hardware available.  However not all components can be directly controlled with PWM, only components like LEDs which can tolerate the fast signal change and 'average' it out to be a different intensity of light.

As mentioned too other MicroPython boards have a slightly different syntax than the PWM class in the MicroPython ESP8266 port shown above. Consult your board's documentation for more details.  The basic ideas of frequency and duty cycle are the same but other boards differ in how they expose control over those parameters using timers and other lower-level details.

This guide was first published on Sep 06, 2016. It was last updated on Sep 06, 2016.
This page (Pulse-Width Modulation) was last updated on Jul 15, 2020.