在嵌入式系统中,中断服务程序(ISR)是响应硬件事件的核心机制,直接决定系统的实时性和可靠性。

ISR的本质

ISR与普通函数区别

普通函数
显式调用
执行可预测
无严格时间限制
使用任务栈
ISR
硬件自动触发
异步不可预测
执行时间极短
可能使用独立中断栈
中断嵌套风险

特性

普通函数

ISR

调用方式

显式函数调用

触发调用

执行时机

程序流程决定

异步、不可预测

进入/退出

简单跳转/返回

保存/恢复完整上下文

执行时间

无严格限制

必须极短

栈使用

使用当前任务栈

可能使用独立中断栈

可重入性

可重入设计

通常不可重入,需特别处理

资源访问

常规方式

需考虑与主程序/其他ISR的竞争

设计原则

// ❌ 错误示例:ISR中做太多事情
voidUART_IRQHandler(void) {
    char buffer[256];
    int i;
    
    // 接收数据
    for (i = 0; i < 256; i++) {
        while (!(USART1->SR & USART_SR_RXNE));
        buffer[i] = USART1->DR;
    }
    
    // 复杂的数据处理
    process_large_data(buffer, 256);
    
    // 发送响应
    send_response_over_uart(buffer);
}

// ✅ 正确示例:ISR只做最必要的工作
volatileuint8_t uart_rx_buffer[256];
volatileuint16_t uart_rx_index = 0;
volatileuint8_t uart_data_ready = 0;

voidUART_IRQHandler(void) {
    if (USART1->SR & USART_SR_RXNE) {
        uint8_t data = USART1->DR;
        
        if (uart_rx_index < 255) {
            uart_rx_buffer[uart_rx_index++] = data;
        }
        
        if (data == '\n') {
            uart_data_ready = 1;
        }
    }
}

// 在主循环中处理数据
voidmain_loop(void) {
    while (1) {
        if (uart_data_ready) {
            process_large_data(uart_rx_buffer, uart_rx_index);
            send_response_over_uart(uart_rx_buffer);
            uart_rx_index = 0;
            uart_data_ready = 0;
        }
    }
}

特点

  1. 1. 精简高效:ISR执行时间应尽量短(通常<10% CPU周期)

  2. 2. 避免阻塞:禁止使用delay()等阻塞式函数

  3. 3. 减少复杂度:避免在ISR中进行复杂计算、大量数据处理

  4. 4. 任务分离:ISR只做数据接收/发送,复杂处理交给主循环或任务

可重入性

什么是可重入性

可重入函数是指在被调用过程中再次被中断并重新进入,仍能正确执行的函数。

是
否
函数执行
被高优先级中断打断
重新进入同一函数
是否可重入?
正确执行
数据破坏/系统崩溃

可重入函数的条件

  1. 1. 不使用全局或静态变量

  2. 2. 不修改自身代码

  3. 3. 只调用可重入函数

// ❌ 不可重入函数:使用静态变量
intbad_counter(void) {
    staticint count = 0;  // 静态变量,不可重入
    count++;
    return count;
}

// ✅ 可重入函数:使用参数传递状态
intgood_counter(int *count) {
    (*count)++;  // 使用外部传入的指针
    return *count;
}

// ❌ 不可重入:返回指向静态数据的指针
char *bad_format_time(time_t t) {
    staticchar buffer[32];  // 静态缓冲区
    strftime(buffer, sizeof(buffer), "%Y-%m-%d", localtime(&t));
    return buffer;  // 多次调用会覆盖同一缓冲区
}

// ✅ 可重入:调用者提供缓冲区
voidgood_format_time(time_t t, char *buffer, size_t size) {
    strftime(buffer, size, "%Y-%m-%d", localtime(&t));
}

ISR中的可重入问题

// ❌ 危险示例:ISR和主程序共享全局变量
uint32_t system_tick = 0;

voidSysTick_Handler(void) {
    system_tick++;  // 读-改-写操作,可能被打断
}

uint32_tget_tick(void) {
    return system_tick;  // 32位读取在8位/16位MCU上不是原子操作
}

// ✅ 安全示例1:使用volatile + 临界区保护
volatileuint32_t system_tick = 0;

voidSysTick_Handler(void) {
    system_tick++;
}

uint32_tget_tick(void) {
    uint32_t tick;
    __disable_irq();
    tick = system_tick;
    __enable_irq();
    return tick;
}

// ✅ 安全示例2:使用原子操作(C11/C17)
#include <stdatomic.h>

atomic_uint_fast32_t system_tick = ATOMIC_VAR_INIT(0);

voidSysTick_Handler(void) {
    atomic_fetch_add(&system_tick, 1);
}

uint32_tget_tick(void) {
    returnatomic_load(&system_tick);
}

浮点运算的特殊处理

在ISR中使用浮点数时,需要特别注意浮点寄存器的保存和恢复。

// ❌ 危险:在ISR中使用浮点运算(某些架构)
float float_value = 0.0f;

voidTIM_IRQHandler(void) {
    float_value += 0.1f;  // 可能破坏主程序的浮点上下文
}

// ✅ 安全:编译器特定指令保存浮点上下文
#pragma SAVE
#pragma FPU
voidTIM_IRQHandler(void) {
    float_value += 0.1f;
}
#pragma RESTORE

// ✅ 更好:避免在ISR中使用浮点数
volatileuint32_t integer_counter = 0;

voidTIM_IRQHandler(void) {
    integer_counter++;  // 使用整数,主循环中再转换
}

