一、中断基础

中断分类

1.外部中断(EXTI)

GPIO 中断

2.外设中断

定时器中断、UART 中断、SPI 中断、ADC / DMA 中断、Wi-Fi / BLE 中断

中断优先级

1-7级 1级优先级最低 (指的esp32 不针对全部)

设置中断优先级函数: gpio_install_isr_service(0) :默认Level 1(最低优先级)

中断触发方式

1.上升沿触发(Rising Edge)

2.下降沿触发(Falling Edge)

3.边沿触发双沿(Any Edge)

4.高电平触发(High Level)

5.低电平触发(Low Level)

中断处理注意事项

中断服务函数(ISR)是嵌入式开发中的关键部分,编写时需遵循以下原则以确保系统稳定性和实时性:

  • 保持简短:ISR 应尽可能短小,只做最必要的处理(如设置标志位、清除中断标志),将耗时操作移至主循环或任务中。
  • 避免阻塞函数:禁止在 ISR 内调用可能引起阻塞的库函数(如 printf()delay()、动态内存分配等),这些操作会严重破坏实时性并可能导致系统崩溃。
  • 保护共享数据:ISR 与主程序共享的变量需进行保护。常用方法包括:
    • 使用 volatile 关键字声明变量,防止编译器优化导致读写不一致。
    • 在访问关键数据区时临时关闭中断(portENTER_CRITICAL() / portEXIT_CRITICAL() 或类似机制),但应尽量缩短关中断时间。
    • 使用原子操作或硬件支持的同步原语。
  • 注意可重入性:如果中断可能嵌套或被更高优先级中断打断,需确保 ISR 使用的函数和数据结构是可重入的。
  • 及时清除中断标志:在 ISR 退出前,务必清除对应的中断标志位,避免中断被连续触发。

不良写法 vs 推荐写法对比

不良写法示例(在 ISR 中进行复杂处理和打印)

// 中断服务函数
void IRAM_ATTR gpio_isr_handler(void* arg) {
    int sensor_value = read_adc(); // 可能耗时的操作
    process_data(sensor_value);    // 复杂计算
    printf("ADC value: %d\n", sensor_value); // 阻塞式打印
    // ... 其他操作
}

推荐写法示例(ISR 仅设置标志,主循环处理)

// 全局变量,使用 volatile 修饰
volatile bool adc_data_ready = false;
volatile int adc_raw_value = 0;

// 中断服务函数
void IRAM_ATTR gpio_isr_handler(void* arg) {
    adc_raw_value = read_adc(); // 快速读取原始值
    adc_data_ready = true;      // 设置标志
    // 清除中断标志
    gpio_clear_intr_status(GPIO_NUM_4);
}

// 主循环中处理
void main_task(void* arg) {
    while (1) {
        if (adc_data_ready) {
            // 关中断保护共享变量(可选,取决于架构)
            portENTER_CRITICAL();
            int local_value = adc_raw_value;
            adc_data_ready = false;
            portEXIT_CRITICAL();
            
            // 在主循环中进行耗时处理
            process_data(local_value);
            printf("ADC value: %d\n", local_value); // 在主循环中打印
        }
        vTaskDelay(10 / portTICK_PERIOD_MS);
    }
}

通过遵循上述注意事项,可以编写出高效、可靠的中断服务程序,确保嵌入式系统的实时响应与稳定运行。

二、任务通知(Freertos延伸)

任务通知原理

1.中断里发消息到别的线程

xTaskNotifyFromISR

2.让别的线程去执行操作

xTaskNotifyWait

这样就不会占用中断执行的时间

BaseType_t xTaskNotifyFromISR(
    TaskHandle_t xTaskToNotify,       // 要通知的目标任务句柄
    uint32_t ulValue,                 // 通知值(可以传递数据)
    eNotifyAction eAction,            // 通知动作(如何更新通知值)
    BaseType_t *pxHigherPriorityTaskWoken  // 是否有更高优先级任务被唤醒
);
BaseType_t xTaskNotifyWait(
    uint32_t ulBitsToClearOnEntry,   // 进入时清除的通知位
    uint32_t ulBitsToClearOnExit,    // 退出时清除的通知位
    uint32_t *pulNotificationValue,  // 出参:接收到的通知值
    TickType_t xTicksToWait          // 等待超时时间
);

中断相关代码

#include "bsp_exti.h"


TaskHandle_t reg_exti_task_handle = NULL;  // 外部中断通知的目标任务句柄


/**
 * @brief GPIO中断服务函数声明(IRAM_ATTR表示该函数存放在IRAM中,提高中断响应速度)
 */
static void IRAM_ATTR gpio_isr_handler(void* arg);

/**
 * @brief 初始化外部中断
 * @param gpio_num 要配置的GPIO引脚号
 * @note 配置GPIO为输入模式,使能上拉电阻,下降沿触发中断
 */
