FreeRTOS学习笔记——FreeRTOS 中断配置和临界段

FreeRTOS 的中断配置是一个很重要的内容,需要根据所使用的MCU 来具体配置。这需要了解MCU 架构中有关中断的知识,本章会结合Cortex-M 的NVIC 来讲解STM32 平台下的FreeRTOS 中断配置,本章分为如下几部分:
4.1 Cortex-M 中断
4.2 FreeRTOS 中断配置宏
4.3 FreeRTOS 开关中断
4.4 临界段代码

4.5 FreeRTOS 中断测试实验

4.1 Cortex-M 中断
4.1.1 中断简介
中断是微控制器一个很常见的特性,中断由硬件产生,当中断产生以后CPU 就会中断当前的流程转而去处理中断服务,Cortex-M 内核的MCU 提供了一个用于中断管理的嵌套向量中断控制器(NVIC)。Cotex-M3 的NVIC 最多支持240 个IRQ(中断请求)、1 个不可屏蔽中断(NMI)、1 个Systick(滴答定时器)定时器中断和多个系统异常。
4.1.2 中断管理简介
Cortex-M 处理器有多个用于管理中断和异常的可编程寄存器,这些寄存器大多数都在NVIC 和系统控制块(SCB)中,CMSIS 将这些寄存器定义为结构体。以STM32F103 为例,打开core_cm3.h,有两个结构体,NVIC_Type 和SCB_Type,如下:


NVIC 和SCB 都位于系统控制空间(SCS)内,SCS 的地址从0XE000E000 开始,SCB 和NVIC的地址也在core_cm3.h 中有定义,如下:


以上的中断控制寄存器我们在移植FreeRTOS 的时候是不需要关心的,这里只是提一下,大家要是感兴趣的话可以参考Cortex-M 的权威指南,我们重点关心的是是三个中断屏蔽寄存器:PRIMASK、FAULTMASK 和BASEPRI,这三个寄存器后面会详细的讲解。
4.1.3 优先级分组定义
当多个中断来临的时候处理器应该响应哪一个中断是由中断的优先级来决定的,高优先级的中断(优先级编号小)肯定是首先得到响应,而且高优先级的中断可以抢占低优先级的中断,这个就是中断嵌套。Cortex-M 处理器的有些中断是具有固定的优先级的,比如复位、NMI、HardFault,这些中断的优先级都是负数,优先级也是最高的。Cortex-M 处理器有三个固定优先级和256 个可编程的优先级,最多有128 个抢占等级,但是实际的优先级数量是由芯片厂商来决定的。但是,绝大多数的芯片都会精简设计的,以致实际上支持的优先级数会更少,如8 级、16 级、32 级等,比如STM32 就只有16 级优先级。在设计芯片的时候会裁掉表达优先级的几个低端有效位,以减少优先级数,所以不管用多少位来表达优先级,都是MSB 对齐的,如图4.1.3.1 就是使用三位来表达优先级。


在图4.1.3.1 中,Bit0~Bit4 没有实现,所以读它们总是返回零,写如它们的话则会忽略写入的值。因此,对于3 个位的情况,可是使用的优先级就是8 个:0X00(最高优先级)、0X20、0X40、0X60、0X80、0XA0、0XC0 和0XE0。注意,这个是芯片厂商来决定的!不是我们能决定的,比如STM32 就选择了4 位作为优先级!有读者可能就会问,优先级配置寄存器是8 位宽的,为什么却只有128 个抢占等级?8 位不应该是256 个抢占等级吗?为了使抢占机能变得更可控,Cortex-M 处理器还把256 个优先级按位分为高低两段:抢占优先级(分组优先级)和亚优先级(子优先级),NVIC 中有一个寄存器是“应用程序中断及复位控制寄存器(AIRCR)”,AIRCR 寄存器里面有个位段名为“优先级组”,如表4.1.3.1 所示:


表4.1.3.1 中PRIGROUP 就是优先级分组,它把优先级分为两个位段:MSB 所在的位段(左边的)对应抢占优先级,LSB 所在的位段(右边的)对应亚优先级,如表4.1.3.2 所示。



可以看出STM32 有5 个分组,但是一定要注意!STM32 中定义的分组0 对应的值是7!如果我们选择分组4,即NVIC_PriorityGroup_4 的话,那4 位优先级就都全是抢占优先级了,没有亚优先级,那么就有0~15 共16 个优先级。而移植FreeRTOS 的时候我们配置的就是组4,

如图4.1.3.2 所示:


图4.1.3.2 优先级分组配置

如果使用ALIENTEK 的基础例程的话默认配置的组2,所以在将基础例程中的外设驱动移植到FreeRTOS 下面的时候需要修改优先级配置。主要是FreeRTOS 的中断配置没有处理亚优先级这种情况,所以只能配置为组4,直接就16 个优先级,使用起来也简单!

