4. ESP32 定时器与PWM:硬件定时器配置、PWM原理、LED呼吸灯实现、舵机控制实战

这一章,我们来啃两块硬骨头——硬件定时器和PWM。

说实话,这两样东西在嵌入式开发里太常用了。定时器是系统的“心跳”,PWM则是控制功率和位置的“手”。我刚开始做IoT项目时,总觉得用软件延时也能凑合,直到有一次做多路舵机控制,软件延时把整个系统卡得死死的……嗯,从那以后,我再也不敢轻视硬件定时器了。

本章核心目标:
  • 搞懂ESP32硬件定时器的工作原理与配置方法
  • 理解PWM信号的生成机制
  • 用PWM实现LED呼吸灯效果
  • 实战驱动SG90舵机

4.1 硬件定时器:ESP32的精准节拍器

ESP32内部有4组64位硬件定时器。每组都可以独立配置,精度高到微秒级。你想想看,软件延时靠CPU空转,多浪费资源。硬件定时器就不一样了——它自己跑自己的,到时间了给你发个中断,CPU该干嘛干嘛。

4.1.1 定时器核心参数

参数 说明 典型值
分频器(divider) 将APB时钟(80MHz)分频 80 → 1MHz计数频率
计数方向 递增或递减 递增(默认)
报警值(alarm value) 触发中断的计数值 1000 → 1ms中断
自动重载 到达报警值后是否自动复位 使能

我个人习惯把分频器设为80,这样计数频率就是1MHz,每个计数单位对应1微秒。算时间特别方便。

4.1.2 定时器配置实战

直接上代码。这个例子配置一个1ms中断的硬件定时器:

#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// 定时器回调函数
static void my_timer_callback(void* arg) {
    static uint32_t count = 0;
    count++;
    // 每1ms执行一次
    if (count % 1000 == 0) {
        printf("1秒到了!\n");
    }
}

void app_main(void) {
    // 创建定时器
    esp_timer_handle_t timer;
    esp_timer_create_args_t timer_args = {
        .callback = &my_timer_callback,
        .arg = NULL,
        .name = "my_timer"
    };
    
    ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer));
    
    // 启动定时器,周期1ms
    ESP_ERROR_CHECK(esp_timer_start_periodic(timer, 1000));  // 单位:微秒
    
    printf("定时器已启动,每1ms触发一次中断\n");
    
    // 主任务继续做其他事
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}
我的经验:定时器回调函数里不要做耗时操作。我曾经在里面写了个printf,结果中断嵌套把系统搞崩了。回调里只做标志位设置或数据采集,具体处理放到主循环或任务里。

4.2 PWM原理:从方波到模拟控制

PWM,全称脉冲宽度调制。说白了,就是通过调整方波的占空比,来模拟不同的电压或功率。

为什么需要PWM?因为数字芯片只能输出0和1。但很多设备需要“中间值”——比如LED的亮度、电机的转速、舵机的角度。PWM就是解决这个问题的。

4.2.1 关键参数

参数 含义 示例
频率(Frequency) 每秒脉冲个数 1000Hz → 周期1ms
占空比(Duty Cycle) 高电平时间占周期的比例 50% → 一半时间高电平
分辨率(Resolution) 占空比可调节的精细度 8位 → 0~255

你想想看,如果频率太低,LED会闪烁,电机会有噪音。频率太高,驱动电路又跟不上。我一般LED用1kHz,舵机用50Hz,电机用10kHz~20kHz。

4.2.2 ESP32的PWM实现

ESP32用LEDC(LED Control)模块来实现PWM。名字叫LEDC,但实际能控制舵机、电机、蜂鸣器,啥都行。

LEDC的配置流程很简单:

  1. 配置定时器:设置PWM的频率和分辨率
  2. 绑定通道:将定时器与PWM通道关联
  3. 设置占空比:输出想要的PWM波形

来看代码:

#include "driver/ledc.h"

#define LED_GPIO      2      // GPIO2,板载LED
#define LEDC_TIMER    LEDC_TIMER_0
#define LEDC_CHANNEL  LEDC_CHANNEL_0
#define LEDC_FREQ     1000   // 1kHz
#define LEDC_RESOLUTION LEDC_TIMER_8_BIT  // 8位分辨率

void pwm_init(void) {
    // 1. 配置定时器
    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);
    
    // 2. 配置通道
    ledc_channel_config_t channel_conf = {
        .gpio_num = LED_GPIO,
        .speed_mode = LEDC_HIGH_SPEED_MODE,
        .channel = LEDC_CHANNEL,
        .timer_sel = LEDC_TIMER,
        .duty = 0,
        .hpoint = 0
    };
    ledc_channel_config(&channel_conf);
}

void set_pwm_duty(uint32_t duty) {
    ledc_set_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL, duty);
    ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL);
}
注意:设置占空比后,一定要调用ledc_update_duty(),否则不会生效。我刚开始用的时候忘了这步,调了半天发现LED死活不亮……

