通常需要在中断服务函数(ISR)中调用FreeRTOS的API函数,但许多的API在ISR中是不安全的,其中一些API会将调用的任务转换到阻塞态,如果在ISR中调用了这类API则会出现很多问题。FreeRTOS通过提供两个版本的API来解决这个问题,一个版本供任务调用,一个版本供ISR调用,用于ISR版本的API其函数名都带有"FromISR"后缀。用于中断版的API代码更简洁,ISR代码更高效,并且中断版的输入参数更简单。
注意:千万不要在中断服务函数中调用没有"FromISR"后缀的API函数。
如果中断当前执行的上下文(发送中断,暂停当前执行的任务,进入到中断服务函数中执行),则中断服务函数退出时接下来运行的任务可能与进入中断之前运行的任务不同。例如:任务1正在运行,任务2在等待队列中的数据,现在发送了中断,任务1被抢占,进入中断后,ISR向队列中写入数据,此时任务进入就绪态,ISR结束后任务2进入运行态。这种情况下中断前运行的时任务1,中断后运行的时任务2.
如果API函数(入队、延时时间到达等API)解除阻塞的任务的优先级高于运行态任务的优先级,根据内核的调度策略,应该会立即切换到高优先级的任务,实际发生任务切换时,取决于调用API函数的上下文:
在中断安全版的API中不自动切换上下文有一下几个原因:
pxHigherPriorityTaskWoken参数是可选的。如果不需要,将pxHigherPriorityTaskWoken设置为NULL即可。
taskYIELD() 是一个可以在任务中调用以请求上下文切换的宏。portYIELD_FROM_ISR() 和portEND_SWITCHING_ISR() 都是taskYIELD() 的中断安全版本。 portYIELD_FROM_ISR() 和portEND_SWITCHING_ISR() 以相同的方式使用,并执行相同的操作。 一些FreeRTOS移植仅提供两个宏中的一个。 较新的FreeRTOS移植提供两种宏。 本文将使用portYIELD_FROM_ISR()宏。
portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
从中断安全版的API传出的xHigherPriorityTaskWoken可以直接用于这两个宏的参数,如果xHigherPriorityTaskWoken为pdFALSE,调用portYIELD_FROM_ISR()将不会发生上下文切换,否则就会发生上下文切换,并且处于运行态的任务会改变。大多数Free RTOS的移植中允许在ISR内的任何地方调用portYIELD_FROM_ISR(),但一些小型处理器只允许在ISR最后调用portYIELD_FROM_ISR()。
通常,ISR需要尽可能的短,简单。原因有一下几点:
中断服务程序必须记录中断的产生原因,并清除中断。中断所需要的其他任何处理都可以在任务中执行,以保证ISR尽可能快的退出,这称为延迟中断处理,因为将中断所需处理的放到了任务里处理。将中断处理推迟到任务还允许应用程序对于其他任务确定处理的优先级,并可以使用所有API函数。如果延迟处理的任务优先级高于其他任务优先级,则将立即执行处理,就像在ISR里执行一下。下图描述了这种情况,任务1是普通应用程序任务,任务2是延迟中断处理任务。
图中,中断处理中t2开始,并执行到t4才结束,但是仅在t2到t3之间才是ISR的处理时间,如果没有使用延迟终端处理,那么在t2到t4之间的整个时间段都会在ISR中。关于何时在ISR中执行最好,何时将处理推迟到任务中,没有绝对的规则,但在以下情况发生时将处理延迟到任务是最有用的:
二值信号量API的中断安全版用于每次发送特定中断时解除阻塞的任务,从而使任务与中断同步。这可以将大部分中断事件处理在同步的任务中实现,只将非常快和短的部分保留在ISR中。
前面描述了如果中断处理特别关键,那么可以设置延迟处理任务的优先级,以确保该任务始终抢占
统中其他任务,然后就可以在ISR中包含portYIELD_FROM_ISR()的调用,确保ISR直接返回到延迟中断处理的任务。这样就可以使整个事件处理在时间上连续执行,就像它已经在ISR本身内实现一样。下图描述了上面相同的情况,但加入了信号量 。
延迟处理任务中经过二值信号量的"take"操作后进入阻塞状态,等待二值信号量可用,当事件发生时,ISR对同一个二值信号量执行"give"操作后,解除阻塞的延迟处理任务,使之继续执行所需处理的事情。"take"和"give"是根据使用场景可能有不同定义,在此中断方案中,二值信号量可视为长度为1的队列,因此,该队列要么为空,要么为满。调用xSemaphoreTake(),延迟处理任务会尝试读取队列里的标志(有标志时队列满,无标志时队列为空,即为二值),如果队列为空,则任务进入阻塞态;当事件发生时,ISR中调用xSemaphoreGiveFromISR()将标志放入队列,延迟处理任务退出阻塞态,将标志获取后,队列再次为空,处理完这次后再去获取标志,如果没有标志,任务阻塞等待下次事件发生,如果有标志,获取标志处理事件。如下图描述。
图中,事件发生后中断执行了"give"操作,之后任务执行了"take"操作,处理任务时再次发生了事件,因为任务已经执行了"take"操作,所以后面中断的这次"give"操作才会成功。这就是为什么将该场景描述称为队列的原因。
xSemaphoreCreateBinary()用于创建一个二值信号量,在使用二值信号量之前必须要创建它才可以使用,它的原型如下:
SemaphoreHandle_t xSemaphoreCreateBinary( void );
参数:无
返回值:
"take"一个信号量表示获取或接收到一个信号量,只有在信号量可用是才能获取,除了递归互斥锁外,所有信号量都可以使用xSemaphoreTake()获取,但是不能在ISR中调用它。它的原型如下:
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );
参数:
xSemaphoreGiveFromISR()可以用于二值信号量和计数信号量。它是xSemaphoreGive()的中断安全版本。它的原型如下:
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken );
参数:
示例中每500ms产生一次软件中断,然后使用二值信号量实现中断与任务同步。
#define mainINTERRUPT_NUMBER 3
/* 产生软件中断的任务,示例中用于模拟,实际使用中可能是硬件中断 */
static void vPeriodicTask( void *pvParameters )
{
const TickType_t xDelay500ms = pdMS_TO_TICKS( 500UL );
for( ;; )
{
/* 阻塞500ms,然后发送软件中断 */
vTaskDelay( xDelay500ms );
/* 发送软件中断,输出提示信息 */
vPrintString( "Periodic task - About to generate an interrupt.\r\n" );
vPortGenerateSimulatedInterrupt( mainINTERRUPT_NUMBER );
vPrintString( "Periodic task - Interrupt generated.\r\n\r\n\r\n" );
}
}
/* 延迟处理任务 */
static void vHandlerTask( void *pvParameters )
{
for( ;; )
{
/* 获取信号量,如果信号量不可用,则阻塞等待 */
xSemaphoreTake( xBinarySemaphore, portMAX_DELAY );
/* 处理事件 */
vPrintString( "Handler task - Processing event.\r\n" );
}
}
/* ISR */
static uint32_t ulExampleInterruptHandler( void )
{
BaseType_t xHigherPriorityTaskWoken;
/* xHigherPriorityTaskWoken 指向的变量在使用前需要初始化为pdFALSE */
xHigherPriorityTaskWoken = pdFALSE;
/* 发送一个信号量 */
xSemaphoreGiveFromISR( xBinarySemaphore, &xHigherPriorityTaskWoken );
/* 根据xHigherPriorityTaskWoken 判断是否需要执行调度 */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
int main( void )
{
/* 创建二值信号量 */
xBinarySemaphore = xSemaphoreCreateBinary();
/* 创建成功 */
if( xBinarySemaphore != NULL )
{
/* 创建延迟处理任务 */
xTaskCreate( vHandlerTask, "Handler", 1000, NULL, 3, NULL );
/* 创建产生软件中断的任务,示例中用于模拟,实际使用中可能是硬件中断 */
xTaskCreate( vPeriodicTask, "Periodic", 1000, NULL, 1, NULL );
/* 设置ISR函数,示例中用于模拟,实际使用中可能是硬件中断 */
vPortSetInterruptHandler( mainINTERRUPT_NUMBER, ulExampleInterruptHandler );
/* 启动调度 */
vTaskStartScheduler();
}
for( ;; );
}
上面的示例使用了二值信号量实现任务与中断的同步,执行的顺序如下:
从这个步骤中可以看出,只有在中断发生的频率较低时,这种程序结构才能有足够的能力处理事件。考虑下面的情况,任务完成第一个中断处理前发生了第二次中断,第三次中断:
在实际应用中,中断由硬件产生,并且它的发生时间不可预测,因此为了最大限度的减少错过中断的可能性,必须对延迟处理中断任务进行结构化(后面会介绍其他方法),以便它处理所有已发生的事件。
上面示例中延迟处理任务还有一个问题:当它调用xSemaphoreTake()时它没有使用超时。如果将xSemaphoreTake()的xTicksToWait参数设置为portMAX_DELAY,意味着它将无期限的等待信号量可用。这种方式常常用于代码中,因为这种方式可以简化代码逻辑,也容易理解,但是,无期限等待在实际代码中可能会产生不良操作,因为它使系统很难在错误中恢复。例如,一个任务正在等待中断给出信号量,但硬件中的错误阻止了中断的发生:
下面的示例实现的是一个UART的延迟处理程序,假设每接收到一个字符时UART都会产生一个接收中断,并且UART接收到的字符在它的硬件FIFO中。
static void vUARTReceiveHandlerTask( void *pvParameters )
{
/* 两个中断事件预期的最大间隔时间 */
const TickType_t xMaxExpectedBlockTime = pdMS_TO_TICKS( 500 );
for( ;; )
{
/* 在设定的等待时间内获取信号量*/
if( xSemaphoreTake( xBinarySemaphore, xMaxExpectedBlockTime ) == pdPASS )
{
/* 获得了信号量。 在下次调用xSemaphoreTake()之前处理所有的Rx事件。每个Rx事件都会
在UART的接收FIFO中放置一个字符,并假设UART_RxCount()返回FIFO中的字符数*/
while( UART_RxCount() > 0 )
{
/* 假设UART_ProcessNextRxEvent()处理一个Rx字符,将FIFO中的字符数减少1 */
UART_ProcessNextRxEvent();
}
}
else
{
/* 在预计时间内没有发生中断,就可能产生了错误需要清除错误标志 */
UART_ClearErrors();
}
}
}