1. 简介
中断管理是一个操作系统中最核心的功能之一。在FreeRTOS中,中断总是可以打断task(尽管是最高优先级的task),而task永远不可能打断中断ISR(interrupt service routine)。本文先介绍了FreeRTOS中的中断管理,然后介绍了中断处理函数中如何使用FreeRTOS的API,最后介绍了中断处理函数与任务之间的同步方法——信号量。
2. 中断处理流程
2.1. 中断系统的初始化
在FreeRTOS中,中断向量表的定义在Port层完成,在GCC ARMv7中,其基本形式如下:
.extern FreeRTOS_IRQ_Handler
.extern FreeRTOS_SWI_Handler
.section .vectors
_vector_table:
_freerots_vector_table:
B _boot
B Undefined
LDR PC, _swi
B PrefetchAbortHandler
B DataAbortHandler
NOP
LDR PC, _irq
B FIQHandler
_irq: .word FreeRTOS_IRQ_Handler
_swi: .word FreeRTOS_SWI_Handler
其中,.vectors
段在链接脚本中与.text
段关联,并且被放置在.text
的首位。
在portmacro.h
中定义了extern void vPortInstallFreeRTOSVectorTable( void )
函数用于装载该中断向量(由Port层实现)。在ARMv7中并不一定需要装载,因为可以默认从0x00000000访问。对于ARMv8,则需要在开启调度器的时候装载中断向量表的地址。
此外,中断控制器的初始化也需要在Port层完成。而中断所使用的栈则是在_boot
中完成。
2.2. 中断处理
这里我们不讨论硬件级别的行为,只考虑OS需要完成的软件行为。并且,我们以ARMv7为例,中断仅限于IRQ异常的处理。
当中断发生时,CPU会跳转到异常向量表中IRQ的异常向量处,执行一个跳转指令跳转到FreeRTOS_IRQ_Handler
。
在FreeRTOS_IRQ_Handler
中,进行OS级别的中断处理:
-
调整返回地址:LR -= 4
-
将IRQ的LR和SPSR压入IRQ栈
-
切换到SVC模式:此时依然处于中断屏蔽状态
-
将寄存器上下文压栈:
PUSH {R0-R4, R12}
-
增加中断嵌套层数:使用了R3存储
ulPortInterruptNesting
的地址,R1存储中断嵌套层数的原始值 -
读取GIC的CPU接口中的IAR,获取中断ID:表示CPU正在处理该中断
-
确保栈地址是4字节对齐:使用R2记录调整量
-
保存顶级中断响应函数的上下文:
PUSH {R0-R4, LR}
,这里把R4也压栈是为了让栈满足32位地址对齐。 -
调用
FreeRTOS_ApplicationIRQHandler
:该函数主要完成下列操作{ 读取中断ID,调用GIC驱动中注册的对应的响应函数。 }
-
恢复顶级中断响应函数的上下文:
POP {R0-R4, LR}
-
恢复栈地址
-
屏蔽中断:
CPSID i
-
通过EOIR通知GIC中断处理完成
-
恢复中断嵌套层数
-
如果中断嵌套层数不为0,那么直接跳转到不进行任务切换的中断退出,否则继续向下执行
-
若
ulPortYieldRequired == pdTRUE
,表明退出中断前需要进行一次任务切换:R1记录ulPortYieldRequired
的地址。
不进行任务切换的中断退出:
- 恢复被中断的现场:
POP {R0-R4, R12}
- 切换到IRQ模式
- 恢复LR和SPSR
- 从中断返回:
MOVS PC, LR
进行任务切换的中断退出:任务切换部分可参看任务切换小节
- 清除
ulPortYieldRequired
标志 - 恢复被中断的现场:
POP {R0-R4, R12}
- 切换到IRQ
- 恢复LR和SPSR
- 保存切出任务的上下文(
portSAVE_CONTEXT
) - 执行任务切换(
vTaskSwitchContext()
) - 恢复切入任务的上下文(
portRESTORE_CONTEXT
)
2.3. 对浮点的支持
在ARM CA9中,支持NEON和VFP硬件浮点。如果开启了NEON和VFP硬件支持,GCC通常会使用向量化编译优化memcpy()
和memset()
等类似函数的实现(而FreeRTOS的队列的实现基于memcpy()
)。这便意味着对其调用则可能破坏对应的浮点寄存器,对于中断处理函数而言,这通常是不符合预期的。因为默认情况下,中断处理过程中不会保护浮点上下文,因此如果ISR中使用了这类函数,那么它可能会影响在任务中执行的浮点运算的结果。
为了解决该问题,FreeRTOS提供了FPU-Safe版本的中断处理实现,即在中断处理中增加浮点上下文的保护。这种实现的缺点很明显,就是会降低中断处理的性能(因为浮点寄存器的保护和增加内存访问的时间)。
此外,还有另外一种解决方式,则是在临界区内使用硬件浮点运算。那就不存在中断打断的问题了。
2.4. 中断嵌套
对于支持中断嵌套的Port,需要在FreeRTOSConfig.h
中定义如下宏定义;
宏 | 描述 |
---|---|
configMAX_SYSCALL_INTERRUPT_PRIORITY 或configMAX_API_CALL_INTERRUPT_PRIORITY
|
定义了可调用FreeRTOS的API的最高中断优先级,其中configMAX_SYSCALL_INTERRUPT_PRIORITY 用于旧版本的FreeRTOS Port中 |
configKERNEL_INTERRUPT_PRIORITY |
Tick中断的优先级,必须设置为最低的中断优先级,这个宏在不同Port中名字不一样 |
只有优先级大于configMAX_SYSCALL_INTERRUPT_PRIORITY
(数值上可能是低于它)的中断,才允许中断当前的中断处理。并且,要求这些中断响应函数不能调用FreeRTOS的API。
3. 在ISR中使用FreeRTOS API
3.1. 中断安全API
在FreeRTOS中,某些系统调用提供了任务上下文和中断上下文的调用版本。对于带有FromISR后缀的API则是中断上下文的版本。
引入这种两种上下文分离的API的优势:使得任务上下文和中断上下文的调用都更加高效,因为
- 不需要判断调用者所处上下文。
- API的参数可能不是共享的,即有的参数对于任务上下文来说是无用的,有的参数则对于中断上下文而言是无用的。
- 每个Port都需要实现中断上下文的判断。
使用分离的API的劣势通常是在使用三方代码时,有的时候必须同时在任务上下文和中断上下文中调用FreeRTOS的API,解决方案通常是:
- 可以将中断处理推迟到任务中处理。
- 实现调用上下文的判断,根据所处上下文来调用合适的API。
3.2. xHigherPriorityTaskWoken参数
在中断处理中,如果调用了某些API使得比被中断的任务更高优先级的任务被唤醒了,那么在退出中断之前,可以触发一次调度。而是否具有更高优先级的任务被唤醒的标志就是xHigherPriorityTaskWoken
。它通常用于在FromISR后缀的函数中作为输出参数。如果xHigherPriorityTaskWoken == pdTRUE
,表示有比被中断的task更高优先级的task被唤醒,需要进行上下文切换。当然,程序员也可以选择不进行切换。
为什么不能直接在API里面触发调度呢,有以下一些原因:
- 避免不必要的任务切换:例如Uart Shell,不必要为每一个字符都进行一次调度;
- 控制程序的执行:中断总是随机的,但有时会有任务不能被中断切换的需求(此时需要挂起调度器);
- 更高的可移植性:可以在所有平台中使用;
当然如果不想使用这个参数,被唤醒的任务也会在下一次调度时进行被选中执行。
3.3. portYIELD_FROM_ISR()和portEND_SWITCHING_ISR()宏
两者完全相同,用于发出一个上下文切换请求。是taskYIELD()
的中断安全版本。
3.4. portASSERT_IF_INTERRUPT_PRIORITY_INVALID()宏
对于某些支持中断嵌套的FreeRTOS port,FreeRTOS引入了Maximum System Call (或Maximum API Call)中断优先级的概念。在进入临界区时,所有不大于该优先级的中断都会被屏蔽;反之,所有高于该优先级的中断不会被屏蔽,但在他们的中断处理函数中,不能调用FreeRTOS的API。
portASSERT_IF_INTERRUPT_PRIORITY_INVALID()
宏就是为了检测当前中断优先级是否有效。
4. 延迟的中断处理
这部分的设计思路与Linux中的中断处理的上、下半部类似。这是为了尽可能减少ISR的处理逻辑,将耗时的任务放在任务上下文进行处理。
基本思路是通过FreeRTOS的同步机制,在任务上下文等待某些指定事件,这些事件由中断产生;当事件发生时,激活任务执行对应的处理。
在FreeRTOS中,同步机制包括队列、事件组、信号量和IPC等。这里只介绍信号量。
4.1. 集中式延迟中断处理
在软件定时器管理文章中的PendFunction章节提到了延迟函数调用,因此,可以使用这个功能来实现集中式的延迟中断处理。
需要注意的是,在中断上下文中,应该使用ISR安全的APIxTimerPendFunctionCallFromISR()
。
使用RTOS Timer Task的优势:
- 更低的资源使用
- 简单
劣势:
- 灵活性有限:优先级是固定的
- 更低的确定性:因为会先处理已在队列中的请求。
5. 信号量
信号量是用于任务和任务之间,任务与中断处理之间的同步机制。它不能传递消息,只实现同步功能。如果需要传递消息,可通过队列实现。
但实际上,在FreeRTOS中,信号量是基于队列实现的,所以总的来说,他们的性能相差不大。
信号量可分为二值信号量和计数信号量,实质上,二值信号量是一种特殊的计数信号量。他们的区别在于创建的流程不同,其他接口完全一致。
下面则分别来看看它们的具体设计和实现。
由于FreeRTOSv9中,队列和信号量的实现大部分耦合在同一个函数中了,而FreeRTOSv10将这部分功能解耦,更易于理解和维护,因此这里基于FreeRTOSv10的实现进行介绍。
5.1. 二值信号量
5.1.1. 创建二值信号量
函数原型:configSUPPORT_DYNAMIC_ALLOCATION == 1
#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, senSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
创建一个二值信号量,并返回该信号量的句柄。此时返回的信号量数值为0。详细的实现过程可以参看队列管理文章中的创建队列小节。
FreeRTOS还提供了静态创建的版本,这里不再赘述。
5.1.2. 获取信号量
函数原型:
#define xSemaphoreTake( xSemaphore, xBlockTime ) xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )
从xSemphore
获取一个信号量,当且仅当该信号量可用时。可设置等待的时间xBlockTime
。如果在指定时间内获取成功,则返回pdTRUE
;否则返回pdFALSE
。
这里只关注信号量的部分。其中与互斥锁相关的实现将在后续介绍共享资源管理的文章中给出。获取信号量的具体实现如下:
5.1.3. 归还信号量
函数原型:
#define xSemaphoreGive( xSemaphore ) xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )
归还信号量xSemaphore
。如果成功归还,则返回pdTRUE
;如果该信号量之前从未被获取,则返回pdFALSE
。
具体的实现可参看队列管理文章中的入队操作。
需要注意的是,该接口只用于任务上下文。对于中断上下文,需要使用以下接口:
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken ) xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )
其实现与xQueueGenericSendFromISR()
类似,只是其消息长度为0,下面看见其具体的实现:
{
初始化`pxQueue = ( Queue_t * ) xQueue`。
参数校验:Assert `pxQueue != NULL`,`pxQueue->uxItemSize == 0`,`!( ( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ) && ( pxQueue->pxMutexHolder != NULL ) )`。
校验中断优先级(`portASSERT_IF_INTERRUPT_PRIORITY_INVALID()`)。
进入临界区,并保存当前的中断优先级屏蔽状态(`uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR()`)。
获取队列当前的消息数量(`uxMessagesWaiting = pxQueue->uxMessagesWaiting`)。
如果队列的消息未满(`uxMessagesWaiting < pxQueue->uxLength`):
{
获取当前的队列计数写锁(`cTxLock = pxQueue->cTxLock`)。
由于这里不需要考虑互斥锁的情况,所以不需要考虑优先级继承的情况,只是单存的增加队列的消息计数(`pxQueue->uxMessagesWaiting = uxMessagesWaiting + 1`)。
如果队列处于计数写锁为加锁状态(`cTxLock == queueUNLOCKED`):
{
(仅开启USE_QUEUE_SETS):
{
如果队列处于某个队列组中(`pxQueue->pxQueueSetContainer != NULL`):
{
将消息通知到队列组,如果高优先级的任务被唤醒(`prvNotifyQueueSetContainer( pxQueue, queueSEND_TO_BACK ) != pdFALSE`),设置更高优先级任务唤醒标志(`*pxHigherPriorityTaskWoken = pdTRUE`)。
}
否则,即队列不在任何队列组中:
{
如果有任务等待该队列的消息(`listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE`):
{
将任务从事件任务队列中唤醒,且唤醒了更高优先级的任务(`xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) )`),设置更高优先级任务唤醒标志(`*pxHigherPriorityTaskWoken = pdTRUE`)。
}
}
}
}
否则,即队列处于加锁状态:
{
将队列写计数锁增1(`pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 )`)。
}
设置返回值`xReturn = pdPASS`。
}
否则,即队列消息已满:
{
设置返回值`xReturn = errQUEUE_NULL`。
}
退出临界区,并恢复中断优先级屏蔽状态(`portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus )`)。
返回`xReturn`。
}
5.1.4. 获取信号量数值
函数原型:
#define uxSemaphoreGetCount( xSemaphore ) uxQueueMessagesWaiting( ( QueueHandle_t ) ( xSemaphore ) )
返回信号量当前的可用数量。
详细实现可参看队列管理文章中的查询队列状态小节。
5.1.5. 删除信号量
函数原型:
#define vSemaphoreDelete( xSemaphore ) vQueueDelete( ( QueueHandle_t ) )
具体实现可参看队列管理文章中的删除队列小节。
5.2. 计数信号量
计数信号量的主要作用:
- 计算事件的发生次数,每发生一次事件则计数增1,而获取事件时减1。使用时,通常初始化为0。
- 资源管理:计数值表示资源的数量,当资源被分配时计数减1,归还时增1。使用时,通常初始化为资源的数量。
5.2.1. 创建计数信号量
函数原型:需要开启configUSE_COUNTING_SEMAPHORES = 1
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )
该函数实际上调用的是xQueuCreateCountingSemaphore()
接口,其中uxMaxCount
表示计数信号量的最大值,uxInitialCount
是初始值。如果创建成功,则返回信号量的句柄。
下面来看看其具体实现:
{
参数校验:Assert `uxMaxCount != 0`, `uxInitialCount <= uxMaxCount`。
创建队列(`xHandle = xQueueGenericCreate( uxMaxCount, queueSEMAPHORE_QUEUE_ITEM_LENGTH, queuQUEUE_TYPE_COUNTING_SEMAPHORE )`)。
如果创建成功(`xHandle != NULL`):
{
初始化队列消息数量(`( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount`)。
}
返回`xHandle`。
}
可以看到,最终还是通过xQueueGenericCreate()
创建了一个队列。
5.3. 信号量的实现细节
5.3.1. 信号量的接口
5.3.1.1. 信号量的句柄
句柄定义:
typedef QueueHandle_t SemaphoreHandle_t;
可以看到,信号量的句柄实际上就是队列的句柄。
5.3.2. 信号量的内涵
5.3.2.1. 信号量的私有宏
定义了信号量的消息长度和队列长度:
#define semBINARY_SEMAPHORE_QUEUE_LENGTH ( ( uint8_t ) 1U )
#define semSEMAPHORE_QUEUE_ITEM_LENGTH ( ( uint8_t ) 0U )
#define semGIVE_BLOCK_TIME ( ( TickType_t ) 0U )
6. 在ISR中使用队列
在队列管理文章中提到,中断上下文中也可以使用队列功能,但需要注意的是,需要调用ISR安全的接口,例如xQueueSendToFrontFromISR()
、xQueueSendToBackFromISR()
。文章来源:https://www.toymoban.com/news/detail-494848.html
通常不建议在ISR中传输频繁抵达的数据,取而代之的是可以采用如下方法:文章来源地址https://www.toymoban.com/news/detail-494848.html
- 使用DMA
- 使用线程安全的buffer作为中转
- 在ISR中进行数据处理,然后将处理后的数据通过队列传给task
到了这里,关于FreeRTOS学习-中断管理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!