找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4505

积分

0

好友

577

主题
发表于 前天 01:24 | 查看: 15| 回复: 0

在云栈社区的嵌入式开发讨论中,中断服务程序(ISR)始终是一个绕不开的核心话题。它承担着响应硬件事件的重任,直接决定系统的实时性与可靠性。一个编写不当的 ISR,轻则让系统响应迟缓,重则引发难以排查的随机故障。那么,怎么才能写出合格的 ISR 呢?

ISR 的本质

ISR 与普通函数的区别

很多人刚开始接触嵌入式开发时,会把 ISR 当成一个“特殊的函数”来看。但 ISR 和普通函数的差异,远不止写法上的不同。下表梳理了二者的核心区别:

特性 普通函数 ISR
调用方式 显式函数调用 触发调用
执行时机 程序流程决定 异步、不可预测
进入/退出 简单跳转/返回 保存/恢复完整上下文
执行时间 无严格限制 必须极短
栈使用 使用当前任务栈 可能使用独立中断栈
可重入性 可重入设计 通常不可重入,需特别处理
资源访问 常规方式 需考虑与主程序/其他ISR的竞争

设计原则

ISR 的设计原则可以用一句话概括:快进快出,只做最必要的事。下面这个例子,很直观地展示了初学者最容易犯的错误,以及对应的正确写法。

// ❌ 错误示例:ISR中做太多事情
void UART_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只做最必要的工作
volatile uint8_t uart_rx_buffer[256];
volatile uint16_t uart_rx_index = 0;
volatile uint8_t uart_data_ready = 0;

void UART_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;
        }
    }
}

// 在主循环中处理数据
void main_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. 精简高效:ISR 执行时间应尽量短(通常 <10% CPU 周期)
  2. 避免阻塞:禁止使用 delay() 等阻塞式函数
  3. 减少复杂度:避免在 ISR 中进行复杂计算、大量数据处理
  4. 任务分离:ISR 只做数据接收/发送,复杂处理交给主循环或任务

可重入性

什么是可重入性

所谓可重入函数,是指在被调用过程中再次被中断并重新进入,仍能正确执行的函数。这个概念直接关系到 ISR 的安全性——如果你的 ISR 里调用了一个不可重入函数,就相当于埋下了一颗不定时炸弹。

可重入函数的条件

一个函数要做到可重入,必须同时满足以下三条:

  1. 不使用全局或静态变量
  2. 不修改自身代码
  3. 只调用可重入函数
// ❌ 不可重入函数:使用静态变量
int bad_counter(void) {
    static int count = 0;  // 静态变量,不可重入
    count++;
    return count;
}

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

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

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

ISR 中的可重入问题

ISR 和主程序共享全局变量是最常见的隐患。当 32 位变量在 8 位或 16 位 MCU 上被读取时,一次读操作可能被拆成多条指令,这期间如果来了中断,读到的值就可能面目全非。

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

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

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

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

void SysTick_Handler(void) {
    system_tick++;
}

uint32_t get_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);

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

uint32_t get_tick(void) {
    return atomic_load(&system_tick);
}

浮点运算的特殊处理

在 ISR 中使用浮点数需要格外小心。某些架构下,浮点寄存器不会自动保存和恢复,ISR 里的浮点运算可能会悄悄破坏主程序的浮点上下文。

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

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

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

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

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

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

临界区保护

什么是临界区

临界区是指访问共享资源的代码段,在同一时间只能有一个执行流进入。如果没有保护,多个执行流同时读写同一份数据,轻则数据错乱,重则系统崩溃。临界区保护到底有哪些常用手段?

临界区保护的常用方法

关中断(最简单但影响大)

关中断是最直接的办法,但也最粗暴——它会暂时屏蔽所有中断,影响系统实时性。

// 基础实现
void critical_section_example(void) {
    __disable_irq();  // 关中断

    // 临界区代码
    shared_data++;

    __enable_irq();  // 开中断
}

// ✅ 更安全:保存中断状态
void safe_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)

void usage_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,互斥锁能提供更优雅的同步机制。

// ✅ 在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);
    }
}

中断嵌套处理

什么是中断嵌套

中断嵌套是指高优先级中断打断正在执行的低优先级中断。这种机制在实时系统中很常见,但如果优先级配置不当,也会引发优先级反转等问题。

Cortex-M 中的优先级配置

以 STM32F4 为例,Cortex-M 内核通过抢占优先级和子优先级共同决定中断响应顺序。

#include "stm32f4xx.h"

void NVIC_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 反复触发,直接耗尽 CPU 资源。

// ❌ 错误:忘清除中断标志,导致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 中调用非重入函数

malloc()free() 这类库函数往往不可重入,在 ISR 里调用它们极有可能引发难以追踪的问题。

// ❌ 危险:在ISR中调用malloc()
void Bad_ISR(void) {
    char *buffer = malloc(256);  // malloc可能不可重入
    // 使用buffer
    free(buffer);
}

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

void Good_ISR(void) {
    // 使用预分配的缓冲区
    isr_buffer[0] = data;
}

volatile 使用不当

共享变量不加 volatile 是新手最常见的失误之一。编译器优化可能把你认为会重新读取的变量直接缓存起来,导致主循环永远等不到 ISR 设置的值。

// ❌ 错误:共享变量未用volatile声明
uint8_t flag = 0;  // 编译器可能优化掉读取操作

void ISR(void) {
    flag = 1;
}

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

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

void ISR(void) {
    flag = 1;
}

void main_loop(void) {
    while (!flag);  // 正确读取
    process_data();
}

printf 调试 ISR

在 ISR 里用 printf 打印调试信息,听起来很自然,实际上会引入严重的实时性问题。

// ❌ 错误:在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(最快)
}

总结

ISR 是嵌入式系统中最关键也最容易出错的部分。遵循这些编写规范,可以帮助你避开常见陷阱,写出安全、高效且可靠的中断服务程序。如果你在实践中遇到过其他棘手的中断问题,也不妨回到云栈社区与更多嵌入式开发者一起交流,共同少走弯路。




上一篇:嵌入式C语言volatile的5个常见误区:编译器优化与并发安全区别
下一篇:嵌入式C语言实现继承:结构体嵌套、向上转型与 container_of 实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-9-4 20:03 , Processed in 0.803514 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表