
本帖最后由 xiaojie0513 于 2018-9-9 11:25 编辑 . N" b: S+ T/ y大家周末好,刚回学校,乱七八糟的事情一堆,抽个时间更新下~ 在文章的最前面,本章主要讲解RTOS的临界段 ▲▲▲▲▲ 本文是杰杰原创,转载请说明出处:RTOS的临界段知识详解什么是临界段% ]5 B* i( \/ L3 } 代码的临界段也称为临界区,指处理时不可分割的代码区域,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即打开中断。 临界段的作用 其实在RTOS中,使用最多的临界段是OS本身的调用,但是我们用户也是需要对临界资源进行保护的(临界资源是一次仅允许一个线程使用的共享资源),特别是一些全局变量,当线程正在使用的时候不希望有人来打断我的操作,就行很多时候我们写代码时,需要集中精力,不希望别人打断我们的思路一样。这样子使得系统的运行更加稳定健壮。4 y8 s9 h# h, g8 d$ ^# l 什么时候会打断代码的执行?1 s* a# Y2 z$ |: {- T5 ^ 顾名思义,代码正在正常运行的时候,基本不会被打断,能被打断的都是系统发生了异常(中断也是异常),在OS中,除了外部中断能将正在运行的代码打断,还有线程的调度——PendSV,系统产生 PendSV中断,在 PendSV Handler 里面实现线程的切换。我们要将这项东西屏蔽掉,保证当前只有一个线程在使用临界资源。# e6 B7 H$ Z$ R5 v 8 O) \% |- f0 _- x7 f, Q( K 如何关闭中断?6 |, m# |, a4 T+ d+ v- ? 其实,在我们常用的MCU中,一般为Cortex-M内核的,M内核是有一些指令能快速关闭中断,一起来看看Cortex-M权威指南吧(以Cortex-M3为例)。 ![]() 简单来说,快速屏蔽中断就是处理这些内核寄存器,在Cortex-M中有相应的操作指令,一般我们无需关注,因为OS已经给我们写好了这些底层的东西。不过如果你是想自己写一个OS的话,可以了解一下,要访问 PRIMASK, FAULTMASK 以及 BASEPRI,同样要使用 MRS/MSR 指令,如:
其实,为了快速地开关中断, CM3 还专门设置了一条 CPS 指令,有 4 种用法:1 Y. E( t |# x' q ![]() 1CPSID I ![]() 2CPSIE I ![]() 3CPSID F ;FAULTMASK=1, ;关异常- n9 g: i+ B4 l+ [3 m" M 4CPSIE F ;FAULTMASK=0 ;开异常 上面的代码中的PRIMASK和 FAULTMAST 是 Cortex-M 内核 里面三个中断屏蔽寄存器中的两个,还有一个是 BASEPRI,这些寄存器都用于屏蔽中断。具体的作用见表格(表格出自《【野火】RT-Thread 内核实现与应用开发实战指南》)
- A2 s5 h" q% m" W: H' { 不同OS的处理临界段的区别9 X2 R# w$ Q. V1 l- i( K) [7 U ( J% q, \( n6 N" v: `* H9 B7 L1 O FreeRTOS:FreeRTOS对中断的开和关是通过操作 BASEPRI 寄存器来实现的,即大于等于 BASEPRI 的值的中断会被屏蔽,小于 BASEPRI 的值的中断则不会被屏蔽。这样子的好处就是用户可以设置 BASEPRI 的值来选择性的给一些非常紧急的中断留一条后路。比如飞控的防撞处理。代码在portmacro.h 中实现:4 k# \* h U9 f- i8 h1 @4 \ 屏蔽中断:" O9 @" ]: P+ r# {% ^ 1static portFORCE_INLINE void vPortRaiseBASEPRI( void )9 {% ~/ i ]5 e 2{1 J d" p% Z6 v 3uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;& ^$ l* F/ Y$ ` Q4 `, l5 r5 @2 H 4/ T/ W; A# }+ c! ?5 w5 ~2 G 5 __asm 6 {. n5 R# c f7 n7 y+ l- c5 t+ g1 ~6 J 7 msr basepri, ulNewBASEPRI 8 dsb 9 isb 10 }* F8 B6 P% o# M' t 11}" C; \+ H2 g9 f6 }6 u 打开中断: 1static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )0 R* n( `- Z6 s7 O. i8 q# Z2 o9 ^+ X 2{ 3 __asm 4 { 5 msr basepri, ulBASEPRI 6 }9 }7 {$ H$ S T1 T6 h: d0 U) d 7} RT-Thread:与FreeRTOS不同的是,RT-Thread 对临界段的保护处理的很干脆,不管三七二十一直接把中断全部关了(直接操作PRIMASK内核寄存器), 只有NMI FAULT 和硬 FAULT能被相应。 这种方法简单粗暴,是很不错的选择。一般我们临界段的处理时间是比较短的,关了再开其实并没有太大的影响。9 x2 Q* S3 ]/ |( E K 现在要看看RT-Thread的关中断的代码实现: 1rt_hw_interrupt_disable PROC8 \0 X7 Q9 Y8 {" y; w( R7 l9 Z' d 2 EXPORT rt_hw_interrupt_disable" D7 v4 d$ n3 m& a$ a1 e w 3 MRS r0, PRIMASK$ w! i6 H8 u7 m& Y 4 CPSID I' Q# S: V( n/ z4 f! Q 5 BX LR 6 ENDP 开中断:( n8 }( Z" g" [! E 1rt_hw_interrupt_enable PROC 2 EXPORT rt_hw_interrupt_enable. x! ]! I7 }6 H7 W& f) Z 3 MSR PRIMASK, r0 4 BX LR5 ?5 @: o5 y0 X) Q9 G d& _0 V) q 5 ENDP 这短短的几句代码其实还是很有意思的,我就引用火哥的话来解释一下这些处理操作(我个人是不会汇编的,但是跟着书来解读这些代码还是很轻而易举的)9 H6 R* q5 g% j6 u D5 N 可能有人懂汇编的话,就会看出来,关中断,不就是直接使用 CPSID I 指令就行了嘛~开中断,不就是使用 CPSIE I 指令就行了嘛,为啥跟我等凡人想的不一样?2 C0 q/ D& C3 F( I( G' j; |' G! ^ RT-Thread的处理好像是多此一举了,实则不然,“所有东西的存在必然有其存在的意义”这句话应该没人反驳吧~~因为RT-Thread要防止用户错误地退出了中断临界段,因为这样子可能会产生巨大的危害,所以RT-Thread将当前的PRIMASK的状态保存起来,这样子就必须要关多少次中断就得开多少次中断。- ]/ D& H' l' l7 `0 w* V6 M+ n' d \ 怎么说呢,用例子来证明吧: 1/* 临界段 1 开始 */* @/ h( B- }7 D2 N! O 2rt_hw_interrupt_disable(); /* 关中断,PRIMASK = 1 */( v; u4 ~1 L% ]% [4 ~6 c 3{3 M" c3 z( v' v' n 4 /* 临界段 2 */ 5 rt_hw_interrupt_disable(); /* 关中断,PRIMASK = 1 */- @4 Y' G& {( f' v0 P- Y( V 6 {, G9 N; W; w0 k 7 } 8 rt_hw_interrupt_enable(); /* 开中断,PRIMASK = 0 */ (注意)2 m* f4 V, z2 }" ] 9}/ T" v% o, x9 N* n) N7 _ 10/* 临界段 1 结束 */; p* E6 `) U9 k, a4 Q) s 11rt_hw_interrupt_enable(); /* 开中断,PRIMASK = 0 */ 如果直接操作PRIMASK,而不保存PRIMASK的状态,这样子当临界段2结束后调用一次打开中断,那么连临界段1的后半部分就无效了。而RT-Thread的实现就能很好避免这种问题,也用代码来说明吧:$ ?* e% ^. J# h) I K+ u! P3 f! S+ K 1/* 临界段 1 开始 */ 2level1 = rt_hw_interrupt_disable(); /* 关中断,level1=0,PRIMASK=1 */ 3{ 4 /* 临界段 2 */ 5 level2 = rt_hw_interrupt_disable(); /* 关中断,level2=1,PRIMASK=1 */ 6 {! d, v: [ i' e! m 7 }* B0 i5 i' D7 D8 ]3 H) t 8 rt_hw_interrupt_enable(level2); /* 开中断,level2=1,PRIMASK=1 */ : W7 Y& y' Y( x, a6 ^ 9} 10/* 临界段 1 结束 */3 p# l0 m* Z* f% d: ~+ M' O& u 11rt_hw_interrupt_enable(level1); /* 开中断,level1=0,PRIMASK=0 */ " h d- i- q, V d1 l 1 c' g1 `% B# }6 q- y 这样子就完全避免了对吧! 有人又会问了,FreeRTOS的临界段能允许嵌套吗,答案是肯定的,FreeRTOS中早已给我们想好调用的函数了,并且全部使用宏定义实现了: 1#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()( a3 L# v) I7 U7 L$ m3 _' T2 [ 2#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 ) 3#define portENTER_CRITICAL() vPortEnterCritical()* x& @2 ?) B$ m. t5 t& v" z 4#define portEXIT_CRITICAL() vPortExitCritical(), A, \/ J- Y1 \; J9 s 5#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI(): x* N7 D- c ?; P J7 p 6#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)7 U5 e8 A0 ]/ D) ^% s; x1 d) w 其实原理都是差不多的,通过保存和恢复寄存器basepri的数值就可以实现嵌套使用。/ l, f1 X) F' y% y6 y 1UBaseType_t uxSavedInterruptStatus; 2 3uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();9 c1 C z! \: b% j1 U. b 4{- N0 D6 c0 ^2 L3 a0 V3 B4 a 5 uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();9 h9 K$ q- I! `( t* z( T# | 6 { _4 T% @, @8 y8 ]5 A* T( b 7 //临界区代码8 z1 }; y: f) @! y3 A 8 }& J( P, M& h* _' A; [5 }1 q 9 portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );9 x: F0 t! `7 S5 h- I$ _ 10}; l3 v$ e; t7 D" G 11portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus ); 进入临界段源码的实现:: ?, `7 P" n, u$ j0 C+ u 1static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )9 s" ~& }8 m/ } 2{ 3uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; 4# O% S9 e- d% p$ U, U2 t% F 5 __asm 6 { 7 mrs ulReturn, basepri' R6 R q( }. S" u3 S8 v3 W 8 msr basepri, ulNewBASEPRI 9 dsb 10 isb( J+ U5 X, K- H: q3 @& l; K- e4 } 11 }% |" F9 s! t" ^6 a, ^& S' } 12 return ulReturn;% t2 U4 V* q! C! [ 13} * }1 X% u* `; }% ^ 退出临界段源码实现:(跟前面的函数一样): x% n' b5 z9 b/ i0 W* F 1static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI ) 2{ 3 __asm 4 {5 A& V5 D1 Y$ S5 y 5 msr basepri, ulBASEPRI 6 } 7}1 }# J3 g8 f: {, T7 V 1 ]5 w$ Y) O! Y( f* s$ n z 总结5 u4 t' e; Q0 L T! O# S$ c( b 1 l" V, b! t- H* O 对于时间关键的任务而言,恰如其分地使用 PRIMASK 和 BASEPRI 来暂时关闭一些中断是非常重要的。 FreeRTOS源码中就有多处临界段的处理,除了FreeRTOS操作系统源码所带的临界段以外,用户写应用的时候也有临界段的问题,比如以下两种:
那假如我有一个线程,处理的时间较长,但是我又不想被其他线程打断,关中断可能影响系统的正常运行,怎么办呢?其实很简单,在OS中一般可以直接挂起调度器,系统正常运行,但是不会切换线程,当我处理完再把调度器解除即可。 }; N( d6 k2 u g8 N" M& K( k RTOS使用得好,开发起来比裸机更简单,使用得不好,那将是噩梦——杰杰 -完- ) x9 ?- `/ Y9 P1 d! l9 ~5 V7 F |
总结回复可见
; y x, f5 N/ u# J$ u' w
好,楼主引导咱们学freertos.
以前看过 ucos源码, freertos 已经用在项目中, 却没仔细看过,
用os的体会是, 要加入消息驱动的理念,消息驱动+状态机,天生在一起的. 如果用面向对象的思想, 那是再好不过了. 使用对象时加互斥量或信号保护.尽量避免使用全局变量. 对于全局变量, 比如一个32位的时间滴答变量, 在8,16位机中使用开关中断存取.而在32位机中, 由于读写都是原子操作(编程时4字节对齐即可,缺省的,1,2字节对齐是否原子操作就不知道了), 直接读写就可以了.& y' ? Q/ Z: M
对于串口这样的收发数据, 也可以利用FIFO避免使用开关中断. 以前咱都这么干的, 现在直接用 stm32cubemx 生成的代码, 懒得改了. 不理会那点性能损失.
, t5 L b% N) l0 K% l7 W. i
另外, 在任务可抢占的os中, 把使用公共变量的若干线程, 设置为同一个优先级, 避免在使用公共变量时被其它打断, 不失为一个好方法." @3 O- w( s3 w9 }+ O U" B* F% v( ^
! F# W( Q6 I! x* ]+ m
-----------------------------------------------------------------------------------
以上, & D& e y1 U3 w+ j5 W
% s4 T5 J( ? v0 M
不客气的