Pulse width modulation (PWM) is one of the five basic functionalities of any microcontroller. The other four are digital input, digital output, analog input and serial data communication. Most microcontrollers do not have a built-in digital-to-analog converter to output analog signals. However, most microcontrollers have one or more PWM output interfaces. PWM signals are periodic rectangular signals with programmable ON and OFF pulse duration. With a modulated pulse width, these signals can approximate true analog output. PWM signals are not intended for analog data communication, supplying electrical power, or working with analog circuits. However, these signals are very useful for providing output power to external components such as LED lights and speakers. These signals can also be used as control signals for actuators such as servo motors.
PWM signals are used to control devices rather than supplying power to them. The power output of a GPIO microcontroller is too small to drive virtually any actuator or device. For example, the maximum current that the ESP32 and ESP8266 boards can absorb or supply is only 40 mA and the operating voltage is only 3.3V. External power amplifier circuits are required to drive components such as speakers and doorbells with PWM signals.
What is pulse width modulation?
Pulse width modulation changes the average power delivered by a signal by dividing the square wave signal into discrete parts. It's not a real continuous signal. Instead, the ON and OFF duration of a periodic digital signal is modulated to reduce the effective output voltage/power. For example, if a microcontroller's GPIO outputs 3.3V at the digital output while generating a PWM signal at 50% of the pin's duty cycle, it will produce an effective voltage of approximately 1.65V, i.e. 3.3 /two.
PWM vs. analog output
PWM signals are not true analog signals. True analog signals are continuous and can have any fluctuating value. PWM signals are generated by dividing the output duration of digital signals by discrete values. Therefore, PWM signals can come close to the expected analog outputs, but can never have similar fluctuating values. How close a PWM signal can approach a floating point value depends on its resolution. For example, ESP8266 has a 10-bit PWM resolution with an operating voltage on its GPIO of 3.3V. Thus, the voltages that the ESP8266 PWM controller can approximate will be in steps of 3.3/1023 V or 3.22 mV.
Also, note that PWM signals are periodic signals. They cannot approximate non-periodic continuous signals; Even though they are periodic signals, PWM signals cannot accurately approximate all frequencies. How closely a PWM signal can match an output frequency depends on the microcontroller's base frequency and the PWM resolution. For example, the base frequency in ESP32 can range from 1 Hz to 40 MHz, and the PWM resolution can be set from 1 to 16 bits. Let it be set to 10 bits. Suppose you want to emit a frequency of 300 KHz. If the base frequency is set to 40 MHz, the divisor required to generate the 300 KHz frequency will be 40,000,000/300,000, that is, 133.33. If we use 133 as a divider, the actual frequency of the signal we get is 40,000,000/133, that is, 300,751 Hz. This is not precisely 300 KHz.
Also, note that the PWM output largely depends on the specific behavior of the specific microcontroller/gate. For example, Arduino boards use a dedicated hardware clock to generate PWM signals. ESP boards use timers and interrupts for PWM generation, starting with a theoretical base frequency of 80 MHz. The PWM controller in the ESP32 consists of two submodules – LED Control (LEDC) and Motor Control Pulse Width Modulator (MCPWM) . In MicroPython, Expressif's IoT Development Framework (IDF) is used as a software development kit. The kit uses the same API for PWM implementation in ESP8266 and ESP32. In ESP32, the MicroPython firmware uses an LEDC peripheral for PWM generation. The minimum and maximum frequencies of the Arduino depend only on the base clock frequency and PWM resolution. But in the case of ESP boards, it depends not only on the hardware implementation, such as the base frequencies supported in the Expressif IDF, but also on the software implementation in the MicroPython firmware. We will discuss this in detail later in this article.
MicroPython Machine Library
The machine module manages many hardware-related functions in MicroPython. This module consists of several classes that are written to control digital input/output, control output signals from external devices, pulse width modulation, analog to digital conversion, control ADC, UART, SPI, I2C, I2S, Timer peripherals, RTC, Watchdog timer and manage SD card. The PWM controller is managed by the machine module PWM class.
MicroPython PWM Class
The PWM class was written to provide pulse width modulation on MicroPython supported boards. This class can be imported into a MicroPython script using the following instructions.
PWM machine import
After importing, an object must be instantiated from the PWM class. For this, the machine.PWM method is provided. This method has the following syntax.
class machine.PWM (target, *, frequency, duty_u16, duty_ns)
This construct returns a PWM object. The destination parameter is the PWM pin on which the PWM output should be generated. This can be specified by a port-specific identifier that can be an integer, a string, or a tuple depending on the specific port. The frequency , duty_u16, and duty_ns are optional parameters. The frequency parameter is the frequency in Hz for the PWM cycle. The duty_u16 parameter specifies the duty cycle as ratio duty_cycle/65535. The duty_ns parameter specifies the duration of the pulse in nanoseconds. If frequency is specified, only duty_u16 or only duty_ns must be provided. Some of the valid examples of PWM object instantiation are as follows.
pwm0 = machine.PWM(Pin(0))
pwm0 = PWM(Pin(0))
pwm0 = PWM(Pin(0), 5000)
pwm0 = PWM(Pin(0), 5000, 2**16*1//2) //50% duty cycle
pwm0 = PWM(Pin(0), 5000, 250_000)
The class includes the following methods for configuring the PWM output.
PWM.freq : This method is used to get or set the current frequency of the PWM output. If called without arguments, returns the frequency in Hz. A single integer argument can be passed to the method call to set the frequency in Hz. The acceptable frequency range depends on the specific microcontroller/gate.
PWM.duty : This method gets or sets the duty cycle as ratio duty_cycle/1023. If called without arguments, it returns the duty cycle as an unsigned integer value between 0 and 1023. An unsigned integer argument with a value from 0 to 1023 must be passed to set the duty cycle. For example, pwm0.duty(512) sets the duty cycle of the PWM object pwm0 to 50%.
PWM.duty_u16 : This method gets or sets the duty cycle as the 16-bit unsigned value in the range 0 to 65535. If called without arguments, it returns the duty cycle as a 16-bit unsigned number between 0 and 65535. An unsigned long argument with a value from 0 to 65535 must be passed to define the duty cycle.
PWM.duty_ns : This method sets or gets the current pulse width of the PWM output in nanoseconds. When called without arguments, returns the pulse width in nanoseconds. A value in the range 0 to 1000,000,000 must be passed to set the pulse width.
PWM.init(*, frequency, duty_u16, duty_ns) : Modifies the PWM object configuration settings after the object has already been constructed. It accepts the same parameters as the constructor object, except the PWM pin.
PWM.deinit : This method deactivates the PWM object.
It is worth noting that frequency and duty cycle resolution are interdependent. The higher the frequency, the lower the duty cycle resolution available.
PWM on ESP8266
ESP8266 has four dedicated PWM output interfaces. These PWM interfaces are listed below.
MicroPython allows you to specify a frequency range of 1 Hz to 1 KHz and a 10-bit PWM resolution for ESP8266. In MicroPython, up to 8 PWM outputs can be performed on the ESP8266. Take a look at the following screenshot of esppwm.c in MicroPython firmware. In ESP8266 it is also possible to implement PWM by software. This involves the use of timer interrupts. The PWM software in the ESP8266 can have a resolution of up to 44 nanoseconds. The PWM frequency range can be adjusted from 1 microsecond to 10,000 microseconds, i.e. 100 Hz to 1KHz. The PWM software in the ESP8266 allows for 14-bit duty cycle resolution at a frequency of 1 KHz. In ESP8266, PWM can be output on all GPIO except GPIO16.
For ESP8266, the PWM object can be instantiated using the machine.PWM constructor. Note that the duty_u16 or duty_ns parameters are not acceptable for the ESP8266 port. The PWM frequency can be set or obtained using the PWM.freq method. The acceptable value for setting the frequency is between 1 and 1000. The PWM duty cycle can be set or obtained using the PWM.duty method. The method accepts a 10-bit value as a parameter and defines the frequency as duty_cycle/1023 ratio. Note that you cannot use duty_u16 or duty_ns to set or get the duty cycle of the ESP8266 as the board only allows 10-bit PWM resolution. A PWM object can be disabled by calling the PWM.deinit method.
PWM on ESP32
The PWM controller in ESP32 consists of two different submodules – LED Control (LEDC) and Motor Control Pulse Width Modulator (MCPWM). The MicroPython firmware only uses the LEDC module for PWM generation. This is evident in the following screenshot of ports/esp32/machine_pwm.c in the MicroPython firmware.
There are 16 independent PWM channels in ESP32. These channels are divided into two groups – a group of eight high-speed channels and another group of 8 low-speed channels. PWM channels can be multiplexed to any GPIO except the input-only pins, i.e. GPIO34~39. For each group, there are four timers per eight channels. This means that each timer is coupled to two PWM channels. Therefore, only eight different PWM frequencies can be generated at a time from 16 PWM channels. However, it is possible to have different duty cycles across all 16 PWM channels. MicroPython allows you to use PW software to generate a different PWM frequency from 8 PWM channels.
The ESP32 PWM controller LEDC module can use one of the three different source clocks listed below.
The MicroPython firmware uses only two clock sources APB_CLK and REF_TICK. If the frequency of a PWM object is set below 10 Hz, REF_CLK is used by the firmware; otherwise APB_CLK will be used as the clock source. Take a look at the following screenshot of ports/esp32/machine_pwm.c in MicroPython firmware.
The PWM frequency is also limited by the duty cycle resolution in addition to the clock source. Both PWM frequency and duty cycle resolution are inversely proportional. The higher the frequency, the lower the duty cycle resolution available.
Finally, PWM resolution and duty cycle resolution are also limited by MicroPython's PWM implementation. While the APB_CLK base clock used by Expressif's IDF uses a theoretical frequency of 80 MHz, the maximum PWM frequency set for ESP32 is 40 MHz with a duty cycle resolution of 1 bit. This means that a 40 MHz PWM signal can be generated with a duty cycle of 50% or 100%. The maximum PWM frequency that can be output at 8-bit resolution is 312.5 KHz. The maximum PWM frequency that can be output at 10-bit resolution is 78.125 KHz.
For ESP32, MicroPython allows specifying PWM frequency from 1KHz to 40 MHz. Duty cycle resolution can be from 1 bit to 16 bits. To avoid errors in PWM generation, first determine the desired duty cycle resolution. The greater the resolution of the duty cycle, more precisely, the output voltage levels can be controlled. After setting the duty cycle resolution, you can calculate the highest possible PWM frequency using the following equation.
Max_PWM_freq = 80,000,000/2 ^ Duty Cycle Resolution Bit
For example, if we require 13-bit duty cycle resolution, the maximum PWM frequency will be as follows.
Freq_PWM_max = 80,000,000/2^13
= 9.765 KHz
For ESP32, the PWM object can be instantiated using the machine.PWM constructor. The duty cycle can be set using PWM.duty , PWM.duty_u16 or PWM.duty_ns methods. The PWM frequency can be set using PWM.freq method. PWM settings can be changed by calling PWM.init method. Changes to PWM settings happen without CPU interruptions. The PWM object can be disabled by calling the PWM.deinit method.
Note that some ESP32 boards have fewer PWM channels. The following table lists the PWM channels and their specifications for different ESP32 boards.
PWM Applications
PWM signals are used for diverse applications such as dimming LEDs, dimming lights, controlling the direction of a servo motor, activating buzzers, controlling the speed of DC motors, generating audio signals from speakers, encoding of messages for analog communication devices and production of analog voltages. The following table lists some of the recommended PWM frequencies and duty cycle resolution for many common applications.
Faded LED on ESP8266 using MicroPython firmware
Fading of LED lights is one of the typical applications of PWM signals. LED fading is often used to monitor the PWM output of a microcontroller. In this article, we will use MicroPython to dim the LED on ESP8266.
Required components
- ESP8266x1
- 5mm LED x1
- 330Ω resistance x1
- breadboard x1
- Connecting wires/jumper wires
Circuit Connections
Connect the LED's anode to GPIO14 of the ESP8266 and its cathode to ground through a 330 ohm series resistor. In this way, the LED is connected to the ESP32/ESP8266 in such a way that it glows when the GPIO source supplies current to the LED while remaining off until the GPIO is in logic LOW.
Note that the MicroPython firmware must already be loaded on the ESP8266. Learn more about uploading MicroPython firmware to the ESP8266 and ESP32.
MicroPython Script
Pin machine import, PWM
of time matter sleep
frequency = 1000
led = PWM (Pin (14), frequency)
while True:
for duty_cycle in the range (0, 1024):
led.duty(duty_cycle)
sleep (0.005)
How it works
The LED is connected to the ESP8266 so that it glows when the board supplies current to it. The ESP8266 provides a PWM output for the LED. The PWM frequency is set to 1 KHz, which is the highest frequency available for ESP8266. The duty cycle can be adjusted with a 10-bit resolution. Therefore, the duty cycle increases from 0 to 1023 in five millisecond intervals. This provides a fade-in and fade-out effect for five seconds.
The code
The code starts by importing the Pin and PWM classes from the machine and the sleep classes from the timing modules. The frequency is stored in a variable of 1000. A PWM object is instantiated by calling the PWM method. In an infinite while loop, the duty cycle is changed from 0 to 1023 and applied by calling the PWM.duty method for 5 millisecond intervals.
Results