-
2021-06-05 14:47:16
物联网临界段:
一,临界段的作用:
什么是临界段:在程序访问资源时,不希望被其他任务或者中断打断,这段要执行的代码,称为临界代码段。不想被打断访问的资源:
1,读取或者修改变量(全局变量)
2,调用公共函数的代码(不可重入函数)
3,使用硬件资源(外设)
4,对时序有精准要求的操作
5,用户不想被打断的代码二,临界段API:
void taskDISABLE_INTERRPUTS(void);这个函数的功能是:关闭中断,这里关中断只是频率低于configMAX_SYSCALL_INTERRUPT_PRIORITY优先级的中断(一些实时性的时间不能关闭,所以自己去配置这个优先级),采样此接口不允许嵌套。
void taskENABLE_INTERRUPTS(void);
功能:开启中断
注意事项:采用此接口,不允许嵌套。void taskENTER_CRITICAL(void);
功能:进入临界段,内部调用taskDISABLE_INTERRUPTS,支持嵌套任务在运行态内调用,不被中断打断,一直处于运行状态。注意事项:由于支持嵌套处理,有进入必须有退出,保证成对出现,保证临界段代码执行周期比较快,否则影响调度器进行调度,不能在中断中使用。
UBaseType_t taskENTER_CRITICAL_FROM_ISR(void);
功能:进入临界段(在中断中使用),内部调用taskDISABLE_INTERRUPTS,支持嵌套
返回值:UBaseType_t,返回上次中断屏蔽寄存器操作值。
更多相关内容 -
FreeRTOS 中断配置和临界段
2018-06-13 18:42:06"FreeRTOS学习笔记——FreeRTOS 中断配置和临界段"博客中所用代码 -
freertos临界段保护
2022-01-29 14:40:50freertos临界段保护中断的基础知识cortex-m里面开中断、关中断指令关中断和开中断进入临界段和退出临界段 中断的基础知识 嵌套: 嵌套向量中断控制器 NVIC(Nested Vectored Interrupt Controller与内核是紧耦合的。...freertos临界段保护
中断的基础知识
嵌套:
嵌套向量中断控制器 NVIC(Nested Vectored Interrupt Controller与内核是紧耦合的。提供如下的功能:可嵌套中断支持、向量中断支持、动态优先级调整支持、中断延迟大大缩短、 中断可屏蔽。
所有的外部中断和绝大多数系统异常均支持可嵌套中断。异常都可以被赋予不同的优先级,当前优先级被存储在 xPSR的专用字段中。当一个异常发生时,硬件自动比较该异常的优先级和当前的异常优先级,如果发现该异常的优先级更高,处理器就会中断当前的中断服务例程(或者是普通程序),而服务新来的异常(立即抢占)。
如果优先级组设置使得中断嵌套层次很深,要确认主堆栈空间足够用。 异常服务程序总是使用MSP,主堆栈的容量应是嵌套最深时需要的量。
优先级:
CM3 支持中断嵌套,使得高优先级异常会抢占(preempt)低优先级异常。
有3个系统异常:复位,NMI 以及硬 fault,它们有固定的优先级,并且优先级号是负数,高于所有其它异常。所有其它异常的优先级都是可编程的(但不能编程为负数)。
CM3 支持3个固定的高优先级和多达256级的可编程优先级,并且支持128级抢占。但是大多数CM3芯片实际上支持的优先级数会更少如8级、16级、32级等。
裁掉表达优先级的几个低端有效位,从而让优先级数减少。如果使用更多的位来表达优先级,优先级数增加,需要的门也更多,带来更多的成本和功耗。
使用3个位来表达优先级,优先级配置寄存器的结构如下图所示,能够使用的8个优先级为:0x00(最高),0x20,0x40,0x60,0x80, 0xA0,0xC0,0xE0。
为了使抢占机能变得更可控,CM3 把 256 级优先级按位分成高低两段,分别是抢占优先级和亚优先级。抢占优先级决定了抢占行为。当抢占优先级相同的异常有不止一个悬起时,就优先响应亚优先级最高的异常(亚优先级处理内务)。
优先级分组规定:亚优先级至少是1个位。所以抢占优先级最多是7个位,最多只有 128 级抢占的现象。
下图是只使用 3 个位来表达优先级,从bit5处分组,得到4级抢占优先级,每个抢占优先级的内部有2个亚优先级。
下图是3 位优先级,从比特1处分组,(虽然[4:0]未使用,却允许从它们中分组)。
应用程序中断及复位控制寄存器(AIRCR)(地址:0xE000_ED00)如下。
中断的悬起与解悬:
中断发生时,正在处理同级或高优先级异常,或者被掩蔽,则中断不能立即得到响应。此时中断被悬起。
可以通过中断设置悬起寄存器(SETPEND)、中断悬起清除寄存器(CLRPEND)读取中断的悬起状态,还可以写它们来手工悬起中断。
咬尾中断Tail‐Chaining:
处理器在响应某异常时,如果又发生其优先级高的异常,当前异常被阻塞,转而执行优先级高的异常。那么异常执行返回后,系统处理悬起的异常时,如果先POP再把POP出的再PUSH回去,这就是浪费CPU时间。
所以CM3不POP这些寄存器,而是继续使用上一个异常已经PUSH好的成果。如下图所示。
晚到的高优先级异常:
入栈的阶段,尚未执行其服务例程时,如果此时收到了高优先级异常的请求,入栈后,将执行高优先级异常的服务例程。如果高优先级异常来得太晚,以至于已经执行了前一个异常的指令,则按普通的抢占处理,这会需要更多的处理器时间和额外32字节的堆栈空间。高优先级异常执行完毕后,以咬尾中断方式执行之前被抢占的异常。
cortex-m里面开中断、关中断指令
临界段:一段在执行的时候不能被中断的代码段(必须完整运行、不能被打断的代码段)。一般是对全局变量操作时候,用到临界段。当一个任务在访问某个全局变量时,如果被其他中断打断,改变了该全局变量,再回到上个任务时,全局变量已经不是当时的它了,这种情况可能会导致不可意料的后果。
临界段被打断的情况:系统调度(最终也是产生PendSV中断);外部中断。
freertos进入临界段代码时需要关闭中断,处理完临界段代码再打开中断。
首先看下面的代码。
__asm void prvStartFirstTask( void ) { PRESERVE8 /* 在Cortex-M中,0xE000ED08是SCB_VTOR这个寄存器的地址, 里面存放的是向量表的起始地址,即MSP的地址 */ ldr r0, =0xE000ED08 ldr r0, [r0] ldr r0, [r0] /* 设置主堆栈指针msp的值 */ msr msp, r0 /* 使能全局中断 */ cpsie i cpsie f dsb isb /* 调用SVC去启动第一个任务 */ svc 0 nop nop } __asm void vPortSVCHandler( void ) { extern pxCurrentTCB; PRESERVE8 ldr r3, =pxCurrentTCB /* 加载pxCurrentTCB的地址到r3 */ ldr r1, [r3] /* 加载pxCurrentTCB到r1 */ ldr r0, [r1] /* 加载pxCurrentTCB指向的值到r0,目前r0的值等于第一个任务堆栈的栈顶 */ ldmia r0!, {r4-r11} /* 以r0为基地址,将栈里面的内容加载到r4~r11寄存器,同时r0会递增 */ msr psp, r0 /* 将r0的值,即任务的栈指针更新到psp */ isb mov r0, #0 /* 设置r0的值为0 */ msr basepri, r0 /* 设置basepri寄存器的值为0,即所有的中断都没有被屏蔽 */ orr r14, #0xd bx r14 } __asm void xPortPendSVHandler( void ) { extern pxCurrentTCB; extern vTaskSwitchContext; PRESERVE8 /* 当进入PendSVC Handler时,上一个任务运行的环境即: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参) 这些CPU寄存器的值会自动保存到任务的栈中,剩下的r4~r11需要手动保存 */ /* 获取任务栈指针到r0 */ mrs r0, psp isb ldr r3, =pxCurrentTCB /* 加载pxCurrentTCB的地址到r3 */ ldr r2, [r3] /* 加载pxCurrentTCB到r2 */ stmdb r0!, {r4-r11} /* 将CPU寄存器r4~r11的值存储到r0指向的地址 */ str r0, [r2] /* 将任务栈的新的栈顶指针存储到当前任务TCB的第一个成员,即栈顶指针 */ stmdb sp!, {r3, r14} /* 将R3和R14临时压入堆栈 */ mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY /* 进入临界段 */ msr basepri, r0 dsb isb bl vTaskSwitchContext /* 调用函数vTaskSwitchContext,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */ mov r0, #0 /* 退出临界段 */ msr basepri, r0 ldmia sp!, {r3, r14} /* 恢复r3和r14 */ ldr r1, [r3] ldr r0, [r1] /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/ ldmia r0!, {r4-r11} /* 出栈 */ msr psp, r0 isb bx r14 nop }
上面这些代码,可以看到有下面这些指令。
cpsie i cpsie f msr basepri, r0
为了快速地开关中断,CM3 专门设置了 CPS 指令,有 4 种用法。
CPSID I ;PRIMASK=1, ;关中断 CPSIE I ;PRIMASK=0, ;开中断 CPSID F ;FAULTMASK=1, ;关异常 CPSIE F ;FAULTMASK=0 ;开异常
可以看到,上面指令还是控制的PRIMASK和FAULTMASK寄存器。
如下图所示,可以通过CPS 指令打开全局中断或者关闭全局中断。
basepri是中断屏蔽寄存器,下面这个设置,优先级大于等于11的中断都将被屏蔽。相当于关中断进入临界段。
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY msr basepri, r0 /* #define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 /* 高四位有效,即等于0xb0,或者是11 */ 191转成二进制就是11000000,高四位就是1100 */
下面这个代码:优先级高于0的中断被屏蔽,相当于是开中断退出临界段。
mov r0, #0 /* 退出临界段 */ msr basepri, r0
关中断和开中断
下面这个代码,带返回值的意思是:往BASEPRI写入新的值的时候,先将BASEPRI的值保存起来,更新完BASEPRI的值的时候,将之前保存好的BASEPRI的值返回,返回的值作为形参传入开中断函数。
不带返回值的意思是:在往 BASEPRI 写入新的值的时候,不用先将 BASEPRI 的值保存起来, 不用管当前的中断状态是怎么样的,既然不用管当前的中断状态,也就意味着这样的函数不能在中断里面调用。
/*portmacro.h*/ /*不带返回值的关中断函数,不能嵌套,不能在中断中使用*/ #define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI() /*不带中断保护的开中断函数*/ #define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 ) /*带返回值的关中断函数,可以嵌套,可以在中断里面使用*/ #define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI() /*带中断保护的开中断函数*/ #define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x) #define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 /* 高四位有效,即等于0xb0,或者是11 */ /*不带返回值的关中断函数*/ static portFORCE_INLINE void vPortRaiseBASEPRI( void ) { uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; __asm { /* Set BASEPRI to the max syscall priority to effect a critical section. */ msr basepri, ulNewBASEPRI dsb isb } } /*带返回值的关中断函数*/ static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void ) { uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; __asm { /* Set BASEPRI to the max syscall priority to effect a critical section. */ mrs ulReturn, basepri msr basepri, ulNewBASEPRI dsb isb } return ulReturn; } /*不带中断保护的开中断函数和带中断保护的开中断函数,区别在于参数的值*/ static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI ) { __asm { /* Barrier instructions are not used as this function is only used to lower the BASEPRI value. */ msr basepri, ulBASEPRI } }
进入临界段和退出临界段
对于不带中断保护情况,vPortEnterCritical函数里面的uxCriticalNesting是一个全局变量,记录临界段嵌套次数,vPortExitCritical函数每次将uxCriticalNesting减一,只有当uxCriticalNesting = 0才会调用portENABLE_INTERRUPTS函数使能中断。这样的话,在有多个临界段代码的时候,不会因为某一个临界段代码的退出而打断其他临界段的保护,只有所有的临界段代码都退出后,才会使能中断。
带中断保护的,主要就是往BASEPRI写入新的值的时候,先将BASEPRI的值保存起来,更新完BASEPRI的值的时候,将之前保存好的BASEPRI的值返回,返回的值作为形参传入开中断函数。
/*进入临界段,不带中断保护*/ #define taskENTER_CRITICAL() portENTER_CRITICAL() /*退出临界段,不带中断保护*/ #define taskEXIT_CRITICAL() portEXIT_CRITICAL() /*进入临界段,带中断保护,可以嵌套*/ #define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR() /*退出临界段,带中断保护,可以嵌套*/ #define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x ) /*进入临界段,不带中断保护*/ #define portENTER_CRITICAL() vPortEnterCritical() /*退出临界段,不带中断保护*/ #define portEXIT_CRITICAL() vPortExitCritical() /*进入临界段,带中断保护,可以嵌套*/ #define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI() /*退出临界段,带中断保护,可以嵌套*/ #define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x) /*进入临界段,不带中断保护*/ void vPortEnterCritical( void ) { /*不带返回值的关中断函数,不能嵌套,不能在中断中使用*/ portDISABLE_INTERRUPTS(); uxCriticalNesting++; if( uxCriticalNesting == 1 ) { configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 ); } } /*退出临界段,不带中断保护*/ void vPortExitCritical( void ) { configASSERT( uxCriticalNesting ); uxCriticalNesting--; if( uxCriticalNesting == 0 ) { /*不带中断保护的开中断函数*/ portENABLE_INTERRUPTS(); } } /*进入临界段,带中断保护,可以嵌套*/ static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void ) { uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; __asm { /* Set BASEPRI to the max syscall priority to effect a critical section. */ mrs ulReturn, basepri msr basepri, ulNewBASEPRI dsb isb } return ulReturn; } /*退出临界段,带中断保护,可以嵌套*/ static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI ) { __asm { /* Barrier instructions are not used as this function is only used to lower the BASEPRI value. */ msr basepri, ulBASEPRI } }
/*临界段代码的应用场合*/ /* 在中断场合,临界段可以嵌套 */ { uint32_t ulReturn; /* 进入临界段,临界段可以嵌套 */ ulReturn = taskENTER_CRITICAL_FROM_ISR(); /* 临界段代码 */ /* 退出临界段 */ taskEXIT_CRITICAL_FROM_ISR( ulReturn ); } /* 在非中断场合,临界段不能嵌套 */ { /* 进入临界段 */ taskENTER_CRITICAL(); /* 临界段代码 */ /* 退出临界段*/ taskEXIT_CRITICAL(); }
-
uCOS-III 学习记录(5)——临界段
2022-01-28 14:35:45文章目录1 临界段2 临界段的保护2.1 Cortex-M 内核的中断指令2.2 开中断和关中断2.2.1 关中断 CPU_SR_Save()(cpu_a.asm)2.2.2 开中断 CPU_SR_Restore()(cpu_a.asm)2.2.3 宏定义封装(cpu.h)2.3 临界段保护的...参考内容:《[野火]uCOS-III内核实现与应用开发实战指南——基于STM32》第 10 章。
文章目录
1 临界段
临界段,又叫做临界区。对于多线程而言,它是一段不可分割、不可上下文切换的代码。对于 uCOS 而言,它是一段不可被中断的代码。临界段是不能被中断的,需要关中断或锁调度器(OSSched)以保护临界段。
什么情况下临界段代码会被打断?由刚才的描述可知,临界段被打断有两种情况:
- 外部中断。
- 系统调度。在系统调度中会产生 PendSV 异常中断,最终也可以归结为中断。
因此,对临界段保护的实质就是控制中断的开启和关闭。
uCOS 定义了进入临界段的宏和退出临界段的宏,这两个宏分别实现了中断的开启和关闭。
- OS_CRITICAL_ENTER() 或 CPU_CRITICAL_ENTER()
- OS_CRITICAL_EXIT() 或 CPU_CRITICAL_EXIT()
还有一个宏,用于存储中断的状态:
- CPU_SR_ALLOC()
2 临界段的保护
2.1 Cortex-M 内核的中断指令
在 CM 内核中,中断是通过 CPS 指令来控制的。
CPSID I ;PRIMASK=1,关中断 CPSIE I ;PRIMASK=0,开中断 CPSID F ;FAULTMASK=1,关异常 CPSIE F ;FAULTMASK=0,开异常
PRIMASK 和 FAULTMAST 是 CM 内核里面三个中断屏蔽寄存器中的两个,还有一个是 BASEPRI,最后一个用不到,就不进行介绍了。
- PRIMASK:只有 1 个比特位的寄存器。在它被置 1 后,就关掉所有可屏蔽的异常,只剩下 NMI(不可屏蔽中断)和硬件 FAULT(HardFault) 可以响应。它的默认值是 0,表示没有关中断。
- FAULTMASK:只有 1 个比特位的寄存器。当它置 1 时,只有 NMI(不可屏蔽中断)才能响应,所有其他的异常,甚至是硬件 FAULT(HardFault),也通通闭嘴。它的默认值是 0,表示没有关异常。
因此,也可以通过 MSR 指令修改 PRIMASK(或 FAULTMASK)来开启或关闭中断:
MOVS R0, #1 MSR PRIMASK, R0 ; 将 1 写入 PRIMASK 禁止所有中断 MOVS R0, #0 MSR PRIMASK, R0 ; 将 0 写入 PRIMASK 使能中断
2.2 开中断和关中断
2.2.1 关中断 CPU_SR_Save()(cpu_a.asm)
该函数完成的事情:
- 存储中断状态:通过 MRS 指令将特殊寄存器 PRIMASK 寄存器的值存储到通用寄存器 R0。(当在 C 中调用汇编的子程序返回时,会将 R0 作为函数的返回值。所以在 C 中调用 CPU_SR_Save() 的时候,需要事先声明一个变量用来存储 CPU_SR_Save() 的返回值,即 R0 寄存器的值,也就是 PRIMASK 的值。)
- 关闭中断:即使用 CPS 指令将 PRIMASK 寄存器的值置 1。
- 子程序返回。
; CPU_SR CPU_SR_Save (void); (临界段关中断,R0 为返回值) CPU_SR_Save MRS R0, PRIMASK ; 将 PRIMASK 寄存器的值存入 R0 中 CPSID I ; 关中断 BX LR
2.2.2 开中断 CPU_SR_Restore()(cpu_a.asm)
该函数完成的事情:
- 恢复中断状态:通过 MSR 指令将通用寄存器 R0 的值存储到特殊寄存器 PRIMASK。(当在 C 中调用汇编的子程序返回时,会将第一个形参传入到通用寄存器 R0。所以在 C 中调用 CPU_SR_Restore() 的时候,需要传入一个形参,该形参是进入临界段之前保存的 PRIMASK 的值。)
- 子程序返回。
; void CPU_SR_Restore (CPU_SR cpu_sr); (临界段开中断,R0 为形参) CPU_SR_Restore MSR PRIMASK, R0 ; 将 R0 的值存入 PRIMASK 寄存器中 BX LR
为什么开中断不直接使用 CPS 指令呢?待会在应用那节(2.3.2 节)你就会明白了。
2.2.3 宏定义封装(cpu.h)
最后,在 cpu.h 中,将开中断和关中断的函数封装成一个宏,方便调用。
/*********************************CPU寄存器数据类型定义*********************************/ typedef volatile CPU_INT32U CPU_REG32; typedef CPU_REG32 CPU_SR; /*********************************临界段定义*********************************/ #define CPU_SR_ALLOC() CPU_SR cpu_sr = (CPU_SR)0 // 用于存放中断状态 #define CPU_INT_DIS() do { cpu_sr = CPU_SR_Save(); } while(0) // 关闭中断,存储中断状态 #define CPU_INT_EN() do { CPU_SR_Restore(cpu_sr); } while(0) // 恢复中断状态 #define CPU_CRITICAL_ENTER() do { CPU_INT_DIS(); } while(0) #define CPU_CRITICAL_EXIT() do { CPU_INT_EN(); } while(0) /*********************************函数声明(cpu_a.asm)*********************************/ void CPU_IntDis (void); void CPU_IntEn (void); CPU_SR CPU_SR_Save (void); void CPU_SR_Restore (CPU_SR cpu_sr);
2.3 临界段保护的应用
2.3.1 一层临界段的应用
如果有这么一段临界段代码:
/* 临界段代码保护 */ { /* 临界段开始 */ { /* 执行临界段代码,不可中断 */ } /* 临界段结束 */ }
那么使用以上宏定义的格式为:
/* 临界段代码保护 */ { CPU_SR_ALLOC(); /* cpu_sr = 0 */ CPU_INT_DIS(); /* 关中断 */ /* 临界段开始 */ { /* 执行临界段代码,不可中断 */ } /* 临界段结束 */ CPU_INT_EN(); /* 开中断 */ }
若将其展开,则变成:
/* 临界段代码保护 */ { CPU_SR cpu_sr = (CPU_SR)0; /* (a) 定义一个变量,用于存放中断状态,初始化 cpu_sr = 0 */ cpu_sr = CPU_SR_Save(); /* (b) cpu_sr 保存当前中断状态,然后关中断 */ /* 临界段开始 */ { /* 执行临界段代码,不可中断 */ } /* 临界段结束 */ CPU_SR_Restore(cpu_sr); /* (c) cpu_sr 写入中断状态,恢复之前的中断状态 */ }
这个过程如下:
- (a)临界段开始前,定义一个变量 cpu_sr,用于存储 PRIMASK 的值,即用于保存中断状态。
- (b)先将当前中断状态(即 PRIMASK)的值存储在 cpu_sr 中。因为当前未关闭中断,所以 PRIMASK = 0,cpu_sr = 0。然后关闭中断,使 PRIMASK = 1。
- (c)执行完临界段代码后,恢复此前的中断状态,将 cpu_sr 写入到 PRIMASK 中。当前 PRIMASK = 1,cpu_sr = 0,写入后,PRIMASK = 0,恢复此前中断状态。
2.3.2 多层临界段的应用
如果是两层临界段代码的嵌套:
/* 临界段代码保护 */ { /* 临界段 1 开始 */ { /* 临界段 2 开始 */ { /* 执行临界段代码,不可中断 */ } /* 临界段 2 结束 */ } /* 临界段 1 结束 */ }
进入临界段 1 前,需要关闭中断;进入临界段 2 前,也需要关闭中断,那么使用宏定义的格式为:
/* 临界段代码保护 */ { CPU_SR_ALLOC(); /* cpu_sr = 0 */ CPU_INT_DIS(); /* 关中断(临界段 1) */ /* 临界段 1 开始 */ { CPU_SR_ALLOC(); /* cpu_sr = 0 */ CPU_INT_DIS(); /* 关中断(临界段 2) */ /* 临界段 2 开始 */ { /* 执行临界段代码,不可中断 */ } /* 临界段 2 结束 */ CPU_INT_EN(); /* 开中断(临界段 2) */ } /* 临界段 1 结束 */ CPU_INT_EN(); /* 开中断(临界段 1) */ }
展开宏定义,代码可等效为:
/* 临界段代码保护 */ { CPU_SR cpu_sr1 = (CPU_SR)0; /* (a) 定义一个变量,用于存放中断状态,初始化 cpu_sr1 = 0 */ cpu_sr1 = CPU_SR_Save(); /* (b) cpu_sr1 保存当前中断状态,然后关中断 */ /* 临界段 1 开始 */ { CPU_SR cpu_sr2 = (CPU_SR)0; /* (c) 定义一个变量,用于存放中断状态,初始化 cpu_sr2 = 0 */ cpu_sr2 = CPU_SR_Save(); /* (d) cpu_sr2 保存当前中断状态,然后关中断 */ /* 临界段 2 开始 */ { /* 执行临界段代码,不可中断 */ } /* 临界段 2 结束 */ CPU_SR_Restore(cpu_sr2); /* (e) cpu_sr2 写入中断状态,恢复之前的中断状态 */ } /* 临界段 1 结束 */ CPU_SR_Restore(cpu_sr1); /* (f) cpu_sr1 写入中断状态,恢复之前的中断状态 */ }
这个过程如下:
- (a)临界段 1 开始前,定义一个变量 cpu_sr1 用于存储中断状态。
- (b)先将当前中断状态(即 PRIMASK)的值存储在 cpu_sr1 中。因为当前未关闭中断,所以 PRIMASK = 0,cpu_sr1 = 0。然后关闭中断,使 PRIMASK = 1。
- (c)临界段 2 开始前,再定义一个变量 cpu_sr2 用于存储中断状态。
- (d)先将当前中断状态(即 PRIMASK)的值存储在 cpu_sr2 中。因为当前已经关闭中断,所以 PRIMASK = 1,cpu_sr2 = 1。然后关闭中断,使 PRIMASK = 1(其实在之前 PRIMASK 就已经为 1 了)。
- (e)执行完临界段 2 后,恢复此前的中断状态,将 cpu_sr2 写入到 PRIMASK 中。当前 PRIMASK = 1,cpu_sr2 = 1,写入后,PRIMASK = 1,恢复此前中断状态,即还是关中断状态。(这一步算是关键了,也彻底弄明白开中断为何不直接用 CPS 指令的原因。若使用 CPS 直接开中断,则会出现:回到临界段 1 时,发现中断居然开着!)
- (f)执行完临界段 1 后,恢复此前的中断状态,将 cpu_sr1 写入到 PRIMASK 中。当前 PRIMASK = 1,cpu_sr1 = 0,写入后,PRIMASK = 0,恢复此前中断状态,也就是开中断状态。
3 测量关中断时间
本部分先忽略,因为还不是我重点关注的部分。待有时间再来研究。
-
RTOS的临界段知识详解
2018-09-09 00:34:21大家周末好,刚回学校,乱七八糟的事情一堆,抽个...为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即打开中断。 临界段的作用 其实在RTOS中,使用最多的临界段是OS...大家周末好,刚回学校,乱七八糟的事情一堆,抽个时间更新下~
写在前面的话
本章主要讲解RTOS的临界段
什么是临界段
代码的临界段也称为临界区,指处理时不可分割的代码区域,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即打开中断。
临界段的作用
其实在RTOS中,使用最多的临界段是OS本身的调用,但是我们用户也是需要对临界资源进行保护的(临界资源是一次仅允许一个线程使用的共享资源),特别是一些全局变量,当线程正在使用的时候不希望有人来打断我的操作,就行很多时候我们写代码时,需要集中精力,不希望别人打断我们的思路一样。这样子使得系统的运行更加稳定健壮。
什么时候会打断代码的执行?
顾名思义,代码正在正常运行的时候,基本不会被打断,能被打断的都是系统发生了异常(中断也是异常),在OS中,除了外部中断能将正在运行的代码打断,还有线程的调度——PendSV,系统产生 PendSV中断,在 PendSV Handler 里面实现线程的切换。我们要将这项东西屏蔽掉,保证当前只有一个线程在使用临界资源。
如何关闭中断?
其实,在我们常用的MCU中,一般为Cortex-M内核的,M内核是有一些指令能快速关闭中断,一起来看看Cortex-M权威指南吧(以Cortex-M3为例)。
简单来说,快速屏蔽中断就是处理这些内核寄存器,在Cortex-M中有相应的操作指令,一般我们无需关注,因为OS已经给我们写好了这些底层的东西。不过如果你是想自己写一个OS的话,可以了解一下,要访问 PRIMASK, FAULTMASK 以及 BASEPRI,同样要使用 MRS/MSR 指令,如:
1MRS R0, BASEPRI ;读取 BASEPRI 到 R0 中 2MRS R0, FAULTMASK ;似上 3MRS R0, PRIMASK ;似上 4MSR BASEPRI, R0 ;写入 R0 到 BASEPRI 中 5MSR FAULTMASK, R0 ;似上 6MSR PRIMASK, R0 ;似上
只有在特权级下,才允许访问这 3 个寄存器。
其实,为了快速地开关中断, CM3 还专门设置了一条 CPS 指令,有 4 种用法:
1 CPSID I ;PRIMASK=1, ;关中断 2 CPSIE I ;PRIMASK=0, ;开中断 3 CPSID F ;FAULTMASK=1, ;关异常 4 CPSIE F ;FAULTMASK=0 ;开异常
上面的代码中的PRIMASK和 FAULTMAST 是 Cortex-M 内核 里面三个中断屏蔽寄存器中的两个,还有一个是 BASEPRI,这些寄存器都用于屏蔽中断。具体的作用见表格(表格出自《【野火】RT-Thread 内核实现与应用开发实战指南》)
名字 功能描述 PRIMASK 这是个只有单一比特的寄存器。 在它被置 1 后,就关掉所有可屏蔽的异常,只剩下 NMI 和硬 FAULT 可以响应。它的缺省值是 0,表示没有关中断。 FAULTMASK 这是个只有 1 个位的寄存器。当它置 1 时,只有 NMI 才能响应,所有其它的异常,甚至是硬 FAULT,也通通闭嘴。它的缺省值也是 0,表示没有关异常。 BASEPRI 这个寄存器最多有 9 位(由表达优先级的位数决定)。它定义了被屏蔽优先级的阈值。当它被设成某个值后,所有优先级号大于等于此值的中断都被关(优先级号越大,优先级越低)。但若被设成 0,则不关闭任何中断, 0 也是缺省值。 不同OS的处理临界段的区别
FreeRTOS:
FreeRTOS对中断的开和关是通过操作 BASEPRI 寄存器来实现的,即大于等于 BASEPRI 的值的中断会被屏蔽,小于 BASEPRI 的值的中断则不会被屏蔽。这样子的好处就是用户可以设置 BASEPRI 的值来选择性的给一些非常紧急的中断留一条后路。比如飞控的防撞处理。代码在portmacro.h 中实现:
屏蔽中断:
1static portFORCE_INLINE void vPortRaiseBASEPRI( void ) 2{ 3uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; 4 5 __asm 6 { 7 msr basepri, ulNewBASEPRI 8 dsb 9 isb 10 } 11}
打开中断:
1static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI ) 2{ 3 __asm 4 { 5 msr basepri, ulBASEPRI 6 } 7}
RT-Thread:
与FreeRTOS不同的是,RT-Thread 对临界段的保护处理的很干脆,不管三七二十一直接把中断全部关了(直接操作PRIMASK内核寄存器), 只有NMI FAULT 和硬 FAULT能被相应。 这种方法简单粗暴,是很不错的选择。一般我们临界段的处理时间是比较短的,关了再开其实并没有太大的影响。
现在要看看RT-Thread的关中断的代码实现:
1rt_hw_interrupt_disable PROC 2 EXPORT rt_hw_interrupt_disable 3 MRS r0, PRIMASK 4 CPSID I 5 BX LR 6 ENDP
开中断:
1rt_hw_interrupt_enable PROC 2 EXPORT rt_hw_interrupt_enable 3 MSR PRIMASK, r0 4 BX LR 5 ENDP
这短短的几句代码其实还是很有意思的,我就引用火哥的话来解释一下这些处理操作(我个人是不会汇编的,但是跟着书来解读这些代码还是很轻而易举的)
可能有人懂汇编的话,就会看出来,关中断,不就是直接使用 CPSID I 指令就行了嘛~开中断,不就是使用 CPSIE I 指令就行了嘛,为啥跟我等凡人想的不一样?
RT-Thread的处理好像是多此一举了,实则不然,“所有东西的存在必然有其存在的意义”这句话应该没人反驳吧~~因为RT-Thread要防止用户错误地退出了中断临界段,因为这样子可能会产生巨大的危害,所以RT-Thread将当前的PRIMASK的状态保存起来,这样子就必须要关多少次中断就得开多少次中断。
怎么说呢,用例子来证明吧:
1/* 临界段 1 开始 */ 2rt_hw_interrupt_disable(); /* 关中断,PRIMASK = 1 */ 3{ 4 /* 临界段 2 */ 5 rt_hw_interrupt_disable(); /* 关中断,PRIMASK = 1 */ 6 { 7 } 8 rt_hw_interrupt_enable(); /* 开中断,PRIMASK = 0 */ (注意) 9} 10/* 临界段 1 结束 */ 11rt_hw_interrupt_enable(); /* 开中断,PRIMASK = 0 */
如果直接操作PRIMASK,而不保存PRIMASK的状态,这样子当临界段2结束后调用一次打开中断,那么连临界段1的后半部分就无效了。而RT-Thread的实现就能很好避免这种问题,也用代码来说明吧:
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 { 7 } 8 rt_hw_interrupt_enable(level2); /* 开中断,level2=1,PRIMASK=1 */ 9} 10/* 临界段 1 结束 */ 11rt_hw_interrupt_enable(level1); /* 开中断,level1=0,PRIMASK=0 */
这样子就完全避免了对吧!
有人又会问了,FreeRTOS的临界段能允许嵌套吗,答案是肯定的,FreeRTOS中早已给我们想好调用的函数了,并且全部使用宏定义实现了:
1#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI() 2#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 ) 3#define portENTER_CRITICAL() vPortEnterCritical() 4#define portEXIT_CRITICAL() vPortExitCritical() 5#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI() 6#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
其实原理都是差不多的,通过保存和恢复寄存器basepri的数值就可以实现嵌套使用。
1UBaseType_t uxSavedInterruptStatus; 2 3uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR(); 4{ 5 uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR(); 6 { 7 //临界区代码 8 } 9 portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus ); 10} 11portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
进入中断源码的实现:
1static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void ) 2{ 3uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; 4 5 __asm 6 { 7 mrs ulReturn, basepri 8 msr basepri, ulNewBASEPRI 9 dsb 10 isb 11 } 12 return ulReturn; 13}
退出中断源码实现:(跟前面的函数一样)
1static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI ) 2{ 3 __asm 4 { 5 msr basepri, ulBASEPRI 6 } 7}
总结
对于时间关键的任务而言,恰如其分地使用 PRIMASK 和 BASEPRI 来暂时关闭一些中断是非常重要的。
FreeRTOS源码中就有多处临界段的处理,除了FreeRTOS操作系统源码所带的临界段以外,用户写应用的时候也有临界段的问题,比如以下两种:
-
读取或者修改变量(特别是用于任务间通信的全局变量)的代码,一般来说这是最常见的临界代码。
-
调用公共函数的代码,特别是不可重入的函数,如果多个任务都访问这个函数,结果是可想而知的。
总之,对于临界段要做到执行时间越短越好,否则会影响系统的实时性。
那假如我有一个线程,处理的时间较长,但是我又不想被其他线程打断,关中断可能影响系统的正常运行,怎么办呢?其实很简单,在OS中一般可以直接挂起调度器,系统正常运行,但是不会切换线程,当我处理完再把调度器解除即可。
RTOS使用得好,开发起来比裸机更简单,使用得不好,那将是噩梦——杰杰
-end-
-
-
【FreeRTOS】中断与临界段
2021-08-20 20:05:20三、临界段代码 1、进入临界段函数 2、退出临界段 3、用法 一、Cortex-M中断 1、中断简介 Cortex-M内核MCU提供了一个用于中断管理的嵌套向量中断控制器(NVIC),M3、M4的NVIC最多支持240个中断... -
FreeRTOS内核详解(1) —— 临界段保护原理
2021-12-13 11:52:26什么是临界段 临界段用一句话概括就是一段在执行的时候不能被中断的代码段。 在 FreeRTOS 里面,这个临界段最常出现的就是对全局变量的操作,由于不同任务间可以切换运行,当一个任务在访问某个全局变量时,这时其他... -
FreeRTOS-临界段
2020-06-26 17:48:36什么是临界段?临界段就是在执行时不能被打断(除非是系统调度,或外部中断)的代码。 FreeRTOS对临界段的保护还是通过对中断的开和关来控制。 Cortex-M内核快速关中断指令: ①: 这个寄存器是只有一位的寄存器。... -
FreeRTOS——临界段保护
2020-02-14 13:53:49一、什么是临界段 临界段就是一段在执行的时候不能被打断的代码。在FreeRTOS中,临界段最常出现的就是对全局变量的操作。那么在什么情况下临界段可以被打断?一个是系统调度,另一个是外部中断。但是在FreeRTOS中,... -
临界段代码保护
2021-10-14 15:37:271:临界段:在执行的时候不能被中断的代码段,最常见的是对全局变量的操作,能关闭的最大中断 由这个宏决定configMAX_SYSCALL_INTERRUPT_PRIORITY 他是按照单片机的中断等级的,比他小的都不能控制,因为操作的是... -
FreeRTOS临界段代码保护[STM32]
2022-04-09 20:47:34临界状态,也就是程序进入了临界区,临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个 线程 访问的特性。 当有线程进入临界区段时,其他线程或是进程必须... -
代码的临界段
2021-09-20 16:59:03代码的临界段也称为临界区,指处理时不可分割的代码。 一旦这部分代码开始执行,则不允许任何中断打入。 -
freertos临界段代码保护机制
2020-02-27 14:55:3615.1临界段 代码的临界段也称为临界区,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即开中断。 lFreeRTOS临界段... -
FreeRTOS之临界段的保护
2021-03-08 22:02:31临界段含义:一段在执行的过程中不可被中断的代码段 FREEROTS的临界段比较容易 临界段的保护无非是利用开关中断来实现的 有四种: 1、不带返回值的关中断函数:不可嵌套---这样的函数不能再中断中调用 2、带... -
FreeRTOS 临界段和开关中断
2019-01-31 08:40:50临界段 代码的临界段也称为临界区,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码 的执行不被中断,在进入...FreeRTOS 的源码中有多处临界段的地方, 临界段虽然保护了关键代码的执行不被打断,... -
ucosIII临界段代码
2020-07-25 17:09:10临界段:临界段并不是一片区域,而是一段时间 临界段代码:一段完整的程序必须连续运行,不能被打断的代码 中断处理函数和任务都会访问的临界段代码,需要用关中断的方式加以保护 只有任务访问的临界段代码,一般用... -
从0到1教你写UCOS-III 第九部分:临界段
2019-05-02 21:59:309.1 临界段简介: 临界段代码,也称作临界域,是一段不可分割的代码。 uCOS 中包含了很多临界段代码。如果临界段可能被中断,那么就需要关中断以保护临界段。如果临界段可能被任务级代码打断,那么需要锁调度器... -
临界段
2016-12-04 10:09:42临界段只能用于一个进程中不同线程的通信; 与互斥量的不同体现在两点上: 1.互斥量是内核变量,所以可以跨进程使用,当必须在进程间使用时,需要用互斥量;临界段不是互斥变量,他是属于进程内部的,对于进程间的... -
rtthread临界段的保护
2020-12-17 22:42:10title: rtthread临界段的保护 date: 2020-10-22 15:29:49 tags: rtthread 临界段用一句话概括就是一段在执行的时候不能被中断的代码段。 Cortex-M 内核快速关中断指令 CPSID I ;PRIMASK=1 ;关中断 CPSIE I ;... -
RT-Thrdad中临界段的作用
2020-03-15 17:46:21临界段是程序在执行时的一段不受干扰的代码执行时间,当进入临界段的时候,其他中断和高优先级的线程无法打断临界段代码的执行,因此,临界段经常会用于有时序的程序,比如I2C,USART等。 例如要重定义串口的rt_... -
从0到1写RT_Thread内核 ——— 临界段保护的实现
2020-02-26 16:06:58临界段通俗的解释就是一段不能被打断执行的代码,比如说再对内部FLASH进行写入时,可以加上临界段的保护,多线程对一个全局变量的操作时,加上临界段的保护可以避免一些意外的情况发生。比如这个线程在进行对此全局... -
2.临界段代码学习
2017-06-15 09:11:53临界段代码的定义 临界段代码,也称作临界域,是一段不可分割的代码。uC/OS-III中包含了很多临界段代码。如果临界段可能被中断,那么就需要关中断以保护临界段。如果临界段可能被任务级代码打断,那么需要锁调度器... -
临界段的保护.zip
2020-06-26 16:55:44观看野火视频教程写的代码,freeRTOS临界段的保护。解释请参考我的博客。欢迎下载使用。欢迎批评指正。 -
四、FreeRTOS 中断配置和临界段
2020-02-13 20:45:10FreeRTOS 系统本身就有很多的临界段代码,这些代码都加了临界段代码保护,我们在写自己的用户程序的时候有些地方也需要添加临界段代码保护。 FreeRTOS 与 临 界 段 代 码 保 护 有 关 的 函 数 有 4 个 : ... -
FreeRTOS — 临界段和开关中断
2018-04-18 17:50:34为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即开中断。FreeRTOS 源码中就有多处临界段的处理,跟 FreeRTOS 一样,uCOS-II 和 uCOS-III 源码中都是有临... -
RTOS临界段如何实现嵌套
2022-04-18 17:46:58如上代码存在临界段嵌套问题,f1执行完后就打开中断了,所以f2的do_something不处于临界段中 解决方法 以FreeRTOS为例子 非中断中,增加全局变量记录嵌套次数,只有为0时才使能中断 中断中,通过备份basepri来实现... -
uCOS-III(3) 临界段与就绪列表
2020-04-01 19:39:18临界段临界段简介快速指令就绪列表优先级就绪列表 临界段简介 临界段是什么,临界段就是你不可逾越的底线。兵来将挡水来土掩,中断来了关中断。但是总会有人能突破你的底线,你比如说,系统调度和外部中断,前面有讲... -
FreeRTOS临界段
2021-03-09 19:42:52临界断代码也叫做临界区,是指那些必须完整运行,不能被打断的代码段,FreeRTOS与临界断代码保护有关的函数有4个: taskENTER_CRITICAL() ——任务级进入临界断 taskEXIT_CRITICAL() ——任务级退出临界断 ... -
STM32CubeMX学习笔记——FreeRTOS临界段保护
2019-10-24 16:36:18临界段指的是在程序运行过程中,禁止被打断运行的代码。 比如:在使用模拟I2C 时,操作IO口可能需要几十个us,这段时间必须是严格的时序。 若在此期间,被其他原因(如中断)打断,那后果可想而知。 所以,这种禁止... -
STM32之FreeRTOS:(一) 中断配置和临界段的使用
2021-03-22 20:55:07STM32之FreeRTOS:(一) 中断配置和临界段的使用 文章目录STM32之FreeRTOS:(一) 中断配置和临界段的使用前言 一、stm32的NVIC 分组配置二、FreeRTOS 相关API函数1.开关中断2.临界段代码1、taskENTER_CRITICAL()和 ...