本帖最后由 xiaojie0513 于 2018-9-9 11:25 编辑 大家周末好,刚回学校,乱七八糟的事情一堆,抽个时间更新下~ 在文章的最前面,本章主要讲解RTOS的临界段 ▲▲▲▲▲ 本文是杰杰原创,转载请说明出处:RTOS的临界段知识详解$ B$ U- _/ d" y" I! d3 g! O* v什么是临界段 代码的临界段也称为临界区,指处理时不可分割的代码区域,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即打开中断。) b7 m- d- v3 _1 @- i& E, f 临界段的作用 其实在RTOS中,使用最多的临界段是OS本身的调用,但是我们用户也是需要对临界资源进行保护的(临界资源是一次仅允许一个线程使用的共享资源),特别是一些全局变量,当线程正在使用的时候不希望有人来打断我的操作,就行很多时候我们写代码时,需要集中精力,不希望别人打断我们的思路一样。这样子使得系统的运行更加稳定健壮。 & y& Y8 P8 l5 M" T' r& l9 J 什么时候会打断代码的执行? 顾名思义,代码正在正常运行的时候,基本不会被打断,能被打断的都是系统发生了异常(中断也是异常),在OS中,除了外部中断能将正在运行的代码打断,还有线程的调度——PendSV,系统产生 PendSV中断,在 PendSV Handler 里面实现线程的切换。我们要将这项东西屏蔽掉,保证当前只有一个线程在使用临界资源。( m/ R1 R2 x/ t, c) B; i3 M: d9 ? 如何关闭中断? 其实,在我们常用的MCU中,一般为Cortex-M内核的,M内核是有一些指令能快速关闭中断,一起来看看Cortex-M权威指南吧(以Cortex-M3为例)。( Z2 H4 m f: _. Y/ l7 V6 j2 M " j( D- `2 r/ p' s; A9 `简单来说,快速屏蔽中断就是处理这些内核寄存器,在Cortex-M中有相应的操作指令,一般我们无需关注,因为OS已经给我们写好了这些底层的东西。不过如果你是想自己写一个OS的话,可以了解一下,要访问 PRIMASK, FAULTMASK 以及 BASEPRI,同样要使用 MRS/MSR 指令,如:# P+ u! t. l- b$ c4 L' n5 _' Z
其实,为了快速地开关中断, CM3 还专门设置了一条 CPS 指令,有 4 种用法: - w4 ?, f- V G: ~- r/ z, [# s1CPSID I RIMASK=1, ;关中断2CPSIE I RIMASK=0, ;开中断/ Y3 [2 w# @7 G$ c F. n4 y# ^! {3CPSID F ;FAULTMASK=1, ;关异常; f5 h9 |( P! E* N 4CPSIE F ;FAULTMASK=0 ;开异常 / k0 P R' t% ? 上面的代码中的PRIMASK和 FAULTMAST 是 Cortex-M 内核 里面三个中断屏蔽寄存器中的两个,还有一个是 BASEPRI,这些寄存器都用于屏蔽中断。具体的作用见表格(表格出自《【野火】RT-Thread 内核实现与应用开发实战指南》)
不同OS的处理临界段的区别 FreeRTOS:FreeRTOS对中断的开和关是通过操作 BASEPRI 寄存器来实现的,即大于等于 BASEPRI 的值的中断会被屏蔽,小于 BASEPRI 的值的中断则不会被屏蔽。这样子的好处就是用户可以设置 BASEPRI 的值来选择性的给一些非常紧急的中断留一条后路。比如飞控的防撞处理。代码在portmacro.h 中实现:4 M5 B7 s2 k8 x/ C0 x w2 H 屏蔽中断: 1static portFORCE_INLINE void vPortRaiseBASEPRI( void ) 2{0 [+ m0 P& V2 ?+ C# z: U1 d$ H 3uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; 4) k: g* p7 e, Y: I 5 __asm8 V+ ^( }& w( D2 ~8 m1 ? 6 { 7 msr basepri, ulNewBASEPRI) }6 J- J6 u% L* e% n! ~7 m) P 8 dsb4 L, K% {" E9 F$ e 9 isb _" B( c) y% a X) M4 X' Y9 z 10 } 11} 打开中断: 1static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI ) 2{ 3 __asm 4 { 5 msr basepri, ulBASEPRI 6 }; @# ?7 t' k0 \* r0 V' j 7}# b* B6 W* _3 p* x2 ~; N RT-Thread:与FreeRTOS不同的是,RT-Thread 对临界段的保护处理的很干脆,不管三七二十一直接把中断全部关了(直接操作PRIMASK内核寄存器), 只有NMI FAULT 和硬 FAULT能被相应。 这种方法简单粗暴,是很不错的选择。一般我们临界段的处理时间是比较短的,关了再开其实并没有太大的影响。! K6 M2 V+ ^4 D h6 F$ H1 N$ F 现在要看看RT-Thread的关中断的代码实现:0 D! T* a- ?* ]( y 1rt_hw_interrupt_disable PROC2 N& p5 D! [1 r0 R) V; K( D 2 EXPORT rt_hw_interrupt_disable% S- o" d* }) M+ R 3 MRS r0, PRIMASK 4 CPSID I 5 BX LR 6 ENDP$ j& R; i' d1 F4 p 开中断: 1rt_hw_interrupt_enable PROC0 r" `: g- u V9 P, {6 f7 ] 2 EXPORT rt_hw_interrupt_enable5 c2 ^# t0 M0 t, X# V 3 MSR PRIMASK, r0* s7 X! X& l4 ~3 Y 4 BX LR; T, ^4 R0 ?# u" O* G. ~# f, C 5 ENDP ! n5 ^: k3 o* W) x1 o5 A 这短短的几句代码其实还是很有意思的,我就引用火哥的话来解释一下这些处理操作(我个人是不会汇编的,但是跟着书来解读这些代码还是很轻而易举的)0 J8 m: F& Y6 x( c, R( v; [: v4 o 可能有人懂汇编的话,就会看出来,关中断,不就是直接使用 CPSID I 指令就行了嘛~开中断,不就是使用 CPSIE I 指令就行了嘛,为啥跟我等凡人想的不一样?9 T0 n" Q+ g1 ]( s* E8 S# Z& A$ B RT-Thread的处理好像是多此一举了,实则不然,“所有东西的存在必然有其存在的意义”这句话应该没人反驳吧~~因为RT-Thread要防止用户错误地退出了中断临界段,因为这样子可能会产生巨大的危害,所以RT-Thread将当前的PRIMASK的状态保存起来,这样子就必须要关多少次中断就得开多少次中断。$ x1 S3 R. @$ v8 p/ P 怎么说呢,用例子来证明吧: 1/* 临界段 1 开始 */ 2rt_hw_interrupt_disable(); /* 关中断,PRIMASK = 1 */ 3{ 4 /* 临界段 2 */8 Y) y7 _2 y4 c t% |6 g 5 rt_hw_interrupt_disable(); /* 关中断,PRIMASK = 1 */ 6 { 7 }! `* ]( J7 X2 ` W7 u4 U* w/ B9 ] 8 rt_hw_interrupt_enable(); /* 开中断,PRIMASK = 0 */ (注意) 9}# J& f. V! G. {5 o 10/* 临界段 1 结束 */ 11rt_hw_interrupt_enable(); /* 开中断,PRIMASK = 0 */ 如果直接操作PRIMASK,而不保存PRIMASK的状态,这样子当临界段2结束后调用一次打开中断,那么连临界段1的后半部分就无效了。而RT-Thread的实现就能很好避免这种问题,也用代码来说明吧:+ Q( n: i0 Y( I" p; ~& @ 1/* 临界段 1 开始 */ 2level1 = rt_hw_interrupt_disable(); /* 关中断,level1=0,PRIMASK=1 */3 k$ V) W5 B: E3 h* t1 d2 K 3{ 4 /* 临界段 2 */ 5 level2 = rt_hw_interrupt_disable(); /* 关中断,level2=1,PRIMASK=1 */ 6 {7 p* J1 ]8 ~4 c2 _, k0 p 7 }8 b' H0 J _) L/ ~ 8 rt_hw_interrupt_enable(level2); /* 开中断,level2=1,PRIMASK=1 */ % q" o9 n2 Y3 [: ]; ` 9}+ o5 c* a0 p! p8 s) C 10/* 临界段 1 结束 */ 11rt_hw_interrupt_enable(level1); /* 开中断,level1=0,PRIMASK=0 */ ' _( y* A$ d1 |1 W: e$ f7 ] % X6 o( u& g3 a; l1 n 这样子就完全避免了对吧! 有人又会问了,FreeRTOS的临界段能允许嵌套吗,答案是肯定的,FreeRTOS中早已给我们想好调用的函数了,并且全部使用宏定义实现了: 1#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI() 2#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 ) 3#define portENTER_CRITICAL() vPortEnterCritical()! Y2 N) c, u/ O+ c 4#define portEXIT_CRITICAL() vPortExitCritical() 5#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()' A9 Z$ L3 {5 @3 \% q: j 6#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)& l/ c- p1 d& g, Z/ g& B+ h. u 其实原理都是差不多的,通过保存和恢复寄存器basepri的数值就可以实现嵌套使用。( x7 @+ z, F& t7 K. \/ l 1UBaseType_t uxSavedInterruptStatus;/ L$ L2 p, w5 ]: L6 w3 w2 s% r 25 V3 c3 o/ e5 u3 y* |" a- ?* S 3uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR(); 4{: s, t6 w# c% o7 x* ~ 5 uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();; g. [, a' x( \( l1 ~/ V 6 { 7 //临界区代码9 i! \; J" n; I6 K' E- G7 D 8 }9 f4 l) A( d% i9 O# m! A8 r 9 portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );0 G- \6 j. S5 s# `5 E6 @ 10}( T# j5 ?0 G9 `0 o* o5 q/ }% ? 11portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus ); V/ H* u. G) P7 N 进入临界段源码的实现: 1static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void ) 2{ 3uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;8 z% E0 s A. n8 j+ Z 4 5 __asm 6 {8 ]# m) R8 @1 F( _% G 7 mrs ulReturn, basepri 8 msr basepri, ulNewBASEPRI 9 dsb6 | j4 p. u6 k, o/ w0 K 10 isb) [! ^& \+ Y5 X9 ?8 n 11 } 12 return ulReturn;2 I8 {) {7 M5 W0 Q( @1 n 13} , W/ X; j; c; l& V3 r 退出临界段源码实现:(跟前面的函数一样) 1static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI ) 2{ 3 __asm 4 { 5 msr basepri, ulBASEPRI% D, i) @0 A5 x( r& Q 6 } 7} 4 ~9 ^ `; N* K8 x0 \ 总结! N- V( }7 K0 {( f3 R 对于时间关键的任务而言,恰如其分地使用 PRIMASK 和 BASEPRI 来暂时关闭一些中断是非常重要的。1 G; N& b' ?2 ~5 V FreeRTOS源码中就有多处临界段的处理,除了FreeRTOS操作系统源码所带的临界段以外,用户写应用的时候也有临界段的问题,比如以下两种:
那假如我有一个线程,处理的时间较长,但是我又不想被其他线程打断,关中断可能影响系统的正常运行,怎么办呢?其实很简单,在OS中一般可以直接挂起调度器,系统正常运行,但是不会切换线程,当我处理完再把调度器解除即可。 ' a/ N7 I, a4 `7 K w7 ] RTOS使用得好,开发起来比裸机更简单,使用得不好,那将是噩梦——杰杰 -完- |
微信公众号
手机版
总结回复可见
1 ], Q2 e; Y$ ^, l2 w! @9 |
好,楼主引导咱们学freertos.
以前看过 ucos源码, freertos 已经用在项目中, 却没仔细看过, % z) n- p8 B" {7 _7 z0 M
用os的体会是, 要加入消息驱动的理念,消息驱动+状态机,天生在一起的. 如果用面向对象的思想, 那是再好不过了. 使用对象时加互斥量或信号保护.尽量避免使用全局变量. 对于全局变量, 比如一个32位的时间滴答变量, 在8,16位机中使用开关中断存取.而在32位机中, 由于读写都是原子操作(编程时4字节对齐即可,缺省的,1,2字节对齐是否原子操作就不知道了), 直接读写就可以了.
# E, i% m* q. V; n
对于串口这样的收发数据, 也可以利用FIFO避免使用开关中断. 以前咱都这么干的, 现在直接用 stm32cubemx 生成的代码, 懒得改了. 不理会那点性能损失., L5 U+ k0 r" C# B+ k
另外, 在任务可抢占的os中, 把使用公共变量的若干线程, 设置为同一个优先级, 避免在使用公共变量时被其它打断, 不失为一个好方法.7 b1 I8 N3 D: h! J, e. |9 A: p
----------------------------------------------------------------------------------- ~! F5 s; \. V' n- ?) z
以上, / {5 C1 b" D) l2 I
4 d; v6 ?& g) g0 O
9 F4 r" H1 v% m& u8 F9 F/ l. k" `- [
不客气的