深入学习FreeRTOS中断管理机制

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级别的中断处理:

  1. 调整返回地址:LR -= 4

  2. 将IRQ的LR和SPSR压入IRQ栈

  3. 切换到SVC模式:此时依然处于中断屏蔽状态

  4. 将寄存器上下文压栈:PUSH {R0-R4, R12}

  5. 增加中断嵌套层数:使用了R3存储ulPortInterruptNesting的地址,R1存储中断嵌套层数的原始值

  6. 读取GIC的CPU接口中的IAR,获取中断ID:表示CPU正在处理该中断

  7. 确保栈地址是4字节对齐:使用R2记录调整量

  8. 保存顶级中断响应函数的上下文:PUSH {R0-R4, LR},这里把R4也压栈是为了让栈满足32位地址对齐。

  9. 调用FreeRTOS_ApplicationIRQHandler:该函数主要完成下列操作

    {
        读取中断ID,调用GIC驱动中注册的对应的响应函数。
    }
    
  10. 恢复顶级中断响应函数的上下文:POP {R0-R4, LR}

  11. 恢复栈地址

  12. 屏蔽中断:CPSID i

  13. 通过EOIR通知GIC中断处理完成

  14. 恢复中断嵌套层数

  15. 如果中断嵌套层数不为0,那么直接跳转到不进行任务切换的中断退出,否则继续向下执行

  16. ulPortYieldRequired == pdTRUE,表明退出中断前需要进行一次任务切换:R1记录ulPortYieldRequired的地址。

不进行任务切换的中断退出:

  1. 恢复被中断的现场:POP {R0-R4, R12}
  2. 切换到IRQ模式
  3. 恢复LR和SPSR
  4. 从中断返回:MOVS PC, LR

进行任务切换的中断退出:任务切换部分可参看任务切换小节

  1. 清除ulPortYieldRequired标志
  2. 恢复被中断的现场:POP {R0-R4, R12}
  3. 切换到IRQ
  4. 恢复LR和SPSR
  5. 保存切出任务的上下文(portSAVE_CONTEXT
  6. 执行任务切换(vTaskSwitchContext()
  7. 恢复切入任务的上下文(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_PRIORITYconfigMAX_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()

    通常不建议在ISR中传输频繁抵达的数据,取而代之的是可以采用如下方法:

  • 使用DMA
  • 使用线程安全的buffer作为中转
  • 在ISR中进行数据处理,然后将处理后的数据通过队列传给task
  • 物联沃分享整理
    物联沃-IOTWORD物联网 » 深入学习FreeRTOS中断管理机制

    发表评论