你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

RTOS临界段知识详解

[复制链接]
xiaojie0513 发布时间:2018-9-9 11:23
本帖最后由 xiaojie0513 于 2018-9-9 11:25 编辑 * |0 ^0 x& N2 |2 `% o0 Q
大家周末好,刚回学校,乱七八糟的事情一堆,抽个时间更新下~
: D) w( `& Z: H9 @* [
在文章的最前面,本章主要讲解RTOS的临界段
▲▲▲▲▲
本文是杰杰原创,转载请说明出处:RTOS的临界段知识详解
$ x$ c/ _! R: H5 W5 K
什么是临界段6 C# _! l' l) M
    代码的临界段也称为临界区,指处理时不可分割的代码区域,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即打开中断。" Q% k5 W: |' b; q9 y" f: l, p2 B) n
临界段的作用% I4 _( b! j, u. T
    其实在RTOS中,使用最多的临界段是OS本身的调用,但是我们用户也是需要对临界资源进行保护的(临界资源是一次仅允许一个线程使用的共享资源),特别是一些全局变量,当线程正在使用的时候不希望有人来打断我的操作,就行很多时候我们写代码时,需要集中精力,不希望别人打断我们的思路一样。这样子使得系统的运行更加稳定健壮。8 U1 @3 ^* q1 E3 U
. y8 o  Q" X3 l% x# ?. a0 `, L
什么时候会打断代码的执行?
7 R+ h% L; I3 @& w6 V    顾名思义,代码正在正常运行的时候,基本不会被打断,能被打断的都是系统发生了异常(中断也是异常),在OS中,除了外部中断能将正在运行的代码打断,还有线程的调度——PendSV,系统产生 PendSV中断,在 PendSV Handler 里面实现线程的切换。我们要将这项东西屏蔽掉,保证当前只有一个线程在使用临界资源。+ o! `1 S) L2 @- O- v
$ t9 J& y' b+ `9 D
如何关闭中断?: u4 r5 d/ K2 `$ I
    其实,在我们常用的MCU中,一般为Cortex-M内核的,M内核是有一些指令能快速关闭中断,一起来看看Cortex-M权威指南吧(以Cortex-M3为例)。! ^8 K5 e2 V# x" e
( T8 l2 r7 ~* ?
    简单来说,快速屏蔽中断就是处理这些内核寄存器,在Cortex-M中有相应的操作指令,一般我们无需关注,因为OS已经给我们写好了这些底层的东西。不过如果你是想自己写一个OS的话,可以了解一下,要访问 PRIMASK, FAULTMASK 以及 BASEPRI,同样要使用 MRS/MSR 指令,如:
2 J8 n) b' \9 C
  1. MRS R0, BASEPRI ;读取 BASEPRI 到 R0 中/ L, J( y( F9 Y/ N* m0 K7 A
  2. MRS R0, FAULTMASK ;似上
    , L5 E( R' n- h9 j; W- K" u
  3. MRS R0, PRIMASK ;似上
    ! M- Y: V- G# k2 i6 J
  4. MSR BASEPRI, R0 ;写入 R0 到 BASEPRI 中
    " t' Y+ r3 ?6 @- @1 ]
  5. MSR FAULTMASK, R0 ;似上7 r4 F$ N, o2 }3 }! \$ Y
  6. MSR PRIMASK, R0 ;似上只有在特权级下,才允许访问这 3 个寄存器。
复制代码
  q" O: G$ D1 T6 |$ {
    其实,为了快速地开关中断, CM3 还专门设置了一条 CPS 指令,有 4 种用法:  c( l+ h) N6 H7 a$ ~: J

& U$ O1 p  j( T1 |2 V7 q6 I1CPSID I RIMASK=1, ;关中断
5 P: `' F2 r6 a/ @" z2CPSIE I RIMASK=0, ;开中断/ b: U; L" r3 M. c
3CPSID F ;FAULTMASK=1, ;关异常
& e& K; r4 T3 g4CPSIE F ;FAULTMASK=0 ;开异常
) W8 j, I) F8 p, F( {' B3 N2 p& t
! n0 H" V* i( a2 W9 H& f   上面的代码中的PRIMASK和 FAULTMAST 是 Cortex-M 内核 里面三个中断屏蔽寄存器中的两个,还有一个是 BASEPRI,这些寄存器都用于屏蔽中断。具体的作用见表格(表格出自《【野火】RT-Thread 内核实现与应用开发实战指南》)
名字 功能描述
PRIMASK 这是个只有单一比特的寄存器。 在它被置 1 后,就关掉所有可屏蔽的异常,只剩下 NMI 和硬 FAULT 可以响应。它的缺省值是 0,表示没有关中断。
FAULTMASK 这是个只有 1 个位的寄存器。当它置 1 时,只有 NMI 才能响应,所有其它的异常,甚至是硬 FAULT,也通通闭嘴。它的缺省值也是 0,表示没有关异常。
BASEPRI 这个寄存器最多有 9 位(由表达优先级的位数决定)。它定义了被屏蔽优先级的阈值。当它被设成某个值后,所有优先级号大于等于此值的中断都被关(优先级号越大,优先级越低)。但若被设成 0,则不关闭任何中断, 0 也是缺省值。
1 C) z- ~  s" o, d8 I: d, l
: ^) l9 o( `. E+ P
不同OS的处理临界段的区别
: Z8 R' W3 j" @4 ~! g
' H7 f* ~( Y; t' W
FreeRTOS:* {. H5 O1 I, K6 B7 D$ R6 [5 ]
    FreeRTOS对中断的开和关是通过操作 BASEPRI 寄存器来实现的,即大于等于 BASEPRI 的值的中断会被屏蔽,小于 BASEPRI 的值的中断则不会被屏蔽。这样子的好处就是用户可以设置 BASEPRI 的值来选择性的给一些非常紧急的中断留一条后路。比如飞控的防撞处理。代码在portmacro.h 中实现:6 N& ^# s6 d/ o0 [" [+ G
屏蔽中断:- a7 ]0 `/ e0 T* n8 z- G
1static portFORCE_INLINE void vPortRaiseBASEPRI( void )0 b' r0 p% ~4 x
2
{
+ o) e- {- {! h& E* C% ?2 { 3uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;5 u- [& G* r: |! z
42 q/ k! P3 B% E& b
5    __asm
. T" k. l: O2 _$ ]. H1 l, y5 p 6    {
1 i! V/ A& |, t3 z. v- _7 t 7        msr basepri, ulNewBASEPRI! R" J, Q! ~1 ^4 t/ @
8        dsb
" P. o% f7 }) X' x" i) w 9        isb- t- F+ U$ }! h+ S3 y6 p" A
10    }
: [2 t* q* _2 @; ^0 K11}
' d9 O  u* x7 C! j, u打开中断:6 g( w; O; p5 S4 \6 X
1static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
  O9 u. |6 ]  u8 z: L$ I2
{
$ z: {. [5 D: M/ w1 v. H/ V0 Z3    __asm
& }" {8 q; h9 W/ G# h) U! @% L4    {! K6 c: N( z5 c0 e6 n" d+ Y
5        msr basepri, ulBASEPRI8 U. z, S0 A3 k
6    }# ~' M! @3 G, u
7}
4 y/ i$ e4 O2 {2 X3 l+ B; L5 L. p% X3 H3 L) r& C( y
RT-Thread:1 I% z# i* \, v( w9 I7 ?3 Y, i
    与FreeRTOS不同的是,RT-Thread 对临界段的保护处理的很干脆,不管三七二十一直接把中断全部关了(直接操作PRIMASK内核寄存器), 只有NMI FAULT 和硬 FAULT能被相应。 这种方法简单粗暴,是很不错的选择。一般我们临界段的处理时间是比较短的,关了再开其实并没有太大的影响。
3 v" s2 c. m2 ~/ m) F* E# D% g( ^    现在要看看RT-Thread的关中断的代码实现:0 k) m: v; M; y. K$ f9 z; R
1rt_hw_interrupt_disable    PROC- X$ n' T. s% c0 C/ C
2    EXPORT  rt_hw_interrupt_disable3 Y8 c* ~' ~1 |# N& v2 Z
3    MRS     r0, PRIMASK; V' q# j' \: g; B6 R
4    CPSID   I
" u7 B. U6 w5 ^( T) N; p5    BX      LR) u' c5 R& M* {& \6 b0 N
6    ENDP2 u7 V3 W1 X; K4 D" h2 w7 a2 K  ]' `; z
1 j+ p2 T; @) |
开中断:6 t0 d) e0 W- E6 r+ Z4 d
1rt_hw_interrupt_enable    PROC
: Q9 G( ^2 \; W/ X8 {5 ?9 ~8 _7 `2    EXPORT  rt_hw_interrupt_enable
; C+ a8 _7 p) m3 ~. x/ K1 @/ C3    MSR     PRIMASK, r0* D6 x4 e7 u% y$ B1 ]
4    BX      LR) e3 I1 d; a# e) [5 V- U
5    ENDP
/ _1 U* i4 L" \4 i3 V: T8 m/ Q4 a8 t, y$ Y
这短短的几句代码其实还是很有意思的,我就引用火哥的话来解释一下这些处理操作(我个人是不会汇编的,但是跟着书来解读这些代码还是很轻而易举的): [# i) z$ V' W0 w6 a' W
    可能有人懂汇编的话,就会看出来,关中断,不就是直接使用 CPSID I 指令就行了嘛~开中断,不就是使用 CPSIE I 指令就行了嘛,为啥跟我等凡人想的不一样?
* x0 ~4 A$ P1 [' Q* m' B. F    RT-Thread的处理好像是多此一举了,实则不然,“所有东西的存在必然有其存在的意义”这句话应该没人反驳吧~~因为RT-Thread要防止用户错误地退出了中断临界段,因为这样子可能会产生巨大的危害,所以RT-Thread将当前的PRIMASK的状态保存起来,这样子就必须要关多少次中断就得开多少次中断。
/ r$ n0 r% @2 H0 r" _0 v+ |$ ?" H怎么说呢,用例子来证明吧:
* j$ [3 ^* e: ~- b4 m8 L! | 1/* 临界段 1 开始 */
+ o0 Z9 B  j) J( \  b9 O6 V 2rt_hw_interrupt_disable(); /* 关中断,PRIMASK = 1 */+ L; \, {% e" y6 j( i; t
3{
% \& Z% P& v2 P; ]; K" e* \( p4 ~ 4  /* 临界段 2 */) G; G; r* ~; t0 D
5  rt_hw_interrupt_disable(); /* 关中断,PRIMASK = 1 */
8 p& O: N$ w0 ~3 X- N, H 6  {* t7 X. H) D! Y: N# O& {0 u* D
7  }
3 _. |2 f! n, {" Z2 V6 C 8  rt_hw_interrupt_enable(); /* 开中断,PRIMASK = 0 */ (注意)& ]: ]6 V) g. w) Q, y/ m" Q
9}
- d* H$ }: g$ {! ^+ B10/* 临界段 1 结束 */9 A& ^$ Z; c6 t- n* }
11rt_hw_interrupt_enable(); /* 开中断,PRIMASK = 0 */& Q9 A& P* p& m
5 m3 B+ E1 D3 B9 ~
    如果直接操作PRIMASK,而不保存PRIMASK的状态,这样子当临界段2结束后调用一次打开中断,那么连临界段1的后半部分就无效了。而RT-Thread的实现就能很好避免这种问题,也用代码来说明吧:
, H" [9 l& q+ _+ ~) m7 | 1/* 临界段 1 开始 */, ]2 w& I6 ]* s% G7 b7 U3 e
2level1 = rt_hw_interrupt_disable(); /* 关中断,level1=0,PRIMASK=1 */3 y* Q# Y# h3 T+ d
3{
# G: c. ?( [$ |3 z 4  /* 临界段 2 */1 T/ p: Y" X; y3 _
5  level2 = rt_hw_interrupt_disable(); /* 关中断,level2=1,PRIMASK=1 */
) k8 y5 v6 N( I& m7 n: S: ^ 6  {
6 g% m2 N% {+ S( M% P5 | 7  }  |# \7 D. @  W( J7 }
8  rt_hw_interrupt_enable(level2); /* 开中断,level2=1,PRIMASK=1 */
: G8 v# r$ ^" t$ F 9}
1 Z1 L4 P- a- X/ o( d, p" C10/* 临界段 1 结束 */& a* ]0 G( Y- l; H4 @: `
11rt_hw_interrupt_enable(level1); /* 开中断,level1=0,PRIMASK=0 */
) m# N0 z9 ~% N5 y
, p- a! j- a! n这样子就完全避免了对吧!2 r# L: Z6 a2 i8 V
    有人又会问了,FreeRTOS的临界段能允许嵌套吗,答案是肯定的,FreeRTOS中早已给我们想好调用的函数了,并且全部使用宏定义实现了:8 t8 Z* o6 C, ~. y, F
1#define portDISABLE_INTERRUPTS()                vPortRaiseBASEPRI()
" |# C2 a  ~7 d; K2 v2#define portENABLE_INTERRUPTS()                 vPortSetBASEPRI( 0 )
; d( K* R* T0 H, u3#define portENTER_CRITICAL()                    vPortEnterCritical()
% N$ q4 V; p3 Y/ }- r) r- V; u4#define portEXIT_CRITICAL()                     vPortExitCritical()
& s9 T1 P) |2 C$ O. ?5#define portSET_INTERRUPT_MASK_FROM_ISR()       ulPortRaiseBASEPRI()
/ z1 U+ P0 f" W0 u, j6 P6#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x)    vPortSetBASEPRI(x)# h" ~! G. O% Z' o( ^

