Pulse Width Modulation (PWM) via the lgpio library is a method of rapidly toggling a Raspberry Pi GPIO pin between 3.3V HIGH and 0V LOW states at a specific frequency to simulate a variable analog voltage. In a real circuit, this changes a simple binary on/off digital output into a precisely metered average voltage, allowing you to dim LEDs, control servo angles, or dictate DC motor speed without wasting power as heat through resistors. If you are migrating from the deprecated RPi.GPIO library, mastering lgpio.tx_pwm() is your first step toward modern, character-device-based GPIO control on Pi OS Bookworm and later.
The Core Mechanics of lgpio PWM
Unlike older libraries that relied on user-space memory mapping, lgpio interfaces directly with the Linux kernel's character device (/dev/gpiochip0). When you call the PWM function, the library configures high-resolution kernel timers to flip the pin state.
Suppose you want to run a 12V brushless DC fan at 60% speed using an IRLB8721 logic-level MOSFET. You need a frequency high enough to avoid audible whine, so you choose 25,000 Hz (25 kHz).
- Period: 1 / 25,000 = 0.00004 seconds (40 µs).
- Duty Cycle: 60%.
- High Time: 40 µs × 0.60 = 24 µs (Pin outputs 3.3V).
- Low Time: 40 µs × 0.40 = 16 µs (Pin outputs 0V).
- Average Voltage at Pi Pin: 3.3V × 0.60 = 1.98V.
The MOSFET sees this 1.98V average on its gate, switching the 12V drain-source path on for 24 µs and off for 16 µs. The fan's internal inductance smooths this into a steady 60% speed.
Where You Meet This in Practice
You will configure PWM parameters differently depending on the physical load you are driving. The two variables—frequency (Hz) and duty cycle (%)—must be matched to the component's physics.
- Standard RC Servos (e.g., SG90, MG996R): Require exactly 50 Hz. The duty cycle translates to pulse width (1ms to 2ms) to set the physical angle from 0° to 180°.
- LED Dimming: Requires 1 kHz to 5 kHz. Anything below 100 Hz causes visible flicker on camera; anything above 10 kHz can cause electromagnetic interference (EMI) with nearby AM radios or audio equipment.
- DC Motors and Fans: Require 20 kHz to 25 kHz. This pushes the switching frequency above the human hearing range, eliminating the high-pitched squeal that occurs at 1 kHz.
- Proportional Valves / Heaters: Can often run at 10 Hz to 100 Hz, as the thermal mass or fluid inertia naturally filters the pulses.
Software vs. Hardware PWM: The Decision Path
A frequent point of failure on the workbench is using the wrong PWM type for a jitter-sensitive application. The Raspberry Pi has dedicated hardware PWM silicon, but lgpio.tx_pwm() generates software PWM via kernel timers. Use this decision tree to pick your approach.
| If your project requires... | Then choose... | Why? |
|---|---|---|
| Basic LED dimming, DC fan control, or simple heaters (1-4 pins) | lgpio Software PWM (tx_pwm) |
Works on any GPIO pin. Kernel timers are stable enough for 95% of physical loads. |
| Driving more than 4 servos simultaneously | PCA9685 I2C Board | Offloads timing to a dedicated 16-channel chip. Zero CPU jitter, frees up Pi resources. |
| Audio tone generation, RF transmission, or strict zero-jitter robotics | Pi Hardware PWM (GPIO 12, 13, 18, 19) | Software PWM has microsecond jitter during OS context switches. Hardware PWM is driven by dedicated silicon. |
Step-by-Step: Configuring lgpio.tx_pwm() in Python
Before running this code, ensure the lgpio Python module is installed (pip install lgpio). You must also wire your Pi's GPIO 18 to the gate of a logic-level MOSFET (like the IRLB8721), with a 10kΩ pull-down resistor between the gate and ground to prevent the motor from spinning during Pi boot-up.
Safety Warning: Always include a flyback diode (e.g., 1N4007) wired in reverse-parallel across your DC motor or fan terminals. Inductive kickback will instantly destroy your MOSFET and can feed lethal voltage spikes back into your Pi's 3.3V rail.
import lgpio
import time
# --- Configuration ---
GPIO_CHIP = 0 # /dev/gpiochip0 on standard Pi OS
GPIO_PIN = 18 # Physical pin 12, BCM 18
PWM_FREQ = 25000 # 25 kHz (inaudible for fans/motors)
DUTY_CYCLE = 60.0 # 60% speed
def run_motor_pwm():
# Open the GPIO chip character device
h = lgpio.gpiochip_open(GPIO_CHIP)
# Claim the pin as an output
lgpio.gpio_claim_output(h, GPIO_PIN)
try:
print(f"Starting PWM at {PWM_FREQ}Hz, {DUTY_CYCLE}% duty cycle.")
# lgpio.tx_pwm(handle, gpio, frequency, duty_cycle)
lgpio.tx_pwm(h, GPIO_PIN, PWM_FREQ, DUTY_CYCLE)
# Run for 5 seconds
time.sleep(5)
except KeyboardInterrupt:
print("Interrupted by user.")
finally:
# CRITICAL: Stop PWM and release pin to prevent runaway motors
print("Stopping PWM and cleaning up.")
lgpio.tx_pwm(h, GPIO_PIN, 0, 0) # Set duty cycle to 0 to halt
lgpio.gpio_free(h, GPIO_PIN)
lgpio.gpiochip_close(h)
if __name__ == "__main__":
run_motor_pwm()
According to the official lgpio Python documentation, passing a duty cycle of 0 effectively halts the waveform and leaves the pin in a predictable state, which is why we use it in the finally block rather than just closing the chip.
Common Confusions and Pitfalls
When debugging a circuit that isn't responding correctly to lgpio PWM, builders usually fall into one of three traps:
- Confusing Duty Cycle with Pulse Width: In
lgpio.tx_pwm(), the fourth argument is duty cycle (a percentage from 0.0 to 100.0). Builders coming from older libraries or C-level hardware registers often pass microsecond values (like1500for a servo), which the library interprets as 100% (capped) or throws an error. For a 50Hz servo needing a 1.5ms pulse, you must calculate the percentage: 1.5ms / 20ms = 7.5%. - Using Standard MOSFETs instead of Logic-Level: A standard IRF520 MOSFET requires 10V on its gate to fully open. The Pi only outputs 3.3V. The MOSFET will operate in its linear (resistive) region, overheat, and fail to pass full current to your load. Always buy Logic-Level MOSFETs (prefix IRL, e.g., IRLZ44N or IRLB8721) which are fully saturated at 3.3V.
- Ignoring the Pull-Down Resistor: During the 10-15 seconds a Raspberry Pi takes to boot, GPIO pins float. A floating gate on a MOSFET will pick up ambient EMI and partially turn on, causing your motor to stutter or your MOSFET to overheat before your Python script even starts. A 10kΩ resistor from Gate to Ground holds it firmly at 0V until
lgpioclaims the pin.
FAQ: lgpio PWM Edge Cases
Can I use lgpio to drive a 5V servo directly from the Pi pin?
No. While the Pi's 3.3V PWM signal is usually enough to trigger the control logic on a 5V servo, the Pi's GPIO pins can only safely source about 16mA total across all pins. A servo under load draws hundreds of milliamps. You must power the servo's VCC and GND from an external 5V buck converter, connecting only the PWM signal wire to the Pi, and ensuring the Pi's GND and the external power supply's GND are bonded together.
Why does my LED flicker slightly when the Pi is compiling code?
That is software PWM jitter. When the Linux kernel pauses the lgpiod process to handle heavy CPU loads, the high-resolution timer misses a microsecond beat. For LEDs and motors, this is invisible. If you are building a DAC (Digital-to-Analog Converter) for audio or a strict timing application, you must switch to the Pi's dedicated Hardware PWM pins (GPIO 12, 13, 18, or 19) using a library like rpi-hardware-pwm, as documented in the Raspberry Pi hardware specifications.
What happens if I set the frequency to 1 Hz?
lgpio will happily toggle the pin once per second. However, if you are driving an inductive load like a motor, a 1 Hz frequency will cause the motor to physically start and stop every second, drawing massive inrush currents and likely destroying your driver circuit. Always keep motor PWM above 1 kHz unless you are intentionally building a slow-blink circuit.






