ESP32 PWM 完全入门:LEDC 通道、呼吸灯、舵机频率与分辨率权衡
前置知识:GPIO 输出 只能「全开 3.3V / 全关 0V」。想要 LED 半亮、电机变速、舵机转向?你需要 PWM——让引脚以极高频率快速开关,用「占空比」模拟出中间效果。
一、PWM 的核心概念:频率 × 占空比
占空比 25%: ████────────────████──────────── → LED 约 25% 亮度
占空比 75%: ██████████████──██████████████── → LED 约 75% 亮度
频率:每秒重复上述波形的次数(Hz)。人眼/机械响应跟不上开关速度,
看到的就是「平均电压」= 3.3V × 占空比。
- 频率太低 → LED 可见闪烁、电机抖动
- 频率太高 → 分辨率被压缩(见第二节的数学约束)
- 经验值:LED 呼吸灯 1~5kHz、舵机 50Hz、有源蜂鸣器不用 PWM、直流电机 20kHz(超出可听声频段)
二、LEDC:ESP32 的 PWM 引擎
ESP32 不用软件模拟 PWM,而是有专用的 LEDC(LED Control)外设:经典 ESP32 提供 16 个独立通道(8 个高速 + 8 个低速),每路可独立设置频率与占空比。
1. 频率与分辨率的硬约束
LEDEC 定时器位数是有限的,频率和分辨率此消彼长:
分辨率 = log2( 时钟 80MHz / 频率 )
| 目标频率 | 最高可用分辨率 | 典型用途 |
|---|---|---|
| 50 Hz | 20 位 | 舵机(绰绰有余,常用 14 位) |
| 1 kHz | 16 位 | LED 呼吸灯 |
| 5 kHz | 14 位 | LED |
| 40 kHz | 11 位 | 红外载波 |
| 20 kHz | 12 位 | 电机(无啸叫) |
若请求 5kHz + 16 位,底层会静默降分辨率——占空比精度不如预期时先查这张表。
2. 新旧 API 对照(重要!教程分水岭)
arduino-esp32 2.x(网上大多数旧教程):
ledcSetup(channel, freq, resolution); // 配置通道
ledcAttachPin(pin, channel); // 引脚绑定通道
ledcWrite(channel, duty); // 写占空比
arduino-esp32 3.x(2024 起的版本,简化为引脚直连):
ledcAttach(pin, freq, resolution); // 引脚直接配置
ledcWrite(pin, duty); // 直接对引脚写
// 兼容捷径:analogWrite(pin, 0~255) 也能直接用(内部走 LEDC)
⚠️ 抄旧教程报
ledcSetup was not declared?就是 3.x 移除了旧 API。本站统一按 3.x 撰写。
三、实战 1:呼吸灯(PWM 的 Hello World)
#define LED_PIN 2
void setup() {
ledcAttach(LED_PIN, 2000, 12); // 2kHz,12 位(0~4095)
}
void loop() {
// 渐亮:占空比 0 → 4095
for (int duty = 0; duty <= 4095; duty += 8) {
ledcWrite(LED_PIN, duty);
delay(2);
}
// 渐灭:4095 → 0
for (int duty = 4095; duty >= 0; duty -= 8) {
ledcWrite(LED_PIN, duty);
delay(2);
}
}
非阻塞版(结合 第一个程序 的 millis() 范式,让呼吸灯不卡主循环):
uint32_t t0 = 0;
void loop() {
if (millis() - t0 > 4) {
t0 = millis();
float phase = (millis() % 4000) / 4000.0; // 4 秒周期
int duty = (sin(phase * 2 * PI) + 1) / 2 * 4095; // 正弦呼吸
ledcWrite(LED_PIN, duty);
}
// 这里可以并行跑其他逻辑(读传感器、收网络消息……)
}
四、实战 2:舵机(50Hz 的教科书案例)
舵机内部期望的是 50Hz 的脉冲,脉宽 0.5~2.5ms 决定角度:
| 脉宽 | 角度 |
|---|---|
| 0.5 ms | 0° |
| 1.5 ms | 90° |
| 2.5 ms | 180° |
12 位分辨率下 20ms 周期对应 4095,换算:
#define SERVO_PIN 18
void setup() {
ledcAttach(SERVO_PIN, 50, 12);
writeAngle(90); // 回中
}
void writeAngle(int angle) {
// 0.5ms~2.5ms → duty 102~512
int duty = map(angle, 0, 180, 102, 512);
ledcWrite(SERVO_PIN, duty);
}
void loop() {
writeAngle(0); delay(1000);
writeAngle(90); delay(1000);
writeAngle(180); delay(1000);
}
⚠️ 供电红线:舵机堵转电流可达 1A+,绝不能用开发板 3.3V/5V 引脚供电——独立 5V 电源(与 ESP32 共地),否则瞬间掉电触发 Brownout 重启。原理见选型指南的电源陷阱一节。
五、实战 3:三色 RGB 灯(多通道协同)
共阴 RGB LED 三个引脚各接一个通道即可调出任意颜色:
#define R_PIN 25
#define G_PIN 26
#define B_PIN 27
void setup() {
ledcAttach(R_PIN, 5000, 8);
ledcAttach(G_PIN, 5000, 8);
ledcAttach(B_PIN, 5000, 8);
}
void setColor(int r, int g, int b) { // 0~255
ledcWrite(R_PIN, r);
ledcWrite(G_PIN, g);
ledcWrite(B_PIN, b);
}
void loop() {
setColor(255, 0, 0); delay(500); // 红
setColor(0, 255, 0); delay(500); // 绿
setColor(0, 0, 255); delay(500); // 蓝
setColor(255, 255, 0); delay(500); // 黄
}
WS2812B 可寻址灯带不走 LEDC(需特殊时序),由 RMT 外设驱动——属于 L4 知识点,本站后续专文讲解。
六、避坑速查表(收藏级)
| 场景 | 结论 |
|---|---|
抄旧教程报错 ledcSetup 未定义 | arduino-esp32 3.x 改用 ledcAttach |
| LED 肉眼可见闪烁 | 频率太低,升到 1kHz 以上 |
| 舵机抖动/嗡嗡响 | 频率不是 50Hz;或供电不足(用独立 5V) |
| 占空比精度不足 | 频率过高压缩了分辨率(见第二节表) |
| 调光到最低仍微亮 | LED 驱动电路漏电,非 PWM 问题 |
| 电机调速啸叫 | 20kHz 以上超出可听频段 |
analogWrite 能用吗 | 3.x 可以(内部走 LEDC),但分辨率固定 8 位 |
七、延伸阅读
- 🧭 系列前作:ADC 模拟输入入门——读模拟量
- 🧭 基础前置:GPIO 完全入门——数字输入输出
- ⏭️ 下一篇:中断与去抖(外部事件响应,即将上线)
版权声明:本文采用 CC BY-SA 4.0 协议授权。转载与二创请注明出处:ESPDIY 创客工坊 - espdiy.com
ESP32 PWM Complete Guide: LEDC Channels, Breathing LEDs, Servos and Resolution Trade-offs
Prerequisite: GPIO output is all-or-nothing — full 3.3V or 0V. Want a half-bright LED, a variable-speed motor or a steered servo? You need PWM: switching the pin extremely fast and letting the duty cycle fake the in-between levels.
1. PWM fundamentals: frequency × duty cycle
25% duty: ████────────────████──────────── → LED at ~25% brightness
75% duty: ██████████████──██████████████── → LED at ~75% brightness
Frequency: how many times per second the pattern repeats. Eyes and mechanics
can't follow the switching, so what's perceived is the average = 3.3V × duty.
- Too low a frequency → visible LED flicker, motor chatter
- Too high a frequency → resolution gets squeezed (math in §2)
- Rules of thumb: LED breathing 1–5kHz, servos 50Hz, DC motors 20kHz (above the audible band)
2. LEDC: the ESP32’s PWM engine
There’s no software bit-banging — the ESP32 has a dedicated LEDC (LED Control) peripheral: 16 independent channels (8 high-speed + 8 low-speed) on the classic chip, each with its own frequency and duty.
1. The hard frequency/resolution constraint
The LEDC timer width is finite — frequency and resolution trade off:
resolution = log2( 80MHz clock / frequency )
| Target frequency | Max usable resolution | Typical use |
|---|---|---|
| 50 Hz | 20 bits | Servos (14 bits is plenty) |
| 1 kHz | 16 bits | LED breathing |
| 5 kHz | 14 bits | LEDs |
| 40 kHz | 11 bits | IR carriers |
| 20 kHz | 12 bits | Motors (no whine) |
Requesting 5kHz + 16-bit silently downgrades resolution — if duty precision looks off, check this table first.
2. Old vs new API (the tutorial dividing line)
arduino-esp32 2.x (most legacy tutorials):
ledcSetup(channel, freq, resolution); // configure channel
ledcAttachPin(pin, channel); // bind pin to channel
ledcWrite(channel, duty);
arduino-esp32 3.x (2024 onward, pin-direct):
ledcAttach(pin, freq, resolution); // configure pin directly
ledcWrite(pin, duty); // write to the pin
// Shortcut: analogWrite(pin, 0~255) also works (backed by LEDC)
⚠️ Copied an old tutorial and hit
ledcSetup was not declared? That’s the 3.x API removal. This site writes for 3.x only.
3. Project 1: breathing LED (the PWM Hello World)
#define LED_PIN 2
void setup() {
ledcAttach(LED_PIN, 2000, 12); // 2kHz, 12-bit (0~4095)
}
void loop() {
for (int duty = 0; duty <= 4095; duty += 8) { // fade in
ledcWrite(LED_PIN, duty);
delay(2);
}
for (int duty = 4095; duty >= 0; duty -= 8) { // fade out
ledcWrite(LED_PIN, duty);
delay(2);
}
}
Non-blocking version (combining the millis() paradigm from Your First Program so the main loop stays free):
uint32_t t0 = 0;
void loop() {
if (millis() - t0 > 4) {
t0 = millis();
float phase = (millis() % 4000) / 4000.0; // 4s period
int duty = (sin(phase * 2 * PI) + 1) / 2 * 4095; // sinusoidal breathing
ledcWrite(LED_PIN, duty);
}
// other logic runs here in parallel (sensors, network...)
}
4. Project 2: servos (the 50Hz textbook case)
A servo expects 50Hz pulses; the pulse width 0.5–2.5ms sets the angle:
| Pulse width | Angle |
|---|---|
| 0.5 ms | 0° |
| 1.5 ms | 90° |
| 2.5 ms | 180° |
At 12-bit resolution a 20ms period maps to 4095:
#define SERVO_PIN 18
void setup() {
ledcAttach(SERVO_PIN, 50, 12);
writeAngle(90); // center
}
void writeAngle(int angle) {
// 0.5ms~2.5ms → duty 102~512
int duty = map(angle, 0, 180, 102, 512);
ledcWrite(SERVO_PIN, duty);
}
void loop() {
writeAngle(0); delay(1000);
writeAngle(90); delay(1000);
writeAngle(180); delay(1000);
}
⚠️ Power red line: a stalled servo can draw 1A+ — never power it from the dev board’s 3.3V/5V pins. Use a separate 5V supply (sharing GND with the ESP32) or enjoy instant Brownout resets. See the power traps section of the Chip Selection Guide.
5. Project 3: RGB LEDs (multi-channel coordination)
A common-cathode RGB LED needs one channel per color:
#define R_PIN 25
#define G_PIN 26
#define B_PIN 27
void setup() {
ledcAttach(R_PIN, 5000, 8);
ledcAttach(G_PIN, 5000, 8);
ledcAttach(B_PIN, 5000, 8);
}
void setColor(int r, int g, int b) { // 0~255
ledcWrite(R_PIN, r);
ledcWrite(G_PIN, g);
ledcWrite(B_PIN, b);
}
void loop() {
setColor(255, 0, 0); delay(500); // red
setColor(0, 255, 0); delay(500); // green
setColor(0, 0, 255); delay(500); // blue
setColor(255, 255, 0); delay(500); // yellow
}
WS2812B addressable strips don’t use LEDC (they need special timing) — that’s the RMT peripheral, an L4 topic covered in a dedicated article later.
6. Pitfall cheat sheet (bookmark this)
| Scenario | Verdict |
|---|---|
Legacy tutorial errors ledcSetup undefined | 3.x uses ledcAttach |
| Visible LED flicker | Frequency too low — go above 1kHz |
| Servo jitters/buzzes | Not 50Hz; or inadequate power (separate 5V) |
| Duty resolution insufficient | Frequency too high — see the §2 table |
| Slight glow at zero duty | Driver circuit leakage, not a PWM issue |
| Motor speed control whines | Go above 20kHz |
Can I use analogWrite? | Yes on 3.x (LEDC-backed) but fixed 8-bit |
7. Further reading
- 🧭 Series: ADC input guide — reading analog quantities
- 🧭 Foundation: GPIO Complete Guide — digital in/out
- ⏭️ Next: interrupts & debouncing (coming soon)
License: CC BY-SA 4.0. Repost & remix with attribution: ESPDIY Maker Lab - espdiy.com