FreeRTOS —— 6.中断管理

6.1本章介绍与范围

事件

嵌入式实时系统必须采取措施以响应源自环境的事件。 例如,到达以太网外围设备的数据包(事件)可能需要传递到TCP / IP堆栈进行处理(操作)。 非平凡的系统将不得不处理源自多个来源的事件,所有这些事件都将具有不同的处理开销和响应时间要求。 在每种情况下,都必须对最佳事件处理实施策略做出判断:

1.如何检测事件? 通常使用中断,但也可以查询输入。
2.使用中断时,应在中断服务程序(ISR)内执行多少处理,而在中断服务程序外进行多少处理? 通常希望使每个ISR尽可能短。
3.如何将事件传达给主要(非ISR)代码,以及如何构造该代码以最好地适应潜在的异步事件的处理?

FreeRTOS并未在应用程序设计器上强加任何特定的事件处理策略,但确实提供了允许以简单且可维护的方式实现所选策略的功能。

区分任务的优先级和中断的优先级很重要:

  • 任务是与运行FreeRTOS的硬件无关的软件功能。 任务的优先级由应用程序编写器在软件中分配,并且软件算法(调度程序)确定哪个任务将处于“运行”状态。

  • 尽管中断服务程序是用软件编写的,但它是一项硬件功能,因为硬件控制着哪个中断服务程序将运行以及何时运行。 任务仅在没有ISR运行时运行,因此优先级最低的中断将中断优先级最高的任务,并且任务无法抢占ISR。

FreeRTOS将在其上运行的所有体系结构都能够处理中断,但是与中断进入和中断优先级分配有关的详细信息在体系结构之间有所不同。

范围

本章旨在使读者更好地理解:

  • 可以在中断服务程序中使用哪些FreeRTOS API函数。
  • 将中断处理推迟到任务的方法。
  • 如何创建和使用二进制信号量以及对信号量进行计数。
  • 二进制和计数信号量之间的差异。
  • 如何使用队列将数据传入和传出中断服务程序。
  • 一些FreeRTOS端口可用的中断嵌套模型。

6.2从ISR使用FreeRTOS API

安全的中断API

通常,有必要使用来自中断服务例程(ISR)的FreeRTOS API函数提供的功能,但是许多FreeRTOS API函数执行的操作在ISR内部无效,其中最引人注目的是放置称为“任务”的任务。 API函数进入Blocked状态; 如果从ISR调用了API函数,则不是从任务中调用它,因此没有可以置于“已阻塞”状态的调用任务。 FreeRTOS通过提供一些API函数的两个版本来解决此问题。 一种用于任务的版本,一种用于ISR的版本。 供ISR使用的功能的名称后方附加了“ FromISR”。

注意:切勿从ISR调用名称中没有“ FromISR”的FreeRTOS API函数。

使用单独的中断安全API的好处

具有用于中断的单独的API,可以使任务代码更高效,ISR代码更高效,并且中断输入更简单。 要了解原因,请考虑替代解决方案,该解决方案本来是可以从任务和ISR调用的每个API函数提供一个单独的版本。 如果可以同时从任务和ISR调用相同版本的API函数,则:

  • API函数将需要其他逻辑来确定是从任务还是从ISR调用它们。 附加逻辑将通过功能引入新的路径,从而使功能更长,更复杂且更难测试。
  • 如果从任务中调用该函数,则某些API函数参数将被废弃,而从ISR调用该函数时,某些API函数参数将被废弃。
  • 每个FreeRTOS端口都需要提供一种机制来确定执行上下文(任务或ISR)。
  • 不容易确定执行上下文(任务或ISR)的体系结构,使用起来需要额外的,浪费的,更复杂的,以及非标准的中断入口代码,来允许提供执行上下文。

使用单独的中断安全API的缺点

具有某些API函数的两个版本可以使任务和ISR更加高效,但会带来一个新问题。 有时有必要从任务和ISR中调用不属于FreeRTOS API而是使用FreeRTOS API的函数。

  1. 将中断处理推迟到task1,因此仅从任务上下文调用API函数。
  2. 如果您使用的是支持中断嵌套的FreeRTOS端口,请使用以“ FromISR”结尾的API函数版本,因为可以从任务和ISR中调用该版本(反之则不然,可以执行此操作的API函数 不能以“ FromISR”结尾不能从ISR调用)。
  3. 第三方代码通常包括一个RTOS抽象层,可以实现该层以测试从中调用函数(任务或中断)的上下文,然后调用适合该上下文的API函数。

xHigherPriorityTaskWoken参数

本节介绍xHigherPriorityTaskWoken参数的概念。 如果您还没有完全理解本节,请不要担心,因为以下各节提供了实际示例。

如果上下文切换是由中断执行的,则中断退出时运行的任务可能与进入中断时正在运行的任务不同-中断将中断一个任务,但返回到另一个任务。

一些FreeRTOS API函数可以将任务从“阻止”状态转移到“就绪”状态。 xQueueSendToBack()之类的功能已经看到了这一点,如果有一个任务处于“阻塞”状态等待数据在主题队列上可用,它将取消阻塞任务。

