中断管理
章节介绍和范围
事件
嵌入式实时系统必须对源自环境的事件做出响应。例如,到达以太网外设的数据包(事件)可能需要传递到 TCP/IP 堆栈进行处理(操作)。非琐碎的系统必须服务于来自多个源的事件,所有这些源都有不同的处理开销和响应时间要求。在每种情况下,都必须对最佳事件处理实施策略作出判断:
如何检测事件?通常使用中断,但也可以轮询输入。
当使用中断时,中断服务程序(ISR)内部应执行多少处理,外部应执行多少处理?通常希望每个 ISR尽可能短。
如何将事件与主(非 ISR)代码通信,以及如何构造此代码以最佳地适应潜在异步事件的处理?
FreeRTOS 不会对应用程序设计者强加任何特定的事件处理策略,但确实提供了允许以简单且可维护的方式实施所选策略的功能。
重要的是区分任务的优先级和中断的优先级:
任务是与运行 FreeRTOS 的硬件无关的软件功能。 应用程序编写者在软件中分配任务的优先级,软件算法(调度程序)决定哪个任务将处于运行状态。
尽管是用软件编写的,但中断服务程序是硬件特性,因为硬件控制将运行哪个中断服务程序以及何时运行。 任务只有在没有 ISR 运行时才会运行,所以最低优先级的中断会中断最高优先级的任务,任务没有办法抢占 ISR。
所有运行FreeRTOS的体系结构都能够处理中断,但有关中断条目和中断优先级分配的细节因体系结构而异。
范围
本章旨在让读者更好地理解:
哪些 FreeRTOS API 函数可以在中断服务程序中使用。
将中断处理推迟到任务的方法。
如何创建和使用二进制信号量和计数信号量。
二进制和计数信号量之间的区别。
如何使用队列将数据传入和传出中断服务程序。
一些 FreeRTOS 移植时可用的中断嵌套模型。
从 ISR 使用 FreeRTOS API
中断安全 API
通常需要使用来自中断服务函数 (ISR) 的 FreeRTOS API 函数提供的功能,但许多 FreeRTOS API 函数执行在 ISR 内无效的操作——其中最值得注意的是放置调用 API函数进入阻塞状态; 如果从 ISR 调用 API 函数,则它不是从任务调用的,因此没有调用任务可以置于阻塞状态。 FreeRTOS 通过提供一些 API 函数的两个版本来解决这个问题; 一种用于任务的版本,一种用于 ISR 的版本。 旨在从 ISR 使用的函数在其名称后加上“FromISR
”。
注意:切勿从 ISR 调用名称中不含“
FromISR
”的 FreeRTOS API 函数。
使用单独的中断安全 API 的好处
具有用于中断的单独 API 可以使任务代码更高效,ISR 代码更有效,并且中断输入更简单。 要了解为什么,考虑另一种解决方案,它将提供一个API函数的单一版本,可以从一个任务和一个ISR调用。如果可以从任务和ISR调用相同版本的API函数,则:
API 函数需要额外的逻辑来确定它们是从任务还是从 ISR 调用的。 额外的逻辑会在函数中引入新的路径,使函数变得更长、更复杂、更难测试。
当从任务调用函数时,某些 API 函数参数将过时,而当从 ISR 调用函数时,其他函数参数将过时。
每个 FreeRTOS 移植都需要提供一种机制来确定执行上下文(任务或 ISR)。
难以确定执行上下文(任务或 ISR)的架构将需要额外的、浪费的、使用更复杂的非标准中断入口代码,允许软件提供执行上下文。
使用单独的中断安全 API 的缺点
拥有两个版本的某些 API 函数可以提高任务和 ISR 的效率,但会带来新的问题;有时需要从任务和 ISR 调用不属于 FreeRTOS API 的函数,但需要使用 FreeRTOS API。
这通常只有在集成第三方代码时才会出现问题,因为这是软件设计不受应用程序编写者控制的唯一时候。 如果这确实成为一个问题,那么可以使用以下技术之一来克服该问题:
将中断处理推迟到任务,因此 API 函数只能从任务的上下文中调用。
如果您使用的是支持中断嵌套的 FreeRTOS 移植,则使用以 “
FromISR
” 结尾的 API 函数版本,因为该版本可以从任务和 ISR 中调用(反之则不然,不以 “FromISR
” 结尾的 API 函数 不能从 ISR 调用“FromISR
”)。第三方代码通常包括一个 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 函数的中断安全版本中,上下文切换不会自动发生有几个原因:
避免不必要的上下文切换
在任务需要执行任何处理之前,中断可能会执行多次。 例如,考虑一个任务处理一个由中断驱动的
UART
接收到的字符串的场景; 每次接收到一个字符时,UART ISR
都切换到任务是一种浪费,因为任务只有在接收到完整字符串后才能执行。控制执行序列
中断可能偶尔发生,而且时间不可预测。 专业的 FreeRTOS 用户可能希望暂时避免在其应用程序的特定点不可预测地切换到不同的任务——尽管这也可以使用 FreeRTOS 调度程序锁定机制来实现。
可移植性
它是可以跨所有 FreeRTOS 移植使用的最简单的机制。
效率
面向较小处理器架构的端口仅允许在 ISR 的最后请求上下文切换,而消除该限制将需要额外且更复杂的代码。 它还允许在同一 ISR 内多次调用 FreeRTOS API 函数,而不会在同一 ISR 内生成多个上下文切换请求。
在 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()
都以同样的方式使用,并且做同样的事情。 某些 FreeRTOS 移植仅提供两个宏之一。 较新的 FreeRTOS 移植提供这两种宏。本书中的示例使用 portYIELD_FROM_ISR()
。
清单 87. portEND_SWITCHING_ISR()
宏
清单 88. portYIELD_FROM_ISR()
宏
从中断安全 API 函数传出的 xHigherPriorityTaskWoken
参数可以直接用作调用 portYIELD_FROM_ISR()
中的参数。
如果 portYIELD_FROM_ISR()
xHigherPriorityTaskWoken
参数是 pdFALSE(0),则不请求上下文切换,并且宏不起作用。 如果 portYIELD_FROM_ISR()
xHigherPriorityTaskWoken
参数不是 pdFALSE,则请求上下文切换,并且处于 运行状态的任务可能会更改。 中断将始终返回到处于运行状态的任务,即使在中断执行期间处于运行状态的任务发生了变化。
大多数 FreeRTOS 移植允许在 ISR 内的任何位置调用 portYIELD_FROM_ISR()
。 一些 FreeRTOS 移植(主要用于较小架构的端口)仅允许在 ISR 的最后调用 portYIELD_FROM_ISR()
。
延迟中断处理
保持 ISR 尽可能短通常被认为是最佳实践。 原因包括:
即使任务已被分配非常高的优先级,如果硬件不提供中断,则它们只会运行。
ISR 可以中断(添加“抖动”)任务的开始时间和执行时间。
根据运行 FreeRTOS 的架构,在执行 ISR 时可能无法接受任何新中断,或至少是新中断的子集。
应用程序编写者需要考虑并防范任务和 ISR 同时访问变量、外设和内存缓冲区等资源的后果。
一些 FreeRTOS 移植允许中断嵌套,但中断嵌套会增加复杂性并降低可预测性。中断越短,嵌套的可能性就越小。
中断服务程序必须记录中断的原因,并清除中断。 中断所需的任何其他处理通常可以在任务中执行,从而允许中断服务程序尽可能快地退出。这称为“延迟中断处理”,因为中断所需的处理被从ISR“延迟”到任务。
将中断处理推迟到任务还允许应用程序编写者优先处理相对于应用程序中其他任务的处理,并使用所有 FreeRTOS API 函数。
如果中断处理的任务的优先级高于任何其他任务的优先级,则处理将立即执行,就像处理已在 ISR 本身中执行一样。 这种情况如图 48 所示,其中任务 1 是一个正常的应用程序任务,任务 2 是中断处理被推迟到的任务。
图 48. 高优先级任务中的中断处理
在图 48 中,中断处理从时间 t2 开始,实际上在时间 t4 结束,但只有时间 t2 和 t3 之间的时间段用于 ISR。 如果未使用延迟中断处理,则时间 t2 和 t4 之间的整个时间段都将花费在 ISR 中。
关于何时最好执行 ISR 中的中断所需的所有处理以及何时最好将部分处理推迟到任务,没有绝对的规则。 在以下情况下将处理推迟到任务最有用:
中断所需的处理并非微不足道。 例如,如果中断只是存储模数转换的结果,那么几乎可以肯定这最好在 ISR 内部执行,但如果转换结果还必须通过软件过滤器,那么它可能是最好在任务中执行过滤器。
中断处理可以方便地执行 ISR 内部无法执行的操作,例如写入控制台或分配内存。
中断处理不是确定性的——这意味着事先不知道处理需要多长时间。
以下各节描述并演示了本章到目前为止介绍的概念,包括可用于实现延迟中断处理的 FreeRTOS 功能。
用于同步的二进制信号量
二进制信号量API的中断安全版本可以用于在每次特定中断发生时解除任务阻塞,从而有效地将任务与中断同步。这允许大部分中断事件处理在同步任务中实现,只有非常快和短的部分直接留在ISR中。如前一节所述,二进制信号量用于将中断处理“推迟”到任务。
任务:使用直接到任务的通知将一个任务从中断中解禁,比使用二进制信号量更有效率。直接到任务的通知在第9章 "任务通知 "中才会涉及。
正如之前在图48中所展示的,如果中断处理是特别关键的时间,那么可以设置延迟处理任务的优先级,以确保该任务总是优先于系统中的其他任务。然后,ISR可以被实现,包括对 portYIELD_FROM_ISR()
,确保ISR直接返回到中断处理的任务中。这样做的效果是确保整个事件处理的在时间上连续执行(没有中断),这就像所有的事件都是在ISR本身。图49重复了图48中的情景,但文字被更新为描述如何使用信号量来控制延迟处理任务的执行。
图49. 使用二进制信号来实现延迟的中断处理
延迟处理任务使用一个阻塞的 "拿走" 调用到 信号量 ,作为一种手段进入阻塞状态以等待事件的发生。当事件发生时,ISR在同一信号上使用 "给出" 操作来解除对任务的封锁,这样所需的事件处理可以继续进行。
“接受信号”和“给予信号”是具有不同含义的概念根据它们的使用情况,有不同的含义。在这种中断同步的情况下,二进制信号量在概念上可以被认为是一个长度为1的队列。该队列可以在任何时候都最多包含一个项,所以总是要么是空的,要么是满的(hence, binary)。通过调用 xSemaphoreTake()
,中断处理被推迟到的任务有效地试图用一个阻塞时间从队列中读取,如果队列是空的,则导致任务进入阻塞状态。当事件发生时,ISR使用 xSemaphoreGiveFromISR()
函数将一个令牌(信号量)放入队列,使队列变满。这将导致任务退出阻塞状态并移除令牌,使队列再次变空。当任务完成其处理后,它再次尝试从队列中读出并发现队列是空的,于是重新进入阻塞状态以等待下一个事件。这个序列在图50中展示。
图50显示了中断“给出”信号,尽管它并没有首先“拿走”它,以及任务“拿”走了信号,但没有把它送回去。这就是为什么这种情况被描述为在概念上类似于向队列中写和从队列中读。它经常引起混淆因为它并不遵循与其他信号使用场景相同的规则,在其他场景中,一个任务使用信号量的任务必须一直把它送回来--比如第7章中资源管理描述的场景。
图50. 使用二进制信号量使一个任务与中断同步
xSemaphoreCreateBinary() API函数
FreeRTOS V9.0.0还包括 xSemaphoreCreateBinaryStatic()
函数,该函数在编译时分配内存。所需的内存,以便在编译时静态地创建一个二进制信号:对所有各种类型的FreeRTOS 信号量的句柄都存储在一个SemaphoreHandle_t
类型的变量中。
在使用信号量之前,必须先创建它。要创建一个二进制信号量,请使用 xSemaphoreCreateBinary() API
函数。
一些信号量API函数实际上是宏,而不是函数。为了简单起见,在本书中被称为函数。
清单89. xSemaphoreCreateBinary()
API函数原型
表33. xSemaphoreCreateBinary()
的返回值
xSemaphoreTake() API函数
“取用”信号量是指 "获得 "或 "接收 "该信号量。只有在信号量是可用的情况下才可以采取。
所有不同类型的FreeRTOS信号,除了递归互斥,都可以使用 xSemaphoreTake()
函数。
xSemaphoreTake()
不能从一个中断服务例程中使用。
清单90. xSemaphoreTake()
的API函数原型
表34. xSemaphoreTake()
参数和返回值
xSemaphoreGiveFromISR() API函数
可以使用xSemaphoreGiveFromISR()
函数“给出”二进制和计数信号量。
计数信号量将在本书后面的章节中介绍。
xSemaphoreGiveFromISR()
是 xSemaphoreGive()
的中断安全版本,所以具有pxHigherPriorityTaskWoken
参数,这在本章开始时已经描述过。
清单91. xSemaphoreGiveFromISR()
API函数原型
表35. xSemaphoreGiveFromISR()
参数和返回值
示例16. 使用二进制信号量使一个任务与中断同步
这个例子使用一个二进制信号量来解除一个任务与中断服务程序之间的阻断——有效地使该任务与中断同步。
一个简单的周期性任务被用来每500毫秒产生一个软件中断。一个软件中断是为了方便而使用的,因为在一些目标环境中,连接到一个真正的中断的复杂性,所以使用软件中断是为了方便。清单92显示了周期性任务的实现。请注意,该任务在中断产生前后都会打印出一个字符串。这使得执行的顺序可以在例子执行时产生的输出中被观察到。
清单92. 实现例16中定期生成软件中断的任务
清单93显示了中断处理被推迟的任务的实现——该任务通过使用一个二进制信号量同步。同样,在任务的每次迭代中都会打印出一个字符串,所以任务和中断的执行顺序可以从例子执行时产生的输出中看出来。
清单93. 中断处理所涉及的任务的实现 (与中断同步的任务)。
清单94显示了这个ISR,除了 "给 "信号以解除对中断处理的任务的封锁外,没有什么作用。
注意xHigherPriorityTaskWoken
变量是如何被使用的。在调用xSemaphoreGiveFromISR()
之前,它被设置为pdFALSE,然后在调用portYIELD_FROM_ISR()
时作为参数使用。如果xHigherPriorityTaskWoken
等于pdTRUE,在 portYIELD_FROM_ISR()
宏中会要求进行上下文切换。
与大多数FreeRTOS运行的架构不同,FreeRTOS的Windows端口需要一个ISR来返回一个值。与Windows端口一起提供的 portYIELD_FROM_ISR()
宏的实现包括返回语句,所以清单94没有明确显示返回值。
清单94. 示例16中使用的软件中断的ISR
main()
函数创建了二进制信号量,创建了任务,安装了中断处理程序。处理并启动调度程序。该函数的实现见清单95。
为安装中断处理程序而调用的函数的语法是针对FreeRTOS Windows端口的,对于其他FreeRTOS端口可能有所不同。请参考FreeRTOS.org网站上的特定端口文档页面,以及FreeRTOS下载中提供的例子,以找到你正在使用的端口所需的语法。
清单95. 示例16中 main()
的实现
例16产生的输出如图51所示。正如预期的那样,vHandlerTask()
在中断产生后立即进入了运行状态,所以任务的输出与周期性任务产生的输出相分离。图52中提供了进一步的解释。
图51. 执行示例16时产生的输出
图52. 执行例16时的执行顺序
完善示例16中使用的任务的实现
示例16使用一个二进制信号量来同步一个任务和一个中断。其执行顺序如下:
中断发生了。
ISR执行并“给出”信号量以解除对任务的封锁。
任务在ISR之后立即执行,并“拿走”了信号量。
任务处理了该事件,然后试图再次“拿走”信号量——进入阻塞状态,因为信号量还不可用(另一个中断还没有发生)。
只有当中断发生的频率相对较低时,示例16中的任务结构才是充分的。为了理解这个原因,我们可以考虑一下,如果在任务完成对第一个中断的处理之前,又发生了第二个、第三个中断,会发生什么:
当第二个ISR执行时,信号量将是空的,所以ISR将给出信号量,且任务在完成对第一个事件的处理后将立即处理第二个事件。这种情况在图53中显示。
当第三个ISR执行时,信号量已经可用,防止ISR再次给出信号量,所以任务不会知道第三个事件已经发生。这种情况在图54中显示。
图53. 在任务完成处理第一个事件之前发生一个中断时的情景
图54 在任务完成处理第一个事件之前发生两个中断时的情景
示例16中使用的延迟中断处理任务,如清单93所示,其结构是在每次调用 xSemaphoreTake()
时只处理一个事件。这对示例16来说是足够的,因为产生事件的中断是由软件触发的,而且发生的时间是可预测的。在实际应用中,中断是由硬件产生的,并且发生在不可预测的时间。因此,为了尽量减少错过中断的机会,延迟中断处理任务的结构必须使它在每次调用 xSemaphoreTake()
之间处理所有已经存在的事件
。清单96证明了这一点,它显示了一个UART的延迟中断处理任务的结构。在清单96中,假设UART在每次收到字符时都会产生一个接收中断,并且UART将收到的字符放入一个硬件的 FIFO(一个硬件缓冲区)。
事件:另外,也可以使用计数信号或直接到任务的通知来计数事件。计数信号将在下一节中描述。直接到在第9章,任务的通知中描述。直接到任务的通知是首选方法,因为它们在运行时间和RAM的使用上都是最有效的。
示例16中使用的延迟中断处理任务还有一个弱点;它在调用 xSemaphoreTake()
时没有使用超时。相反,该任务将portMAX_DELAY
作为 xSemaphoreTake()
的xTicksToWait
参数,这导致该任务无限期地等待(没有超时),等待 信号量的出现。无限期超时经常被用在示例代码中,因为它们的使用简化了示例的结构,从而使示例更容易理解。然而,在实际应用中,无限期超时通常是不好的做法,因为它们使人们很难从错误中恢复。作一个例子,考虑这样的情景。一个任务正在等待一个中断来给出 信号量,但是硬件中的错误状态阻止了中断的产生:
如果任务在没有超时的情况下等待,它将不知道错误状态,并将永远等待。
如果任务在等待时有超时,那么
xSemaphoreTake()
将在超时结束时返回pdFAIL,然后任务可以在下次执行时检测并清除错误。这种情况在清单96中也有演示。
清单96. 推荐的延迟中断处理任务的结构,以UART接收处理程序为例
计数信号量
正如二进制信号量可以被认为是长度为1的队列一样,计数信号量可以被认为是长度为1以上的队列。任务对存储在队列中的数据不感兴趣,只是对队列中的项目数量感兴趣。 configUSE_COUNTING_SEMAPHORES
必须在FreeRTOSConfig.h
中设置为1,才能使用计数信号量 。
每当一个 计数信号量 被 "给予 "时,其队列中的另一个空间就被使用。队列中的项目数量是 信号量的“计数”值。
计数事件
在这种情况下,事件处理程序会在每次事件发生时 “给”一个信号量——导致信号的计数值在每次“给出 ”时被递增。 一个任务在每次处理一个事件时都会“拿走”一个信号量——导致信号量的数值被递减。在每次 "取 "的时候都会递减。计数值是已经发生的事件数与已经发生的事件数之间的差值。这个机制是图55中所示。
用于统计事件的计数信号器在创建时,其初始计数值为0。
计数事件:使用直接到任务的通知来计数事件比使用计数信号量更有效。直接到任务的通知在第9章才会涉及。
资源管理
在这种情况下,计数值表示可用资源的数量。为了获得资源的控制权,一个任务必须首先获得一个信号量——减少 信号量的计数值。当计数值达到0时,就没有可用的资源了。当一个任务完成了对资源的控制,它就会“给”回 信号量——增加 信号量的计数值。
用于管理资源的计数信号量在创建时,其初始计数值等于可用资源的数量。第7章介绍了使用信号量来管理资源。
图55. 使用一个计数信号量来 "计数 "事件
xSemaphoreCreateCounting() API函数
FreeRTOS V9.0.0还包括xSemaphoreCreateCountingStatic()
函数,该函数在编译时分配静态地创建一个计数信号量的内存:所有不同类型的FreeRTOS信号的句柄都存储在一个SemaphoreHandle_t
类型的变量中。
在使用信号量之前,必须先创建它。要创建一个计数信号量,请使用 xSemaphoreCreateCounting()
API函数。
清单97. xSemaphoreCreateCounting()
API函数原型
表36. xSemaphoreCreateCounting()
参数和返回值
示例17. 使用计数信号来使一个任务与中断同步
示例17改进了示例16的实现,用binary 信号量代替了计数信号量。main()
被修改为包含一个对 xSemaphoreCreateCounting()
,以取代对 xSemaphoreCreateBinary()
的调用。新的API 调用显示在清单98中。
清单98. 示例17中用于创建计数信号量的 xSemaphoreCreateCounting()
的调用
为了模拟高频率发生的多个事件,中断服务例程被改变为在每个中断中“给予”信号量不止一次。每个事件都被锁在 信号量的计数值中。修改后的中断服务例程显示在清单99中。
清单99. 示例17所使用的中断服务例程的实现
所有其他的函数与示例16中使用的函数相比都没有修改。
示例17执行时产生的输出如图56所示。可以看出,在每次产生中断时,被推迟处理的任务会处理所有三个[模拟]事件。这些事件被锁在信号的计数值中,允许任务依次处理它们。
图56. 执行示例17时产生的输出
推迟工作到RTOS守护进程任务
到目前为止,所介绍的延迟中断处理的例子都要求应用程序编写者为每个使用延迟处理技术的中断创建一个任务。也可以使用xTimerPendFunctionCallFromISR()
API函数将中断处理推迟到RTOS守护任务中——无需为每个中断创建一个单独的任务。将中断处理推迟到守护任务被称为“集中的推迟中断处理”。
xTimerPendFunctionCallFromISR():在第五章中指出,守护任务最初被称为定时器服务任务,因为它最初只用来执行软件定时器回调函数。因此,
xTimerPendFunctionCall()
是在timers.c
中实现的,根据将函数的名称与实现该函数的文件名放在一起的惯例,该函数的名称前缀为Timer
。
第5章描述了与软件定时器有关的FreeRTOS API函数如何在定时器命令队列中向守护任务发送命令。xTimerPendFunctionCall()
和 xTimerPendFunctionCallFromISR()
API函数使用相同的定时器命令队列,向守护任务发送 "执行函数 "命令。发送给守护任务的函数随后在守护任务的上下文中被执行。
集中式延时中断处理的优点包括:
降低资源使用量
它消除了为每个延迟中断创建一个单独任务的需要。
简化的用户模
递延中断处理函数是一个标准的C函数。
集中式延时中断处理的缺点包括:
灵活性较低
不可能单独设置每个递延中断处理任务的优先级。每个递延中断处理功能以守护任务的优先级执行。如第五章所述,守护任务的优先级是由
configTIMER_TASK_PRIORITY
在FreeRTOSConfig.h
中的编译时间配置常数来设置。减少决定论
xTimerPendFunctionCallFromISR()
将一个命令发送到定时器命令队列的后面。在xTimerPendFunctionCallFromISR()
发送给队列的"execute function
"命令之前,已经在定时器命令队列中的命令将被守护任务处理。
不同的中断有不同的时间限制,所以在同一个应用程序中使用两种推迟中断处理的方法是很常见的。
xTimerPendFunctionCallFromISR() API函数
xTimerPendFunctionCallFromISR()
是 xTimerPendFunctionCall()
的中断安全版本。
这两个API函数都允许由应用程序编写者提供的函数由RTOS守护任务执行,因此也是在RTOS守护任务的上下文中执行。要执行的函数和函数的输入参数值都被发送到定时器命令队列中的守护任务。因此,该函数实际执行的时间取决于守护任务相对于应用程序中其他任务的优先级。
清单100. xTimerPendFunctionCallFromISR()
API函数原型
清单101. xTimerPendFunctionCallFromISR()
的xFunctionToPend
参数中传递的函数必须符合的原型。
表37. xTimerPendFunctionCallFromISR()
参数和返回值
示例18. 集中的延迟中断处理
示例18提供了与示例16类似的功能,但没有使用信号量,也没有专门创建一个任务来执行中断所需要的处理。相反,处理是由RTOS守护任务执行的。
示例18所使用的中断服务例程如清单102所示。它调用 xTimerPendFunctionCallFromISR()
,将一个指向vDeferredHandlingFunction()
函数的指针传递给守护任务。延迟的中断处理由vDeferredHandlingFunction()
函数执行。
中断服务程序每次执行时都会增加一个名为 ulParameterValue
的变量。ulParameterValue
在调用xTimerPendFunctionCallFromISR()
时被用作 ulParameter2
的值,因此在守护任务执行vDeferredHandlingFunction()
时也将被用作 ulParameter2
的值。该函数的另一个参数pvParameter1
,在这个例子中没有使用。
清单102. 示例18中使用的软件中断处理程序
vDeferredHandlingFunction()
的实现在清单103中显示。它打印出一个固定的字符串,以及其ulParameter2
的参数值。
vDeferredHandlingFunction()
必须具有清单101中的原型,尽管在这个例子中,只有一个参数被实际使用。
清单103. 执行示例18中中断所需的处理的函数
示例18使用的main()
函数在清单104中显示。它比示例16使用的main()
函数更简单,因为它没有创建一个信号量或一个任务来执行延迟中断处理。
vPeriodicTask()
是周期性产生软件中断的任务。它被创建时的优先级低于守护任务的优先级,以确保一旦守护任务离开阻塞状态,它就会被守护任务抢占。
清单104. 示例18中 main()
的实现
示例18产生的输出如图57所示。守护任务的优先级高于产生软件中断的任务的优先级,所以vDeferredHandlingFunction()
在中断产生后立即被守护任务执行。这导致 vDeferredHandlingFunction()
输出的消息出现在周期性任务输出的两个消息之间,就像用 信号量来解锁专门的延迟中断处理任务时那样。图58中提供了进一步的解释。
图57. 执行示例18时产生的输出
图58 执行示例18时的执行顺序
在中断服务程序中使用队列
二进制和计数信号被用来交流事件。队列被用来交流事件,并传输数据。
xQueueSendToFrontFromISR()
是 xQueueSendToFront()
的版本,可以在中断服务例程中安全使用;xQueueSendToBackFromISR()
是 xQueueSendToBack()
的版本,可以在中断服务例程中安全使用;而xQueueReceiveFromISR()
是 xQueueReceive()
的版本,可以在中断服务例程中安全使用。
xQueueSendToFrontFromISR() 和xQueueSendToBackFromISR() API 函数
清单105. xQueueSendToFrontFromISR()
API函数原型
清单106. xQueueSendToBackFromISR()
API函数原型
xQueueSendFromISR()
和 xQueueSendToBackFromISR()
在功能上等同。
表38. xQueueSendToFrontFromISR()
和 xQueueSendToBackFromISR()
参数和返回值
从ISR中使用队列时的考虑因素
队列提供了一种简单方便的方式将数据从中断传递给任务,但如果数据到达的频率很高,使用队列的效率就不高。
FreeRTOS下载中的许多演示程序包括一个简单的UART驱动程序,它使用一个队列将字符从UART的接收ISR中传递出来。在这些演示程序中,使用队列有两个原因:演示队列在ISR中的使用,以及故意加载系统以测试FreeRTOS端口。以这种方式使用队列的ISR绝对不是为了代表一种有效的设计,除非数据到达的速度很慢,否则建议生产代码不要复制这种技术。更有效的技术,那些适合于生产代码,包括:
使用直接内存访问(DMA)硬件来接收和缓冲字符。这种方法实际上没有软件开销。然后可以使用直接到任务的
通知
来解除对任务的封锁,只有在检测到传输中断后才会处理缓冲区。将收到的每个字符复制到线程安全的RAM
缓冲区
。同样,在收到一个完整的消息后,或在检测到传输中断后,可以使用一个直接到任务的通知来解除对处理缓冲区的任务的封锁。在ISR中直接处理收到的字符,然后使用队列将数据处理的结果(而不是原始数据)发送给一个任务。这在之前由图34演示过。
通知:直接到任务的通知提供了从ISR中解除任务阻塞的最有效方法。直接到任务的通知将在第9章 "任务通知 "中介绍。
缓冲区:作为FreeRTOS+TCP(http://www.FreeRTOS.org/tcp)的一部分提供的 "流缓冲 "可用于此目的。
示例19. 在一个队列中从一个中断中发送和接收信息
这个例子演示了 xQueueSendToBackFromISR()
和 xQueueReceiveFromISR()
在同一个中断中使用。和以前一样,为了方便,中断是由软件产生的。
创建一个周期性任务,每200毫秒向一个队列发送五个数字。只有在所有五个数字都被发送后,它才会产生一个软件中断。该任务的实现在清单107中显示。
清单 107. 示例19中写入队列的任务的实现
中断服务例程反复调用 xQueueReceiveFromISR()
,直到所有由周期性任务写入队列的值都被读出,队列留空。每个收到的值的最后两位被用作一个字符串数组的索引。然后通过调用 xQueueSendFromISR()
,将相应索引位置的字符串指针发送到不同的队列。中断服务例程的实现见清单108。
清单108. 例19所使用的中断服务例程的实现
从中断服务例程中接收字符指针的任务在队列中阻塞,直到有消息到达,在接收到每个字符串时打印出来。它的实现在清单109中显示。
清单109. 打印出从示例19中的中断服务例程收到的字符串的任务
像往常一样,main()
在启动调度程序之前创建所需的队列和任务。它的实现在清单110中显示。
清单110. 示例19的main()
函数
执行示例19时产生的输出如图59所示。可以看出,中断接收了所有五个整数,并产生了五个字符串作为响应。图60中给出了更多的解释。
图59. 执行示例19时产生的输出
图60. 例19产生的执行顺序
中断嵌套
在任务优先级和中断优先级之间出现混淆是很常见的。本节讨论的是中断优先级,即中断服务例程(ISR)执行时相对于对方的优先级。分配给一个任务的优先级与分配给一个中断的优先级没有任何关系。硬件决定ISR的执行时间,而软件决定任务的执行时间。为响应硬件中断而执行的ISR将中断一个任务,但一个任务不能抢先执行ISR。
支持中断嵌套的端口需要在 FreeRTOSConfig.h 中定义表39中详述的一个或两个常量。
configMAX_API_CALL_INTERRUPT_PRIORITY
都定义了同一个属性。旧的 FreeRTOS 端口使用 configMAX_SYSCALL_INTERRUPT_PRIORITY
,而新的 FreeRTOS 端口使用 configMAX_API_CALL_INTERRUPT_PRIORITY
。
表39. 控制中断嵌套的常量
每个中断源都有一个数值优先级,和一个逻辑优先级:
数值优先级
数字优先级只是分配给中断优先级的数字。例如,如果一个中断被分配的优先级为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. 影响中断嵌套行为的常量
参考图61:
当内核或应用程序处于关键部分时,使用优先级1到3(包括)的中断被阻止执行。以这些优先级运行的ISR可以使用中断安全的FreeRTOS API函数。关键部分的描述见第7章。
使用优先级为4或以上的中断不受关键部分的影响,因此在硬件本身的限制下,调度员所做的任何事情都不会阻止这些中断立即执行。以这些优先级执行的ISR不能使用任何 FreeRTOS的API函数。
通常,需要非常严格的时间精度的功能(例如电机控制)会使用高于
configMAX_SYSCALL_INTERRUPT_PRIORITY
的优先级,以确保调度器不会在中断响应时间中引入抖动。
对ARM Cortex-M
和ARM GIC用户的说明
Cortex-M
和ARM GIC用户的说明Cortex-M
处理器上的中断配置是混乱的,而且容易出错。为了帮助你的开发,FreeRTOS Cortex-M
端口自动检查中断配置,但只有在 configASSERT()
被定义的情况下可以。configASSERT()
在第11.2节描述。
Cortex-M:本节仅部分适用于
Cortex-M0
和Cortex-M0+
内核。
ARM Cortex
内核和ARM通用中断控制器(GIC)使用数字上的低优先级数字来表示逻辑上的高优先级中断。这似乎有悖于直觉,而且很容易忘记。如果你想给一个中断分配一个逻辑上的低优先级,那么它必须被分配一个数字上的高值。如果你想给一个中断分配一个逻辑上的高优先级。 那么它就必须被分配一个数字上的低值。
Cortex-M
中断控制器允许最多有8位用于指定每个中断的优先级,使255成为最低的优先级,零是最高优先级。然而,Cortex-M
微控制器通常只实现8个可能的位数。实际实现的位数取决于微控制器系列。
当八个可能的位中只有一个子集被实现时,只有字节中最有意义的位可以被使用--留下最没有意义的位。未实现的位可以取任何值,但把它们设置为1是正常的。 图62展示了二进制101的优先级是如何存储在一个实现了四个优先位的Cortex-M
微控制器中。
图62 二进制101的优先级是如何被一个实现了四个优先位的Cortex-M微控制器存储的
在图62中,二进制值101被移到了最有意义的四个位,因为最没有意义的四个位没有实现。未实现的位被设置为1。
一些库函数希望在优先权值被移到已执行的(最重要的)位子上之后再指定。当使用这样的函数时,图62中所示的优先级可以被指定为十进制95。十进制95是二进制101向上移位4,成为二进制101nnnn(其中"n"是一个未实现的位),未实现的位设置为1,成为二进制1011111。
一些库函数希望在优先权值被移到已执行的(最有意义的)位之前就被指定。当使用这样的函数时,图62中的优先权必须被指定为十进制5。十进制5是二进制101,没有任何移位。
configMAX_SYSCALL_INTERRUPT_PRIORITY
和 configKERNEL_INTERRUPT_PRIORITY
必须以允许它们直接写入Cortex-M
寄存器的方式来指定,所以在优先级值被上移到实现的位后。
configKERNEL_INTERRUPT_PRIORITY
必须始终设置为可能的最低中断优先级。 未实现的优先级位可以被设置为1,所以不管实际实现了多少个优先级位,这个常数总是可以被设置为255。
Cortex-M
中断的默认优先级为0--可能的最高优先级。Cortex-M硬件的实现不允许configMAX_SYSCALL_INTERRUPT_PRIORITY
被设置为0,所以使用FreeRTOS API的中断的优先级决不能停留在其默认值。
Last updated