uCOS 原理 - 临界段代码保护

tech

This article was last updated on <span id="expire-date"></span> days ago, the information described in the article may be outdated.

在 Cortex 内核中有着大量的中断向量,当中断被设置并且发生的时候,系统就会从 Thread 模式切换至 Handler 模式;而 NVIC 则保证了中断可嵌套。

但是有的时候我们希望某些代码的执行过程中不要被中断,这些代码被称为临界段代码 Critical Section;那么,在 uCOS 中,系统又是如何做到的呢?

临界段代码是指那些需要连续运行,不可以被打断的代码;一般在STM32上我们有两种需要关注的临界段代码:

  1. 外设初始化相关代码

    部分外设的初始化强依赖于时序,如果在这些初始化代码的执行过程中触发了中断可能会导致外设初始化失败或者不可预测的行为。

  2. 不可重入的函数

    我们看一个例子就可以明白什么叫做不可重入函数了:

    1
    2
    3
    4
    5
    6
    static int temp;
    void swap(int* x, int* y){
    temp = *x;
    *x = *y;
    *y = temp
    }

    现在某一个低优先级的任务正在执行 swap 函数,并且已经执行完 temp = *x 这条指令了,假设这个时候 temp 存储在栈上并且被赋值为 1

    此时发生了中断,某一个高优先级的任务抢占了 CPU,并且该高优先级任务也调用了 swap 函数,将 temp 赋值为了 3

    当高优先级任务释放了 CPU 使用权,奇怪的事情发生了,原本应该被赋值为 1temp 变量被刷写为了 3,这就导致变量 y 的值在该函数调用完成后出现了错误。

而应对临界段代码,在 uCOS-II 中可以首先关闭中断,当临界段代码执行完毕后在重新开启中断;在实际使用中,我们只需要使用两个宏函数包裹需要的临界段代码即可:

1
2
3
OS_ENTER_CRITICAL();
/* Critical Section Code */
OS_EXIT_CRITICAL();

uCOS-II 中提供了三种方法保护临界段代码:

  1. 第一种方法是使用一条指令关闭中断,在退出临界段代码时重新开启中断。

    这种方法不可以嵌套,如果用户在进入临界段代码之前就已经关闭了中断,那么无论如何在退出时中断都会被重新开启,这往往不是我们想看到的。

  2. 第二种方法是将 xPSR 寄存器的状态入栈之后在关闭中断,退出临界段代码时出栈即可。

    但是这种方法有时会出现很严重的问题:因为 Cortex-M3 内核是允许从 SP 寄存器相对寻址的,并且大部分的编译器在函数内部都是这么做的,那么如果我们使用堆栈去保存 xPSR 寄存器状态时,编译器未必能够察觉到我们操作了 SP 指针,而在临界段的代码可能会因此出现严重的问题。

    举个例子:

    1
    2
    3
    4
    int a = 0xABCD1234;
    OS_ENTER_CRITICAL();
    func(a);
    OS_EXIT_CRITICAL();

    假设在某个任务函数内的局部变量 a 保存在了栈上,我们进入临界段代码之后栈指针被推动了(为了保存 xPSR 的内容),但是编译器却并没有察觉到我们修改了 SP 指针,从而变量 a 仍然是按照推动之前的栈指针进行相对寻址,这会导致严重的问题。

  3. 第三个方法是我们最常用的方法,在这种方法我们使用一个变量保存当前处理器的中断使能状态,在退出临界段代码时恢复之前的状态;这样就避免了前面说到的两个问题。

在 uCOS-II 中通过定义 OS_CRITICAL_METHOD 可以选中我们想要的保护方式;因为前两种方法会出现各种各样的问题,事实上我们在 Cortex-M3 中只使用方法三完成临界段保护。

然而无论如何,关闭中断的函数也需要一定的时间去完成,当调用关闭中断的宏函数 OS_ENTER_CRITICAL 后,为了尽快的完成关闭中断的任务,方法三在 STM32 中 uCOS-II 使用了三条汇编指令:

1
2
3
4
CPU_SR_Save
MRS R0, PRIMASK
CPSID I
BX LR

在使用时,我们需要定义一个名为 cpu_sr 的变量,用于保存当前的中断使能状态;在保存状态之后使用 CPSID I 指令禁止中断,需要注意的是硬件失败仍然会被响应,这里禁止的仅仅是 ISR;最后跳转至我们需要执行的临界段代码即可。

另外一个需要注意的事情就是在临界段代码内 不能够 使用任何的中断资源,有朋友可能会觉得这是没有必要强调的,但事实是,我们仍然需要小心的对待。例如我们在临界段内使用了一个 delay 延迟函数,而很不巧,这个延迟的时基是由 SysTick 中断提供的,这就会导致我们的代码 hang 在 delay 函数内部。

Author: 桂小方

Permalink: https://init.blog/ucos-cortex-critical-section/

文章许可协议:

如果你觉得文章对你有帮助,可以 支持我

Comments