FreeRTOS基础六:中断管理1

嵌入式实时系统需要对外界的某个事件做出及时的响应动作。例如串口外设收到了一帧数据后,需要通知数据解析任务,同时还要将数据帧传递给解析任务,完成数据的处理。设计出一种好的策略来完成这个过程时需要考虑以下几个问题:

  • 如何检测事件?中断是主要的事件检测手段,有时候也可以使用轮询法。
  • 当中断发生后,应该将多少处理工作放在中断服务函数(ISR)中,将另外的多少处理工作放在中断函数外(例如main函数)?通常的经验是,在中断服务函数中尽可能做最少的工作,保证中断服务函数能迅速执行完成。
  • 当中断检测到事件发生时,中断函数如何与main函数通信?如何组织代码的结构?

FreeRTOS提供的一些机制能够很好的解决以上问题。

首先我们需要明白一点,FreeRTOS的任务优先级和单片机的中断优先级是不同的,是两个不同层次的概念:

  • 任务以及任务优先级是FreeRTOS抽象出的软件层次的概念,与FreeRTOS应用程序所运行的硬件系统无关。任务优先级的意义和行为由FreeRTOS内核定义和控制。
  • 中断是单片机硬件提供的一种机制,通过对单片机的中断相关寄存器进行设置,来控制中断的响应和处理过程。当中断发生时,中断服务函数(ISR)会抢占任何优先级的运行着的任务,无论这个中断的优先级多么低。但是任务在任何情况下都不能打断中断函数的运行。

在中断函数中使用FreeRTOS的API

在FreeRTOS中,许多内核函数都有两个版本,一个是普通版本,另一个是带"FromISR"后缀的版本,也可以叫做“中断安全版本”。在中断函数(ISR)中只能使用带"FromISR"后缀的版本,不能使用普通版本。普通版本可以使用在main函数或者任务中。这样设计的目的是为了让内核代码执行起来更加高效。但是这种设计也会遇到挑战。

假设你想要写一个硬件驱动库,库里某个函数在设计时使用了FreeRTOS的API,那么你就需要编写两个版本,一个普通版本用于在任务中使用,另一个中断安全版本用于在ISR中使用。这样就会很麻烦。解决办法有如下几种:

  • 推移中断处理工作到一个任务中(详见后文介绍的中断推移处理法),这样就只会使用到普通版本的API,而不需要使用中断安全版本。
  • 如果移植的目标平台支持中断嵌套,则可以总是使用带"FromISR"后缀的版本。即带"FromISR"后缀的版本既可以在ISR中调用,也可以在任务中调用。但是普通版本一定不能在ISR中调用,在ISR中只能使用中断安全版本。(注:个人觉得任何情况下都不要使用这个特性)

在任务环境下,调用一个内核API函数时,可能会导致任务切换。例如一个低优先级的任务执行了xQueueSendToBack()函数,成功向队列中入队了一个元素,另一个优先级更高的,因为调用xQueueReceive()而从阻塞态变为就绪态。由于优先级相比较高,则它会立刻进入到运行态。如果在FreeRTOSConfig.h中将configUSE_PREEMPTION设置为1,即使用抢占式调度算法,那么这种情况会在内核API函数中会自动切换到高优先级的任务——也就是在内核API函数退出之前自动完成。

然而在中断环境下(inside an ISR)表现却不一样。具体说,一个任务A正在运行的时候,被一个中断打断了,在中断函数中执行了一个xxxFromISR内核函数,使得一个比任务A更高优先级的任务B被唤醒了,此时应该将A变为就绪态,将更高优先级的B变为运行态,则中断退出后,让任务B开始运行而不是A。但是FreeRTOS不会在中断函数中自动做任务切换,这是FreeRTOS的在衡量多种利弊的情况下的设计决策。虽然FreeRTOS不自动做任务切换,但是它会告诉开发者是否需要做任务切换,这样开发者可以向内核申请任务切换。

A switch to a higher priority task will not occur automatically inside an interrupt. Instead, a
variable is set to inform the application writer that a context switch should be performed.
Interrupt safe API functions (those that end in “FromISR”) have a pointer parameter called
pxHigherPriorityTaskWoken that is used for this purpose.

The FromISR variants API function require an extra parameter, BaseType_t*pxHigherPriorityTaskWoken , which will indicate whether or not a higher-priority task needs to be switched into context immediately following the interrupt.