如果不受FreeRTOS API函数阻止的任务的优先级高于处于“运行”状态的任务的优先级,则根据FreeRTOS调度策略,应切换到更高优先级的任务。 切换到更高优先级的任务实际发生的时间取决于调用API函数的上下文:

  • 如果从任务中调用了API函数
    如果在FreeRTOSConfig.h中将configUSE_PREEMPTION设置为1,则切换到更高优先级的任务会自动在API函数中发生-因此在退出API函数之前。 在图43中已经看到了这一点,其中写入计时器命令队列导致在退出写入命令队列的函数之前切换到RTOS守护程序任务。
  • 如果API函数是从中断中调用的
    在中断内不会自动发生切换到更高优先级的任务。 而是设置一个变量来通知应用程序编写者应该执行上下文切换。 中断安全API函数(以“ FromISR”结尾的函数)具有一个称为pxHigherPriorityTaskWoken的指针参数,用于此目的。
    如果应该执行上下文切换,则中断安全API函数会将* pxHigherPriorityTaskWoken设置为pdTRUE。 为了能够检测到这种情况,在首次使用之前,必须将pxHigherPriorityTaskWoken指向的变量初始化为pdFALSE。
    如果应用程序编写者选择不向ISR请求上下文切换,则优先级较高的任务将保持在“就绪”状态,直到调度程序下次运行时为止-最坏的情况是在下一个滴答中断期间。
    FreeRTOS API函数只能将* pxHighPriorityTaskWoken设置为pdTRUE。 如果ISR调用了多个FreeRTOS API函数,则可以在每个API函数调用中将相同的变量作为pxHigherPriorityTaskWoken参数传递,并且只需要在首次使用该变量之前将其初始化为pdFALSE。

有几种原因导致在API函数的中断安全版本内不会自动发生上下文切换:

  1. 避免不必要的上下文切换
    在任务必须执行任何处理之前,中断可能会执行多次。 例如,考虑一个任务处理一个由中断驱动的UART接收的字符串的情况。 每次接收到一个字符时,UART ISR切换到该任务都是浪费的,因为该任务只有在接收到完整的字符串之后才能执行处理。

  2. 控制执行顺序
    中断可能偶尔发生,并且发生在不可预测的时间。 FreeRTOS专家用户可能希望暂时避免在应用程序的特定位置上无法预测地切换到其他任务,尽管这也可以使用FreeRTOS调度程序锁定机制来实现。

  3. 便携性
    这是可以在所有FreeRTOS端口上使用的最简单的机制。

  4. 效率
    以较小的处理器体系结构为目标的端口仅允许在ISR的最后请求上下文切换,而取消该限制将需要更多且更复杂的代码。 它还允许在同一ISR中对FreeRTOS API函数进行多次调用,而无需为同一ISR中的上下文切换生成多个请求。

  5. 在RTOS滴答中断中执行
    如本书稍后所见,可以将应用程序代码添加到RTOS滴答中断中。 尝试在滴答中断中进行上下文切换的结果取决于所使用的FreeRTOS端口。 充其量,这将导致对调度程序的不必要的调用。

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

portYIELD_FROM_ISR()和portEND_SWITCHING_ISR()宏

本节介绍了用于从ISR请求上下文切换的宏。 如果您还没有完全理解本节,请不要担心,因为以下各节提供了实际示例。

taskYIELD()是可以在任务中调用以请求上下文切换的宏。 portYIELD_FROM_ISR()和portEND_SWITCHING_ISR()都是taskYIELD()的中断安全版本。 portYIELD_FROM_ISR()和portEND_SWITCHING_ISR()都以相同的方式使用,并且执行相同的操作2。 某些FreeRTOS端口仅提供两个宏之一。 较新的FreeRTOS端口提供了两个宏。 本书中的示例使用portYIELD_FROM_ISR()。

在这里插入图片描述
从中断安全API函数传递的xHigherPriorityTaskWoken参数可以直接用作对portYIELD_FROM_ISR()的调用中的参数。

如果portYIELD_FROM_ISR()xHigherPriorityTaskWoken参数为pdFALSE(零),则不请求上下文切换,并且该宏无效。 如果portYIELD_FROM_ISR()xHigherPriorityTaskWoken参数不是pdFALSE,则请求上下文切换,并且处于“运行”状态的任务可能会更改。 即使在中断执行期间处于运行状态的任务发生更改,中断也将始终返回到正在运行状态的任务。

大多数FreeRTOS端口都允许在ISR中的任何位置调用portYIELD_FROM_ISR()。 一些FreeRTOS端口(主要是用于较小体系结构的端口)仅允许在ISR的最后调用portYIELD_FROM_ISR()。

6.3 延迟中断处理

通常认为最佳做法是使ISR尽可能短。 原因包括:

  • 即使为任务分配了很高的优先级,它们也只会在硬件不提供中断服务的情况下运行。
  • ISR可能会破坏任务的开始时间和执行时间(增加“抖动”)。
  • 根据运行FreeRTOS的体系结构,在执行ISR时,可能无法接受任何新的中断或至少新中断的一个子集。
  • 应用程序编写者需要考虑任务和ISR同时访问的资源(例如变量,外围设备和内存缓冲区)的后果并加以防范。
  • 一些FreeRTOS端口允许中断嵌套,但是中断嵌套会增加复杂性并降低可预测性。 中断时间越短,嵌套的可能性就越小。

中断服务程序必须记录中断原因,并清除中断。 中断所需的任何其他处理通常都可以在任务中执行,从而允许中断服务例程尽快退出。 这称为“延迟的中断处理”,因为中断所需的处理是从ISR到任务的“延迟”。

将中断处理推迟到任务,还允许应用程序编写者相对于应用程序中的其他任务优先处理,并使用所有FreeRTOS API函数。

如果推迟中断处理的任务的优先级高于任何其他任务的优先级,则将立即执行该处理,就像该处理是在ISR本身中执行的一样。 图48中显示了这种情况,其中任务1是正常的应用程序任务,任务2是将中断处理推迟到的任务。

在这里插入图片描述

在图48中,中断处理在时间t2开始,并有效地在时间t4结束,但是在ISR中仅花费了时间t2和t3之间的时间。 如果未使用延迟的中断处理,则时间t2和t4之间的整个时间段将用在ISR中。

