RTOS串口驱动:中断服务函数优先级与多任务互斥 - 第4节

4.1 中断向量表:CPU 响应中断的入口索引

中断向量表(Interrupt Vector Table, IVT)是处理器在响应中断时查找服务函数入口地址的固定数据结构。对于 ARM Cortex-M 系列内核,向量表默认位于地址 0x00000000,其中前 16 个为系统异常向量,从第 17 个开始为外部中断向量。

核心概念: 每个中断源对应向量表中的一个 4 字节入口,存放该中断的服务函数地址。当 CPU 检测到中断请求且优先级满足条件时,硬件自动从向量表中取出对应地址并跳转执行。
RTOS 注意事项: 在启用 RTOS 后,通常需要将向量表重定位到 SRAM 区域(通过 SCB->VTOR 寄存器设置),以便在运行时动态更新中断服务函数指针,支持中断服务函数的动态注册。
ARM Cortex-M 中断向量表结构 中断向量表(起始地址 0x00000000) 偏移地址 中断类型 服务函数入口 0x0000 初始栈指针 (MSP) __initial_sp 0x0004 复位异常 (Reset) Reset_Handler 0x0008 NMI 异常 NMI_Handler ... 系统异常 (16个) ... 0x0040 外部中断 #0 (IRQ0) USART1_IRQHandler 0x0044 外部中断 #1 (IRQ1) USART2_IRQHandler ... 外部中断 (最多 240 个) ... 串口中断入口 💡 向量表偏移:通过 SCB->VTOR 寄存器可重定位向量表,RTOS 常将其移至 SRAM 以实现动态注册

4.2 NVIC 配置:中断优先级与使能控制

嵌套向量中断控制器(NVIC)是 Cortex-M 内核中管理中断的核心外设。NVIC 负责中断的使能/除能、挂起/清除、优先级分组以及优先级设置。在串口驱动中,正确配置 NVIC 是确保中断响应实时性的前提。

4.2.1 优先级分组与抢占优先级

Cortex-M3/M4 支持 8 位优先级寄存器,但具体实现位数由芯片厂商决定(通常为 4 位)。通过 NVIC_PriorityGroupConfig() 设置优先级分组,决定抢占优先级和子优先级的位数分配。

分组编号 抢占优先级位数 子优先级位数 抢占优先级等级数 适用场景
NVIC_PriorityGroup_0 0 4 1 所有中断同级,无嵌套
NVIC_PriorityGroup_1 1 3 2 简单系统,两级嵌套
NVIC_PriorityGroup_2 2 2 4 常用配置,平衡嵌套深度
NVIC_PriorityGroup_3 3 1 8 RTOS 推荐,多级嵌套
NVIC_PriorityGroup_4 4 0 16 完全抢占,无子优先级
RTOS 关键约束: 在 RTOS 环境下,通常将系统节拍定时器(SysTick)中断设置为最高抢占优先级,串口中断设置为中等优先级,任务级代码运行在最低优先级(即线程模式)。务必确保串口中断优先级不高于 SysTick,否则可能导致系统调度延迟。

4.2.2 NVIC 配置代码模板

以下代码展示了串口中断的 NVIC 配置流程,包括使能中断、设置优先级和清除挂起标志。

/* 串口中断 NVIC 配置函数 */
void USART_NVIC_Config(void)
{
    NVIC_InitTypeDef NVIC_InitStructure;

    /* 1. 设置优先级分组:3 位抢占 + 1 位子优先级 */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_3);

    /* 2. 配置 USART1 中断 */
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    /* 3. 设置抢占优先级为 2(0 最高,7 最低) */
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
    /* 4. 设置子优先级为 0 */
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    /* 5. 使能中断通道 */
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    /* 6. 清除可能的挂起标志(上电后可能遗留) */
    NVIC_ClearPendingIRQ(USART1_IRQn);
}
NVIC 配置流程与中断优先级决策 设置优先级分组 NVIC_PriorityGroup_3 选择中断通道 USART1_IRQn 设置优先级值 抢占=2, 子=0 使能中断通道 ENABLE 清除挂起标志 NVIC_ClearPendingIRQ 配置完成 中断已就绪 ⚠️ 优先级决策原则: • SysTick 中断优先级应设为最高(抢占=0),确保系统节拍不受阻塞 • 串口中断优先级设为中等(抢占=2~3),避免长时间屏蔽其他外设中断

4.3 中断服务函数(ISR)编写基础

中断服务函数是中断驱动串口接收/发送的核心执行体。ISR 必须遵循“短小精悍”原则——只做最必要的操作,将耗时处理推迟到任务级。串口 ISR 的典型职责包括:读取数据寄存器、判断中断类型、将数据存入缓冲区、通知任务处理。

4.3.1 ISR 命名与向量表关联

在 STM32 标准库中,中断服务函数名已在启动文件中预定义。开发者只需实现同名函数即可自动关联到向量表。常见串口 ISR 名称如下:

中断向量名称 对应外设 触发条件
USART1_IRQHandler USART1 接收/发送/错误中断
USART2_IRQHandler USART2 接收/发送/错误中断
USART3_IRQHandler USART3 接收/发送/错误中断
UART4_IRQHandler UART4 接收/发送/错误中断

