4. PWM与模拟输出:LED呼吸灯、电机调速、PWM频率与占空比
说到PWM,我刚开始做嵌入式那会儿,总觉得它就是“让灯忽亮忽暗”的小把戏。直到有一次做直流电机项目,电机嗡嗡响就是不转,我才意识到——PWM这东西,玩不好真会翻车。
说白了,PWM就是通过快速开关信号,让负载“感觉”到的是一个平均电压。你开关得够快,负载就反应不过来,看到的、感受到的就是一个稳定的中间值。
4.1 PWM的核心参数:频率与占空比
这两个参数,是PWM的灵魂。我见过不少新手调呼吸灯,灯闪得跟警灯似的,其实就是频率没设对。
| 参数 | 定义 | 典型范围(ESP32) | 影响 |
|---|---|---|---|
| 频率 | 每秒开关次数(Hz) | 1 Hz ~ 40 MHz | 频率太低会闪烁,太高会增加开关损耗 |
| 占空比 | 高电平时间占整个周期的比例 | 0% ~ 100% | 决定等效输出电压的大小 |
我的经验:LED呼吸灯用1 kHz左右就够了,电机调速建议5 kHz~20 kHz。低于1 kHz人眼能感觉到闪烁,高于20 kHz电机铁损会明显增加。
4.2 ESP32的PWM硬件结构
ESP32的PWM模块叫LEDC(LED Controller),名字虽然叫LED,但驱动电机、舵机完全没问题。它内部有8个通道,每个通道可以独立设置频率和占空比。
注意:ESP32的LEDC通道虽然多,但定时器只有4个。如果你需要多个不同频率的PWM信号,记得合理分配定时器资源。我踩过这个坑——8个通道全用同一个定时器,结果电机和LED的频率需求冲突,折腾了半天。
4.3 LED呼吸灯实战
呼吸灯是PWM入门的经典项目。原理很简单:让占空比从0逐渐增加到100%,再逐渐减小到0%,循环往复。
#include <driver/ledc.h>
#define LED_GPIO 2
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_FREQ 1000 // 1 kHz
#define LEDC_RESOLUTION LEDC_TIMER_10_BIT // 10位分辨率
void ledc_init(void) {
ledc_timer_config_t timer_conf = {
.speed_mode = LEDC_HIGH_SPEED_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_RESOLUTION,
.freq_hz = LEDC_FREQ,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer_conf);
ledc_channel_config_t channel_conf = {
.channel = LEDC_CHANNEL,
.duty = 0,
.gpio_num = LED_GPIO,
.speed_mode = LEDC_HIGH_SPEED_MODE,
.hpoint = 0,
.timer_sel = LEDC_TIMER
};
ledc_channel_config(&channel_conf);
}
void breath_led(void) {
for (int duty = 0; duty < 1024; duty++) {
ledc_set_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL);
vTaskDelay(pdMS_TO_TICKS(2));
}
for (int duty = 1023; duty >= 0; duty--) {
ledc_set_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL);
vTaskDelay(pdMS_TO_TICKS(2));
}
}
效果:LED从暗到亮再到暗,周期约4秒。调整
vTaskDelay的延时可以改变呼吸速度。我个人习惯把延时控制在1~5ms之间,太短了CPU占用高,太长了呼吸不平滑。
4.4 直流电机调速实战
电机调速和LED呼吸灯原理一样,但有几个坑要注意。我曾经用PWM直接驱动一个12V电机,结果ESP32的GPIO直接烧了——因为电机启动电流太大。
正确的做法是:用PWM控制MOS管或电机驱动芯片(如L298N、TB6612),再由驱动芯片控制电机。
#define MOTOR_GPIO 4
#define MOTOR_CHANNEL LEDC_CHANNEL_1
#define MOTOR_TIMER LEDC_TIMER_1
#define MOTOR_FREQ 5000 // 5 kHz
#define MOTOR_RESOLUTION LEDC_TIMER_8_BIT // 8位分辨率
void motor_init(void) {
ledc_timer_config_t timer_conf = {
.speed_mode = LEDC_HIGH_SPEED_MODE,
.timer_num = MOTOR_TIMER,
.duty_resolution = MOTOR_RESOLUTION,
.freq_hz = MOTOR_FREQ,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer_conf);
ledc_channel_config_t channel_conf = {
.channel = MOTOR_CHANNEL,
.duty = 0,
.gpio_num = MOTOR_GPIO,
.speed_mode = LEDC_HIGH_SPEED_MODE,
.hpoint = 0,
.timer_sel = MOTOR_TIMER
};
ledc_channel_config(&channel_conf);
}
void motor_set_speed(int percent) {
if (percent < 0) percent = 0;
if (percent > 100) percent = 100;
int duty = (percent * 255) / 100;
ledc_set_duty(LEDC_HIGH_SPEED_MODE, MOTOR_CHANNEL, duty);
ledc_update_duty(LEDC_HIGH_SPEED_MODE, MOTOR_CHANNEL);
}
避坑指南:电机启动时,占空比不要从0直接跳到100%。我建议做一个软启动——每50ms增加5%的占空比。这样电机电流不会瞬间飙升,电源也不会被拉垮。
4.5 频率选择的实战经验
频率选不对,项目白费。我整理了一个速查表,你直接照着用就行。
| 应用场景 | 推荐频率 | 分辨率 | 说明 |
|---|---|---|---|
| LED呼吸灯 | 1 kHz | 10位 | 人眼无闪烁,平滑度高 |
| 直流电机调速 | 5~20 kHz | 8位 | 避免人耳听到啸叫 |
| 舵机控制 | 50 Hz | 10位 | 标准舵机周期20ms |
| 加热器控制 | 1~10 Hz | 8位 | 热惯性大,低频即可 |
注意:频率越高,分辨率越低。ESP32的LEDC在40 MHz时钟下,如果设置20 kHz频率,最大分辨率只有11位。你想想看,分辨率不够的话,电机低速时可能一卡一卡的。
4.6 常见问题与故障定位
做PWM项目,我遇到过不少奇奇怪怪的问题。这里列几个典型的,你遇到了可以直接对号入座。
- LED不亮或一直亮:检查GPIO是否被其他外设占用,或者LEDC通道是否初始化成功。我习惯在初始化后读一下
ledc_get_duty()确认值对不对。 - 电机嗡嗡响但不转:频率太低,人耳能听到。把频率提高到10 kHz以上就好了。
- 电机转速不稳:电源供电不足。电机启动瞬间电流可能是额定电流的3~5倍,建议在电机驱动板电源端加一个大电容(470 μF以上)。
- PWM输出波形异常:用逻辑分析仪或示波器看波形。我遇到过定时器配置冲突导致波形错乱的情况,重新分配定时器就解决了。
我的调试习惯:先用一个LED做PWM输出测试,确认通道和定时器配置正确后,再接到电机或其它负载上。这样能快速排除软件配置问题。
嗯,PWM这部分内容就这些。记住频率和占空比这两个核心参数,再结合ESP32的LEDC硬件结构,大部分PWM应用你都能搞定。下次遇到PWM相关的问题,先检查定时器配置,再查通道绑定,基本就能定位到问题。