FreeRTOS——中断管理(基于百问网FreeRTOS教学视频)


在RTOS中,需要应对各种事件,这些事件是通过硬件中断产生的。当系统运行Task1的时候,用户按下了按键,触发按键中断,这个中断的处理流程如下:

  • CPU 跳到固定地址去执行代码,这个固定地址通常被称为中断向量,这个跳转时硬件实现的 。
  • 执行代码做什么?
  • 保存现场:Task1 被打断,需要先保存 Task1 的运行环境,比如各类寄存器的值 。
  • 分辨中断、调用处理函数(这个函数就被称为 ISR,interrupt service routine) 。
  • 恢复现场:继续运行 Task1,或者运行其他优先级更高的任务。

ISR是在内核中被调用的,ISR执行过程中,用户的任务无法执行,ISR要尽量快,否则:

  • 其他优先级的中断无法被处理,实时性无法保证;
  • 用户任务无法执行,显得系统卡顿。

如果硬件中断的处理就是非常耗时间,对于这类中断的处理就分为两部分:

  • ISR:尽快做清理,记录工作,然后触发某个任务;
  • 任务:更复杂的事情放在任务中处理。

所以,ISR和任务之间需要进行通信。

要在FreeRTOS中熟练使用中断,有几个原则要先说明:

  • FreeRTOS 把任务认为是硬件无关的,任务的优先级由程序员决定,任务何时运行由调度器决定
  • ISR 虽然也是使用软件实现的,但是它被认为是硬件特性的一部分,因为它跟硬件密切相关
  • 何时执行?由硬件决定
  • 哪个 ISR 被执行?由硬件决定
  • ISR 的优先级高于任务:即使是优先级最低的中断,它的优先级也高于任 务。任务只有在没有中断的情况下,才能执行。

一、引入两套API函数

FreeRTOS中很多API函数都有两套:一套在任务中使用,另一套在ISR中使用。后者的函数名含有"FromISR"后缀。

为什么要引入两套API函数?

  • 很多 API 函数会导致任务计入阻塞状态;
  • 运行这个函数的任务进入阻塞状态 ;
  • 比如写队列时,如果队列已满,可以进入阻塞状态等待一会 ;
  • ISR 调用 API 函数时,ISR 不是"任务",ISR 不能进入阻塞状态 ;

所以,在任务中、在 ISR 中,这些函数的功能是有差别的

FreeRTOS 使用两套函数,而不是使用一套函数,是因为有如下好处:

  • 使用同一套函数的话,需要增加额外的判断代码、增加额外的分支,是的函 数更长、更复杂、难以测试 ;

在任务、ISR 中调用时,需要的参数不一样,比如:

  • 在任务中调用:需要指定超时时间,表示如果不成功就阻塞一会 ;
  • 在 ISR 中调用:不需要指定超时时间,无论是否成功都要即刻返回 ;
  • 如果强行把两套函数揉在一起,会导致参数臃肿、无效
  • 移植 FreeRTOS 时,还需要提供监测上下文的函数,比如 is_in_isr() ;
  • 有些处理器架构没有办法轻易分辨当前是处于任务中,还是处于 ISR 中,就 需要额外添加更多、更复杂的代码 。

使用两套函数可以让程序更高效,但是也有一些缺点,比如你要使用第三方库函数时, 即会在任务中调用它,也会在ISR总调用它。这个第三方库函数用到了FreeRTOS的API函数, 你无法修改库函数。这个问题可以解决:

  • 把中断的处理推迟到任务中进行(Defer interrupt processing),在任务中调用库函数;
  • 尝试在库函数中使用"FromISR"函数;
  • 在任务中、在 ISR 中都可以调用"FromISR"函数 ;
  • 反过来就不行,非 FromISR 函数无法在 ISR 中使用;
  • 第三方库函数也许会提供 OS 抽象层,自行判断当前环境是在任务还是在ISR中,分别调用不同的函数。

