FreeRTOS 中的临界区,也叫临界段,指的是一段必须完整执行、不能在中途被打断的代码。
在裸机程序里,我们主要担心中断打断当前代码;到了 RTOS 中,除了中断,还多了任务调度。一个任务正在修改共享数据时,可能被更高优先级任务抢占;一个任务正在按固定时序配置外设时,也可能被中断插入。只要这段代码被打断后会破坏数据一致性或时序要求,它就需要临界区保护。
临界区适合保护什么
课堂里把临界区的使用场合分成三类:
| 场合 | 例子 | 为什么需要保护 |
|---|---|---|
| 外设时序 | IIC、SPI、某些初始化序列 | 配置步骤必须连续执行,中途插入其他代码可能导致外设状态异常 |
| 系统内部 | FreeRTOS 内核链表、任务状态、计数变量 | 内核数据结构必须保持一致,不能改到一半被调度走 |
| 用户代码 | 多个任务共享的变量、缓冲区、状态机 | 防止一个任务读到另一个任务尚未更新完成的数据 |
临界区并不是“所有共享资源都要无脑关中断”。它更适合短小、必须原子完成、不能阻塞的代码段。代码越长,中断和调度被推迟的时间越长,系统实时性就越差。
FreeRTOS 临界区的基本思路
FreeRTOS 进入临界区时会关闭可被内核管理的中断,退出临界区时再恢复中断。这样做以后,当前代码不会被普通任务调度打断,也不会被 FreeRTOS 管理范围内的中断打断。
在 Cortex-M 移植层中,这个“关中断”通常不是简单粗暴地屏蔽所有硬件中断,而是通过 BASEPRI 屏蔽 configMAX_SYSCALL_INTERRUPT_PRIORITY 及其以下优先级的中断。也就是说,高于 FreeRTOS 管理范围的紧急中断仍然可以响应。
从应用层理解,可以先记住一句话:
临界区用牺牲一小段时间的中断响应和任务调度,换取这段代码的原子性。
任务级临界区 API
任务中使用的 API 是:
| API | 作用 |
|---|---|
taskENTER_CRITICAL() | 进入临界区 |
taskEXIT_CRITICAL() | 退出临界区 |
典型写法如下:
taskENTER_CRITICAL();
{
/* 必须连续执行的代码 */
}
taskEXIT_CRITICAL();
例如多个任务都会修改同一个全局变量时,可以这样保护:
static uint32_t g_count;
void task1(void *argument)
{
while (1)
{
taskENTER_CRITICAL();
{
g_count++;
}
taskEXIT_CRITICAL();
vTaskDelay(pdMS_TO_TICKS(100));
}
}
这里的重点不是 g_count++ 这行代码有多复杂,而是它在机器指令层面可能包含“读、改、写”多个动作。如果中途被其他任务打断,就可能出现计数丢失。
中断级临界区 API
如果临界区代码位于中断服务函数中,要使用带 FROM_ISR 后缀的版本:
| API | 作用 |
|---|---|
taskENTER_CRITICAL_FROM_ISR() | 在 ISR 中进入临界区,并保存中断屏蔽状态 |
taskEXIT_CRITICAL_FROM_ISR() | 在 ISR 中退出临界区,并恢复之前保存的状态 |
使用格式如下:
void EXTI15_10_IRQHandler(void)
{
UBaseType_t uxSavedInterruptStatus;
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
{
/* ISR 中必须保护的短代码 */
}
taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);
}
中断里不要使用任务级的 taskENTER_CRITICAL()。ISR 有自己的上下文和返回路径,必须用 ISR 专用版本保存并恢复原来的中断状态。
为什么临界区必须成对使用
临界区的第一条规则就是成对使用:
taskENTER_CRITICAL();
/* ... */
taskEXIT_CRITICAL();
如果只进入不退出,系统可能长时间处于中断屏蔽状态,任务调度也无法正常发生。这个问题非常隐蔽,因为代码可能不会立刻崩溃,而是表现为定时器不走、任务不切换、外设响应变慢。
写临界区时可以养成一个习惯:先写好进入和退出,再往中间填代码。
临界区支持嵌套
临界区和简单的“关中断、开中断”最大的区别之一,是它支持嵌套。
taskENTER_CRITICAL();
{
/* 第一层临界区 */
taskENTER_CRITICAL();
{
/* 第二层临界区 */
}
taskEXIT_CRITICAL();
/* 仍然还在第一层临界区内 */
}
taskEXIT_CRITICAL();
FreeRTOS 内部会维护临界区嵌套计数。每进入一次临界区,计数加一;每退出一次,计数减一。只有计数回到 0 时,系统才真正恢复中断屏蔽状态。
这能避免一个常见错误:外层代码还没有执行完,内层函数退出时却把中断提前打开了。
临界区里不要做什么
临界区应该尽量短。下面这些操作不适合放在临界区里:
vTaskDelay()这类主动阻塞的 API。- 等待队列、信号量、互斥量,并且等待时间不为 0 的 API。
- 耗时很长的循环、打印、大块内存处理。
- 可能触发复杂外设等待的代码。
原因很简单:临界区期间调度和一部分中断被推迟。你把临界区写得越长,系统实时性就越差。临界区应该像一次很短的“锁门改账本”:进去,改完,立刻出来。
临界区和任务调度的关系
FreeRTOS 的任务调度依赖系统节拍中断和 PendSV。临界区屏蔽了 FreeRTOS 管理范围内的中断以后,任务切换自然也会被推迟。
所以临界区的保护能力很强:
- 可以防止任务之间的抢占。
- 可以防止 FreeRTOS 管理范围内的 ISR 介入。
- 可以保护任务和 FreeRTOS 可管理 ISR 都可能访问的共享资源。
但它的代价也明显:
- 中断响应会被延迟。
- 系统节拍处理可能被推迟。
- 临界区过长会伤害实时性。
因此,只有当资源会被 ISR 访问,或者代码必须连中断都不能插入时,才优先考虑临界区。
一个实用判断
写 FreeRTOS 代码时,可以这样判断要不要用临界区:
| 问题 | 建议 |
|---|---|
| 资源会被任务和 ISR 同时访问吗 | 使用临界区,ISR 中使用 FROM_ISR 版本 |
| 只是多个任务之间抢同一个资源吗 | 可以优先考虑挂起调度器、互斥量或其他同步机制 |
| 代码会不会阻塞或等待 | 不要放进临界区 |
| 代码是否很短,并且必须原子完成 | 可以使用临界区 |
小结
临界区保护的核心是:进入时关闭 FreeRTOS 管理范围内的中断,退出时恢复,从而保证一小段代码完整执行。
使用时记住四点:
- 任务中使用
taskENTER_CRITICAL()和taskEXIT_CRITICAL()。 - 中断中使用
taskENTER_CRITICAL_FROM_ISR()和taskEXIT_CRITICAL_FROM_ISR()。 - API 必须成对使用,并且支持嵌套。
- 临界区必须短,不能把阻塞、等待和耗时代码放进去。
临界区是 FreeRTOS 中很强的保护手段,但强不代表应该滥用。它适合解决“这段代码绝对不能被打断”的问题;如果只是任务之间不想互相抢资源,下一篇的调度器挂起和恢复会更轻一些。









