登录后台

页面导航

本文编写于 256 天前,最后修改于 256 天前,其中某些信息可能已经过时。

中断管理

实时系统必需对环境中的事件做出响应。一个好的事件处理实现应当遵循以下策略:

  1. 如何检测这个事件?通常是使用中断,但也可以轮询等待。
  2. 使用中断时,中断服务程序(ISR)如何同非中断处理程序通信,这些代码如何组织,如何适应潜在的异步事件?

不同与任务这种软件相关的功能与硬件无关。中断处理程序是硬件功能,因此运行哪一个,何时运行ISR都是硬件控制的。任务只会在没有ISR运行时运行,因此最低优先级的硬件中断都会打断最高优先级的任务,一个任务是不可以抢占ISR的。

ISR中使用FreeRTOS函数

中断安全函数

经常需要在ISR中使用FreeRTOS提供的函数,但其中的很多函数不能在ISR中调用,因为它们是提供给任务使用的,会引起调度者进入阻塞状态。FreeRTOS的函数会提供两个版本,一个版本用于任务,另一个用于ISR。ISR版本的函数会有FRomISR的后缀。

使用中断安全函数的优点

对函数进行区分,可以让任务和ISR代码更加高效,中断也更加简单。如果不进行区分会出现以下问题:

  • 这个函数需要判断调用者是任务还是ISR。这些增加的逻辑会通过函数引入新的路径,使函数更长,更加复杂,更难以测试
  • 调用者不同,参数也可能不同
  • 每一个FreeRTOS函数都要提供决定执行上正文的机制
  • 对于不容易确定执行上下文的结构会要更多的附加代码,产生更复杂的用法和不标准的中断进入代码。

使用中断安全函数的缺点

一些函数拥有两个版本会更加高效,但同时也引入了新的问题:有时候在任务和ISR中都要调用一个非FreeRTOS的函数。解决方法如下:

  • 将ISR中的操作推迟到任务中,在任务中调用第三方函数
  • 如果支持中断嵌套,可以使用带FromISR的版本

xHigherPriorityTaskWoken参数

如果陷入到中断中,改变了上下文。中断退出执行时返回的任务可能和中断打断时的任务不是同一个任务。中断打断一个任务,返回不同的任务。

一些FreeRTOS函数可以将任务状态从阻塞态改变为就绪态。例如xQueueSendToBack(),它会将一个在阻塞态等待队列上数据的可用任务解除阻塞。如果解除阻塞状态的任务,其优先级高于当前运行任务的优先级,那么调度器会切换到最高优先级任务。什么时候发生切换依赖于具体调用函数的上下文:

  • 如果这个解除阻塞态的函数是在一个任务中被调用的:在FreeRTOSConfig中的configUSE_PREEMPTION设置为1,那么在调用这个函数时就会切换到最高优先级任务运行

  • 如果这个函数是在一个中断中调用。中断处理程序不会发生切换到高优先级任务的操作。作为替换,会设置一个变量用于提醒程序开发者有一个任务切换应当处理。那些中断安全函数有一个指针参数叫做xHigherPriorityTaskWoken,就是用来做这个提醒的。

    如果存在一个任务切换要处理,那么中断安全函数就会设置xHigherPriorityTaskWoken为pdTRUE。xHigherPriorityTaskWoken初始化时设置为pdFALSE。FreeRTOS函数只能设置xHigherPriorityTaskWoken参数一次。如果ISR调用多个FreeRTOS函数,那么每个函数调用都会传递相同的参数给xHigherPriorityTaskWoken。在中断安全函数中不进行上下文切换有以下几个原因:

    1. 避免不必要的上下文切换:一个任务处理可能需要不止一次中断。例如uart中断驱动接收一个字符串。对于uart中断来说,每一次接收到一个字符就切换到任务是很浪费的行为,最好是中断驱动接收到完整的字符串后再切换到相应的任务。
    2. 控制执行次序:中断是在不可预知的时间发生的。FreeRTOS在应用中的特定时间想暂时避免切换到不同的任务。
    3. 可移植性:这是用于访问所有FreeRTOS端口最简单的方法
    4. 性能:一些更小的处理器架构只允许在ISR结尾处切换上下文,要避免这些限制会引入更多的、更复杂的代码。同一个ISR中也允许调用多个FreeRTOS函数;同一个ISR中的上下文切换也不用生成更多的需求
    5. 在实时系统的tick中断中执行:tick中断处理程序是可定制的。导致其ISR中进行上下文切换依赖于使用的FreeRTOS函数