void bsp_exti_init(gpio_num_t gpio_num)
{
    // 配置GPIO参数:输入模式、上拉使能、下降沿触发中断
    gpio_config_t key_config = {
        .pin_bit_mask = (1ULL<<gpio_num),       // 设置目标GPIO引脚位掩码
        .mode = GPIO_MODE_INPUT,                // 设置为输入模式
        .pull_up_en = GPIO_PULLUP_ENABLE,       // 使能上拉电阻
        .pull_down_en = GPIO_PULLDOWN_DISABLE,  // 禁用下拉电阻
        .intr_type = GPIO_INTR_NEGEDGE          // 下降沿触发中断
    };
    gpio_config(&key_config);  // 应用GPIO配置

    // 静态变量标记ISR服务是否已安装,确保只安装一次
    static bool isr_service_installed = 0;
    if(0 == isr_service_installed) 
    gpio_install_isr_service(0);   // 安装GPIO ISR服务,参数0表示使用默认配置
    isr_service_installed = 1;
    
    // 为指定GPIO添加中断处理函数,传入GPIO编号作为参数
    gpio_isr_handler_add(gpio_num, gpio_isr_handler, (void *)gpio_num);
    
}

/**
 * @brief GPIO中断服务函数
 * @param arg 传入的参数,此处为GPIO编号
 * @note IRAM_ATTR属性确保函数存放在IRAM中,避免从Flash加载带来的延迟
 *       实现了20ms软件消抖功能
 */
static void IRAM_ATTR gpio_isr_handler(void* arg)
{
    static uint64_t last_time = 0;   // 上次中断触发时间,用于软件消抖
    uint64_t now = esp_timer_get_time();  // 获取当前时间(微秒)
    // 软件消抖:如果距离上次中断不足20ms,则忽略本次中断
    if (now - last_time < 20000) return;
    last_time = now;
    
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;  // 标记是否有更高优先级任务被唤醒
    gpio_num_t gpio_num = (gpio_num_t)arg;          // 获取触发中断的GPIO编号

    // 从中断中发送任务通知,携带GPIO编号,使用覆盖模式
    xTaskNotifyFromISR(reg_exti_task_handle, gpio_num, eSetValueWithOverwrite, &xHigherPriorityTaskWoken);

    // 如果有更高优先级任务被唤醒,进行上下文切换
    if(xHigherPriorityTaskWoken)
    {
           portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}


/**
 * @brief 注册外部中断通知的目标任务句柄
 * @param task_handle 目标任务句柄,中断发生时会向该任务发送通知
 */
void bsp_exti_register_task_handle(TaskHandle_t task_handle)
{
    reg_exti_task_handle = task_handle;  // 保存任务句柄,供中断服务函数使用
}

任务通知的代码

#include "task_exti_prco.h"


TaskHandle_t exti_task_prco_handle = NULL;  // 外部中断处理任务的任务句柄,供中断服务函数使用

/**
 * @brief 外部中断处理任务函数
 * @param pvParameters 任务创建时传入的参数(本任务未使用)
 * @note 该任务通过任务通知接收中断信号,根据GPIO编号执行相应操作
 */
void exti_task_prco(void *pvParameters)
{
    uint32_t notify_value = 0;  // 用于存储接收到的通知值(即触发中断的GPIO编号)
    
    // 将本任务句柄注册到BSP层,使中断服务函数知道向哪个任务发送通知
    bsp_exti_register_task_handle(exti_task_prco_handle);
    
    while (1)
    {    
        // 永久等待任务通知,进入时不清除任何位,退出时清除所有位
        // 收到通知后,notify_value 中保存的是触发中断的GPIO编号
        xTaskNotifyWait(0, ULONG_MAX, &notify_value, portMAX_DELAY);
        
        // 根据接收到的GPIO编号判断是哪个按键触发的中断
        if(notify_value == KEY_GPIO)
        {
            bsp_gpio_toggle_level(LED_GPIO);  // 翻转LED电平状态
            printf("key gpio notify\n");       // 打印调试信息:KEY按键触发
        }
        else if(notify_value == BOOT_GPIO)
        {
            bsp_gpio_toggle_level(LED_GPIO);  // 翻转LED电平状态
            printf("boot gpio notify\n");     // 打印调试信息:BOOT按键触发
        }
    }
}

/**
 * @brief 创建外部中断处理任务
 * @return BaseType_t 任务创建结果,pdPASS表示成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY表示内存不足
 * @note 任务栈大小4096字节,优先级10
 */
BaseType_t exti_task_prco_create(void)
{
    // 创建外部中断处理任务
    // 参数说明:任务函数、任务名称、栈深度(字)、任务参数、优先级、任务句柄指针
    BaseType_t ret = xTaskCreate(exti_task_prco, "exti_task_prco",4096, NULL, 10, &exti_task_prco_handle);
    return ret;
}

注释是由我让TRAE插件自动生成的,方便后续查看函数意思。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