4.1.4 优先级设置
每个外部中断都有一个对应的优先级寄存器,每个寄存器占8 位,因此最大宽度是8 位,但是最小为3 位。4 个相临的优先级寄存器拼成一个32 位寄存器。如前所述,根据优先级组的设置,优先级又可以分为高、低两个位段,分别抢占优先级和亚优先级。STM32 我们已经设置位组4,所以就只有抢占优先级了。优先级寄存器都可以按字节访问,当然也可以按半字/字来访问,有意义的优先级寄存器数目由芯片厂商来实现,如表4.1.4.1 和4.1.4.2 所示:


上面说了, 4 个相临的寄存器可以拼成一个32 位的寄存器, 因此地址0xE000_ED20~0xE000_ED23 这四个寄存器就可以拼一个地址为0xE000_ED20 的32 位寄存器。这一点很重要!因为FreeRTOS 在设置PendSV 和SysTick 的中断优先级的时候都是直接操作的地址0xE000_ED20。

4.1.5 用于中断屏蔽的特殊寄存器
在4.1.2 小节中说了我们在STM32 上移植FreeRTOS 的时候需要重点关注PRIMASK、FAULTMASK 和BASEPRI 这三个寄存器,本节就来学习一下这三个寄存器。
1、PRIMASK 和FAULTMASK 寄存器
在许多应用中,需要暂时屏蔽所有的中断一执行一些对时序要求严格的任务,这个时候就可以使用PRIMASK 寄存器,PRIMASK 用于禁止除NMI 和HardFalut 外的所有异常和中断,汇编编程的时候可以使用CPS(修改处理器状态)指令修改PRIMASK 寄存器的数值:


PRIMASK 寄存器还可以通过MRS 和MSR 指令访问,如下:


以及:


UCOS 中的临界区代码代码保护就是通过开关中断实现的(UCOSIII 也可以使用禁止任务调度的方法来实现临界区代码保护,这里不讨论这种情况),而开关中断就是直接操作PRIMASK寄存器的,所以在UCOS 中关闭中断的时候时关闭了除复位、NMI 和HardFault 以外的所有中断!
FAULTMASK 比PRIMASK 更狠,它可以连HardFault 都屏蔽掉,使用方法和PRIMASK 类似,FAULTMASK 会在退出时自动清零。汇编编程的时候可以利用CPS 指令修改FAULTMASK 的当前状态:


还可以利用MRS 和MSR 指令访问FAULTMASK 寄存器:


以及:


2、BASEPRI 寄存器
PRIMASK 和FAULTMASK 寄存器太粗暴了,直接关闭除复位、NMI 和HardFault 以外的其他所有中断,但是在有些场合需要对中断屏蔽进行更细腻的控制,比如只屏蔽优先级低于某一个阈值的中断。那么这个作为阈值的优先级值存储在哪里呢?在BASEPRI 寄存器中,不过如果向BASEPRI 写0 的话就会停止屏蔽中断。比如,我们要屏蔽优先级不高于0X60 的中断,则可以使用如下汇编编程:


如果需要取消BASEPRI 对中断的屏蔽,可以使用如下代码:


注意!FreeRTOS 的开关中断就是操作BASEPRI 寄存器来实现的!它可以关闭低于某个阈

值的中断,高于这个阈值的中断就不会被关闭!

4.2 FreeRTOS 中断配置宏
4.2.1 configPRIO_BITS
此宏用来设置MCU 使用几位优先级,STM32 使用的是4 位,因此此宏为4!
4.2.2 configLIBRARY_LOWEST_INTERRUPT_PRIORITY
此宏是用来设置最低优先级,前面说了,STM32 优先级使用了4 位,而且STM32 配置的使用组4,也就是4 位都是抢占优先级。因此优先级数就是16 个,最低优先级那就是15。所以此宏就是15,注意!不同的MCU 此值不同,具体是多少要看所使用的MCU 的架构,本教程只针对STM32 讲解!
4.2.3 configKERNEL_INTERRUPT_PRIORITY

此宏用来设置内核中断优先级,此宏定义如下:


宏configKERNEL_INTERRUPT_PRIORITY 为, 宏configLIBRARY_LOWEST_INTERRUPT_PRIORITY 左移8-configPRIO_BITS 位,也就是左移4位。为什么要左移4 位呢?前面我们说了,STM32 使用了4 位作为优先级,而这4 位是高4 位,因此要左移4 位才是真正的优先级。当然了也可以不用移位, 直接将宏configLIBRARY_LOWEST_INTERRUPT_PRIORITY 定义为0XF0!不过这样看起来不直观。宏configKERNEL_INTERRUPT_PRIORITY 用来设置PendSV 和滴答定时器的中断优先级,port.c 中有如下定义:


