PWM
PWM (pulse-width modulation) outputs a square wave with a configurable period and on-time. The ratio of on-time to period (the duty cycle) is what your hardware reads โ bright/dim for an LED, position for a servo, speed for a motor.
Reference: Zephyr PWM API.
Enable PWMโ
CONFIG_PWM=y
ESP32-S3 uses the LEDC peripheral for PWM. nRF52/53/54 has dedicated PWM0..PWM3 instances. Both expose the same Zephyr API โ the difference is hidden behind the binding.
Devicetree โ declare a PWM consumerโ
PWM in Zephyr is two layers: a controller node (the hardware peripheral) and a consumer node (your LED, servo, etc.) that references it.
/ {
pwmleds {
compatible = "pwm-leds";
status_led: pwm_led_0 {
pwms = <&ledc0 0 PWM_MSEC(20) PWM_POLARITY_NORMAL>;
};
};
};
The pwms cells are: <controller channel period flags>. Period is in nanoseconds โ the PWM_MSEC() / PWM_USEC() / PWM_HZ() macros do the conversion.
C code โ set duty cycleโ
#include <zephyr/drivers/pwm.h>
static const struct pwm_dt_spec status_led =
PWM_DT_SPEC_GET(DT_NODELABEL(status_led));
int main(void)
{
if (!pwm_is_ready_dt(&status_led)) {
return -ENODEV;
}
/* 50% duty cycle = half the period on */
pwm_set_pulse_dt(&status_led, status_led.period / 2);
return 0;
}
To fade the LED:
for (int i = 0; i <= 100; i++) {
uint32_t pulse = (status_led.period * i) / 100;
pwm_set_pulse_dt(&status_led, pulse);
k_msleep(10);
}
Servo example โ pulse width determines angleโ
Hobby servos expect a 20 ms period with a 1โ2 ms pulse:
- 1.0 ms โ 0ยฐ
- 1.5 ms โ 90ยฐ (center)
- 2.0 ms โ 180ยฐ
static const struct pwm_dt_spec servo =
PWM_DT_SPEC_GET(DT_NODELABEL(servo));
void servo_set_angle(uint8_t degrees)
{
/* Map 0..180 to 1ms..2ms in nanoseconds */
uint32_t pulse_ns = PWM_USEC(1000) + (degrees * PWM_USEC(1000)) / 180;
pwm_set_pulse_dt(&servo, pulse_ns);
}
Common gotchasโ
| Symptom | Cause |
|---|---|
| LED is full-on or full-off, never dim | Period too short for the LEDC clock โ try PWM_MSEC(1) or longer |
| Servo jitters | Period drifted off 20 ms โ use PWM_MSEC(20) exactly, don't compute from frequency |
pwm_set_pulse_dt() returns -EINVAL | Pulse longer than period โ clamp it |
| No output on the pin | Pin mux not set up in pinctrl โ check the binding |
Don't set the period to less than ~1 ยตs on ESP32 LEDC. The hardware clock divider can't represent very short periods at high resolution, and you'll get aliasing instead of clean PWM.