本帖最后由 xiaojie0513 于 2018-9-9 11:25 编辑 9 g: F h2 Y/ P( k大家周末好,刚回学校,乱七八糟的事情一堆,抽个时间更新下~ 在文章的最前面,本章主要讲解RTOS的临界段 ▲▲▲▲▲ 本文是杰杰原创,转载请说明出处:RTOS的临界段知识详解3 }5 D8 r" }* ]* [- x什么是临界段 代码的临界段也称为临界区,指处理时不可分割的代码区域,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即打开中断。0 `" i+ b7 ?! O 临界段的作用 其实在RTOS中,使用最多的临界段是OS本身的调用,但是我们用户也是需要对临界资源进行保护的(临界资源是一次仅允许一个线程使用的共享资源),特别是一些全局变量,当线程正在使用的时候不希望有人来打断我的操作,就行很多时候我们写代码时,需要集中精力,不希望别人打断我们的思路一样。这样子使得系统的运行更加稳定健壮。. D8 O k r. y4 y. W& \1 F% ]5 j ) T' _) K% z) v& d6 R- Q7 ^( \ 什么时候会打断代码的执行?1 v7 e& N1 n- b4 L2 m 顾名思义,代码正在正常运行的时候,基本不会被打断,能被打断的都是系统发生了异常(中断也是异常),在OS中,除了外部中断能将正在运行的代码打断,还有线程的调度——PendSV,系统产生 PendSV中断,在 PendSV Handler 里面实现线程的切换。我们要将这项东西屏蔽掉,保证当前只有一个线程在使用临界资源。4 F( K7 q. s, m+ {6 m5 G {0 r 如何关闭中断? 其实,在我们常用的MCU中,一般为Cortex-M内核的,M内核是有一些指令能快速关闭中断,一起来看看Cortex-M权威指南吧(以Cortex-M3为例)。- u1 M1 L& C" \* i+ x & k6 D* {( s! a8 n7 L# ~5 x4 h3 W 简单来说,快速屏蔽中断就是处理这些内核寄存器,在Cortex-M中有相应的操作指令,一般我们无需关注,因为OS已经给我们写好了这些底层的东西。不过如果你是想自己写一个OS的话,可以了解一下,要访问 PRIMASK, FAULTMASK 以及 BASEPRI,同样要使用 MRS/MSR 指令,如:- z" [/ G. D m& b
其实,为了快速地开关中断, CM3 还专门设置了一条 CPS 指令,有 4 种用法:1 C, l" j! Y& s* b, U 1CPSID I RIMASK=1, ;关中断9 U2 I N/ g' u8 g/ h9 t9 I 2CPSIE I RIMASK=0, ;开中断3 C* ~% a2 |1 Y% U 3CPSID F ;FAULTMASK=1, ;关异常 4CPSIE F ;FAULTMASK=0 ;开异常 2 y. z3 I t6 C* S2 U/ P 上面的代码中的PRIMASK和 FAULTMAST 是 Cortex-M 内核 里面三个中断屏蔽寄存器中的两个,还有一个是 BASEPRI,这些寄存器都用于屏蔽中断。具体的作用见表格(表格出自《【野火】RT-Thread 内核实现与应用开发实战指南》)
0 t l0 W3 n0 A w1 _# O; c* Q5 ~ 不同OS的处理临界段的区别6 M2 _8 F/ P b6 z' G0 ]# s , [ n8 |6 N6 d) h& W: S FreeRTOS:FreeRTOS对中断的开和关是通过操作 BASEPRI 寄存器来实现的,即大于等于 BASEPRI 的值的中断会被屏蔽,小于 BASEPRI 的值的中断则不会被屏蔽。这样子的好处就是用户可以设置 BASEPRI 的值来选择性的给一些非常紧急的中断留一条后路。比如飞控的防撞处理。代码在portmacro.h 中实现: 屏蔽中断:/ j" f" z% u4 u 1static portFORCE_INLINE void vPortRaiseBASEPRI( void ) 2{" t) H# r' ]" q( b3 y 3uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;! \$ R$ b Z$ |) l) p) o7 O* u 4 5 __asm 6 { 7 msr basepri, ulNewBASEPRI4 w- \( t. {1 T, s# A0 U 8 dsb* w& U8 V+ Y$ U& O3 ] 9 isb" b( ?% v. j+ Q 10 } 11}. {, D! c$ N! l. A 打开中断:9 V4 Z1 x: j$ G( a% V$ }* E 1static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI ) 2{$ L. ~1 o3 i L 3 __asm 4 {# F' j" n2 @. Y8 D. b9 O 5 msr basepri, ulBASEPRI$ r2 \+ S* v5 }; a 6 }/ p P7 R% e+ ?9 D5 N) T 7} RT-Thread:9 F5 c! d! {0 \4 m与FreeRTOS不同的是,RT-Thread 对临界段的保护处理的很干脆,不管三七二十一直接把中断全部关了(直接操作PRIMASK内核寄存器), 只有NMI FAULT 和硬 FAULT能被相应。 这种方法简单粗暴,是很不错的选择。一般我们临界段的处理时间是比较短的,关了再开其实并没有太大的影响。 现在要看看RT-Thread的关中断的代码实现: 1rt_hw_interrupt_disable PROC% V9 q; M" V& I- |# x+ q, E/ j6 I 2 EXPORT rt_hw_interrupt_disable4 T# [8 |0 H: }4 @ ~6 P 3 MRS r0, PRIMASK1 [/ k3 W5 S4 \; i. C 4 CPSID I, b" Q e; o' u: t( O 5 BX LR 6 ENDP3 T% ^2 u ^7 ]5 Y 开中断: 1rt_hw_interrupt_enable PROC) A8 g# }) Z- m( B$ S9 H* W 2 EXPORT rt_hw_interrupt_enable. M( f5 d/ j& [: } 3 MSR PRIMASK, r0 4 BX LR2 r% `0 x% t: p' e. |/ t( g1 Y 5 ENDP ; Z1 ]0 O& j, \" k 这短短的几句代码其实还是很有意思的,我就引用火哥的话来解释一下这些处理操作(我个人是不会汇编的,但是跟着书来解读这些代码还是很轻而易举的)$ W; b2 K. E% l4 [, K4 I/ f 可能有人懂汇编的话,就会看出来,关中断,不就是直接使用 CPSID I 指令就行了嘛~开中断,不就是使用 CPSIE I 指令就行了嘛,为啥跟我等凡人想的不一样?7 u, U6 `, C1 n9 ?: K U RT-Thread的处理好像是多此一举了,实则不然,“所有东西的存在必然有其存在的意义”这句话应该没人反驳吧~~因为RT-Thread要防止用户错误地退出了中断临界段,因为这样子可能会产生巨大的危害,所以RT-Thread将当前的PRIMASK的状态保存起来,这样子就必须要关多少次中断就得开多少次中断。 怎么说呢,用例子来证明吧:0 g( K) u6 F5 [% ?0 {& i 1/* 临界段 1 开始 */+ @) x) X* y3 z2 R( ^- o+ } 2rt_hw_interrupt_disable(); /* 关中断,PRIMASK = 1 */2 j+ u* y; G1 _ u8 L9 R Y 3{ 4 /* 临界段 2 */. S* [# S) R8 N 5 rt_hw_interrupt_disable(); /* 关中断,PRIMASK = 1 */* V3 r5 Z9 ]! K 6 {( z4 ?, x# |! [5 j 7 }) [4 Z- R @9 z/ ~) ~ 8 rt_hw_interrupt_enable(); /* 开中断,PRIMASK = 0 */ (注意) 9}- X% F$ q, [. y 10/* 临界段 1 结束 */ 11rt_hw_interrupt_enable(); /* 开中断,PRIMASK = 0 */4 v+ T6 k Y. I1 M7 o/ }, |/ V$ f0 x 5 G4 q6 z2 H3 m& y H 如果直接操作PRIMASK,而不保存PRIMASK的状态,这样子当临界段2结束后调用一次打开中断,那么连临界段1的后半部分就无效了。而RT-Thread的实现就能很好避免这种问题,也用代码来说明吧: 1/* 临界段 1 开始 */3 y8 g$ g9 R0 N4 ?& s5 O6 v 2level1 = rt_hw_interrupt_disable(); /* 关中断,level1=0,PRIMASK=1 */ 3{ 4 /* 临界段 2 *// E% c! F2 ^. J" _; c 5 level2 = rt_hw_interrupt_disable(); /* 关中断,level2=1,PRIMASK=1 */ 6 { 7 }5 a% O% _' ~2 E+ I2 b 8 rt_hw_interrupt_enable(level2); /* 开中断,level2=1,PRIMASK=1 */ 9 M9 d/ ~6 y" Q' V: O/ [. t5 w 9} 10/* 临界段 1 结束 */6 ~* ], p% b# [. \6 J. w" ^ 11rt_hw_interrupt_enable(level1); /* 开中断,level1=0,PRIMASK=0 */ , j1 ^, {* j% m$ Y ) R5 I# }: T( w p' G4 o3 c9 I 这样子就完全避免了对吧! 有人又会问了,FreeRTOS的临界段能允许嵌套吗,答案是肯定的,FreeRTOS中早已给我们想好调用的函数了,并且全部使用宏定义实现了: 1#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI() 2#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 ) 3#define portENTER_CRITICAL() vPortEnterCritical()3 [ i- s5 f& I; @$ V 4#define portEXIT_CRITICAL() vPortExitCritical() 5#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI() a$ E" I/ {# D* O+ V2 t) C 6#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)/ O1 g/ A: c- l$ | 其实原理都是差不多的,通过保存和恢复寄存器basepri的数值就可以实现嵌套使用。, L! ?, n" t/ ?, B6 c 1UBaseType_t uxSavedInterruptStatus; 2) ?/ X. F9 p1 ~ 3uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();9 P& h! `, m4 l 4{ 5 uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR(); 6 {8 D9 c: Q( e4 P i0 K 7 //临界区代码- Q& M) ^% k, ~; {. c 8 }( y' S1 z, o" P% ]% L 9 portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus ); 10} 11portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );- A, a. \% R- ^9 M6 ` B+ F+ J& r! p$ N- t1 w 进入临界段源码的实现: 1static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )4 Y- J) x2 R& G% a1 h 2{ 3uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;, J: Q3 J0 |3 T( E 4 5 __asm6 i* |0 O7 n Q2 \% K! Q d2 A 6 { 7 mrs ulReturn, basepri2 r% k! m' D2 ]' c4 B8 V 8 msr basepri, ulNewBASEPRI, x# r9 X6 {* V! a 9 dsb: U1 ^6 ]* p2 Y, L! b4 S 10 isb/ B% y+ c# W$ A. R: c' X 11 } 12 return ulReturn; 13} 7 Z- ~1 |7 t4 U+ v/ E2 C. e 退出临界段源码实现:(跟前面的函数一样)8 y+ \ g) M+ j7 @1 G+ L" L$ ? 1static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )% w& z+ J' F c* S 2{$ ~" w0 B9 y$ e- {1 N7 L2 @0 L* l" n 3 __asm 4 {' y- r1 `/ j& _8 O0 _ c b 5 msr basepri, ulBASEPRI. f" ` }; M7 u9 P7 L 6 }+ @" E2 o$ M# n: F: v 7} 总结; i! Y% G. W8 p/ i8 y " {9 J* k2 F% R0 Q 对于时间关键的任务而言,恰如其分地使用 PRIMASK 和 BASEPRI 来暂时关闭一些中断是非常重要的。- L$ A S8 n# V- s+ x$ ]. I FreeRTOS源码中就有多处临界段的处理,除了FreeRTOS操作系统源码所带的临界段以外,用户写应用的时候也有临界段的问题,比如以下两种:
那假如我有一个线程,处理的时间较长,但是我又不想被其他线程打断,关中断可能影响系统的正常运行,怎么办呢?其实很简单,在OS中一般可以直接挂起调度器,系统正常运行,但是不会切换线程,当我处理完再把调度器解除即可。 ) j! M6 A+ `# Y# e; q2 k RTOS使用得好,开发起来比裸机更简单,使用得不好,那将是噩梦——杰杰 -完- |
总结回复可见
好,楼主引导咱们学freertos.
以前看过 ucos源码, freertos 已经用在项目中, 却没仔细看过,
用os的体会是, 要加入消息驱动的理念,消息驱动+状态机,天生在一起的. 如果用面向对象的思想, 那是再好不过了. 使用对象时加互斥量或信号保护.尽量避免使用全局变量. 对于全局变量, 比如一个32位的时间滴答变量, 在8,16位机中使用开关中断存取.而在32位机中, 由于读写都是原子操作(编程时4字节对齐即可,缺省的,1,2字节对齐是否原子操作就不知道了), 直接读写就可以了.3 O6 d% u* w9 B" B; J5 ]
对于串口这样的收发数据, 也可以利用FIFO避免使用开关中断. 以前咱都这么干的, 现在直接用 stm32cubemx 生成的代码, 懒得改了. 不理会那点性能损失.; l7 i1 |6 a& F2 t
5 l6 J- o' d# W8 _
另外, 在任务可抢占的os中, 把使用公共变量的若干线程, 设置为同一个优先级, 避免在使用公共变量时被其它打断, 不失为一个好方法.
' Z3 t0 \9 n0 L; `* o$ }1 q
-----------------------------------------------------------------------------------
以上, 6 M3 x: Z' j+ I2 i
* N m. K1 s8 e: B$ T* ^
% }, Z! U9 ~4 b+ Q4 f7 z& {; I
不客气的