6.13. Generating analog with PWM and an RC filter¶
The ADC reads voltages on a pin. The opposite – producing an intermediate voltage between 0 V and Vcc on a pin – is harder, because a GPIO output only knows how to drive its two rails. The standard substitute is to switch the pin between the rails fast enough that the average voltage is what you care about.
6.13.1. Pulse-width modulation¶
A pulse-width-modulated (PWM) signal is a square wave at a fixed frequency whose high-time – the fraction of each cycle spent at Vcc instead of ground – is set in software. That fraction is the duty cycle. The average voltage of the waveform is the duty cycle times Vcc:
V_avg = duty × Vcc
A 25 % duty cycle averages to Vcc / 4; a 50 % duty cycle to
Vcc / 2; a 75 % duty cycle to 3 × Vcc / 4.
PWM at 25 %, 50 %, and 75 % duty cycle. The average voltage tracks the duty cycle.¶
The frequency is set when the PWM is configured; the duty cycle
is what software changes on the fly. The machine.PWM
class wraps a hardware timer channel that generates the
waveform without CPU help – once configured, the signal
continues at the chosen frequency and duty cycle until changed.
6.13.2. The machine.PWM class¶
Construct a PWM instance with the pin and an
initial frequency and duty:
from machine import PWM, Pin
pwm = PWM(Pin("P7"), freq=20_000, duty_u16=32768)
That starts a 20 kHz square wave at 50 % duty on P7. Two
methods change the output on the fly:
pwm.duty_u16(16384) # change to 25 % (16384 / 65535)
pwm.freq(5_000) # change to 5 kHz
duty_u16() takes an unsigned 16-bit integer
mapping 0 to “always low” and 65535 to “always high”.
freq() sets the carrier frequency in hertz.
Note
Every PWM channel on the same hardware timer shares its
frequency. Calling freq() on one channel
changes every other channel attached to that timer. Use
channels of different timers when outputs must run at
different frequencies.
Call deinit() to release the timer channel
when the output is no longer needed.
6.13.3. Averaging with an RC low-pass filter¶
Raw PWM is not a smooth voltage; it is a square wave whose average is what we want. To extract that average, pass the PWM through a low-pass filter – the same resistor-and-capacitor combination used for switch debouncing in Debouncing.
PWM through an RC low-pass filter: the capacitor averages the square wave into a DC voltage proportional to the duty cycle.¶
The filter’s cutoff frequency – the boundary between
frequencies it passes and those it blocks – is set by the same
RC product that gave the time constant for the debounce
circuit:
f_c = 1 / (2π × R × C)
For the filter to extract a clean DC voltage from a PWM input,
the cutoff frequency must be much lower than the PWM frequency
itself. The DC component (frequency 0) passes through
unchanged; the PWM’s fundamental harmonic (at the PWM
frequency) is attenuated by roughly f_c / f_PWM. A ratio of
1 / 200 cuts the residual ripple at the output to about
0.5 % of the input swing.
A reasonable starting point for a slow-changing setpoint:
PWM frequency
f_PWM = 20 kHz– well above audio, and easy for the timer to generate cleanly.Filter values
R = 1.6 kΩ,C = 1 µF– givingf_c = 1 / (2π × 1.6 kΩ × 1 µF) ≈ 100 Hz.
The 200× suppression at the carrier reduces the PWM’s full
swing down to roughly Vcc / 200 of residual ripple at
V_out – about 16 mV at 3.3 V.
Two practical notes:
The filter’s output impedance is roughly
R. Any downstream load that draws current turnsRand the load into a divider that pullsV_outbelow the ideal average, exactly like the divider on the Reading analog with the ADC page. Feed an ADC pin or a high-impedance buffer, not a load that sinks milliamps.The cap takes about
5 × R × C ≈ 8 msto settle when the duty cycle changes;V_outlags the duty setting by that much. For a setpoint that needs to update faster, raise the cutoff (smallerRorC) and accept more ripple.