8 G! |8 u  H  K& K! {    其实原理都是差不多的,通过保存和恢复寄存器basepri的数值就可以实现嵌套使用。
* y# v* l' S7 g/ n, l 1UBaseType_t uxSavedInterruptStatus;
; Y7 ?5 l# I( _1 a- K3 w 2
9 }, _( Q) F7 E: v1 E; D  U 3uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
, U: |6 l: j' C$ v 4{
" N: g7 B6 D3 j. S: X) q/ K 5  uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
0 d/ @  v( @0 C# n( i$ r( Z# ^ 6  {
5 Q# l; J# k7 Y$ I 7     //临界区代码: o+ O: V! |$ Y+ h) s1 Q  O
8  }9 K+ Q. N8 X) R! {1 d9 O
9  portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );0 M% X+ c+ o! j) V+ j* D
10}/ S) F+ D) M# s, R4 f" y/ y8 Q
11portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
2 s7 a6 q$ ?, E8 `% H% H8 a- e& Z) f7 h) i7 x9 u
    进入临界段源码的实现:1 d7 }/ C* Q/ r/ l, B6 f
1static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
6 O* Y8 a# T8 O0 Y* v 2
{/ F5 F: V0 z7 z: `3 I  S- l
3uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
. W! c2 Q2 B) e$ T/ }( D 4
& A! x6 H' C: t3 _9 X& p9 `9 K2 [$ j 5    __asm
" x. j/ R9 G6 V& | 6    {
4 [/ h+ d& s# f+ ~ 7        mrs ulReturn, basepri( C" E6 b" f7 k, f, Q
8        msr basepri, ulNewBASEPRI
% @  p0 Z/ }. ~9 q) d, x 9        dsb
: C& s# v/ P4 R4 }7 {10        isb4 h  M& u: K8 R. A3 V4 |! Q
11    }3 k2 w9 r8 R. _' C# b$ B( a3 [; D
12    return ulReturn;
  e: t- K6 J- g! Q& @( ~0 |13}
