STM32 临界区是什么:为什么有时候要用 __disable_irq() 保护变量

STM32 临界区是什么:为什么有时候要用 __disable_irq() 保护变量

前面写外部中断按键时,我们遇到一个很容易让新手停住的东西:

bash 复制代码
__disable_irq()

__enable_irq()

很多人第一次看到它,心里会嘀咕:

bash 复制代码
这是什么?

为什么读一个按键事件,还要关中断?

关中断会不会很危险?

不用行不行?

这些问题都很正常。

因为从 LED、蜂鸣器、普通 GPIO 输入一路学过来,我们写的大部分代码都是"主循环自己跑自己的"。到了外部中断这里,程序突然变成了:

bash 复制代码
主循环在跑

中断也可能随时插进来

这时候,有些变量就不是一个地方在用,而是主循环和中断都在用。

这篇就专门把这个问题讲清楚:

bash 复制代码
什么是临界区?

为什么主循环和中断共享变量时要小心?

__disable_irq() 到底保护了什么?

什么时候该用,什么时候不要乱用?

本篇不写新工程,也不展开新代码。我们只把原理讲透。

先从外部中断按键说起

在外部中断按键那一篇里,按键按下后,大概会发生这样的流程:

bash 复制代码
按键电平变化

  -> EXTI 触发中断

  -> 进入 HAL 回调

  -> 记录"按键按下事件"

  -> 主循环发现这个事件

  -> 翻转 LED

这里最关键的不是 LED,也不是 EXTI,而是这句:

bash 复制代码
记录"按键按下事件"

通常我们会用一个变量来表示这件事:

bash 复制代码
0:没有按键事件

1:有按键按下事件

中断里负责把它置为 1。

主循环里负责读取它,然后清成 0。

听起来很简单,对吧?

但问题也正是从这里开始的。

一个变量,被两个地方同时惦记

我们先不看代码,用一张表理解。

|

位置

|

会对事件变量做什么

|

| --- | --- |

|

中断回调

|

按键来了,把事件变量置为 1

|

|

主循环

|

发现事件变量是 1,处理它,然后清成 0

|

这就叫:

bash 复制代码
共享变量

它被两个执行环境共享:

bash 复制代码
主循环

中断

注意,"共享"本身不是问题。

真正的问题是:

bash 复制代码
中断可能在主循环执行到一半的时候突然插进来

也就是说,主循环不是从头到尾安安静静执行完,再轮到中断。

中断是随时可能打断它的。

这就像你正在记账:

bash 复制代码
你刚看到账本上写着:有 1 笔待处理

正准备把它处理掉

突然有人又塞进来 1 笔新的

你没注意,顺手把待处理标记清空了

最后结果就是:

bash 复制代码
新来的那一笔被你清掉了

在程序里,这种现象就叫事件丢失。

为什么"读一下再清零"也可能出问题

很多新手会觉得:

bash 复制代码
我不就是读一下变量,再把它清零吗?

这么简单也会出问题?

会。

原因是:你看到的一行 C 语句,到了 CPU 执行时,往往不是一个不可拆开的动作。

比如"读取事件、判断事件、清除事件",从逻辑上看是一件事。

但 CPU 真正执行时,它可能是几步:

bash 复制代码
读取变量

判断变量

准备清零

写回清零

中断可能刚好插在这些步骤中间。

举个时间线:

|

时刻

|

发生了什么

|

| --- | --- |

|

1

|

主循环读取事件变量,发现是 1

|

|

2

|

主循环准备处理这个事件

|

|

3

|

就在这时,又来一次按键中断

|

|

4

|

中断把事件变量再次置为 1

|

|

5

|

中断结束,回到主循环

|

|

6

|

主循环继续执行,把事件变量清成 0

|

看起来每一步都合理。

但最后结果是:

bash 复制代码
第二次按键事件丢了

因为中断刚置好的 1,被主循环后面的清零覆盖了。

这就是临界区要解决的问题。

临界区到底是什么

临界区不是 STM32 独有的概念。

它在单片机、RTOS、Linux、上位机多线程里都会出现。

用最朴素的话说:

bash 复制代码
临界区,就是一小段不能被打断的关键操作

它通常有两个特点:

  1. 这段代码正在访问共享资源;

  2. 如果执行到一半被打断,结果可能出错。

在按键事件里,共享资源就是:

bash 复制代码
按键事件变量

主循环读取它、清除它。

中断回调修改它。

所以主循环在"读取并清除事件"的这一小段里,需要确保:

bash 复制代码
不要刚读到一半,中断又进来改同一个变量

这段"读取并清除事件"的操作,就是临界区。

__disable_irq() 做了什么

__disable_irq() 的作用可以先简单理解为:

bash 复制代码
临时禁止 CPU 响应普通中断

对应的 __enable_irq() 就是:

bash 复制代码
重新允许 CPU 响应中断

