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 Hz20 位舵机(绰绰有余,常用 14 位)
1 kHz16 位LED 呼吸灯
5 kHz14 位LED
40 kHz11 位红外载波
20 kHz12 位电机(无啸叫)

若请求 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 ms0°
1.5 ms90°
2.5 ms180°

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 frequencyMax usable resolutionTypical use
50 Hz20 bitsServos (14 bits is plenty)
1 kHz16 bitsLED breathing
5 kHz14 bitsLEDs
40 kHz11 bitsIR carriers
20 kHz12 bitsMotors (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 widthAngle
0.5 ms0°
1.5 ms90°
2.5 ms180°

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)

ScenarioVerdict
Legacy tutorial errors ledcSetup undefined3.x uses ledcAttach
Visible LED flickerFrequency too low — go above 1kHz
Servo jitters/buzzesNot 50Hz; or inadequate power (separate 5V)
Duty resolution insufficientFrequency too high — see the §2 table
Slight glow at zero dutyDriver circuit leakage, not a PWM issue
Motor speed control whinesGo above 20kHz
Can I use analogWrite?Yes on 3.x (LEDC-backed) but fixed 8-bit

7. Further reading


License: CC BY-SA 4.0. Repost & remix with attribution: ESPDIY Maker Lab - espdiy.com