在所有的带FromISR的内核函数都有一个pxHigherPriorityTaskWoken指针参数。如果在中断函数中,因为执行带FromISR的内核函数而唤醒了一个更高优先级的任务,需要做任务切换动作,则这个带FromISR的内核函数在内部会将参数pxHigherPriorityTaskWoken指向的值设置为pdTRUE,否则不改变这个参数指向的值。这个通过指针返回的参数用于告知开发者,是否需要向内核申请一次任务切换。所以为了检测*pxHigherPriorityTaskWoken是否为pdTRUE,应该在ISR中先将它初始化为pdFALSE。

下面是一个例子: 在中断函数中,xHigherPriorityTaskWoken被初始化为pdFALSE,这个参数传递给xQueueSendFromISR()函数,如果此函数内部执行的时候将一个更高优先级的任务唤醒了,则会将xHigherPriorityTaskWoken设置为pdTRUE。开发者可以申请一次任务切换,具体做法是:调用portYIELD_FROM_ISR()函数,如果参数xHigherPriorityTaskWoken为pdTRUE,则调度器会将这个更高优先级的任务切换为运行态,否则不会发生任务切换

void USART2_IRQHandler( void )
{
	//初始化为pdFALSE
	portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;

	//如果发生了RXNE中断
	if( USART2->ISR & USART_ISR_RXNE_Msk)
	{
        //获取到收到的串口数据
		uint8_t tempVal = (uint8_t) USART2->RDR;   
		
	    //将收到的串口数据放到队列uart2_BytesReceived中
		xQueueSendFromISR(uart2_BytesReceived, &tempVal,&xHigherPriorityTaskWoken);
	}
	portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

在FreeRTOS中几乎所有的中断函数都应该使用这种代码模式。在后文介绍的基于推移中断处理法设计的中断函数中,也会手动调用portYIELD_FROM_ISR()函数,因为ISR中唤醒的是中断推移处理任务,这样可以保证中断处理的实时性。

如果开发者忽略xHigherPriorityTaskWoken,选择不在ISR中手动执行任务切换,则更高优先级的任务会保持就绪态,直到下一次调度器运行或者下一个tick中断来临的时候,才会将这个高优先级的任务切换为运行态,这将会影响到中断处理的实时性。

在中断函数中手动申请一次执行任务上下文切换的函数为portYIELD_FROM_ISR() 或者 portEND_SWITCHING_ISR(),他们都以xHigherPriorityTaskWoken为参数调用。二者的作用的相同的,一般使用portYIELD_FROM_ISR()即可。

如果参数xHigherPriorityTaskWoken为pdFALSE,则portYIELD_FROM_ISR()不起任何作用,如果参数为pdTRUE,则内核将会执行任务上下文切换,即将进入到中断前的那个任务变为就绪态,将新的更高优先级的任务变为运行态,中断函数退出后,CPU总是把把处理器时间给到运行态的任务。

几乎所有的移植目标平台都允许在中断函数ISR中的任何地方调用portYIELD_FROM_ISR(),只有一些低端的平台只允许在中断函数ISR的最后调用。一般习惯都是在中断函数的最后调用portYIELD_FROM_ISR()函数

推移中断处理法

有经验的嵌入式开发者都会有一个良好的习惯——尽可能将中断函数的处理代码写的简短,在中断函数中只执行必要的代码,保证中断函数能快速执行完毕。这样做的原因有如下几点:

  • 当中断发生时,中断服务函数(ISR)会抢占任何优先级的运行着的任务,无论这个中断的优先级多么低。但是任务在任何情况下都不能打断中断函数的运行。如果中断服务函数执行非常耗时,则任务的执行将受到不可忽视的影响。
  • 在任务和中断函数中同时访问一些资源,如全局数据,硬件外设,内存,可能发生资源访问的竞争现象,导致执行结果变得复杂而难以预测,甚至出现资源访问异常。
  • 有些移植环境支持中断嵌套,但是中断嵌套会增加复杂性和不可预测性。保持中断函数简短简洁,可以降低中断嵌套发生的概率。

具体来说,当中断发生时,中断函数只需要记录中断发生的原因和关键数据,然后清除中断标志,并将剩余需要处理的工作推迟转移到一个任务中,使得中断函数可以快速退出,这就是所谓的推移中断处理法(Deferred Interrupt Processing)

An interrupt service routine must record the cause of the interrupt, and clear the interrupt.
Any other processing necessitated by the interrupt can often be performed in a task, allowing the interrupt service routine to exit as quickly as is practical. This is called ‘deferred interrupt processing’, because the processing necessitated by the interrupt is ‘deferred’ from the ISR to a task.

下图是一个例子,Task1是一个优先级较低的普通任务,Task2是处理中断推移工作的任务,它的优先级相对高一些,一开始处于阻塞态。在执行到t2时刻时,发生了中断,于是中断函数ISR打断了Task1,中断函数处理完成后,用信号量等机制(后文介绍)通知并导致Task2转变为就绪态,由于Task2优先级比Task1高,所以可以立马运行,来处理中断推迟工作,然后再次进入到阻塞状态,等待下一次中断发生。 这里的Task2就是中推推移处理任务。可以发现,从中断函数中退出后,就立马开始执行Task2中推推移处理任务了,这种效果使得中断处理连贯且及时。

FreeRTOS基础六:中断管理1_第1张图片

二进制信号量同步机制

二进制信号量就是一个长度为1的队列,其元素个数只能是0或者1,因此叫做二进制信号量。因此在使用时我们只关心二进制信号量队列的长度,而不关心它的队列元素数据。二进制信号量可以用于两个任务的同步,也可以用于同步中断函数和它的中断推移处理任务。

二进制信号量本质是队列,为了优化内存占用。二进制信号量的队列元素大小为0,所以增加计数值并不会增加队列元素的内存占用。元素个数是由队列结构体中的uxMessagesWaiting成员来计数的。

使用二进制信号量机制可以非常高效的将中断函数和它的中断推移处理任务同步起来。

忠告:对于在中断函数中通知一个任务从阻塞变为就绪态,使用任务直接通知机制(direct to task notification)比使用二进制信号量更加高效。优先使用任务通知机制代替二进制信号量,在不能使用任务通知的情况下才考虑使用二进制信号量。

如果一个中断的处理需要非常及时,那么可以将它的中断推移处理任务的优先级设置为最高,这样就能保证中断推移处理任务总会抢占其他的任务,同时,在中断函数中,调用portYIELD_FROM_ISR(),使得中断函数退出后直接进入到更高优先级的中断推移处理任务,中间切换没有时间间隔,更加保证了中断处理的连贯性和及时性。

当用于同步中断函数和中断推移处理任务时,中断推移处理任务调用xSemaphoreTake()来尝试从队列中获取信号量,如果队列中没有信号量,则任务因为调用这个函数而进入阻塞态。当中断触发时,中断函数在适当的时候调用xSemaphoreGiveFromISR()向队列中放入一个信号量,二进制信号量队列满,使得中断推移处理任务进入到就绪态,然后进入到运行态。任务运行后,从队列中取出信号量,此时队列为空。任务处理完中断剩余工作后,会再次尝试读取信号量,因此再次阻塞,等待下一次中断发生。

使用下面的函数创建一个二进制信号量。SemaphoreHandle_t是二进制信号量的句柄类型,它本质上就是等价于QueueHandle_t类型,内核代码用得typedef做的类型别名。所以这个函数本质上就是创建了一个长度为1的队列。创建好的二进制信号量是空的,队列元素个数为0。

SemaphoreHandle_t xSemaphoreCreateBinary( void );

返回值:如果返回NULL,则代表系统没有足够的堆内存,创建失败。创建成功则返回二进制信号量的句柄。

使用下面的函数来从队列中读取一个二进制信号量。此函数不能在中断函数中使用。

BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );

参数xSemaphore:目标信号量的句柄。实际上就是一个队列的句柄。

参数xTicksToWait:等待信号量可读的阻塞时间,即等待队列非空的等待时间,详见队列函数xQueueReceive()的此参数的意义。

返回值:当成功获取到信号量时返回pdPASS,否则返回pdFALSE。详见队列函数xQueueReceive()的此参数的意义。

使用下面的函数在中断函数中发送一个二进制信号量。

BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore,
                                  BaseType_t *pxHigherPriorityTaskWoken );

参数xSemaphore:目标信号量的句柄。实际上就是一个队列的句柄。

参数pxHigherPriorityTaskWoken:这个参数前面已经介绍了,这里不翻译了。If calling xSemaphoreGiveFromISR() causes a task to leave the Blocked state, and the unblocked task has a priority higher than the currently executing task (the task that was interrupted), then, internally, xSemaphoreGiveFromISR() will set *pxHigherPriorityTaskWoken to pdTRUE.If xSemaphoreGiveFromISR() sets this value to pdTRUE,then normally a context switch should be performed before the interrupt is exited. This will ensure that the interrupt returns directly to the highest priority Ready state task.

返回值:返回pdTRUE代表信号量发送成功。返回pdFALSE代表队列中已经有信号量了(队列满了),发送失败。

