4. ESP32 定时器:从硬件定时器到呼吸灯实战

定时器这东西,说白了就是单片机的“心跳”。没有它,你的程序就像没戴手表的打工人——啥时候该干啥全凭感觉。ESP32 的定时器资源挺丰富,配置起来也灵活。我个人习惯把定时器分成两类:一类是纯粹的计时中断,另一类是生成 PWM 波形。今天咱们就把这两块都捋清楚。

本章核心脉络:硬件定时器配置 → 定时中断回调 → PWM 输出原理 → 呼吸灯实验。一条线走下来,你就能用定时器干点“真活”了。

4.1 硬件定时器:ESP32 的“心跳”单元

ESP32 内部有 4 个 64 位通用定时器,每个定时器都可以独立配置。它们挂在 APB 总线上,时钟源通常是 80 MHz(APB 时钟)。

为什么是 64 位?因为 32 位定时器在 80 MHz 下,最大计时才 53 秒左右。64 位能撑到几万年,做长时间计时不用愁溢出。我在项目中遇到过需要连续运行 72 小时的设备状态记录,用 64 位定时器就很从容。

参数 说明
定时器数量 4 个(timer0 ~ timer3)
位宽 64 位
时钟源 APB 时钟(默认 80 MHz)
工作模式 单次 / 自动重载
中断 可配置,支持回调函数

4.2 定时中断回调:让代码“按时”执行

配置定时中断其实就三步:初始化定时器、设置计数值和分频、注册中断回调。我刚开始用 ESP-IDF 时,总搞混 timer_config() 和 timer_set_counter_value() 的顺序。后来发现,先配置再设值,顺序错了定时器可能不工作。

来看一个典型的 1 秒定时中断示例:

#include "driver/timer.h"

#define TIMER_DIVIDER    80    // 80 MHz / 80 = 1 MHz, 即 1 us 一跳
#define TIMER_SCALE      (TIMER_BASE_CLK / TIMER_DIVIDER)  // 1 MHz

static void IRAM_ATTR timer_isr_handler(void *arg) {
    // 清除中断标志
    timer_group_clr_intr_status_in_isr(TIMER_GROUP_0, TIMER_0);
    timer_counter_value_clear(TIMER_GROUP_0, TIMER_0);

    // 你的定时任务放这里
    // 比如翻转一个 GPIO
    gpio_set_level(LED_GPIO, !gpio_get_level(LED_GPIO));
}

void timer_init_example(void) {
    timer_config_t config = {
        .divider = TIMER_DIVIDER,
        .counter_dir = TIMER_COUNT_UP,
        .counter_en = TIMER_PAUSE,
        .alarm_en = TIMER_ALARM_EN,
        .auto_reload = true,
    };
    timer_init(TIMER_GROUP_0, TIMER_0, &config);

    // 设置计数值:1 秒 = 1,000,000 次
    timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, 1000000);
    timer_enable_intr(TIMER_GROUP_0, TIMER_0);
    timer_isr_register(TIMER_GROUP_0, TIMER_0, timer_isr_handler, NULL, ESP_INTR_FLAG_IRAM, NULL);

    timer_start(TIMER_GROUP_0, TIMER_0);
}

注意:中断回调函数必须加 IRAM_ATTR 属性,否则在 Flash 加密或缓存未命中时会导致崩溃。我曾经因为这个 bug 排查了整整一个下午……

4.3 PWM 输出原理:用定时器“模拟”模拟信号

PWM 的全称是脉冲宽度调制。说白了,就是让 GPIO 以固定频率输出高低电平,通过改变高电平占一个周期的时间比例(占空比),来“假装”输出不同的电压。

ESP32 的 PWM 有两种实现方式:

  • 软件 PWM:用定时器中断手动翻转 GPIO。精度有限,但灵活。
  • 硬件 PWM(LEDC):ESP32 内置的 LED PWM 控制器,专门为 LED 调光和电机控制设计。精度高,不占 CPU。