对于何时最佳执行ISR中的中断所必需的所有处理,以及何时最佳地将部分处理推迟给任务,没有绝对的规则。 在以下情况下,将处理推迟到任务中最有用:

  • 中断所必需的处理并非易事。 例如,如果中断只是存储模数转换的结果,则几乎可以肯定,这最好在ISR内部执行,但是如果转换结果也必须通过软件过滤器传递,则可能是 最好在任务中执行过滤器。
  • 对于中断处理而言,执行在ISR内部无法执行的操作(如写入控制台或分配内存)非常方便。
  • 中断处理不是确定性的,即事先不知道该处理将花费多长时间。

以下各节描述并演示了本章到目前为止引入的概念,包括可用于实现延迟中断处理的FreeRTOS功能。

6.4 用于同步的二进制信号量

二进制信号量API的中断安全版本可以使用各个特定中断发生时疏通任务,有效地同步与中断任务。 这允许大多数中断事件处理在同步任务中实现,而只有非常短的部分直接保留在ISR中。 如前一节所述,二进制信号量用于“更好”地对任务3进行中断处理。

如之前的图48所示,如果中断处理的时间特别紧迫,则可以设置延迟处理任务的优先级,以确保该任务始终抢占系统中的其他任务。 然后可以将ISR实现为包括对portYIELD_FROM_ISR()的调用,以确保ISR直接返回到将中断处理推迟到的任务。 这具有确保整个事件处理在时间上连续执行(不间断)的效果,就好像所有事件处理都是在ISR本身中实现的一样。 图49重复了图48中所示的场景,但是对文本进行了更新,以描述如何使用信号量控制延迟处理任务的执行。

在这里插入图片描述
延迟的处理任务使用对信号量的阻止“接听”调用作为进入“阻止”状态以等待事件发生的一种方式。 事件发生时,ISR对相同的信号量使用“给予”操作来解除阻止任务,以便可以继续进行所需的事件处理。

“使用信号量”和“提供信号量”是根据使用情况而具有不同含义的概念。在此中断同步方案中,二进制信号量在概念上可以视为长度为1的队列。队列在任何时候最多可以包含一个项目,因此始终为空或已满(因此为二进制)。通过调用xSemaphoreTake(),有效地将中断处理推迟到的任务尝试以阻塞时间从队列中读取,如果队列为空,则使该任务进入“阻塞”状态。事件发生时,ISR使用xSemaphoreGiveFromISR()函数将令牌(信号量)放入队列中,从而使队列已满。这将导致任务退出“已阻止”状态并删除令牌,从而使队列再次为空。任务完成处理后,它将再次尝试从队列中读取数据,并发现队列为空,请重新进入阻塞状态以等待下一个事件。图50演示了此序列。

图50显示了中断“给予”信号量,即使它没有先“获取”信号量,任务也“获取”了信号量,但从未将其返回。 这就是为什么该场景在概念上类似于写入队列和从队列读取的原因。 它经常引起混乱,因为它没有遵循与其他信号量使用方案相同的规则,在这种情况下,使用信号量的任务必须始终将其归还给您,例如第7章“资源管理”中描述的方案。

在这里插入图片描述

xSemaphoreCreateBinary()API函数

FreeRTOS V9.0.0还包含xSemaphoreCreateBinaryStatic()函数,该函数在编译时静态分配创建二进制信号量所需的内存:各种FreeRTOS信号量的句柄都存储在SemaphoreHandle_t类型的变量中。

在使用信号量之前,必须先创建它。 要创建二进制信号量,请使用xSemaphoreCreateBinary()API函数4

在这里插入图片描述
表33. xSemaphoreCreateBinary()返回值

参数名称描述
Returned value如果返回NULL,则无法创建信号,因为FreeRTOS没有足够的堆内存来分配信号数据结构。返回的非NULL值表示信号灯已成功创建。 返回的值应存储为创建的信号量的句柄。

xSemaphoreTake()API函数

“接收”信号灯意味着“获取”或“接收”信号灯。 信号量只有在可用时才能使用。

可以使用xSemaphoreTake()函数“获取”除递归互斥锁以外的所有各种FreeRTOS信号量。

不得在中断服务程序中使用xSemaphoreTake()。

在这里插入图片描述
表34. xSemaphoreTake()参数和返回值

参数名称/返回值描述
xSemaphore信号灯被“取”。信号量由SemaphoreHandle_t类型的变量引用。 必须先明确创建它,然后才能使用它。
xTicksToWait如果该信号尚不可用,则该任务应保持在“阻塞”状态以等待该信号灯的最长时间。如果xTicksToWait为零,则在信号量不可用时,xSemaphoreTake()将立即返回。堵塞时间以滴答周期指定,因此它表示的绝对时间取决于滴答频率。 宏pdMS_TO_TICKS()可用于将以毫秒为单位的时间转换为以滴答数为单位的时间。
Returned value有两个可能的返回值:1. pdPASS 仅当对xSemaphoreTake()的调用成功获取信号量时,才返回pdPASS。 如果指定了阻止时间(xTicksToWait不为零),则可能无法立即将调用任务置于“阻塞”状态,以等待信号量,但该信号量在阻止时间到期之前变为可用。2. pdFALSE 信号灯不可用。如果指定了阻塞时间(xTicksToWait不为零),则调用任务将被置于“阻塞”状态,以等待信号量变为可用,但是阻塞时间在此之前已过期。

xSemaphoreGiveFromISR()API函数

可以使用xSemaphoreGiveFromISR()函数“给定”二进制和计数信号量5

xSemaphoreGiveFromISR()是xSemaphoreGive()的中断安全版本,本章开始部分介绍的pxHigherPriorityTaskWoken参数也是如此。

在这里插入图片描述

