本帖最后由 杨为民 于 2024-3-1 19:25 编辑
由于STC单片机硬件上有丰富的片上设备和中断资源,STC单片机上的RTOS的应用领域也将非常广。临界区保护是RTOS系统中的一个重要组成部分,但是采用哪种方法或者说提供哪些可选择的方法,对于不同的设计者有不同的考虑。 本文通过对STC单片机上现有的一些RTOS的临界区保护的讨论,进一步展示各种临界区保护方法的特点和适用领域。 一、CosyOS-RTOS的临界区保护方法 (1)临界区保护是单片机RTOS程序设计中的重要问题,通常在uC/OS-II、uC/OS-III和RT-Thread等RTOS中都采用了简单粗暴却也行之有效的方法:进入临界区保护时“关闭总中断”(EA=0),退出临界区保护时“打开总中断”(EA=1)。 (2)但是中断程序是单片机程序的最重要组成部分,在不正确的时机关闭和打开总中断,就影响了或者屏蔽了所有的中断,有可能对单片机整个系统产生影响,比如用定时器中断产生的1MHz的信号发生器,或者是依赖中断的高速PWM程序。这与临界区保护就形成了“鱼与熊掌”不可兼得的形势。 (3)临界区保护分为两种情况,一种是操作系统本身对系统自身的资源(任务表、调度过程等)的保护。另一种是对单片机公共资源和公共程序(过程)的保护,比如对一个12位的ADC,一个任务(如实验板上的NTC)读了一个字节后,任务被切换了,新任务重新进行了ADC变换,然后任务再切换回来后读到的另一个字节已经不是原来的字节了,就会产生错误。为了避免出现这类错误,就需要公共资源临界区保护。如果按照传统操作系统的临界区保护方法(队列、加锁解锁等)编程太过复杂,不得已FreeRTOS、RTT、uC/OS这些RTOS才提供了“关全局中断”(EA=0)的通用临界区保护手段。 (4)最近推出的STC单片机上运行的CosyOS-RTOS将上面两种情况分别对待,作者分别给出了解决方法: “如果是全局变量访问,CosyOS提供的服务可确保不会重入,包括在中断中的读、写、和自运算。如果涉及到某些对硬件和寄存器的操作,如果仅是在任务中,可以用临界区的方法,仅关闭任务调度;如果是在中断中,只能用户自己实现机制保护或关闭总中断或用户中断了。然而,对于OS来说,在系统及服务层面上已经实现了全局不关中断,用户需要关闭中断是他的自由。Keil RTX4/5也是相同的道理,也会遇到这种情况。” 我觉得分别解决临界区保护这是一条正确的道路。 (5)在CosOS中,RTOS系统核心的临界区保护方法如下: 其中“uEnterCritical”是一个宏定义: 其中“__enter_critical”是一个实际的函数: 其中函数的实体是可以提供给用户选择或者自定义的宏“mEnterCritical”: 这个宏是一个后条件循环,首先执行了“mSTK_Disable”(ET0 = 0)。 (6)在这里进入临界区保护首先关闭了定时器0的中断允许位,这时定时器0中断就不会再发生了,RTOS系统也就不会再进行任务调度,这样就保护了与任务调度有关的RTOS系统的变量与过程的执行的完整性。 二、RTOS临界区保护的设计理念 临界区保护对于单片机RTOS编程肯定是必要的,但是在具体的某一个RTOS里面如何实现是一个选择,是一种设计理念。
(7)目前STC单片机的uC/OS-II版本是从PC DOS平台上运行的版本移植而来,而DOS平台本身就提供了系统核心级的保护,因此提供了三种方法供用户选择。 再移植到STC8单片机上,目前实现的为: (8)而在移植到STC32G上的FreeRTOS中,临界区保护方法从中断与非中断的角度划分为两种: 第一种在非中断程序中使用的一般称为“任务级临界区保护”,第二种在中断程序中使用的一般称为“系统级临界区保护”。
在FreeRTOS中实现任务级临界区保护的方法为: 仍然是简单粗暴的开关总中断了事。 对于系统级临界区保护FreeRTOS有更深层的考虑,采用了比较复杂的方法来实现,这里就不具体介绍了。 三、RTOS临界区保护嵌套的问题 (9)临界区保护嵌套是指用户的某个程序过程需要整个地进行保护,但是这个过程包含多个系统函数,这些函数里面已经有临界区保护的进入和退出(配对的关闭和打开总中断)了,这时虽然在过程的开始进入临界区保护(关闭了总中断),但是如果临界区保护不是可以嵌套的,这时等不到整个过程结束,里面包含的函数中的退出临界区保护程序(EA=1)就会提前将总中断打开,这就违背了程序设计者的初衷,严重的可能产生可怕的后果。 (10)这种嵌套关系就像C语言里面的注释语句。大家可以看到很多RTOS程序的程序行后面都有一个“/*”与“*/”配对注释,这是C语言语法中的“行注释语句”。 如果你要把多个程序行组成的一整段程序用“/*”与“*/”对都注释掉(块注释),但其中的某几行程序包含这种行注释,那么你可以看到你编译器碰到第一个行注释的结尾“*/”就离开注释区,把其后到下一个行注释开始的“/*”的部分露出来,没有进行注释,给程序员带来很大的麻烦。这也就是提倡使用C++的行尾注释符“//”语法的原因了。
(11)对于笔者前文介绍的移植到STC8上面的“uC/OS-II”就存在这个问题,其中系统中断的程序为: 其中的第472行程序关闭总中断和第485行打开总中断就是想保护整个系统中断(定时器0中断)的完整执行。
为了显示这个临界区过程保护的情况,笔者在P2端口连接了一个8路逻辑分析仪来监测这个中断的高速过程。其中第1路高电平代表进入系统中断(第470行,第488行),第2路代表高电平代表总中断EA打开(第473行,第486行),同时在临界区保护的宏定义也增加相应的语句: (12)下图是逻辑分析仪监测的截屏: 最上面第0通道是一个作为参考10KHz的定时器中断信号,第1通道是20毫秒的系统中断,第2通道是总中断EA的开关情况。 对于第0通道参考中断,每个中断中只有一个临界区保护,不存在嵌套,因此对应第0路的高电平脉冲,只有一次关开总中断的过程。 但是对于第1通道系统中断,每个中断中包含了第479行的系统时间Tick函数(内含临界区保护代码了)和第482行的系统中断任务调度函数(内含临界区保护代码了),因此在与第1通道高电平脉冲对应的过程中,第2通道显示有总中断EA有多次的开关过程,这就是临界区保护嵌套现象。 显然,这种多次的开关总中断,并不符合程序设计者的初衷。 四、结束语 因此作为一个优秀的RTOS,系统的设计者同时也应该给出临界区嵌套问题的解决方法。 (13)参考前面的PC DOS下的uC/OS-II系统的临界区保护方法选择图,第2种和第3种方法都是支持临界区保护嵌套的,作者选择了第2种,并且在其著作中阐明迫不得已用户才需选择第1种,因为它是不支持嵌套的。 (14)STC8上的uC/OS-II移植者也是希望支持临界区保护嵌套,在上面程序图中,uC/OS-II移植版的方法3忠实地将8086CPU上的方法3移植到了STC8单片机上,但是临界区嵌套保护的效果显然不理想,所以移植者写下移植体会:“很遗憾,第三种方式“屡试不爽””。 我认为效果不理想的问题出在两种CPU的中断过程不一样。8086 CPU在进入中断时把中断标志FLAG硬件方式推入了堆栈,中断退出时又硬件把这个中断标志恢复了。 无论8051或者80251的中断硬件都没有自动地将总中断标志EA推入和弹出系统堆栈,因此uC/OS-II的第3种方法很难以实现,应该在今后的移植中去除。 (15)而RT-Thread的作者认识到临界区保护嵌套使用的必要性,因此给出了一种关闭中断和允许临界区保护嵌套的特色方法,一举成名。这时后话,待将RTT移植到STC单片机后再讨论。 (16)建议:在CosyOS中,作者也可以简单地加入一个开关总中断的临界区保护方法让用户选择,既与现在的方法不冲突,又增加了RTOS的完整性: #define OS_ENTER_CRITICAL() EA=0 /* 直接禁止中断*/
#define OS_EXIT_CRITICAL() EA=1 /* 直接允许中断 */
|