4、PWM与模拟输出:LEDC控制器原理,PWM频率与占空比设置,呼吸灯实验,舵机控制基础
说到模拟输出,很多刚接触单片机的朋友第一反应就是「DAC」。但ESP32上只有两个真正的DAC通道,而且精度有限。实际项目中,我们绝大多数「模拟」需求——调光、调速、舵机角度——都是用PWM搞定的。
ESP32的LEDC控制器,说白了就是一套硬件PWM发生器。它不只能控制LED,还能控制电机、舵机、蜂鸣器。我最早用ESP32做四轴飞行器飞控的时候,四个电机的PWM信号就是靠LEDC生成的,稳定得很。
4.1 LEDC控制器架构
LEDC模块内部有两层结构:定时器和通道。定时器决定PWM的频率和分辨率,通道决定哪个引脚输出以及占空比是多少。
每个定时器可以独立设置时钟源、分频系数和计数上限。通道则负责把定时器的计数值和用户设定的占空比阈值做比较,输出高电平或低电平。
关键参数速查表
| 参数 | 说明 | 典型值 |
|---|---|---|
| 频率 | PWM波形的周期倒数 | 50 Hz(舵机)、1 kHz(LED) |
| 占空比 | 高电平时间占整个周期的比例 | 0% ~ 100% |
| 分辨率 | 占空比可调节的级数 | 8 bit(256级)、10 bit(1024级) |
| 定时器时钟 | LEDC内部计数器的时钟源 | APB_CLK(80 MHz) |
4.2 PWM频率与占空比设置
设置PWM其实就三步:配定时器、绑通道、写占空比。我习惯把这三步封装成一个函数,项目里反复调用。
#include <driver/ledc.h>
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_MODE LEDC_LOW_SPEED_MODE
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_DUTY_RES LEDC_TIMER_13_BIT // 分辨率 13 bit
#define LEDC_FREQUENCY 5000 // 频率 5 kHz
void pwm_init(void) {
// 1. 配置定时器
ledc_timer_config_t timer_conf = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = LEDC_FREQUENCY,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer_conf);
// 2. 配置通道
ledc_channel_config_t channel_conf = {
.gpio_num = 2,
.speed_mode = LEDC_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.duty = 0,
.hpoint = 0
};
ledc_channel_config(&channel_conf);
}
void set_duty(uint32_t duty) {
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
}
这里有个坑——ledc_set_duty只是把值写进寄存器,不会立即生效。必须调用ledc_update_duty才会真正更新输出。我曾经在这个问题上卡了半小时,波形死活不变,后来才发现忘了调用更新函数。
频率与分辨率的关系
ESP32的LEDC定时器是固定位宽的计数器。分辨率越高(bit数越大),能达到的最高频率就越低。公式很简单:
最大频率 = 时钟源频率 / (2^分辨率)
比如80 MHz时钟、13 bit分辨率:80,000,000 / 8192 ≈ 9.7 kHz。想要更高频率?那就得降低分辨率。
4.3 呼吸灯实验
呼吸灯是PWM入门的经典实验。原理很简单:占空比从0逐渐增加到最大值,再从最大值逐渐减小到0,循环往复。
我最早做呼吸灯用的是vTaskDelay加循环,后来发现这样会阻塞任务。更好的做法是用定时器中断或者软件定时器来更新占空比。
void breath_led_task(void *pvParameters) {
uint32_t duty = 0;
int8_t direction = 1;
while (1) {
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
duty += direction * 10;
if (duty >= 8191) {
direction = -1;
} else if (duty <= 0) {
direction = 1;
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
步长设为10,延时10 ms,一个完整的呼吸周期大约1.6秒。你可以调整这两个参数来改变呼吸速度。步长越大、延时越短,呼吸越快。
注意占空比上限
13 bit分辨率下,占空比最大值是8191(2^13 - 1)。如果你设成8192,LED会突然熄灭——因为计数器溢出回零了。我曾经在调试一个调光灯具时遇到过这个问题,灯在最高亮度突然闪了一下,排查了半天才发现是占空比越界。
4.4 舵机控制基础
舵机控制是PWM的另一个典型应用。标准舵机使用50 Hz的PWM信号,周期20 ms。占空比对应角度:
| 角度 | 脉宽 | 占空比(50 Hz) |
|---|---|---|
| 0° | 1.0 ms | 5% |
| 90° | 1.5 ms | 7.5% |
| 180° | 2.0 ms | 10% |
注意,不同品牌的舵机对脉宽范围的定义略有差异。便宜的舵机可能0°对应0.5 ms,180°对应2.5 ms。我建议拿到舵机后先用示波器或者逻辑分析仪实测一下。
#define SERVO_FREQ 50
#define SERVO_DUTY_RES LEDC_TIMER_14_BIT // 16384 级
// 角度转占空比
uint32_t angle_to_duty(int angle) {
// 0° -> 1.0 ms, 180° -> 2.0 ms
// 50 Hz 周期 = 20 ms
// 14 bit 满值 = 16384
float pulse_ms = 1.0 + (angle / 180.0) * 1.0;
return (uint32_t)((pulse_ms / 20.0) * 16384);
}
void set_servo_angle(int angle) {
if (angle < 0) angle = 0;
if (angle > 180) angle = 180;
uint32_t duty = angle_to_duty(angle);
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
}
舵机控制要点
- 频率必须设为50 Hz,否则舵机会抖动或啸叫
- 分辨率建议用14 bit,角度控制更精细
- 改变角度后要给舵机留出响应时间(约200~500 ms)
- 不要频繁发送角度指令,舵机内部电机有机械惯性
说到舵机,我想起一个项目——用ESP32控制六足机器人。18个舵机同时工作,每个舵机都需要独立的PWM通道。LEDC最多支持16个通道,刚好够用。但要注意,通道多了之后,定时器资源要合理分配。我把所有舵机都挂在同一个定时器上,因为它们频率相同(50 Hz),只是占空比不同。
多通道共享定时器
如果你有多个舵机或者多个LED需要控制,可以把它们绑定到同一个定时器上。这样节省定时器资源,而且所有通道的频率完全一致。唯一的要求是——它们必须工作在相同的频率和分辨率下。
嗯,PWM这部分内容其实不难,但细节很多。频率、分辨率、占空比这三个参数搞清楚了,LEDC控制器就算掌握了。呼吸灯和舵机是两个最典型的应用场景,把这两个实验做一遍,PWM这块基本就通了。