可以看出, portNVIC_PENDSV_PRI 和portNVIC_SYSTICK_PRI 都是使用了宏configKERNEL_INTERRUPT_PRIORITY , 为什么宏portNVIC_PENDSV_PRI 是宏configKERNEL_INTERRUPT_PRIORITY 左移16 位呢?宏portNVIC_SYSTICK_PRI 也同样是左移24 位。4.1.4 小节讲过了,PendSV 和SysTcik 的中断优先级设置是操作0xE000_ED20 地址的,这样一次写入的是个32 位的数据, SysTick 和PendSV 的优先级寄存器分别对应这个32位数据的最高8 位和次高8 位,不就是一个左移16 位,一个左移24 位了。PendSV 和SysTick 优先级是在哪里设置的呢?在函数xPortStartScheduler()中设置,此函数在文件port.c 中,函数如下:



上述代码中红色部分就是设置PendSV 和SysTick 优先级的, 它们是直接向地址portNVIC_SYSPRI2_REG 写入优先级数据,portNVIC_SYSPRI2_REG 是个宏,在文件port.c 中由定义,如下:


可以看到宏portNVIC_SYSPRI2_REG 就是地址0XE000ED20!同时也可以看出在FreeRTOS中PendSV 和SysTick 的中断优先级都是最低的!
4.2.4 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
此宏用来设置FreeRTOS 系统可管理的最大优先级,也就是我们在4.1.5 小节中讲解BASEPRI 寄存器说的那个阈值优先级,这个大家可以自由设置,这里我设置为了5。也就是高于5 的优先级(优先级数小于5)不归FreeRTOS 管理!
4.2.5 configMAX_SYSCALL_INTERRUPT_PRIORITY
此宏是configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 左移4 位而来的,原因和宏configKERNEL_INTERRUPT_PRIORITY 一样。此宏设置好以后,低于此优先级的中断可以安全的调用FreeRTOS 的API 函数,高于此优先级的中断FreeRTOS 是不能禁止的,中断服务函数也不能调用FreeRTOS 的API 函数!以STM32 为例,有16 个优先级,0 为最高优先级,15 为最低优先级,配置如下:
● configMAX_SYSCALL_INTERRUPT_PRIORITY==5
● configKERNEL_INTERRUPT_PRIORITY==15

结果如图4.2.5.1 所示:


由于高于configMAX_SYSCALL_INTERRUPT_PRIORITY 的优先级不会被FreeRTOS 内核屏蔽,因此那些对实时性要求严格的任务就可以使用这些优先级,比如四轴飞行器中的壁障检测。

4.3 FreeRTOS 开关中断
FreeRTOS 开关中断函数为portENABLE_INTERRUPTS ()和portDISABLE_INTERRUPTS(),这两个函数其实是宏定义,在portmacro.h 中有定义,如下:


可以看出开关中断实际上是通过函数vPortSetBASEPRI(0)和vPortRaiseBASEPRI()来实现的,这两个函数如下:


函数vPortSetBASEPRI()是向寄存器BASEPRI 写入一个值,此值作为参数ulBASEPRI 传递进来,portENABLE_INTERRUPTS()是开中断,它传递了个0 给vPortSetBASEPRI(),根据我们前面讲解BASEPRI 寄存器可知,结果就是开中断。函数vPortRaiseBASEPRI() 是向寄存器BASEPRI 写入宏configMAX_SYSCALL_INTERRUPT_PRIORITY , 那么优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断就会被屏蔽!
4.4 临界段代码
临界段代码也叫做临界区,是指那些必须完整运行,不能被打断的代码段,比如有的外设的初始化需要严格的时序,初始化过程中不能被打断。FreeRTOS 在进入临界段代码的时候需要关闭中断,当处理完临界段代码以后再打开中断。FreeRTOS 系统本身就有很多的临界段代码,这些代码都加了临界段代码保护,我们在写自己的用户程序的时候有些地方也需要添加临界段代码保护。
FreeRTOS 与临界段代码保护有关的函数有4 个: taskENTER_CRITICAL() 、taskEXIT_CRITICAL() 、taskENTER_CRITICAL_FROM_ISR() 和taskEXIT_CRITICAL_FROM_ISR(),这四个函数其实是宏定义,在task.h 文件中有定义。这四个函数的区别是前两个是任务级的临界段代码保护,后两个是中断级的临界段代码保护。
4.4.1 任务级临界段代码保护
taskENTER_CRITICAL()和taskEXIT_CRITICAL()是任务级的临界代码保护,一个是进入临界段,一个是退出临界段,这两个函数是成对使用的,这函数的定义如下:


而portENTER_CRITICAL()和portEXIT_CRITICAL()也是宏定义,在文件portmacro.h 中有定义,如下:


函数vPortEnterCritical()和vPortExitCritical()在文件port.c 中,函数如下:


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


(1)、进入临界区。
(2)、退出临界区。
(1)和(2)中间的代码就是临界区代码,注意临界区代码一定要精简!因为进入临界区会关闭中断,这样会导致优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断得不到及时的响应!

4.4.2 中断级临界段代码保护
函数taskENTER_CRITICAL_FROM_ISR()和taskEXIT_CRITICAL_FROM_ISR()中断级别临界段代码保护, 是用在中断服务程序中的, 而且这个中断的优先级一定要低于configMAX_SYSCALL_INTERRUPT_PRIORITY!原因前面已经说了。这两个函数在文件task.h中有如下定义:


接着找portSET_INTERRUPT_MASK_FROM_ISR() 和portCLEAR_INTERRUPT_MASK_FROM_ISR(),这两个在文件portmacro.h 中有如下定义:


vPortSetBASEPRI()前面已经讲解了,就是给BASEPRI 寄存器中写入一个值。函数ulPortRaiseBASEPRI()在文件portmacro.h 中定义的,如下:


(1)、先读出BASEPRI 的值,保存在ulReturn 中。
(2)、将configMAX_SYSCALL_INTERRUPT_PRIORITY 写入到寄存器BASEPRI 中。
(3)、返回ulReturn,退出临界区代码保护的时候要使用到此值!

中断级临界代码保护使用方法如下:



(1)、进入临界区。
(2)、退出临界区。
4.5 FreeRTOS 中断测试实验
4.5.1 实验程序设计
1、实验目的
上面我们讲了在FreeRTOS 中优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断会被屏蔽掉,高于的就不会,那么本节我们就写个简单的例程测试一下。使用两个定时器,一个优先级为4,一个优先级为5,两个定时器每隔1s 通过串口输出一串字符串。然后在某个任务中关闭中断一段时间,查看两个定时器的输出情况。。
2、实验设计
本实验设计了两个任务start_task()和interrupt_task(), 这两个任务的任务功能如下:start_task():创建另外一个任务。interrupt_task() : 中断测试任务, 任务中会调用FreeRTOS 的关中断函数portDISABLE_INTERRUPTS()来将中断关闭一段时间。
3、实验工程

FreeRTOS 实验4-1 FreeRTOS 中断测试实验。

点击下载代码

4、实验程序与分析

● 任务设置


● main()函数



● 任务函数


(1)、创建一个任务来执行开关中断的动作,任务函数为interrupt_task()。
(2)、当任务interrupt_task()运行5 次以后关闭中断。
(3) 、调用函数portDISABLE_INTERRUPTS() 关闭中断。优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断都会被关闭,高于的不会受任何影响。
(4)、调用函数delay_xms()延时5S,此函数是对delay_us()的简单封装,delay_xms()会用来模拟关闭中断一段时间,此函数不会引起任务调度!
(5)、调用函数portENABLE_INTERRUPTS()重新打开中断。

● 中断初始化及处理过程




(1)、设置定时器3 的抢占优先级为4,高于configMAX_SYSCALL_INTERRUPT_PRIORITY,因此在调用函数portDISABLE_INTERRUPTS()关闭中断的时候定时器3 是不会受影响的。
(2)、设置定时器5 的抢占优先级为5,等于configMAX_SYSCALL_INTERRUPT_PRIORITY,因此在调用函数portDISABLE_INTERRUPTS()关闭中断的时候定时器5 中断肯定会被关闭的。
(3)和(4)、定时器3 和定时5 串口输出信息。
4.5.2 实验程序运行结果

编译并下载代码到开发板中,打开串口调试助手查看数据输出,结果如图4.5.2.1 所示:


图4.5.2.1 串口调试助手

从图4.5.2.1 可以看出,一开始没有关闭中断,所以TIM3 和TIM5 都正常运行,红框所示部分。当任务interrupt_task()运行了5 次以后就关闭了中断,此时由于TIM5 的中断优先级为5,等于configMAX_SYSCALL_INTERRUPT_PRIORITY,因此TIM5 被关闭。但是,TIM3 的中断优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY,不会被关闭,所以TIM3 正常运行,绿框所示部分。中断关闭5S 以后就会调用函数portENABLE_INTERRUPTS()重新打开中断,重新打开中断以后TIM5 恢复运行,蓝框所示部分。







猜你喜欢

转载自blog.csdn.net/tichimi3375/article/details/80683411
今日推荐