所以它们经常成对出现:

bash 复制代码
进入临界区:先关中断

访问共享变量:读、改、清

退出临界区:再开中断

它想保证的是:

bash 复制代码
这几步操作一口气做完,中间不要被中断插队

放回按键事件里,就是:

bash 复制代码
主循环读取事件变量

主循环清除事件变量

这两个动作之间,不允许按键中断插进来改同一个变量

这样就能避免某些非常隐蔽的事件丢失。

它不是为了"消抖"

这里一定要分清楚。

__disable_irq() 不是用来消抖的。

消抖解决的是:

bash 复制代码
机械按键刚按下/松开时,电平会乱跳

临界区解决的是:

bash 复制代码
主循环和中断同时访问同一个变量,可能互相打断

这两个问题经常出现在同一个按键工程里,但它们不是一回事。

可以这样记:

|

问题

|

解决手段

|

| --- | --- |

|

按键电平抖动

|

延时确认、状态机、定时扫描

|

|

共享变量被打断

|

临界区、关中断、原子操作、队列

|

所以你在按键外部中断代码里看到 __disable_irq(),它保护的是事件变量,不是过滤按键抖动。

为什么不一直关中断

既然关中断可以避免被打断,那能不能在一大段代码前面关掉,处理完再打开?

不建议。

甚至可以说,绝大多数情况下不要这么干。

因为中断本来就是用来及时响应外部事件的。

如果你关中断时间太长,可能会出现这些问题:

  • 串口接收不及时,数据丢失;

  • 定时器中断延后,时间不准;

  • 按键响应变慢;

  • PWM、ADC、DMA 回调被拖延;

  • 系统里某些依赖中断的逻辑变得不稳定。

所以临界区有一个很重要的原则:

bash 复制代码
关中断时间越短越好

只保护真正需要"一口气完成"的几行操作。

不要在关中断期间做这些事:

  • printf()

  • HAL_Delay()

  • 等待某个外设完成;

  • 复杂计算;

  • 大量循环;

  • 读写 Flash;

  • 访问慢速通信外设。

这些动作都可能耗时太久。

临界区应该像过马路:

bash 复制代码
看准、快速通过、马上恢复交通

不要站在路中间聊天。

volatile 能不能替代临界区

很多人还会问:

bash 复制代码
共享变量加 volatile 不就行了吗?

不行。

volatile 和临界区解决的不是同一个问题。

volatile 主要是告诉编译器:

bash 复制代码
这个变量可能随时被别的地方改变

每次用它时都要真的去内存里读,不要自作聪明缓存起来

它解决的是编译器优化问题。

但它不能保证:

bash 复制代码
读变量

判断变量

清变量

这几步不会被中断打断。

所以:

bash 复制代码
volatile 让你读到真实变化

临界区让关键读改操作不被插队

两个概念都重要,但不能互相替代。

在主循环和中断共享变量时,经常会同时看到:

bash 复制代码
变量用 volatile 修饰

读改清操作放进临界区

这不是重复,而是各管一件事。

是不是所有共享变量都要关中断

也不是。

这就要看风险。

如果中断只是设置一个标志,主循环只是读取这个标志,并且不清除、不修改,那风险相对低。

如果主循环要做"读完再清",就要小心。

如果变量是多字节数据,比如 16 位、32 位计数值,在某些平台或某些访问方式下,也要考虑读到一半被打断的问题。

更典型的场景有:

|

场景

|

为什么要小心

|

| --- | --- |

|

按键事件标志

|

中断置位,主循环读后清零,可能丢事件

|

|

串口接收计数

|

中断增加计数,主循环读取和清理

|

|

定时器毫秒计数

|

中断更新,主循环读取多字节值

|

|

环形缓冲区读写指针

|

中断写入,主循环读取

|

|

ADC/DMA 完成标志

|

回调置位,主循环处理后清除

|

判断要不要保护,可以先问自己一句:

bash 复制代码
这个变量是不是会被中断和主循环同时读写?

如果答案是"是",再问第二句:

bash 复制代码
如果中断刚好插在中间,会不会丢事件、读错值、覆盖状态?

如果答案还是"可能会",那就应该考虑临界区。

除了关全局中断,还有没有别的办法

有。

__disable_irq() 是最直接、最容易理解的办法,但不是唯一办法。

随着工程复杂起来,你会遇到更多选择:

|

方法

|

适合场景

|

| --- | --- |

|

短时间关全局中断

|

裸机小工程,保护很短的共享变量操作

|

|

只关闭某个外设中断

|

不想影响所有中断,只保护某一路中断相关变量

|

|

使用计数而不是 0/1 标志

|

避免多个事件挤在一起只记成 1 次

|

|

环形缓冲区

|

串口、CAN、按键队列等多个事件缓存

|

|

RTOS 临界区

|

FreeRTOS 等系统里用系统提供的临界区接口

|

|

