如果我对本翻译内容享有所有权。允许任何人复制使用本文章,不会收取任何费用。如有平台向你收取费用与本人无任何关系
第六章 . 中断管理
章节介绍和范围
事件
嵌入式实时系统必需对环境中的事件做出响应。比如,外部网络设备收到一个发送给TCP/IP栈等待处理的包。非传统的系统会有来自不同源的服务事件,这些服务事件有不同的处理头和响应时间要求。因此,一个好的事件处理实现应当遵循下面策略:
- 如何检测这个事件?通常是使用中断,但也可以轮询输入。
- 当使用中断时,中断服务程序(ISR)中应当处理多少具体工作,中断服务程序外又处理多少工作?通常要尽量使中断服务程序短小。
- 中断服务程序(ISR)如何同非中断处理程序通信,这些代码如何组织,最好地适应潜在的异步事件?
FreeRTOS没有给开发者强加一些特别的事件处理策略,但也没有提供可以选择实现简单或维护方便策略的功能。
下面介绍一下任务优先级和中断优先级的区别:
- 任务是FreeRTOS软件功能,和硬件无关。任务优先级是开发者指定的,软件的算法决定了当前应当运行那个任务。
- 不同于软件指定,中断处理程序是硬件功能,因此运行那个,何时运行中断服务程序是硬件控制的。任务只会在没有ISR运行时运行,因为最低优先级的硬件中断都会中断最高优先级的任务,一个任务是不能抢占ISR的。
所有FreeRTOS可以运行的平台上都可使用硬件中断,但中断进入细节不同,不同的平台上硬件中断优先级设计也各有不同。
范围
本章是为了教会你:
- 中断服务程序中可以使用那些FreeRTOS函数
- 将中断处理程序延迟到任务中处理
- 如何创建一个二进制信号量和普通信号量
- 二进制信号量和普通信号量的区别
- 如何用队列将数据传进或传出中断服务程序
- 一些FreeRTOS端口中可以使用中断嵌套模型
ISR中使用FreeRTOS函数
中断安全函数
经常需要在ISR中使用FreeRTOS提供的函数,但其中的很多不能在ISR中使用,因为它们是给任务使用的会引进调用者进入阻塞;如果在ISR中调用一个FreeRTOS函数,而不是一个任务,这里就没有可以让它进行阻塞状态的任务。FreeRTOS是以一些函数提供2个版本来解决这个问题,一个版本用于任务,另一个用于ISR。ISR版本的函数会有"FromISR"后缀。
注意不要在中断处理程序中调用没有"FromISR"后缀的函数。
使用中断安全函数的好处
中断中对函数有区分,可以让任务和ISR代码更高效。中断也更简单。考虑如下代替方案,提供一个任务和ISR都可以调用的版本,那么这个版本:
- 这个版本的函数就要判断调用来自中断还是任务。这些增加的逻辑会通过函数引入新的路径,使函数更长,更复杂,更难于测试。
- 调用者是任务时要包含一些任务参数,调用者是ISR是又要包含另外的参数。
- 每个FreeRTOS函数都要提供决定执行上正文的机制。
- 对于不容易确定执行上正文的结构会要更多的附加代码,浪费,更复杂的用法,和不标准的中断进入代码。这里指的是允许在程序中控制执行上正文。
使用分开中断安全函数的缺点
一些函数有2个版本的会更高效,但也引入了一个新的问题;有时候在任务和ISR中都要调用一个非FreeRTOS函数。
这通常是引入第三方代码时都会出现,因为那是唯一程序员不能控制的。如果这成为一个问题,那么可以用下面的方法解决:
- 将中断处理程序中的操作推迟到任务中,这时就只在任务中使用它了。
- 如果你用的FreeRTOS端口支持中断嵌套,那么可以使用带"FromISR"的版本,因为这个版本ISR和任务中都可以使用。相反没有"FromISR"的版本就只能用在任务中了,不能用在ISR中。
- 第3方代码通常包含一个实时系统抽象层,可以用于测试当前上下方是任务还是中断,就可以调用这个函数确定当前上正文状态。
xHigherPriorityTaskWoken参数
这一节介绍了xHigherPriorityTaskWoken的概念。如果你还不是完全理解这一节内容也不要担心,下面提供了实际例子。
如果一个中断处理程序中改变了上正文,那么中断退出执行时返回的任务可能和中断打断的任务不是同一个任务,中断打断一个任务,但会返回不同的任务。
一些FreeRTOS函数可以将任务状态从阻塞态改变为就绪态。比如之前的xQueueSendToBack(),它会将一个在阻塞状态,等待子队列上数据可用的任务解除阻塞。
如果被FreeRTOS解除阻塞的任务的优先级高于当前运行的任务,那么以FreeRTOS调度器原则,会将当前运行任务切换为最高优先级任务。什么时候发生切换最高优先级任务依赖于具体调用函数上下文:
- 如果函数是一个任务调用的
如果FreeRTOSConfig.h中的configUSE_PREEMPTION
设置为1,那么在调用这个函数时就会切换到最高优先级任务运行,就是会在这个函数退出前切换。图43中已经可以看出来,在那里向队列写数据引起切换到实时系统守护任务在这个写入数据到队列函数返回之前。
- 如果函数是在一个中断中调用(通常会调用中断安全版本的函数)
中断处理程序中不会发生切换到高优先及任务的操作。作为替换,会设置一个变量用于提醒程序开发者有一个上下文切换应当处理。那些中断安全函数,就是带"FromISR"后缀的函数有一个指针参数,叫做"xHigherPriorityTaskWoken"就是用来做这个提醒的。
如果存在一个上下文切换要处理,那么中断安全版本的函数就会设置"xHigherPriorityTaskWoken"为pdTRUE。为了可以检测到它的发生,在第一次使用它之前必需要将它初始化为pdFALSE。
如果在ISR中不需要切换上下文,那么在下次调度器运行之前,最高优先级任务会保持在就绪状态,最坏的情况是要等到下一个tick周期。
FreeRTOS函数只能设置"xHigherPriorityTaskWoken"一次。如果ISR调用超过一个FreeRTOS函数,那么每个FreeRTOS函数调用都会传递相同的参数给xHigherPriorityTaskWoken,它在第一次使用前也要初始化为pdFALSE。
在中断安全版本的函数中不进行上下文切换有以下几个原因:
- 避免不必要的上下文切换
一个中断可能不只一次的需要一个任务处理一些程序。比如,考虑下面的情况。一个UART中断驱动接收一个字符串;对于UART中断来说,每次接收到一个字符就切换到任务是很浪费的行为,因为任务只须在UART驱动接收完整的字符串后再切换到相应任务,而不是每接收一个字符就切换到相关任务,再接收一个字符。
- 控制执行次序
中断可以偶尔发生,或在不可预知的时间。专业的FreeRTOS用户可能在应用中的特定时间想暂时避免切换到不同的任务,尽管这也可以通过锁定FreeRTOS调度器实现。
- 可移植性
这是用于访问所有FreeRTOS端口最简单的方法。
- 性能
更小处理架构的端口只允许在ISR结尾处切换上下文,要避免这些限制会引入更多的代码和更复杂的代码。同一个ISR中也允许在超过一个的FreeRTOS调用,同一个ISR中的上下文切换也不用生成更多的需求。
- 在实时系统的tick中断中执行
从前面的章节我们知道,tick中断处理程序是可定制的。导致tick中断处理程序中进行上下文切换依赖于使用的FreeRTOS函数。充其量会在调度器中引入不必要的调用。
"pxHigherPriorityTaskWoken"是一个可选项,如果不使用,那么将"pxHigherPriorityTaskWoken"设置为NULL。
portYIELD_FROM_ISR()
和portEND_SWITCHING_ISR()
宏
这节介绍了用于中断中切换上下文的宏。你不理解也不要担心,下面会给出实际例子。
taskYIELD()是一个任务中切换上正文的宏。portYIELD_FROM_ISR()
和portEND_SWITCHING_ISR()
是它的中断版本。它们的用法是一样的,也是做相同的事情。一些版本的FreeRTOS只提供它们中的一个。最新版本的FreeRTOS两个宏都提供。这个本书使用的是portYIELD_FROM_ISR()
。
// portYIELD_FROM_ISR()宏。列表87
portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
xHigherPriorityTaskWoken参数传递给中断安全函数外,可以用于作为调用portYIELD_FROM_ISR()
的直接参数。
如果portYIELD_FROM_ISR()
的xHigherPriorityTaskWoken参数是pdFALSE,那么不进行上下文切换,这个宏没有影响。如果xHigherPriorityTaskWoken不是pdFALSE,那么就进行上下文切换,当前运行的任务就可能改变。中断总是会返回运行中的任务,对于中断执行打断了任务执行的情况也一样。
大部分版本的FreeRTOS可以在中断的任意位置调用portYIELD_FROM_ISR()
,但小部分小处理器的FreeRTOS版本只能在ISR结尾部分调用portYIELD_FROM_ISR()
。
推迟中断处理操作
通常的选择是中断处理程序要尽量的短小,因为如下原因:
- 即使一个任务有一个很高的优先级,也只能在没有硬件中断时执行。
- ISRs可以在任务开始,执行中的任意时刻打断任务。
- 依赖于FreeRTOS运行的处理器平台。一个ISR执行过程中可能不能接收新的中断或中断集。
- 程序设计者需要考虑变量,外设,内存被中断和任务同时访问的顺序和冲突。
- 有一些版本FreeRTOS允许中断嵌套,但中断嵌套会增加复杂度,降低可预测性。中断越短,嵌套的可能就越小。
中断处理程序可能记录中断的原因,并会清除它。而其他具体的中断处理却可以放在任务中进行,这样就可以让中断处理程序快速的退出。这就叫做推迟中断操作,因为具体的处理操作需要从ISR中推迟到任务中进行。
将ISR处理推迟到一个任务中同样允许程序设计者使用函数对这种处理代码进行优先级管理,和其他的任务一模一样。
如果ISR中的操作推迟到的任务优先级相比其他任何任务优先级都高,那么这些操作会立即处理,就像ISR中处理一样。图48展示了这样的时序,任务1是普通任务,任务2就是ISR推迟的操作。
# 中断操作推迟到最高优先级任务。图48
ISR | -- |
Task2 | ---- |
Task1 |---- ----|
t1 t2t3 t4
# t1: 中断发生时任务1在运行
# t2: ISR运行,操作中断的外设,清除中断,唤醒任务2
# t3: 任务2优先级高于任务1,因此ISR返回后立即执行任务2,进行ISR推迟的操作
# t4: 任务2进入阻塞等待下一次中断,任务1再次进入运行状态
图48中中断在t2时刻发生,实际上在t4处理完毕,但只有t2到t3是ISR程序在运行。如果不推迟中断处理,那么t2到t4都会是ISR在运行。
没有绝对的规则规定什么时候要在ISR程序中进行全部操作,什么时候推迟部分操作到任务中。但下列操作应该考虑推迟到任务:
- 中断处理中不重要的操作。比如中断只保存一个模拟信号为数字信号,可以基本确定在ISR中操作最好。但如果转换结果需要通过软件过滤器传递,可能最好就在任务中进行数据过滤。
- 不方便放在ISR中的操作。比如向终端写数据,分配内存。
- 中断操作不确定,就是不能提前知道这个操作要花费多长时间。
下面的章节会描述和展示前面介绍了如此多概念的东西如何实现,包括怎么样用FreeRTOS实现中断操作推迟功能。
二进制信号量用于同步
中断安全版本的二进制信号量函数可以用于解锁一个任务,每当中断发生时,可以的对任务和中断进行同步。这样大部分中断操作都可以在同步任务中实现,只有很快和少数的部分依然保留在ISR中。就像前面描述的,二进制信号量可以用于推迟ISR操作到任务。
在图48中,如果中断处理程序有一个特别的时间周期。中断推迟任务的优先级可以设置为最高,确保可以抢占系统中的其他任务。ISR可以在实现中包括一个portYIELD_FROM_ISR()
调用。确保中断返回后立即执行中断推迟任务。这样可以确保事件处理在时间上连续,就像只使用ISR实现一样。图49重复了图48,但是文字描述更新为,中断推迟任务如何用二进制信号量控制。
# 用二进制信号量实现中断处理推迟。图49
ISR | -- |
Task2 | ---- |
Task1 |---- ----|
t1 t2t3 t4
# t1: 中断发生前任务1运行,任务2阻塞,等待一个信号量
# t2: ISR运行,操作中断的外设,清除中断,发送一个信号量给任务2
# t4: 任务2完成更多的中断操作,然后再次进入阻塞,等待一个信号量
中断操作推迟任务使用了一个阻塞的信号量’take’调用,意思是进入阻塞态,等待一个事件发生。当时间发生了,ISR会使用同一个信号量的’give’操作解锁这个任务,就这样实现了整个操作的处理。
'获取信号量’和’提供信号量’在不同的使用场景有不同的含义。在这个中断同步场景下,二进制信号量可以是一个只有一个元素的队列。队列中一次最多只有一个元素,因此不是空就是满。通过调用xSemaphoreTake(),中断处理推迟任务会试图带阻塞时间的从队列中读取数据,如果队列为空,就会进入阻塞状态。当事件发生时,ISR程序调用xSemaphoreGiveFromISR()存放一个钥匙到队列中,让队列满。导致中断处理推迟任务退出阻塞态,并删除钥匙,队列再次变空。当这个任务操作完成,又试图从队列读取数据,发现队列为空,进入阻塞态,等待事件发生。这个顺序在图50中展示。
图50中展示了中断提供信号量,即使没有任务在获取钥匙。任务获取钥匙,因为有钥匙,所以不阻塞。这就是为什么说这种场景和读队列相似了。这就是为啥二进制信号量不像其他信号量共同规则限制的原因。其他的信号量的获取必须在提供之前,第7章资源管理中有给出这样的场景。
// 用二进制信号量同步中断和任务。图50
-----------------------------------------------------
| ---------- Task |
| | | xSemaphoreTake() |
| ---------- 任务阻塞,等待信号量 |
-----------------------------------------------------
----------------------------------------------------
| 中断 ---------- |
| xSemaphoreGiveFromISR()-->| O | |
| 中断发生提供信号量 ---------- |
----------------------------------------------------
----------------------------------------------------
| ---------- Task |
| | O |------->xSemaphoreTake() |
| ---------- 解锁任务,信号量可用 |
----------------------------------------------------
-----------------------------------------------------------
| ---------- Task |
| | |-----> xSemaphoreTake() O |
| ---------- 成功获取信号量,再次不可用|
------------------------------------------------------------
-----------------------------------------------------------
| ---------- Task |
| | | 任务开始处理具体操作 |
| ---------- 完成后,再次试图获取信号量|
------------------------------------------------------------
xSemaphoreCreateBinary()函数
FreeRTOS V9.0.0也包含一个xSemaphoreCreateBinaryStatic()函数,它会在编译的时候分配二进制信号量需要的内存。各种类型的FreeRTOS信号量都存放在SemaphoreHandle_t
的类型变量中。
在使用信号量之前都要创建它。可以使用xSemaphoreCreateBinary()函数创建一个二进制信号量。
// xSemaphoreCreateBinary()原型。列表89
SemaphoreHandle_t xSemaphoreCreateBinary( void );
/* 返回值
* 如果返回NULL,二进制信号量不能被创建,因为没有足够的堆空间用于FreeRTOS分配给二进制信号量数据结构
* 如果返回一个非NULL值,表示二进制信号量成功创建。这个返回值就是成功创建的二进制信号量的句柄。
*/
xSemaphoreTake()函数
获取一个信号量意味着获取或叫接收这种信号量。只有信号量可用时才能成功获取。
除了递归互斥量,其他所有的信号量都可以使用xSemaphoreTake()函数获取。
xSemaphoreTake()不应该在中断处理程序中使用。
// xSemaphoreTake()原型。列表90
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );
/* 参数
* xSemaphore: 要获取的信号量。信号量是使用`SemaphoreHandle_t`格式的变量引用的。它必须在使用之前显示的创建。
* xTicksToWait: 如果信号量不可用,任务应当进入阻塞态,等待信号量可用的最大tick数量。
* 如果xTicksToWait为0,信号量不可用时会立即返回。
* 阻塞时间不为0,那么这个时间和tick频率有关。可以使用pdMS_TO_TICKS()将时钟时间转换为tick数量。
* 如果FreeRTOSConfig.h文件中的INCLUDE_vTaskSuspend为1,可以将xTicksToWait切。设置为portMAX_DELAY,任务会一直等待直到信号量可用。
* 返回值
* 可能有两个返回值
* pdPASS: 如果xSemaphoreTake()成功获取信号量,就返回pdPASS
* 如果阻塞时间不为0,信号量不可用时,调用任务可能进入阻塞,等待信号量可用。但最后在超时前信号量可用返回pdPASS。
* pdFALSE: 信号量不可用。
* 如果阻塞时间不为0,信号量不是立即可用,调用任务可能进入阻塞,等待信号量可用。最后超时前信号量都不可用就返回pdFALSE。
*/
xSemaphoreGiveFromISR()函数
二进制和如同信号量都可以使用xSemaphoreGiveFromISR()函数提供信号量。
xSemaphoreGiveFromISR()是xSemaphoreGive()的中断安全版本,它有一个pxHigherPriorityTaskWoken前面的章节已经有介绍。
// xSemaphoreGiveFromISR()原型。列表91
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );
/* 参数
* xSemaphore: 将被提供的信号量。
* 一个SemaphoreHandle_t格式的信号量引用,使用之前必须显示创建。
* pxHigherPriorityTaskWoken: 一个信号可能对应多个任务阻塞,等待它可用。调用xSemaphoreGiveFromISR()可以使信号量可用,因此一个等待信号量的任务离开阻塞态,离开阻塞态的任务如果优先级高于当前执行任务(被中断打断的任务),那么xSemaphoreGiveFromISR()就会设置pxHigherPriorityTaskWoken为pdTRUE。
* 如果xSemaphoreGiveFromISR()设置pxHigherPriorityTaskWoken为pdTRUE,那么通常在中断退出之前就应该执行一次上下文切换。这样可以确保中断返回后直接进入最高优先级就绪任务。
* 返回值
* 可能有两个返回值
* pdPASS: xSemaphoreGiveFromISR()运行成功返回pdPASS。
*,pdFALSE: 如果一个信号量已经可用,不能再次被提供,xSemaphoreGiveFromISR()就会返回pdFALSE。
*/
使用二进制信号量同步任务和中断
这个例子用二进制信号量解除任务阻塞,信号量由中断服务程序提供。高效同步任务和中断。
一个简单的周期任务用作每500ms生成一个软件中断。软件中断使用很方便,可以混合一些勾子到目标环境真实中断中。列表92列出了这个周期任务。注意任务在生成前后都会打印字符串。这样可以在程序运行时方便的观察到运行顺序。
// 例16周期生成软件中断实现。列表92
/* 这个例子中会用到多个软件中断。这个代码展示在Windows项目,数字0到2被FreeRTOS自己使用,因此3是第一个程序可用的数字。0,1,2用于标准输入,标准输出,标准出错。所以只能用3了。这里的数字关系到程序的文件描述符,有兴趣可以看linux文件描述符介绍。*/
#define mainINTERRUPT_NUMBER 3
static void xPeriodicTask( void *pvParametera ){
const TickType_t xDelay500ms = pdMS_TO_TICKS(500);
for(;;){
/* 阻塞知道到生成软件中断的时间 */
vTaskDelay( xDelay500ms );
/* 生成中断,在生成中断前后打印字符串,方便从输出内容观察执行顺序
* 生成中断的语法以来具体FreeRTOS使用端口。下面的语法只能用于FreeRTOS的Windows中的模拟器生成模拟中断*/
vPrintString("Peroidic task - About to generate an interrupt.\r\n");
// 这个模拟生成中断的代码linux中是没有的。
vPortGenerateSimulatedInterrupt( mainINTERRUPT_NUMBER );
vPrintString("Peroidic task - Interrupt generated.\r\n\r\n\r\n");
}
}
列表92例出了中断处理推迟实例的任务实现,这个任务用二进制信号量和中断实现同步。每次运行这个任务都会打印出一个字符串,这样实例运行时就可以通过观察打印信息知道任务和中断的执行顺序。
需要注意的是,列表93是例16的匹配代码,实例中的中断是软件生成的。不适用于硬件生成中断的情形。下面的代码结构就可以用来适应一个具体的硬件中断了。
// 中断处理推迟的任务实现。列表93
static void vHandlerTask(void *pvParametera){
for(;;){
/* 用二进制信号量等待事件,二进制信号量在调度器开启前创建,更会在这个任务运行之前。二进制信号量不可用之前,任务会一直阻塞。意味着只有二进制信号量成功获取才会返回,所以不需要检测xSemaphoreTake()的返回值,只能返回pdPASS */
xSemaphoreTake(xBinarySemaphore, portMAX_DELAY);
/* 到这里事件已经发生,处理这个事件,这里只打印一个衣服串*/
vPrintString("Handler task - processing event.\r\n");
}
}
列表94显示了ISR,除了给中断处理推迟任务提供信号量外非常的小。
注意怎样使用xHigherPriorityTaskWoken变量。在调用xSemaphoreGiveFromISR()之前设置为pdFALSE,然后等portYIELD_FROM_ISR()
调用后使用它。如果xHigherPriorityTaskWoken为pdTRUE,portYIELD_FROM_ISR()
就会进行一个上下文切换。
ISR中的portYIELD_FROM_ISR()
宏调用会强制一个上下文切换,这对于所有的FreeRTOS都适用,但不同的FreeRTOS可能会有一些区别。可以到FREERTOS.org网站上找到对应版本FreeRTOS手册和FreeRTOS下载页下载实例,找到你使用版本函数的语法。
不像大多数FreeRTOS架构,Windows版本的FreeRTOS需要ISR有一个返回值,Windows版本的portYIELD_FROM_ISR()
实现包含一个状态返回值,所以列表94中没有明确的返回一个具体值。
// 例16,软件中断使用的ISR。列表94
static uint32_t ulExampleInterruptHandler( void ){
BaseType_t xHigherPriorityTaskWoken;
/* xHigherPriorityTaskWoken初始化为pdFALSE,因为如果中断安全函数进行了上下文切换就会设置为pdTRUE */
xHigherPriorityTaskWoken = pdFALSE;
/* 提供信号量,解除任务阻塞。传递xHigherPriorityTaskWoken地址作为中断安全版本函数的第二个参数 */
xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);
/* 传递xHigherPriorityTaskWoken值到portYIELD_FROM_ISR()。如果xHigherPriorityTaskWoken设置为pdTRUE,那么调用portYIELD_FROM_ISR()会进行上下文切换。如果调用portYIELD_FROM_ISR()时xHigherPriorityTaskWoken仍然是pdFALSE,那么不会对ISR有任何影响。不像大多数FreeRTOS端口,Windows中的ISR需要有一个返回值,Windows版本的portYIELD_FROM_ISR()中包含一个返回状态,所以这个函数中不会有单独的返回语句。*/
/* 这里我说说自己看法,上面的xSemaphoreGiveFromISR()会提供一个信号量,如果这个信号量会引起一个比当前中断返回任务优先级更高的任务被唤醒,进入就绪状态。如果这个中断不调用上下文切换,直接返回,就会导致返回后执行的不是最高优先级任务。因为信号量唤醒的任务才是最高优先级任务,不是这个ISR打断的任务,这个ISR返回又只能进入被打断函数。就出现了调度器在执行较低优先级任务,这是不被允许的。所以才会有上下文切换函数。先在ISR中切换上下文,执行了最高的信号量唤醒任务,在从ISR返回。这样就没有问题了。*/
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
main()函数中创建二进制信号量,创建任务,安装中断操作,开启调度器。具体实现列在列表95中。
安装中断的管理者在Windows版本的FreeRTOS中有特定的语法,可能在其他版本的FreeRTOS中不相同。可以在FreeRTOS.org网站的特定用户手册或FreeRTOS下载页面的实例中找到你需要使用的函数语法。
// 例16,main()函数实现。列表95
int main(void){
/* 使用信号量前需要显示的创建。这个例子中创建一个二进制信号量*/
xBinarySemaphore = xSemaphoreCreateBinary();
/* 检查信号量创建成功*/
if(xBinarySemaphore != NULL){
/* 创建操作任务,这个任务就是中断推迟的操作任务。这个任务就是用二进制信号量和中断实现同步的。这个操作任务有一个高优先级,保证中断退出后立即运行。这里优先级是3。*/
xTaskCreate(vHandlerTask, "Handler", 1000, NULL, 3, NULL);
/* 周期性生成中断的任务。这个任务的优先级低于操作任务优先级,确保每次操作任务退出阻塞状态时会被先占。就是比操作任务后执行。*/
xTaskCreate(xPeriodicTask, "Periodic", 1000, NULL, 1, NULL);
/* 安装软件中断操作函数。这个函数的具体语法依赖于FreeRTOS的版本,这里使用的语法只适用于Windows版本的FreeRTOS,中断是模拟出来的*/
vPortSetInterruptHandler(mainINTERRUPT_NUMBER, ulExampleInterruptHandler);
/*开启调度器*/
vTaskStartScheduler();
}
/* 正常情况下面代码不应该会运行*/
for(;;);
}
例16程序运行的输出如图51。如愿以偿,中断一旦生成,vHandlerTask()就进入运行状态,所以操作任务的输出被周期性中断生产任务分开。更多的解释在图52中提供。
# 例16执行的输出结果。图51
Handler task - Processing event.
Peroidic task - Interrupt generated.
Peroidic task - About to generate an interrupt.
Handler task - Processing event.
Peroidic task - Interrupt generated.
Peroidic task - About to generate an interrupt.
Handler task - Processing event.
Peroidic task - Interrupt generated.
Peroidic task - About to generate an interrupt.
Handler task - Processing event.
Peroidic task - Interrupt generated.
Peroidic task - About to generate an interrupt.
Handler task - Processing event.
Peroidic task - Interrupt generated.
#例16的执行顺序,图52
中断 | --- --- |
操作任务 | ---- ---- |
周期生成 | --- --- --- --- |
空闲任务 |-- -- ------|
t1 t2 t3 t4 t5 time
#t2: 大部分时间空闲任务执行,每500ms被周期生成中断任务抢占。
#t3: 周期生成中断任务首先打印消息,然后生成中断。ISR程序立即运行。
#t4: ISR提供信号量,引起vHandlerTask()离开阻塞状态。ISR以后返回到vHandlerTask,因为它是就绪状态中优先级最高的任务。vHandlerTask()在进入阻塞状态,等待下一个中断前打印自身的消息。
#t5: 周期生成中断任务再次成为最高优先级就绪任务,在再次进入阻塞状态,等待下一个运行周期之前打印它的第二条消息。这以后就只有空闲任务可以运行了。
改进例16中使用的任务
例16中用二进制信号量同步任务和中断。执行顺序如下:
- 中断发生
- ISR执行,提供信号量解锁操作任务
- 操作任务在ISR后立即执行,并取得信号量
- 任务处理事件,然后再次试图获取信号量,因为信号量不再可用,进入阻塞状态
例16中的任务结构只适合中断发生频率低的情况。为啥啦,考虑下面情况。第二个,第三个中断发生在第一个中断操作任务没有处理完成前时:
- 当第二个ISR执行时,信号量是空的,因此ISR会提供一个信号量,任务处理完第一个中断时间后,就会立即处理第二个中断时间。这种情况在图53中展示。
- 当第三个ISR执行,信号量已经可用,阻碍ISR再次提供信号量,因此任务就不会知道第三事件已经发生。这种情况在图54中展示。
# 第二个中断事件发生在第一个中断事件处理完之前的情形。图53
-----------------------------------------------------
| ---------- Task |
| | | xSemaphoreTake() |
| ---------- 任务阻塞,等待信号量|
------------------------------------------- ----------
------------------------------------------------------
| 中断 ---------- 任务 |
| xSemaphoreGiveFromISR()-->| O | 离开阻塞态|
| 中断发生提供信号量 ---------- |
-------------------------------------------------------
------------------------------------------------------------
| ---------- Task |
| | |-----> xSemaphoreTake() O |
| ---------- 成功获取信号量,再次不可用 |
------------------------------------------------------------
---------------------------------------------------------
| 中断 ---------- 任务 |
| xSemaphoreGiveFromISR()-->| O | 任然处理第|
| 另外中断发生提供信号量 ---------- 一个中断 |
----------------------------------------------------------
----------------------------------------------------------
| ---------- Task |
| | |-----> xSemaphoreTake() |
| ---------- |
| 当处理完第一个时间,再次调用xSemaphoreTake(), |
| 因为另外一个中断已经发生,信号量已经可用 |
----------------------------------------------------------
----------------------------------------------------------
| ---------- Task |
| | | vProcessEvent() O |
| ---------- |
| 任务获取信号量,不进入阻塞,处理第二个事件。 |
----------------------------------------------------------
# 第一个中断未处理完成之前又发生两个中断事件的情形。图54
----------------------------------------------------
| ---------- Task |
| | | xSemaphoreTake() |
| ---------- 任务阻塞,等待信号量 |
-----------------------------------------------------
---------------------------------------------------
| 中断 ---------- 任务 |
| xSemaphoreGiveFromISR()-->| O | 离开阻塞态 |
| 中断发生提供信号量 ---------- |
---------------------------------------------------
---------------------------------------------------
| ---------- Task |
| | |-----> xSemaphoreTake() O |
| ----------成功获取信号量,信号量再次 |
| 不可用。任务开始处理事件 |
---------------------------------------------------
---------------------------------------------------
| 中断 ---------- 任务 |
| xSemaphoreGiveFromISR()-->| O | 任然任然处 |
| 处理第一个事件时第二个事件 ---------- 理第一个事 |
| 发生。ISR再次提供信号量, |
| 事件不会发生丢失。 |
---------------------------------------------------
---------------------------------------------------
| 中断 ---------- 任务 |
| xSemaphoreGiveFromISR()-->| O | 任然处理第 |
| ---------- 一个中断 |
| 在处理第一个中断事件时,发生第三个中断事件, |
| ISR不能再次提供信号量,因为信号量已经可用, |
| 造成事件丢失。 |
---------------------------------------------------
---------------------------------------------------
| ---------- Task |
| | O |-----> xSemaphoreTake() |
| ---------- |
| 当处理完第一个事件,再次调用xSemaphoreTake(), |
| 因为另外一个中断已经发生,信号量已经可用 |
---------------------------------------------------
---------------------------------------------------
| ---------- Task |
| | | vProcessEvent() O |
| ---------- |
| 任务获取信号量,不进入阻塞,处理第二个事件。 |
---------------------------------------------------
---------------------------------------------------
| ---------- Task |
| | | xSemaphoreTake() |
| ---------- |
| 当处理完成第二个事件,再次调用xSemaphoreTake(), |
| 但信号量不可用,任务进入阻截,等等下一次中断。 |
| 尽管这里的第三个事件还没有进行处理。 |
---------------------------------------------------
例16中的中断处理推迟任务使用在列表93中有展示,它每次调用xSemaphoreTake()只处理一个事件。这对于例16是适用的,因为这里的中断事件是软件模拟的,会以预先定义好的时间发生。在实际的应用中,中断是由硬件生成的,可以发生在任何时间。因此为了减少错过中断,需要将中断处理推迟任务结构化。以使它能处理两次调用xSemaphoreTake()函数之间所有已经发生的中断,这种结构化代码在列表96中列出。它演示了如何用结构化的中断处理推迟方法处理一个UARAT。在列表96中假设,接收到一个字符就会生成一个接收中断,UART会将接收到的字符放进一个FIFO(一个硬件缓存)中。
例16中的中断处理推迟任务还有一个缺点: xSemaphoreTake()函数中没有使用超时时间。其中将超时时间设置为portMAX_DELAY
,导致信号量可用前任务一直等下去。无限制超时时间经常在例子中使用,因为它使实例最简单,也让实例更容易理解。但无限制超时时间在应用中是不建议使用的,因为用它之后就不能从一个错误中恢复了。试考虑下面情况,任务在等一个信号量,但硬件发生错误阻碍中断产生:
- 如果任务进行无限制超时等待,它就不会知道出错了,会一直等待下去。
- 如果任务设置好了超时等待时间,那么xSemaphoreTake()超时时间到就会返回pdFAIL,那么任务下次运行时就会捕获并清除这个错误,这种情况在列表96中有列出。
// 使用一个UART接收处理实例,中断处理推迟任务推荐结构。列表96
static void vUARTReceiveHandlerTask( void *pvParameters ){
/* 两个中断间的最大期望时间 */
const TickType_t xMaxExoectedBlockTime = pdMS_TO_TICKS(500);
for(;;){
/* UART接收中断会提供信号量。等待下次中断的最大超时时间就是上面的500ms ×/
if(xSemaphoreTake( xBinarySemaphore, xMaxExoectedBlockTime ) == pdPASS ){
/* 获取到信号量。在再次调用xSemaphoreTake()之前处理所有接收到的事件。每个接收事件都会存放一个字符在UART的接收FIFO中,假设UART_RxCount()会返回接收到的字符数量*/
while( UART_RxCount() > 0 ){
/* UART_ProcessNextRxEvent()假设用作处理一个接收的字符,且会增加FIFO中的字符下标1格*/
UART_ProcessNextRxEvent();
}
/* 没有更多的接收事件了(FIFO中没有更多的字符了),因此循环调用xSemaphoreTake()等待下次中断。发生在这个位置和xSemaphoreTake()之间的其它中断会被锁存在信号量中,因此不会丢失。*/
}
else{
/* 在期望的时间内没有接收到数据。检查是否要清除,UART中可能已经发生了硬件错误,它阻碍UART产生更多的中断 */
UART_ClearErrors();
}
}
}
普通信号量
就像二进制信号量可以认为是一个长度为一的队列,普通信号量可以看作一个不只一个元素的队列。任务不关心队列中的数据,只关心队列中数据数量。FreeRTOSConfig.h文件中的configUSE_COUNTING_SEMAPHHORES
设置为1就可以使用普通信号量。
每次向普通信号量中提供一个信号量,队列中就用掉一个存储空间。队列中数据数量就是信号量的值的数量。
普通信号量通常用于以下2种情况:
- 事件数量:这种情况下,每发生一次事件,事件处理者就会提供一个信号量。每次提供信号量都会引起信号量数值(数量)自增1个。任务处理事件时要获取信号量,每次获取信号量会引起信号量的值(数量)自减1。这里的数量值和已经发生和已经处理事件次数是不同的。它的结构会在图55展示。
用于计量事件次数的普通信号量初始创建时的值是0。
- 资源管理:这种情况下,这个数值会用于表示资源的数量。对于在管理范围的资源,任务在使用前要先获取一个信号量(就是要减少普通信号量值)。当这个数值到0时,就不再有可用的资源了。当任务使用资源完毕,它会给普通信号量提供资源(就是会增加普通信号量值)。
普通信号量用于管理资源时,初始创建普通信号量时值等于可用资源数量。第7章有用普通信号量用作资源管理。
# 使用一个普通信号量计量事件。图55
-------------------------------------------------------------------------
| 信号量数值为0 Task |
| ----------------- xSemaphoreTake() |
| | | | | | |
| ----------------- 任务阻塞等待一个信号量 |
-------------------------------------------------------------------------
-------------------------------------------------------------------------
| 中断 信号量数值为1 Task |
| xSemaphoreGiveFromISR() ----------------- xSemaphoreTake() |
| 一个中断发生, | | | | O | |
| 提供一个信号量。 ----------------- |
-------------------------------------------------------------------------
-------------------------------------------------------------------------
| 信号量数值为1 Task |
| ----------------- xSemaphoreTake() |
| | | | | O | |
| ----------------- 任务离开阻塞状态 |
-------------------------------------------------------------------------
-------------------------------------------------------------------------
| 信号量数值为0 Task O |
| ----------------- xSemaphoreTake() |
| | | | | | 成功获取信号量,信号量不 |
| ----------------- 再可用。任务开始处理事件 |
-------------------------------------------------------------------------
-------------------------------------------------------------------------
| 中断 信号量数值为2 Task |
| xSemaphoreGiveFromISR() ----------------- xSemaphoreTake() |
| 任务处理中断时又发生 | | | O | O | |
| 2个中断,两个ISR都提供 ----------------- |
| 信号量。有效存储两个事件,所以不会丢失。 任务还在处理事件1 |
-------------------------------------------------------------------------
-------------------------------------------------------------------------
| 中断 信号量数值为1 Task |
| ----------------- xSemaphoreTake() |
| | | | | O | |
| ----------------- O |
| 当任务处理完毕第一个中断事件,会继续调用xSemaphoreTake()。 |
| 另外两个信号量已经可用,任务会获取一个信号量离开阻塞状态, |
| 获取一个信号量后,还有一个任然可用。 |
-------------------------------------------------------------------------
xSemaphoreCreateCounting()函数
FreeRTOS也包括xSemaphoreCreateCountingStatic()函数,它会在编译阶段静态分配普通信号量需要的空间。所有FreeRTOS信号量句柄都用SemaphoreHandle_t格式的变量保存。
在使用信号量之前需要创建它。使用xSemaphoreCreateCounting()函数创建普通信号量。
// xSemaphoreCreateCounting()函数原型。列表97
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);
/* 参数
* uxMaxCount: 这个值是普通信号量可以达到的最大值。对于连续的队列结构,这个值就是队列长度有效值。
* 当用普通信号量管理可以访问的资源数量,uxMaxCount应该设置为资源可用数量。
* uxInitValue: 普通信号量创建时的初始值。
* 如果普通信号量用作计数或时间标记,uxInitValue应该初始化为0,因为假设创建信号量时没有事件发生。
* 当普通信号量用作管理可以访问的资源集合,uxInitialCount应该初始化为uxMaxCount,因为假设创建普通信号量是所有资源都是可以访问的。
* 返回值:
* 如果返回NULL,普通信号量可能因为没有足够的堆空间分配给信号量数据结构而创建失败。第二章中提供了更加详细的堆管理说明。
* 如果返回一个非NULL的值,表示信号量成功创建普通信号量。这个返回值就是成功创建的信号量的引用句柄。
*/
例17,用普通信号量同步任务和中断
例17用普通信号量而不是二进制信号量对例16进行了改进。main()中调用xSemaphoreCreateCounting()而不是xSemaphoreCreateBinary()。新的函数调用列在列表98中。
// 例17调用xSemaphoreCreateCounting()创建普通信号量。列表98
/* 使用信号量前需要显示的创建。这个实例中创建一个普通信号量。这个信号量有一个最大值10,初始值是0 */
xCountingSemaphore = xSemaphoreCreateCounting( 10, 0 );
为模拟多个事件高频率发生,中断处理程序改变为每次中断不只提供一个信号量。每个事件都可以用普通信号量锁存。改变后的中断处理程序列在列表99中。
// 例17使用的中断处理程序实现。列表99
static uint32_t ulExampleInterruptHandle(void){
BaseType_t xHigherPriorityTaskWoken;
/* xHigherPriorityTaskWoken必须初始化为pdFALSE,因为如果一个中断安全版本函数需要一个上下文切换,它才会被设置为pdTRUE */
xHigherPriorityTaskWoken = pdFALSE;
/* 多次提供信号量。第一个会解锁中断处理推迟任务,第二个提供信号量会被锁存进信号量,允许中断处理推迟任务按顺序处理事件,而不会丢失事件。这样可以模拟多个中断的接收和处理,即使这样的事件是同一个中断模拟发生的。*/
xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken );
/* 将xHigherPriorityTaskWoken的值传递到portYIELD_FROM_ISR()。如果xHigherPriorityTaskWoken是pdTRUE,那么调用portYIELD_FROM_ISR()就会进行上下文切换。相反如果在。xHigherPriorityTaskWoken还是pdFALSE,调用portYIELD_FROM_ISR()就不会有影响。不像大多数FreeRTOS版本,Windows版本的ISR需求有一个返回值,这个返回状态包含在Windows版本的portYIELD_FROM_ISR()中 */
portYIELD_FROM_ISR(xHigherPriorityTaskWoken );
}
其他的函数就和例16是一样的了,没有改变。
例17的输出如图56所示。可以看出,中断处理推迟任务处理了所有的3个模拟事件,每次生成一个中断时。这些事件都锁存在普通的信号量之中,允许任务顺序处理它们。
Handler task - Processing event.
Handler task - Processing event.
Handler task - Processing event.
Periodic task - Interrupt generated.
Handler task - Processing event.
Handler task - Processing event.
Handler task - Processing event.
Periodic task - Interrupt generated.
Handler task - Processing event.
Handler task - Processing event.
Handler task - Processing event.
Periodic task - Interrupt generated.
推迟工作放到实时系统守护进程
前面的中断处理推迟需要程序设计者为每个使用中断处理推迟技术的中断创建一个任务。也可以使用xTimerPendFunctionCallFromISR()函数推迟中断处理到实时系统守护任务中,而不用为每个中断创建一个任务。推迟中断处理到守护任务被叫做"集中推迟中断处理"。
第五章描述了FreeRTOS软件时间任务相关函数如何通过时间命令队列发送命令给守护任务,xTimerPendFunctionCallFromISR()和xTimerPendFunctionCall()函数使用同一个时间任务队列发送一个"执行函数"命令给守护任务。这些函数发送给守护任务,会在守护任务上下文执行。
集中推迟中断处理的优势:
- 更少的代码量
不用每个中断都创建一个任务
- 模型简单
中断处理推迟函数是标准的C函数
集中推迟中断处理的劣势:
- 更不灵活
不能为每一个中断推迟任务设置优先级。每个中断推迟处理函数都是以守护任务优先级运行。就像第5章描述一样,守护任务优先级是在FreeRTOSConfig.h中的configTIMER_TASK_PRIORITY
设置,在编译时就确定的。
- 更少的决策性
xTimerPendFunctionCallFromISR()发送一个命令给时间命令队列。在xTimerPendFunctionCallFromISR()发送"执行函数"命令之前,会首先执行已经在时间命令队列中的命令。
不同的中断有不同的时间限制,所以应用中通常两种中断推迟处理方法都会用上。
xTimerPendFunctionCallFromISR()函数
xTimerPendFunctionCallFromISR()是xTimerPendFunctionCall()函数的中断安全版本。两个函数都允许程序设计者提供一个函数来在守护任务上下文中运行。两个函数将执行的函数和输入参数都是通过时间命令队列发送给守护任务。什么时候具体执行,需要依赖于程序中的守护任务和其他任务的优先级。
// xTimerPendFunctionCallFromISR()函数原型。列表100
BaseType_t xTimerPendFunctionCallFromISR( PendedFunction_t xFunctionToPend, void *pvParametera1, uint32_t ulParameter2, BaseType_t *pvHigherPriorityTaskWoken );
// 函数原型
void vPendableFunction( void *pvParametera1, uint32_t ulParameter2 );
/* 参数:
* xFunctionToPend: 守护任务将要执行的函数指针(实际上就是函数的名字)。函数原型必须和上面一样
* pvParametera1: 守护任务即将执行的函数的pvParametera1参数。void *格式的参数允许它传递任何格式的参数。比如整数可以强制转换成void *,结构体也可以强制转换为void *
* ulParameter2: 守护任务即将执行的函数的ulParameter2参数一样的作用.
* pxHigherPriorityTaskWoken: xTimerPendFunctionCallFromISR()会向时间命令队列写命令。如果实时系统守护任务在阻塞状态,等待时间任务队列可用,那么写入的数据会导致守护任务离开阻塞状态。如果守护任务优先极比当前运行任务优先级高,那么xTimerPendFunctionCallFromISR()就会将*pxHigherPriorityTaskWoken设置为pdTRUE。
* 如果xTimerPendFunctionCallFromISR()设置为pdTRUE,那么应当在退出中断前进行一个上下文切换。这样才能确保中断返回到守护任务,因为守护任务是就绪态的最高优先级任务。
* 返回值:
* 可能有两个返回值
* 1. pdPASS:如果"执行函数"命令写入到时间命令队列就会返回pdPASS。
* 2. pdFAIL:如果时间命令队列已经满,"执行函数"命令不能写入到时间命令队列,就会返回pdFAIL。第五章有讲如何设置命令队列长度。
*/
例18集中推迟中断操作
例18提供的函数与例16类似,但没有用普通信号量,也没有为处理中断推迟操作专门建立一个特别的任务,它的处理操作由实时系统的守护任务完成。
例18的中断服务程序在列表102中列出。它有调用xTimerPendFunctionCallFromISR()函数,传递了一个vDeferredHandingFunction()函数的指针给它。这里的中断推迟操作就是由vDeferredHandingFunction()实现。
中断服务程序每次运行都会增加一个叫ulParameterValue的变量值。ulParameterValue用做xTimerPendFunctionCallFromISR()函数中的ulParameter2参数,因此当vDeferredHandingFunction()被守护任务执行时也会用于到vDeferredHandingFunction()的ulParameter2参数,pvParametera1在这个实例中没有使用。
// 例18软件中断使用。列表102
static uint32_t ulExampleInterruptHandler(void){
static uint32_t ulParameterValue = 0;
BaseType_t xHigherPriorityTaskWoken;
/* xHigherPriorityTaskWoken必需初始化为pdFALSE,因为如果中断安全版本函数中如果需要进行上下文切换会被设置为pdTRUE. */
xHigherPriorityTaskWoken = pdFALSE;
/* 发送一个中断推迟函数句柄给守护任务。中断推迟函数句柄的pvParametera1没有用,因此这里设置为NULL。中断推迟函数句柄的ulParameter2用于传递一个每次运行中断函数时自增的数字 */
xTimerPendFunctionCallFromISR( vDeferredHandingFunction, NULL, ulParameterValue, &xHigherPriorityTaskWoken);
ulParameterValue++;
/* 将xHigherPriorityTaskWoken传递给portYIELD_FROM_ISR()。如果xHigherPriorityTaskWoken为pdTRUE,那么portYIELD_FROM_ISR()会进行一个上下文切换。如果xHigherPriorityTaskWoken还是pdFALSE,那么portYIELD_FROM_ISR()不会做任何事。不像大多数FreeRTOS函数,Windows版本的ISR审请函数有一个返回值,就是说portYIELD_FROM_ISR()会自动返回一个状态值。*/
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
vDeferredHandingFunction()函数的实现列在列表103中。它会打印一个字符串和ulParameter2的值。
vDeferredHandingFunction()需要有一个列表101的原型,尽管这个实例中有一个参数没有使用。
// 例18中断需要的推迟操作函数
static void vDeferredHandingFunction(void *pvParametera1, uint32_t ulParameter2){
/* 这里处理事件,这个实例中只打印一个字符串和ulParameter2的值。pvParametera1在这里没有使用*/
vPrintStringAndNumber("Handler function - Processing event ", ulParameter2);
}
main()函数列在列表104中。它比例16中的main()函数简单,因为它不创建普通信号量,也不创建处理推迟操作的任务。
// 例18main()函数实现。列表104
int main(void){
/* 生成软件中断的任务优先级比守护任务低。守护任务优先级是FreeRTOSConfig.h文件中的configTIMER_TASK_PRIORITY决定,在编译时明确下来*/
const UBaseType_t ulPeriodicTaskPriority = configTIMER_TASK_PRIORITY - 1;
/* 创建一个周期软件中断任务 */
xTaskCreate(xPeriodicTask, "Periodic", 1000, NULL, ulPeriodicTaskPriority, NULL);
/* 给软件中断安装句柄。这里的语法依赖于不同版本FreeRTOS。这个语法是Windows版本的,其它版本的类似。*/
vPortSetInterruptHandler(mainINTERRUPT_NUMBER, ulExampleInterruptHandler);
/* 开启调度器 */
vTaskStartScheduler();
/* 正常情况不会运行到这里的 */
for(;;);
}
例18的输出如图57,守护任务优先级高于软件中断生产任务,因此软件中断一旦生成vDeferredHandingFunction()就会被守护任务运行。这就导致vDeferredHandingFunction()函数的消息在周期生成软件中断任务输出消息的中间,就和用普通信号量实现中断推迟操作一样。更多的图53解释在图58中。
# 例18的运行输出。图57
Peroidic task - About to generate an interrupt.
Handler function - Processing event 0
Peroidic task - Interrupt generated.
Peroidic task - About to generate an interrupt.
Handler function - Processing event 1
Peroidic task - Interrupt generated.
Peroidic task - About to generate an interrupt.
Handler function - Processing event 2
Peroidic task - Interrupt generated.
Peroidic task - About to generate an interrupt.
Handler function - Processing event 3
Peroidic task - Interrupt generated.
# 例18运行时序
2
中断 | -- -- |
守护任务| 1 --- --- |
周期任务| -- 3 -- -- -- |
空闲任务|----- 4 ---------- ----|
t1 t2 Time
# 1: 大部分时间都是空闲任务在运行。每500ms被周期任务抢占
# 2: 周期任务打印第一个消息,然后产生一个中断。中断服务程序立即执行
# 3: 中断调用xTimerPendFunctionCallFromISR()函数,它会往中断命令队列写命令,引起守护任务离开阻塞态。中断服务程序立即返回进入守护任务,因为守护任务是就绪态中优先级最高的任务。守护任务打印消息,包含一个自增的参数值,之后返回阻塞态,等待另外的命令发送到时间命令队列,或软件定时到期。
# 4: 周期任务再次成为最高优先级任务,它会在再次进行阻塞态,等待下个周期前打印第2个消息。之后就是空闲任务运行。
在中断服务程序中使用队列
二进制和普通信号量可用于事件交流。队列可以用于事件交流和传递数据。
xQueueSendToFrontFromISR()是xQueueSendToFront()的中断安全版本,可以用于中断服务程序。xQueueSendToBackFromISR()是xQueueSendToBack()的中断安全版本,可以用于中断服务程序。xQueueReceiveFromISR()是xQueueReceive()用于中断服务程序的中断安全版本。
xQueueSendToFrontFromISR()和xQueueSendToBackFromISR()函数
// xQueueSendToFrontFromISR()原型。列表105
BaseType_t xQueueSendToFrontFromISR( QueueHandle_t xQueue, void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken );
// xQueueSendToBackFromISR()原型。列表106
BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue, void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken);
xQueueSendFromISR()和xQueueSendToBackFromISR()对等的。
# 参数
# xQueue: 将要写入或读取的队列句柄。这个句柄是用函数xQueueCreate()创建后返回的
# pvItemToQueue: 将会被复制进队列的数据指针。每个队列的数据项大小是在创建时初始化设定的。也就是多少数据会被复制到队列的存储区域中。
# pxHigherPriorityTaskWoken: 一个单独的队列上可能阻塞有多个任务,它们都在等待队列数据可用。调用xQueueSendToFrontFromISR()或xQueueSendToFrontFromISR()可以使队列数据可用,导致一个任务离开阻塞状态。如果一个任务离开阻塞状态,如果离开阻塞状态的任务优先级高于当前运行任务优先级,那么这个函数就会设置*pxHigherPriorityTaskWoken为pdTRUE。
# 如果xQueueSendToFrontFromISR()或xQueueSendToBackFromISR()将这个值设置为pdTRUE,那么就应当在中断退出前进行一次上下文切换。这样能够确保返回后进入最高优先级就绪态任务。
# 返回值
# 可能有2个值
# 1. pdPASS: 数据成功写入到队列就返回pdPASS
# 2. errQUEUE_FULL: 如果因为队列已经满,导致数据不能发送到队列就会返回errQUEUE_FULL。
将队列用于ISR的思考
队列提供了一个中断和任务传递数据的简单,方便的方式,但如果数据以高频率传递,不推荐用队列传递。
很多在FreeRTOS下载的实例应用程序包含一个简单的UART驱动,它用队列将UART的接收中断服务程序收到数据传出来。这些样例中用队列有两个原因:为了展示ISR中的队列使用,为了测试FreeRTOS函数故意载入系统。这些ISR中使用队列不会阻止高效设计,除非数据传的很慢,更好的做法是用伪代码而不是复制这个技术。下面是一些更高效的方法:
- 使用直接内存访问(DMA)接收和缓存字符。这个方法没有软件开销。一个直接到任务的通知,可以用于处理那些需要处理捕获到的字符的任务,让它们离开阻塞态。
- 每次收到字符就复制到一个线程安全的RAM缓存。同样,一个直接到任务的通知可以让任务离开阻塞态,这些任务会在完全接收到消息后处理消息缓存,这个数据传递会以一个break结束。
- 直接在ISR中处理接收到的字符,然后用一个队列将接收到的数据传递给任务。这些会在图34中展示。
例19,用队列中断中发送和接收
这个例子展示了在同一个中断中使用xQueueSendToBackFromISR()和xQueueReceiveFromISR()。同上面一样这里的中断是软件产生的。
直接到任务的消息是ISR提供的让任务离开阻塞态最有效的方法,直接到任务的消息会在第9章任务消息中介绍
流缓存作为FreeRTOS+TCP的一部分提供,可以用在这里
创建一个周期任务,每200ms发送5个数字到队列。在一个数字发送后产生一个软件中断。这个任务实现列在列表107中。
// 向队列写数据的任务实现。列表107
static void vIntegerGenerator(void *pvParameters){
TickType_t xLastExecutionTime;
uint32_t ulValueToSend = 0;
xLastExecutionTime = xTaskGetTickCount();
for(;;){
/* 这是一个周期任务。阻塞直到到期再次运行。这个任务每200ms执行一次 */
vTaskDelayUntil( &xLastExecutionTime, pdMS_TO_TICKS(200));
/* 发送5个数字到队列,每次发送值都比之前的值大。中断处理程序会从队列中读取这些值。中断处理程序总是会试图让队列变空,因此这里发送5个数字时不需要指定阻塞时间 */
for( i = 0; i < 5; i++ ){
xQueueSendToBack(xIntegerQueue, &ulValueToSend, 0);
ulValueToSend++;
}
/* 生成中断,让中断处理程序可以从队列读取数字。这里生成中断的语法依赖于具体的FreeRTOS版本。这里的语法是Windows版本的FreeRTOS语法,这里的中断是模拟的 */
vPrintString("Generator task - About to generate an interrupt.\r\n");
vPortGenerateSimulatedInterrupt(mainINTERRUPT_NUMBER);
vPrintString("Generator task - Interrupt generate.\r\n\r\n\r\n");
}
}
中断处理程序重复调用xQueueReceiveFromISR()函数,直到所有被周期任务写入到队列的数字都被读出来,队列再次变空。每次接收到的值的最后2位用作字符串的索引。然后将字符串指针当前索引位置用xQueueSendFromISR()调用发送给不同的队列。中断服务程序实现列在列表108中。
// 例19使用的中断服务程序。列表108
static uint32_t ulExampleInterruptHandler(void){
BaseType_t xHigherPriorityTaskWoken;
uint32_t ulRecevedNumber;
/* 这些字符串定义为静态常量确保它们不是在中断处理程序的栈上分配,即使中断服务程序退出也不会释放 */
static const char *pcStrings[] = {
"String 0\r\n",
"String 1\r\n",
"String 2\r\n",
"String 3\r\n",
};
/* 和前面一样,xHigherPriorityTaskWoken要初始化为pdFALSE,让它可以捕获到中断安全版本中设置的pdTRUE。注意中断安全版本函数只能将xHigherPriorityTaskWoken设置为pdTRUE,在调用xQueueReceiveFromISR()和调用xQueueSendToBackFromISR()使用同一个xHigherPriorityTaskWoken变量是安全的。*/
xHigherPriorityTaskWoken = pdFALSE;
/* 从队列中读取数据,直到队列为空 */
while( xQueueReceiveFromISR( xIntegerQueue, &ulRecevedNumber, &xHigherPriorityTaskWoken) != errQUEUE_EMPTY){
/* 传递接收到的数据的最后两位(0-3),然后用传递的值作为pcStrings[]数组的下标,选择一个字符串发送给另外一个队列。 */
ulRecevedNumber &= 0x03;
xQueueSendToBackFromISR(xStringQueue, &pcStrings[ulRecevedNumber], &xHigherPriorityTaskWoken);
}
/* 如果从xIntegerQueue队列接收动作引起一个任务离开阻塞态,而且这个任务的优先级比当前运行状态任务优先级高,那么xHigherPriorityTaskWoken会被xQueueReceiveFromISR()设置为pdTRUE。
* 如果发送给xStringQueue队列动作引起一个任务离开阻塞态,而且这个任务的优先级比当前运行状态任务优先级高,那么xHigherPriorityTaskWoken会被xQueueSendToBackFromISR()设置为pdTRUE。
* xHigherPriorityTaskWoken是作为portYIELD_FROM_ISR()函数的参数使用的。如果xHigherPriorityTaskWoken等于pdTRUE,那么调用portYIELD_FROM_ISR()会进行一个上下文切换。如果xHigherPriorityTaskWoken为pdFALSE,那么portYIELD_FROM_ISR()什么都不会做。
* Windows版本的portYIELD_FROM_ISR()有一个返回状态。所以这个函数没有一个明确的return语句 */
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
下面这个任务会阻塞在队列上,直到中断服务程序向这个队列上写入数据,然后打印接收到的数据。它的实现列在列表109中。
// 例19打印从中断服务程序接收到的字符串。列表109
static void vStringPrinter( void *pvParameters ){
char *pcString;
for(;;){
/* 阻塞在队列上,等待数据到达*/
xQueueReceive(xStringQueue, &pcString, portMAX_DELAY);
/* 打印接收到的字符串 */
vPrintString(pcString);
}
}
和上面一样,main()函数在开启调度器之前创造需要的队列和任务。它的实现列在列表110中。
// 例19main()函数实现。列表110
int main(void){
/* 在使用队列前必须显示的创建它。创建2个例子中用到的队列。一个队列保存格式为uint32_t的变量,另外一个保存char *的变量。2个队列都最多可以保存10个项目,实际的应用中应当检查返回值,确保队列成功创建 */
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);
/* 安装中断服务程序。这里的语法依赖FreeRTOS版本。这里用的是FreeRTOS的Windows版本,这里的中断是软件模拟的*/
vPortSetInterruptHandler(mainINTERRUPT_NUMBER, ulExampleInterruptHandler);
/* 开启调度器 */
vTaskStartScheduler();
/* 如果一切正常,那么不会运行到这里,因为调度器现在应在运行任务。如果运行到这里,那么可以是因为没有足够的堆用于创建空闲任务。第2章有更多堆内存管理的信息 */
for(;;);
}
例19运行时的输出如图59。可以看出,中断中接收到了5个整数,生成5个字符串作为响应。更多细节可在图60中查看。
# 例19运行时的输出。图59
Generator task - About to generate an interrupt.
String 2
String 3
String 0
String 1
String 2
Generator task - Interrupt generated.
Generator task - About to generate an interrupt.
String 3
String 0
String 1
String 2
String 3
Generator task - Interrupt generated.
# 例19运行时序。图60
中断 | 2 -- -- |
字符打印 | 3 --- --- |
中断生成 | 1 ---- 4 -- ---- -- |
空闲任务 |----- 5 ----------- ----|
t1 Time
# 1: 大部分时间是空闲任务在运行。每200ms会被整数生成任务抢占
# 2: 整数生成任务写5个值到队列,然后生成一个中断
# 3: 中断服务程序从队列读数据,并写另一个队列。每从队列读到一个整数就向另一个队列写一个字符串。向队列写字符串会让字符串打印任务离开阻塞态
# 4: 字符串打印任务是最高优先级任务,因此会在中断服务程序后立即运行。它打印从队列中读取到的每个字符串,当队列为空时进入阻塞状态,允许更低优先级的整数生成任务再次运行
# 5: 整数生成任务是周期任务,因此会等到下一周期再运行。空闲任务成为唯一任务。200ms后再次执行前面相似的过程。
中断嵌套
很容易在任务优先级和中断优先级之间产生混淆。这里说的中断优先级,是指各个ISR执行的关系。任务优先级没有办法和中断优先级关联起来。硬件会决定什么时候ISR执行,软件决定什么时候任务运行。一个ISR用于响应一个硬件中断,它会中断一个任务,但任务不能抢占ISR。
支持中断嵌套的FreeRTOS版本需要在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或configMAX_API_CALL_INTERRUPT_PRIORITY | 可以被调用的最高FreeRTOS中断安全函数优先级 |
|configKERNEL_INTERRUPT_PRIORITY | tick中断使用优先级,总是设置为可能的最低优先级。如果这个版本FreeRTOS没有使用configMAX_SYSCALL_INTERRUPT_PRIORITY常量,那么使用中断安全版本FreeRTOS函数的中断也必须以configKERNEL_INTERRUPT_PRIORITY优先级运行|
每一个中断源都有一个数字化优先级和逻辑化优先级。
- 数字优先级:数字化的优先级只是一个代表中断的简单数字。比如,如果一个中断优先级时7,那么他的数字化优先级就是7。同样,如果一个中断优先级时200,它的数字化优先级就是200。
- 逻辑化优先级:一个中断的逻辑化优先级是指中断优先于其他中断。如果两个不同优先级中断同时发生,那么处理器会执行逻辑化优先级更高的中断ISR而不是逻辑化优先级更低的ISR。依赖于不同架构,一个中断中可以嵌套逻辑优先级更低的中断;在一些处理器上,数字化优先级设计来比逻辑化优先级更高,但有些处理器逻辑化优先级比数字化优先级更高。
一个完整的中断嵌套模型是通过设置一个比configKERNEL_INTERRUPT_PRIORITY
更高的configMAX_SYSCALL_INTERRUPT_PRIORITY
的逻辑化优先级创建的。更多的细节在图61中,它展示了下面的情形:
- 处理器有7个唯一的中断优先级
- 数字化优先级7设计为比数字化优先级1有更高的逻辑化优先级
-
configKERNEL_INTERRUPT_PRIORITY
设置为1
-
configMAX_SYSCALL_INTERRUPT_PRIORITY
设置为3
# 常量对中断嵌套行为的影响。图61
configMAX_SYSCALL_INTERRUPT_PRIORITY = 3
configKERNEL_INTERRUPT_PRIORITY = 1
---------------
| 中断7 |
---------------
| 中断6 |
---------------
| 中断5 |
---------------
| 中断4 |
---------------
| 中断3 |
---------------
| 中断2 |
---------------
| 中断1 |
---------------
# 不调用任何接口函数的中断可以用任何优先级和嵌套
# 优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY(3)的中断不会被任何系统动作中断,可以嵌套,但不能使用任何FreeRTOS接口函数
# 优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY(3)的中断可以调用FreeRTOS函数,可以嵌套,但会被关键部分阻止运行
图61解释:
- 优先级为1到3的中断,当内核或程序在运行就不能运行中断。在这些中断优先级下的ISR可以使用中断安全版本的FreeRTOS函数。代码的核心部分会在第七章介绍。
- 优先级大于等于4的中断不会收到核心代码影响。也就是说调度器不会阻止中断立即执行,只受到硬件的限制。在这些优先级下的中断不能使用任何FreeRTOS函数。
- 通常需要非常精确时间的中断,比如电机控制都会以一个比
configMAX_SYSCALL_INTERRUPT_PRIORITY
更大的优先级运行,确保调度器不会在中断响应中引入抖动。
ARM Cortex-M 和ARM GIC用户的注意事项
Cortex-M一般不允许注册中断处理程序的,而且容易出错。为了方便开发,FreeRTOS的Cortex-M版本会自动检测中断配置,ff需要定义configASSERT(),configASSERT()在11.2章节介绍。
ARM Cortex内核的通用中断控制(GIC)用低的数字化优先级和高的逻辑化优先级。这看起来反直觉,还容易忘记。如果你希望逻辑化优先级更低,那么数字化优先级就更高。或者相反如果你想逻辑化优先级更高,那么数字化优先级就更低。
Cortex-M中断控制允许最多8位用于指定一个中断的优先级,也就是255种可能值。0是最高的优先级。尽管Cortex-M通常只实现了可能8位优先级子集。具体多少位优先级依赖于控制器组。
当只实现了可能8位的中断系统子集时,就只有最重要的位可以使用,抛弃那些没有实现的位。没有实现的位可以设置为任意值,但一般设置为1。图62有展示出来,它显示了一个二进制为101优先级在实现4个中断位的Cortex-M处理器中是如何存放的。
# 二进制为101中断优先级怎样保存于只实现了4位中断控制的Cortex-M处理器上。图62
---------------------------------
| 0 | 1 | 0 | 1 | 1 | 1 | 1 | 1 |
---------------------------------
Bit7Bit6Bit5Bit4Bit3Bit2Bit1Bit0
图62中二进制101已经被转移到了最重要的4为上,因为低4为没有实现。没有实现的4为设置为了1。
有一些库函数可能需要设定的优先级是经过移位操作后的优先级。用这样的函数指定优先级为图62中的优先级时,优先级参数就是十进制的95。十进制的95是二进制101移位四次,生成二进制101nnnn(这里的n是没有实现的位),没有实现的位用代替,就是十进制的95了。
有一些函数需要设定膜优先级是不进行移位操作到具体实现位的。用这种函数指定优先级为图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函数的中断不应当使用默认值。