一、为什么中断优先级配置对实时系统如此重要

在嵌入式实时系统开发中,中断是整个系统响应的核心枢纽。你可以把它想象成一个公司里的电话系统——不同的电话代表不同的紧急事件,而中断优先级决定了哪些电话能立刻被打进来,哪些需要排队等待。如果这个优先级设置不当,最严重的后果就是高紧急事件被低紧急事件挡住,导致系统错过关键的时间窗口。

在实际开发中,我们经常会遇到这样的场景:一个电机控制系统需要在特定时刻完成高速位置采样,同时又要处理通信数据包的接收。如果这两个中断的优先级没有合理分配,可能就会出现通信数据包接收时电机采样被延迟,导致控制精度下降。这种延迟往往只有几十到几百微秒,但足以让整个系统的性能打折扣。

1.1 中断响应延迟的本质

中断响应延迟指的是从硬件发出中断请求到中断服务程序真正开始执行之间的时间间隔。这个延迟由多个部分组成:CPU当前的执行状态(如果正在执行某个中断,新的中断需要等待)、中断控制器的仲裁时间、以及RTOS内核从被唤醒到上下文切换完成的时间。

在RT-Thread中,这个延迟尤其值得重视,因为RT-Thread是一个抢占式实时操作系统,它的调度机制会在中断返回时检查是否有更高优先级的任务需要执行。如果中断优先级配置混乱,可能导致不必要的上下文切换,或者更糟的情况——高优先级中断被低优先级中断阻塞。

1.2 优先级配置不当带来的典型问题

最常见的三个问题:第一,中断风暴。当多个中断同时触发,如果优先级设置不当,CPU会在多个中断之间来回切换,导致真正需要执行的任务得不到调度。第二,优先级反转。低优先级中断持有了某个资源,高优先级中断在等待这个资源,结果高优先级任务被低优先级任务间接阻塞。第三,丢数据。高速通信中断如果优先级太低,在高负载场景下可能来不及处理,导致FIFO溢出或数据丢失。

/*
 * 技术栈:RT-Thread 5.x + STM32F4xx (ARM Cortex-M4)
 * 示例:中断响应延迟的直观测量
 * 说明:通过记录中断触发时间戳和ISR执行时间戳的差值,量化延迟
 */
#include <rtthread.h>
#include "board.h"
#include " drv_usart.h"

#define INTERUPT_MEASURE_COUNT   100  /* 测量次数 */
#define USART1_RX_BUF_SIZE       256  /* 接收缓冲区大小 */

static uint32_t interrupt_timestamp[INTERUPT_MEASURE_COUNT]; /* 存储每次中断的延迟 */
static uint32_t interrupt_delay[INTERUPT_MEASURE_COUNT];      /* 存储计算出的延迟 */
static rt_uint8_t rx_buffer[USART1_RX_BUF_SIZE];              /* UART1接收缓冲区 */
static volatile rt_uint32_t rx_head = 0;                      /* 环形缓冲区写指针 */
static volatile rt_uint32_t rx_tail = 0;                      /* 环形缓冲区读指针 */
static volatile rt_uint32_t measure_idx = 0;                  /* 当前测量索引 */

/* 记录当前时刻的系统时间戳(单位:微秒,需根据RTC或定时器配置) */
static inline uint32_t get_current_us(void)
{
    /* 利用硬件定时器获取高精度时间戳 */
    uint32_t ticks;
    uint32_t prescaler = 84000 - 1;  /* STM32F4定时器预分频器值 */
    uint32_t arr = 0xFFF;            /* 自动重装载值 */
    uint32_t timer_value = TIM3->CNT;

    /* 计算公式:时间(μs) = 定时器计数值 / (系统时钟频率 / (预分频器+1) / (ARR+1)) */
    /* 对于168MHz系统时钟,预分频84000-1,ARR=0xFFF,分辨率约为1μs */
    ticks = (timer_value * prescaler * arr) / (168000000 / (prescaler + 1) / (arr + 1));
    return ticks;
}

/* UART1中断服务程序——在ISR内部测量响应延迟 */
void USART1_IRQHandler(void)
{
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        /* 记录中断实际被服务的时间 */
        if (measure_idx < INTERUPT_MEASURE_COUNT)
        {
            interrupt_timestamp[measure_idx] = get_current_us();
            measure_idx++;
        }

        /* 实际的数据处理逻辑 */
        uint8_t data = USART_ReceiveData(USART1);
        rx_buffer[rx_head % USART1_RX_BUF_SIZE] = data;
        rx_head++;
    }

    /* 清除中断标志位 */
    USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}

/* 独立的任务,用于在特定时刻触发中断请求并统计延迟 */
static void measure_interrupt_delay_thread(void *parameter)
{
    uint32_t sum = 0, max_delay = 0, min_delay = 0xFFFFFFFF;
    uint32_t expected_time;

    rt_thread_mdelay(100);  /* 等待系统稳定 */

    /* 在任务中发送一个字符到UART1,这会触发一个已知时间的中断 */
    for (int i = 0; i < INTERUPT_MEASURE_COUNT; i++)
    {
        expected_time = get_current_us();
        rt_hw_usart_send(1, 'A');  /* 发送字符触发中断 */
        rt_thread_mdelay(1);       /* 等待中断完成 */

        if (measure_idx > i)
        {
            interrupt_delay[i] = interrupt_timestamp[i] - expected_time;
            sum += interrupt_delay[i];

            if (interrupt_delay[i] > max_delay)
                max_delay = interrupt_delay[i];
            if (interrupt_delay[i] < min_delay)
                min_delay = interrupt_delay[i];
        }
    }

    /* 打印统计结果 */
    rt_kprintf("===== 中断响应延迟统计 =====\n");
    rt_kprintf("测量次数: %d\n", INTERUPT_MEASURE_COUNT);
    rt_kprintf("平均延迟: %u us\n", sum / INTERUPT_MEASURE_COUNT);
    rt_kprintf("最大延迟: %u us\n", max_delay);
    rt_kprintf("最小延迟: %u us\n", min_delay);
    rt_kprintf("============================\n");

    while (1)
    {
        rt_thread_mdelay(1000);
    }
}
RT_THREAD_DEFINE(measure_task, "measure_task", measure_interrupt_delay_thread, RT_NULL, 512, 5, 10, RT_WHILE_LOOP);

