《Mastering the FreeRTOS Real Time Kernel》读书笔记(5)中断管理

发布时间 2023-10-16 13:35:57作者: BD4RTZ

6.中断管理

在读这一章之前一直有一些疑惑,FreeRTOS中的中断是软中断吗,还是将外部硬中断的触发后,导入FreeRTOS的内部进行调度处理。如果是第一种,软中断和第三章讲的任务有区别吗,还是只是优先级比所有任务高。如果是第二种的话,外部中断的服务函数是不是不能写内容了,FreeRTOS的运行和裸机程序水火不容吗?

6.1 章节介绍

事件

嵌入式实时系统必须对源自环境的事件做出响应。在不同情景下,都必须判断最佳事件处理实现策略:
1.应如何检测事件?通常使用中断,但也可以轮询输入。
2.当使用中断时,中断服务例程(ISR)内部应该执行多少处理,外部应该执行多少?通常希望每个ISR尽可能短。
3.如何将事件传达给主(非ISR)代码,以及如何构建此代码以最好地适应潜在异步事件的处理?

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

  • 任务是一种与运行FreeRTOS的硬件无关的软件功能。任务的优先级由应用程序编写器在软件中分配,软件算法(调度器)决定哪个任务将处于运行状态。
  • 虽然是用软件编写的,但中断服务例程是一种硬件功能,因为硬件控制哪个中断服务例程将运行,以及何时运行。只有当没有ISR运行时,任务才会运行,因此最低优先级的中断将抢占最高优先级的任务,并且任务无法预先占用ISR。

至此,我们开头的问题已经解决了一部分。FreeRTOS使用的是第一种和第二种结合。在触发机制上应该使用的是硬件中断触发,当触发过后,将标志位(可能叫信号量)传递进内核调度器。在执行机制上,类似于软中断,官方明确说明,软中断的优先级比任何任务的优先级都要高,这也印证了猜想。还有的问题就是,其他逻辑程序还允许被设计吗,特别是那些需要时间要求很高的,us级程序。我猜想是不可以的,或者必须协调好裸机程序和RTOS内核之间的关系,所以一个项目是否使用RTOS还是需要再三讨论的。

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

6.2 在ISR中使用的API

许多FreeRTOS API函数执行的操作在ISR内部无效。FreeRTOS通过提供一些API函数的两个版本来解决这个问题;一个版本供任务使用,一个版本由ISR使用。打算从ISR中使用的函数的名称后面附加了“FromISR”。

优点:在中断中使用单独的API可以提高任务代码的效率,提高ISR代码的效率并简化中断条目。
缺点:有时需要从任务和ISR调用一个不属于FreeRTOS API的函数。
1.将中断处理延迟到某一个任务。
2.建议使用以“FromISR”结尾的API函数版本,因为该版本可以从任务和ISR调用。
3.第三方代码通常包括RTOS抽象层,可以实现该层来测试调用函数的上下文(任务或中断)。

xHigherPriorityTaskWoken参数

那么当中断退出时运行的任务可能与输入中断时正在运行的任务不同——中断将中断一个任务,但返回到另一个任务。

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

  • 如果在FreeRTOSConfig.h中将configUSE_PREEMPTION设置为1,则在API函数退出之前,在API函数-so内自动切换到更高优先级的任务。

在中断中不会自动切换到优先级更高的任务。相反,设置了一个变量来通知应用程序编写器应该执行上下文切换。中断安全API函数(以“FromISR”结尾的函数)有一个名为pxHigherPriorityTaskWoken的指针参数,用于此目的。

  • 如果要执行上下文切换,那么中断安全API函数会将*pxHigherPriorityTaskWoken设置为pdTRUE。为了能够检测到这种情况的发生,pxHigherPriorityTaskWoken指向的变量在第一次使用之前必须初始化为pdFALSE。

FreeRTOS API函数只能将*pxHighPriorityTaskWoken设置为pdTRUE。如果一个ISR调用多个FreeRTOS API函数,那么在每个API函数调用中都可以传递相同的变量作为pxHigherPriorityTaskWoken参数,并且该变量只需要在首次使用之前初始化为pdFALSE。

如果只使用API函数的中断安全版本,上下文切换不会自动发生。

portYIELD_FROM_ISR()和portEND_SWITCHING_ISR()宏

用于从ISR请求上下文切换的宏。

taskYIELD()是一个宏,可以在任务中调用它来请求上下文切换。portYIELD_FROM_ISR()和portEND_SWITCHING_ISR()都是taskYIELD()的中断安全版本。

portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );

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

6.3 尽量延迟中断的相关处理

即不要在RTOS的中断中进行大批量的处理函数,可以将其转移到任务中。

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

将中断处理延迟到任务还允许应用程序编写器相对于应用程序中的其他任务对处理进行优先级排序,并允许使用所有FreeRTOS API函数。如果中断处理延迟到的任务的优先级高于任何其他任务的优先级,则该处理将立即执行,就像该处理是在ISR本身中执行的一样,还更加安全。

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

二进制信号量用于将中断处理“推迟”到task,通俗的讲,就是标志位传递。

接受信号量”和“给予信号量”这两个概念根据其使用场景而具有不同的含义。在这个中断同步场景中,二进制信号量在概念上可以被认为是一个长度为1的队列。

通过调用xSemaphoreTake(),中断处理被延迟到的任务有效地尝试以块时间从队列中读取,如果队列为空,则导致任务进入“阻止”状态。

当事件发生时,ISR使用xSemaphoreGiveFromISR()函数将一个令牌(信号量)放入队列中,使队列充满。这将导致任务退出“阻止”状态并删除令牌,使队列再次为空。