推迟中断处理操作

通常的选择是中断处理程序要尽量短小,原因如下:

  • 即使一个任务有一个很高的优先级,也只能在没有硬件中断时执行

  • ISRs可以在任务的任意食客打断任务

  • 依赖于FreeRTOS运行的处理器平台,一个ISR执行过程中可能不能接收新的中断

  • 程序设计者需要考虑变量、外设、内存被中断和任务同时访问的顺序和冲突

  • 有一些版本FreeRTOS允许中断嵌套,但是中断嵌套会增加复杂度,降低可预测性。中断越短,嵌套的可能性就越小。中断处理程序可能记录中断的原因,并清除中断。而其他具体的中断处理可以放在任务中进行,这样就可以让中断处理程序快速的退出,这就叫推迟中断操作。

    将ISR处理推迟到一个任务中,同样允许程序设计者使用函数对这种处理代码进行优先级管理,和其他的任务一摸一样。如果ISR中的操作推迟到的任务,其优先级相比其他任何任务优先级都高,那么这些操作会立即处理,就像在ISR中处理一样

# 中断操作推迟到最高优先级任务

ISR     |    --        |
Task2   |      ----    |
Task1   |----      ----|
       t1   t2t3  t4
# t1: 中断发生时任务1在运行
# t2: ISR运行,操作中断的外设,清除中断,唤醒任务2
# t3: 任务2优先级高于任务1,因此ISR返回后立即执行任务2,进行ISR推迟的操作
# t4: 任务2进入阻塞等待下一次中断,任务1再次进入运行状态

没有绝对规定什么时候要在ISR程序中进行全部操作,什么时候推迟部分操作到任务中。但以下操作需要考虑将操作推迟到任务中:

  • 中断处理中不重要的操作:比如中断只保存一个模拟信号为数字信号,可以基本确定在ISR中操作最好。但如果转换结果需要通过软件过滤器传递,可能最好就在任务中进行数据过滤。
  • 不方便放在ISR中的操作。比如向终端写数据,分配内存
  • 中断操作不确定

二进制信号量用于同步

中断安全版本的二进制信号量函数可以用于解锁一个任务,每当中断发生时,可以对任务和中断进行同步。这样大部分中断操作都可以在同步任务中实现,只有很快和很少的部分依然保留在ISR中。二进制信号量可以用于推迟ISR操作到任务。

中断操作推迟任务使用了一个阻塞的信号量take调用,意思是进入阻塞态,等待一个事件发生。当事件发生了,ISR会使用同一个信号量的give操作解锁这个任务,这样就实现了整个操作的处理。

获取信号量和提供信号量在不同的使用场景有不同的含义。在这个中断同步场景下,二进制信号量可以是一个只有一个元素的队列。队列中一次最多只有一个元素,因此不是空就是满。通过调用xSemaphoreTake(),中断处理推迟任务会试图带阻塞时间的从队列中读取数据,如果队列为空,就会进入阻塞状态。当事件发生时,ISR调用xSemaphoreGiveFromISR()存放一个钥匙到队列中,让队列满。此后中断处理推迟任务退出阻塞态,并取出钥匙,队列再次变空,当这个任务操作完成,又试图从队列中获取钥匙,进入阻塞态。

// 用二进制信号量同步中断和任务

-----------------------------------------------------
|             ----------                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()原型。
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()原型
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。
 */

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

// 周期生成软件中断实现
/* 这个例子中会用到多个软件中断。这个代码展示在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");
    }
}
// 中断处理推迟的任务实现。
static void vHandlerTask(void *pvParametera){
    for(;;){
        /* 用二进制信号量等待事件,二进制信号量在调度器开启前创建,更会在这个任务运行之前。二进制信号量不可用之前,任务会一直阻塞。意味着只有二进制信号量成功获取才会返回,所以不需要检测xSemaphoreTake()的返回值,只能返回pdPASS */
        xSemaphoreTake(xBinarySemaphore, portMAX_DELAY);
        /* 到这里事件已经发生,处理这个事件,这里只打印一个衣服串*/
        vPrintString("Handler task - processing event.\r\n");
    }
}
// 软件中断使用的ISR
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()函数实现
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(;;);
}
# 执行的输出结果

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.

改进上述例子中使用的任务

上述例子中用二进制信号量同步任务和中断,执行顺序如下:

  1. 中断发生
  2. ISR执行,提供信号量解锁操作任务
  3. 操作任务在ISR后立即执行,并取得信号量
  4. 任务处理事件,然后再次试图获取信号量,因为信号量不可用,进入阻塞状态

