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));
}
}
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的配置流程很简单:
- 配置定时器:设置PWM的频率和分辨率
- 绑定通道:将定时器与PWM通道关联
- 设置占空比:输出想要的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);
}
4.5 本章知识结构
下面这张图,把定时器、PWM、LED呼吸灯、舵机控制的关系理清楚了:
4.6 常见问题与调试技巧
做这几个实验,你可能会遇到一些问题。我把自己踩过的坑列出来:
| 问题 | 原因 | 解决方法 |
|---|---|---|
| LED不亮 | GPIO口不对或占空比为0 | 检查GPIO编号,确认duty>0 |
| 呼吸灯闪烁 | PWM频率太低 | 频率提高到1kHz以上 |
| 舵机抖动 | PWM频率不准或电源不足 | 确认50Hz,外接5V电源 |
| 舵机转不到180° | 脉宽范围不对 | 用示波器实测,调整映射值 |
好了,这一章的内容就到这里。定时器和PWM是嵌入式开发的基石,掌握了它们,后面做传感器驱动、电机控制、智能家居设备对接,都会轻松很多。动手把代码跑起来,有问题随时翻回来看看。