我个人强烈推荐用硬件 PWM。原因很简单——不占 CPU。你想想看,如果呼吸灯用软件 PWM,CPU 得一直响应中断,别的活还干不干了?

特性 软件 PWM 硬件 PWM(LEDC)
CPU 占用 高(每周期中断) 低(硬件自动输出)
分辨率 受中断频率限制 最高 20 位
通道数 受定时器数量限制 8 个独立通道
适用场景 简单指示、低频输出 LED 调光、电机控制

4.4 呼吸灯实验:从原理到代码

呼吸灯的效果,就是让 LED 从暗到亮、再从亮到暗,循环往复。本质上就是 PWM 占空比从 0% 逐渐增加到 100%,再逐渐减小到 0%。

用 ESP32 的 LEDC 实现起来非常直接:

#include "driver/ledc.h"

#define LEDC_GPIO        GPIO_NUM_2
#define LEDC_CHANNEL     LEDC_CHANNEL_0
#define LEDC_TIMER       LEDC_TIMER_0
#define LEDC_FREQ        5000    // 5 kHz PWM 频率
#define LEDC_RESOLUTION  LEDC_TIMER_13_BIT  // 13 位分辨率,即 0~8191

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 = {
        .gpio_num = LEDC_GPIO,
        .speed_mode = LEDC_HIGH_SPEED_MODE,
        .channel = LEDC_CHANNEL,
        .timer_sel = LEDC_TIMER,
        .duty = 0,
        .hpoint = 0,
    };
    ledc_channel_config(&channel_conf);
}

void breath_led_task(void *arg) {
    int duty = 0;
    int step = 8;  // 步进值,控制呼吸速度

    while (1) {
        // 渐亮
        for (duty = 0; duty <= 8191; duty += step) {
            ledc_set_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL, duty);
            ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL);
            vTaskDelay(pdMS_TO_TICKS(10));
        }
        // 渐暗
        for (duty = 8191; duty >= 0; duty -= step) {
            ledc_set_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL, duty);
            ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL);
            vTaskDelay(pdMS_TO_TICKS(10));
        }
    }
}

调参技巧:步进值 step 控制呼吸速度,vTaskDelay 控制平滑度。step 越大呼吸越快,但会显得“跳变”。我个人喜欢 step=4、delay=5ms,效果最自然。

4.5 知识结构图:定时器与 PWM 的关系

下面这张图帮你理清硬件定时器、PWM 和呼吸灯之间的层级关系:

ESP32 定时器与 PWM 知识结构 硬件定时器 定时中断模式 PWM 输出模式 定时任务调度 (传感器采样、状态刷新) LED 调光 / 电机控制 (呼吸灯、舵机、直流电机) 推荐优先使用硬件 PWM(LEDC)

4.6 避坑指南与经验总结

做定时器和 PWM 开发,有几个坑我替你们踩过了:

  • 中断服务函数不要太长。ISR 里只做标志位设置或数据拷贝,别做延时、打印、复杂计算。否则系统会变得“卡顿”,甚至触发看门狗复位。
  • LEDC 的 duty 值不要超过分辨率上限。13 位分辨率对应 0~8191,你设个 10000 进去,结果可能不是你想要的。
  • 定时器分频系数要算清楚。80 MHz 时钟,分频 80 得到 1 MHz,也就是 1 微秒一跳。分频 1000 得到 80 kHz,每跳 12.5 微秒。算错了,时间就不准了。
  • 多定时器共用中断优先级。ESP32 的定时器中断默认优先级相同,如果两个定时器同时触发,后注册的可能被延迟。必要时可以调整中断优先级。

一句话总结:定时器是 ESP32 的“时间管理者”,PWM 是定时器的“打工仔”。用好 LEDC 硬件模块,呼吸灯这种效果就是几行代码的事。