4. 定时器与PWM:ESP定时器API、软件定时器、硬件定时器、LEDC(PWM)驱动、呼吸灯实验
定时器和PWM,这两个东西在嵌入式开发里太常用了。说白了,定时器就是帮我们计时的,PWM就是模拟电压的。我刚开始用ESP32的时候,觉得定时器API挺绕的,后来摸清楚了,其实就那么回事。
4.1 ESP32的定时器体系
ESP32的定时器分两类:软件定时器和硬件定时器。很多人搞混,我简单说说区别。
| 类型 | 精度 | 资源消耗 | 适用场景 |
|---|---|---|---|
| 软件定时器 | 毫秒级(受FreeRTOS调度影响) | 低(基于任务) | 按键消抖、周期性状态查询 |
| 硬件定时器 | 微秒级(硬件计数器) | 高(占用外设) | PWM生成、精确延时、电机控制 |
嗯,这里要注意:软件定时器本质上是FreeRTOS的软件定时器,它依赖系统tick。如果你的系统负载高,定时器回调可能会被延迟。硬件定时器就不一样了,它自己跑自己的,不受任务调度影响。
4.2 软件定时器实战
我个人习惯用软件定时器做周期性的非实时任务。比如每隔100ms读一次传感器,这种场景用软件定时器就够了。
#include "freertos/FreeRTOS.h"
#include "freertos/timers.h"
// 定时器回调函数
void my_timer_callback(TimerHandle_t xTimer) {
// 这里放你的周期性任务
ESP_LOGI("TIMER", "定时器触发了");
}
void app_main(void) {
// 创建软件定时器
TimerHandle_t my_timer = xTimerCreate(
"my_timer", // 定时器名字
pdMS_TO_TICKS(1000), // 周期:1000ms
pdTRUE, // 自动重载
(void *)0, // 定时器ID
my_timer_callback // 回调函数
);
if (my_timer != NULL) {
xTimerStart(my_timer, 0); // 启动定时器
}
}
4.3 硬件定时器:ESP_TIMER API
需要高精度的时候,就得用硬件定时器了。ESP-IDF提供了esp_timer API,它基于硬件定时器实现,精度能达到微秒级。
#include "esp_timer.h"
void periodic_timer_callback(void* arg) {
int64_t time_since_boot = esp_timer_get_time();
ESP_LOGI("TIMER", "当前系统时间: %lld us", time_since_boot);
}
void app_main(void) {
const esp_timer_create_args_t timer_args = {
.callback = &periodic_timer_callback,
.arg = NULL,
.name = "periodic"
};
esp_timer_handle_t timer_handle;
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle));
ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle, 500000)); // 500ms
}
为什么用esp_timer而不是直接用硬件定时器寄存器?说白了,esp_timer帮你封装好了,你不用自己去配置定时器的分频系数、计数值这些底层东西。而且它还能获取系统时间,这个在调试时特别有用。
4.4 LEDC:ESP32的PWM驱动
LEDC是ESP32专门用来生成PWM的外设。它支持8个通道,每个通道可以独立配置频率和占空比。我一般用它来控制LED亮度、舵机角度、电机速度。
| 参数 | 说明 | 典型值 |
|---|---|---|
| 频率 | PWM波的频率 | 50Hz(舵机)、5kHz(LED) |
| 占空比 | 高电平占整个周期的比例 | 0% ~ 100% |
| 分辨率 | 占空比的调节精度 | 8位(0-255)、13位(0-8191) |
#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_2
void ledc_init(void) {
// 配置定时器
ledc_timer_config_t timer_conf = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_TIMER_13_BIT, // 13位分辨率
.freq_hz = 5000, // 5kHz
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer_conf);
// 配置通道
ledc_channel_config_t channel_conf = {
.gpio_num = LEDC_GPIO,
.speed_mode = LEDC_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.duty = 0,
.hpoint = 0
};
ledc_channel_config(&channel_conf);
}
void set_duty(uint32_t duty) {
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
}
4.5 呼吸灯实验:把知识串起来
呼吸灯这个实验,说白了就是用PWM控制LED亮度,让亮度从暗到亮、再从亮到暗,循环变化。我拿这个实验来教大家怎么把定时器和PWM结合起来用。
实现思路很简单:用软件定时器每10ms触发一次,在回调里改变PWM的占空比。占空比从0逐渐增加到最大值,然后再逐渐减小到0,如此循环。
#include "freertos/FreeRTOS.h"
#include "freertos/timers.h"
#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_2
#define LEDC_DUTY_MAX (8191) // 13位分辨率最大值
static int duty_direction = 1; // 1:增加 -1:减小
static uint32_t current_duty = 0;
void breath_timer_callback(TimerHandle_t xTimer) {
// 更新占空比
current_duty += duty_direction * 50;
// 边界判断
if (current_duty >= LEDC_DUTY_MAX) {
current_duty = LEDC_DUTY_MAX;
duty_direction = -1;
} else if (current_duty <= 0) {
current_duty = 0;
duty_direction = 1;
}
// 设置PWM占空比
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, current_duty);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
}
void app_main(void) {
// 初始化LEDC
ledc_init();
// 创建软件定时器,10ms触发一次
TimerHandle_t breath_timer = xTimerCreate(
"breath",
pdMS_TO_TICKS(10),
pdTRUE,
NULL,
breath_timer_callback
);
if (breath_timer != NULL) {
xTimerStart(breath_timer, 0);
}
}
关键点总结:
- 定时器周期决定了呼吸速度:周期越短,呼吸越快
- 占空比步长决定了平滑度:步长越小,过渡越平滑
- LEDC分辨率越高,呼吸效果越细腻
4.6 定时器与PWM的知识体系
我把这章的知识点画了个图,方便大家理解它们之间的关系。
这张图把定时器和PWM的关系理清楚了。左边是定时器体系,中间是PWM配置参数,右边是呼吸灯实验怎么把两者结合起来。你想想看,呼吸灯其实就是定时器驱动PWM占空比变化的过程。
嗯,这章的内容就这些。定时器和PWM是嵌入式开发的基础,呼吸灯实验虽然简单,但把两个知识点串起来了。你把这个实验跑通了,后面做更复杂的控制就心里有底了。