二、RT-Thread中断优先级体系详解

2.1 硬件中断优先级与RTOS优先级的映射关系

理解RT-Thread中断优先级配置的第一步,是搞清楚硬件中断和RTOS任务优先级之间的对应关系。在ARM Cortex-M系列处理器上,硬件中断的优先级由NVIC(嵌套向量中断控制器)管理,它的优先级位宽取决于芯片的具体实现。比如STM32F4的NVIC支持4位优先级,也就是0-15共16个优先级级别。

RT-Thread运行在硬件中断之上,它有自己的任务优先级体系。但关键点在于:硬件中断优先级和RTOS任务优先级是两套独立的系统。硬件中断之间的竞争由NVIC决定,而硬件中断与RTOS任务之间的竞争,则由RT-Thread的中断使能/禁用机制来决定。

/*
 * 技术栈:RT-Thread 5.x + STM32F4xx (ARM Cortex-M4)
 * 示例:NVIC中断优先级组配置与优先级分配
 * 说明:正确配置优先级组是合理分配中断优先级的基础
 */
#include <rtthread.h>
#include "stm32f4xx.h"

/*
 * 优先级组配置详解:
 * ARM Cortex-M的NVIC支持将优先级位分割为"抢占优先级"和"子优先级"两部分。
 * 抢占优先级决定了中断之间的抢占能力(高抢占优先级的中断可以打断低抢占优先级的中断)。
 * 子优先级决定了同抢占优先级下中断之间的排序(相同抢占优先级的高子优先级中断先响应)。
 *
 * 优先级组配置(PriorityGroup)决定了这两个字段的位宽分配:
 *   GROUP_0: 0位抢占优先级 + 4位子优先级 = 16个子优先级级别,无抢占能力
 *   GROUP_1: 1位抢占优先级 + 3位子优先级 = 2个抢占级别,每个级别4个子优先级
 *   GROUP_2: 2位抢占优先级 + 2位子优先级 = 4个抢占级别,每个级别2个子优先级
 *   GROUP_3: 3位抢占优先级 + 1位子优先级 = 8个抢占级别,每个级别2个子优先级
 *   GROUP_4: 4位抢占优先级 + 0位子优先级 = 16个抢占级别,无子优先级
 *
 * 选择策略:
 * - 如果需要中断嵌套(一个中断可以打断另一个中断),必须使用GROUP_1及以上配置
 * - GROUP_4提供了最多的抢占级别,适合中断种类多且差异大的场景
 * - GROUP_2或GROUP_3是较为均衡的选择,兼顾抢占能力和子优先级排序
 */
void configure_nvic_priority_group(void)
{
    /* 配置优先级组为GROUP_2:2位抢占优先级,2位子优先级 */
    /* 这意味着:4个抢占优先级级别(0-3),每个级别下2个子优先级 */
    NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);

    /*
     * 分配示例:
     * 抢占优先级0(最高):系统核心中断
     *   - 子优先级0:外部中断EXTI0(电机编码器脉冲输入)
     *   - 子优先级1:定时器中断TIM2(系统心跳)
     *
     * 抢占优先级1(高):实时数据采集
     *   - 子优先级0:UART1(主通信串口)
     *   - 子优先级1:SPI1(高速传感器SPI)
     *
     * 抢占优先级2(中):辅助功能
     *   - 子优先级0:UART2(调试串口)
     *   - 子优先级1:ADC中断
     *
     * 抢占优先级3(最低):后台服务
     *   - 子优先级0:I2C1(EEPROM等低速外设)
     *   - 子优先级1:软定时器中断
     */

    /* 配置电机编码器中断——最高抢占优先级 */
    NVIC_SetPriority(EXTI0_IRQn, (0 << 2) | 0);  /* 抢占优先级0,子优先级0 */
    NVIC_EnableIRQ(EXTI0_IRQn);

    /* 配置系统心跳定时器中断 */
    NVIC_SetPriority(TIM2_IRQn, (0 << 2) | 1);  /* 抢占优先级0,子优先级1 */
    NVIC_EnableIRQ(TIM2_IRQn);

    /* 配置主通信串口中断 */
    NVIC_SetPriority(USART1_IRQn, (1 << 2) | 0);  /* 抢占优先级1,子优先级0 */
    NVIC_EnableIRQ(USART1_IRQn);

    /* 配置SPI传感器中断 */
    NVIC_SetPriority(SPI1_IRQn, (1 << 2) | 1);  /* 抢占优先级1,子优先级1 */
    NVIC_EnableIRQ(SPI1_IRQn);

    /* 配置调试串口中断 */
    NVIC_SetPriority(USART2_IRQn, (2 << 2) | 0);  /* 抢占优先级2,子优先级0 */
    NVIC_EnableIRQ(USART2_IRQn);

    /* 配置ADC中断 */
    NVIC_SetPriority(ADC_IRQn, (2 << 2) | 1);  /* 抢占优先级2,子优先级1 */
    NVIC_EnableIRQ(ADC_IRQn);

    /* 配置EEPROM I2C中断 */
    NVIC_SetPriority(I2C1_EV_IRQn, (3 << 2) | 0);  /* 抢占优先级3,子优先级0 */
    NVIC_EnableIRQ(I2C1_EV_IRQn);

    /* 打印配置结果,用于验证 */
    rt_kprintf("NVIC优先级组已配置为GROUP_2\n");
    rt_kprintf("EXTI0 抢占优先级=%d, 子优先级=%d\n",
               NVIC_GetPriority(EXTI0_IRQn) >> 2,
               NVIC_GetPriority(EXTI0_IRQn) & 0x03);
    rt_kprintf("USART1 抢占优先级=%d, 子优先级=%d\n",
               NVIC_GetPriority(USART1_IRQn) >> 2,
               NVIC_GetPriority(USART1_IRQn) & 0x03);
}

2.2 中断优先级组配置的决策依据