4.3 LED呼吸灯实现

呼吸灯,就是让LED的亮度从暗到亮、再从亮到暗,循环变化。说白了,就是不断改变PWM的占空比。

实现思路:

  • 用定时器每10ms更新一次占空比
  • 占空比从0递增到255,再从255递减到0
  • 循环往复
void breath_led_task(void *arg) {
    int duty = 0;
    int step = 5;  // 步进值,控制变化速度
    
    while (1) {
        // 渐亮
        for (duty = 0; duty <= 255; duty += step) {
            set_pwm_duty(duty);
            vTaskDelay(pdMS_TO_TICKS(10));
        }
        // 渐暗
        for (duty = 255; duty >= 0; duty -= step) {
            set_pwm_duty(duty);
            vTaskDelay(pdMS_TO_TICKS(10));
        }
    }
}

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

效果怎么样?LED会像呼吸一样平滑变化。步进值调小一点,变化更细腻;调大一点,变化更快。我个人喜欢步进值设为3,看起来最自然。

4.4 舵机控制实战

舵机,说白了就是一个可以精确控制角度的电机。机器人、云台、智能锁,到处都有它的身影。

4.4.1 舵机控制原理

标准舵机(如SG90)的控制信号就是PWM:

  • 周期:20ms(频率50Hz)
  • 高电平宽度决定角度:
角度 高电平宽度 占空比(8位分辨率)
0° 0.5ms 约2.5% → duty≈6
90° 1.5ms 约7.5% → duty≈19
180° 2.5ms 约12.5% → duty≈32

这里有个坑:不同品牌的舵机,脉宽范围可能不一样。我遇到过一款舵机,0°对应0.6ms,180°对应2.4ms。所以拿到舵机先看数据手册,别想当然。

4.4.2 舵机驱动代码

#define SERVO_GPIO     4
#define SERVO_FREQ     50     // 50Hz
#define SERVO_RESOLUTION LEDC_TIMER_10_BIT  // 10位分辨率,0~1023

// 角度转占空比
uint32_t angle_to_duty(int angle) {
    // 0° → duty≈51 (0.5ms)
    // 180° → duty≈256 (2.5ms)
    // 线性映射
    return (uint32_t)(51 + (angle * 205) / 180);
}

void servo_init(void) {
    ledc_timer_config_t timer_conf = {
        .speed_mode = LEDC_HIGH_SPEED_MODE,
        .timer_num = LEDC_TIMER_1,
        .duty_resolution = SERVO_RESOLUTION,
        .freq_hz = SERVO_FREQ,
        .clk_cfg = LEDC_AUTO_CLK
    };
    ledc_timer_config(&timer_conf);
    
    ledc_channel_config_t channel_conf = {
        .gpio_num = SERVO_GPIO,
        .speed_mode = LEDC_HIGH_SPEED_MODE,
        .channel = LEDC_CHANNEL_1,
        .timer_sel = LEDC_TIMER_1,
        .duty = angle_to_duty(90),  // 初始90°
        .hpoint = 0
    };
    ledc_channel_config(&channel_conf);
}

void servo_set_angle(int angle) {
    if (angle < 0) angle = 0;
    if (angle > 180) angle = 180;
    
    uint32_t duty = angle_to_duty(angle);
    ledc_set_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_1, duty);
    ledc_update_duty(LEDC_HIGH_SPEED_MODE, LEDC_CHANNEL_1);
}
避坑指南:舵机转动需要时间。如果你连续快速设置多个角度,舵机会跟不上。我一般每次设置后至少等500ms,让舵机转到位置再发下一个指令。

4.5 本章知识结构

下面这张图,把定时器、PWM、LED呼吸灯、舵机控制的关系理清楚了:

ESP32 定时器与PWM知识结构 硬件定时器 PWM 脉冲宽度调制 LED 呼吸灯 舵机控制 占空比渐变 定时器每10ms更新 50Hz PWM信号 脉宽0.5ms~2.5ms 定时器驱动PWM → 控制外设

4.6 常见问题与调试技巧

做这几个实验,你可能会遇到一些问题。我把自己踩过的坑列出来:

问题 原因 解决方法
LED不亮 GPIO口不对或占空比为0 检查GPIO编号,确认duty>0
呼吸灯闪烁 PWM频率太低 频率提高到1kHz以上
舵机抖动 PWM频率不准或电源不足 确认50Hz,外接5V电源
舵机转不到180° 脉宽范围不对 用示波器实测,调整映射值
重要提醒:舵机启动瞬间电流很大,可能达到500mA以上。不要直接从ESP32的3.3V引脚取电,会烧板子。我一般用外部5V电源,共地就行。

好了,这一章的内容就到这里。定时器和PWM是嵌入式开发的基石,掌握了它们,后面做传感器驱动、电机控制、智能家居设备对接,都会轻松很多。动手把代码跑起来,有问题随时翻回来看看。