第四章:定时器与PWM——硬件定时器配置、PWM原理、呼吸灯实验

各位同学,今天我们来聊聊嵌入式开发里两个非常基础但又极其重要的东西——定时器和PWM。说实话,我刚开始学单片机那会儿,觉得定时器就是个“倒计时”的工具,PWM就是“调亮度”的。但后来做项目多了才发现,这两个东西的玩法远比想象中丰富。

嗯,咱们先从定时器讲起。

4.1 硬件定时器:不只是“计时”那么简单

ESP32内部有多个硬件定时器,每个定时器都是独立的64位计数器。你想想看,64位意味着什么?2^64次方,这个数字大到几乎不可能溢出。我在做工业控制项目时,就特别喜欢用这个特性来做长时间的精确定时,不用像8位单片机那样频繁处理溢出中断。

4.1.1 定时器的核心参数

配置一个定时器,说白了就是告诉它三件事:

  • 分频系数(divider):ESP32的主频通常是80MHz或240MHz,你得先分频到合适的频率。我个人习惯用80分频,这样每个计数单位就是1微秒,计算起来特别方便。
  • 计数方向:向上计数还是向下计数?大部分场景用向上计数,从0开始加到目标值。
  • 自动重载:计到目标值后,是停下来还是自动重新开始?做周期性任务时一定要开启自动重载。

关键点:ESP32的定时器是64位的,但配置寄存器时要注意高低32位分开写。我曾经在这里踩过坑——直接写64位数据,结果高32位没写进去,定时器跑飞了。

4.1.2 代码实战:配置一个1ms中断定时器

来看一段实际代码。这个例子我经常用在按键消抖和LED扫描中:

#include "esp_timer.h"
#include "driver/timer.h"

// 定时器配置结构体
timer_config_t config = {
    .divider = 80,          // 80分频,1计数=1us
    .counter_dir = TIMER_COUNT_UP,
    .counter_en = TIMER_PAUSE,
    .alarm_en = TIMER_ALARM_EN,
    .auto_reload = TIMER_AUTORELOAD_EN,
};

void timer_init(void) {
    // 初始化定时器0
    timer_init(TIMER_GROUP_0, TIMER_0, &config);
    
    // 设置计数器初始值
    timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0);
    
    // 设置报警值:1000计数 = 1ms
    timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, 1000);
    
    // 注册中断服务
    timer_isr_callback_add(TIMER_GROUP_0, TIMER_0, timer_isr_handler, NULL, 0);
    
    // 启动定时器
    timer_start(TIMER_GROUP_0, TIMER_0);
}

// 中断回调函数
bool IRAM_ATTR timer_isr_handler(void *args) {
    static uint32_t tick_count = 0;
    tick_count++;
    // 这里可以放你的定时任务
    return true;
}

我的小技巧:中断回调函数里尽量少做事,只做标志位设置或简单计数。真正耗时的处理放到主循环里。不然中断嵌套多了,系统响应会变慢。

4.2 PWM原理:从“开关”到“模拟”的魔法

PWM,全称是脉冲宽度调制。说白了,就是通过快速开关信号,让负载感受到一个“平均”的效果。你想想看,LED灯如果以极快的速度亮灭,人眼是看不出来的,只会觉得亮度变了。

这里有几个关键参数:

  • 频率:每秒开关多少次。控制LED时,频率要高于100Hz,不然会看到闪烁。我一般用1kHz以上。
  • 占空比:高电平时间占整个周期的比例。0%就是全灭,100%就是全亮。
  • 分辨率:占空比可以调节的精细程度。8位分辨率就是0-255共256级。

注意:ESP32的PWM频率和分辨率是相互制约的。想要高分辨率,频率就得降低。我在做电机控制时,需要20kHz的PWM频率,分辨率就只能降到10位(1024级)。

4.3 呼吸灯实验:从理论到实践

好了,理论知识讲完了,咱们来做个好玩的——呼吸灯。这个实验我每次教新人都必做,因为它完美展示了定时器和PWM的配合使用。

4.3.1 实验原理

呼吸灯的效果就是LED从暗到亮,再从亮到暗,循环往复。实现思路很简单:

  1. 用定时器产生一个周期性的中断,比如每10ms一次。
  2. 每次中断里,改变PWM的占空比。
  3. 占空比从0逐渐增加到255,再从255逐渐减小到0。

4.3.2 完整代码实现

#include "driver/ledc.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define LED_GPIO      2
#define PWM_CHANNEL   LEDC_CHANNEL_0
#define PWM_TIMER     LEDC_TIMER_0

// PWM配置
ledc_channel_config_t ledc_channel = {
    .channel    = PWM_CHANNEL,
    .duty       = 0,
    .gpio_num   = LED_GPIO,
    .speed_mode = LEDC_HIGH_SPEED_MODE,
    .timer_sel  = PWM_TIMER,
};

ledc_timer_config_t ledc_timer = {
    .duty_resolution = LEDC_TIMER_8_BIT,  // 8位分辨率
    .freq_hz         = 1000,               // 1kHz频率
    .speed_mode      = LEDC_HIGH_SPEED_MODE,
    .timer_num       = PWM_TIMER,
};

void breath_led_task(void *pvParameters) {
    int duty = 0;
    int direction = 1;  // 1:增加, -1:减少
    
    // 初始化PWM
    ledc_timer_config(&ledc_timer);
    ledc_channel_config(&ledc_channel);
    
    while(1) {
        // 设置占空比
        ledc_set_duty(ledc_channel.speed_mode, ledc_channel.channel, duty);
        ledc_update_duty(ledc_channel.speed_mode, ledc_channel.channel);
        
        // 改变占空比
        duty += direction * 5;  // 每次变化5级,让呼吸更平滑
        
        // 边界判断
        if(duty >= 255) {
            duty = 255;
            direction = -1;
        } else if(duty <= 0) {
            duty = 0;
            direction = 1;
        }
        
        // 延时30ms
        vTaskDelay(pdMS_TO_TICKS(30));
    }
}

void app_main(void) {
    xTaskCreate(breath_led_task, "breath_led", 2048, NULL, 5, NULL);
}

运行效果:LED会像呼吸一样,从暗到亮,再从亮到暗,循环周期大约2秒。你可以调整duty的变化步长和vTaskDelay的延时时间来改变呼吸速度。

4.3.3 避坑指南

做这个实验时,我遇到过几个常见问题:

  • LED不亮:检查GPIO引脚是否正确,ESP32的GPIO2默认是板载LED,但有些开发板可能不同。
  • 呼吸不平滑:占空比变化步长太大,建议用3-5的步长,配合30-50ms的延时。
  • 频率太低看到闪烁:PWM频率至少设到500Hz以上,我一般用1kHz。

进阶玩法:你可以用两个定时器,一个控制呼吸周期,一个控制PWM输出。这样呼吸效果会更自然,而且CPU占用更低。我在做智能家居的氛围灯时就是这么干的。

4.4 本章小结

这一章我们学了:

  • ESP32硬件定时器的配置方法,包括分频、计数方向、自动重载等关键参数。
  • PWM的工作原理,频率、占空比、分辨率三者的关系。
  • 通过呼吸灯实验,把定时器和PWM结合起来使用。

说实话,定时器和PWM是嵌入式开发里最常用的外设之一。你以后做电机控制、LED调光、音频输出,甚至通信协议模拟,都离不开它们。建议你把今天的代码自己动手跑一遍,然后试着改改参数,看看效果有什么变化。

下一章我们会讲中断系统,到时候你会看到定时器和中断是如何完美配合的。嗯,今天就到这里,有问题随时问我。