选择哪个优先级组,需要结合系统的具体需求来定。如果你的系统中只有一个中断源比较紧急(比如电机编码器),其他中断都可以被它打断,那么用GROUP_1就够了。如果你的系统有很多不同紧急程度的中断,而且需要细粒度的排序控制,那么GROUP_3或GROUP_4会更合适。

但有一个重要的规则需要注意:在相同抢占优先级下,子优先级更高的中断只会先被响应,但不会打断正在执行的同抢占优先级中断。也就是说,子优先级只影响"排队顺序",不影响"抢占能力"。

2.3 RT-Thread中断优先级分配策略

在RT-Thread中,中断本身并没有像任务那样的"优先级"概念。RT-Thread管理的是任务优先级,而中断只是作为唤醒任务的机制存在。所以当我们说"中断优先级配置"时,实际上是在做两件事:一是配置硬件中断的NVIC优先级(决定中断之间的响应顺序),二是设计中断与任务之间的协同机制(决定中断如何高效地唤醒对应任务)。

/*
 * 技术栈:RT-Thread 5.x + STM32F4xx (ARM Cortex-M4)
 * 示例:中断与任务优先级的协同设计
 * 说明:通过信号量和事件集将中断与任务解耦,实现高效的优先级协同
 */
#include <rtthread.h>
#include "board.h"

#define MOTOR_TASK_PRIORITY       10   /* 电机控制任务优先级(数值越小优先级越高) */
#define SENSOR_TASK_PRIORITY      20   /* 传感器数据任务优先级 */
#define COMM_TASK_PRIORITY        30   /* 通信任务优先级 */
#define HEARTBEAT_TASK_PRIORITY   40   /* 心跳监测任务优先级 */

/* 中断信号量——用于快速唤醒等待中的任务 */
static rt_sem_t motor_sem;           /* 电机编码器中断信号量 */
static rt_sem_t sensor_sem;          /* 传感器数据中断信号量 */
static rt_sem_t comm_sem;            /* 通信中断信号量 */

/* 编码器脉冲计数器 */
static volatile uint32_t encoder_count = 0;
static volatile rt_bool_t encoder_flag = RT_FALSE;

/* EXTI0中断服务程序——电机编码器脉冲 */
void EXTI0_IRQHandler(void)
{
    if (EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        /* ISR中只完成最必要的操作:计数和清除标志 */
        encoder_count++;
        encoder_flag = RT_TRUE;

        /* 释放信号量,唤醒电机控制任务 */
        /* 注意:这里使用rt_sem_release_from_isr,
           它会在中断上下文中安全地操作信号量 */
        rt_sem_release_from_isr(motor_sem);
    }

    EXTI_ClearITPendingBit(EXTI_Line0);
}

/* SPI1中断服务程序——传感器数据就绪 */
void SPI1_IRQHandler(void)
{
    if (SPI_GetITStatus(SPI1, SPI_IT_RXNE) != RESET)
    {
        uint8_t data = SPI_ReceiveData(SPI1);
        /* 这里将数据存入环形缓冲区 */
        /* 简化处理,实际项目中应使用更完善的缓冲区管理 */

        rt_sem_release_from_isr(sensor_sem);
    }

    /* 清除中断标志 */
    SPI_I2C_ClearITPendingBit(SPI1, SPI_I2C_IT_RXNE);
}

/* 电机控制任务——最高优先级任务 */
static void motor_control_thread(void *parameter)
{
    rt_s32_t result;
    uint32_t last_count = 0;

    rt_kprintf("电机控制任务启动,优先级: %d\n", MOTOR_TASK_PRIORITY);

    while (1)
    {
        /* 等待编码器中断唤醒 */
        /* timeout参数设为RT_WAITING_FOREVER,表示无限等待 */
        /* 如果不设超时,任务会一直阻塞直到收到信号量 */
        result = rt_sem_take(motor_sem, RT_WAITING_FOREVER);

        if (result == RT_EOK)
        {
            /* 获取当前编码器计数 */
            uint32_t current_count = encoder_count;

            /* 计算位置变化量 */
            uint32_t delta = current_count - last_count;
            last_count = current_count;

            /* 在这里执行电机控制逻辑 */
            /* 比如根据位置变化调整PWM输出 */
            /* 具体实现取决于电机控制算法 */

            /* 清除标志 */
            encoder_flag = RT_FALSE;
        }
    }
}

/* 传感器数据处理任务 */
static void sensor_data_thread(void *parameter)
{
    rt_s32_t result;

    rt_kprintf("传感器任务启动,优先级: %d\n", SENSOR_TASK_PRIORITY);

    while (1)
    {
        result = rt_sem_take(sensor_sem, RT_WAITING_FOREVER);

        if (result == RT_EOK)
        {
            /* 处理传感器数据 */
            /* 数据解析、滤波、融合等操作 */
            /* 注意:这里的处理不应该太长,
               否则会影响更低优先级任务的调度 */
        }
    }
}

/* 系统初始化函数 */
static int motor_system_init(void)
{
    /* 创建信号量,初始值为0 */
    motor_sem = rt_sem_create("motor_sem", 0, RT_IPC_FLAG_FIFO);
    sensor_sem = rt_sem_create("sensor_sem", 0, RT_IPC_FLAG_FIFO);
    comm_sem = rt_sem_create("comm_sem", 0, RT_IPC_FLAG_FIFO);

    if (motor_sem == RT_NULL || sensor_sem == RT_NULL || comm_sem == RT_NULL)
    {
        rt_kprintf("信号量创建失败!\n");
        return -RT_ERROR;
    }

    /* 创建电机控制任务——最高优先级 */
    rt_thread_t motor_thread = rt_thread_create(
        "motor_ctrl",                        /* 线程名称 */
        motor_control_thread,                /* 线程入口函数 */
        RT_NULL,                             /* 线程参数 */
        256,                                 /* 线程栈大小 */
        MOTOR_TASK_PRIORITY,                 /* 线程优先级 */
        5);                                  /* 时间片 */

    if (motor_thread != RT_NULL)
    {
        rt_thread_startup(motor_thread);
    }

    /* 创建传感器任务 */
    rt_thread_t sensor_thread = rt_thread_create(
        "sensor_data",
        sensor_data_thread,
        RT_NULL,
        512,
        SENSOR_TASK_PRIORITY,
        5);

    if (sensor_thread != RT_NULL)
    {
        rt_thread_startup(sensor_thread);
    }

    return RT_EOK;
}
INIT_COMPONENT_EXPORT(motor_system_init);

