目录
- 1、概述
- 2、在ISR中使用FreeRTOS中专用的API
- 2.1 独立的用于ISR中的API
- 2.2 关于xHigherPriorityTaskWoken 参数的初步理解
- 3、延迟中断处理的方法-将中断中的处理推迟到任务中去
- 4 方法一:用二进制信号量来同步ISR与”延时处理的任务“
- 4.1 二进制信号量
- 4.2 函数用法
- 4.2.1 xSemaphoreCreateBinary(void)
- 4.2.2 xSemaphoreGive() & xSemaphoreGiveFromISR()
- 4.2.3 xSemaphoreTake()
- 4.3 使用二进制信号量将任务与中断同步实例
- 5 方法二:使用计数信号量实现ISR与任务的同步
- 5.1 xSemaphoreCreateCounting()
- 5.2 实例:使用计数信号量将任务与中断同步
- 6 方法三:将工作推迟到 RTOS 守护进程
- 6.1 RTOS Daemon Task 守护进程
- 6.2 发送延迟处理任务到守护进程
- 6.3 xTimerPendFunctionCallFromISR()
- 6.4 实例
- 7 在中断服务程序中使用队列
- 7.1 相关函数
- 7.2 使用 ISR 中的队列时的注意事项
- 7.3 例
- 总结
1、概述
MCU中都是以中断的方式处理各类事件,需要硬件的支持。因此中断系统是一个非常重要的系统。而FreeRTOS是以任务的方式处理事件。任务是纯软件的方式。因此FreeRTOS不可避免的要同时处理好硬件层的中断与软件层的任务的关系。
中断是需要硬件支持 的,但是中断被触发后,必然要跳转到中断处理程序ISR去完成后续的事件处理,而ISR则是软件代码。也就是说不论硬件的中断还是软件的任务,最终的处理其实都是软件的代码实现。虽然是用软件编写的,通常认为中断服务程序ISR是一种硬件功能的一部分,因为硬件控制着哪个中断服务程序将运行以及何时运行。
理解到这里很重要。软件任务有优先级,硬件功能的中断也有优先级。任务只有在没有 ISR 运行时才会运行,因此最低优先级的中断会中断最高优先级的任务,任务无法抢占 ISR。
由于FreeRTOS中的任务是在每一个Tick中必须参与调度的。而中断的优先级又比任务的优先级高。因此中断的ISR一旦运行,则任务的调度实际是停止的**(当然,Tick中断也是系统及中断,这里涉及到系统中断中的优先级问题,而本文是讨论中断与任务的关系,因此不在此详述。)**。因此就要求ISR中的所有事件的处理代码不能占用太长的CPU时间,否则必然会导至整个多任务系统的停止。
基于以上的原因,处理好中断与任务之间的关系,数据的传递,执行时间的分配等就成为我们必须时刻关注的。
因此,本章重点完成任务与中断相关的以下内容详述:
-> 可以在中断服务例程中使用哪些 FreeRTOS API 函数。
-> 将中断处理推迟到任务的方法。
-> 如何创建和使用二进制信号量和计数信号量。
-> 二进制信号量和计数信号量之间的区别。
-> 如何使用队列将数据传入和传出中断服务程序。
-> 某些 FreeRTOS 端口可用的中断嵌套模型。
2、在ISR中使用FreeRTOS中专用的API
前面已说明了,ISR不能执行太长时间,特别是不能有可能导至阻塞的代码或函数语句。否则一旦ISR执行太长时间(比如进入阻塞状态)将会导至整个系统停摆(因为ISR阻塞期间FreeRTOS任务调度无法进行)。
实际使用中,我们通常会在中断服务例程 (ISR) 中使用 FreeRTOS API 函数,但许多 FreeRTOS API 函数执行的操作会使得程序进入Blocked状态;比如读到队列数据时,可能会进入阻塞, 以等待队列中有可读的数据。因此这类会导致阻塞状态的API是不能在ISR使用的。
FreeRTOS为解决ISR的尽可能短而高效持行的问题提供了两个办法。1、是提ISR专用的API,2、把某些长耗时工作延迟到其后的任务中持行。以下就这两方面进行解说。
2.1 独立的用于ISR中的API
FreeRTOS通过提供两个版本的一些API函数解决了这个问题;一个版本用于任务,一个版本用于ISR。用于 ISR 的函数在其名称后附加了“FromISR” 。比如以下的队列接收函数,提供了两个版本,一个是任务中使用的API,一个是ISR中用的API。
注意:切勿从 ISR 调用名称中没有“FromISR”的 FreeRTOS API 函数。
2.2 关于xHigherPriorityTaskWoken 参数的初步理解
有这和一个场 景:
从一个正在执行的任务A中,因为中断而跳转到ISR进行处理,而假设ISR中用xQueueSendFromISR()函数向队列写入了一个元素,使得原本一个优先级高于A的任务B(因为在等待读取这个队列的数据而处于阻塞状态)可以退出阻塞,则ISR处理完后,是跳转到任务A中,还是跳转到优先级较高的任务B。
FreeRTOS在这里的处理方式是由使用者来选择。
1、自然情况下,应该是ISR执行完后,应该跳回中断发生的位置,即任务A中。
2、在运行带FromISR的函数里,带有一个标志变量 xHigherPriorityTaskWoken, 这个变量会当有更高优先级任务B离开阻塞状态时(就如上面场景中所述的),而被自动设为pdTRUE,这样使用者可以手动在ISR中去切换到任务B,而不是切换回任务A。这个手动切换的命令为:portYIELD_FROM_ISR( xHigherPriorityTaskWoken ); 当xHigherPriorityTaskWoken为pdTRUE时则切换到任务B,否则还是回到任务A中。
以上这部分的任务切换,按官方的术语,叫上下文切换。
实例 :
void vBufferISR( void ) //这是一个ISR中断处理程序
{
char cIn;
BaseType_t xHigherPriorityTaskWoken;
xHigherPriorityTaskWoken = pdFALSE; //默认值必须设为pdFALSE
do{ //该循环的意图是从buffer中不断读出一个字符,并写入到队列中
cIn = INPUT_BYTE( RX_REGISTER_ADDRESS ); //从buffer中读出字符
xQueueSendToBackFromISR( xRxQueue, &cIn, &xHigherPriorityTaskWoken );//写入队列,如果出现有相应的高优先级的任务退出阻塞进入ready状态,则xHigherPriorityTaskWoken变量将被设为pdTRUE.
} while( INPUT_BYTE( BUFFER_COUNT ) );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken ); //根据xHigherPriorityTaskWorken的值决定是回到该ISR的中断点,还是切换到新的任务里。
}
3、延迟中断处理的方法-将中断中的处理推迟到任务中去
由于一个ISR必须尽可能的短,这样对于一些可能长时间运行的工作,就不适合在ISR中运行,而FreeRTOS的解决办法就是,把这些必须长时间运行的工作移到ISR运行结束后所切换到的任务中去运行。从而保证ISR尽可能的短。这称为“延迟中断处理”,因为中断所需的处理从 ISR“延迟”到任务。
官方文档里的一个示图,很好的说明了这种“将中断中的处理推迟到任务中去”的处理过程。
t1时刻,Task1任务正常运行。
t2时刻,出现一个中断,这时ISR开始运行。ISR任务执行过程中使得task2任务退出阻塞态,进入ready态。
t3时刻,ISR结束,并由开发者选择进行上下文切换,使Task2任务进入运行。这里有个细节,我们在设置Task2时,如果把优先级设置得高于Task1,那么ISR一结束,会因为portYIELD_FROM_ISR( xHigherPriorityTaskWoken ); 函数的调用而直接切换到Task2。如果Task2的优先级并未高于Task1,那么即使调用了portYIELD_FROM_ISR( xHigherPriorityTaskWoken ); Task2就不会马上执行,而只能等到调度器来决定切换到哪个任务。所以正常在使用的时,我们一般会把Task2的优先级高于Task1。
t4时刻,当Task2重新进入阻塞状态后,调度器重新切换到Task1任务执行。
选择“延时中断处理”的场景,一般是中断ISR中只处理与外设的快速通信,而对于读入的数据的运算与处理则放到“被延时的处理任务”中去处理。
如,串口中断负责将串口数据读出并写入到一个队列中,而“延时处理任务”则负责将队列中的数据进行转换运算等需要大量时间才能完成的工作。
4 方法一:用二进制信号量来同步ISR与”延时处理的任务“
上一节讲到可以把一些耗时多的工作放到ISR以外的任务中去完成。这个任务叫做“延时处理任务 ”。具体如何实现这种“中断延时处理”的机制。本节就介绍以二进制信号量来实现ISR与“延时处理任务”的同步。
4.1 二进制信号量
二进制信号量实质就是一个标志。其用法通常是这样的。平常,该标志为空,没有内容。take任务要读取该标志“行为是TAKE”,当该标志为空时则take任务进入阻塞状态。give任务向这个标志写入一个标志,写入后,系统会通知take任务,使take任务退出阻塞状态,进入ready状态。而这个二进制信号量的实现是以队列的方式实现的,二进制信号是相当于一个只有一个元素的队列。
所以二进制信号量常用于同步两个任务或ISR与任务之间的持行顺序。take任务此刻需要在阻塞状态中等待二进制信号量。当一个ISR中断服务程序中发现一个GIVE指令,向二进制信号量写入一个标志。这时会使take任务退出阻塞状态进入ready状态。这样在ISR里就可以直接激活这个take任务了。见下图:
以上就是二进制信号量的使用方法。可以将二进制信号量用来实现在ISR与“延迟处理函数”的同步。
4.2 函数用法
4.2.1 xSemaphoreCreateBinary(void)
描述:
创建一个二进制信号量,并返回一个用于引用信号量的句柄。
返回值:
NULL : 没有创建二进制信号量。
其它非NULL在值: 二进制信号量被成功创建,返回的值是这个二进制信号量的引用句柄 。
举例:
4.2.2 xSemaphoreGive() & xSemaphoreGiveFromISR()
参数:
参数 | 说明 |
xSemaphore | 目标二进制信号量的句柄 |
pxHigherPriorityTaskWoken | 用于决定ISR是否执行上下文切换的标识变量 |
pxHigherPriorityTaskWoken详解:可能有多个任务因为等待一个信号量变为可用而处于阻塞状态。调用xSemaphoreGiveFromISR()可以使信号量可用,从而使这些等待的任务离开Blocked状态。如果调用xSemaphoreGiveFromISR()导致任务离开“阻塞”状态,并且未阻塞的任务的优先级高于或等于当前正在执行的任务(被事件中断前正在执行的任务),则xSemaphreeGivefromISR(()将在内部将*pxHigherPriorityTaskWoken设置为pdTRUE。如果xSemaphoreGiveFromISR()将此值设置为pdTRUE,则应在中断退出之前执行上下文切换。这将确保中断直接返回到最高优先级的就绪状态任务。
返回值
值 | 说明 |
pdTRUE | 函数执行成功 |
errQUEUE_FULL | 如果一个二进制信号量已处于可用(被置位了),则返回该失败值 |
补充说明
在中断服务例程中调用xSemaphoreGiveFromISR()可能会导致正等待获取信号量的任务脱离“阻塞状态”。如果这个脱离阻塞状态的任务的优先级高于或等于当前正在执行的任务(被事件中断前正在执行的任务),则应执行上下文切换。上下文切换将确保中断直接返回到最高优先级的就绪状态任务。
与xSemaphoreGive()API函数不同,xSemaphreeGiveFromISR()本身不会执行上下文切换。它将只指示是否需要上下文切换。
在启动调度程序之前,不得调用xSemaphoreGiveFromISR()。因此,在启动调度程序之前,不允许执行调用xSemaphoreGiveFromISR()的中断。
4.2.3 xSemaphoreTake()
描述:
用于获取此前已经创建的信号量。这个信号量可以是二进制信号量,计数信号量,互斥信号量。
参数
参数 | 说明 |
xSemaphore | 信号量句柄。 |
xTicksToWait | 如果要获取的信号量不可用时,使本任务进入阻塞状态等待的时间 |
返回值
|值|说明|
|pdPASS|当成功获取信号量时,返回pdPASS|
|pdFAIL|返回该值时,说明获取信号量失败|
补充说明:
xSemaphoreTake()只能从正在执行的任务中调用,因此在调度程序处于初始化状态(在启动调度程序之前)时不能调用。
xSemaphoreTake()不能在关键节内或在调度程序挂起时调用
4.3 使用二进制信号量将任务与中断同步实例
此例是官方文档中的一个例子,部份代码为伪代码。重点再于理解用二进制信号量实现的同步逻辑。
此示例使用二进制信号量从中断服务例程中解除处于阻塞中的任务——有效地将任务与中断同步。
例中由一个简单的周期性任务用于每 500 毫秒生成一个软件中断。使用软件中断是为了方便,因为在某些目标环境中挂接到真实中断
很复杂。伪代码中 显示了周期性任务的实现。请注意,该任务在生成中断之前和之后都打印出一个字符串。这允许在执行示例时
产生的输出中观察执行顺序
以下是中断处理程序,在这里,由于向二进制信号量做了GIVE操作,所以会同时使那些正在等待信号量而阻塞的任务(本例为下方的vHandleTask()任务)进入ready状态。
以下是主函数。程序开始运行,先通过主函数分别启动了周期性产生中断的vPriodicTask()任务和中断延迟处理任务vHandlerTask()。注意以下的任务的优先级,vHandlerTask的优无级为3,比其它都高,是为了确保ISR可以立即切换到该任务执行。
运行时序图
示例 使用二进制信号量将任务与中断同步。执行顺序如下:
- 中断发生。
- ISR 执行并“给予”信号量以解除阻塞任务。
- ISR 之后立即执行的任务,并“获取”了信号量。
- vHandlerTask()任务处理了事件,然后再次尝试“获取”信号量——进入Blocked 状态,因为信号量尚不可用(另一个中断已尚未发生)
输出结果
产生了如图51所示的输出。正如预期的那样,一旦中断生成,vHandlerTask()就会进入Running状态,因此任务的输出会分割周期性任务产生的输出(如图Handler task被显示在两个Perodic task中间)。
二进制信号量的弱点
二进制信号量的使用场景一般是中断发生得相对低频的情况下。因为如果太高频,会出现上第一个信号量被take正在任务中处理,第二个信号量已被GIVE,但已无法被take,因为take任务正在处理第一个事件,而这时又一个事件发生,第三个信号量就无法再give,这时新的信号量实际未被GIVE,而是被丢弃了。
5 方法二:使用计数信号量实现ISR与任务的同步
二进制信号量只能使用在中断较低频的场景下,因此很自然的想到,把信号量中记录事件的标志数量增大就可以很好的适应相对高频的使用场景。这里引入了计数信号量。
正如二进制信号量可以被认为是长度为 1 的队列一样,计数信号量也可以被认为是长度大于 1 的队列。任务对存储在队列中的数据不感兴趣——只对队列中的项目数感兴趣。configUSE_COUNTING_SEMAPHORES 必须在FreeRTOSConfig.h 中设置为 1,以便计数信号量可用。
计数信号量常被用于以下两个场景:
1、 计数事件1
在这种情况下,事件处理程序将在每次事件发生时“给予”一个信号量——导致信号量的计数值在每次“给予”时递增。任务每次处理事件时都会“获取”一个信号量——导致信号量的计数值在每次“获取”时递减。计数值是已发生的事件数与已处理的事件数之差。这种机制如图 55 所示。创建用于计数事件的计数信号量,其初始计数值为零。
2、 资源管理。
在这种情况下,计数值表示可用资源的数量。为了获得对资源的控制,任务必须首先获得一个信号量——递减信号量的计数值。当计数值达到零时,没有空闲资源。当任务完成资源后,它会“返回”信号量——增加信号量的计数值。
5.1 xSemaphoreCreateCounting()
描述
创建一个计数信号量,并返回其句柄。
参数
参数 | 说明 |
uxMaxCount | 可以达到的最大计数值。当信号量达到这个值时,它就不能再被“given给予”了。 |
uxInitialCount | 创建信号量时分配给它的计数值。 |
返回值
NULL : 没有分配
非NULL的其它值 : 分配成功,值是该信号量的句柄。
5.2 实例:使用计数信号量将任务与中断同步
通过使用计数信号量代替二进制信号量对示例 4.3 的实现进行了改进。 main() 已更改为包括对xSemaphoreCreateCounting() 的调用,而不是对 xSemaphoreCreateBinary() 的调用。因此修改的代码如下:
xCountingSemaphore = xSemaphoreCreateCounting( 10, 0 ); //创建的是计数信号量,长度为10,初始计数值为0
为了模拟高频发生的多个事件,中断服务程序被更改为每个中断多次“给予”信号量。每个事件都被锁存在信号量的计数值中。修改后的中断服务例程如下 所示。
static uint32_t ulExampleInterruptHandler( void )
{
BaseType_t xHigherPriorityTaskWoken;
xHigherPriorityTaskWoken = pdFALSE;
/*多次“给予”信号量。第一个将解除对延迟中断处理任务的阻塞,下面的“给予”是为了证明信号量锁存事件
以允许中断被延迟到的任务依次处理它们,而不会丢失事件。这模拟了处理器接收到的多个中断,即使在这种
情况下,事件是在单个中断发生中模拟的。 */
xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken ); //根据参数的值决定是否上下文切换到“延迟中断处理任务”
}
其它的代码就省略了,都与示例4.3一样。最后给出运行结果。
6 方法三:将工作推迟到 RTOS 守护进程
到目前为止所介绍的延迟中断处理示例要求应用程序编写者为每个使用延迟处理技术的中断创建一个任务。还可以使用 xTimerPendFunctionCallFromISR()1 API 函数将不能在中断中处理的一些工作推迟到 RTOS 守护程序任务——无需为每个中断创建单独的任务。将中断处理推迟到守护程序任务称为“集中延迟中断处理”。
6.1 RTOS Daemon Task 守护进程
RTOS Daemon Task - RTOS守护程序,是RTOS软件定时器功能系统的主要模块 。我们在创建完任务后,必须调用vTaskStartSchedule()启动定时器,在此期间由调度器自动创建的。用于在系统的每一个tick中断时进行定时器相关的工作,包括执行定时器命令队列的命令,执行定时器的回调函数等。
6.2 发送延迟处理任务到守护进程
上一章描述了与软件定时器相关的 FreeRTOS API 函数如何将命令发送到定时器命令队列上的守护进程任务。
xTimerPendFunctionCall() 和xTimerPendFunctionCallFromISR() API 函数使用相同的计时器命令队列向守护程序任务发送“要推迟的工作”命令。发送给守护任务的函数然后在守护任务的上下文中执行。
xTimerPendFunctionCallFromISR() 是 xTimerPendFunctionCall() 的中断安全版本。
这两个 API 函数都允许应用程序编写者提供的函数(回调函数)由 RTOS 守护程序任务执行,因此也可以在 RTOS 守护程序任务的上下文中执行。要执行的函数和函数的输入参数的值都通过 timer 命令发送到守护程序任务队列。因此,函数实际执行的时间取决于守护程序任务相对于应用程序中其他任务的优先级。
6.3 xTimerPendFunctionCallFromISR()
描述:
从应用程序中断服务例程中使用,将回调函数的执行延迟到RTOS守护程序任务。
理想情况下,中断服务例程(ISR)尽可能短,但有时ISR要么有很多处理要做,要么需要执行不确定的处理。在这些情况下,xTimerPendFunctionCallFromISR()可用于将函数的处理延迟到RTOS后台进程任务。这允许回调函数与中断在时间上连续执行,就像回调在中断本身中执行一样。可以延迟到RTOS守护进程任务的回调函数必须具有以下的原型
参数
参数 | 说明 |
xFunctionToPend | 要从守护程序任务执行的函数。该函数必须符合所示的PendedFunction_t原型。 |
pvParameter1 | 将作为函数的第一个参数传递给回调函数的值。该参数具有void类型,可用于传递任何类型。例如,整数类型可以转换为void,或者void*可以用于指向结构 |
ulParameter2 | 将作为函数的第二个参数传递给回调函数的值。 |
pxHigherPriorityTaskWoken | 调用xTimerPendFunctionCallFromISR()将导致在队列上向RTOS计时器守护进程任务发送消息。如果守护程序任务的优先级(由FreeRTOSConfig.h中的configTIMER_task_priority值设置)高于当前正在运行的任务(中断中断的任务)的优先级,则*pxHigherPriorityTaskWoken将为在xTimerEndFunctionCallFromISR()内设置为pdTRUE,指示在中断退出之前应请求上下文切换。因此,*pxHigherPriorityTaskWoken必须初始化为pdFALSE。 |
返回值
值 | 说明 |
pdPASS | 函数执行成功,命令被成功发送给守护进程 |
任意其它值 | 由于消息队列已满,消息未发送到RTOS守护程序任务。队列的长度由的值设置 FreeRTOSConfig.h中的configTIMER_QUEUE_LENGTH |
注意
FreeRTOSConfig.h 文件中的 INCLUDE_xTimerPendFunctionCall 与 configUSE_TIMERS 宏必须都设置为1, xTimerPendFunctionCallFromISR() 才可用。
6.4 实例
本例 提供了与前例 4.3 类似的功能,但没有使用信号量,也没有创建专门用于执行中断所需处理的任务。相反,处理由 RTOS 守护程序任务执行。示例 的中断服务例程如下所示。它调用 xTimerPendFunctionCallFromISR() 以将指向名为vDeferredHandlingFunction() 的函数的指针传递给守护程序任务。延迟中断处理由vDeferredHandlingFunction() 函数执行。
中断服务例程每次执行时都会增加一个名为 ulParameterValue 的变量。 ulParameterValue 在调用xTimerPendFunctionCallFromISR() 时用作 ulParameter2 的值,因此在守护任务执行vDeferredHandlingFunction() 时也会用作对 vDeferredHandlingFunction() 的调用中的 ulParameter2 的值。该函数的另一个参数 pvParameter1 在此示例中未使用。
static uint32_t ulExampleInterruptHandler( void )
{
static uint32_t ulParameterValue = 0;
BaseType_t xHigherPriorityTaskWoken;
xHigherPriorityTaskWoken = pdFALSE; //必须初始化为pdFALSE
/* 将指向中断延迟处理函数的指针发送给守护程序任务。未使用延迟处理函数的 pvParameter1 参数,因此只需设置
为 NULL。延迟处理函数的 ulParameter2 参数用于传递每次执行此中断处理程序时加一的数字。*/
xTimerPendFunctionCallFromISR( vDeferredHandlingFunction, /* Function to execute. */
NULL, /* Not used. */
ulParameterValue, /* Incrementing value. */
&xHigherPriorityTaskWoken );
ulParameterValue++;
/* 将 xHigherPriorityTaskWoken 值传递给 portYIELD_FROM_ISR()。如果在 xTimerPendFunctionCallFromISR() 运行过程中将
xHigherPriorityTaskWoken 设置为 pdTRUE,则调用 portYIELD_FROM_ISR() 将请求上下文切换(即中断程序执行后后会切换到守护程序)。如果
xHigherPriorityTaskWoken 仍为 pdFALSE,则调用 portYIELD_FROM_ISR() 将无效。 */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
vDeferredHandlingFunction() 的实现如下 所示。它打印出一个固定的字符串,以及它的 ulParameter2 参数的值。
static void vDeferredHandlingFunction( void *pvParameter1, uint32_t ulParameter2 )
{
vPrintStringAndNumber( "Handler function - Processing event ", ulParameter2 );
}
以下是主函数main()的代码
int main( void )
{
/* 生成软件中断的任务的优先终低于守护进程任务。守护进程任务的优先级由FreeRTOSConfig.h中的宏configTIMER_TASK_PRIORITY确定。*/
const UBaseType_t ulPeriodicTaskPriority = configTIMER_TASK_PRIORITY - 1;
/* 创建一个周期性生成软中断的任务。 */
xTaskCreate( vPeriodicTask, "Periodic", 1000, NULL, ulPeriodicTaskPriority, NULL );
/* 以下语句设定了中断对应的ISR */
vPortSetInterruptHandler( mainINTERRUPT_NUMBER, ulExampleInterruptHandler );
vTaskStartScheduler();
/* As normal, the following line should never be reached. */
for( ;; );
}
输出结果
执行时序
1、大部分时间是空闲任务在运行。每500毫秒它被Periodic任务抢占,软件中断产生函数。
2、Periodic任务打印一个信息后生成一个中断。中断服务程序ulExampleInterruptHandler()立即执行。
3、 中断调用 xTimerPendFunctionCallFromISR(),它写入定时器命令队列,导致守护程序任务解除阻塞。然后中断服务程序直接返回到守护任务,因为守护任务是最高优先级的就绪状态任务。守护程序任务在返回阻塞状态以等待另一条消息到达计时器命令队列或软件计时器到期之前打印出它的消息,包括递增的参数值。
4、守护进程执行完成后,Periodic又成为ready状态中的最高优先级的任务,因此继续执行。
7 在中断服务程序中使用队列
7.1 相关函数
从前面的介绍可以发现二进制信号量和计数信号量可用于传达事件。而队列则可用于传递事件和传输数据。
xQueueSendToFrontFromISR() 是可在中断服务例程中安全使用的 xQueueSendToFront() 版本,
xQueueSendToBackFromISR() 是可在中断服务例程中安全使用的 xQueueSendToBack() 版本,
xQueueReceiveFromISR() 是 xQueueReceive() 可以在中断服务例程中安全使用。
参数
参数 | 说明 |
xQueue | 队列句柄 |
pvItemToQueue | 要拷贝给队列的数据的指针 |
pxHigherPriorityTaskWoken | 看前面的介绍 |
返回值
pdPASS : 发送成功
errQUEUE_FULL :队列已满,发送失败,返回该值
7.2 使用 ISR 中的队列时的注意事项
队列提供了一种将数据从中断传递到任务的简单方便的方法,但是如果数据到达的频率很高,则使用队列效率不高。
队列的长度毕竟有限,如果数据传入的频率高于数据读出的速度,则很容易造成写入数据丢失或造成写入阻塞。因此,在ISR中使用队列时必须平稀好这个频率。
7.3 例
此示例演示在同一中断中使用 xQueueSendToBackFromISR() 和 xQueueReceiveFromISR()。和以前一样,为方便起见,中断由软件生成。
一、创建一个周期性任务,每 200 毫秒向队列发送五个数字。只有在发送完所有五个值后,它才会生成软件中断。任务实现如下所示
static void vIntegerGenerator( void *pvParameters )
{
TickType_t xLastExecutionTime;
uint32_t ulValueToSend = 0;
int i;
/* 用vTaskDelayUntil()函数,获得tick计数. */
xLastExecutionTime = xTaskGetTickCount();
for( ;; )
{
/*延迟阻塞200毫秒. */
vTaskDelayUntil( &xLastExecutionTime, pdMS_TO_TICKS( 200 ) );
/* 发送5个数字给队列,这些数值由中断服务程序读取。中断服务程序总能清空队列,因此确保了本任务能够在不阻塞的情况下
一次写5个数据进队列。*/
for( i = 0; i < 5; i++ )
{
xQueueSendToBack( xIntegerQueue, &ulValueToSend, 0 );
ulValueToSend++;
}
/* 打印一个消息后,生成软中断*/
vPrintString( "Generator task - About to generate an interrupt.\r\n" );
vPortGenerateSimulatedInterrupt( mainINTERRUPT_NUMBER );
vPrintString( "Generator task - Interrupt generated.\r\n\r\n\r\n" );
}
}
二、中断服务程序反复调用xQueueReceiveFromISR(),直到周期性任务写入队列的所有值都被读出,队列为空。每个接收到的值的最后两位用作字符串数组的索引。然后使用对 xQueueSendFromISR() 的调用将指向相应索引位置处的字符串的指针发送到不同的队列。中断服务例程的实现如下 所示
static uint32_t ulExampleInterruptHandler( void )
{
BaseType_t xHigherPriorityTaskWoken;
uint32_t ulReceivedNumber;
static const char *pcStrings[] =
{
"String 0\r\n",
"String 1\r\n",
"String 2\r\n",
"String 3\r\n"
};
xHigherPriorityTaskWoken = pdFALSE; //必须初始化为pdFALSE
/* 把队列读到空 */
while( xQueueReceiveFromISR( xIntegerQueue,
&ulReceivedNumber,
&xHigherPriorityTaskWoken ) != errQUEUE_EMPTY )
{
ulReceivedNumber &= 0x03; /*从队列中读到的是数值,只取低三位,用做数组pcStrings的序号 */
xQueueSendToBackFromISR( xStringQueue,
&pcStrings[ ulReceivedNumber ],
&xHigherPriorityTaskWoken );
}
/* 如果从 xIntegerQueue 队列接收数据后导致有与该队列关联的其它任务离开 Blocked 状态,并且离开 Blocked 状态的任务的优先级高于此时处于 Running 状态的任务的优先级,则 xHigherPriorityTaskWoken 将在 xQueueReceiveFromISR() 中设置为 pdTRUE .如果发送到 xStringQueue队列 导致有其它任务任务离开 Blocked 状态,并且如果离开 Blocked 状态的任务的优先级高于处于
Running 状态的任务的优先级,则 xHigherPriorityTaskWoken 将在 xQueueSendToBackFromISR() 中设置为 pdTRUE .
xHigherPriorityTaskWoken 用作 portYIELD_FROM_ISR() 的参数。如果 xHigherPriorityTaskWoken 等于 pdTRUE,则
调用 portYIELD_FROM_ISR() 将请求上下文切换。如果 xHigherPriorityTaskWoken 仍为 pdFALSE,则调用
portYIELD_FROM_ISR() 将无效。 */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
三、从中断服务例程接收字符串指针的任务在队列没有数据时阻塞,直到消息到达,并在收到每个字符串时打印出来。其实现下 所示。
static void vStringPrinter( void *pvParameters )
{
char *pcString;
for( ;; )
{
/* Block on the queue to wait for data to arrive. */
xQueueReceive( xStringQueue, &pcString, portMAX_DELAY );
/* Print out the string received. */
vPrintString( pcString );
}
}
四、主函数
int main( void )
{
xIntegerQueue = xQueueCreate( 10, sizeof( uint32_t ) ); //创建队列
xStringQueue = xQueueCreate( 10, sizeof( char * ) );
/* 创建一个用队列向中断服务程序传递整数的任务,该任务的优先级为1 */
xTaskCreate( vIntegerGenerator, "IntGen", 1000, NULL, 1, NULL );
/* 创建一个任务,把从中断服务程序传送来的这符串打印同来,该任务有较高的优先级2. */
xTaskCreate( vStringPrinter, "String", 1000, NULL, 2, NULL );
/*安装一个软件中断与服务程序的对应关系 */
vPortSetInterruptHandler( mainINTERRUPT_NUMBER, ulExampleInterruptHandler );
/* 调度器开始工作 */
vTaskStartScheduler();
for( ;; );
}
运行结果:
执行时序分析
1、大部分时间,空闲任务处于运行状态,位每200毫秒,IntegerGenerator任务抢占执行一次。
2、IntegerGenerator生成5个数字并写入队列。然后生成一次中断。
3、中断每次从队列中读出一个数值,就对应写一个字符串到另一个队列中。这个写入的队列会使StringPrinter任务退出阻塞进入ready状态。
4、在中断服务程序结束后,StringPrinter成为ready状态下最高优先级的任务,因此马上执行,并从队列中读出字符串进行打印。直到把队列读空后,又再一次进入阻塞状态。这样,优先级比它低的IntegerGenerator任务就继续执行。
5、IntegerGenerator是周期性函数,因此进入阻塞,等下一个周期再次执行。
总结
和其它操作系统一样,中断的处理与协调是FreeRTOS中非常重要的一个部分。系统的中断是必须马上执行的,所以即使FreeRTOS中的最高优先级的任务也必须为中断让路。因此中断的执行时间太长,会影响到FreeRTOS的正常调度。所以FreeRTOS对于中断服务程序的要求是就必须尽可能的短,尽可能在最短的时间里执行完毕。为此,FreeRTOS提供了两个机制来解决这个问题。
1、提供了中断中专用的API,这些API 的函数名都带有FromISR后缀。
2、提供了一个优先的上下文切换机制(把任务延迟到任务的机制),使得ISR执行后可以立刻切换到关联的任务中去执行,以免还要按常规方式返回到中断断点处。这个机制的运用到了xHigherPriorityTaskWoken 变量 和 portYIELD_FROM_ISR( xHigherPriorityTaskWoken );函数。
使用FreeRTOS“把任务延迟到任务”的机制,可以通过二进制信号量,计数信号量,守护进程,队列等多种方式实现。