3. PWM与模拟输出:从呼吸灯到舵机控制
说到PWM,很多刚接触嵌入式的朋友第一反应就是「模拟输出」。其实没那么玄乎——PWM说白了就是快速开关信号,靠占空比来「骗」过人的眼睛或者设备的响应速度。我在做第一个智能灯项目时,就靠这招实现了无级调光,效果还挺惊艳的。
核心概念:PWM(脉冲宽度调制)通过快速切换高低电平,用占空比模拟连续变化的模拟量。ESP32的LEDC控制器最多支持16路PWM通道,频率可独立设置。
3.1 analogWrite的原理——ESP32里没有这函数
如果你从Arduino转过来,可能会习惯性地写analogWrite(pin, value)。嗯,这里要注意:ESP32的Arduino核心并没有原生的analogWrite。我第一次移植项目时就踩了这个坑,代码编译报错,查了半天才发现。
ESP32用的是LEDC(LED Control)外设,本质上是一个硬件PWM发生器。你需要手动配置:
- 通道:0~15,共16个独立通道
- 频率:从几Hz到40MHz,取决于分辨率
- 分辨率:1~16位,决定占空比的精度
- 引脚:任意GPIO都可以映射到PWM通道
说白了,analogWrite的底层就是三步:配置通道参数、绑定引脚、写占空比寄存器。ESP32把这套流程拆开了,反而更灵活。
我的习惯:在项目初始化阶段,我会把所有PWM通道统一配置好,放在一个pwm_setup()函数里。这样后期调试时,改频率或分辨率只需要改一个地方。
3.2 LED呼吸灯实现——从硬件到代码
呼吸灯是PWM入门的经典案例。你想想看,LED从暗到亮再到暗,人眼感受到的就是平滑变化——实际上LED只是在以人眼无法察觉的速度切换亮灭。
实现思路很简单:
- 配置LEDC通道,频率选5000Hz(避开人眼闪烁感知)
- 设置分辨率为8位(0~255)
- 在主循环中逐渐改变占空比
- 到达边界后反向变化
// LED呼吸灯完整示例
#define LED_PIN 2
#define LED_CH 0
#define LED_FREQ 5000
#define LED_RES 8
void setup() {
ledcSetup(LED_CH, LED_FREQ, LED_RES);
ledcAttachPin(LED_PIN, LED_CH);
}
void loop() {
// 渐亮
for (int duty = 0; duty <= 255; duty++) {
ledcWrite(LED_CH, duty);
delay(10);
}
// 渐暗
for (int duty = 255; duty >= 0; duty--) {
ledcWrite(LED_CH, duty);
delay(10);
}
}
这段代码跑起来,LED就会像呼吸一样平滑变化。我曾经在一个展会上用这个效果做产品演示,观众还以为我用了什么高级模拟电路——其实就是PWM。
避坑指南:delay(10)会让整个程序阻塞。我在做多路呼吸灯时,改用millis()非阻塞计时,或者用定时器中断来更新占空比。否则你连按键扫描都做不了。
3.3 舵机控制(SG90)——PWM的另一个经典应用
SG90是5V微型舵机,控制信号就是50Hz的PWM。它的角度映射关系很固定:
| 角度 | 脉宽(高电平时间) | 占空比(50Hz, 8位) |
|---|---|---|
| 0° | 1.0 ms | 约 13 |
| 90° | 1.5 ms | 约 19 |
| 180° | 2.0 ms | 约 26 |
为什么是50Hz?因为舵机内部的比较器需要20ms的周期来检测脉宽。频率高了,舵机反而会抖动。我在一个机械臂项目里试过100Hz,结果舵机嗡嗡响,角度还不对——后来老老实实改回50Hz。
// SG90舵机控制
#define SERVO_PIN 4
#define SERVO_CH 1
#define SERVO_FREQ 50
#define SERVO_RES 8
void setup() {
ledcSetup(SERVO_CH, SERVO_FREQ, SERVO_RES);
ledcAttachPin(SERVO_PIN, SERVO_CH);
}
void setServoAngle(int angle) {
// 将0~180映射到13~26
int duty = map(angle, 0, 180, 13, 26);
ledcWrite(SERVO_CH, duty);
}
void loop() {
setServoAngle(0);
delay(1000);
setServoAngle(90);
delay(1000);
setServoAngle(180);
delay(1000);
}
个人经验:SG90的5V电源不要直接从ESP32的3.3V引脚取。我吃过这个亏——舵机一转,ESP32就重启。后来加了个独立5V电源,共地就解决了。
3.4 PWM频率与分辨率设置——鱼和熊掌
ESP32的LEDC有一个关键约束:频率和分辨率成反比。底层时钟是80MHz(APB时钟),经过分频后得到目标频率。公式是:
分频系数 = 80,000,000 / (2分辨率 × 目标频率)
分频系数必须是整数,所以不是所有组合都能实现。我整理了一个常用对照表:
| 目标频率 | 最大分辨率 | 占空比范围 | 典型应用 |
|---|---|---|---|
| 50 Hz | 16 位 | 0~65535 | 舵机控制 |
| 1000 Hz | 13 位 | 0~8191 | 电机调速 |
| 5000 Hz | 10 位 | 0~1023 | LED调光 |
| 40 kHz | 8 位 | 0~255 | 高速开关 |
你想想看,如果我要控制一个高精度舵机,16位分辨率意味着我可以把180°分成65536份——理论上精度能达到0.0027°。但实际舵机本身的机械精度也就1°左右,所以8位或10位完全够用。
我曾经犯过的错:在一个LED项目中,我选了40kHz频率和16位分辨率,结果PWM根本不工作。后来一算,分频系数不是整数,LEDC自动取了近似值,实际频率偏差很大。所以配置前最好先算一下,或者用ESP32的ledcSetup()返回值确认实际频率。
3.5 知识体系总览
下面这张图把本章的核心逻辑串起来了——从PWM原理到具体应用,再到参数配置的权衡:
这张图把本章的知识点串成了一条线:从PWM原理出发,到ESP32的硬件实现,再到LED和舵机两个典型应用,最后落到参数配置的约束条件上。你写代码的时候,脑子里要有这张图——频率和分辨率怎么选,心里就有数了。
本章小结:
- ESP32用LEDC实现PWM,需要手动配置通道、频率、分辨率
- 呼吸灯用5000Hz+8位分辨率,人眼感觉最平滑
- SG90舵机必须用50Hz,脉宽1~2ms对应0~180°
- 频率和分辨率相互制约,配置前先算分频系数
- 舵机独立供电,避免拉垮ESP32