三、中断优先级配置的核心方法与实践

3.1 基于任务优先级的中断-任务协同设计

中断优先级配置的核心思想是:将紧急程度分为不同层次,在每一层中合理分配优先级。在实际操作中,你需要先列出系统中所有的中断源,然后按照"如果这个中断晚了会导致什么后果"来排序。

举个例子:假设你的系统有电机编码器、温度传感器、通信串口、按键输入四种中断源。电机编码器最紧急,因为它直接关系到电机位置控制,如果采样晚了可能导致过冲。温度传感器次之,虽然温度变化相对缓慢,但如果检测到过热需要立刻采取保护措施。通信串口和按键输入相对不那么紧急,它们可以在稍后处理。

3.2 中断延迟时间的精确计算

要精确控制中断延迟,你需要建立一个延迟预算模型。这个模型包含四个部分:

  1. 中断潜伏时间:从硬件中断请求到CPU真正开始执行中断服务程序的时间。这部分主要由NVIC仲裁和CPU当前指令的执行状态决定,通常在几个时钟周期到几十个时钟周期之间。

  2. 内核调度延迟:在RT-Thread中,当中断返回后,内核会检查是否有更高优先级的任务需要被唤醒。如果唤醒的任务优先级高于当前运行的任务,会发生上下文切换,这个切换过程本身也有开销。

  3. 中断服务程序执行时间:ISR本身的执行时间。好的ISR设计应该尽量简短,把复杂处理放到任务中完成。

  4. 任务唤醒到开始执行的时间:从中断释放信号量到对应任务真正被调度运行的时间。这个时间取决于当时CPU是否正在执行其他任务,以及是否有更高优先级的任务在运行。

/*
 * 技术栈:RT-Thread 5.x + STM32F4xx (ARM Cortex-M4)
 * 示例:延迟预算模型与中断延迟跟踪系统
 * 说明:实现一个完整的中断延迟测量框架,帮助开发者量化延迟
 */
#include <rtthread.h>
#include "board.h"
#include <stdint.h>
#include <string.h>

/* 延迟跟踪配置 */
#define DELAY_TRACK_SIZE          1024   /* 跟踪记录数量 */
#define DELAY_TRACK_SAMPLE_RATE   1      /* 采样间隔(每个中断都采样) */

/* 延迟跟踪数据结构 */
typedef struct
{
    uint32_t irq_number;        /* 中断号 */
    uint32_t trigger_time;      /* 中断触发时间戳 */
    uint32_t isr_entry_time;    /* ISR开始执行时间 */
    uint32_t isr_exit_time;     /* ISR结束执行时间 */
    uint32_t task_wakeup_time;  /* 对应任务被唤醒时间 */
    uint32_t latency;           /* 计算出的总延迟 */
} delay_track_entry_t;

/* 延迟跟踪环形缓冲区 */
static delay_track_entry_t track_buffer[DELAY_TRACK_SIZE];
static volatile uint32_t track_head = 0;
static volatile uint32_t track_tail = 0;
static volatile uint32_t track_count = 0;  /* 当前跟踪的记录总数 */

/* 延迟统计结构 */
typedef struct
{
    uint32_t max_latency;       /* 最大延迟 */
    uint32_t min_latency;       /* 最小延迟 */
    uint32_t total_latency;     /* 总延迟 */
    uint32_t count;             /* 样本数 */
    uint32_t avg_latency;       /* 平均延迟 */
    float    p95_latency;       /* 95%分位延迟 */
} delay_stat_t;

/* 获取高精度系统时间戳(微秒) */
static uint32_t get_us_timestamp(void)
{
    /* 使用DWT(数据_watchpoint_与追踪_单元)的周期计数器 */
    /* DWT CYCCNT提供1MHz的计数精度 */
    return DWT->CYCCNT;
}

/* 初始化DWT周期计数器 */
static void init_dwt_counter(void)
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;  /* 启用跟踪 */
    DWT->CYCCNT = 0;                                    /* 重置计数器 */
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;                /* 启动周期计数器 */
}

/*
 * 中断延迟跟踪器——在中断服务程序中调用
 * 注意:此函数在中断上下文中执行,必须极其精简
 */
static void track_interrupt_delay(uint32_t irq_number, uint32_t trigger_time)
{
    uint32_t entry_time = get_us_timestamp();

    /* 计算ISR入口延迟(从触发到ISR开始) */
    uint32_t entry_latency = entry_time - trigger_time;

    /* 将数据存入环形缓冲区 */
    if (track_count < DELAY_TRACK_SIZE)
    {
        track_buffer[track_head].irq_number = irq_number;
        track_buffer[track_head].trigger_time = trigger_time;
        track_buffer[track_head].isr_entry_time = entry_time;
        track_buffer[track_head].latency = entry_latency;
        track_head = (track_head + 1) % DELAY_TRACK_SIZE;
        track_count++;
    }
    else
    {
        /* 缓冲区满,覆盖最旧的数据 */
        track_buffer[track_tail].irq_number = irq_number;
        track_buffer[track_tail].trigger_time = trigger_time;
        track_buffer[track_tail].isr_entry_time = entry_time;
        track_buffer[track_tail].latency = entry_latency;
        track_tail = (track_tail + 1) % DELAY_TRACK_SIZE;
    }
}

/*
 * 延迟统计计算——在任务上下文中调用
 * 注意:此函数耗时较长,不应在中断上下文中调用
 */
