4. 定时器与PWM输出:ESP Timer API使用、硬件定时器配置、LEDC实现PWM调光、呼吸灯实验

定时器和PWM,这两个东西在嵌入式开发里太常用了。说白了,定时器就是芯片的“心跳”,PWM就是靠这个心跳模拟出来的“假模拟信号”。这一章,咱们就把它俩彻底搞明白。

4.1 硬件定时器:ESP32的“节拍器”

ESP32内部有4组64位通用定时器,每组都支持独立配置。我个人习惯把它们分成两类:一类是周期性中断,用来做固定频率的任务调度;另一类是单次定时,用来做超时检测。

定时器特性 说明
定时器数量 4组(TIMG0 ~ TIMG3)
计数器位宽 64位(可配置为16/32/64位)
时钟源 APB_CLK(默认80MHz)或XTAL_CLK
工作模式 递增/递减/自动重载
中断能力 支持边沿触发和电平触发

嗯,这里要注意:ESP32的定时器时钟源默认是APB_CLK,也就是80MHz。如果你改了CPU频率,APB_CLK也会跟着变,定时器的实际精度就会受影响。我在项目里踩过这个坑——调试时好好的,一上量产固件,定时周期全偏了。后来查了半天才发现是CPU频率配置不一样。

4.2 ESP Timer API:新框架,更顺手

ESP-IDF从v4.4开始推荐使用esp_timer这个高层API。它底层其实还是用硬件定时器,但帮你封装好了回调、优先级、任务上下文这些麻烦事。

核心API一览:

  • esp_timer_create() — 创建定时器句柄
  • esp_timer_start_periodic() — 启动周期性定时
  • esp_timer_start_once() — 启动单次定时
  • esp_timer_stop() — 停止定时器
  • esp_timer_delete() — 删除定时器

为什么推荐用这个?因为它的回调是在高优先级任务里执行的,不会被其他用户任务阻塞。你想想看,如果定时器回调被一个延时函数卡住,那整个时序就乱套了。

// 一个简单的周期性定时器示例
#include "esp_timer.h"

void my_timer_callback(void* arg) {
    // 这里放你的定时任务
    // 注意:不要做阻塞操作,不要调用 vTaskDelay()
    static uint32_t count = 0;
    count++;
    // 比如翻转一个GPIO
    gpio_set_level(LED_GPIO, count % 2);
}

void app_main(void) {
    const esp_timer_create_args_t timer_args = {
        .callback = &my_timer_callback,
        .arg = NULL,
        .name = "my_periodic_timer"
    };

    esp_timer_handle_t timer_handle;
    ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle));
    ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle, 1000000)); // 1秒 = 1,000,000微秒
}

我的经验:回调函数里千万别做malloc、printf、vTaskDelay这些操作。我曾经在回调里加了个打印调试信息,结果系统直接崩溃——因为printf本身会占用中断上下文,而esp_timer的回调是在任务上下文里跑的,但优先级很高,一旦阻塞就会导致看门狗复位。

4.3 LEDC:ESP32的PWM“专业户”

LEDC(LED Controller)是ESP32专门为PWM输出设计的硬件模块。它支持8个通道,每个通道可以独立配置频率和占空比。说白了,它就是专门干“调光”这活的。

LEDC的核心参数有三个:

  • 频率(freq) — PWM波的频率,人眼能感知的闪烁大约是50Hz以下,所以调光一般用1kHz以上
  • 分辨率(duty_resolution) — 占空比的精度,比如10位分辨率就是0~1023
  • 定时器(timer) — LEDC内部有4个定时器,多个通道可以共用同一个定时器
参数 推荐值 说明
PWM频率 1kHz ~ 5kHz 调光用,太高了MOS管开关损耗大
占空比分辨率 10位 ~ 13位 位数越高,调光越细腻
定时器时钟源 LEDC_USE_APB_CLK 默认80MHz,够用

4.4 呼吸灯实验:从亮到暗,再从暗到亮

呼吸灯的原理很简单:让PWM的占空比按照正弦波或者三角波变化。我一般用三角波,因为计算简单,效果也够平滑。

具体步骤是这样的:

  1. 配置LEDC定时器和通道
  2. 设置PWM频率为1kHz,分辨率为10位
  3. 在主循环里,让占空比从0递增到1023,再从1023递减到0
  4. 每次变化后加一个短延时,控制呼吸速度
#include "driver/ledc.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define LEDC_TIMER      LEDC_TIMER_0
#define LEDC_MODE       LEDC_LOW_SPEED_MODE
#define LEDC_CHANNEL    LEDC_CHANNEL_0
#define LEDC_GPIO       GPIO_NUM_2
#define LEDC_FREQ_HZ    1000
#define LEDC_RESOLUTION LEDC_TIMER_10_BIT

void app_main(void) {
    // 配置定时器
    ledc_timer_config_t timer_conf = {
        .speed_mode = LEDC_MODE,
        .timer_num = LEDC_TIMER,
        .duty_resolution = LEDC_RESOLUTION,
        .freq_hz = LEDC_FREQ_HZ,
        .clk_cfg = LEDC_USE_APB_CLK
    };
    ESP_ERROR_CHECK(ledc_timer_config(&timer_conf));

    // 配置通道
    ledc_channel_config_t channel_conf = {
        .gpio_num = LEDC_GPIO,
        .speed_mode = LEDC_MODE,
        .channel = LEDC_CHANNEL,
        .timer_sel = LEDC_TIMER,
        .duty = 0,
        .hpoint = 0
    };
    ESP_ERROR_CHECK(ledc_channel_config(&channel_conf));

    // 呼吸灯主循环
    int duty = 0;
    int step = 5;  // 步进值,控制呼吸速度
    while (1) {
        // 渐亮
        for (duty = 0; duty <= 1023; duty += step) {
            ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
            ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
            vTaskDelay(pdMS_TO_TICKS(10));
        }
        // 渐暗
        for (duty = 1023; duty >= 0; duty -= step) {
            ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
            ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
            vTaskDelay(pdMS_TO_TICKS(10));
        }
    }
}

避坑提醒:调用ledc_set_duty()之后,一定要调用ledc_update_duty(),否则占空比不会生效。我刚开始用LEDC时,调了半天灯都不亮,最后发现是忘了调用update函数。这个坑,我估计你也可能会遇到。

4.5 知识体系:定时器与PWM的关系

下面这张图,把这一章的核心逻辑串起来了。你看一遍,应该就能明白ESP32里定时器和PWM是怎么配合工作的。

ESP32 定时器与PWM知识体系 时钟源 (APB_CLK 80MHz) 硬件定时器 (TIMG0 ~ TIMG3) 64位计数器 · 递增/递减/自动重载 ESP Timer API (高层封装) 周期性/单次 · 高优先级回调 LEDC PWM 控制器 8通道 · 独立频率/占空比 PWM 波形输出 (GPIO) 呼吸灯 (占空比渐变) 💡 核心思路 定时器提供时间基准 LEDC生成PWM波形 占空比变化实现呼吸效果

从这张图可以看得很清楚:时钟源是基础,硬件定时器是中间层,ESP Timer API和LEDC是两种不同的上层应用方式。ESP Timer适合做周期性任务调度,LEDC专门做PWM输出。呼吸灯实验,就是把LEDC的占空比按照一定规律变化而已。

我的建议:如果你只是做简单的LED调光,用LEDC就够了。但如果你需要精确的时间控制,比如做电机控制或者传感器采样,那就用ESP Timer。两者各司其职,别混着用。