FreeRTOS(6)——临界区保护

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 时,系统才真正恢复中断屏蔽状态。

这能避免一个常见错误:外层代码还没有执行完,内层函数退出时却把中断提前打开了。

临界区里不要做什么

临界区应该尽量短。下面这些操作不适合放在临界区里:

  1. vTaskDelay() 这类主动阻塞的 API。
  2. 等待队列、信号量、互斥量,并且等待时间不为 0 的 API。
  3. 耗时很长的循环、打印、大块内存处理。
  4. 可能触发复杂外设等待的代码。

原因很简单:临界区期间调度和一部分中断被推迟。你把临界区写得越长,系统实时性就越差。临界区应该像一次很短的“锁门改账本”:进去,改完,立刻出来。

临界区和任务调度的关系

FreeRTOS 的任务调度依赖系统节拍中断和 PendSV。临界区屏蔽了 FreeRTOS 管理范围内的中断以后,任务切换自然也会被推迟。

所以临界区的保护能力很强:

  • 可以防止任务之间的抢占。
  • 可以防止 FreeRTOS 管理范围内的 ISR 介入。
  • 可以保护任务和 FreeRTOS 可管理 ISR 都可能访问的共享资源。

但它的代价也明显:

  • 中断响应会被延迟。
  • 系统节拍处理可能被推迟。
  • 临界区过长会伤害实时性。

因此,只有当资源会被 ISR 访问,或者代码必须连中断都不能插入时,才优先考虑临界区。

一个实用判断

写 FreeRTOS 代码时,可以这样判断要不要用临界区:

问题建议
资源会被任务和 ISR 同时访问吗使用临界区,ISR 中使用 FROM_ISR 版本
只是多个任务之间抢同一个资源吗可以优先考虑挂起调度器、互斥量或其他同步机制
代码会不会阻塞或等待不要放进临界区
代码是否很短,并且必须原子完成可以使用临界区

小结

临界区保护的核心是:进入时关闭 FreeRTOS 管理范围内的中断,退出时恢复,从而保证一小段代码完整执行。

使用时记住四点:

  1. 任务中使用 taskENTER_CRITICAL()taskEXIT_CRITICAL()
  2. 中断中使用 taskENTER_CRITICAL_FROM_ISR()taskEXIT_CRITICAL_FROM_ISR()
  3. API 必须成对使用,并且支持嵌套。
  4. 临界区必须短,不能把阻塞、等待和耗时代码放进去。

临界区是 FreeRTOS 中很强的保护手段,但强不代表应该滥用。它适合解决“这段代码绝对不能被打断”的问题;如果只是任务之间不想互相抢资源,下一篇的调度器挂起和恢复会更轻一些。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