二、两套API函数列表

类型在任务中在 ISR 中
队列(queue)xQueueSendToBack
xQueueSendToFront
xQueueReceive
xQueueOverwrite
xQueuePeek
xQueueSendToBackFromISR
xQueueSendToFrontFromISR
xQueueReceiveFromISR
xQueueOverwriteFromISR
xQueuePeekFromISR
信号量(semaphore)xSemaphoreGive
xSemaphoreTake
xSemaphoreGiveFromISR
xSemaphoreTakeFromISR
事件组(event group)xEventGroupSetBits
xEventGroupGetBits
xEventGroupSetBitsFromISR
xEventGroupGetBitsFromISR
任务通知(task notification)xTaskNotifyGive
xTaskNotify
vTaskNotifyGiveFromISR
xTaskNotifyFromISR
软件定时器(software timer)xTimerStart
xTimerStop
xTimerReset
xTimerChangePeriod
xTimerStartFromISR
xTimerStopFromISR
xTimerResetFromISR
xTimerChangePeriodFromISR

2.1 xHigherPriorityTaskWoken 参数

xHigherPriorityTaskWoken 的含义是:是否有更高优先级的任务被唤醒了。如果为 pdTRUE,则意味着后面要进行任务切换。 以写队列为例:

任务A调用xQueueSendToBack()写队列,有几种情况发生:

  • 队列满了,任务 A 阻塞等待,另一个任务 B 运行;
  • 队列没满,任务 A 成功写入队列,但是它导致另一个任务 B 被唤醒,任务 B的优先级更高:任务 B 先运行 ;
  • 队列没满,任务 A 成功写入队列,即刻返回 。

可以看到,在任务中调用 API 函数可能导致任务阻塞、任务切换,这叫做"context switch", 上下文切换。这个函数可能很长时间才返回,在函数的内部实现了任务切换。

pxHigherPriorityTaskWoken 参数,就是用来保存函数的结果:是否需要切换

  1. *pxHigherPriorityTaskWoken 等于 pdTRUE:函数的操作导致更高优先级的任务就绪了,ISR 应该进行任务切换。
  2. *pxHigherPriorityTaskWoken 等于 pdFALSE:没有进行任务切换的必要。

xQueueSendToBackFromISR()函数也可能导致任务切换,但是不会在函数内部进行切换,而是返回一个参数:表示是否需要切换,函数原型与用法如下:

/* 
 * 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞 
 */ 
 BaseType_t xQueueSendToBackFromISR( QueueHandle_t xQueue, 
					const void *pvItemToQueue, 
					BaseType_t *pxHigherPriorityTaskWoken); 

用法示例:

BaseType_t xHigherPriorityTaskWoken = pdFALSE; 
	xQueueSendToBackFromISR(xQueue, pvItemToQueue, &xHigherPriorityTaskWoken); 

	if (xHigherPriorityTaskWoken == pdTRUE) 
	{ 
 		/* 任务切换 */ 
	} 

在ISR中调用API时不进行任务切换,而只是在"xHigherPriorityTaskWoken"中标记一下, 除了效率,还有多种好处:

  • 效率高:避免不必要的任务切换
  • 让 ISR 更可控:中断随机产生,在 API 中进行任务切换的话,可能导致问题 更复杂
  • 可移植性
  • 在 Tick 中断中,调用 vApplicationTickHook():它运行与 ISR,只能使用"FromISR"的函数

2.2 切换任务

FreeRTOS 的 ISR 函数中,使用两个宏进行任务切换:

这两个宏做的事情是完全一样的,在老版本的 FreeRTOS 中,

/* 使用汇编实现 */
portEND_SWITCHING_ISR( xHigherPriorityTaskWoken ); 
/* 
 *或使用 
 */
/* 使用 C 语言实现 */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken ); 

新版本都统一使用 portYIELD_FROM_ISR。