void calculate_delay_statistics(delay_stat_t *stat)
{
    uint32_t sorted_latencies[DELAY_TRACK_SIZE];
    uint32_t i, j;
    uint32_t p95_index;

    /* 复制数据到独立数组以便排序 */
    memcpy(sorted_latencies, track_buffer,
           sizeof(uint32_t) * track_count);

    /* 初始化统计 */
    stat->max_latency = 0;
    stat->min_latency = 0xFFFFFFFF;
    stat->total_latency = 0;
    stat->count = track_count;

    /* 计算排序(简单选择排序,仅用于演示) */
    for (i = 0; i < track_count - 1; i++)
    {
        for (j = i + 1; j < track_count; j++)
        {
            if (sorted_latencies[i] > sorted_latencies[j])
            {
                uint32_t temp = sorted_latencies[i];
                sorted_latencies[i] = sorted_latencies[j];
                sorted_latencies[j] = temp;
            }
        }
    }

    /* 计算统计值 */
    stat->min_latency = sorted_latencies[0];
    stat->max_latency = sorted_latencies[track_count - 1];

    for (i = 0; i < track_count; i++)
    {
        stat->total_latency += sorted_latencies[i];
    }

    stat->avg_latency = stat->total_latency / track_count;

    /* 计算P95延迟(95%的请求延迟低于这个值) */
    p95_index = (uint32_t)((float)track_count * 0.95);
    if (p95_index >= track_count) p95_index = track_count - 1;
    stat->p95_latency = sorted_latencies[p95_index];
}

/* 延迟分析报告任务 */
static void delay_report_thread(void *parameter)
{
    delay_stat_t stat;
    uint32_t last_report_time = get_us_timestamp();

    init_dwt_counter();

    rt_kprintf("延迟跟踪系统已启动,使用DWT计数器\n");
    rt_kprintf("跟踪缓冲区大小: %d\n", DELAY_TRACK_SIZE);

    while (1)
    {
        rt_thread_mdelay(1000);  /* 每秒报告一次 */

        /* 重置跟踪缓冲区 */
        track_head = 0;
        track_tail = 0;
        track_count = 0;

        /* 在这里你可以触发各种中断来测试延迟 */
        /* 下面的代码块仅做演示 */

        /* 模拟发送一些UART数据触发中断 */
        for (int i = 0; i < 50; i++)
        {
            rt_hw_usart_send(1, 'X');
            rt_thread_delay(rt_tick_from_millisecond(1));
        }

        /* 等待所有中断处理完成 */
        rt_thread_mdelay(50);

        /* 计算统计 */
        calculate_delay_statistics(&stat);

        rt_kprintf("\n===== 延迟分析报告 =====\n");
        rt_kprintf("采样数: %u\n", stat.count);
        if (stat.count > 0)
        {
            rt_kprintf("最小延迟: %u us\n", stat.min_latency);
            rt_kprintf("最大延迟: %u us\n", stat.max_latency);
            rt_kprintf("平均延迟: %u us\n", stat.avg_latency);
            rt_kprintf("P95延迟:  %u us\n", (uint32_t)stat.p95_latency);
            rt_kprintf("抖动范围: %u us\n", stat.max_latency - stat.min_latency);
        }
        rt_kprintf("========================\n\n");
    }
}
RT_THREAD_DEFINE(delay_report, "delay_report", delay_report_thread, RT_NULL, 1024, 8, 5, RT_WHILE_LOOP);

四、中断嵌套与优先级抢占机制

4.1 中断嵌套的配置与风险

中断嵌套是指一个高优先级中断可以在另一个低优先级中断执行过程中打断它。这在Cortex-M架构中是通过NVIC的抢占优先级机制实现的。开启嵌套能减少高优先级中断的响应延迟,因为它不需要等待低优先级中断执行完毕。

但嵌套也带来了风险。如果嵌套层次太深,可能导致栈溢出。每次中断都会消耗栈空间来保存现场,嵌套5层可能就需要消耗几十到上百个字节的栈空间。此外,嵌套还会增加中断延迟的不确定性——高优先级中断的延迟取决于低优先级中断的执行时间。

4.2 嵌套场景下的优先级排序原则

在设计嵌套中断时,需要遵循几个原则:第一,只有真正紧急的中断才需要高抢占优先级。不是所有中断都需要抢占能力。第二,同抢占优先级下的中断不会互相抢占,所以如果你有一组中断不需要互相打断,可以把它们放在同一个抢占优先级下,用子优先级排序。第三,栈空间要充足。

/*
 * 技术栈:RT-Thread 5.x + STM32F4xx (ARM Cortex-M4)
 * 示例:安全的中断嵌套配置与栈空间管理
 * 说明:展示如何处理嵌套中断带来的栈压力和延迟不确定性
 */
#include <rtthread.h>
#include "stm32f4xx.h"
#include "board.h"

#define MAX_INTERRUPT_NESTING_DEPTH   4    /* 最大预期嵌套深度 */
#define NESTING_SAFE_MARGIN           3    /* 安全余量(额外预留的层数) */

/*
 * 栈空间需求估算公式:
 * 每个中断层需要的栈空间 = 中断上下文保存结构 + 函数调用栈
 * 在Cortex-M4上,中断上下文保存结构约为108字节
 * 加上函数调用栈,通常每个中断层需要约150-200字节
 *
 * 总栈空间 = 中断嵌套深度 × 每层栈空间 + 安全余量
 * 例如:4层嵌套 × 200字节/层 + 3层余量 × 200字节 = 1400字节
 *
 * 建议:为中断处理配置至少2KB的独立中断栈(MTX在Cortex-M4上支持MSP/PSP切换)
 */

/*
 * 中断嵌套计数器——用于监控嵌套深度
 * 注意:必须使用volatile关键字,因为在ISR中修改
 */
static volatile int nesting_depth = 0;
static volatile int max_observed_nesting = 0;  /* 记录观测到的最大嵌套深度 */

/* 最高优先级中断——紧急保护(例如过温保护) */
void EXTI1_IRQHandler(void)  /* 假设温度传感器连接到EXTI1 */
{
    nesting_depth++;
    if (nesting_depth > max_observed_nesting)
    {
        max_observed_nesting = nesting_depth;
    }

    /* 执行紧急保护逻辑 */
    uint32_t temp = read_temperature_sensor();

    if (temp > TEMP_CRITICAL_THRESHOLD)
    {
        /* 立即切断电机电源——这是紧急保护,优先级最高 */
        motor_power_off();
    }

    nesting_depth--;
}

