
本帖最后由 xiaojie0513 于 2018-9-9 11:25 编辑 5 C& ~: y" t1 u& i大家周末好,刚回学校,乱七八糟的事情一堆,抽个时间更新下~ 在文章的最前面,本章主要讲解RTOS的临界段 ▲▲▲▲▲ 本文是杰杰原创,转载请说明出处:RTOS的临界段知识详解什么是临界段9 H6 M! [) Z. z, V 代码的临界段也称为临界区,指处理时不可分割的代码区域,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即打开中断。% T5 q$ s) _$ c5 E 临界段的作用5 _" |0 r& I; H0 Y 其实在RTOS中,使用最多的临界段是OS本身的调用,但是我们用户也是需要对临界资源进行保护的(临界资源是一次仅允许一个线程使用的共享资源),特别是一些全局变量,当线程正在使用的时候不希望有人来打断我的操作,就行很多时候我们写代码时,需要集中精力,不希望别人打断我们的思路一样。这样子使得系统的运行更加稳定健壮。 9 p- e4 e7 H8 r8 L- B 什么时候会打断代码的执行?& f. Y( ?& e, { 顾名思义,代码正在正常运行的时候,基本不会被打断,能被打断的都是系统发生了异常(中断也是异常),在OS中,除了外部中断能将正在运行的代码打断,还有线程的调度——PendSV,系统产生 PendSV中断,在 PendSV Handler 里面实现线程的切换。我们要将这项东西屏蔽掉,保证当前只有一个线程在使用临界资源。 如何关闭中断? 其实,在我们常用的MCU中,一般为Cortex-M内核的,M内核是有一些指令能快速关闭中断,一起来看看Cortex-M权威指南吧(以Cortex-M3为例)。 ![]() 简单来说,快速屏蔽中断就是处理这些内核寄存器,在Cortex-M中有相应的操作指令,一般我们无需关注,因为OS已经给我们写好了这些底层的东西。不过如果你是想自己写一个OS的话,可以了解一下,要访问 PRIMASK, FAULTMASK 以及 BASEPRI,同样要使用 MRS/MSR 指令,如:
其实,为了快速地开关中断, CM3 还专门设置了一条 CPS 指令,有 4 种用法:" M* s& @3 a+ S0 l5 ] ![]() 1CPSID I ![]() 2CPSIE I ![]() 3CPSID F ;FAULTMASK=1, ;关异常! v( q% K. |! u7 x) j! k 4CPSIE F ;FAULTMASK=0 ;开异常 5 M) d) b. X! l" p8 R. v) a2 C 上面的代码中的PRIMASK和 FAULTMAST 是 Cortex-M 内核 里面三个中断屏蔽寄存器中的两个,还有一个是 BASEPRI,这些寄存器都用于屏蔽中断。具体的作用见表格(表格出自《【野火】RT-Thread 内核实现与应用开发实战指南》)
+ j" ?6 \" x% k% r 不同OS的处理临界段的区别7 N# A5 r5 Z$ Q FreeRTOS:FreeRTOS对中断的开和关是通过操作 BASEPRI 寄存器来实现的,即大于等于 BASEPRI 的值的中断会被屏蔽,小于 BASEPRI 的值的中断则不会被屏蔽。这样子的好处就是用户可以设置 BASEPRI 的值来选择性的给一些非常紧急的中断留一条后路。比如飞控的防撞处理。代码在portmacro.h 中实现: 屏蔽中断: 1static portFORCE_INLINE void vPortRaiseBASEPRI( void ) 2{ 3uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; 4; O6 V. p9 ^0 G 5 __asm3 z2 w+ R I% S! r& `( b" P+ E 6 {! W8 P6 Z7 v9 Z8 x 7 msr basepri, ulNewBASEPRI+ R4 L/ t2 g4 c4 A7 J- ] 8 dsb# |& I* X. Z- v8 G* P% R 9 isb 10 } P4 V! S5 g1 J- _ 11}6 g3 @' m. j: [: @- E4 G5 ] 打开中断: 1static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI ) 2{ 3 __asm 4 { 5 msr basepri, ulBASEPRI 6 } 7}: }6 ?5 l5 J9 G/ q) Z RT-Thread:与FreeRTOS不同的是,RT-Thread 对临界段的保护处理的很干脆,不管三七二十一直接把中断全部关了(直接操作PRIMASK内核寄存器), 只有NMI FAULT 和硬 FAULT能被相应。 这种方法简单粗暴,是很不错的选择。一般我们临界段的处理时间是比较短的,关了再开其实并没有太大的影响。 现在要看看RT-Thread的关中断的代码实现:: E+ f+ t. Q9 j" }! c5 Y 1rt_hw_interrupt_disable PROC 2 EXPORT rt_hw_interrupt_disable: H$ q- Q4 W% { 3 MRS r0, PRIMASK) m8 |: ?7 y: Q# y 4 CPSID I 5 BX LR 6 ENDP/ a4 F& J j3 ]' _: a6 L: s9 r8 J6 o 0 B" u0 k$ l E& B& @7 [8 r" E 开中断:! x) t0 b# }1 u- v y 1rt_hw_interrupt_enable PROC 2 EXPORT rt_hw_interrupt_enable 3 MSR PRIMASK, r0; I; H- ^% d2 r# f 4 BX LR9 O5 A# q% W" P8 v( D7 e0 K9 J 5 ENDP! S( y( V; ?9 J! @8 U5 [% ]( F 这短短的几句代码其实还是很有意思的,我就引用火哥的话来解释一下这些处理操作(我个人是不会汇编的,但是跟着书来解读这些代码还是很轻而易举的); h2 @8 D0 ` b- f2 j* X! n 可能有人懂汇编的话,就会看出来,关中断,不就是直接使用 CPSID I 指令就行了嘛~开中断,不就是使用 CPSIE I 指令就行了嘛,为啥跟我等凡人想的不一样? RT-Thread的处理好像是多此一举了,实则不然,“所有东西的存在必然有其存在的意义”这句话应该没人反驳吧~~因为RT-Thread要防止用户错误地退出了中断临界段,因为这样子可能会产生巨大的危害,所以RT-Thread将当前的PRIMASK的状态保存起来,这样子就必须要关多少次中断就得开多少次中断。 }3 d3 p5 C8 e8 f7 m 怎么说呢,用例子来证明吧: 1/* 临界段 1 开始 */ 2rt_hw_interrupt_disable(); /* 关中断,PRIMASK = 1 */. ^+ X$ d" @8 l3 h% R6 B, G 3{ 4 /* 临界段 2 */ 5 rt_hw_interrupt_disable(); /* 关中断,PRIMASK = 1 */0 |& e; Z0 k( L$ J( E; i5 a6 m% B 6 { 7 }1 x4 J% w: f. S7 y' e# s" s 8 rt_hw_interrupt_enable(); /* 开中断,PRIMASK = 0 */ (注意)* M' Z; x2 k, X/ m6 c+ R) F- u 9}/ N( l4 M. w( \7 R5 P7 v- w$ ]- A 10/* 临界段 1 结束 */" H2 e8 p- u+ R. \" F8 s9 q- R 11rt_hw_interrupt_enable(); /* 开中断,PRIMASK = 0 */ 如果直接操作PRIMASK,而不保存PRIMASK的状态,这样子当临界段2结束后调用一次打开中断,那么连临界段1的后半部分就无效了。而RT-Thread的实现就能很好避免这种问题,也用代码来说明吧:, y: Y: d* ~/ N( g! n6 Z! f 1/* 临界段 1 开始 */$ V' q: j; y9 r 2level1 = rt_hw_interrupt_disable(); /* 关中断,level1=0,PRIMASK=1 */ \) X8 Z" ], j 3{9 G5 R& B; S: J2 l0 j 4 /* 临界段 2 *// \4 g. w F$ C3 H 5 level2 = rt_hw_interrupt_disable(); /* 关中断,level2=1,PRIMASK=1 */ % E. _: u+ X1 y 6 { 7 } 8 rt_hw_interrupt_enable(level2); /* 开中断,level2=1,PRIMASK=1 */ 1 T. L% Z: e" e) K/ H* Z 9}1 b e0 d& G- W! g6 O) w 10/* 临界段 1 结束 */* o( ?7 `( w1 E" Z _$ d 11rt_hw_interrupt_enable(level1); /* 开中断,level1=0,PRIMASK=0 */ & I" r5 K& |' I+ U2 p1 t 这样子就完全避免了对吧!( D5 B$ G1 }7 |! P1 G 有人又会问了,FreeRTOS的临界段能允许嵌套吗,答案是肯定的,FreeRTOS中早已给我们想好调用的函数了,并且全部使用宏定义实现了: 1#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI(). v9 w2 r; X. a! S% ~4 {; P 2#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 ) 3#define portENTER_CRITICAL() vPortEnterCritical()/ I; i2 g @. F" H6 Y2 s- _% e 4#define portEXIT_CRITICAL() vPortExitCritical() 5#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()/ a% U; j3 ]% S3 P+ y' r) B 6#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x) 其实原理都是差不多的,通过保存和恢复寄存器basepri的数值就可以实现嵌套使用。6 a% }$ L; Y6 h, `) R9 J+ l1 Z 1UBaseType_t uxSavedInterruptStatus; 2; y- L9 }: Q7 J( U8 ~# q) u4 p0 Z; a0 v 3uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();5 d# x( \: m1 a( n L# J4 c 4{ 5 uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();( ]) K1 c# V2 P/ K. r( c 6 { 7 //临界区代码 8 }2 b. S1 i" G) K 9 portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );6 U8 K# {: `/ G) P3 l& {+ P% G$ t. C 10} q8 L; h1 V; n/ K2 ? 11portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );, `( k* j) W1 `' |, \* Q! D 5 P# W6 Y7 G4 Q( x 进入临界段源码的实现:+ o( s! D7 _/ r3 F; j* O. D5 T 1static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void ) 2{ 3uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;4 Q! e0 x- \( C- v 4 5 __asm" ?7 k' v, w4 S! q# Y3 W9 w0 M 6 { 7 mrs ulReturn, basepri. i/ t+ a8 f; R5 [" P. G1 O+ S p 8 msr basepri, ulNewBASEPRI2 z2 X, C: r9 n- K/ z$ b4 }) t) S8 A5 e. v 9 dsb" ~/ G. N* d5 K! r1 z1 ~5 b 10 isb7 D" C; C1 s ^# B+ x 11 } 12 return ulReturn; 13}# W+ J% b C- l t& z1 Q3 `# w 退出临界段源码实现:(跟前面的函数一样): ^# h' }9 B9 F' B4 S! ` 1static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI ) 2{3 Z- \8 g( {+ e+ @2 v7 u3 Y 3 __asm* M* ^' o' a' f7 p) z* k3 `) e& b 4 { 5 msr basepri, ulBASEPRI 6 }& F- e6 ]* P9 L8 P! M- d" Z- M 7}& w+ T: q% J2 l& c* W / k, g; X' M; _5 r W7 [! R3 r 总结 对于时间关键的任务而言,恰如其分地使用 PRIMASK 和 BASEPRI 来暂时关闭一些中断是非常重要的。! r- |( k, o- M$ Q FreeRTOS源码中就有多处临界段的处理,除了FreeRTOS操作系统源码所带的临界段以外,用户写应用的时候也有临界段的问题,比如以下两种:) }. t. \: H0 U; a- C/ Q
那假如我有一个线程,处理的时间较长,但是我又不想被其他线程打断,关中断可能影响系统的正常运行,怎么办呢?其实很简单,在OS中一般可以直接挂起调度器,系统正常运行,但是不会切换线程,当我处理完再把调度器解除即可。 % w. k+ E! T# c6 w# _' z I4 k6 F + `' s4 {: g5 W RTOS使用得好,开发起来比裸机更简单,使用得不好,那将是噩梦——杰杰 -完- |
总结回复可见
0 R; M2 E, W( A2 e
好,楼主引导咱们学freertos.$ l; }, R8 n1 g
以前看过 ucos源码, freertos 已经用在项目中, 却没仔细看过,
用os的体会是, 要加入消息驱动的理念,消息驱动+状态机,天生在一起的. 如果用面向对象的思想, 那是再好不过了. 使用对象时加互斥量或信号保护.尽量避免使用全局变量. 对于全局变量, 比如一个32位的时间滴答变量, 在8,16位机中使用开关中断存取.而在32位机中, 由于读写都是原子操作(编程时4字节对齐即可,缺省的,1,2字节对齐是否原子操作就不知道了), 直接读写就可以了.
, h, {8 e* J/ C. D6 H
对于串口这样的收发数据, 也可以利用FIFO避免使用开关中断. 以前咱都这么干的, 现在直接用 stm32cubemx 生成的代码, 懒得改了. 不理会那点性能损失.
! d! q' W3 l; y+ @) H
另外, 在任务可抢占的os中, 把使用公共变量的若干线程, 设置为同一个优先级, 避免在使用公共变量时被其它打断, 不失为一个好方法.' f. s. X) W5 O% i7 I/ D c" g9 w
: o0 \/ @' M7 H4 e1 b
-----------------------------------------------------------------------------------0 H( Y- A4 f: F/ M! O
以上, 0 B& x9 n7 `+ w9 D' ~- k
不客气的