参数名称/返回值描述
xSemaphore信号量被“给予”。信号量由SemaphoreHandle_t类型的变量引用,必须在使用前明确创建。
pxHigherPriorityTaskWoken单个信号量可能会阻塞一个或多个任务,以等待该信号量变为可用。 调用xSemaphoreGiveFromISR()可以使信号灯可用,并因此导致正在等待信号灯的任务离开阻塞状态。 如果调用xSemaphoreGiveFromISR()导致任务退出“已阻止”状态,并且未阻止的任务的优先级高于当前正在执行的任务(被中断的任务),则内部,xSemaphoreGiveFromISR()会将* pxHigherPriorityTaskWoken设置为pdTRUE。如果xSemaphoreGiveFromISR()将此值设置为pdTRUE,则通常应在退出中断之前执行上下文切换。 这将确保中断直接返回到最高优先级的就绪状态任务。
Returned value有两个可能的返回值:1. pdPASS 仅当成功调用xSemaphoreGiveFromISR()时,才会返回pdPASS。2. pdFAIL 如果信号灯已经可用,则无法给出该信号灯,并且xSemaphoreGiveFromISR()将返回pdFAIL。

例子16.使用二进制信号量使任务与中断同步

本示例使用二进制信号量从中断服务例程中取消阻止任务-有效地将任务与中断同步。

一个简单的定期任务用于每500毫秒生成一次软件中断。 由于在某些目标环境中挂接到实际中断的复杂性,因此使用软件中断是为了方便。 清单92显示了定期任务的实现。 请注意,该任务在生成中断之前和之后都打印出一个字符串。 这样可以在执行示例时在输出中观察执行顺序。

在这里插入图片描述

清单93显示了将中断处理推迟到的任务的实现-通过使用二进制信号量将该任务与软件中断同步。 同样,在任务的每次迭代中都会打印出一个字符串,因此从执行示例时所产生的输出中可以明显看出任务和中断的执行顺序。

应该注意的是,清单93中所示的代码对于示例16是足够的,在示例16中,中断是由软件生成的,但对于由硬件外围设备生成中断的场景来说,是不够的。 以下小节介绍如何更改代码的结构以使其适合与硬件生成的中断一起使用。

在这里插入图片描述

清单94显示了ISR。 除了“提供”信号量以解除阻止延迟处理的任务之外,这几乎没有其他作用。

注意如何使用xHigherPriorityTaskWoken变量。 在调用xSemaphoreGiveFromISR()之前将其设置为pdFALSE,然后在调用portYIELD_FROM_ISR()时用作参数。 如果xHigherPriorityTaskWoken等于pdTRUE,则将在portYIELD_FROM_ISR()宏内请求上下文切换。

ISR的原型以及用于强制进行上下文切换的宏均适用于FreeRTOS Windows端口,而对于其他FreeRTOS端口则可能不同。 请参阅FreeRTOS.org网站上特定于端口的文档页面,以及FreeRTOS下载中提供的示例,以查找所用端口所需的语法。

与大多数运行FreeRTOS的体系结构不同,FreeRTOS Windows端口需要ISR返回值。 Windows端口随附的portYIELD_FROM_ISR()宏的实现包括return语句,因此清单94没有显示明确返回的值。

在这里插入图片描述

main()函数创建二进制信号量,创建任务,安装中断处理程序,并启动调度程序。 清单95显示了该实现。

用来安装中断处理程序的函数的语法特定于FreeRTOS Windows端口,对于其他FreeRTOS端口可能有所不同。 请参阅FreeRTOS.org网站上特定于端口的文档页面,以及FreeRTOS下载中提供的示例,以查找所用端口所需的语法。

在这里插入图片描述
例16产生了如图51所示的输出。正如预期的那样,一旦产生中断,vHandlerTask()就会进入运行状态,因此任务的输出会分割周期性任务产生的输出。 图52提供了进一步的说明。

在这里插入图片描述

在这里插入图片描述
改进示例16中使用的任务的实现
示例16使用二进制信号量将任务与中断同步。 执行顺序如下:

1.发生中断。

2.执行ISR和“送”的信号解除封锁任务。

3.任务在ISR之后立即执行,并“获取”信号量。

4.任务处理了事件,然后尝试再次“获取”信号量-由于信号量尚不可用(尚未发生另一个中断),因此进入“阻止”状态。

仅当中断以相对较低的频率发生时,示例16中使用的任务的结构才足够。 要了解原因,请考虑如果在任务完成对第一个中断的处理之前发生了第二个然后是第三个中断,将会发生什么情况:

  • 当第二个ISR执行时,信号量将为空,因此ISR将给出信号量,并且该任务将在完成对第一个事件的处理后立即处理第二个事件。 该场景如图53所示。
  • 当执行第三个ISR时,该信号量将已经可用,从而阻止了ISR再次提供该信号量,因此任务将不知道发生了第三个事件。 该场景如图54所示。
    在这里插入图片描述
    在这里插入图片描述

示例16中使用的延迟中断处理任务(如清单93所示)的结构使其每次调用xSemaphoreTake()时仅处理一个事件。 这对于示例16足够了,因为产生事件的中断是由软件触发的,并且发生在可预测的时间。 在实际应用中,中断是由硬件产生的,并且发生在不可预测的时间。 因此,为了最大程度地减少错过中断的机会,必须构造延迟的中断处理任务,以便它处理xSemaphoreTake()6的每次调用之间已经可用的所有事件。 清单96证明了这一点,该清单显示了如何构造UART的延迟中断处理程序。 在清单96中,假设UART每次接收到一个字符时都会产生一个接收中断,并且UART将接收到的字符放入硬件FIFO(硬件缓冲区)中。