但是这种方式只适合中断发生频率较低的情景下。因为如果出现:第二个、第三个中断发生在第一个中断操作任务没有处理完之前时:

  • 当第二个ISR执行时,信号量为空,第二个ISR会提供一个信号量。任务处理完第一个中断后,就会立即处理第二个中断。

    # 第二个中断事件发生在第一个中断事件处理完之前的情形
    -----------------------------------------------------
    |             ----------           Task              |
    |             |          |           xSemaphoreTake()  |
    |             ----------         任务阻塞,等待信号量|
    -------------------------------------------   ----------
    ------------------------------------------------------         
    | 中断1                      ----------       任务    |
    | xSemaphoreGiveFromISR()-->|     O    |       离开阻塞态|
    | 中断发生提供信号量            ----------           |
    -------------------------------------------------------
    ------------------------------------------------------------
    |             ----------                   Task              |
    |             |         |-----> xSemaphoreTake() O         |
    |             ----------        成功获取信号量,再次不可用  |             
    ------------------------------------------------------------
    ---------------------------------------------------------           
    | 中断2                       ----------    任务           |
    | xSemaphoreGiveFromISR()-->|    O    |       仍然处理第|
    | 另外中断发生提供信号量       ----------   一个中断        |
    ----------------------------------------------------------
    ----------------------------------------------------------
    |             ----------      Task                     |
    |             |       |-----> xSemaphoreTake()         |
    |             ----------                               |             
    | 当处理完第一个中断,再次调用xSemaphoreTake(),              |
    | 因为另外一个中断已经发生,信号量已经可用                     |
    ----------------------------------------------------------
    ----------------------------------------------------------
    |             ----------                       Task        |
    |             |        |              vProcessEvent()   O  |
    |             ----------                               |             
    |         任务获取信号量,不进入阻塞,处理第二个中断。       |
    ----------------------------------------------------------
  • 当第三个ISR执行,信号量已经可用,阻碍ISR再次提供信号量,因此任务就不会知道第三个中断已经发生

    # 第一个中断未处理完成之前又发生两个中断事件的情形
    ----------------------------------------------------
    |             ----------           Task           |
    |             |        |        xSemaphoreTake()    |
    |             ----------      任务阻塞,等待信号量   |
    -----------------------------------------------------
    ---------------------------------------------------             
    | 中断1                       ----------    任务     |
    | xSemaphoreGiveFromISR()-->|    O   |  离开阻塞态  |
    | 中断发生提供信号量          ----------            |
    ---------------------------------------------------
    ---------------------------------------------------
    |             ----------        Task              |
    |             |        |-----> xSemaphoreTake() O |
    |             ----------成功获取信号量,信号量再次  |
    |                       不可用。任务开始处理中断1    |             
    ---------------------------------------------------
    ---------------------------------------------------             
    | 中断2                       ----------    任务    |
    | xSemaphoreGiveFromISR()-->|    O   |    仍然处 |
    | 处理第一个中断时第二个中断     ----------   理中断1 |
    | 发生。ISR再次提供信号量,                          |
    | 中断不会发生丢失。                                 |
    ---------------------------------------------------
    ---------------------------------------------------             
    | 中断                       ----------    任务    |
    | xSemaphoreGiveFromISR()-->|    O   |  仍然处理第 |
    |                           ----------  一个中断   |
    | 在处理第一个中断事件时,发生第三个中断事件,           |
    | ISR不能再次提供信号量,因为信号量已经可用,           | 
    | 造成事件丢失。                                    |
    ---------------------------------------------------
    ---------------------------------------------------
    |             ----------        Task              |
    |             |   O    |-----> xSemaphoreTake()   |
    |             ----------                          |             
    | 当处理完第一个事件,再次调用xSemaphoreTake(),       |
    | 因为另外一个中断已经发生,信号量已经可用               |
    ---------------------------------------------------
    ---------------------------------------------------
    |             ----------        Task              |
    |             |        |      vProcessEvent()   O |
    |             ----------                          |             
    | 任务获取信号量,不进入阻塞,处理第二个中断。         |
    ---------------------------------------------------
    ---------------------------------------------------
    |             ----------        Task              |
    |             |        |        xSemaphoreTake()  |
    |             ----------                          |
    | 当处理完成第二个事件,再次调用xSemaphoreTake(),      |
    | 但信号量不可用,任务进入阻塞,等待下一次中断。          |
    | 尽管这里的第三个中断还没有进行处理。                  |
    ---------------------------------------------------

