4. PWM与模拟输出:让LED呼吸,让蜂鸣器唱歌

各位同学,今天我们来聊一个非常实用的技术——PWM。说白了,PWM就是让数字信号“假装”成模拟信号的一种手段。你可能会问:ESP32不是数字芯片吗?怎么输出模拟电压?嗯,这里就要靠PWM来帮忙了。

4.1 PWM原理:别被名字吓到

PWM的全称是Pulse Width Modulation,脉冲宽度调制。我刚开始学的时候,觉得这名字挺唬人。其实理解起来很简单:

  • 周期:信号重复一次的时间
  • 占空比:高电平时间占整个周期的比例
  • 频率:1秒内信号重复的次数

举个例子你就明白了。假设一个周期是1秒,如果高电平持续0.5秒,低电平0.5秒,那占空比就是50%。如果高电平0.8秒,低电平0.2秒,占空比就是80%。

核心概念:PWM不是真正的模拟输出,而是通过快速切换高低电平,让负载“感觉”到平均电压。人眼有视觉暂留效应,LED快速闪烁时,我们看到的其实是平均亮度。

我在项目中遇到过一个问题:用PWM控制电机时,频率选得太低,电机发出刺耳的嗡嗡声。后来把频率提高到20kHz以上,声音就消失了。你想想看,这就是PWM频率选择的重要性。

4.2 ESP32的PWM通道

ESP32的PWM功能比Arduino强不少。它内部有16个独立的PWM通道,每个通道都可以独立设置频率和占空比。我个人习惯把通道0-7留给LED控制,通道8-15留给电机或蜂鸣器,这样管理起来清晰。

参数 ESP32 PWM能力 说明
通道数 16个 独立配置,互不干扰
分辨率 1-16位 常用8位(0-255)或10位(0-1023)
频率范围 1Hz - 40MHz 实际常用1kHz-40kHz

小技巧:控制LED时,频率选1kHz左右就够了,人眼分辨不出闪烁。控制蜂鸣器时,频率要匹配音调,比如440Hz就是标准A音。

4.3 analogWrite()函数:简单粗暴

在Arduino生态里,analogWrite()是控制PWM的标准函数。但在ESP32上,情况有点不同。ESP32的Arduino核心库并没有直接提供analogWrite(),而是用ledc系列函数来实现PWM。

不过别担心,我给大家封装了一个好用的版本。你可以直接这样用:

// 设置PWM参数
ledcSetup(0, 1000, 8);    // 通道0,频率1kHz,分辨率8位
ledcAttachPin(2, 0);      // 将通道0绑定到GPIO2

// 输出PWM
ledcWrite(0, 128);        // 占空比50%,亮度一半

这段代码做了三件事:

  1. ledcSetup:配置通道0,频率1000Hz,分辨率8位(值范围0-255)
  2. ledcAttachPin:把通道0的信号输出到GPIO2引脚
  3. ledcWrite:设置占空比,128就是50%

注意:ESP32的GPIO引脚不是所有都能输出PWM。我建议使用GPIO2、4、5、12-19这些引脚,它们通常没问题。曾经有学员用了GPIO34,折腾半天没输出,后来才发现这个引脚只能输入不能输出。

4.4 实战:LED呼吸灯效果

呼吸灯是PWM最经典的入门实验。说白了,就是让LED从暗到亮,再从亮到暗,循环往复。来看代码:

int brightness = 0;
int fadeAmount = 5;

void setup() {
  ledcSetup(0, 1000, 8);
  ledcAttachPin(2, 0);
}

void loop() {
  ledcWrite(0, brightness);
  brightness = brightness + fadeAmount;
  
  if (brightness <= 0 || brightness >= 255) {
    fadeAmount = -fadeAmount;
  }
  
  delay(30);
}

这段代码的逻辑很直白:

  • brightness从0开始,每次增加5
  • 当达到255时,fadeAmount变成-5,开始递减
  • 回到0后,再变成+5,如此循环
  • delay(30)控制变化速度,数值越小呼吸越快

我个人习惯把delay改成30-50毫秒,这样呼吸效果最自然。太快了像频闪,太慢了像死机。

4.5 驱动有源蜂鸣器:让声音有音调

有源蜂鸣器内部自带振荡电路,给它一个高电平就会响。但如果我们用PWM控制,就能让它发出不同音调的声音。为什么会这样?因为蜂鸣器的音调取决于PWM的频率。

来看一个简单的音乐播放器:

// 定义音符频率(单位:Hz)
#define NOTE_C4  262
#define NOTE_D4  294
#define NOTE_E4  330
#define NOTE_F4  349
#define NOTE_G4  392
#define NOTE_A4  440
#define NOTE_B4  494
#define NOTE_C5  523

void setup() {
  ledcSetup(1, 1000, 8);    // 通道1给蜂鸣器
  ledcAttachPin(4, 1);      // GPIO4接蜂鸣器
}

void loop() {
  // 播放一段旋律:小星星
  playNote(NOTE_C4, 500);
  playNote(NOTE_C4, 500);
  playNote(NOTE_G4, 500);
  playNote(NOTE_G4, 500);
  playNote(NOTE_A4, 500);
  playNote(NOTE_A4, 500);
  playNote(NOTE_G4, 1000);
  delay(200);
}

void playNote(int frequency, int duration) {
  ledcWriteTone(1, frequency);  // 设置频率
  ledcWrite(1, 128);            // 50%占空比,音量适中
  delay(duration);
  ledcWrite(1, 0);              // 停止发声
  delay(50);
}

这里有个关键函数ledcWriteTone(),它会自动调整PWM频率到目标音调。占空比设成128(50%),声音最清晰。我试过100%占空比,声音反而发闷。

避坑指南:我曾经用有源蜂鸣器播放音乐,发现声音特别小。后来查资料才知道,有源蜂鸣器内部振荡电路会干扰PWM信号。解决办法是:在蜂鸣器两端并联一个100μF的电解电容,声音立马洪亮起来。

4.6 常见问题与调试技巧

做PWM实验时,你可能会遇到这些问题:

现象 可能原因 解决方法
LED不亮 引脚接错或PWM未启动 检查GPIO编号,确认ledcSetup已调用
LED闪烁明显 PWM频率太低 把频率提高到500Hz以上
蜂鸣器声音嘶哑 占空比不合适 占空比设在30%-70%之间
多个PWM互相干扰 通道号冲突 每个设备使用独立通道

嗯,这里要特别提醒:ESP32的PWM通道虽然多,但每个通道只能绑定一个引脚。如果你想让多个LED同时呼吸,就得用多个通道。我一般会写一个数组来管理所有通道,这样代码更整洁。

4.7 小结与作业

今天我们学了PWM的核心原理,掌握了ESP32上使用ledc函数控制PWM的方法。你想想看,从LED呼吸灯到蜂鸣器音乐,本质上都是通过调整频率和占空比来实现的。

课后作业:

  1. 用PWM控制RGB三色LED,实现颜色渐变效果(红→绿→蓝→紫)
  2. 编写一个简单的电子琴程序,用4个按键控制4个音调
  3. 尝试用PWM控制舵机(注意:舵机需要50Hz的PWM信号)

下一章我们会讲ADC与模拟输入,到时候就能用传感器采集真实世界的模拟信号了。做好准备,内容会更精彩。