例16中使用的延迟中断处理任务还有另一个弱点。 调用xSemaphoreTake()时,它没有使用超时。 而是,任务将portMAX_DELAY作为xSemaphoreTake()xTicksToWait参数传递,这导致任务无限期(无超时)等待信号量可用。 在示例代码中经常使用不确定的超时,因为它们的使用简化了示例的结构,因此使示例更易于理解。 但是,在实际应用程序中,不确定的超时通常是不好的做法,因为它们使从错误中恢复很困难。 例如,请考虑以下情形:任务正在等待中断以发出信号,但是硬件中的错误状态阻止了中断的产生:

  • 如果任务正在等待而没有超时,则它将不知道错误状态,并将永远等待。

  • 如果任务正在等待超时,则xSemaphoreTake()将在超时到期时返回pdFAIL,然后任务可以在下一次执行时检测并清除错误。 清单96中也演示了这种情况。

在这里插入图片描述

6.5信号量计数

正如可以将二进制信号量视为长度为1的队列一样,将计数信号量视为长度大于1的队列。 任务对队列中存储的数据不感兴趣,仅对队列中的项目数感兴趣。 必须在FreeRTOSConfig.h中将configUSE_COUNTING_SEMAPHORES设置为1,以对信号量进行计数。

每次“发出”计数信号量时,都会在队列中使用另一个空间。 队列中的项目数是信号量的“计数”值。

计数信号量通常用于两件事:

1.计数事件6
在这种情况下,事件处理程序将在每次事件发生时“给予”信号量-导致信号量的计数值在每次“给予”时增加。任务在每次处理事件时都会“获取”信号量-导致每次“获取”时信号量的计数值都会减少。计数值是已发生的事件数与已处理的事件数之间的差。这种机制如图55所示。

用于计数事件的计数信号量的初始计数值为零。

2.资源管理。
在这种情况下,计数值指示可用资源的数量。要获得对资源的控制权,任务必须首先获得一个信号量-减少该信号量的计数值。当计数值达到零时,将没有可用资源。当任务使用资源完成时,它“返回”信号量,从而增加了信号量的计数值。

创建用于管理资源的计数信号量,以使它们的初始计数值等于可用资源的数量。 第7章介绍使用信号量来管理资源。

在这里插入图片描述

xSemaphoreCreateCounting()API函数

FreeRTOS V9.0.0还包括xSemaphoreCreateCountingStatic()函数,该函数在编译时静态分配创建计数信号量所需的内存:各种FreeRTOS信号量的句柄存储在SemaphoreHandle_t类型的变量中。

在使用信号灯之前,必须先创建它。 要创建计数信号量,请使用xSemaphoreCreateCounting()API函数。
在这里插入图片描述

表36. xSemaphoreCreateCounting()参数和返回值

参数名称/返回值描述
uxMaxCount信号量将计数到的最大值。 为了继续进行队列类比,uxMaxCount值实际上是队列的长度。当信号量用于计数或锁存事件时,uxMaxCount是可以锁存的最大事件数。当信号量用于管理对资源集合的访问时,应将uxMaxCount设置为可用资源的总数。
uxInitialCount信号量创建后的初始计数值。当使用信号量来计数或锁存事件时,应将uxInitialCount设置为零,因为大概是在创建信号量时,尚未发生任何事件。当使用信号量来管理对资源集合的访问时,应将uxInitialCount设置为等于uxMaxCount-就像在创建信号量时一样,所有资源均可用。
Returned value如果返回NULL,则无法创建信号量,因为FreeRTOS没有足够的堆内存来分配信号量数据结构。 第2章提供了有关堆内存管理的更多信息。返回的非NULL值表示信号灯已成功创建。 返回的值应存储为创建的信号量的句柄。

例17.使用计数信号量将任务与中断同步

示例17通过使用计数信号量代替二进制信号量对示例16的实现进行了改进。 main()已更改为包括对xSemaphoreCreateCounting()的调用,而不是对xSemaphoreCreateBinary()的调用。 清单98中显示了新的API调用。
在这里插入图片描述
为了模拟在高频下发生的多个事件,更改了中断服务程序,以使每个中断多次给出信号量。 每个事件都锁在信号量的计数值中。 清单99显示了修改后的中断服务例程。

在这里插入图片描述

所有其他功能均与示例16中使用的功能保持不变。

执行示例17时产生的输出如图56所示。可以看出,每次产生中断时,将中断处理推迟到的任务都会处理所有三个[模拟]事件。 事件被锁存到信号量的计数值中,从而使任务可以依次处理它们。

6.6将工作推迟到RTOS守护程序任务

到目前为止提供的延迟中断处理示例要求应用程序编写者为每个使用延迟处理技术的中断创建一个任务。 也可以使用xTimerPendFunctionCallFromISR()7 API函数将中断处理推迟到RTOS守护程序任务,从而不必为每个中断创建单独的任务。 将中断处理延迟到守护程序任务称为“集中式延迟中断处理”。

第5章介绍了与软件计时器相关的FreeRTOS API函数如何将命令发送到计时器命令队列上的守护程序任务。 xTimerPendFunctionCall()和xTimerPendFunctionCallFromISR()API函数使用相同的计时器命令队列将“执行函数”命令发送到守护程序任务。 发送到守护程序任务的函数然后在守护程序任务的上下文中执行。

集中式延迟中断处理的优点包括:

  • 降低资源使用
    这样就无需为每个延迟的中断创建单独的任务。

  • 简化的用户模型
    延迟中断处理功能是标准的C函数。

集中式延迟中断处理的缺点包括:

  • 灵活性较差
    不可能分别设置每个延迟中断处理任务的优先级。 每个延迟的中断处理功能均以守护程序任务的优先级执行。 如第5章所述,守护程序任务的优先级由FreeRTOSConfig.h中的configTIMER_TASK_PRIORITY编译时间配置常量设置。

  • 确定性欠佳
    xTimerPendFunctionCallFromISR()将命令发送到计时器命令队列的后面。 计时器命令队列中已经存在的命令将由守护程序任务处理,然后由xTimerPendFunctionCallFromISR()发送“执行功能”命令到队列中。

