Appearance
第三章:中断与定时器
3.1 中断基础
什么是中断?
没有中断(轮询):
CPU ──→ 检查按键 → 没按 → 检查按键 → 没按 → ... → 按了!→ 处理
浪费 CPU 时间,响应有延迟
有中断:
CPU ──→ 做其他事情 ──────────────────────────────→ 继续
↑ 按键按下,硬件通知 CPU
└→ 暂停当前任务 → 执行 ISR → 返回
CPU 利用率高,响应及时ISR(中断服务函数)的限制
c
// ❌ ISR 中禁止的操作:
ESP_LOGI(...) // 不能用,会阻塞
vTaskDelay(...) // 不能用,会阻塞
malloc() / free() // 不安全
任何可能阻塞的函数
// ✅ ISR 中允许的操作:
gpio_set_level(...) // 快速 GPIO 操作
xQueueSendFromISR(...) // 向队列发送(带 FromISR 后缀)
xSemaphoreGiveFromISR(...) // 释放信号量
portYIELD_FROM_ISR(...) // 请求任务切换3.2 GPIO 中断
完整示例:按键中断 + 队列通知
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "esp_log.h"
#define BTN_PIN GPIO_NUM_0
#define LED_PIN GPIO_NUM_2
static const char *TAG = "GPIO_INT";
static QueueHandle_t gpio_evt_queue = NULL; // 事件队列
// ISR:中断服务函数(必须在 IRAM 中执行)
static void IRAM_ATTR gpio_isr_handler(void *arg)
{
uint32_t gpio_num = (uint32_t)arg;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 把 GPIO 编号发送到队列(不阻塞)
xQueueSendFromISR(gpio_evt_queue, &gpio_num, &xHigherPriorityTaskWoken);
// 如果唤醒了更高优先级的任务,请求立即切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// 处理任务:在普通任务中处理中断事件
static void gpio_task(void *arg)
{
uint32_t gpio_num;
int led_state = 0;
uint32_t press_count = 0;
while (1) {
// 阻塞等待队列消息(portMAX_DELAY = 永久等待)
if (xQueueReceive(gpio_evt_queue, &gpio_num, portMAX_DELAY)) {
press_count++;
led_state = !led_state;
gpio_set_level(LED_PIN, led_state);
ESP_LOGI(TAG, "GPIO%"PRIu32" 中断触发,第 %"PRIu32" 次,LED=%s",
gpio_num, press_count, led_state ? "ON" : "OFF");
}
}
}
void app_main(void)
{
// 1. 配置 LED
gpio_reset_pin(LED_PIN);
gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);
gpio_set_level(LED_PIN, 0);
// 2. 配置按键(下降沿中断)
gpio_config_t btn_cfg = {
.pin_bit_mask = (1ULL << BTN_PIN),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_NEGEDGE, // 下降沿(按下)触发
};
gpio_config(&btn_cfg);
// 3. 创建事件队列(深度 10,每个元素 4 字节)
gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
// 4. 安装 GPIO 中断服务
gpio_install_isr_service(0); // 0 = 默认标志
gpio_isr_handler_add(BTN_PIN, gpio_isr_handler, (void*)BTN_PIN);
// 5. 创建处理任务
xTaskCreate(gpio_task, "gpio_task", 2048, NULL, 10, NULL);
ESP_LOGI(TAG, "GPIO 中断初始化完成,按下 BOOT 键测试");
}中断触发类型
c
GPIO_INTR_DISABLE // 禁用中断
GPIO_INTR_POSEDGE // 上升沿(低→高)
GPIO_INTR_NEGEDGE // 下降沿(高→低)
GPIO_INTR_ANYEDGE // 任意边沿
GPIO_INTR_LOW_LEVEL // 低电平触发(持续触发,慎用)
GPIO_INTR_HIGH_LEVEL // 高电平触发(持续触发,慎用)3.3 硬件定时器(ESP Timer)
ESP Timer:高精度软件定时器
c
#include "esp_timer.h"
static const char *TAG = "TIMER";
// 定时器回调函数
static void periodic_timer_cb(void *arg)
{
static uint32_t count = 0;
count++;
// 注意:回调运行在定时器任务中,可以用 ESP_LOGI
ESP_LOGI(TAG, "定时器触发 #%"PRIu32",时间: %lld us",
count, esp_timer_get_time());
}
void app_main(void)
{
// 创建周期定时器
esp_timer_handle_t periodic_timer;
esp_timer_create_args_t timer_args = {
.callback = periodic_timer_cb,
.arg = NULL,
.name = "periodic"
};
esp_timer_create(&timer_args, &periodic_timer);
// 启动:每 1 秒触发一次(单位:微秒)
esp_timer_start_periodic(periodic_timer, 1000000);
ESP_LOGI(TAG, "定时器已启动");
// 10 秒后停止
vTaskDelay(pdMS_TO_TICKS(10000));
esp_timer_stop(periodic_timer);
esp_timer_delete(periodic_timer);
ESP_LOGI(TAG, "定时器已停止");
}单次定时器
c
// 单次触发(触发后自动停止)
esp_timer_start_once(timer_handle, 5000000); // 5 秒后触发一次获取系统运行时间
c
int64_t time_us = esp_timer_get_time(); // 微秒,从启动开始
uint32_t time_ms = xTaskGetTickCount() * portTICK_PERIOD_MS; // 毫秒3.4 LEDC(PWM 输出)
PWM 基础概念
PWM(脉冲宽度调制):
频率:每秒多少个周期(Hz)
占空比:高电平时间 / 总周期时间(0%~100%)
占空比 0%: ___________ → LED 全灭
占空比 25%: ─┐___┌─┐___ → LED 较暗
占空比 50%: ─┐_┌─┐_┌─┐ → LED 中等
占空比 75%: ─┐┌─┐┌─┐┌─ → LED 较亮
占空比 100%:─────────── → LED 全亮LEDC 配置与使用
c
#include "driver/ledc.h"
#define LED_PIN GPIO_NUM_2
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_MODE LEDC_LOW_SPEED_MODE
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_DUTY_RES LEDC_TIMER_13_BIT // 13位分辨率:0~8191
#define LEDC_FREQUENCY 5000 // 5kHz
void ledc_init(void)
{
// 1. 配置定时器
ledc_timer_config_t timer_cfg = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = LEDC_FREQUENCY,
.clk_cfg = LEDC_AUTO_CLK,
};
ESP_ERROR_CHECK(ledc_timer_config(&timer_cfg));
// 2. 配置通道
ledc_channel_config_t channel_cfg = {
.speed_mode = LEDC_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.intr_type = LEDC_INTR_DISABLE,
.gpio_num = LED_PIN,
.duty = 0, // 初始占空比 0
.hpoint = 0,
};
ESP_ERROR_CHECK(ledc_channel_config(&channel_cfg));
}
// 设置亮度(0~100 百分比)
void led_set_brightness(uint8_t percent)
{
if (percent > 100) percent = 100;
// 13位分辨率最大值 8191,计算对应 duty
uint32_t duty = (8191 * percent) / 100;
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
}
void app_main(void)
{
ledc_init();
// 呼吸灯效果
while (1) {
// 渐亮
for (int i = 0; i <= 100; i++) {
led_set_brightness(i);
vTaskDelay(pdMS_TO_TICKS(20));
}
// 渐灭
for (int i = 100; i >= 0; i--) {
led_set_brightness(i);
vTaskDelay(pdMS_TO_TICKS(20));
}
}
}LEDC 渐变(硬件渐变,无需 CPU 干预)
c
// 硬件自动渐变到目标占空比
ledc_set_fade_with_time(
LEDC_MODE,
LEDC_CHANNEL,
target_duty, // 目标占空比
2000 // 渐变时间 2000ms
);
ledc_fade_start(LEDC_MODE, LEDC_CHANNEL, LEDC_FADE_NO_WAIT);3.5 综合示例:PWM + 中断
c
// 按键控制 LED 亮度(每次按下增加 20%,超过 100% 归零)
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "driver/ledc.h"
#include "esp_log.h"
#define BTN_PIN GPIO_NUM_0
#define LED_PIN GPIO_NUM_2
static QueueHandle_t btn_queue;
static const char *TAG = "PWM_BTN";
static void IRAM_ATTR btn_isr(void *arg)
{
uint32_t val = 1;
BaseType_t woken = pdFALSE;
xQueueSendFromISR(btn_queue, &val, &woken);
portYIELD_FROM_ISR(woken);
}
void app_main(void)
{
// 初始化 LEDC
ledc_timer_config_t t = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.timer_num = LEDC_TIMER_0,
.duty_resolution = LEDC_TIMER_13_BIT,
.freq_hz = 5000,
.clk_cfg = LEDC_AUTO_CLK,
};
ledc_timer_config(&t);
ledc_channel_config_t c = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LEDC_CHANNEL_0,
.timer_sel = LEDC_TIMER_0,
.gpio_num = LED_PIN,
.duty = 0,
};
ledc_channel_config(&c);
// 初始化按键中断
gpio_config_t btn_cfg = {
.pin_bit_mask = (1ULL << BTN_PIN),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.intr_type = GPIO_INTR_NEGEDGE,
};
gpio_config(&btn_cfg);
btn_queue = xQueueCreate(5, sizeof(uint32_t));
gpio_install_isr_service(0);
gpio_isr_handler_add(BTN_PIN, btn_isr, NULL);
int brightness = 0;
uint32_t dummy;
while (1) {
if (xQueueReceive(btn_queue, &dummy, portMAX_DELAY)) {
vTaskDelay(pdMS_TO_TICKS(20)); // 简单消抖
brightness = (brightness + 20) % 120; // 0,20,40,60,80,100,0...
if (brightness > 100) brightness = 0;
uint32_t duty = (8191 * brightness) / 100;
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty);
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
ESP_LOGI(TAG, "亮度: %d%%", brightness);
}
}
}📝 第三章练习题
练习 3-1:定时器计时(基础)
目标:掌握 esp_timer 使用
任务:
- 创建一个 100ms 周期定时器
- 每次触发时计数 +1
- 每 10 次(1 秒)打印一次 "运行时间: Xs"
- 60 秒后自动停止并打印 "计时结束"
练习 3-2:按键中断计数(基础)
目标:掌握 GPIO 中断 + 队列模式
任务:
- 用中断方式检测按键
- 统计按键次数
- 每次按下打印次数
- 按下 10 次后打印 "达到 10 次,重置计数"
要求:必须使用 ISR + Queue 模式,不能用轮询
练习 3-3:呼吸灯(进阶)
目标:掌握 LEDC PWM
任务:实现呼吸灯,要求:
- 亮度变化曲线为非线性(模拟人眼感知,使用 gamma 校正)
- 呼吸周期可通过宏定义调整(默认 2 秒)
- 串口打印当前占空比百分比
Gamma 校正公式:
c
// 人眼对亮度的感知是非线性的
// linear_val: 0~100 线性值
// 输出: 0~100 gamma 校正后的值
float gamma_correct(float linear_val) {
return powf(linear_val / 100.0f, 2.2f) * 100.0f;
}练习 3-4:秒表(进阶)
目标:综合中断 + 定时器
任务:用两个按键实现秒表:
- 按键A:开始/暂停计时
- 按键B:复位(归零)
- 每 100ms 更新一次显示(串口打印)
- 格式:
[运行中] 00:01.234或[已暂停] 00:01.234
状态机:
STOPPED → 按A → RUNNING → 按A → PAUSED → 按A → RUNNING
任意状态 → 按B → STOPPED(归零)练习 3-5:PWM 舵机控制(挑战)
目标:PWM 时序控制
背景:标准舵机控制信号:
周期:20ms(50Hz)
脉宽:0.5ms~2.5ms 对应 0°~180°
0° → 高电平 0.5ms
90° → 高电平 1.5ms
180° → 高电平 2.5ms任务:
- 配置 LEDC 输出 50Hz PWM
- 实现
servo_set_angle(uint8_t angle)函数(0~180°) - 按键A:角度 +10°
- 按键B:角度 -10°
- 串口打印当前角度
提示:
c
// 50Hz,周期 20ms = 20000us
// 13位分辨率,总计数 8192
// 0.5ms 对应 duty = 8192 * 0.5 / 20 = 205
// 2.5ms 对应 duty = 8192 * 2.5 / 20 = 1024
uint32_t angle_to_duty(uint8_t angle) {
return 205 + (uint32_t)angle * (1024 - 205) / 180;
}练习 3-6:思考题
ISR 中为什么不能调用
ESP_LOGI?如果强行调用会发生什么?portYIELD_FROM_ISR(xHigherPriorityTaskWoken)的作用是什么?如果不加这行,程序还能工作吗?有什么区别?ESP Timer 和 FreeRTOS 软件定时器(
xTimerCreate)有什么区别?各适合什么场景?LEDC 13位分辨率意味着什么?为什么不用 8 位(0~255)?分辨率越高越好吗?
本章小结
| 知识点 | 掌握程度自评 |
|---|---|
| 中断概念与 ISR 限制 | ⬜⬜⬜⬜⬜ |
| GPIO 中断配置 | ⬜⬜⬜⬜⬜ |
| ISR + Queue 通信模式 | ⬜⬜⬜⬜⬜ |
| ESP Timer 定时器 | ⬜⬜⬜⬜⬜ |
| LEDC PWM 配置 | ⬜⬜⬜⬜⬜ |
| PWM 占空比计算 | ⬜⬜⬜⬜⬜ |
上一章:← GPIO 与 LED 控制下一章:串口通信 UART →