4.3.2 串口 ISR 标准框架

以下代码展示了一个完整的串口接收中断服务函数框架,包含中断类型判断、数据读取和缓冲区写入。

/* 串口1中断服务函数 */
void USART1_IRQHandler(void)
{
    uint8_t data;
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    /* 1. 判断中断类型:接收中断 */
    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        /* 2. 读取数据寄存器(硬件自动清除 RXNE 标志) */
        data = (uint8_t)(USART1->DR & 0xFF);

        /* 3. 将数据写入接收环形缓冲区 */
        if (xQueueSendFromISR(xRxQueue, &data, &xHigherPriorityTaskWoken) != pdPASS)
        {
            /* 缓冲区满:溢出处理(可置位溢出标志) */
            g_uart_overflow = 1;
        }

        /* 4. 退出 ISR 时请求上下文切换 */
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }

    /* 5. 判断中断类型:发送完成中断 */
    if (USART_GetITStatus(USART1, USART_IT_TC) != RESET)
    {
        /* 清除发送完成标志 */
        USART_ClearITPendingBit(USART1, USART_IT_TC);

        /* 通知发送任务可以继续发送下一帧 */
        xSemaphoreGiveFromISR(xTxSemaphore, &xHigherPriorityTaskWoken);
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }

    /* 6. 判断中断类型:错误中断(溢出/帧错误/噪声) */
    if (USART_GetITStatus(USART1, USART_IT_ORE) != RESET)
    {
        USART_ClearITPendingBit(USART1, USART_IT_ORE);
        /* 记录错误计数 */
        g_uart_error_count++;
    }
}
ISR 编写黄金法则:
• 使用 FromISR 后缀的 API 函数(如 xQueueSendFromISR)操作 RTOS 对象
• 通过 portYIELD_FROM_ISR 触发上下文切换,确保高优先级任务及时执行
• 避免在 ISR 中调用阻塞函数(如 vTaskDelay)或打印调试信息
• 中断标志位清除操作必须在 ISR 返回前完成,防止重复触发

4.4 中断使能与全局控制

除了 NVIC 通道使能,串口外设本身也提供中断使能寄存器。开发者需要在串口初始化阶段,通过 USART_ITConfig() 函数开启特定中断类型。

/* 串口中断使能配置(在 USART_Init 之后调用) */
void USART_InterruptEnable(void)
{
    /* 使能接收中断:当接收到数据时触发 */
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

    /* 使能发送完成中断:当数据发送完成时触发 */
    USART_ITConfig(USART1, USART_IT_TC, ENABLE);

    /* 使能错误中断:检测溢出、帧错误等 */
    USART_ITConfig(USART1, USART_IT_ORE, ENABLE);

    /* 注意:不要使能发送缓冲区空中断(TXE),
       除非使用中断方式发送,否则会持续触发 */
}
串口中断使能层级结构 CPU 全局中断 __enable_irq() NVIC 通道使能 NVIC_Init() RXNE 接收中断 USART_ITConfig() TC 发送完成中断 USART_ITConfig() 三层使能必须全部开启,中断才能到达 ISR。任何一层未使能,中断均不会触发。

4.5 中断优先级与 RTOS 调度互斥

在 RTOS 环境下,中断优先级设置直接影响系统实时性和任务调度的确定性。错误的优先级配置可能导致优先级反转、死锁或任务饿死。

RTOS 中断优先级设计准则:
  1. SysTick 最高优先级: 系统节拍中断必须设置为最高抢占优先级(通常为 0),确保时间片轮转和任务切换不受阻塞。
  2. 串口中断中等优先级: 建议设置为 2~3(抢占优先级),避免长时间屏蔽其他外设中断,同时保证串口数据不丢失。
  3. 临界区保护: 在 ISR 中访问共享资源(如环形缓冲区)时,使用 taskENTER_CRITICAL_FROM_ISR() 保护,防止与任务级代码产生竞态。
  4. 中断延迟控制: 串口 ISR 执行时间应控制在微秒级,避免影响其他中断的响应延迟。
/* 中断优先级配置示例(适用于 FreeRTOS) */
void System_Interrupt_Config(void)
{
    /* 设置优先级分组:3 位抢占 + 1 位子优先级 */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_3);

    /* SysTick 中断:最高优先级 */
    NVIC_SetPriority(SysTick_IRQn, 0);

    /* 串口中断:中等优先级 */
    NVIC_SetPriority(USART1_IRQn, 2);

    /* 其他外设中断:较低优先级 */
    NVIC_SetPriority(TIM2_IRQn, 4);
    NVIC_SetPriority(EXTI0_IRQn, 5);
}
常见错误: 将串口中断优先级设置为与 SysTick 相同或更高,会导致以下问题:
• 串口 ISR 执行期间,SysTick 无法抢占,系统节拍丢失,任务调度延迟
• 高频率串口数据流可能导致系统节拍长时间无法响应,最终触发看门狗复位
• 在 FreeRTOS 中,portYIELD_FROM_ISR 依赖于 SysTick 中断进行上下文切换,若串口优先级高于 SysTick,切换请求可能被延迟

4.6 本章小结