In this tutorial, we will see how to use PWM signals in MicroPython to generate pseudo-analog outputs to control, for example, LED brightness or motor speed.
PWM (Pulse Width Modulation) is a technique we have frequently seen on the blog, which allows us to generate “more or less analog” signals cheaply with a microcontroller.
Basically, since having hardware to make a real analog signal is expensive, what we do is a cycle where we turn a digital signal on and off at certain intervals.
- Duty Cycle: It is the percentage of time the signal is in a high state. For example, a 50% duty cycle means the signal is on half the time and off the other half.
- Frequency: It is the number of times the signal repeats per second, measured in Hertz (Hz).
The PWM frequency must be adjusted according to the device being controlled. In general, a frequency between 500 Hz and 1 kHz is usually suitable in many cases.
If the device is “slow” (has a lot of inertia) compared to the PWM frequency, it may behave more or less as if it were receiving an analog signal.
But not all devices will accept your pseudo-analog signal. Some of them may behave erratically and we can even damage them if they are not prepared for the voltage levels.
PWM Configuration in MicroPython
To use PWM in MicroPython, we must first import the machine module, which provides the necessary functions to interact with the hardware (as we did with digital outputs).
Then, we configure a GPIO pin as a PWM output like this,
pwm = PWM(led_pin)
Now we can use some of the PWM methods to configure our pseudo-analog output. For example
| Command | Description |
|---|---|
pwm.freq(freq) | Sets or returns the frequency of the PWM signal in hertz (Hz). |
pwm.duty(duty_cycle) | Adjusts the duty cycle, varying the LED brightness (value between 0 and 1023). |
pwm.deinit() | Deactivates the PWM signal on the specified pin. |
pwm.duty_u16(duty_cycle) | Adjusts the duty cycle with 16-bit resolution (value between 0 and 65535). |
pwm.init(freq, duty) | Initializes the PWM with a specific frequency and duty cycle. |
Practical Example
Let’s see it better with a simple example. We are going to control the brightness of an LED using PWM.
from machine import Pin, PWM
import time
# GPIO pin configuration where the LED is connected
led_pin = Pin(2, Pin.OUT) # Using GPIO pin 2 (common in ESP32/ESP8266)
pwm = PWM(led_pin) # Configure the pin as a PWM output
# PWM frequency configuration (for example, 1000 Hz)
pwm.freq(1000)
# Function to vary the brightness of the LED
def vary_brightness():
for duty_cycle in range(0, 1024): # Range from 0 to 1023 (10 bits)
pwm.duty(duty_cycle) # Adjust the duty cycle
time.sleep_ms(10) # Small pause to observe the change
# Execute the function
vary_brightness()
In this example,
- We import
PinandPWMfrom themachinemodule, andtimeto handle pauses. - We define GPIO pin 2 as an output and configure it for PWM.
- We set the PWM signal frequency to 1000 Hz.
- We use a
forloop to vary the duty cycle from 0 to 1023 (10 bits), which allows controlling the LED brightness. - We add a small pause (
time.sleep_ms(10)) to observe the gradual change in brightness.
Practical Examples
RGB LED Brightness Control
Just as we can control an LED with a PWM signal, we can use three to control an RGB LED, allowing the color to be varied through the intensity of each channel (red, green, and blue).
from machine import Pin, PWM
import time
# Configuration of the pins for the RGB LEDs
red = PWM(Pin(2, Pin.OUT))
green = PWM(Pin(4, Pin.OUT))
blue = PWM(Pin(5, Pin.OUT))
# PWM frequency configuration
red.freq(1000)
green.freq(1000)
blue.freq(1000)
# Function to vary the color of the RGB LED
def vary_color():
for i in range(0, 1024):
red.duty(i)
green.duty(1023 - i)
blue.duty(i // 2)
time.sleep_ms(10)
# Execute the function
vary_color()
Motor Control with PWM
Now, let’s apply PWM to control the speed of a motor. This example is useful in applications like robots or automation systems.
from machine import Pin, PWM
import time
# GPIO pin configuration where the motor is connected
motor_pin = Pin(4, Pin.OUT) # Using GPIO pin 4
pwm = PWM(motor_pin) # Configure the pin as a PWM output
# PWM frequency configuration (for example, 500 Hz)
pwm.freq(500)
# Function to vary the speed of the motor
def vary_speed():
for duty_cycle in range(0, 1024): # Range from 0 to 1023 (10 bits)
pwm.duty(duty_cycle) # Adjust the duty cycle
time.sleep_ms(20) # Small pause to observe the change
# Execute the function
vary_speed()
In this case, the motor will vary its speed according to the applied duty cycle. A 50% duty cycle will make the motor spin at half its maximum speed.
Be careful with motors, depending on their size, they usually don’t like PWM signals at all. They can fry a digital pin due to induced voltages.
Tone Generation with a Buzzer
Passive buzzers require a PWM signal to generate audible tones. Let’s assume a buzzer connected to GPIO15 (through a resistor).
from machine import Pin, PWM
import time
# PWM configuration
buzzer = PWM(Pin(15))
def beep(frequency, duration):
"""
Generates a tone in the buzzer.
:param frequency: Frequency in Hz.
:param duration: Duration in seconds.
"""
buzzer.freq(frequency)
buzzer.duty_u16(32768) # Duty cycle at 50%
time.sleep(duration)
buzzer.duty_u16(0) # Turn off the buzzer
# Generate tones
try:
beep(440, 1) # A4 - 440 Hz for 1 second
beep(880, 0.5) # A5 - 880 Hz for 0.5 seconds
except KeyboardInterrupt:
buzzer.deinit()