忠告:当中断触发频率非常快时,中断推移处理任务可能无法及时取走二进制信号量,导致中断函数中使用xSemaphoreGiveFromISR()向队列中放入信号量时失败,从而丢失了后续的中断处理工作相关条件。因此,二进制信号量不合适用于同步中断函数和中断推移处理任务,这种场合应该使用direct to task notification机制或者计数信号量。

例子

//中断推移处理任务函数
void vHandlerTask( void *pvParameters )
{
    for( ;; )
    {
        //尝试从xBinarySemaphore这个二进制信号量对象取得一个二进制信号量,如果取不到则阻塞  
        //阻塞超时时间为portMAX_DELAY ,即一直阻塞
        xSemaphoreTake( xBinarySemaphore, portMAX_DELAY );
        
        //取到信号量后打印,用于象征性表示中断推移处理任务完成了对中断的最后处理工作
        vPrintString( "Handler task - Processing event.\r\n" );
    }
}

//中断函数
void InterruptHandler( void )
{
    BaseType_t xHigherPriorityTaskWoken;

    xHigherPriorityTaskWoken = pdFALSE;  //必须要初始化为FALSE
        
    //给出一个二进制信号量到,此操作可以唤醒中断推移处理任务。
    //因此在xSemaphoreGiveFromISR函数内部,xHigherPriorityTaskWoken会被设置为TRUE
    xSemaphoreGiveFromISR( xBinarySemaphore, &xHigherPriorityTaskWoken );
    
    //手动申请一次任务上下文切换,由于xHigherPriorityTaskWoken=TRUE,所以
    //任务切换会发生,使得中断推移处理任务可以被选中进入到运行态
    portYIELD_FROM_ISR( xHigherPriorityTaskWoken );

}

计数信号量

计数信号量是长度大于1的队列。在使用计数信号量时,并不在意队列中存储的元素是什么,而是在意队列中有多少个元素,元素个数就是信号量计数值。每次放一个计数信号,则队列中将多一个元素。每次取一次计数信号,则队列中元素个数减一。

计数信号量本质是队列,为了优化内存占用,计数信号量的队列元素大小为0,所以增加计数值并不会增加队列元素的内存占用。计数值是由队列结构体中的uxMessagesWaiting成员来计数的。

为了使用计数信号量,要在FreeRTOSConfig.h中将configUSE_COUNTING_SEMAPHORES配置为1。

计数信号量主要有两种用途:

1、统计事件(中断)发生的次数。初始化的时候计数信号量的值为0,即队列元素个数为0。每次事件(中断)发生时,中断函数会发送一个计数信号量到队列中,使得计数信号值增加1。中断处理任务会尝试从这个队列取出一个计数信号量,使得计数信号值减1。只要队列元素个数不为0,则说明还有中断需要被中断处理任务处理。

2、资源管理。计数信号量的值代表着有多少资源是可用的。初始化的时候计数信号量的值为总共可用的资源数。任务在访问资源的时候,必须先获取一个计数信号量,使得计数值减1。当计数信号量的值降低为0的时候,代表无资源可用。当一个任务用完了某个资源,那么这个资源可以还回去给其他任务使用,则此任务应该放出一个计数信号,增加计数信号量的值。例如,MCU挂接了一个以太网模块,这个模块最多支持8个socket同时使用。那么资源个数就是8,计数信号量的初始值是8。每个任务在创建一个socket的时候,使得计数信号量减少1,使用完socket释放后,使得计数信号量增加1。

创建计数信号量

SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount );

//例如:创建一个最大计数值为10,初始计数值为0的计数信号量对象
xCountingSemaphore = xSemaphoreCreateCounting( 10, 0 );

参数 uxMaxCount:计数信号量的最大计数值,也就是对应的队列的长度。

参数uxInitialCount:计数信号的初始计数值。

返回值:返回NULL代表创建失败。创建成功则返回计数信号量的句柄。

总结

FreeRTOS中的信号量机制有两种:二进制信号量和计数信号量,他们都是基于队列来实现的。

如果需要在中断函数中唤醒它的中断推移处理任务,则应该使用direct to task notification机制,一般不使用二进制信号量。

尽量保持中断函数简单简洁,执行迅速而不耗时。

你可能感兴趣的:(FreeRTOS学习笔记,单片机,嵌入式硬件,freertos)