简介

临界段代码也叫做临界区,是指那些必须完整运行,不能被打断的代码段,比如有的外设的初始化需要严格的时序,初始化过程中不能被打断。FreeRTOS 在进入临界段代码的时候需要关闭中断,当处理完临界段代码以后再打开中断。FreeRTOS 系统本身就有很多的临界段代码,这些代码都加了临界段代码保护,我们在写自己的用户程序的时候有些地方也需要添加临界段代码保护。利用开关中断的方式去保护临界区,只适合与单核的芯片。

FreeRTOS 开关中断

这里介绍FreeRTOS的开关总中断是因为FreeRTOS的临界区保护就是利用开关中断的方式实现的。还需要注意如果芯片是单核的可以采用开关中断实现临界区保护,但如果是多核的开关中断就不能保护临界区的。例如ESP32自带的FreeRTOS,乐鑫已经修改了FreeRTOS的源码去支持双核。FreeRTOS 开关中断函数为 portENABLE_INTERRUPTS ()和 portDISABLE_INTERRUPTS(),这两个函数其实是宏定义,在 portmacro.h 中有定义,如下:

  1. //关闭中断
  2. #define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
  3. //打开中断
  4. #define portENABLE_INTERRUPTS() vPortSetBASEPRI(0)
  5. static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
  6. {
  7. __asm
  8. {
  9. msr basepri, ulBASEPRI
  10. }
  11. }
  12. /*-----------------------------------------------------------*/
  13. //关闭中断
  14. static portFORCE_INLINE void vPortRaiseBASEPRI( void )
  15. {
  16. uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
  17. __asm
  18. {
  19. msr basepri, ulNewBASEPRI
  20. dsb
  21. isb
  22. }
  23. }

函数vPortSetBASEPRI()是向寄存器 BASEPRI 写入一个值,此值作为参数 ulBASEPRI 传递进来,portENABLE_INTERRUPTS()是开中断,它传递了个0给vPortSetBASEPRI(),结果就是开中断。 函 数vPortRaiseBASEPRI() 是向寄存器BASEPRI写入宏configMAX_SYSCALL_INTERRUPT_PRIORITY, 那么优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断就会被屏蔽!

任务级临界段代码保护

taskENTER_CRITICAL()taskEXIT_CRITICAL()是任务级的临界代码保护,一个是进入临界段,一个是退出临界段。

  1. //进入任务临界区
  2. #define taskENTER_CRITICAL() portENTER_CRITICAL()
  3. //退出任务临界区
  4. #define taskEXIT_CRITICAL() portEXIT_CRITICAL()
  5. portENTER_CRITICAL()和 portEXIT_CRITICAL()也是宏定义,在文件 portmacro.h 中有定义,如下:
  6. //进入任务临界区
  7. #define portENTER_CRITICAL() vPortEnterCritical()
  8. //退出任务临界区
  9. #define portEXIT_CRITICAL() vPortExitCritical()
  10. //进入任务临界区
  11. void vPortEnterCritical( void )
  12. {
  13. portDISABLE_INTERRUPTS();
  14. uxCriticalNesting++;
  15. if( uxCriticalNesting == 1 )
  16. {
  17. configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
  18. }
  19. }
  20. //退出任务临界区
  21. void vPortExitCritical( void )
  22. {
  23. configASSERT( uxCriticalNesting );
  24. uxCriticalNesting--;
  25. if( uxCriticalNesting == 0 )
  26. {
  27. portENABLE_INTERRUPTS();
  28. }
  29. }

可以看出在进入函数vPortEnterCritical()以后会首先关闭中断,然后给变量uxCriticalNesting加一,uxCriticalNesting是个全局变量,用来记录临界段嵌套次数的。函数 vPortExitCritical()是 退出临界段调用的,函数每次将 uxCriticalNesting 减一,只有uxCriticalNesting 为 0 的时候才会调用函数 portENABLE_INTERRUPTS()使能中断。这样保证了在有多个临界段代码的时候不会因为某一个临界段代码的退出而打乱其他临界段的保护,只有所有的临界段代码都退出以后才会使能中断!

中断级临界段代码保护

函数taskENTER_CRITICAL_FROM_ISR()taskEXIT_CRITICAL_FROM_ISR()中断级别临界段代码保护,是用在中断服务程序中的,而且这个中断的优先级一定要低于configMAX_SYSCALL_INTERRUPT_PRIORITY

  1. //进入中断临界区
  2. #define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
  3. //退出中断临界区
  4. #define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
  5. //进入中断临界区
  6. #define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
  7. //退出中断临界区
  8. #define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
  9. vPortSetBASEPRI()前面已经讲解了,就是给 BASEPRI 寄存器中写入一个值。
  10. //进入中断临界区
  11. static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
  12. {
  13. uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
  14. __asm
  15. {
  16. mrs ulReturn, basepri //先读出 BASEPRI 的值,保存在 ulReturn 中。
  17. msr basepri, ulNewBASEPRI //将 configMAX_SYSCALL_INTERRUPT_PRIORITY 写入到寄存器 BASEPRI 中。
  18. dsb
  19. isb
  20. }
  21. return ulReturn; //返回 ulReturn,退出临界区代码保护的时候要使用到此值
  22. }

由于系统会有中断嵌套的情况,因此当退出中断临界区的时候要通过进入中断临界区的返回值作为参数。只有当最后一个中断退出时才开启中断。这里可能会疑惑,我们明明已经关闭中断了,为什么还会出现中断嵌套,还记得configMAX_SYSCALL_INTERRUPT_PRIORITY的值吗?这个值的配置决定了我们虽然关闭中断,但并不是关闭所有中断。