第四章:定时器与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从暗到亮,再从亮到暗,循环往复。实现思路很简单:
- 用定时器产生一个周期性的中断,比如每10ms一次。
- 每次中断里,改变PWM的占空比。
- 占空比从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调光、音频输出,甚至通信协议模拟,都离不开它们。建议你把今天的代码自己动手跑一遍,然后试着改改参数,看看效果有什么变化。
下一章我们会讲中断系统,到时候你会看到定时器和中断是如何完美配合的。嗯,今天就到这里,有问题随时问我。