在嵌入式开发中,中断是保证系统实时性和响应速度的重要机制。但有时我们需要暂时关闭中断,以保证某段代码的执行不被打断,例如访问共享资源、进行时序严格的操作等。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