定时器与 PWM 输出详解

引言

上一章我们学习了 GPIO 与中断机制,让 ESP32-S3 能够感知外部事件并响应。但在实际嵌入式项目中,我们常常需要精确的时间控制 ------比如每隔 100ms 采集一次传感器数据,或者输出特定频率的方波驱动舵机。这些需求都离不开定时器PWM 这两个核心外设。

本章将深入讲解 ESP32-S3 的硬件定时器(TIMG)和 LEDC 控制器(PWM),并结合 LED 呼吸灯和舵机控制两个实战案例,让你彻底掌握定时与调光的编程技巧。


一、硬件定时器基础

1.1 什么是硬件定时器

硬件定时器是 MCU 内部的一个独立硬件模块,它依靠自己的时钟源进行计数,不占用 CPU 资源。当计数值达到预设的阈值时,可以触发中断或执行特定操作。

ESP32-S3 内部集成了 4 个 64 位通用定时器(Timer Group 0 和 Timer Group 1,每组含 2 个定时器),每个定时器都可独立配置。

1.2 定时器的核心参数

参数 说明 典型值
分频系数(divider) 对 APB 时钟分频,降低计数频率 2~65536
计数方向 向上计数或向下计数 向上计数
自动重载(auto-reload) 计满后是否自动重新开始 使能
报警值(alarm value) 触发中断的目标计数值 依需求设定

ESP32-S3 的定时器时钟源为 APB 时钟(通常为 80 MHz),经过分频后得到计数时钟。例如 80 MHz 除以 80 得到 1 MHz,即每微秒计数一次。


二、定时器编程实战

2.1 定时器基本配置

使用 ESP-IDF 的 timer_group 驱动库来配置定时器,步骤如下:

c 复制代码
#include "esp_timer.h"
#include "driver/gptimer.h"

// 定时器回调函数
static bool IRAM_ATTR timer_callback(gptimer_handle_t timer,
                                     const gptimer_alarm_event_data_t *edata,
                                     void *user_data) {
    // 定时器中断中做轻量级处理
    // 实际业务通过信号量通知任务层
    BaseType_t high_task_awake = pdFALSE;
    SemaphoreHandle_t sem = (SemaphoreHandle_t)user_data;
    xSemaphoreGiveFromISR(sem, &high_task_awake);
    return (high_task_awake == pdTRUE);
}

void timer_example_init(void) {
    gptimer_handle_t gptimer = NULL;

    // 1. 配置定时器参数
    gptimer_config_t timer_config = {
        .clk_src = GPTIMER_CLK_SRC_DEFAULT,  // 默认时钟源
        .direction = GPTIMER_COUNT_UP,        // 向上计数
        .resolution_hz = 1 * 1000 * 1000,     // 分辨率:1 MHz(1 µs/步)
    };
    gptimer_new_timer(&timer_config, &gptimer);

    // 2. 配置报警
    gptimer_alarm_config_t alarm_config = {
        .alarm_count = 1000000,    // 1,000,000 次计数 = 1 秒
        .reload_count = 0,         // 重载值(自动重载时归零)
        .flags.auto_reload_on_alarm = true,  // 开启自动重载
    };
    gptimer_set_alarm_action(gptimer, &alarm_config);

    // 3. 注册回调函数
    SemaphoreHandle_t sem = xSemaphoreCreateBinary();
    gptimer_event_callbacks_t cbs = {
        .on_alarm = timer_callback,
    };
    gptimer_register_event_callbacks(gptimer, &cbs, sem);

    // 4. 使能并启动定时器
    gptimer_enable(gptimer);
    gptimer_start(gptimer);
}

2.2 软定时器(esp_timer)

除了硬件定时器,ESP-IDF 还提供了基于系统时钟的软定时器 API(esp_timer),用法更简单,精度为微秒级,适合大多数非实时苛刻场景:

c 复制代码
#include "esp_timer.h"

void periodic_timer_callback(void *arg) {
    // 此回调运行在任务上下文中,可以调用 printf!
    printf("1 秒时间到!\n");
}

void app_main(void) {
    const esp_timer_create_args_t timer_args = {
        .callback = &periodic_timer_callback,
        .name = "periodic_1s"
    };

    esp_timer_handle_t timer;
    esp_timer_create(&timer_args, &timer);
    esp_timer_start_periodic(timer, 1000000);  // 每秒触发一次

    // 也可以启动单次定时器:
    // esp_timer_start_once(timer, 5000000);   // 5 秒后触发一次
}

何时用硬件定时器,何时用软定时器?

场景 推荐
高精度时序控制(PWM、步进电机) 硬件定时器
中断中做精确延迟 硬件定时器
周期性任务(数据采集、状态轮询) 软定时器
超时管理、延迟调度 软定时器

三、LEDC 控制器:ESP32-S3 的 PWM 外设

3.1 什么是 PWM

