4、定时器与PWM深入:硬件定时器配置、PWM频率与占空比控制、多通道PWM输出、RGB LED调色
好,咱们这一章来啃一块硬骨头——定时器与PWM。说实话,很多初学者一听到“硬件定时器”就觉得头大,觉得那是底层驱动工程师才该操心的事。但我要告诉你,玩ESP32,不懂定时器和PWM,你连个呼吸灯都做不漂亮。
我个人习惯把定时器比作一个“倒计时的闹钟”。你设好时间,它到点就响。但ESP32的闹钟比你家那个智能多了——它能精确到微秒级,还能自动触发各种动作。而PWM呢,说白了就是“快速开关灯”,快到人眼看不出来,从而模拟出不同的亮度。
核心概念速览:
- 硬件定时器:ESP32内置4个64位定时器,精度极高
- PWM:脉冲宽度调制,通过占空比控制输出功率
- 频率:PWM每秒钟的周期数,单位Hz
- 占空比:一个周期内高电平所占的比例
4.1 硬件定时器配置——别用delay()了
我在项目中遇到过最坑的事,就是有人用delay()来做定时任务。你想想看,delay()一执行,整个CPU都在那傻等,啥也干不了。这在灯控系统里简直是灾难——你这边在delay,那边用户按了开关,系统根本没反应。
ESP32的硬件定时器就不一样了。它独立于CPU运行,到点触发中断,CPU该干嘛干嘛。配置起来其实不复杂,我带你走一遍:
#include "esp_timer.h"
// 定时器回调函数
void timer_callback(void *arg) {
// 这里放你要定时执行的代码
// 比如:切换LED状态、读取传感器等
static bool led_state = false;
led_state = !led_state;
digitalWrite(LED_BUILTIN, led_state);
}
void setup() {
// 创建定时器
esp_timer_handle_t timer;
esp_timer_create_args_t timer_args = {
.callback = &timer_callback,
.arg = NULL,
.name = "my_timer"
};
esp_timer_create(&timer_args, &timer);
// 启动定时器,每1000ms执行一次
esp_timer_start_periodic(timer, 1000000); // 单位是微秒
}
嗯,这里要注意:回调函数里不要做耗时操作,比如Serial.print()大量数据。定时器中断讲究的是“快进快出”,你拖太久会影响系统实时性。
我的小技巧:调试定时器时,我习惯先用一个LED闪烁来验证周期是否准确。肉眼看不出来?用逻辑分析仪抓一下,一清二楚。
4.2 PWM频率与占空比控制——调光的基础
PWM控制LED亮度,原理其实很简单。频率决定了LED会不会闪烁,占空比决定了亮度。人眼对50Hz以上的闪烁就不敏感了,但为了更平滑,我一般用1000Hz以上。
ESP32的LEDC(LED Control)外设就是专门干这个的。它内置了16个通道,可以独立配置频率和占空比。来看个例子:
// LEDC通道配置
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_FREQ 5000 // 5kHz,足够平滑
#define LEDC_RESOLUTION 8 // 8位分辨率,0-255
void setup() {
// 配置定时器
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 = {
.gpio_num = 2, // GPIO2
.speed_mode = LEDC_HIGH_SPEED_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.duty = 0,
.hpoint = 0
};
ledc_channel_config(&channel_conf);
}
void loop() {
// 从暗到亮
for(int duty = 0; duty <= 255; duty++) {
ledc_set_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL);
delay(10);
}
}
分辨率这个参数很多人搞不明白。8位分辨率意味着占空比可以从0到255共256级。如果你想要更细腻的调光,可以用10位(0-1023)甚至12位。但分辨率越高,能支持的最高频率就越低,这是个取舍。
我曾经踩过的坑:有一次我设了16位分辨率,频率设到40kHz,结果PWM输出完全不对。后来查手册才发现,ESP32的LEDC在高分辨率下,时钟源有限,频率上不去。所以一般场景8位5kHz就够用了。
4.3 多通道PWM输出——同时控制多个灯
一个灯太单调了。实际项目中,你往往要同时控制好几个灯——走廊灯、床头灯、氛围灯。ESP32的LEDC有16个通道,理论上可以同时输出16路独立的PWM。
多通道配置其实就是在上面代码的基础上,多配几个通道而已。但要注意一点:共用同一个定时器的通道,频率必须相同。如果你需要不同频率的PWM,就得用不同的定时器。
// 配置两个通道,共用定时器0
// 通道0 -> GPIO2
// 通道1 -> GPIO4
ledc_channel_config_t ch0_conf = {
.gpio_num = 2,
.channel = LEDC_CHANNEL_0,
.timer_sel = LEDC_TIMER_0,
.duty = 0,
// ... 其他参数同上
};
ledc_channel_config(&ch0_conf);
ledc_channel_config_t ch1_conf = {
.gpio_num = 4,
.channel = LEDC_CHANNEL_1,
.timer_sel = LEDC_TIMER_0, // 同一个定时器
.duty = 0,
// ...
};
ledc_channel_config(&ch1_conf);
// 分别控制
ledc_set_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0, 128);
ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_0);
ledc_set_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_1, 64);
ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_1);
你看,代码结构很清晰。每个通道独立设置占空比,互不干扰。这就是硬件PWM的好处——CPU只需要更新数值,波形生成完全由硬件完成。
4.4 RGB LED调色——把理论变成彩虹
终于到了最有趣的部分。RGB LED其实就是把红、绿、蓝三个LED封装在一起。通过调节三路PWM的占空比,就能混合出任意颜色。
我刚开始做RGB调色时,犯过一个低级错误——把三个通道的频率设成了不同的值。结果颜色混出来一直在抖,像接触不良一样。后来才意识到,RGB三通道必须用同一个定时器,频率完全一致,否则混色会不稳定。
#define RED_PIN 25
#define GREEN_PIN 26
#define BLUE_PIN 27
#define CH_RED LEDC_CHANNEL_0
#define CH_GREEN LEDC_CHANNEL_1
#define CH_BLUE LEDC_CHANNEL_2
void setup() {
// 配置定时器(共用)
ledc_timer_config_t timer_conf = {
.speed_mode = LEDC_HIGH_SPEED_MODE,
.timer_num = LEDC_TIMER_0,
.duty_resolution = 8,
.freq_hz = 5000,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer_conf);
// 配置三个通道
ledc_channel_config_t ch_conf;
ch_conf.speed_mode = LEDC_HIGH_SPEED_MODE;
ch_conf.timer_sel = LEDC_TIMER_0;
ch_conf.duty = 0;
ch_conf.hpoint = 0;
ch_conf.gpio_num = RED_PIN;
ch_conf.channel = CH_RED;
ledc_channel_config(&ch_conf);
ch_conf.gpio_num = GREEN_PIN;
ch_conf.channel = CH_GREEN;
ledc_channel_config(&ch_conf);
ch_conf.gpio_num = BLUE_PIN;
ch_conf.channel = CH_BLUE;
ledc_channel_config(&ch_conf);
}
void setColor(int r, int g, int b) {
ledc_set_duty(LEDC_HIGH_SPEED_MODE, CH_RED, r);
ledc_set_duty(LEDC_HIGH_SPEED_MODE, CH_GREEN, g);
ledc_set_duty(LEDC_HIGH_SPEED_MODE, CH_BLUE, b);
ledc_update_duty(LEDC_HIGH_SPEED_MODE, CH_RED);
ledc_update_duty(LEDC_HIGH_SPEED_MODE, CH_GREEN);
ledc_update_duty(LEDC_HIGH_SPEED_MODE, CH_BLUE);
}
void loop() {
// 呼吸灯效果:红色渐变
for(int i = 0; i <= 255; i++) {
setColor(i, 0, 0);
delay(5);
}
for(int i = 255; i >= 0; i--) {
setColor(i, 0, 0);
delay(5);
}
// 彩虹渐变
for(int i = 0; i < 360; i++) {
// HSV转RGB,这里简化处理
// 实际项目中可以用查表法提高效率
setColor(
(sin(i * 0.0174533) * 127 + 128),
(sin((i + 120) * 0.0174533) * 127 + 128),
(sin((i + 240) * 0.0174533) * 127 + 128)
);
delay(10);
}
}
实用建议:如果你想要更丰富的颜色效果,建议用HSV色彩空间而不是RGB。HSV的色相、饱和度、明度三个参数更符合人的直觉。网上有很多HSV转RGB的算法,直接拿来用就行。
4.5 本章小结
这一章内容不少,但都是硬核干货。我们来捋一捋重点:
| 知识点 | 关键点 | 常见坑 |
|---|---|---|
| 硬件定时器 | 独立于CPU运行,微秒级精度 | 回调函数里别做耗时操作 |
| PWM配置 | 频率、分辨率、占空比三者相互制约 | 高分辨率下频率上不去 |
| 多通道输出 | 共用定时器则频率相同 | 不同频率需不同定时器 |
| RGB调色 | 三通道频率必须一致 | 混色不稳定检查频率 |
下一章我们会把这些知识串起来,做一个完整的智能灯控系统。到时候你会看到,定时器负责定时切换模式,PWM负责调光调色,两者配合得天衣无缝。嗯,先消化好这一章的内容,后面就轻松了。
课后小练习:试着用三个按键分别控制RGB三路PWM的占空比,做一个手动调色器。按一下增加10%亮度,长按快速增加。这个练习能帮你巩固多通道控制和按键消抖的知识。