/* 中等优先级中断——电机编码器 */
void EXTI0_IRQHandler(void)
{
    nesting_depth++;
    if (nesting_depth > max_observed_nesting)
    {
        max_observed_nesting = nesting_depth;
    }

    /* 读取编码器脉冲 */
    read_encoder();

    nesting_depth--;
}

/* 低优先级中断——通信 */
void USART1_IRQHandler(void)
{
    nesting_depth++;
    if (nesting_depth > max_observed_nesting)
    {
        max_observed_nesting = nesting_depth;
    }

    /* 处理通信数据 */
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        handle_rx_data();
    }

    nesting_depth--;
}

/*
 * 嵌套监控任务——定期检查嵌套深度是否异常
 * 嵌套深度异常通常意味着中断配置有问题
 */
static void nesting_monitor_thread(void *parameter)
{
    while (1)
    {
        rt_thread_mdelay(1000);  /* 每秒检查一次 */

        rt_kprintf("中断嵌套监控: 当前深度=%d, 最大观测深度=%d\n",
                   nesting_depth, max_observed_nesting);

        if (max_observed_nesting > MAX_INTERRUPT_NESTING_DEPTH)
        {
            rt_kprintf("警告: 嵌套深度超过预期值 %d !\n",
                       MAX_INTERRUPT_NESTING_DEPTH);
            rt_kprintf("建议: 检查是否有不必要的中断嵌套\n");
        }

        /* 重置统计(可选:保留历史数据也可以) */
        max_observed_nesting = 0;
    }
}
RT_THREAD_DEFINE(nesting_monitor, "nest_mon", nesting_monitor_thread,
                 RT_NULL, 256, 10, 5, RT_WHILE_LOOP);

五、典型应用场景

5.1 电机控制中的高速采样

电机控制是中断优先级配置最典型的应用场景之一。在伺服电机或步进电机控制中,编码器反馈需要以很高的频率被采样,通常要求10kHz甚至更高的采样率。这意味着编码器中断必须在微秒级别被响应。

在实际的电机控制系统中,编码器中断通常是整个系统中优先级最高的中断。它需要在中断服务程序中快速读取编码器位置值,然后释放信号量唤醒电机控制任务。控制任务负责执行PID算法并更新PWM输出。

/*
 * 技术栈:RT-Thread 5.x + STM32F4xx + DRY_RUN模式电机控制
 * 示例:高速电机编码器中断处理与优先级配置
 */
#include <rtthread.h>
#include "stm32f4xx.h"
#include "math.h"

#define MOTOR_PID_KP      2.5f   /* 比例系数 */
#define MOTOR_PID_KI      0.1f   /* 积分系数 */
#define MOTOR_PID_KD      0.5f   /* 微分系数 */
#define ENCODER_LINES     1024   /* 编码器线数 */
#define ENCODER_4X        4      /* 四倍频解码 */
#define PULSE_PER_REV     (ENCODER_LINES * ENCODER_4X)  /* 每转脉冲数 */

/* 编码器中断上下文 */
typedef struct
{
    volatile int32_t count;          /* 编码器计数 */
    volatile int32_t last_count;     /* 上次读取的计数 */
    volatile rt_uint8_t data_ready;  /* 数据就绪标志 */
} encoder_context_t;

static encoder_context_t encoder_ctx = { 0, 0, 0 };

/* 电机状态 */
typedef struct
{
    float target_position;      /* 目标位置 */
    float current_position;     /* 当前位置 */
    float velocity;             /* 当前速度 */
    float pid_output;           /* PID输出 */
    float integral;             /* 积分项累积 */
    float last_error;           /* 上次误差 */
} motor_state_t;

static motor_state_t motor_state = { 0 };

/* 编码器中断服务程序——四倍频解码 */
void EXTI0_IRQHandler(void)  /* 编码器A相 */
{
    if (EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        /* 读取A相和B相的状态 */
        uint8_t phase_a = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
        uint8_t phase_b = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1);

        /* 四倍频解码逻辑 */
        /* 方向 = A相与B相的关系 */
        /* 脉冲计数根据相位变化方向递增或递减 */
        if (phase_a == phase_b)
        {
            encoder_ctx.count++;  /* 正转 */
        }
        else
        {
            encoder_ctx.count--;  /* 反转 */
        }

        /* 设置数据就绪标志 */
        encoder_ctx.data_ready = 1;
    }

    EXTI_ClearITPendingBit(EXTI_Line0);
}

void EXTI1_IRQHandler(void)  /* 编码器B相(可选,用于提高精度) */
{
    if (EXTI_GetITStatus(EXTI_Line1) != RESET)
    {
        uint8_t phase_a = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
        uint8_t phase_b = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1);

        if (phase_a != phase_b)
        {
            encoder_ctx.count++;  /* 正转 */
        }
        else
        {
            encoder_ctx.count--;  /* 反转 */
        }

        encoder_ctx.data_ready = 1;
    }

    EXTI_ClearITPendingBit(EXTI_Line1);
}