不同的中断具有不同的时序约束,因此在同一应用程序中同时使用两种延迟处理的方法是很常见的。

xTimerPendFunctionCallFromISR()API函数

xTimerPendFunctionCallFromISR()是xTimerPendFunctionCall()的中断安全版本。 这两个API函数都允许应用程序编写器提供的功能可以由RTOS守护程序任务执行,因此可以在RTOS守护程序任务的上下文中执行。 将要执行的功能以及该功能的输入参数的值都发送到计时器命令队列上的守护程序任务。 因此,函数实际执行的时间取决于守护程序任务相对于应用程序中其他任务的优先级。

在这里插入图片描述

表37. xTimerPendFunctionCallFromISR()参数和返回值

参数名称/返回值描述
xFunctionToPend指向将在守护程序任务中执行的函数的指针(实际上就是函数名称)。 该函数的原型必须与清单101中所示的相同。
pvParameter1将作为守护程序任务的pvParameter1参数传递给守护程序执行的函数的值。 该参数具有void *类型,以使其可以用于传递任何数据类型。 例如,可以将整数类型直接转换为void *,也可以使用void *指向结构。
ulParameter2将作为守护程序任务的ulParameter2参数传递给守护程序任务执行的函数的值。
pxHigherPriorityTaskWokenxTimerPendFunctionCallFromISR()写入计时器命令队列。 如果RTOS守护程序任务处于等待状态,以等待计时器命令队列中的数据可用,则写入计时器命令队列将导致守护程序任务退出“阻止”状态。 如果守护程序任务的优先级高于当前正在执行的任务(被中断的任务)的优先级,则内部将设置xTimerPendFunctionCallFromISR()* pxHigherPriorityTask唤醒为pdTRUE。如果xTimerPendFunctionCallFromISR()将此值设置为pdTRUE,则必须在退出中断之前执行上下文切换。 这将确保中断直接返回到守护程序任务,因为守护程序任务将是最高优先级的就绪状态任务。
Returned value有两个可能的返回值:1. pdPASS 如果将“执行功能”命令写入计时器命令队列,则将返回pdPASS。 2. pdFAIL 如果由于计时器命令队列已满而无法将“执行功能”命令写入计时器命令队列,则将返回pdFAIL。 第5章介绍如何设置计时器命令队列的长度。

例子18.集中的延迟中断处理

示例18提供了与示例16类似的功能,但是没有使用信号灯,也没有创建专门执行中断所必需的处理的任务。 而是由RTOS守护程序任务执行处理。

清单102中显示了示例18使用的中断服务例程。它调用xTimerPendFunctionCallFromISR()将指向vDeferredHandlingFunction()函数的指针传递给守护程序任务。 延迟中断处理由vDeferredHandlingFunction()函数执行

中断服务例程在每次执行时都会递增一个名为ulParameterValue的变量。 ulParameterValue用作对xTimerPendFunctionCallFromISR()的调用中的ulParameter2的值,因此当守护程序任务执行vDeferredHandlingFunction()时,也将用作对vDeferredHandlingFunction()的调用中的ulParameter2的值。 在此示例中,未使用该函数的其他参数pvParameter1。

在这里插入图片描述

清单103中显示了vDeferredHandlingFunction()的实现。它打印出一个固定的字符串及其ulParameter2参数的值。

vDeferredHandlingFunction()必须具有清单101所示的原型,即使在此示例中,实际上仅使用了其参数之一。

在这里插入图片描述

清单104中显示了示例18使用的main()函数。它比示例16使用的main()函数更简单,因为它既不创建信号量也不执行延迟中断处理的任务。

vPeriodicTask()是定期生成软件中断的任务。 它的创建优先级低于守护程序任务的优先级,以确保它在守护程序任务离开“阻塞”状态后立即被守护程序任务抢占。

在这里插入图片描述

示例18产生如图57所示的输出。守护程序任务的优先级高于生成软件中断的任务的优先级,因此,在生成中断后,守护程序任务将立即执行vDeferredHandlingFunction()。 这导致vDeferredHandlingFunction()输出的消息出现在定期任务输出的两条消息之间,就像使用信号量来解除阻止专用的延迟中断处理任务时一样。 图58提供了进一步的说明。

在这里插入图片描述

在这里插入图片描述

6.7在中断服务程序中使用队列

二进制和计数信号量用于传达事件。 队列用于通信事件和传输数据。

xQueueSendToFrontFromISR()是可以在中断服务例程中安全使用的xQueueSendToFront()版本,xQueueSendToBackFromISR()是可以在中断服务例程中安全使用的xQueueSendToBack()版本,xQueueReceiveFromISR()是 在中断服务例程中可以安全使用的xQueueReceive()。

xQueueSendToFrontFromISR()和xQueueSendToBackFromISR()API函数

在这里插入图片描述
xQueueSendFromISR()和xQueueSendToBackFromISR()在功能上是等效的。

表38. xQueueSendToFrontFromISR()和xQueueSendToBackFromISR()参数和返回值

