FreeRTOS学习笔记十【中断管理-上】

目的

  • 介绍可以在中断服务函数例使用的FreeRTOS API函数。
  • 介绍将中断中处理的内容推迟到任务中处理的方法。
  • 创建和使用二值信号量和计数信号量。
  • 二值信号量和计数信号量的区别。
  • 使用队列将数据传入和传出中断服务函数。
  • 介绍一些FreeRTOS移植的中断嵌套模型。

中断中使用API

中断安全的API

通常需要在中断服务函数(ISR)中调用FreeRTOS的API函数,但许多的API在ISR中是不安全的,其中一些API会将调用的任务转换到阻塞态,如果在ISR中调用了这类API则会出现很多问题。FreeRTOS通过提供两个版本的API来解决这个问题,一个版本供任务调用,一个版本供ISR调用,用于ISR版本的API其函数名都带有"FromISR"后缀。用于中断版的API代码更简洁,ISR代码更高效,并且中断版的输入参数更简单。
注意:千万不要在中断服务函数中调用没有"FromISR"后缀的API函数。

xHigherPriorityTaskWoken参数

如果中断当前执行的上下文(发送中断,暂停当前执行的任务,进入到中断服务函数中执行),则中断服务函数退出时接下来运行的任务可能与进入中断之前运行的任务不同。例如:任务1正在运行,任务2在等待队列中的数据,现在发送了中断,任务1被抢占,进入中断后,ISR向队列中写入数据,此时任务进入就绪态,ISR结束后任务2进入运行态。这种情况下中断前运行的时任务1,中断后运行的时任务2.
如果API函数(入队、延时时间到达等API)解除阻塞的任务的优先级高于运行态任务的优先级,根据内核的调度策略,应该会立即切换到高优先级的任务,实际发生任务切换时,取决于调用API函数的上下文:

  • 从任务中调用API函数
    如果将FreeRTOSConfig.h中configUSE_PREEMPTION设置为1,那么切换到高优先级任务的调度会在API函数中自动发送(即在API函数退出之前),如FreeRTOS学习笔记六【任务管理-调度算法】中的抢占式调度。
  • 从中断中调用函数
    在中断内不会自动切换到高优先级的任务,相反,需要应用程序设置变量以通知调度器执行上下文切换。中断安全版的API(以“FromISR”结尾的函数) 具有一个名为pxHigherPriorityTaskWoken的指针参数就是作用于此。如果应该执行上下文切换,则中断安全版API函数将(*pxHigherPriorityTaskWoken)设置为pdTRUE,因此,pxHigherPriorityTaskWoken指向的变量必须在第一次使用前初始化为pdFALSE。如果应用程序选择不从ISR返回时的pxHigherPriorityTaskWoken的状态切换上下文,那么优先级较高的任务将保持就绪态,直到下一次调度程序运行(最坏情况下将在下次滴答中断时切换)。
    API函数只能将(*pxHigherPriorityTaskWoken)设置为pdTRUE,如果ISR调用多个API函数,则可以给每个API传入一个pxHigherPriorityTaskWoken指向的变量,但必须在第一次使用前初始化为pdFALSE。

在中断安全版的API中不自动切换上下文有一下几个原因:

  1. 避免不必要的上下文切换
    在执行任何任务前,中断可以发生很多次。例如:UART中断,每收到一个字节就会中断一次,但是需要在这一段数据接收完后才会处理。
  2. 控制执行顺序
    中断是偶尔发生的,并且发生时间也不可预测。
  3. 可移植性
    不自动切换上下文是所有处理器移植时最简单的机制。
  4. 效率
    在小型处理器中,同一ISR中可以调用多次API,但是不需要(或不允许)在同一个ISR中切换多次上下文,而只能在ISR最后切换上下文。
  5. 在滴答中断中执行
    可以将应用程序代码添加到RTOS的滴答中断中执行,在滴答中断内尝试切换上下文的结果取决于正在使用的FreeRTOS的移植。充其量,它只会导致调度程序不必要的调用。

pxHigherPriorityTaskWoken参数是可选的。如果不需要,将pxHigherPriorityTaskWoken设置为NULL即可。

portYIELD_FROM_ISR() 和portEND_SWITCHING_ISR()宏

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可以破坏任务的运行时间。
  • 某些处理器在执行ISR不能接收新的中断或不能被打断。
  • 某些处理器可以执行中断嵌套,但是中断嵌套越多,就越复杂,也不可预测。
  • 应用程序需要考虑任务和ISR同时访问变量,外设、内存等资源时的注意事项。

中断服务程序必须记录中断的产生原因,并清除中断。中断所需要的其他任何处理都可以在任务中执行,以保证ISR尽可能快的退出,这称为延迟中断处理,因为将中断所需处理的放到了任务里处理。将中断处理推迟到任务还允许应用程序对于其他任务确定处理的优先级,并可以使用所有API函数。如果延迟处理的任务优先级高于其他任务优先级,则将立即执行处理,就像在ISR里执行一下。下图描述了这种情况,任务1是普通应用程序任务,任务2是延迟中断处理任务。
在这里插入图片描述
图中,中断处理中t2开始,并执行到t4才结束,但是仅在t2到t3之间才是ISR的处理时间,如果没有使用延迟终端处理,那么在t2到t4之间的整个时间段都会在ISR中。关于何时在ISR中执行最好,何时将处理推迟到任务中,没有绝对的规则,但在以下情况发生时将处理延迟到任务是最有用的:

  • 中断所需处理的并非简单的任务,例如,如果中断只是存储数模转换的结果,那在ISR中执行时最好的,但如果转换结果需要软件滤波,那么它最好时在任务中执行滤波。
  • 无法在ISR内执行的操作,例如,分配内存。
  • 处理具有不确定时,如事先不知道处理需要多长时间。