* N; Q% R# v: X6 c' W6 e: H
9 t, U: B) l% m: _7 x0 Y9 j2 a    退出临界段源码实现:(跟前面的函数一样)2 \3 K6 [5 d8 v
1static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
9 O! Z# V2 ?/ Z/ @: b& v2 S2
{5 H$ x. n2 ]0 [! j" L4 O
3    __asm) P: a8 g2 m4 U9 x  }" ^6 a2 N  B
4    {% a# g7 m6 ]2 B6 _: J( e
5        msr basepri, ulBASEPRI/ V! T3 g: n8 p; {" E4 j
6    }# a( F) h1 f/ E
7}
! w; y% h. P  D. o- K7 S$ J+ `# M& h* G. _# q
总结
: y- `. J( u& Z( P9 Y7 O7 N5 Y. k8 J( h! ?6 s# N4 Y( ~& p
   对于时间关键的任务而言,恰如其分地使用 PRIMASK 和 BASEPRI 来暂时关闭一些中断是非常重要的。) K+ p1 X4 k) i" n9 f+ j
    FreeRTOS源码中就有多处临界段的处理,除了FreeRTOS操作系统源码所带的临界段以外,用户写应用的时候也有临界段的问题,比如以下两种:8 k" `# ?! h6 B6 R. a; x7 k
  • 读取或者修改变量(特别是用于任务间通信的全局变量)的代码,一般来说这是最常见的临界代码。
  • 调用公共函数的代码,特别是不可重入的函数,如果多个任务都访问这个函数,结果是可想而知的。
    , X; X3 z2 m& C( S& Q1 n
    总之,对于临界段要做到执行时间越短越好,否则会影响系统的实时性。
1 ]& `9 ~2 R/ E' U# b: w    那假如我有一个线程,处理的时间较长,但是我又不想被其他线程打断,关中断可能影响系统的正常运行,怎么办呢?其实很简单,在OS中一般可以直接挂起调度器,系统正常运行,但是不会切换线程,当我处理完再把调度器解除即可。2 u: B! U+ N+ \9 |

1 Z$ J- m% t) y0 r: i8 t( b9 @8 m" L/ V: d2 N
RTOS使用得好,开发起来比裸机更简单,使用得不好,那将是噩梦——杰杰

( w  G- q7 f/ ^. N
▲▲▲▲▲
$ {5 S$ l( c/ U& K, w( |; c
-完-
+ G  j" K2 L: x* Q, v
赞 收藏 评论15 发布时间:2018-9-9 11:23

举报

15个回答
xiaojie0513 回答时间:2018-9-9 11:52:53
@zero99 暑假工作比较忙,没坚持更新,今天先发两篇更新补回来,排版好累啊
yue_viper 回答时间:2018-9-10 17:06:05
总结部分看不到?
xiaojie0513 回答时间:2018-9-10 22:57:07
yue_viper 发表于 2018-9-10 17:06% n) f* K8 |) U( d  ]; r4 W
总结部分看不到?

' }3 B1 Z1 w# P" v3 ]" B总结回复可见
hi201803 回答时间:2018-9-11 07:35:44
本帖最后由 hi201803 于 2018-9-11 09:23 编辑 9 D! o$ x) G9 Y% u

5 u( t' U  ^" V- ?好,楼主引导咱们学freertos.
  T9 F* Z( n6 b  O$ d+ E! e以前看过 ucos源码, freertos  已经用在项目中, 却没仔细看过, 8 A" g$ {! A: G0 [
用os的体会是, 要加入消息驱动的理念,消息驱动+状态机,天生在一起的. 如果用面向对象的思想, 那是再好不过了. 使用对象时加互斥量或信号保护.尽量避免使用全局变量.  对于全局变量, 比如一个32位的时间滴答变量, 在8,16位机中使用开关中断存取.而在32位机中, 由于读写都是原子操作(编程时4字节对齐即可,缺省的,1,2字节对齐是否原子操作就不知道了), 直接读写就可以了.
, v' I0 P+ R. q+ C; @' _6 W7 Y. N
' q" Y" @1 J6 a# C对于串口这样的收发数据, 也可以利用FIFO避免使用开关中断. 以前咱都这么干的, 现在直接用 stm32cubemx 生成的代码, 懒得改了. 不理会那点性能损失.
$ u% H! g, [7 b% b
: f! Z7 Q1 t" }; |* [5 e另外, 在任务可抢占的os中, 把使用公共变量的若干线程, 设置为同一个优先级, 避免在使用公共变量时被其它打断, 不失为一个好方法.
8 C1 ?7 J7 E9 C; h, L% e; N) @: n4 b' ^. B
-----------------------------------------------------------------------------------( u7 `( x4 H) p8 F) T
以上, ; C! [$ `: u( P+ L
0 k; v5 t- p5 \' d0 U
5 |1 \; I5 o' M, q# r* ?& ~
cos12a-21701 回答时间:2018-9-30 13:23:55
回复可见。
Seaman 回答时间:2018-10-10 16:42:07
谢谢楼主分享
吾以外皆吾师 回答时间:2018-10-14 20:18:33
顶顶顶顶顶顶顶顶顶顶顶顶顶顶顶顶顶顶顶顶顶顶哒哒哒哒哒哒多多多多
rockzhouchina 回答时间:2018-10-16 12:44:20
多谢分享
xiaojie0513 回答时间:2018-10-16 14:13:26
rockzhouchina 发表于 2018-10-16 12:444 E0 K7 Z' S! f5 @+ l
多谢分享
. x- N" I- w8 @/ g
不客气的
spectrecai 回答时间:2019-1-25 09:18:34
找点东西看到杰哥ID马上点了进来评论
xiaojie0513 回答时间:2019-1-25 14:50:22
spectrecai 发表于 2019-1-25 09:18
( u, C  i4 t0 `4 b  a3 q7 a& o找点东西看到杰哥ID马上点了进来评论

# Z! B( A  M, L8 d
fillmoreand 回答时间:2019-1-25 14:52:47
嘿嘿  老面孔又相见了
xiaojie0513 回答时间:2019-1-26 08:35:07
fillmoreand 发表于 2019-1-25 14:52
& e5 s9 o% q9 M. ~/ P1 b嘿嘿  老面孔又相见了
3 m! T9 K5 T2 P/ `
xujiantj 回答时间:2019-1-28 15:32:48
谢谢楼主分享
12下一页

所属标签

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版