参数名称/返回值描述
xQueue将数据发送到的队列的句柄。 队列句柄将从用于创建队列的xQueueCreate()调用返回。
pvItemToQueue指向将被复制到队列中的数据的指针。 创建队列时,将设置队列可容纳的每个项目的大小,因此会将这许多字节从pvItemToQueue复制到队列存储区域。
pxHigherPriorityTaskWoken一个队列可能会阻塞一个或多个任务,以等待数据可用。 调用xQueueSendToFrontFromISR()或xQueueSendToBackFromISR()可以使数据可用,并因此导致此类任务退出“已阻止”状态。 如果调用API函数导致任务退出“已阻止”状态,并且未阻止任务的优先级高于当前正在执行的任务(被中断的任务),则API函数会在内部将* pxHigherPriorityTaskWoken设置为pdTRUE。如果xQueueSendToFrontFromISR()或xQueueSendToBackFromISR()将此值设置为pdTRUE,则应在退出中断之前执行上下文切换。 这将确保中断直接返回到最高优先级的就绪状态任务。
Returned value有两个可能的返回值:1. pdPASS 仅当数据已成功发送到队列时,才返回pdPASS。 2. errQUEUE_FULL 如果由于队列已满而无法将数据发送到队列,则返回errQUEUE_FULL。

从ISR使用队列时的注意事项

队列提供了一种将数据从中断传递到任务的简便方法,但是如果数据到达的频率很高,则使用队列效率不高。

FreeRTOS下载中的许多演示应用程序都包含一个简单的UART驱动程序,该驱动程序使用队列将字符从UART的接收ISR中传递出去。 在这些演示中,出于两个原因使用队列:演示ISR正在使用的队列,并故意加载系统以测试FreeRTOS端口。 以这种方式使用队列的ISR绝对不是要代表高效的设计,除非数据到达的速度变慢,否则建议生产代码不要复制该技术。 适用于生产代码的更有效的技术包括:

  • 使用直接内存访问(DMA)硬件来接收和缓冲字符。 这种方法实际上没有软件开销。 然后,仅在检测到传输中断后,才可以使用直接到任务通知8来解除阻塞将处理缓冲区的任务。
  • 将每个接收到的字符复制到线程安全的RAM缓冲区9中。 同样,可以使用直接任务通知来取消阻塞将在接收到完整消息后或检测到传输中断后处理缓冲区的任务。
  • 直接在ISR中处理接收到的字符,然后使用队列仅将处理数据(而不是原始数据)的结果发送到任务。 先前由图34证明了这一点。

例子19.从中断中发送和接收队列

此示例演示了在同一中断中使用的xQueueSendToBackFromISR()和xQueueReceiveFromISR()。 和以前一样,为方便起见,中断是由软件生成的。

创建一个定期任务,该任务每200毫秒将五个数字发送到一个队列。 仅在发送所有五个值之后,它才会生成软件中断。 任务实现如清单107所示。

在这里插入图片描述

中断服务例程会重复调用xQueueReceiveFromISR(),直到已读取了由定期任务写入队列的所有值,并将队列留空。 每个接收值的最后两位用作字符串数组的索引。 然后,使用对xQueueSendFromISR()的调用将指向相应索引位置处的字符串的指针发送到其他队列。 清单108中显示了中断服务例程的实现。

在这里插入图片描述

从中断服务程序接收字符指针的任务在
排队直到消息到达,并在接收到每个字符串时将其打印出来。 清单109显示了它的实现。

在这里插入图片描述

通常,main()在启动调度程序之前会创建所需的队列和任务。 清单110显示了它的实现。

在这里插入图片描述
执行示例19时产生的输出如图59所示。可以看出,中断接收所有五个整数,并产生五个字符串作为响应。 图60给出了更多解释。
在这里插入图片描述
在这里插入图片描述

6.8中断嵌套

任务优先级和中断优先级之间经常会出现混淆。 本节讨论中断优先级,即中断服务程序(ISR)相对于彼此执行的优先级。 分配给任务的优先级与分配给中断的优先级没有任何关系。 硬件决定ISR何时执行,而软件决定任务何时执行。 响应硬件中断而执行的ISR将中断任务,但是任务无法抢占ISR。

支持中断嵌套的端口需要在FreeRTOSConfig.h中定义表39中详细说明的一个或两个常量。 configMAX_SYSCALL_INTERRUPT_PRIORITY和configMAX_API_CALL_INTERRUPT_PRIORITY都定义相同的属性。 较旧的FreeRTOS端口使用configMAX_SYSCALL_INTERRUPT_PRIORITY,较新的FreeRTOS端口使用configMAX_API_CALL_INTERRUPT_PRIORITY。

表39.控制中断嵌套的常量

常数描述
configMAX_SYSCALL_INTERRUPT_PRIORITY or configMAX_API_CALL_INTERRUPT_PRIORITY设置最高的中断优先级,从中可以调用中断安全的FreeRTOS API函数。
configKERNEL_INTERRUPT_PRIORITY设置滴答中断使用的中断优先级,并且必须始终将其设置为最低的中断优先级。如果正在使用的FreeRTOS端口也未使用configMAX_SYSCALL_INTERRUPT_PRIORITY常量,那么任何使用中断安全的FreeRTOS API函数的中断也必须以configKERNEL_INTERRUPT_PRIORITY定义的优先级执行。

每个中断源都有一个数字优先级和一个逻辑优先级:

  • 数字优先级
    数字优先级只是分配给中断优先级的数字。 例如,如果将中断的优先级分配为7,则其数字优先级为7。同样,如果将中断的优先级分配为200,则其数字优先级为200。
  • 逻辑优先级
    中断的逻辑优先级描述了该中断的优先级高于其他中断。
    如果同时发生两个优先级不同的中断,则处理器将为两个中断中优先级较高的中断执行ISR,然后再为两个中断中较低优先级执行ISR。
    中断可以中断(嵌套)具有较低逻辑优先级的任何中断,但是中断不能中断(嵌套)具有相同或更高逻辑优先级的任何中断。

中断的数字优先级和逻辑优先级之间的关系取决于处理器体系结构。 在某些处理器上,分配给中断的数字优先级越高,则该中断的逻辑优先级越高;而在其他处理器体系结构上,分配给中断的数字优先级越高,该中断的逻辑优先级就越低。