PWM(Pulse Width Modulation,脉宽调制)通过调节方波的占空比来模拟模拟量输出。占空比越高,等效电压越高。

r 复制代码
高电平时间 ──┐
             │    ┌────────┐    ┌────────┐
             │    │        │    │        │
             └────┘        └────┘        └────
              ← 周期 T →
              占空比 = 高电平时间 / T
占空比 LED 亮度 舵机角度
0% 熄灭
50% 半亮 90°
100% 最亮 180°

3.2 LEDC 控制器架构

ESP32-S3 的 LEDC(LED Controller) 是一个专为 PWM 输出设计的硬件控制器,具备以下特点:

  • 6 个高速通道 + 6 个低速通道:共 12 个独立 PWM 通道
  • 8/10/12/13/14/15/16/20 位分辨率:灵活选择
  • 自动时钟管理:低速通道可在睡眠模式下工作
  • 硬件渐变(fade):无需 CPU 干预即可平滑改变占空比

3.3 配置步骤与核心 API

c 复制代码
#include "driver/ledc.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_48    // 板载 LED 引脚
#define LEDC_RESOLUTION LEDC_TIMER_13_BIT  // 13 位分辨率:0~8191
#define LEDC_FREQ_HZ    5000          // 5 kHz PWM 频率

void pwm_init(void) {
    // 1. 配置定时器模式
    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_AUTO_CLK,
    };
    ledc_timer_config(&timer_conf);

    // 2. 配置通道
    ledc_channel_config_t channel_conf = {
        .gpio_num = LEDC_GPIO,
        .speed_mode = LEDC_MODE,
        .channel = LEDC_CHANNEL,
        .intr_type = LEDC_INTR_DISABLE,
        .timer_sel = LEDC_TIMER,
        .duty = 0,                 // 初始占空比 0
        .hpoint = 0,
    };
    ledc_channel_config(&channel_conf);
}

// 设置占空比(立即生效)
void set_pwm_duty(uint32_t duty) {
    ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
    ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
}

3.4 硬件渐变:平滑调光

LEDC 最强大的特性之一是硬件自动渐变,无需 CPU 逐级调节:

c 复制代码
// 配置并启动渐变
void fade_to_brightness(uint32_t target_duty, int fade_ms) {
    ledc_set_fade_with_time(LEDC_MODE, LEDC_CHANNEL,
                            target_duty, fade_ms);
    ledc_fade_start(LEDC_MODE, LEDC_CHANNEL,
                    LEDC_FADE_NO_WAIT);
}

渐变期间 CPU 可以处理其他任务,渐变由硬件独立完成。这对于呼吸灯、舞台灯光效果等场景非常实用。


四、实战案例

案例一:呼吸灯效果

将一个 LED 在亮和灭之间平滑渐变,营造"呼吸"效果:

c 复制代码
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/ledc.h"

#define LEDC_GPIO       GPIO_NUM_48
#define LEDC_TIMER      LEDC_TIMER_0
#define LEDC_MODE       LEDC_LOW_SPEED_MODE
#define LEDC_CHANNEL    LEDC_CHANNEL_0
#define LEDC_RESOLUTION LEDC_TIMER_13_BIT   // 0~8191
#define LEDC_FREQ_HZ    5000

#define MAX_DUTY        8191

void app_main(void) {
    // 配置 LEDC
    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_AUTO_CLK,
    };
    ledc_timer_config(&timer_conf);

    ledc_channel_config_t chan_conf = {
        .gpio_num = LEDC_GPIO,
        .speed_mode = LEDC_MODE,
        .channel = LEDC_CHANNEL,
        .intr_type = LEDC_INTR_DISABLE,
        .timer_sel = LEDC_TIMER,
        .duty = 0,
        .hpoint = 0,
    };
    ledc_channel_config(&chan_conf);

    printf("Breathing LED started!\n");

    while (1) {
        // 从暗到亮(渐变 1.5 秒)
        ledc_set_fade_with_time(LEDC_MODE, LEDC_CHANNEL, MAX_DUTY, 1500);
        ledc_fade_start(LEDC_MODE, LEDC_CHANNEL, LEDC_FADE_NO_WAIT);
        vTaskDelay(2000 / portTICK_PERIOD_MS);

        // 从亮到暗(渐变 1.5 秒)
        ledc_set_fade_with_time(LEDC_MODE, LEDC_CHANNEL, 0, 1500);
        ledc_fade_start(LEDC_MODE, LEDC_CHANNEL, LEDC_FADE_NO_WAIT);
        vTaskDelay(2000 / portTICK_PERIOD_MS);
    }
}

案例二:舵机控制

舵机是机器人项目的核心执行部件,通过 PWM 控制旋转角度。标准舵机的控制信号如下:

脉宽(高电平时间) 对应角度
1.0 ms
1.5 ms 90°(中位)
2.0 ms 180°

