大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家分享的是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 <= 1)
{
// 仅当计数器不大于 1 时,才打开系统全局中断,并清零计数器
s_lockObject = 0;
__enable_irq();
}
else
{
// 当计数器大于 1 时,直接计数器减 1 即可
--s_lockObject;
}
}

2.3 终极做法

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

  看到这,你应该明白了 __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裸机环境下临界区保护的三种实现痞子衡便介绍完毕了,掌声在哪里~~~

欢迎订阅

文章会同时发布到我的 博客园主页CSDN主页知乎主页微信公众号 平台上。

微信搜索"痞子衡嵌入式"或者扫描下面二维码,就可以在手机上第一时间看了哦。

痞子衡嵌入式:嵌入式Cortex-M裸机环境下临界区保护的三种实现的更多相关文章

  1. hadoop搭建杂记:Linux下JDK环境变量的设置(三种配置环境变量的方法)

    Linux下JDK环境变量的设置(三种配置环境变量的方法) Linux下JDK环境变量的设置(三种配置环境变量的方法) ①修改/etc/profile文件 如果你的计算机仅仅作为开发使用时推荐使用这种 ...

  2. Liunx 环境下vsftpd的三种实现方法(超详细参数)

    以下文章介绍Liunx 环境下vsftpd的三种实现方法 ftp://vsftpd.beasts.org/users/cevans/vsftpd-2.0.3.tar.gz,目前已经到2.0.3版本.假 ...

  3. LINUX环境并发服务器的三种实现模型

    服务器设计技术有很多,按使用的协议来分有TCP服务器和UDP服务器.按处理方式来分有循环服务器和并发服务器. 1  循环服务器与并发服务器模型 在网络程序里面,一般来说都是许多客户对应一个服务器,为了 ...

  4. [CentOS] 环境变量设置的三种方法

    在CentOS系统中添加环境变量的方法有几种,推荐第三种方法.这里以添加 TexLive 2017 的环境变量为例进行说明. 1. 修改  ~/.bash_profile  文档,在文末添加以下代码: ...

  5. fluent中UDF环境变量问题的三种解决方法

    方法一: 这种方式最简便,首选这种,但是有时会因为不明原因而不好使,我自己电脑刚开始用这种方式是行得通的,但是后来中途装过很多乱七八糟的软件,估计环境变量改乱了,这时候只能用第二种或者第三种方法.先说 ...

  6. Linux下环境变量设置的三种方法

    如想将一个路径加入到$PATH中,可以像下面这样做: 1.控制台中设置,不赞成这种方式,因为他只对当前的shell 起作用,换一个shell设置就无效了:$PATH="$PATH" ...

  7. 逐步搭建Lamp环境之vim的三种模式以及基本命令

    在Linux中vim的三种模式分别为:命令模式.末行模式.编辑模式.以下是三者的关系图: 三种模式的彼此切换: 命令模式是vim中的默认模式. 命令模式切换至末行模式: 使用英文冒号(:). 末行模式 ...

  8. 痞子衡嵌入式:嵌入式Cortex-M中断向量表原理及其重定向方法

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是Cortex-M中断向量表原理及其重定向方法. 接着前文 <嵌入式Cortex-M裸机环境下临界区保护的三种实现> 继续聊, ...

  9. 痞子衡嵌入式:语音处理工具Jays-PySPEECH诞生记(1)- 环境搭建(Python2.7.14 + PyAudio0.2.11 + Matplotlib2.2.3 + SpeechRecognition3.8.1 + pyttsx3 2.7)

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是语音处理工具Jays-PySPEECH诞生之环境搭建. 在写Jays-PySPEECH时需要先搭好开发环境,下表列出了开发过程中会用到的 ...

随机推荐

  1. VB 老旧版本维护系列---兜兜转转有点晕:从服务器通过URL不中转保存的下载

    从服务器通过URL不中转保存的下载 首先引用System.dll 然后新开一个页面,空的,在后台Page_Load方法里写   Dim docPath As String ="" ...

  2. python+selenium基础篇,文件上传

    1.百度查询图片,上传文件 2.实现代码如下: from selenium import webdriverfrom time import sleepdr=webdriver.Firefox()dr ...

  3. mysql表ERROR 144 (HY000)Table 'dede_archives' is marked

    1.故障现象 mysql> select count(*) from dede_archives;ERROR 144 (HY000): Table '.xx' is marked as cras ...

  4. 【NX二次开发】Block UI 角度尺寸

    属性说明 常规         类型 描述     BlockID     String 控件ID     Enable     Logical 是否可操作     Group     Logical ...

  5. NX二次开发】Block UI 体收集器

    属性说明 属性   类型   描述   常规           BlockID    String    控件ID    Enable    Logical    是否可操作    Group    ...

  6. 【NX二次开发】镜像对象

    使用uf5946获取镜像矩阵注意:uf5946镜像这个函数,只能用#define UF_plane_type=46这种类型的数据作为镜像面,不能用#define UF_datum_plane_type ...

  7. 【NX二次开发】开发好几年,还只会用ufusr?其他用户出口函数介绍

    用户出口(User Exit)是NX Open 中的一个重要概念.NX在运行过程中某些特定的位置存在规定的出口,当进程执行到这些出口时,NX会自动检查用户是否在此处已定义了指向内部程序位置的环境变量: ...

  8. 纯小白干货:Java import以及Java类的搜索路径

    如果你希望使用Java包中的类,就必须先使用import语句导入.import语句与C语言中的 #include 有些类似,语法为:import package1[.package2-].classn ...

  9. Duilib的双缓冲实现,附带GDI、WTL的双缓冲实现

    前言: 闪烁问题,之前的经验是使用双缓冲,借此机会,把双缓冲的研究心得总结下. 双缓冲的含义: 缓冲这个词,相信大家都不陌生,Cache.主要是为了解决上下游(或者模块.或者系统)等性能不匹配问题.如 ...

  10. .NET Core/.NET5/.NET6 开源项目汇总10:实用工具

    系列目录     [已更新最新开发文章,点击查看详细] 开源项目是众多组织与个人分享的组件或项目,作者付出的心血我们是无法体会的,所以首先大家要心存感激.尊重.请严格遵守每个项目的开源协议后再使用.尊 ...