在中断中使用队列
FreeRTOS的队列可以方便的实现中断传递数据到任务。但是如果数据到来的频率的非常高,导致中断触发频繁,则这种方式是非常不高效的。正如一些Demo所实现的,在UART中断中接收串口数据,然后放到队列中,然后任务从队列中读数据。使用这种方法只是起到了演示作用,并不具有实战价值。
更高效的方案例如:
- 使用DMA来实现外设(如UART)和内存缓冲的数据传输,这样数据传输时就没有软件导致的CPU负担,当一块数据传输完成时,使用任务通知来通知任务,使得任务唤醒然后开始处理数据。
- 如果DMA不可用,则可以使用内存缓冲区来缓存中断单次收到的数据,当一块数据传输完成时,使用任务通知来通知任务,使得任务唤醒然后开始处理数据。
- 对于只需简单处理的数据,则可以直接在中断中处理,然后将处理的结果通过队列或者任务通知技术发送给任务。
队列相关的API
为了方便演示,首先介绍一下队列相关的内核函数的使用方法。
//作用:在中断函数中向队列的头部放入一个数据,即插入数据到队头
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
void *pvItemToQueue
BaseType_t *pxHigherPriorityTaskWoken
);
//作用:向队列的尾部插入一个数据,即入队
//xQueueSendFromISR()函数的作用与之相同
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
void *pvItemToQueue
BaseType_t *pxHigherPriorityTaskWoken
);
参数xQueue:队列的句柄。
参数pvItemToQueue:发送到队列中的数据的指针,指针指向的数据将被拷贝到队列元素中。
参数pxHigherPriorityTaskWoken:任务切换标志的指针,详见上一篇的讲解。
返回:pdPASS,代表插入成功;errQUEUE_FULL,代表队列已满无法插入。
中断嵌套
数值优先级和逻辑优先级
假设UART1中断优先级寄存器是UP_REG,SPI1中断优先级寄存器是SP_REG。我们设置UP_REG=1,SP_REG=2,那么这里写入寄存器的的1和2就是数值优先级。
所谓逻辑优先级,就是两个或者多个中断在同时发生时,逻辑优先级高的优先响应。另一方面如果系统支持中断嵌套,则也会定义了这样的情形:当逻辑优先级低的中断A正在响应中,另一个逻辑优先级高B的触发时,B会暂时打断A的执行,进而使得系统嵌套执行B的中断函数,等B响应完后,再恢复A的执行。
有些单片机,其优先级寄存器中的数值优先级越大,则对应的逻辑优先级就越高,例如STC 51单片机。而我们常用的CM3/CM4内核,则与之相反,数值优先级越大,则对应的逻辑优先级就越低。
configMAX_SYSCALL_INTERRUPT_PRIORITY
这个宏定义在FreeRTOSConfig.h中,除此之外还有一个宏configMAX_API_CALL_INTERRUPT_PRIORITY与这个宏作用相同,只不过有些移植平台用的前者,有些用的是后者。对于CM3/CM4内核,使用的是configMAX_SYSCALL_INTERRUPT_PRIORITY。
这个宏定义一个逻辑优先级阈值,逻辑优先级低于等于这个阈值的中断将被FreeRTOS内核管理,且可以在这些中断的ISR中使用FreeRTOS的中断安全版本API(即xxxFromISR)。
- 对于CM3/CM4内核,configMAX_SYSCALL_INTERRUPT_PRIORITY 不能设置为 0。
- 逻辑优先级低于等于configMAX_SYSCALL_INTERRUPT_PRIORITY 对应的逻辑优先级的中断受内核管理,当代码运行进入到FreeRTOS实现的临界区(critical section)时,这些中断是被内核临时屏蔽的,也就是即便达到了中断触发条件也不会触发硬件中断,ISR不会立刻执行,只有退出临界区后才能执行ISR,会导致中断处理有延迟。在这些中断的ISR中是可以使用FreeRTOS的中断安全版本的API的。
- 逻辑优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY对应的逻辑优先级的中断,不受内核行为影响,不会被内核屏蔽其响应,表现为和原始硬件定义的行为一样。在这些中断的ISR中是不能使用FreeRTOS的任何API。一般对于时序要求严格的中断,需要配置为高于configMAX_SYSCALL_INTERRUPT_PRIORITY对应的优先级,例如电机控制信号等。
configKERNEL_INTERRUPT_PRIORITY
这个宏定义在FreeRTOSConfig.h中。这个宏用于定义内核中断(对于CM3/CM4就是SysTick中断和PendSV中断)使用的优先级,FreeRTOS要求这个宏的值设置为当前硬件系统的最低逻辑优先级对应的值。
对于CM3和CM4内核,这个宏的值直接被写入到SysTcik和PendSV的中断优先级寄存器中去了。由于CM3/CM4内核的优先级寄存器数值越大,其优先级越低,所以这个宏要设置为255。
- 如果你的目标移植平台没有使用configMAX_SYSCALL_INTERRUPT_PRIORITY宏而只使用了configKERNEL_INTERRUPT_PRIORITY,则只能在优先级为configKERNEL_INTERRUPT_PRIORITY对应的优先级的ISR中使用FreeRTOS的中断安全版API(xxxFromISR)。
- 对于CM3和CM4内核,一般做法是,将configKERNEL_INTERRUPT_PRIORITY设置为最低优先级对应的值,而把configMAX_SYSCALL_INTERRUPT_PRIORITY设置成高于configKERNEL_INTERRUPT_PRIORITY对应的优先级高的值。
configPRIO_BITS
这个宏定义在FreeRTOSConfig.h中。用于定义当前单片机的优先级使用了几位进行编码,例如对于STM32F103就是4。这个宏不影响FreeRTOS系统功能,不是必须定义的,如果定义了,则FreeRTOS可以使用运行时断言机制帮我们检查中断配置的正确性。但是单独只定义这个宏没意义,还需要启用FreeRTOS的断言机制。
为了能让FreeRTOS帮我们检查,开发者需要在FreeRTOSConfig.h定义configASSERT()的实现,configASSERT()的行为和标准C的assert()函数一样,是实现了运行时断言机制,当参数为真(true)时,检查通过,当参数为0(false)时,检查失败,应该及时停止程序运行并进行错误提示。建议在开发阶段启用断言机制,等开发稳定后,再屏蔽断言,毕竟断言是需要运行代码的,增加了代码量和CPU开销。
只要开发者定义了configASSERT,则内核会将configASSERT_DEFINED定义为1,否则内核会将configASSERT_DEFINED定义为0。只有当configASSERT_DEFINED为1的情况下,内核才会启用相关的断言检查机制。因此建议在开发阶段启用断言机制,即提供configASSERT的定义,等开发稳定后,再注释configASSERT的定义。
一种可能的configASSERT实现如下:
//断言不通过时,禁止中断,这样调度器停止工作,然后进入无限循环
#define configASSERT( x ) \
if( ( x ) == 0 ) { taskDISABLE_INTERRUPTS(); for( ;; ); }
configLIBRARY_LOWEST_INTERRUPT_PRIORITY和configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
实际上这个2宏不是FreeRTOS需要的,不是必须定义的,内核代码中根本没有用到这2个宏。之所以定义这2个宏,是为了方便定义前面说到的
configKERNEL_INTERRUPT_PRIORITY 和 configMAX_SYSCALL_INTERRUPT_PRIORITY 。
只要正确理解了configKERNEL_INTERRUPT_PRIORITY 和 configMAX_SYSCALL_INTERRUPT_PRIORITY ,就可以直接定义他们,而不需要使用这2个宏,引入更多的宏反而会使代码复杂难懂。
FreeRTOS中断嵌套例子分析
假设某个单片机,有7个不同数值优先级1~7,且逻辑优先级定义为数值越高,优先级也越高。
由于FreeRTOS规定要配置内核中断优先级为最低,所以configKERNEL_INTERRUPT_PRIORITY配置为1;同时把configMAX_SYSCALL_INTERRUPT_PRIORITY配置为3。如下图所示。
- 优先级1~3的中断受内核管理,当代码运行进入到FreeRTOS实现的临界区(critical section)时,这些中断是被内核临时屏蔽的,也就是即便达到了中断触发条件也不会触发硬件中断,ISR不会立刻执行,只有退出临界区后才能执行ISR,会导致中断处理有延迟。同时这些中断的ISR中是可以使用FreeRTOS的中断安全版本的API的。
- 优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY对应的优先级的,即4~7的优先级,不受内核调度器的影响,表现为和原始硬件行为一样。但要注意在这些中断的ISR中是不能使用FreeRTOS的任何API。一般对于时序要求严格的中断,需要配置为高于configMAX_SYSCALL_INTERRUPT_PRIORITY对应的优先级,例如电机控制信号等。
对于支持中断嵌套的硬件平台,则在FreeRTOS中的正确做法是,配置configKERNEL_INTERRUPT_PRIORITY为最低逻辑优先级对应的值,而把configMAX_SYSCALL_INTERRUPT_PRIORITY设置成逻辑优先级高于configKERNEL_INTERRUPT_PRIORITY对应的逻辑优先级的值。
CM3/CM4内核中断
中断优先级寄存器
CM3/CM4内核的数值优先级越大,则对应的逻辑优先级就越低。
CM3/CM4使用8bit大小的寄存器编码内核异常中断和处理外部中断的优先级,而单片机厂家为了简化设计,通常只使用这8bit的高n位,例如STM32F103就只使用了高4位。这样一来,每个中断优先级寄存器的高4位是可以正常读写的,而低4位写无效,读始终是0。
如下图所示,向一个STM32F103单片机的中断A的优先级寄存器中写入数据0101_1111,向中断B的优先级寄存器中写入数据0111_1111,由于只有高4位有效,因此中断A的数值优先级是5,中断B的数值优先级是7,A的优先级高于B。
请注意,在FreeRTOS中,对于CM3和CM4,configKERNEL_INTERRUPT_PRIORITY和configMAX_SYSCALL_INTERRUPT_PRIORITY配置的值就是直接写入到优先级寄存器中的值,例如将configKERNEL_INTERRUPT_PRIORITY配置为255,则对应的优先级数值为15;将configMAX_SYSCALL_INTERRUPT_PRIORITY配置为191,则对应的优先级数值为11。优先级寄存器中未实现的位可用1填充。
SysTick和PendSV的优先级是通过core_cm3.h中定义的SCB_Type寄存器组中的SHP寄存器组去设置的。具体来说就是地址为0xE000_ED20寄存器,其最高8位用于设置SysTick的优先级,次高8位是设置PendSV的优先级。
//代码来自CM3的port.c
#define portNVIC_PENDSV_PRI
( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL )
#define portNVIC_SYSTICK_PRI
( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )
/* Make PendSV and SysTick the lowest priority interrupts. */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; //设置PendSV中断优先级
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; //设置SysTick中断优先级
但是ST的标准库的NVIC_Init则需要的是优先级数值参数,库函数NVIC_Init内部会将数值移位到高4位,再写入优先级寄存器中。
//优先级分组4,高4位全部用于定义抢占优先级,而不使用子优先级
//设置USART1的优先级数值为13
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 13; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //不用
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
中断分组
为了更好的控制中断的响应顺序,CM3将中断的优先级划分为抢占(PreemptPriority)优先级和子优先级(SubPriority)。也就是将8bit宽度的优先级寄存器的高n位编码抢占(PreemptPriority)优先级,剩余的低位编码子优先级(SubPriority)。具体如何划分,就是通过SCB_Type寄存器组的AIRCR寄存器的[10:8]位段来设置,有8种情况可选,即8个组,如下图:
为了节省成本和降低芯片设计复杂度,芯片设计厂商会裁掉优先级寄存器(IP和SHP寄存器)的低几个位。根据约定,厂商需要在芯片头文件中(例如stm32f10x.h)定义__NVIC_PRIO_BITS这个宏,这个宏值表示用多少位来编码优先级,通常这个值是4。
不出所料,STM32F103简化了优先级寄存器,裁调了最低四位,只使用高四位来编码优先级,因此只有AIRCR[10:8] = 3~7才是有相互区分意义的。如下图所示。
那么对于STM32F103只有4个组可以选择, 如下,其中NVIC_PriorityGroup_4是FreeRTOS推荐的组别设置,因为FreeRTOS没有实现子优先级。也就是FreeRTOS推荐只使用抢占优先级,忽略子优先级的影响。
#define NVIC_PriorityGroup_0 ((uint32_t)0x700) /* 抢占优先级0 bits 子优先级 4 bits */
#define NVIC_PriorityGroup_1 ((uint32_t)0x600) /* 抢占优先级1 bits 子优先级 3 bits */
#define NVIC_PriorityGroup_2 ((uint32_t)0x500) /* 抢占优先级2 bits 子优先级 2 bits */
#define NVIC_PriorityGroup_3 ((uint32_t)0x400) /* 抢占优先级3 bits 子优先级 1 bits */
#define NVIC_PriorityGroup_4 ((uint32_t)0x300) /* 抢占优先级4 bits 子优先级 0 bits */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //设置优先级分组
那么抢占优先级和子优先级的区别是什么呢?
(1)抢占优先级的不同可以形成中断嵌套,抢占优先级高的可以打断抢占优先级低的,形成中断嵌套。相同的抢占优先级不能相互打断,即如果两个抢占优先级相同的中断前后发生的话,后来的中断不能打断前一个中断。
(2)如果多个抢占优先级相同的中断同时发生的话,则看子优先级,子优先级高的先响应。
(3)如果抢占级别和子优先级别都一样时,按照中断的地址来响应,地址低的先响应。
关于优先级分组的设置,其实在绝大多数情况下,优先级的分组都要预先经过计算论证,并且在开机初始化时一次性地设置好,以后就再也不动它了。——《CM3权威指南》
BASEPRI 寄存器
对于有这个寄存器的CM3和CM4,FreeRTOS内核使用BASEPRI 寄存器来实现临界区。这允许RTOS内核只屏蔽中断的一个子集,因此提供了一个灵活的中断嵌套模型。
通过BASEPRI 中设置一个非0的优先级阈值,这样逻辑优先级低于等于这个阈值的中断都会全部被临时屏蔽调,而逻辑优先级高于这个阈值的中断不受影响。向BASEPRI写入0时就会停止屏蔽中断(所以对于CM3/CM4,configMAX_SYSCALL_INTERRUPT_PRIORITY的值不能是0,否则进入临界区无法实现)。
说到这里想必你已经知道了前面configMAX_SYSCALL_INTERRUPT_PRIORITY宏的作用了,是的,在FreeRTOS中,这个宏正是写入到BASEPRI 寄存器的值,以此来实现临界区。如下内核源码所示:
//进入临界区
void vPortEnterCritical( void )
{
//#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
//portDISABLE_INTERRUPTS()等价于vPortRaiseBASEPRI()
portDISABLE_INTERRUPTS();
uxCriticalNesting++;
if( uxCriticalNesting == 1 )
{
configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
}
}
//将basepri寄存器设置为configMAX_SYSCALL_INTERRUPT_PRIORITY配置的值
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
}
}
//退出临界区
//PortSetBASEPRI( 0 )定义在portmacro.h中,作用就是设置BASEPRI为0,也就是取消屏蔽。
void vPortExitCritical( void )
{
configASSERT( uxCriticalNesting );
uxCriticalNesting--;
if( uxCriticalNesting == 0 )
{
//#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
//等价于vPortSetBASEPRI( 0 )
portENABLE_INTERRUPTS();
}
}
我们可以把这一些需要受内核管理的中断,优先级设置的低一些,把另一些不允许关闭的中断优先级设置的高一些,然后通过BASEPRI这个寄存器把优先级低于某个值的(优先级寄存器的值大于等于某个值的)中断一口气全屏蔽掉,这样我们在进入临界段前,只保存BASEPRI的值就好了。FreeRTOS中也是这么做的。
作为对比,uC/OS的临界区是通过PRIMASK寄存器实现的,这个寄存器比BASEPRI寄存器更强势,他可以屏蔽系统中除了系统异常NMI和HardFault外的其余所有中断,keil开发环境下的__enable_irq();__disable_irq();就是操作的这个寄存器 。相比较来说,FreeRTOS的实现更加灵活合理,为开发者提供了更多的选择余地,因为有些应用场合下,例如像电机控制信号等时序严格的中断是不能被屏蔽的。
题外话:
- CM0没有BASEPRI寄存器。对于CM0这种没有BASEPRI寄存器的,FreeRTOS才是通过PRIMASK寄存器实现临界区的。
- FreeRTOS不推荐使用子优先级,是因为只有抢占优先级位域才参与和BASEPRI的比较,子优先级和保留位不参与和BASEPRI的比较。