3. PWM与模拟输出:LED呼吸灯、舵机控制、PWM频率与占空比调节
各位同学,咱们今天聊聊PWM。这东西在嵌入式里太常用了,说白了就是通过数字信号去模拟一个模拟电压。你想想看,ESP32本身是个数字芯片,它没法直接输出0到3.3V之间的任意电压,但有了PWM,这事儿就能办到。
我记得刚入行那会儿,有个项目要用单片机控制电机转速,当时我还在想是不是得加个DAC芯片。后来老工程师跟我说,你试试PWM加个MOS管驱动。一试,嘿,效果还真不错。从那以后,我对PWM就特别上心。
3.1 PWM的基本原理
PWM的全称是Pulse Width Modulation,脉冲宽度调制。它的核心思想很简单:用固定频率的方波,通过改变高电平持续的时间(也就是脉宽),来模拟不同的电压值。
这里有两个关键参数:
- 频率(Frequency):方波每秒钟重复的次数,单位是Hz。频率决定了PWM的"刷新率"。
- 占空比(Duty Cycle):高电平时间占整个周期的百分比。占空比决定了等效输出电压。
举个例子,一个5V的PWM信号,如果占空比是50%,那等效电压就是2.5V。如果占空比是100%,那就是5V。如果占空比是0%,那就是0V。这个等效电压是通过低通滤波或者负载的惯性来"感受"到的。
核心公式:
等效电压 = 高电平电压 × 占空比
占空比 = 高电平时间 / 周期 × 100%
为什么会这样?因为负载(比如LED、电机)对快速变化的电压有惯性。LED的亮度变化跟不上PWM的开关速度,电机转子的转动也跟不上。所以它们感受到的是平均值。
3.2 ESP32的PWM硬件资源
ESP32的PWM模块叫LEDC(LED Control),名字虽然叫LED控制,但实际用途远不止于此。它可以用来控制舵机、电机、甚至产生特定频率的载波信号。
ESP32的LEDC模块有几个特点:
- 有8个通道(0-7),每个通道可以独立配置
- 每个通道可以绑定到任意GPIO引脚
- 支持16位分辨率(0-65535),精度很高
- 频率范围很宽,从几Hz到几MHz都可以
我在项目中遇到过一个问题:用ESP32驱动一个无刷电机电调,电调要求50Hz的PWM信号,占空比范围是5%到10%。当时我直接用默认配置,结果电机死活不转。后来查手册才发现,ESP32的LEDC模块在低频时需要注意时钟源的选择。嗯,这里要注意,低频PWM最好用APB时钟(80MHz)分频,别用REF_TICK(1MHz),否则精度不够。
3.3 LED呼吸灯实战
呼吸灯是PWM最经典的入门实验。它的原理很简单:让LED的亮度从暗到亮,再从亮到暗,循环变化。说白了就是不断改变PWM的占空比。
来看代码:
#include <Arduino.h>
#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);
}
}
这段代码里,我用了8位分辨率,所以占空比范围是0到255。频率设成了5000Hz,也就是5kHz。为什么选5kHz?因为人眼对500Hz以下的闪烁比较敏感,5kHz足够高,LED看起来就是连续变化的。
小技巧:如果你想让呼吸效果更平滑,可以用指数曲线或者正弦曲线来映射占空比,而不是简单的线性变化。人眼对亮度的感知是非线性的,线性变化看起来反而会感觉"跳变"。
3.4 舵机控制实战
舵机是另一个PWM的典型应用。标准舵机(比如SG90、MG996R)的控制信号是50Hz的PWM,周期20ms。占空比范围通常是1ms到2ms,对应0°到180°。
具体对应关系:
| 脉宽(ms) | 占空比(50Hz) | 角度 |
|---|---|---|
| 1.0 | 5% | 0° |
| 1.5 | 7.5% | 90° |
| 2.0 | 10% | 180° |
注意,不同品牌的舵机可能有细微差别。我曾经买过一批便宜的舵机,标称是0°到180°,实际测试发现0°对应0.5ms,180°对应2.5ms。所以拿到新舵机,最好先用示波器或者逻辑分析仪测一下实际范围。
来看舵机控制的代码:
#include <Arduino.h>
#define SERVO_PIN 4
#define SERVO_CH 1
#define SERVO_FREQ 50
#define SERVO_RES 16
void setup() {
ledcSetup(SERVO_CH, SERVO_FREQ, SERVO_RES);
ledcAttachPin(SERVO_PIN, SERVO_CH);
}
void setServoAngle(int angle) {
// 角度映射到脉宽:0° -> 1ms, 180° -> 2ms
// 16位分辨率下,50Hz周期对应65535
// 1ms脉宽 = 65535 * (1ms / 20ms) = 3276
// 2ms脉宽 = 65535 * (2ms / 20ms) = 6553
int pulseWidth = map(angle, 0, 180, 3276, 6553);
ledcWrite(SERVO_CH, pulseWidth);
}
void loop() {
setServoAngle(0);
delay(1000);
setServoAngle(90);
delay(1000);
setServoAngle(180);
delay(1000);
}
这里我用了16位分辨率,因为50Hz频率比较低,16位分辨率能提供更精细的脉宽调节。你想想看,如果只用8位分辨率,0°到180°之间只有255个档位,每个档位对应的角度变化接近0.7°,对于精密控制来说可能不够用。
警告:舵机在启动瞬间电流很大,尤其是带负载的时候。如果用ESP32的3.3V引脚直接供电,很可能导致电压跌落,造成ESP32重启。我建议用外部5V电源给舵机供电,ESP32和舵机共地即可。
3.5 PWM频率与占空比的调节技巧
在实际项目中,PWM的频率选择很重要。不同的应用场景对频率的要求不一样:
- LED调光:频率建议在1kHz以上,避免人眼可见的闪烁。我一般用5kHz到10kHz。
- 舵机控制:标准是50Hz,别乱改。有些数字舵机支持更高的刷新率,但需要看手册。
- 直流电机调速:频率通常在100Hz到20kHz之间。频率太低电机会抖动,频率太高MOS管开关损耗会增大。
- 音频输出:频率要远高于音频范围,一般用20kHz以上,避免产生可闻噪声。
占空比的调节也有讲究。ESP32的ledcWrite函数接受的值范围取决于你设置的分辨率。8位分辨率就是0-255,16位就是0-65535。我个人的习惯是:
- 对于LED这类不需要高精度的,用8位就够了
- 对于舵机、电机这类需要精细控制的,用16位
- 对于音频或者特殊波形,可能需要用到更高的分辨率
另外,ESP32的LEDC模块支持渐变功能(ledcFade),可以自动完成占空比的平滑变化,不需要在主循环里用delay。这在做呼吸灯或者电机缓启动时特别有用。
避坑指南:我曾经在一个项目里同时用了多个PWM通道,结果发现某些通道的输出不稳定。排查了半天,发现是多个通道共用了同一个定时器。ESP32的LEDC模块有4个定时器,每个定时器可以绑定两个通道。如果两个通道的频率不同,就不能共用同一个定时器。所以配置的时候要注意定时器的分配。
3.6 小结
PWM是嵌入式开发中绕不开的一个知识点。从LED呼吸灯到舵机控制,从电机调速到音频输出,到处都有它的身影。ESP32的LEDC模块功能强大,配置灵活,用好它能让你的项目事半功倍。
下一章我们会讲ADC与模拟输入,到时候你会看到,数字世界和模拟世界之间的桥梁,不止PWM这一座。咱们下节课见。