/* 电机PID控制任务——高优先级 */
static void motor_control_task(void *parameter)
{
    float error;
    float derivative;
    float pwm_duty;
    int32_t position_change;
    static uint32_t last_time = 0;
    uint32_t current_time;
    float dt;

    /* 配置NVIC优先级——编码器中断设为最高优先级 */
    NVIC_SetPriority(EXTI0_IRQn, 0);   /* 最高抢占优先级 */
    NVIC_SetPriority(EXTI1_IRQn, 0);
    NVIC_EnableIRQ(EXTI0_IRQn);
    NVIC_EnableIRQ(EXTI1_IRQn);

    rt_kprintf("电机控制任务启动\n");

    while (1)
    {
        rt_thread_mdelay(1);  /* 1ms控制周期 */

        current_time = rt_tick_get();
        dt = (current_time - last_time) * 0.001f;  /* 转换为秒 */
        if (dt <= 0) dt = 0.001f;  /* 防止除零 */
        last_time = current_time;

        /* 读取编码器位置 */
        position_change = encoder_ctx.count - encoder_ctx.last_count;
        encoder_ctx.last_count = encoder_ctx.count;

        /* 计算当前位置(转换为圈数) */
        motor_state.current_position += (float)position_change / (float)PULSE_PER_REV;
        motor_state.velocity = position_change / (PULSE_PER_REV * dt);

        /* PID计算 */
        error = motor_state.target_position - motor_state.current_position;

        /* 积分项 */
        motor_state.integral += error * dt;
        /* 限制积分项防止积分饱和 */
        if (motor_state.integral > 1.0f) motor_state.integral = 1.0f;
        if (motor_state.integral < -1.0f) motor_state.integral = -1.0f;

        /* 微分项 */
        derivative = (error - motor_state.last_error) / dt;
        motor_state.last_error = error;

        /* PID输出 */
        motor_state.pid_output = MOTOR_PID_KP * error +
                                  MOTOR_PID_KI * motor_state.integral +
                                  MOTOR_PID_KD * derivative;

        /* 将PID输出映射到PWM占空比 (0-100%) */
        pwm_duty = motor_state.pid_output * 0.1f;
        if (pwm_duty > 100.0f) pwm_duty = 100.0f;
        if (pwm_duty < -100.0f) pwm_duty = -100.0f;

        /* 更新PWM输出 */
        update_motor_pwm(pwm_duty);

        /* 清除数据就绪标志 */
        encoder_ctx.data_ready = 0;

        /* 调试输出(可选,实际生产应移除或降低频率) */
        /* rt_kprintf("pos=%.4f vel=%.4f err=%.4f pwm=%.2f%%\n",
           motor_state.current_position, motor_state.velocity,
           error, pwm_duty); */
    }
}
RT_THREAD_DEFINE(motor_ctrl, "motor_ctrl", motor_control_task,
                 RT_NULL, 512, 5, 5, RT_WHILE_LOOP);

5.2 通信协议中的时序敏感任务

在某些通信协议中,时序要求非常严格。比如Modbus RTU协议要求字符间隔不超过一定时间,否则会被认为是一个新的帧。如果通信中断优先级设置不当,可能导致帧接收不完整。

/*
 * 技术栈:RT-Thread 5.x + STM32F4xx
 * 示例:时序敏感的通信中断处理
 * 说明:处理有严格时序要求的串口通信协议
 */
#include <rtthread.h>
#include "board.h"

#define COMM_TIMEOUT_TICKS      10       /* 通信超时时间(tick) */
#define COMM_FRAME_MAX_SIZE     256      /* 最大帧长度 */

/* 通信帧缓冲区 */
static uint8_t frame_buffer[COMM_FRAME_MAX_SIZE];
static volatile uint16_t frame_length = 0;
static volatile uint16_t frame_write_index = 0;
static volatile rt_bool_t frame_complete = RT_FALSE;

/* 信号量——通知通信任务帧已完整接收 */
static rt_sem_t frame_complete_sem;

/* 超时检测标志 */
static volatile uint32_t last_byte_time = 0;

/* UART1中断——接收数据处理 */
void USART1_IRQHandler(void)
{
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        uint8_t byte = USART_ReceiveData(USART1);
        uint32_t now = rt_tick_get();

        /* 超时检测:如果距离上一个字节超过一定时间,则认为帧结束 */
        if (frame_length > 0)
        {
            uint32_t interval = now - last_byte_time;
            if (interval > COMM_TIMEOUT_TICKS)
            {
                /* 帧超时,标记帧完成 */
                frame_length = frame_write_index;
                frame_complete = RT_TRUE;
                frame_complete_sem && rt_sem_release_from_isr(frame_complete_sem);
            }
        }

        /* 存储字节 */
        if (frame_write_index < COMM_FRAME_MAX_SIZE)
        {
            frame_buffer[frame_write_index] = byte;
            frame_write_index++;
            frame_length = frame_write_index;
            last_byte_time = now;
        }
    }

    USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}

/* 通信协议处理任务 */
static void comm_protocol_task(void *parameter)
{
    rt_s32_t result;

    while (1)
    {
        /* 等待帧完成信号 */
        result = rt_sem_take(frame_complete_sem, RT_WAITING_FOREVER);

        if (result == RT_EOK && frame_complete)
        {
            /* 处理接收到的完整帧 */
            process_comm_frame(frame_buffer, frame_length);

            /* 重置缓冲区 */
            frame_write_index = 0;
            frame_length = 0;
            frame_complete = RT_FALSE;
        }
    }
}
RT_THREAD_DEFINE(comm_proto, "comm_proto", comm_protocol_task,
                 RT_NULL, 512, 15, 5, RT_WHILE_LOOP);

六、技术优缺点分析

6.1 优先级配置的优势

合理配置中断优先级的最大好处是,它能让实时系统真正做到"实时"。高紧急度的事件不会被低紧急度的事件阻塞,系统的响应延迟更可预测。这对于电机控制、工业传感、通信协议等对时序敏感的应用至关重要。

另一个优势是调试性。当优先级配置合理时,中断之间的执行顺序是确定的。出现延迟问题时,你可以通过优先级配置来缩小排查范围。比如,如果编码器中断总是被延迟,你可以检查是否有更高优先级的中断在执行过长的代码。

6.2 潜在风险与局限性

优先级配置最大的风险是"优先级反转"。当低优先级中断持有了某个共享资源(比如通过互斥锁),而高优先级中断在等待这个资源时,高优先级任务就被低优先级任务间接阻塞了。在RT-Thread中,通常使用优先级继承机制来解决这个问题。

另一个局限是优先级配置是一次性的工作。一旦硬件中断资源确定了,优先级的分配就很难动态调整。如果后续需求发生变化(比如增加了一个新的高紧急度中断),可能需要重新评估整个优先级体系。

此外,优先级配置本身不能解决所有延迟问题。如果中断服务程序中执行了过多的代码,即使它拥有最高优先级,它的执行时间也可能影响到系统其他部分。所以ISR的设计同样重要——它应该尽量短小精悍。

七、开发注意事项

7.1 避免优先级反转

