良许Linux教程网 干货合集 单片机非RTOS时,临界区保护的实现办法

单片机非RTOS时,临界区保护的实现办法

今天我要分享的是三种在Cortex-M裸机环境下实现临界区保护的方法。

如果你曾经使用过RTOS,你可能非常熟悉OS_ENTER_CRITICAL() 和 OS_EXIT_CRITICAL() 这些代码功能。在RTOS中,经常需要处理多个任务(进程),有些情况下需要对特殊操作(例如对XIP下的Flash进行擦除或切换到低功耗模式)进行保护,或者必须互斥地访问某些共享数据区(例如任务A正在读取,任务B需要写入)。这时,就需要使用临界区保护策略。

临界区保护策略指的是系统中硬件临界资源或软件临界资源,多个任务必须互斥地对其进行访问。在RTOS环境下,已经提供了现成的临界区保护接口函数。然而,在裸机环境下也需要这种保护机制。在裸机环境中,临界区保护主要与系统全局中断控制有关。我之前写过一篇名为《嵌入式MCU中通用的三重中断控制设计》的文章,其中介绍的第三重也是最顶层的中断控制——系统全局中断控制。今天,我将从使用系统全局中断控制入手,介绍三种实现临界区保护的方法:

一、临界区保护测试场景

关于临界区保护的测试场景无非两种。第一种场景是受保护的多个任务间并无关联,也不会互相嵌套,如下面的代码所示,task1 和 task2 是按序被保护的,因此 enter_critical() 和 exit_critical() 这两个临界区保护函数总是严格地成对执行:

void critical_section_test(void)
{
    // 进入临界区
    enter_critical();
    // 做受保护的任务1
    do_task1();
    // 退出临界区
    exit_critical();

    // 进入临界区
    enter_critical();
    // 做受保护的任务2,与任务1无关联
    do_task2();
    // 退出临界区
    exit_critical();
}

第二种场景就是多个任务间可能有关联,会存在嵌套情况,如下面的代码所示,task2 是 task1 的一个子任务,这种情况下,你会发现实际上是先执行两次 enter_critical(),然后再执行两次 exit_critical()。需要注意的是 task1 里面的子任务 task3 虽然没有像子任务 task2 那样被主动加一层保护,但由于主任务 task1 整体是受保护的,因此子任务 task3 也应该是受保护的。

void do_task1(void)
{
    // 进入临界区
    enter_critical();
    // 做受保护的任务2,是任务1中的子任务
    do_task2();
    // 退出临界区
    exit_critical(); 

    // 做任务3
    do_task3();
}

void critical_section_test(void)
{
    // 进入临界区
    enter_critical();
    // 做受保护的任务1
    do_task1();
    // 退出临界区
    exit_critical();
}

二、临界区保护三种实现

上面的临界区保护测试场景很清楚了,现在到 enter_critical()、exit_critical() 这对临界区保护函数的实现环节了:

2.1 入门做法

首先是非常入门的做法,直接就是对系统全局中断控制函数 __disable_irq()、__enable_irq() 的封装。回到上一节的测试场景里,这种实现可以很好地应对非嵌套型任务的保护,但是对于互相嵌套的任务保护就失效了。上一节测试代码里,task3 应该也要受到保护的,但实际上并没有被保护,因为紧接着 task2 后面的 exit_critical() 直接就打开了系统全局中断。

void enter_critical(void)
{
    // 关闭系统全局中断
    __disable_irq();
}

void exit_critical(void)
{
    // 打开系统全局中断
    __enable_irq();
}

2.2 改进做法

针对入门做法,可不可以改进呢?当然可以,我们只需要加一个全局变量 s_lockObject 来实时记录当前已进入的临界区保护的次数,即如下代码所示。每调用一次 enter_critical() 都会直接关闭系统全局中断(保证临界区一定是受保护的),并记录次数,而调用 exit_critical() 时仅当当前次数是 1 时(即当前不是临界区保护嵌套情况),才会打开系统全局中断,否则只是抵消一次进入临界区次数而已。改进后的实现显然可以保护上一节测试代码里的 task3 了。

