引言
上一章我们学习了 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 驱动库来配置定时器,步骤如下:
#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),用法更简单,精度为微秒级,适合大多数非实时苛刻场景:
#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,脉宽调制)通过调节方波的占空比来模拟模拟量输出。占空比越高,等效电压越高。
高电平时间 ──┐
│ ┌────────┐ ┌────────┐
│ │ │ │ │
└────┘ └────┘ └────
← 周期 T →
占空比 = 高电平时间 / T
| 占空比 | LED 亮度 | 舵机角度 |
|---|---|---|
| 0% | 熄灭 | 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
#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 逐级调节:
// 配置并启动渐变
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 在亮和灭之间平滑渐变,营造"呼吸"效果:
#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 | 0° |
| 1.5 ms | 90°(中位) |
| 2.0 ms | 180° |
舵机对 PWM 频率有严格要求——通常为 50 Hz(周期 20 ms)。
#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 上与时间和波形相关的两个核心模块:
- 硬件定时器(GPTimer):高精度计数,适合精确时序控制和周期性中断
- 软定时器(esp_timer):使用简单,适合周期性任务调度
- LEDC 控制器:灵活的 PWM 输出,支持 12 个独立通道和硬件渐变
- 实战案例:呼吸灯展示了硬件渐变的优雅应用,舵机控制则展示了 PWM 在机器人领域的核心地位
掌握定时器和 PWM 后,你已具备开发定时采集、灯光控制、电机驱动等常见嵌入式功能的基础能力。
下篇预告
第4章:UART 串口通信 —— 串口是嵌入式开发中最重要的调试工具和通信接口,我们将学习 ESP32-S3 的 UART 驱动、printf 重定向和串口协议解析。
本文基于 ESP-IDF v5.x 编写,PWM 引脚号请根据实际开发板调整。舵机供电需外接电源,切勿直接使用开发板 3.3V 引脚驱动。