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%亮度,长按快速增加。这个练习能帮你巩固多通道控制和按键消抖的知识。