在RT-Thread中,优先级反转问题通常通过优先级继承来解决。当一个高优先级任务等待一个被低优先级任务持有的互斥锁时,低优先级任务会被临时提升到高优先级任务的优先级,等它释放锁之后再降回去。

/*
 * 技术栈:RT-Thread 5.x + STM32F4xx
 * 示例:使用优先级继承互斥锁避免优先级反转
 */
#include <rtthread.h>
#include "board.h"

static rt_mutex_t shared_resource_mutex;
static int shared_data = 0;

/* 初始化互斥锁 */
int init_mutex(void)
{
    /* 使用RT_IPC_FLAG_PRIO标志启用优先级继承 */
    shared_resource_mutex = rt_mutex_create("shared_mutex", RT_IPC_FLAG_PRIO);
    if (shared_resource_mutex == RT_NULL)
    {
        rt_kprintf("互斥锁创建失败\n");
        return -RT_ERROR;
    }
    return RT_EOK;
}

/*
 * 低优先级任务——持有时可能阻塞高优先级任务
 */
static void low_prio_task(void *parameter)
{
    while (1)
    {
        rt_mutex_take(shared_resource_mutex, RT_WAITING_FOREVER);

        /* 模拟处理共享资源 */
        shared_data++;
        rt_thread_mdelay(100);  /* 模拟耗时操作 */

        rt_mutex_release(shared_resource_mutex);
        rt_thread_mdelay(500);
    }
}

/*
 * 高优先级任务——需要访问共享资源
 * 如果低优先级任务持有了互斥锁,优先级继承机制会自动
 * 将低优先级任务的优先级提升到高优先级任务的级别
 * 防止高优先级任务被长时间阻塞
 */
static void high_prio_task(void *parameter)
{
    while (1)
    {
        rt_mutex_take(shared_resource_mutex, RT_WAITING_FOREVER);

        /* 安全访问共享数据 */
        int data = shared_data;
        rt_kprintf("高优先级任务读取数据: %d\n", data);

        rt_mutex_release(shared_resource_mutex);
        rt_thread_mdelay(50);
    }
}

/* 注册任务 */
RT_THREAD_DEFINE(low_prio, "low_prio", low_prio_task, RT_NULL, 256, 30, 5, RT_WHILE_LOOP);
RT_THREAD_DEFINE(high_prio, "high_prio", high_prio_task, RT_NULL, 256, 5, 5, RT_WHILE_LOOP);

7.2 中断服务程序的设计原则

中断服务程序(ISR)的设计有四个核心原则:

  1. 只处理必须立刻完成的操作。ISR应该只完成数据读取、标志位清除、信号量释放这些基本操作,复杂的数据处理应该交给对应的任务去做。

  2. 使用中断安全的API。在ISR中不能使用rt_kprintf等可能引起阻塞的函数,也不能使用需要上下文切换的操作。应该使用RT-Thread提供的中断安全版本API(比如rt_sem_release_from_isr)。

  3. 使用volatile修饰共享变量。任何在ISR中修改、在任务中读取的变量,或者反过来,都应该用volatile修饰,防止编译器优化导致的数据不一致。

  4. 禁止在ISR中禁用全局中断太长时间。如果需要在ISR中禁用中断,要确保禁用时间尽可能短。长时间禁用中断会导致其他中断丢失,可能影响系统的实时性。

7.3 优先级配置的检查清单

在每次修改中断优先级配置后,建议按照以下清单检查:

  1. 所有中断的NVIC优先级是否都已设置?
  2. 优先级组配置是否与中断优先级分配一致?
  3. 栈空间是否足够支撑预期的嵌套深度?
  4. ISR中是否使用了中断安全的API?
  5. ISR中是否存在可能导致阻塞的操作(如打印、互斥锁、信号量等待)?
  6. 是否存在优先级反转的风险?如果使用共享资源,互斥锁是否启用了优先级继承?
  7. 中断使能/禁用的嵌套是否正确?
# 技术栈:RT-Thread finsh/shell 命令行工具
# 示例:通过finsh查看中断和任务信息

# 查看所有任务的信息,包括优先级、状态、栈使用情况
list_thread

# 查看所有信号量的信息
list_sem

# 查看所有互斥锁的信息
list_mutex

# 查看所有事件集的信息
list_event

# 查看所有定时器(包括软件定时器)
list_timer

# 查看中断延迟统计(如果编译了RT_USING_INTERRUPT_INFO组件)
# 这个命令会显示每个中断的触发次数和平均处理时间
irq_latency

# 查看内核的统计信息
kstat

八、文章总结

中断优先级配置是嵌入式实时系统开发中一个看似简单但实则影响深远的工作。它直接关系到系统的实时性、响应速度和可靠性。在RT-Thread这样的实时操作系统上,中断优先级配置不仅仅是设置几个数字,而是一个涉及硬件中断、RTOS任务调度、资源竞争、延迟分析等多个层面的系统工程。

从本文的实践中可以看出,好的中断优先级配置需要做到以下几点:

首先,合理划分优先级层次。不要把所有中断都放在同一个优先级,也不要为了追求速度把所有中断都设为最高优先级。要根据中断的实际紧急程度分层设计。

其次,ISR要短小精悍。中断服务程序中只完成必须立刻做的事情,复杂逻辑放到对应的任务中处理。这不仅能减少中断处理时间,还能降低嵌套深度。

第三,充分利用RT-Thread的机制。信号量、事件集、优先级继承互斥锁等RT-Thread提供的IPC机制,是解决中断与任务协同、资源竞争、优先级反转等问题的有效工具。

第四,建立延迟监控机制。不要凭感觉判断延迟是否达标,要实际测量和跟踪。本文提供的延迟跟踪框架可以作为一个基础,根据具体需求进行扩展。

最后,保持配置的文档化。随着系统的演进,中断优先级配置可能会发生变化。将每次配置决策的理由记录下来,对后续的维护和调试非常有用。

实时系统开发是一个需要不断学习和优化的过程。中断优先级配置只是其中一个方面,但它往往是构建稳定实时系统的第一步。希望本文的示例和分析能为你在实际项目中提供有价值的参考。