voidmain_loop(void) {
    while (1) {
        float float_value = integer_counter * 0.1f;  // 在主循环计算
    }
}

临界区保护

什么是临界区

临界区是指访问共享资源的代码段,在同一时间只能有一个执行流进入。

读取
修改
主程序
共享变量
ISR

临界区保护的常用方法

关中断(最简单但影响大)
// 基础实现
voidcritical_section_example(void) {
    __disable_irq();  // 关中断
    
    // 临界区代码
    shared_data++;
    
    __enable_irq();  // 开中断
}

// ✅ 更安全:保存中断状态
voidsafe_critical_section(void) {
    uint32_t primask = __get_PRIMASK();  // 保存当前中断状态
    __disable_irq();
    
    // 临界区代码
    shared_data++;
    
    __set_PRIMASK(primask);  // 恢复之前的状态
}

// ✅ 封装成宏(推荐)
#define CRITICAL_SECTION_ENTER() \
    uint32_t __primask = __get_PRIMASK(); \
    __disable_irq()

#define CRITICAL_SECTION_EXIT() \
    __set_PRIMASK(__primask)

voidusage_example(void) {
    CRITICAL_SECTION_ENTER();
    shared_data++;
    CRITICAL_SECTION_EXIT();
}
使用基元屏蔽(只屏蔽相关中断)
// ✅ 更好:只屏蔽相关中断,不影响其他中断
void uart_specific_protection(void) {
    NVIC_DisableIRQ(UART1_IRQn);  // 只屏蔽UART1中断
    
    // 临界区:只访问UART相关的共享数据
    uart_rx_buffer[uart_rx_index++] = data;
    
    NVIC_EnableIRQ(UART1_IRQn);  // 恢复UART1中断
}
使用互斥锁(RTOS环境)
// ✅ 在RTOS中使用互斥锁
#include "FreeRTOS.h"
#include "semphr.h"

SemaphoreHandle_t xMutex;

void rtos_critical_section(void) {
    if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
        // 临界区代码
        shared_data++;
        
        xSemaphoreGive(xMutex);
    }
}

中断嵌套处理

什么是中断嵌套

中断嵌套是指高优先级中断打断正在执行的低优先级中断。

高优先级ISR低优先级ISR主程序高优先级ISR低优先级ISR主程序低优先级中断触发高优先级中断触发正常执行进入低优先级ISR执行中...进入高优先级ISR执行完成返回低优先级ISR继续执行返回主程序

Cortex-M中的优先级配置

#include "stm32f4xx.h"

voidNVIC_Configuration(void) {
    // 设置优先级分组:抢占优先级2位,子优先级2位
    // 抢占优先级:决定是否可以打断其他中断
    // 子优先级:决定同优先级中断的响应顺序
    NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
    
    // 配置高优先级中断(抢占优先级0)
    NVIC_SetPriority(EXTI0_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(), 0, 0));
    NVIC_EnableIRQ(EXTI0_IRQn);
    
    // 配置中优先级中断(抢占优先级1)
    NVIC_SetPriority(TIM2_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(), 1, 0));
    NVIC_EnableIRQ(TIM2_IRQn);
    
    // 配置低优先级中断(抢占优先级2)
    NVIC_SetPriority(UART1_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(), 2, 0));
    NVIC_EnableIRQ(UART1_IRQn);
}

常见问题处理

忘清除中断标志
// ❌ 错误:忘清除中断标志,导致ISR反复触发
void Bad_TIM_IRQHandler(void) {
    g_counter++;
    // 忘记清除TIM中断标志!
}

// ✅ 正确:及时清除中断标志
void Good_TIM_IRQHandler(void) {
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);  // 先清除标志
        g_counter++;
    }
}
ISR中调用非重入函数
// ❌ 危险:在ISR中调用malloc()
voidBad_ISR(void) {
    char *buffer = malloc(256);  // malloc可能不可重入
    // 使用buffer
    free(buffer);
}

// ✅ 正确:预先分配缓冲区
staticchar isr_buffer[256];  // 静态缓冲区

voidGood_ISR(void) {
    // 使用预分配的缓冲区
    isr_buffer[0] = data;
}
volatile使用不当
// ❌ 错误:共享变量未用volatile声明
uint8_t flag = 0;  // 编译器可能优化掉读取操作

voidISR(void) {
    flag = 1;
}

voidmain_loop(void) {
    while (!flag);  // 编译器可能优化成死循环!
    process_data();
}

// ✅ 正确:使用volatile
volatileuint8_t flag = 0;  // 告诉编译器变量可能被意外修改

voidISR(void) {
    flag = 1;
}

voidmain_loop(void) {
    while (!flag);  // 正确读取
    process_data();
}
printf调试ISR
// ❌ 错误:在ISR中使用printf
void Bad_ISR(void) {
    printf("ISR triggered\r\n");  // printf很慢,可能导致问题
}

// ✅ 正确:使用简单的标志或GPIO
volatile uint8_t isr_triggered = 0;

void Good_ISR(void) {
    isr_triggered = 1;
    GPIOA->BSRR = GPIO_PIN_0;  // 翻转GPIO(最快)
}

总结

中断服务程序是嵌入式系统中最关键也最容易出错的部分,遵循这些规范,便于写出安全、高效、可靠的中断服务程序。

‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

关注我的微信公众号,回复“星球”加入知识星球,有问必答。
点击“阅读原文”查看知识星球详情,欢迎点分享、收藏、点赞、在看。
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