在实际的应用中,中断是由硬件生成的,可以发生在任何时间。因此为了减少错过中断,需要将中断处理推迟任务结构化。以使它能处理两次调用xSemaphoreTake()函数之间所有已经发生的中断

// 使用一个UART接收处理实例,中断处理推迟任务推荐结构
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个。任务处理事件时要获取信号量,每次获取信号量会引起信号量的值(数量)自减1。这里的数量值和已经发生和已经处理事件次数是不同的。用于计量事件次数的普通信号量初始创建时的值是0。
  2. 资源管理:这种情况下,这个数值会用于表示资源的数量。对于在管理范围的资源,任务在使用前要先获取一个信号量(就是要减少普通信号量值)。当这个数值到0时,就不再有可用的资源了。当任务使用资源完毕,它会给普通信号量提供资源(就是会增加普通信号量值)。普通信号量用于管理资源时,初始创建普通信号量时值等于可用资源数量。
# 使用一个普通信号量计量事件
-------------------------------------------------------------------------
|                       信号量数值为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()函数原型
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);
/* 参数
 * uxMaxCount: 这个值是普通信号量可以达到的最大值。对于连续的队列结构,这个值就是队列长度有效值。
 * 当用普通信号量管理可以访问的资源数量,uxMaxCount应该设置为资源可用数量。
 * uxInitValue: 普通信号量创建时的初始值。
 * 如果普通信号量用作计数或时间标记,uxInitValue应该初始化为0,因为假设创建信号量时没有事件发生。
 * 当普通信号量用作管理可以访问的资源集合,uxInitialCount应该初始化为uxMaxCount,因为假设创建普通信号量是所有资源都是可以访问的。
 * 返回值:
 * 如果返回NULL,普通信号量可能因为没有足够的堆空间分配给信号量数据结构而创建失败。第二章中提供了更加详细的堆管理说明。
 * 如果返回一个非NULL的值,表示信号量成功创建普通信号量。这个返回值就是成功创建的信号量的引用句柄。
 */

推迟操作放到实时系统守护进程

中断处理推迟需要程序设计者为每个使用中断处理推迟技术的中断创建一个任务。也可以使用xTimerPendFunctionCallFromISR()函数推迟中断处理到实时系统守护任务中,而不用为每个中断创建一个任务。推迟中断处理到守护任务被叫做"集中推迟中断处理"。
xTimerPendFunctionCallFromISR()和xTimerPendFunctionCall()函数使用同一个时间任务队列发送一个"执行函数"命令给守护任务。这些函数发送给守护任务,会在守护任务上下文执行。

集中推迟中断处理的优势:

  • 更少的代码量
    不用每个中断都创建一个任务
  • 模型简单
    中断处理推迟函数是标准的C函数

集中推迟中断处理的劣势:

  • 更不灵活
    不能为每一个中断推迟任务设置优先级。每个中断推迟处理函数都是以守护任务优先级运行。守护任务优先级是在FreeRTOSConfig.h中的configTIMER_TASK_PRIORITY设置,在编译时就确定的。
  • 更少的决策性
    xTimerPendFunctionCallFromISR()发送一个命令给时间命令队列。在xTimerPendFunctionCallFromISR()发送"执行函数"命令之前,会首先执行已经在时间命令队列中的命令。

不同的中断有不同的时间限制,所以应用中通常两种中断推迟处理方法都会用上。

// xTimerPendFunctionCallFromISR()函数原型
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。第五章有讲如何设置命令队列长度。
 */

集中推迟中断操作

// 软件中断使用
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);
}
// 中断需要的推迟操作函数
static void vDeferredHandingFunction(void *pvParametera1, uint32_t ulParameter2){
    /* 这里处理事件,这个实例中只打印一个字符串和ulParameter2的值。pvParametera1在这里没有使用*/
    vPrintStringAndNumber("Handler function - Processing event ", ulParameter2);
}
// main()函数实现
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(;;);
}
# 运行输出
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个消息。之后就是空闲任务运行。

中断嵌套

很容易在任务优先级和中断优先级之间产生混淆。这里说的中断优先级,是指各个ISR执行的关系。任务优先级没有办法和中断优先级关联起来。硬件会决定什么时候ISR执行,软件决定什么时候任务运行。一个ISR用于响应一个硬件中断,它会中断一个任务,但任务不能抢占ISR。

博主已关闭本页面的评论功能