深入理解CMSIS内核函数:__disable_irq() 与 __enable_irq()

在嵌入式开发中,中断是保证系统实时性和响应速度的重要机制。但有时我们需要暂时关闭中断,以保证某段代码的执行不被打断,例如访问共享资源、进行时序严格的操作等。CMSIS(Cortex Microcontroller Software Interface Standard)为我们提供了两个简单而强大的内核函数:__disable_irq()__enable_irq()。本文将通俗地讲解它们的作用、实现原理、使用场景以及注意事项。


1. 中断的"开关"

想象一下,你在写一封信,如果中途不断有人打断你,信的内容可能会变得混乱。在单片机里,某些代码段(比如修改一个全局变量、发送一串连续的UART数据)也"害怕"被中断打断,否则可能导致数据错误或逻辑混乱。

这时候,我们可以暂时"关上"中断的大门,等关键代码执行完再"打开"。__disable_irq() 就是用来关门,__enable_irq() 用来开门。


2. 函数介绍

这两个函数定义在CMSIS的 core_cmX.h(X取决于内核版本,如M3/M4为 core_cm3.h/core_cm4.h)中,是内联函数。

c 复制代码
__STATIC_INLINE void __disable_irq(void)
{
  __ASM volatile ("cpsid i" ::: "memory");
}

__STATIC_INLINE void __enable_irq(void)
{
  __ASM volatile ("cpsie i" ::: "memory");
}

它们本质上执行的是Cortex-M内核的汇编指令:

  • CPSID I :设置 PRIMASK=1,禁止所有可配置优先级的中断(除NMI和HardFault外)。
  • CPSIE I :清除 PRIMASK=0,重新使能中断。

所以,__disable_irq() 是全局中断的总开关,一旦关闭,除了不可屏蔽中断(NMI)和硬件错误(HardFault),其他所有中断都不会响应。


3. 典型使用场景:临界区保护

任何需要原子性操作的代码段都可以用它们来保护,这段代码称为"临界区"。

示例: 用STM32标准外设库通过USART发送一个字符串,如果发送过程中被其他中断打断(比如SysTick),可能导致字符发送不连续,接收方解析出错。我们可以这样保护:

c 复制代码
#include "stm32f10x.h"

void USART_SendString(USART_TypeDef* USARTx, char *str)
{
    __disable_irq();          // 关闭全局中断
    while (*str)
    {
        while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET);
        USART_SendData(USARTx, *str++);
    }
    __enable_irq();           // 重新开启全局中断
}

在上述代码中,整个字符串发送过程不会被其他中断打断,保证了数据的完整性和时序。


4. 注意事项

4.1 必须成对使用

每次调用 __disable_irq() 后,必须在合适的时机调用 __enable_irq() 恢复中断,否则系统可能"死机"(中断永远关闭)。

4.2 保存中断状态

如果函数可能在中断已经关闭的情况下被调用,简单的成对使用会错误地重新开启中断。更安全的做法是保存之前的PRIMASK状态,退出前恢复:

c 复制代码
uint32_t primask = __get_PRIMASK();   // 读取当前中断状态
__disable_irq();
// ... 临界区代码 ...
__set_PRIMASK(primask);               // 恢复之前的状态

或者使用CMSIS提供的 __enable_irq 的"嵌套安全"版本(如RTOS中的临界区宏),但普通裸机程序通常只需要成对使用即可。

4.3 避免长时间关闭中断

关闭中断会影响系统实时性,例如无法响应按键、通信中断等。临界区代码应尽量短小精悍,只包含必须原子执行的部分。

4.4 不适用于FreeRTOS等RTOS环境

在RTOS中,直接操作PRIMASK会破坏任务调度和临界区管理,应使用系统提供的 taskENTER_CRITICAL() / taskEXIT_CRITICAL()vPortEnterCritical() 等宏。


5. 完整示例代码

以下代码演示了在STM32F103上,利用 __disable_irq() / __enable_irq() 保护一个简单的共享计数器操作。使用标准外设库初始化USART1,并在主循环中打印计数值。

c 复制代码
#include "stm32f10x.h"
#include <stdio.h>

volatile uint32_t shared_counter = 0;

// 简单延时函数
void Delay(uint32_t nCount)
{
    for(; nCount != 0; nCount--);
}

// 重定向printf到USART1
int fputc(int ch, FILE *f)
{
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    USART_SendData(USART1, (uint8_t)ch);
    return ch;
}

void USART1_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);

    // PA9 TX, PA10 RX
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    USART_InitStructure.USART_BaudRate = 115200;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    USART_Init(USART1, &USART_InitStructure);

    USART_Cmd(USART1, ENABLE);
}

int main(void)
{
    USART1_Init();
    printf("System Start!\r\n");

    while (1)
    {
        // 进入临界区,保护shared_counter操作
        __disable_irq();
        shared_counter++;               // 关键操作:读-改-写
        uint32_t local_copy = shared_counter;
        __enable_irq();

        printf("Counter = %lu\r\n", local_copy);
        Delay(0xFFFFF);
    }
}

代码说明:

  • shared_counter++ 不是原子操作(读-改-写),如果中断服务函数中也修改这个变量,可能造成数据竞争。此处用 __disable_irq() 保护。
  • printf 函数本身依赖USART发送,我们将其放在临界区外面,避免长时间关中断。

6. 替代方案与进阶

如果只想屏蔽部分低优先级中断,可以使用 __set_BASEPRI(priority) 来设置优先级阈值,比PRIMASK更灵活。

对于复杂应用,建议使用RTOS提供的临界区API,它们会自动处理嵌套和状态保存。


7. 总结

  • __disable_irq() / __enable_irq() 是CMSIS提供的全局中断开关函数,操作PRIMASK寄存器。
  • 它们适用于裸机环境中的短临界区保护。
  • 使用时务必注意成对出现,并尽量缩短关闭中断的时间。

希望本文能帮助你快速掌握这两个内核函数的使用。如果觉得有用,欢迎点赞、收藏、关注!


参考资料:

  • ARM Cortex-M3/M4 Technical Reference Manual
  • CMSIS Core Documentation
相关推荐
LCG元3 小时前
【实战】STM32 从零实现 Modbus RTU 从站:寄存器映射、CRC 校验与串口中断解析
stm32·单片机·嵌入式硬件
云泽8084 小时前
深入浅出 STM32(十九):从寄存器原理到 volatile 避坑实战
stm32·单片机·嵌入式硬件
wuyk5555 小时前
《WiFi 嵌入式物联网开发全套实战》| 第01章 WiFi整体架构详解:BSS/ESS/AP/STA模式彻底区分
开发语言·stm32·单片机·嵌入式硬件·mcu·物联网·51单片机
wuyk5555 小时前
第六章:CAN 调试工具、抓包分析、全套故障排查 + CAN-FD 进阶拓展
开发语言·stm32·单片机
那年窗外下的雪.5 小时前
STM32嵌入式学习 01:GPIO 输出与 LED 闪烁
stm32·单片机·学习
雨田言炎7 小时前
STM32专题之内部SRAM
stm32·单片机·嵌入式硬件
芯片设计-曾工9 小时前
YL1621(SSOP28)国产段码LCD驱动芯片|PIN‑PIN兼容HT1621,储能电量显示板CH2505B国产化替换方案
stm32·单片机·嵌入式硬件·mcu·硬件工程
玩转单片机与嵌入式19 小时前
深入浅出TinyML 16:FFT、频带能量和MFCC分别适合什么任务?
stm32·深度学习·tinyml
youshi66681 天前
stm32-PWR、低功耗模式
stm32·单片机·嵌入式硬件