消息队列/事件组

|

任务和中断之间传递事件更清楚

|

新手阶段先掌握一个原则就够了:

bash 复制代码
裸机里保护几行共享变量操作,可以用短临界区;

工程变复杂后,再考虑队列、缓冲区、RTOS 接口。

不要一上来就把简单按键事件写成很复杂的框架。

但也不要完全不知道临界区,等到丢事件时才到处怀疑硬件。

回到按键事件:它到底保护了什么

现在再回头看外部中断按键。

中断回调里做的事很简单:

bash 复制代码
按键来了,把事件标志置起来

主循环里做的事也很简单:

bash 复制代码
看看有没有事件

如果有,就拿走这个事件

顺手把标志清掉

真正需要保护的,就是"拿走并清掉"这一小段。

为什么?

因为它必须保证:

bash 复制代码
我拿到的这个事件,确实被我处理了;

我清掉的这个标志,不会误清掉中断刚刚新放进来的事件。

所以临界区不是为了显得代码高级,也不是固定套路。

它是在告诉 CPU:

bash 复制代码
这几行我正在处理共享变量,先别让中断插队。

处理完马上恢复中断。

这就是它的边界。

新手最容易踩的 5 个坑

1. 把临界区写得太大

临界区只包住共享变量的关键操作。

不要把 LED 控制、串口打印、延时等待都放进去。

2. 以为 volatile 就万事大吉

volatile 能避免变量被编译器优化掉,但不能保证多步操作不被中断打断。

3. 忘了重新开中断

关中断后一定要确保能重新打开。

如果中间写了复杂逻辑、提前返回、错误分支,就容易忘。

所以临界区越短,越不容易出错。

4. 在中断里做太多事情

中断里长时间运行,会影响其他中断响应。

按键中断里记录事件就够了,具体业务放主循环。

5. 用 0/1 标志记录高频事件

如果事件来得很快,0/1 标志只能表示"有过",不能表示"来了几次"。

这时候要考虑计数、队列或缓冲区。

按键这种低频事件,用 0/1 标志通常够用;串口数据这种高频事件,就不太够了。

本篇小结

临界区不是一个神秘概念。

它解决的就是一句话:

bash 复制代码
主循环和中断都要访问同一个变量时,有几步关键操作不能被中断插队。

__disable_irq()__enable_irq() 的作用,就是临时把这几步包起来:

bash 复制代码
先暂停普通中断响应

快速处理共享变量

马上恢复中断响应

你需要记住这几个判断:

  • __disable_irq() 不是用来消抖的;

  • 它保护的是主循环和中断共享的变量;

  • volatile 不能替代临界区;

  • 临界区要尽量短;

  • 不要在临界区里打印、延时、等待外设;

  • 中断里少做事,主循环里做业务;

  • 事件多了以后,要考虑计数、队列或缓冲区。

如果把外部中断按键那篇和这篇连起来看,思路就很清楚了:

bash 复制代码
EXTI 负责发现按键变化

中断回调负责记录事件

临界区负责安全取走事件

主循环负责处理 LED 翻转

这就是裸机 STM32 里非常常见的一种工程写法。

下一次你在代码里看到 __disable_irq(),先别急着害怕,也别随手删掉。

先问一句:

bash 复制代码
这里是不是在保护一个主循环和中断共享的变量?

如果答案是,那它大概率不是多余的。

相关推荐
leo_jk10 小时前
STM32单片机 空闲中断
stm32·单片机·嵌入式硬件
weyyhdke10 小时前
2026电源与MCU控制设计实战:用Gemini3.5镜像站免费优化开关电源环路与电机FOC算法硬核教程
单片机·嵌入式硬件·算法
星夜夏空9910 小时前
STM32单片机学习(22) —— I2C通信协议
stm32·单片机·学习
拾知_H11 小时前
STM32/串口控制LED亮灭
stm32·单片机·嵌入式硬件·串口
szxinmai主板定制专家11 小时前
基于ZYNQ MPSOC ARM+FPGA的超高清实时图像采集与压缩系统设计
linux·运维·服务器·arm开发·人工智能·嵌入式硬件·fpga开发
iCxhust11 小时前
个人计算机的起点,INTEL 8088
c语言·单片机·嵌入式硬件·微机原理·8088单板机
国科安芯11 小时前
国科安芯AS32A601芯片及ANSIC-EVB601开发平台获OneWo-zepLinux全面适配支持
网络·单片机·嵌入式硬件·risc-v·安全性测试
都在酒里11 小时前
算法总结(二)深入浅出 PID 控制算法:原理、优化与 STM32 标准库实现
stm32·算法·pid算法·位置pid·增式pid
ACP广源盛1392462567311 小时前
OpenAI 推出的 GPT-5.5 大模型,倒逼接口芯片升级迭代@ACP#IX8024应用迭代
网络·人工智能·嵌入式硬件·电脑·音视频