嵌入式 C:临界区与关中断
更新时间:2026-09-21。嵌入式/裸机视角。位操作的读改写隐患见 寄存器位操作宏。
一句话回答
当主循环和中断服务函数会同时改同一份数据(或同一个寄存器的不同位)时,把这段读改写代码用「关中断」包成临界区:进入时屏蔽中断并记下原来的开关状态,退出时恢复——保证这几条指令不会被中断打断,但临界区要尽量短,否则会延迟中断响应。
一、为什么需要
flags |= BIT(n); 在机器层面是「读—改—写」多条指令。若执行到一半被中断,而 ISR 里也改了 flags,返回后主循环用旧值继续写,就把 ISR 的改动覆盖掉(丢失更新)。
二、ARM Cortex-M 关中断
Cortex-M 用 PRIMASK 屏蔽可屏蔽中断,CPSID i 关、CPSIE i 开。CMSIS 提供:
c
#include "cmsis_gcc.h" // 或通过设备头间接包含
uint32_t primask = __get_PRIMASK(); // 保存当前状态
__disable_irq(); // 关中断
/* ---- 临界区:操作共享变量 ---- */
counter++;
shared_reg |= BIT(2);
/* ---- 结束 ---- */
__set_PRIMASK(primask); // 恢复(而不是无脑开中断)封装成宏:
c
#define ENTER_CRITICAL() uint32_t _pm = __get_PRIMASK(); __disable_irq()
#define EXIT_CRITICAL() __set_PRIMASK(_pm)保存/恢复而不是固定 __enable_irq(),是为了嵌套调用时不会在本应关闭的地方误开。
三、典型用法
c
void queue_push(uint8_t b) {
ENTER_CRITICAL();
buf[head++] = b; // 主循环/ISR 共享环形缓冲
head &= (SIZE - 1);
EXIT_CRITICAL();
}常见坑
- 临界区太长:在里面做延时、串口打印、复杂运算,导致中断响应被拖、丢字节/丢脉冲。
- 退出时无脑开中断:嵌套临界区里提前打开,破坏外层的保护。
- 只保护写、不保护读:读到一半被改也是撕裂数据,共享对象的读改写都要在临界区内。
- 以为关中断能挡住 NMI/HardFault:PRIMASK 不屏蔽 NMI 和 HardFault,别假设它绝对隔离。
- 多核误用:关本核中断挡不住另一核;多核需要自旋锁/硬件信号量。
总结
主循环与 ISR 共享数据时,用保存 PRIMASK、__disable_irq() 进入、__set_PRIMASK 恢复的临界区包住读改写,保证不被中断打断;临界区务必最短、进出成对,且它只挡可屏蔽中断、不解决多核并发。