static uint32_t s_lockObject;

void init_critical(void)
{
    __disable_irq();
    // 清零计数器
    s_lockObject = 0;
    __enable_irq();
}

void enter_critical(void)
{
    // 关闭系统全局中断
    __disable_irq();
    // 计数器加 1
    ++s_lockObject;
}

void exit_critical(void)
{
    if (s_lockObject else
    {
        // 当计数器大于 1 时,直接计数器减 1 即可
        --s_lockObject;
    }
}

2.3 终极做法

上面的改进做法虽然解决了临界区任务嵌套保护的问题,但是增加了一个全局变量和一个初始化函数,实现不够优雅,并且嵌入式系统里全局变量极容易被篡改,存在一定风险,有没有更好的实现呢?当然有,这要借助 Cortex-M 处理器内核的特殊屏蔽寄存器 PRIMASK,下面是 PRIMASK 寄存器位定义(取自 ARMv7-M 手册),其仅有最低位 PM 是有效的,当 PRIMASK[PM] 为 1 时,系统全局中断是关闭的(将执行优先级提高到 0x0/0x80);当 PRIMASK[PM] 为 0 时,系统全局中断是打开的(对执行优先级无影响)。

image-20231117223108952
image-20231117223108952

看到这,你应该明白了 __disable_irq()、__enable_irq() 功能其实就是操作 PRIMASK 寄存器实现的。既然 PRIMASK 寄存器控制也保存了系统全局中断的开关状态,我们可以通过获取 PRIMASK 值来替代上面改进做法里的全局变量 s_lockObject 的功能,代码实现如下:

uint32_t enter_critical(void)
{
    // 保存当前 PRIMASK 值
    uint32_t regPrimask = __get_PRIMASK();
    // 关闭系统全局中断(其实就是将 PRIMASK 设为 1)
    __disable_irq();

    return regPrimask;
}

void exit_critical(uint32_t primask)
{
    // 恢复 PRIMASK
    __set_PRIMASK(primask);
}

因为 enter_critical()、exit_critical() 函数原型有所变化,因此使用上也要相应改变下:

void critical_section_test(void)
{
    // 进入临界区
    uint32_t primask = enter_critical();
    // 做受保护的任务
    do_task();
    // 退出临界区
    exit_critical(primask);

    // ...
}

附录、PRIMASK寄存器设置函数在各 IDE 下实现

//////////////////////////////////////////////////////
// IAR 环境下实现(见 cmsis_iccarm.h 文件)
#define __set_PRIMASK(VALUE)        (__arm_wsr("PRIMASK", (VALUE)))
#define __get_PRIMASK()             (__arm_rsr("PRIMASK"))

//////////////////////////////////////////////////////
// Keil 环境下实现(见 cmsis_armclang.h 文件)
__STATIC_FORCEINLINE void __set_PRIMASK(uint32_t priMask)
{
  __ASM volatile ("MSR primask, %0" : : "r" (priMask) : "memory");
}

__STATIC_FORCEINLINE uint32_t __get_PRIMASK(void)
{
  uint32_t result;

  __ASM volatile ("MRS %0, primask" : "=r" (result) );
  return(result);
}

至此,Cortex-M裸机环境下临界区保护的三种实现已经讲述完毕,你学废了吗?

以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !

137e00002230ad9f26e78-265x300
本文由 良许Linux教程网 发布,可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
良许

作者: 良许

良许,世界500强企业Linux开发工程师,公众号【良许Linux】的作者,全网拥有超30W粉丝。个人标签:创业者,CSDN学院讲师,副业达人,流量玩家,摄影爱好者。
上一篇
下一篇

发表评论

联系我们

联系我们

公众号:良许Linux

在线咨询: QQ交谈

邮箱: yychuyu@163.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部