舵机对 PWM 频率有严格要求------通常为 50 Hz(周期 20 ms)。

c 复制代码
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/ledc.h"

#define SERVO_GPIO      GPIO_NUM_4
#define SERVO_TIMER     LEDC_TIMER_0
#define SERVO_MODE      LEDC_LOW_SPEED_MODE
#define SERVO_CHANNEL   LEDC_CHANNEL_0
#define SERVO_RESOLUTION LEDC_TIMER_14_BIT   // 14 位:0~16383
#define SERVO_FREQ_HZ   50                   // 50 Hz

// 脉宽换算:50 Hz 对应周期 20 ms
// 14 位分辨率:16383 → 20 ms
// 1 ms = 16383 * 1 / 20 ≈ 819
// 1.5 ms = 1229,2.0 ms = 1638

#define PULSE_0DEG     819     // 1.0 ms → 0°
#define PULSE_90DEG    1229    // 1.5 ms → 90°
#define PULSE_180DEG   1638    // 2.0 ms → 180°

void servo_set_angle(uint8_t angle) {
    // 将角度(0~180)线性映射到脉宽值
    uint32_t pulse = PULSE_0DEG +
        (uint32_t)(PULSE_180DEG - PULSE_0DEG) * angle / 180;

    ledc_set_duty(SERVO_MODE, SERVO_CHANNEL, pulse);
    ledc_update_duty(SERVO_MODE, SERVO_CHANNEL);
}

void app_main(void) {
    // 配置 LEDC 定时器
    ledc_timer_config_t timer_conf = {
        .speed_mode = SERVO_MODE,
        .timer_num = SERVO_TIMER,
        .duty_resolution = SERVO_RESOLUTION,
        .freq_hz = SERVO_FREQ_HZ,
        .clk_cfg = LEDC_AUTO_CLK,
    };
    ledc_timer_config(&timer_conf);

    // 配置舵机通道
    ledc_channel_config_t chan_conf = {
        .gpio_num = SERVO_GPIO,
        .speed_mode = SERVO_MODE,
        .channel = SERVO_CHANNEL,
        .intr_type = LEDC_INTR_DISABLE,
        .timer_sel = SERVO_TIMER,
        .duty = 0,
        .hpoint = 0,
    };
    ledc_channel_config(&chan_conf);

    printf("Servo control started!\n");

    while (1) {
        // 0° → 90° → 180° → 90° → 0° 往复运动
        servo_set_angle(0);
        vTaskDelay(1000 / portTICK_PERIOD_MS);
        servo_set_angle(90);
        vTaskDelay(1000 / portTICK_PERIOD_MS);
        servo_set_angle(180);
        vTaskDelay(1000 / portTICK_PERIOD_MS);
        servo_set_angle(90);
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

注意: 舵机功耗较大,不要直接从 ESP32-S3 的 3.3V 引脚供电!应使用外部 5V 电源,且信号地(GND)与 ESP32-S3 共地。


五、总结

本章我们学习了 ESP32-S3 上与时间和波形相关的两个核心模块:

  1. 硬件定时器(GPTimer):高精度计数,适合精确时序控制和周期性中断
  2. 软定时器(esp_timer):使用简单,适合周期性任务调度
  3. LEDC 控制器:灵活的 PWM 输出,支持 12 个独立通道和硬件渐变
  4. 实战案例:呼吸灯展示了硬件渐变的优雅应用,舵机控制则展示了 PWM 在机器人领域的核心地位

掌握定时器和 PWM 后,你已具备开发定时采集、灯光控制、电机驱动等常见嵌入式功能的基础能力。


下篇预告

第4章:UART 串口通信 ------ 串口是嵌入式开发中最重要的调试工具和通信接口,我们将学习 ESP32-S3 的 UART 驱动、printf 重定向和串口协议解析。


本文基于 ESP-IDF v5.x 编写,PWM 引脚号请根据实际开发板调整。舵机供电需外接电源,切勿直接使用开发板 3.3V 引脚驱动。

相关推荐
Jason_chen2 小时前
Linux 6.2 CAN/CANFD机制详解
后端
Apifox3 小时前
Apifox 6 月更新|Apifox CLI 全面升级、导入导出优化、OAuth 2.0 支持自动刷新令牌
前端·后端·测试
悟空瞎说3 小时前
NestJS 接口设计避坑:摒弃万能用户更新接口,落地单一职责与最小权限原则
后端·nestjs
smallyoung3 小时前
Spring AI 2.0 VectorStore实战:从原理到RAG落地
人工智能·后端
jiayou643 小时前
KingbaseES 表级与列级加密完全指南
数据库·后端
青丘3 小时前
Spring AI整合Milvus向量数据库实战
后端
古茗前端团队5 小时前
急招!前端|测试|后端|产品(名额多,速来)
前端·后端·架构
喵个咪6 小时前
Go-Wind HTTP 服务器从入门到精通
后端·http·go