示例如下:

	void XXX_ISR() 

	{ 

		 int i; 

 		 BaseType_t xHigherPriorityTaskWoken = pdFALSE; 

 		for (i = 0; i < N; i++) 

 		{ 
 			/* 被多次调用 */ 
 			xQueueSendToBackFromISR(..., &xHigherPriorityTaskWoken); 
		} 
		
/* 最后再决定是否进行任务切换 
 * xHigherPriorityTaskWoken 为 pdTRUE 时才切换 
 */ 
	portYIELD_FROM_ISR(xHigherPriorityTaskWoken); 
	}

三、中断延迟处理

处理流程:
在这里插入图片描述

解释说明:

  • t1:任务 1 运行,任务 2 阻塞
  • t2:发生中断,
  • 该中断的 ISR 函数被执行,任务 1 被打断
  • ISR 函数要尽快能快速地运行,它做一些必要的操作(比如清除中断),然后唤 醒任务 2
  • t3:在创建任务时设置任务 2 的优先级比任务 1 高(这取决于设计者),所以ISR 返回后,运行的是任务 2,它要完成中断的处理。任务 2 就被称为"deferred processing task",中断的延迟处理任务。
  • t4:任务 2 处理完中断后,进入阻塞态以等待下一个中断,任务 1 重新运行

四、中断与任务间通信

前面讲解过的队列、信号量、互斥量、事件组、任务通知等等方法,都可使用。要注意的是,在ISR中使用的函数要有"FromISR"后缀。

4.1 优化实时性

以前,在中断函数里写队列时,代码如下:

static void DispatchKey (struct ir_data *pidata)
{
#if 0
	extern QueueHandle_t g_xQueueCar1;
	extern QueueHandle_t g_xQueueCar2;
	extern QueueHandle_t g_xQueueCar3;
	
	xQueueSendToBackFromISR(g_xQueueCar1,pidata,NULL);
	xQueueSendToBackFromISR(g_xQueueCar2,pidata,NULL);
	xQueueSendToBackFromISR(g_xQueueCar3,pidata,NULL);
#endif
	int i;
	for(i=0;i<g_queue_cnt;i++)
	{
		xQueueSendToBackFromISR(g_xQueues[i], pidata, NULL);
	}
}

假设当前运行的是任务A,它的优先级比较低,在它运行过程中发生了中断,中断函数调用了DispatchKey函数写了队列,使得任务B被唤醒了。任务B的优先级比较高,它应该在中断执行完后马上就能运行。但是上述代码无法实现这个目标,xQueueSendFromISR函数会把任务B调整为就绪态,但是不会发起一次调度。

修改代码之后:

static void DispatchKey (struct ir_data *pidata)
{
	BaseType_t xHigherPriorityTaskWoken = pdFALSE;
#if 0
	extern QueueHandle_t g_xQueueCar1;
	extern QueueHandle_t g_xQueueCar2;
	extern QueueHandle_t g_xQueueCar3;
	
	xQueueSendToBackFromISR(g_xQueueCar1,pidata,NULL);
	xQueueSendToBackFromISR(g_xQueueCar2,pidata,NULL);
	xQueueSendToBackFromISR(g_xQueueCar3,pidata,NULL);
#endif
	int i;
	for(i=0;i<g_queue_cnt;i++)
	{
		xQueueSendFromISR(g_xQueues[i], pidata, &xHigherPriorityTaskWoken);
	}
	portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
	
}

在修改后代码的for循环中传入一个变量的地址:&xHigherPriorityTaskWoken,它的初始值是pdFALSE,表示无需发起调度。如果xQueueSendFromISR函数发现唤醒了更高优先级的任务,那么就会把这个变量设置为pdTRUE。

在调用portYIELD_FROM_ISR()函数,如果xHigherPriorityTaskWoken为pdTRUE,它就会发起一次调度。

本程序上机时,我们感觉不到有什么不同。

参考:
FreeRTOS入门与工程实践-基于STM32

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值