当任务完成处理后,它再次尝试从队列中读取,发现队列为空,则重新进入“阻止”状态以等待下一个事件。

创建二进制信号量

SemaphoreHandle_t xSemaphoreCreateBinary( void );

SemaphoreHandle_t类型的变量中。在使用信号量之前,必须先创建它。要创建二进制信号量,请使用xSemaphoreCreateBinary()API函数。

获取信号量

xSemaphoreTake()API函数“Taking”信号量表示“获取”或“接收”信号量。只有当信号量可用时,才能使用它。除了递归互斥之外,所有不同类型的FreeRTOS信号量都可以使用xSemaphoreTake()函数“获取”。xSemaphoreTake()不能从中断服务例程中使用。

参数解释与从队列中获取的函数参数类似。

例子16

先在中断中“赋值”一个二进制信号量,退出中断。当中断服务函数的任务检测到这个二进制信号量后,便开始运行中断服务函数。

示例16中使用的延迟中断处理任务具有另一个弱点;它在调用xSemaphoreTake()时没有使用超时。相反,任务将portMAX_DELAY作为xSemaphoreTake()xTicksToWait参数传递,这导致任务无限期地等待(没有超时)信号量可用。在实际工程中,不建议设置无限制时间超时。也可以参考list96中的以实际计数代替超时。

6.5 计数信号量

计数信号量也可以被认为长度超过1的队列。任务对存储在队列中的数据不感兴趣,只对队列中的项目数感兴趣。

在FreeRTOSConfig.h中,configUSE_COUNTING_SEMAPHORES必须设置为1才能计数可用的信号量。每次“给定”计数信号量时,都会使用其队列中的另一个空间。队列中的项数是信号量的“count”值。

1.计事件的数量
每次事件发生时,事件处理程序都会“给定”一个信号量,导致信号量的计数值在每次“给定”时递增。任务每次处理事件时都会“获取”信号量,导致信号量的计数值在每次“获取”时递减。计数值是已发生的事件数与已处理的事件数之间的差值。

2.统计资源
在这种情况下,计数值表示可用资源的数量。为了获得对资源的控制,任务必须首先获得信号量——递减信号量的计数值。当计数值达到零时,就没有可用资源。当任务使用资源完成时,它会“返回”信号量——增加信号量的计数值。

所有各种类型的FreeRTOS信号量的句柄都存储在SemaphoreHandle_t类型的变量中。在使用信号量之前,必须先创建它。要创建计数信号量,请使用xSemaphoreCreateCount()API函数。

SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount );

1.uxMaxCount:信号量将计数到的最大值。
2.uxInitialCount:信号量创建后的初始计数值。

例子17就是将例子16中的普通二进制信号量替换为计数信号量,并在一次中断中计数三次,通过实验可知,中断服务程序也执行了三次。

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

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

再此之前,还请读者充分理解守护任务的含义和作用。

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

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

BaseType_t xTimerPendFunctionCallFromISR( PendedFunction_t xFunctionToPend,
                                          void *pvParameter1,
                                          uint32_t ulParameter2,
                                          BaseType_t *pxHigherPriorityTaskWoken );

例子18是这种集中式延迟中断处理的实现方式。

6.7 在中断中使用队列函数

主要介绍了队列相关函数在中断中使用的安全版本,但是文章不建议在中断中使用队列相关函数。

BaseType_t xQueueSendToFrontFromISR( QueueHandle_t xQueue,
                                      void *pvItemToQueue
                                      BaseType_t *pxHigherPriorityTaskWoken
                                    );

BaseType_t xQueueSendToBackFromISR( QueueHandle_t xQueue,
                                      void *pvItemToQueue
                                      BaseType_t *pxHigherPriorityTaskWoken
                                  );

参数解释与非中断版本的解释相同。

例子19:在中断中,发送和接受一个队列。

6.8 中断嵌套

支持中断嵌套的端口需要在FreeRTOSConfig.h.中定义。较旧的FreeRTOS端口使用configMAX_SYSCALL_INTERRUPT_PRIORITY,较新的FreeRTOS端口使用configMAX_API_CALL_INTERRUPT.PRIORITY。
这边设置中断优先级的设置,和设置任务优先级的设置类似,可以参考第三章相关部分。

一共7级中断优先级,设置configMAX_SYSCALL_INTERRUPT_priority为3.
1、2、3级别的中断(低优先级):当内核或应用程序位于关键部分时,将阻止使用优先级1到3(包括1到3)的中断执行。以这些优先级运行的ISR可以使用中断安全的FreeRTOS API函数。
4、5、6、7级别的中断(高优先级):使用这些优先级的中断永远不会因为内核正在做的任何事情而延迟,可以嵌套,但不能使用任何FreeRTOS API函数。

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

注意:Cortex-M处理器上的中断配置令人困惑,并且容易出错。为了帮助您的开发,FreeRTOS Cortex-M端口会自动检查中断配置,但前提是已定义configASSERT()。configASSERT()在第11.2节中进行了描述。
ARM Cortex内核和ARM通用中断控制器(GC)使用数字低优先级数字来表示逻辑高优先级中断。这似乎有违直觉,而且很容易忘记。如果您希望为中断分配逻辑上较低的优先级,则必须为其分配数字上较高的值。如果您希望为中断分配逻辑上高的优先级,则必须为其分配数字上低的值。
除此之外,还有一些由于简化带来的问题,具体参考原文。

总结

粗看中断这章,前半部分有些难懂的名词,提醒可以不用看懂。后半部分的例子实现起来比较理想,倒是理解起来比较轻松。但是中断是微控制器的精髓,如此简单的实现必然不符合实际用途,后面不免还要多来翻看、重读此章节。