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只是在以人眼无法察觉的速度切换亮灭。

实现思路很简单:

  1. 配置LEDC通道,频率选5000Hz(避开人眼闪烁感知)
  2. 设置分辨率为8位(0~255)
  3. 在主循环中逐渐改变占空比
  4. 到达边界后反向变化
// 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与模拟输出知识体系 PWM核心原理 频率(50Hz~40MHz) 分辨率(1~16位) 占空比(0~2ⁿ⁻¹) ESP32 LEDC控制器(16通道) LED呼吸灯(5000Hz, 8位) SG90舵机(50Hz, 8~16位) 频率×分辨率 ≤ 80MHz / 分频系数 舵机需独立供电,共地不共源 核心:占空比决定输出,频率决定响应

这张图把本章的知识点串成了一条线:从PWM原理出发,到ESP32的硬件实现,再到LED和舵机两个典型应用,最后落到参数配置的约束条件上。你写代码的时候,脑子里要有这张图——频率和分辨率怎么选,心里就有数了。

本章小结:

  • ESP32用LEDC实现PWM,需要手动配置通道、频率、分辨率
  • 呼吸灯用5000Hz+8位分辨率,人眼感觉最平滑
  • SG90舵机必须用50Hz,脉宽1~2ms对应0~180°
  • 频率和分辨率相互制约,配置前先算分频系数
  • 舵机独立供电,避免拉垮ESP32