使用二值信号量同步

描述

二值信号量API的中断安全版用于每次发送特定中断时解除阻塞的任务,从而使任务与中断同步。这可以将大部分中断事件处理在同步的任务中实现,只将非常快和短的部分保留在ISR中。
前面描述了如果中断处理特别关键,那么可以设置延迟处理任务的优先级,以确保该任务始终抢占
统中其他任务,然后就可以在ISR中包含portYIELD_FROM_ISR()的调用,确保ISR直接返回到延迟中断处理的任务。这样就可以使整个事件处理在时间上连续执行,就像它已经在ISR本身内实现一样。下图描述了上面相同的情况,但加入了信号量 。
在这里插入图片描述
延迟处理任务中经过二值信号量的"take"操作后进入阻塞状态,等待二值信号量可用,当事件发生时,ISR对同一个二值信号量执行"give"操作后,解除阻塞的延迟处理任务,使之继续执行所需处理的事情。"take"和"give"是根据使用场景可能有不同定义,在此中断方案中,二值信号量可视为长度为1的队列,因此,该队列要么为空,要么为满。调用xSemaphoreTake(),延迟处理任务会尝试读取队列里的标志(有标志时队列满,无标志时队列为空,即为二值),如果队列为空,则任务进入阻塞态;当事件发生时,ISR中调用xSemaphoreGiveFromISR()将标志放入队列,延迟处理任务退出阻塞态,将标志获取后,队列再次为空,处理完这次后再去获取标志,如果没有标志,任务阻塞等待下次事件发生,如果有标志,获取标志处理事件。如下图描述。
在这里插入图片描述
图中,事件发生后中断执行了"give"操作,之后任务执行了"take"操作,处理任务时再次发生了事件,因为任务已经执行了"take"操作,所以后面中断的这次"give"操作才会成功。这就是为什么将该场景描述称为队列的原因。

xSemaphoreCreateBinary()

xSemaphoreCreateBinary()用于创建一个二值信号量,在使用二值信号量之前必须要创建它才可以使用,它的原型如下:

SemaphoreHandle_t xSemaphoreCreateBinary( void );

参数:无
返回值:

  • NULL
    创建二值信号量失败,没有足够的内存空间。
  • 非NULL
    创建二值信号量成功,返回的是创建的二值信号量的句柄。

xSemaphoreTake()

"take"一个信号量表示获取或接收到一个信号量,只有在信号量可用是才能获取,除了递归互斥锁外,所有信号量都可以使用xSemaphoreTake()获取,但是不能在ISR中调用它。它的原型如下:

BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );

参数:

  • xSemaphore
    信号量的句柄。
  • xTicksToWait
    xTicksToWait 与其他API中的xTicksToWait 类似。
    返回值:
  • pdPASS
    信号量可用,并成功获取到一个信号量。
  • pdFALSE
    信号量获取失败,表示在阻塞等待xTicksToWait的时间到达后,信号量依然不可用。

xSemaphoreGiveFromISR()

xSemaphoreGiveFromISR()可以用于二值信号量和计数信号量。它是xSemaphoreGive()的中断安全版本。它的原型如下:

BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, 
								  BaseType_t *pxHigherPriorityTaskWoken );

参数:

  • xSemaphore
    信号量的句柄。
  • pxHigherPriorityTaskWoken
    该参数已在前面介绍,所有中断安全版的函数都有该参数。
    返回值:
  • pdPASS
    表示执行"give"操作成功。
  • pdFAIL
    表示信号量一直可用,不能执行"give"操作。

示例

示例中每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( ;; );
}

运行结果:
在这里插入图片描述
任务调度情况:
在这里插入图片描述

对示例的改进

上面的示例使用了二值信号量实现任务与中断的同步,执行的顺序如下:

  1. 发送了中断。
  2. ISR执行并"give"了信号量。
  3. 在ISR之后立即执行了延迟处理任务,并"take"了信号量。
  4. 任务处理了事件,然后再次"take"信号量,此时任务进入阻塞态,因为信号量还不可用。

从这个步骤中可以看出,只有在中断发生的频率较低时,这种程序结构才能有足够的能力处理事件。考虑下面的情况,任务完成第一个中断处理前发生了第二次中断,第三次中断:

  • 当执行第二个ISR时,信号量为空,因此ISR将"give"信号量,并且任务在处理完成第一个事件后会立即处理第一个事件,如下图。在这里插入图片描述
  • 当执行第三个ISR时,信号已经可用(因为第二个ISR还没开始执行,此时信号量"give"函数会返回pdFAIL),因此任务不会知道第三个事件已经发生,如下图。
    在这里插入图片描述

在实际应用中,中断由硬件产生,并且它的发生时间不可预测,因此为了最大限度的减少错过中断的可能性,必须对延迟处理中断任务进行结构化(后面会介绍其他方法),以便它处理所有已发生的事件。
上面示例中延迟处理任务还有一个问题:当它调用xSemaphoreTake()时它没有使用超时。如果将xSemaphoreTake()的xTicksToWait参数设置为portMAX_DELAY,意味着它将无期限的等待信号量可用。这种方式常常用于代码中,因为这种方式可以简化代码逻辑,也容易理解,但是,无期限等待在实际代码中可能会产生不良操作,因为它使系统很难在错误中恢复。例如,一个任务正在等待中断给出信号量,但硬件中的错误阻止了中断的发生:

  • 如果任务在无超时情况下等待,它将不知道错误状态,并永远等待。
  • 如果任务在由超时情况下等待,则xSemaphoreTake()超时后将返回pdFAIL,然后任务就可以做相应的检测以及错误清除。在下面示例中也处理这种情况。

下面的示例实现的是一个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();
		}
	}
}
  • 14
    点赞
  • 67
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值