通过将configMAX_SYSCALL_INTERRUPT_PRIORITY设置为比configKERNEL_INTERRUPT_PRIORITY更高的逻辑中断优先级,可以创建完整的中断嵌套模型。 图61对此进行了演示,该图显示了一个场景:

  • 处理器具有七个独特的中断优先级。

  • 分配给数字优先级为7的中断的逻辑优先级高于分配给数字优先级为1的中断的逻辑优先级。

  • configKERNEL_INTERRUPT_PRIORITY设置为1。

  • configMAX_SYSCALL_INTERRUPT_PRIORITY设置为3。
    在这里插入图片描述
    参见图61:

  • 当内核或应用程序位于关键区域内时,将阻止执行使用优先级1至3(包括1和3)的中断。 以这些优先级运行的ISR可以使用中断安全的FreeRTOS API函数。 关键部分在第7章中进行了描述。

  • 使用优先级4或更高级别的中断不受关键部分的影响,因此调度程序所做的任何事情都不会阻止这些中断在硬件本身的限制内立即执行。 以这些优先级执行的ISR不能使用任何FreeRTOS API函数。

  • 通常,需要非常严格的定时精度的功能(例如,电动机控制)将使用高于configMAX_SYSCALL_INTERRUPT_PRIORITY的优先级,以确保调度程序不会在中断响应时间中引入抖动。

致ARM Cortex-M10和ARM GIC用户的注意事项

Cortex-M处理器上的中断配置令人困惑,并且容易出错。 为了帮助您进行开发,FreeRTOS Cortex-M端口会自动检查中断配置,但前提是已定义configASSERT()。 configASSERT()在第11.2节中介绍。

ARM Cortex内核和ARM通用中断控制器(GIC)使用低优先级数字表示逻辑上高优先级的中断。 这似乎违反直觉,并且很容易忘记。 如果希望将中断分配为逻辑低优先级,则必须为其分配较高的数字值。 如果希望将中断分配为逻辑高优先级,则必须为其分配较低的数字值。

Cortex-M中断控制器最多允许使用八个位来指定每个中断优先级,使255为最低可能的优先级。 零是最高优先级。 但是,Cortex-M微控制器通常仅实现八个可能位的子集。 实际实现的位数取决于微控制器系列。

当仅实现了八个可能位的子集时,仅可以使用字节的最高有效位,而未实现最低有效位。 未实现的位可以取任何值,但是将它们设置为1是正常的。图62演示了这一点,该图显示了二进制文件101的优先级如何存储在实现四个优先级位的Cortex-M微控制器中。

在这里插入图片描述
在图62中,因为未实现最低有效的四位,所以二进制值101已移入最高有效的四位。 未实现的位已设置为1。

一些库函数期望在将优先级值上移至已实现(最高有效)位后指定优先级值。 使用这种功能时,可以将图62中所示的优先级指定为十进制95。十进制95是二进制101向上移四以形成二进制101nnnn(其中“ n”是未实现的位),并且未实现的位设置为1 制作二进制文件1011111。

一些库函数希望在将优先级值上移到已实现(最高有效)位之前指定优先级值。 使用这种功能时,必须将图62所示的优先级指定为十进制5。十进制5是二进制101,无任何移位。

必须以允许将它们直接写入Cortex-M寄存器的方式指定configMAX_SYSCALL_INTERRUPT_PRIORITY和configKERNEL_INTERRUPT_PRIORITY,因此,在将优先级值上移至实现的位之后。

configKERNEL_INTERRUPT_PRIORITY必须始终设置为最低的中断优先级。 可以将未实现的优先级位设置为1,因此无论实际实现了多少个优先级位,常量都可以始终设置为255。

Cortex-M中断的默认优先级为零-可能的最高优先级。 Cortex-M硬件的实现不允许将configMAX_SYSCALL_INTERRUPT_PRIORITY设置为0,因此使用FreeRTOS API的中断的优先级绝不能保持其默认值。


  1. 延迟中断处理将在本书的下一部分中介绍。 ↩︎

  2. 历史上,portEND_SWITCHING_ISR()是FreeRTOS端口中使用的名称,该名称要求中断处理程序使用汇编代码包装器,而portYIELD_FROM_ISR()是FreeRTOS端口中使用的名称,该名称允许将整个中断处理程序用C编写。 ↩︎

  3. 与使用二进制信号量相比,使用直接到任务的通知从中断中取消阻止任务的效率更高。 在第9章“任务通知”之前,不会直接介绍任务通知。 ↩︎

  4. 一些Semaphore API函数实际上是宏,而不是函数。 为简单起见,在本书中将它们全部称为函数。 ↩︎

  5. 计数信号量将在本书的后续部分中介绍。 ↩︎

  6. 使用直接执行任务通知来计数事件比使用计数信号量更为有效。 直到第9章才涉及直接针对任务的通知。 ↩︎ ↩︎

  7. 在第5章中注意到,守护程序任务最初被称为计时器服务任务,因为它最初仅用于执行软件计时器回调函数。 因此,xTimerPendFunctionCall()是在timers.c中实现的,并且根据在函数名称前加上实现该函数的文件的名称的约定,该函数的名称以“ Timer”作为前缀。 ↩︎

  8. 直接任务通知提供了从ISR取消阻塞任务的最有效方法。 第9章,任务通知中介绍了直接执行任务通知。 ↩︎

  9. 作为FreeRTOS + TC(http://www.FreeRTOS.org/tcp)的一部分提供的“流缓冲区”可用于此目的。 ↩︎

  10. 本部分仅部分适用于Cortex-M0和Cortex-M0 +内核。 ↩︎

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值