freertos初学

1.下载 FreeRTOS V9.0.0 源码

1.1介绍

  • 在移植之前,我们首先要获取到 FreeRTOS 的官方的源码包。

  • 这里我们提供两个下载链 接 , 一 个 是 官 网FreeRTOS™ - FreeRTOS™ , 另 外 一 个 是 代 码 托 管 网 站https://sourceforge.net/projects/freertos/files/FreeRTOS/。这里我们演示如何在代码托管网站里面下载。打开网站链接之后,我们选择 FreeRTOS 的最新版本 V9.0.0(2016 年),尽管现在 FreeRTOS 的版本已经更新到 V10.0.1 了,但是我们还是选择 V9.0.0,因为内核很稳定,并且网上资料很多,因为 V10.0.0 版本之后是亚马逊收购了 FreeRTOS 之后才出来的版本,主要添加了一些云端组件,我们本书所讲的 FreeRTOS 是实时内核,采用 V9.0.0 版本足以。

  • 中文网站FreeRTOS task states and state transitions described

1.2下载

代 码 托 管 网 站FreeRTOS Real Time Kernel (RTOS) - Browse /FreeRTOS at SourceForge.net

版本选择V9.00即可。

 

bedf3ae4595d7ceedc5c728314cf5d98.png

选择FreeRTOSV9.00.zip即可

 

0869c536a0f9f2616115172cd2c74544.png

等待几秒会自动下载好

2.FreeRTOS 文件夹内容简介

  • FreeRTOS 包含 Demo 例程和内核源码(比较重要,我们就需要提取该目录下的大部分文件),具体详见下图。

  • FreeRTOS 文件夹下的 Source 文件夹里面包含的是 FreeRTOS 内核的源代码,我们移植FreeRTOS 的时候就需要这部分源代码;

  • FreeRTOS 文件夹下的Demo 文件夹里面包含了 FreeRTOS 官方为各个单片机移植好的工程代码,FreeRTOS 为了推广自己,会给各种半导体厂商的评估板写好完整的工程程序,这些程序就放在 Demo 这个目录下,这部分 Demo 非常有参考价值。

  • 我们把 FreeRTOS 到 STM32 的时候,FreeRTOSConfig.h 这个头文件就是从这里拷贝过来的,下面我们对 FreeRTOS 的文件夹进行分析说明。

3.任务管理

3.1基本概念

  1. 从系统的角度看,任务是竞争系统资源的最小运行单元。

  2. FreeRTOS 是一个支持多任务的操作系统。在 FreeRTOS 中,任务可以使用或等待 CPU、使用内存空间等系统资源,并独 立 于 其 它 任 务 运 行 , 任 何 数 量 的 任 务 可 以 共 享 同 一 个 优 先 级 , 如 果 宏configUSE_TIME_SLICING 定义为 1,处于就绪态的多个相同优先级任务将会以时间片切换的方式共享处理器。简而言之: FreeRTOS 的任务可认为是一系列独立任务的集合。每个任务在自己的环境中运行。在任何时刻,只有一个任务得到运行,FreeRTOS 调度器决定运行哪个任务。调度器会不断的启动、停止每一个任务,宏观看上去所有的任务都在同时在执行。作为任务,不需要对调度器的活动有所了解,在任务切入切出时保存上下文环境(寄存器值、堆栈内容)是调度器主要的职责

  3. 为了实现这点,每个 FreeRTOS 任务都需要有自己的栈空间。

  4. 当任务切出时,它的执行环境会被保存在该任务的栈空间中,这样当任务再次运行时,就能从堆栈中正确的恢复上次的运行环境,任务越多,需要的堆栈空间就越大,而一个系统能运行多少个任务,取决于系统的可用的 SRAM

  5. FreeRTOS 的可以给用户提供多个任务单独享有独立的堆栈空间,系统可以决定任务的状态,决定任务是否可以运行,同时还能运用内核的 IPC 通信资源,实现了任务之间的通信,帮助用户管理业务程序流程。

  6. 这样用户可以将更多的精力投入到业务功能的实现中。FreeRTOS 中的任务是抢占式调度机制,高优先级的任务可打断低优先级任务,低优先级任务必须在高优先级任务阻塞或结束后才能得到调度。同时 FreeRTOS 也支持时间片轮转调度方式,只不过时间片的调度是不允许抢占任务的 CPU 使用权。

  7. 任务通常会运行在一个死循环中,也不会退出,如果一个任务不再需要,可以调用FreeRTOS 中的任务删除 API 函数接口显式地将其删除。

3.2任务调度器的基本概念

  1. FreeRTOS 中提供的任务调度器是基于优先级的全抢占式调度:在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的。系统理论上可以支持无数个优先级(0 ~ N,优先级数值越小的任务优先级越低,0 为最低优先级,分配给空闲任务使用,一般不建议用户来使用这个优先级。假如使能了 configUSE_PORT_OPTIMISED_TASK_SELECTION 这个宏(在 FreeRTOSConfig.h 文件定义),一般强制限定最大可用优先级数目为 32。在一些资源比较紧张的系统中,可以根据实际情况选择只支持 8 个或 32 个优先级的系统配置。

  2. 在系统中,当有比当前任务优先级更高的任务就绪时,当前任务将立刻被换出,高优先级任务抢占处理器运行。一个操作系统如果只是具备了高优先级任务能够“立即”获得处理器并得到执行的特点,那么它仍然不算是实时操作系统。因为这个查找最高优先级任务的过程决定了调度时间是否具有确定性,例如一个包含 n 个就绪任务的系统中,如果仅仅从头找到尾,那么这个时间将直接和 n 相关,而下一个就绪任务抉择时间的长短将会极大的影响系统的实时性。FreeRTOS 内核中采用两种方法寻找最高优先级的任务,第一种是通用的方法,在就绪链表中查找从高优先级往低查找 uxTopPriority,因为在创建任务的时候已经将优先级进行排序,查找到的第一个 uxTopPriority 就是我们需要的任务,然后通过 uxTopPriority 获取对应的任务控制块。第二种方法则是特殊方法,利用计算前导零指令 CLZ,直接在uxTopReadyPriority 这个 32 位的变量中直接得出 uxTopPriority,这样子就知道哪一个优先级任务能够运行,这种调度算法比普通方法更快捷,但受限于平台(在 STM32 中我们就使用这种方法)。

  3. FreeRTOS 内核中也允许创建相同优先级的任务。相同优先级的任务采用时间片轮转方式进行调度(也就是通常说的分时调度器),时间片轮转调度仅在当前系统中无更高优先级就绪任务存在的情况下才有效。为了保证系统的实时性,系统尽最大可能地保证高优先级的任务得以运行。任务调度的原则是一旦任务状态发生了改变,并且当前运行的任务优先级小于优先级队列组中任务最高优先级时,立刻进行任务切换(除非当前系统处于中断处理程序中或禁止任务切换的状态)。

3.3任务状态迁移

FreeRTOS 系统中的每一个任务都有多种运行状态,他们之间的转换关系是怎么样的呢?

  1. 从运行态任务变成阻塞态,或者从阻塞态变成就绪态,这些任务状态是如何进行迁移?下面就让我们一起了解任务状态迁移吧,具体见图

  2.  

    050ed77782ee978400e4fe4e44f05f6d.png

  3. 图中 (1)创建任务→就绪态(Ready):任务创建完成后进入就绪态,表明任务已准备就绪,随时可以运行,只等待调度器进行调度。

  4. 图中 (2)就绪态→运行态(Running):发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运行态。

  5. 图 中(3)运行态→就绪态:有更高优先级任务创建或者恢复后,会发生任务调度,此刻就绪列表中最高优先级任务变为运行态,那么原先运行的任务由运行态变为就绪态,依然在就绪列表中,等待最高优先级的任务运行完毕继续运行原来的任务(此处可以看做是 CPU 使用权被更高优先级的任务抢占了)。

  6. 图 中(4)运行态→阻塞态(Blocked):正在运行的任务发生阻塞挂起、延时、读信号量等待)时,该任务会从就绪列表中删除,任务状态由运行态变成阻塞态,然后发生任务切换,运行就绪列表中当前最高优先级任务。

  7. 图 中(5)阻塞态→就绪态:阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态;如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。

  8. 图 中(6) (7) (8):就绪态、阻塞态、运行态→挂起态(Suspended):任务可以通过调用 vTaskSuspend() API 函数都可以将处于任何状态的任务挂起,被挂起的任务得不到CPU 的使用权,也不会参与调度,除非它从挂起态中解除。

  9. 图 中(9)挂起态→就绪态:把 一 个 挂 起 状态 的 任 务 恢复的 唯 一 途 径 就 是调 用 vTaskResume() 或 vTaskResumeFromISR() API 函数,如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。

3.4 任务状态的概念

FreeRTOS 系统中的每一任务都有多种运行状态。系统初始化完成后,创建的任务就可以在系统中竞争一定的资源,由内核进行调度。

  1. 任务状态通常分为以下四种:

  2. 就绪(Ready):该任务在就绪列表中,就绪的任务已经具备执行的能力,只等待调度器进行调度,新创建的任务会初始化为就绪态。

  3. 运行(Running):该状态表明任务正在执行,此时它占用处理器,FreeRTOS 调度器选择运行的永远是处于最高优先级的就绪态任务,当任务被运行的一刻,它的任务状态就变成了运行态。

  4. 阻塞(Blocked):如果任务当前正在等待某个时序或外部中断,我们就说这个任务处于阻塞状态,该任务不在就绪列表中。包含任务被挂起、任务被延时、任务正在等待信号量、读写队列或者等待读写事件等。

  5. 挂起态(Suspended):处于挂起态的任务对调度器而言是不可见的,让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend()函数;而 把 一 个 挂 起 状态 的任 务 恢复的 唯 一 途 径 就 是 调 用 vTaskResume() 或 vTaskResumeFromISR()函数,我们可以这么理解挂起态与阻塞态的区别,当任务有较长的时间不允许运行的时候,我们可以挂起任务,这样子调度器就不会管这个任务的任何信息,直到我们调用恢复任务的 API 函数;而任务处于阻塞态的时候,系统还需要判断阻塞态的任务是否超时,是否可以解除阻塞。

3.5常用的任务函数讲解

相信大家通过第一部分章节的学习,对任务创建以及任务调度的实现已然掌握了,下面就补充一些 FreeRTOS 提供给我们对任务操作的一些常用函数。

3.5.1vTaskSuspend

  1. 挂起指定任务

  2. 被挂起的任务绝不会得到 CPU 的使用权,不管该任务具有什么优先级。任务可以通过调用 vTaskSuspend()函数都可以将处于任何状态的任务挂起,被挂起的任务得不到 CPU 的使用权,也不会参与调度,它相对于调度器而言是不可见的,除非它从挂起态中解除。任务挂起是我们经常使用的一个函数。

  3. 注意:如果想要使用任务挂起函数 vTaskSuspend()则必须将宏定义INCLUDE_vTaskSuspend 配置为 1。

  4. 任务可以调用 vTaskSuspend()这个函数来挂起任务自身,但是在挂起自身的时候会进行一次任务上下文切换,需要挂起自身就将 xTaskToSuspend 设置为 NULL 传递进来即可。

  5. 无论任务是什么状态都可以被挂起,只要调用了 vTaskSuspend()这个函数就会挂起成功,不论是挂起其他任务还是挂起任务自身

  6. 任务的挂起与恢复函数在很多时候都是很有用的,比如我们想暂停某个任务运行一段时间,但是我们又需要在其恢复的时候继续工作,那么删除任务是不可能的,因为删除了任务的话,任务的所有的信息都是不可能恢复的了,删除是完完全全删除了,里面的资源都被系统释放掉,但是挂起任务就不会这样子,调用挂起任务函数,仅仅是将任务进入挂起态,其内部的资源都会保留下来,同时也不会参与系统中任务的调度,当调用恢复函数的时候,整个任务立即从挂起态进入就绪态,并且参与任务的调度,如果该任务的优先级是当前就绪态优先级最高的任务,那么立即会按照挂起前的任务状态继续执行该任务,从而达到我们需要的效果,注意,是继续执行,也就是说,挂起任务之前是什么状态,都会被系统保留下来,在恢复的瞬间,继续执行。

  7. 这个任务函数的使用方法是很简单的,只需把任务句柄传递进来即可,vTaskSuspend()会根据任务句柄的信息将对应的任务挂起。

  8. /**************************** 任务句柄 ********************************/
    /*
     * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
     * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
     * 这个句柄可以为 NULL。
    */
    static TaskHandle_t LED_Task_Handle = NULL;/* LED 任务句柄 */ 
    ​
    static void KEY_Task(void* parameter)
    {
        while (1) {
            if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) {
                /* K1 被按下 */
                printf("挂起 LED 任务!\n");
                vTaskSuspend(LED_Task_Handle);/* 挂起 LED 任务 */ 
            }
            vTaskDelay(20);/* 延时 20 个 tick */
        }
    }

3.5.2vTaskSuspendAll

  1. 这个函数就是比较有意思的,将所有的任务都挂起,其实源码很简单,也很有意思,不管三七二十一将调度器锁定,并且这个函数是可以进行嵌套的,说白了挂起所有任务就是挂起任务调度器。

  2. 调度器被挂起后则不能进行上下文切换,但是中断还是使能的。

  3. 当调度器被挂起的时候,如果有中断需要进行上下文切换, 那么这个任务将会被挂起,在调度器恢复之后才执行切换任务。

  4. 调度器恢复可以调 用 xTaskResumeAll() 函数,调 用了多少次 的 vTaskSuspendAll() 就 要调用多少 次xTaskResumeAll()进行恢复,xTaskResumeAll()的源码会在恢复任务函数中讲解。

  5. void vTaskSuspendAll( void )
    {
        ++uxSchedulerSuspended; (1)
    }

3.5.3vTaskResume

  1. 任务恢复函数

  2. 既然有任务的挂起,那么当然一样有恢复,不然任务怎么恢复呢,任务恢复就是让挂起的任务重新进入就绪状态,恢复的任务会保留挂起前的状态信息,在恢复的时候根据挂起时的状态继续运行。

  3. 如果被恢复任务在所有就绪态任务中,处于最高优先级列表的第一位,那么系统将进行任务上下文的切换。下面一起看看任务恢复函数 vTaskResume()的源码。

  4. uxSchedulerSuspended 用于记录调度器是否被挂起,该变量默认初始值为 pdFALSE,表明调度器是没被挂起的,每调用一次 vTaskSuspendAll()函数就将变量加一,用于记录调用了多少次 vTaskSuspendAll()函数。

  5. vTaskResume()函数用于恢复挂起的任务。无论任务在挂起时候调用过多少次这个TaskSuspend()函数,也只需调用一次 vTaskResume ()函数即可将任务恢复运行,当然,无论调用多少次的 vTaskResume()函数,也只在任务是挂起态的时候才进行恢复。下面来看看任务恢复函数 vTaskResume()的使用实例。

/*
 * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
 * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
 * 这个句柄可以为 NULL。
 */

static TaskHandle_t LED_Task_Handle = NULL;/* LED 任务句柄 */ 

static void KEY_Task(void* parameter)
{
    while (1) {
        if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) {
            /* K2 被按下 */
            printf("恢复 LED 任务!\n");
            vTaskResume(LED_Task_Handle);/* 恢复 LED 任务! */ 
        }
        vTaskDelay(20);/* 延时 20 个 tick */
    }
}

3.5.4xTaskResumeFromISR

  1. xTaskResumeFromISR()与 vTaskResume()一样都是用于恢复被挂起的任务,不一样的是 xTaskResumeFromISR() 专 门 用 在 中 断 服 务 程 序 中 。

  2. 无 论 通 过 调 用 一 次 或 多 次vTaskSuspend()函数而被挂起的任务,也只需调用一次 xTaskResumeFromISR()函数即可解挂。

  3. 要想使用该函数必须在 FreeRTOSConfig.h 中 把 INCLUDE_vTaskSuspend 和INCLUDE_vTaskResumeFromISR 都定义为 1 才有效。任务还没有处于挂起态的时候,调用xTaskResumeFromISR()函数是没有任何意义的,xTaskResumeFromISR()源码具体见代码清单。

  4. 使用 xTaskResumeFromISR()的时候有几个需要注意的地方

  5. 当函数的返回值为 pdTRUE 时:恢复运行的任务的优先级等于或高于正在运行的任 务 , 表 明 在 中 断 服 务 函 数 退 出 后 必 须 进 行 一 次 上 下 文 切 换 , 使 用portYIELD_FROM_ISR()进行上下文切换。

  6. 当函数的返回值为 pdFALSE 时:恢复运行的任务的优先级低于当前正在运行的任务,表明在中断服务函数退出后不需要进行上下文切换。

  7. xTaskResumeFromISR() 通常被认为是一个危险的函数,因为它的调用并非是固定的,中断可能随时来来临。

  8. 所以,xTaskResumeFromISR()不能用于任务和中断间的同步,如果中断恰巧在任务被挂起之前到达,这就会导致一次中断丢失(任务还没有挂起,调用 xTaskResumeFromISR()函数是没有意义的,只能等下一次中断)。

  9. 这种情况下,可以使用信号量或者任务通知来同步就可以避免这种情况。xTaskResumeFromISR()的使用方法具体见代码清单 16-7 加粗部分。

void vAnExampleISR( void )
{
    BaseType_t xYieldRequired;

    /* 恢复被挂起的任务 */ 
    xYieldRequired = xTaskResumeFromISR( xHandle ); 

    if ( xYieldRequired == pdTRUE ) { 
        /* 执行上下文切换, ISR 返回的时候将运行另外一个任务 */ 
        portYIELD_FROM_ISR(); 
    } 
} 

3.5.5xTaskResumeAll

  1. 之前我们讲解过 vTaskSuspendAll()函数,那么当调用了 vTaskSuspendAll()函数将调度器挂起,想要恢复调度器的时候我们就需要调用 xTaskResumeAll()函数。

  2. xTaskResumeAll 函数的使用方法很简单,但是要注意,调用了多少次vTaskSuspendAll()函数就必须同样调用多少次 xTaskResumeAll()函数。

void vDemoFunction( void )
{
    vTaskSuspendAll();
    /* 处理 xxx 代码 */
    5 vTaskSuspendAll();
    /* 处理 xxx 代码 */
    7 vTaskSuspendAll();
    /* 处理 xxx 代码 */

    xTaskResumeAll(); 
    xTaskResumeAll(); 
    xTaskResumeAll(); 
}

3.5.6vTaskDelete

  1. vTaskDelete()用于删除一个任务。当一个任务删除另外一个任务时,形参为要删除任务创建时返回的任务句柄,如果是删除自身, 则形参为 NULL。

  2. 要想使用该函数必须在FreeRTOSConfig.h 中把 INCLUDE_vTaskDelete 定义为 1,删除的任务将从所有就绪,阻塞,挂起和事件列表中删除。

  3. 删除任务时,只会自动释放内核本身分配给任务的内存。

  4. 应用程序(而不是内核)分配给任务的内存或任何其他资源必须是删除任务时由应用程序显式释放。

  5. 怎么理解这句话?就好像在某个任务中我申请了一大块内存,但是没释放就把任务删除,这块内存在任务删除之后不会自动释放的,所以我们应该在删除任务之前就把任务中的这些资源释放掉,然后再进行删除,否则很容易造成内存泄漏,删除任务的使用很简单。

    /* 创建一个任务,将创建的任务句柄存储在 DeleteHandle 中*/
    TaskHandle_t DeleteHandle;
    
    if (xTaskCreate(DeleteTask,
                    "DeleteTask",
                    STACK_SIZE,
                    NULL,
                    PRIORITY,
                    &DeleteHandle) != pdPASS )
    {
        /* 创建任务失败,因为没有足够的堆内存可分配。 */
    }
    
    void DeleteTask( void )
    {
        /* 用户代码 xxxxx */
        /* ............ */
    
        /* 删除任务本身 */ 
        vTaskDelete( NULL ); 
    }
    
    /* 在其他任务删除 DeleteTask 任务 */ 
    vTaskDelete( DeleteHandle );

     

3.5.7vTaskDelay

  1. vTaskDelay()在我们任务中用得非常之多,每个任务都必须是死循环,并且是必须要有阻塞的情况,否则低优先级的任务就无法被运行了。

  2. 要想使用 FreeRTOS 中的 vTaskDelay()函数必须在 FreeRTOSConfig.h 中把 INCLUDE_vTaskDelay 定义为 1 来使能。代码清单 16-13 vTaskDelay 函数原型

void vTaskDelay( const TickType_t xTicksToDelay )
  1. vTaskDelay()用于阻塞延时,调用该函数后,任务将进入阻塞状态,进入阻塞态的任务将让出 CPU 资源

  2. 延时的时长由形参 xTicksToDelay 决定,单位为系统节拍周期, 比如系统的时钟节拍周期为 1ms,那么调用vTaskDelay(1)的延时时间则为1ms。

  3. vTaskDelay()延时是相对性的延时,它指定的延时时间是从调用 vTaskDelay()结束后开始计算的,经过指定的时间后延时结束。

  4. 比如 vTaskDelay(100), 从调用 vTaskDelay()结束后,任务进入阻塞状态,经过 100 个系统时钟节拍周期后,任务解除阻塞。因此,vTaskDelay()并不适用与周期性执行任务的场合。此外,其它任务和中断活动, 也会影响到 vTaskDelay()的调用(比如调用前高优先级任务抢占了当前任务),进而影响到任务的下一次执行的时间。

  5. 任务的延时在实际中运用特别多,因为需要暂停一个任务,让任务放弃 CPU,延时结束后再继续运行该任务,如果任务中没有阻塞的话,比该任务优先级低的任务则无法得到CPU 的使用权,就无法运行

void vTaskA( void * pvParameters )
{
    while (1) {
        // ...
        // 这里为任务主体代码
        // ...
​
        /* 调用相对延时函数,阻塞 1000 个 tick */ 
        vTaskDelay( 1000 ); 
    }
}

3.5.8vTaskDelayUntil

  1. 在 FreeRTOS 中,除了相对延时函数,还有绝对延时函数 vTaskDelayUntil(),这个绝对延时常用于较精确的周期运行任务,比如我有一个任务,希望它以固定频率定期执行,而不受外部的影响,任务从上一次运行开始到下一次运行开始的时间间隔是绝对的,而不是相对的。

  2. 要想使用该函数必须在 FreeRTOSConfig.h 中把 INCLUDE_vTaskDelayUntil 定义为 1 来使能。vTaskDelayUntil() 与 vTaskDelay () 一 样都 是 用 来 实 现 任 务 的 周 期 性 延 时。

  3. 但vTaskDelay ()的延时是相对的,是不确定的,它的延时是等 vTaskDelay ()调用完毕后开始计算的。并且 vTaskDelay ()延时的时间到了之后,如果有高优先级的任务或者中断正在执行,被延时阻塞的任务并不会马上解除阻塞,所有每次执行任务的周期并不完全确定。

  4. 而vTaskDelayUntil()延时是绝对的,适用于周期性执行的任务。当(*pxPreviousWakeTime + xTimeIncrement)时间到达后,vTaskDelayUntil()函数立刻返回,如果任务是最高优先级的,那么任务会立马解除阻塞,所以说 vTaskDelayUntil()函数的延时是绝对性的

  5. 下面看看 vTaskDelayUntil()的使用方法,注意了,这 vTaskDelayUntil()的使用方法与vTaskDelay()不一样。

  6. 在使用的时候要将延时时间转化为系统节拍,在任务主体之前要调用延时函数。

  7. 任务会先调用 vTaskDelayUntil()使任务进入阻塞态,等到时间到了就从阻塞中解除,然后执行主体代码,任务主体代码执行完毕。

  8. 会继续调用 vTaskDelayUntil()使任务进入阻塞态,然后就是循环这样子执行。即使任务在执行过程中发生中断,那么也不会影响这个任务的运行周期,仅仅是缩短了阻塞的时间而已,到了要唤醒的时间依旧会将任务唤醒

void vTaskA( void * pvParameters )
{
    /* 用于保存上次时间。调用后系统自动更新 */ 
    static portTickType PreviousWakeTime; 
    /* 设置延时时间,将时间转为节拍数 */ 
    const portTickType TimeIncrement = pdMS_TO_TICKS(1000); 

    /* 获取当前系统时间 */ 
    PreviousWakeTime = xTaskGetTickCount(); 

    while (1)
    {
        /* 调用绝对延时函数,任务时间间隔为 1000 个 tick */ 
        vTaskDelayUntil( &PreviousWakeTime,TimeIncrement ); 
        // ...
        // 这里为任务主体代码 
        // ...
    }
}

3.5.9任务的设计要点

作为一个嵌入式开发人员,要对自己设计的嵌入式系统要了如指掌,任务的优先级信息,任务与中断的处理,任务的运行时间、逻辑、状态等都要知道,才能设计出好的系统,所以,在设计的时候需要根据需求制定框架。在设计之初就应该考虑下面几点因素:任务运行的上下文环境、任务的执行时间合理设计

  • FreeRTOS 中程序运行的上下文包括:

    • 中断服务函数。

    • 普通任务。

    • 空闲任务

  1. 中断服务函数:中断服务函数是一种需要特别注意的上下文环境,它运行在非任务的执行环境下(一般为芯片的一种特殊运行模式(也被称作特权模式)),在这个上下文环境中不能使用挂起当前任务的操作,不允许调用任何会阻塞运行的 API 函数接口。另外需要注意的是,中断服务程序最好保持精简短小,快进快出,一般在中断服务函数中只做标记事件的发生,然后通知任务,让对应任务去执行相关处理,因为中断服务函数的优先级高于任何优先级的任务,如果中断处理时间过长,将会导致整个系统的任务无法正常运行。所以在设计的时候必须考虑中断的频率、中断的处理时间等重要因素,以便配合对应中断处理任务的工作。

  2. 任务:任务看似没有什么限制程序执行的因素,似乎所有的操作都可以执行。但是做为一个优先级明确的实时系统,如果一个任务中的程序出现了死循环操作(此处的死循环是指没有阻塞机制的任务循环体),那么比这个任务优先级低的任务都将无法执行,当然也包括了空闲任务因为死循环的时候,任务不会主动让出 CPU,低优先级的任务是不可能得到。CPU 的使用权的,而高优先级的任务就可以抢占 CPU。这个情况在实时操作系统中是必须注意的一点,所以在任务中不允许出现死循环。如果一个任务只有就绪态而无阻塞态,势必会影响到其他低优先级任务的执行,所以在进行任务设计时,就应该保证任务在不活跃的时候,任务可以进入阻塞态以交出 CPU 使用权,这就需要我们自己明确知道什么情况下让任务进入阻塞态,保证低优先级任务可以正常运行。在实际设计中,一般会将紧急的处理事件的任务优先级设置得高一些

  3. 空闲任务:空闲任务(idle 任务)是 FreeRTOS 系统中没有其他工作进行时自动进入的系统任务。因为处理器总是需要代码来执行——所以至少要有一个任务处于运行态。FreeRTOS 为了保证这一点,当调用 vTaskStartScheduler()时,调度器会自动创建一个空闲任务,空闲任务是一个非常短小的循环。用户可以通过空闲任务钩子方式,在空闲任务上钩入自己的功能函数。通常这个空闲任务钩子能够完成一些额外的特殊功能,例如系统运行状态的指示,系统省电模式等。除了空闲任务钩子,FreeRTOS 系统还把空闲任务用于一些其他的功能,比如当系统删除一个任务或一个动态任务运行结束时,在执行删除任务的时候,并不会释放任务的内存空间,只会将任务添加到结束列表中,真正的系统资源回收工作在空闲任务完成,空闲任务是唯一一个不允许出现阻塞情况的任务,因为 FreeRTOS 需要保证系统永远都有一个可运行的任务。对于空闲任务钩子上挂接的空闲钩子函数,它应该满足以下的条件:永远不会挂起空闲任务;不应该陷入死循环,需要留出部分时间用于系统处理系统资源回收。

  4. 任务的执行时间:任务的执行时间一般是指两个方面,一是任务从开始到结束的时间,二是任务的周期。

  5. 在系统设计的时候这两个时间候我们都需要考虑,例如,对于事件 A 对应的服务任务Ta,系统要求的实时响应指标是 10ms,而 Ta 的最大运行时间是 1ms,那么 10ms 就是任务Ta 的周期,1ms 则是任务的运行时间,简单来说任务 Ta 在 10ms 内完成对事件 A 的响应即可。此时,系统中还存在着以 50ms 为周期的另一任务 Tb,它每次运行的最大时间长度是 100us。在这种情况下,即使把任务 Tb 的优先级抬到比 Ta 更高的位置,对系统的实时性指标也没什么影响,因为即使在 Ta 的运行过程中,Tb 抢占了 Ta 的资源,等到 Tb 执行完毕,消耗的时间也只不过是 100us,还是在事件 A 规定的响应时间内(10ms),Ta 能够安全完成对事件 A 的响应。但是假如系统中还存在任务 Tc,其运行时间为 20ms,假如将 Tc的优先级设置比 Ta 更高,那么在 Ta 运行的时候,突然间被 Tc 打断,等到 Tc 执行完毕,那 Ta 已经错过对事件 A(10ms)的响应了,这是不允许的。

  6. 所以在我们设计的时候,必须考虑任务的时间,一般来说处理时间更短的任务优先级应设置更高一些

3.5.10任务管理实验

任务管理实验是将任务常用的函数进行一次实验,在野火 STM32 开发板上进行该试验,通过创建两个任务,一个是 LED 任务,另一个是按键任务,LED 任务是显示任务运行的状态,而按键任务是通过检测按键的按下与否来进行对 LED 任务的挂起与恢复,具体见代码。

​
/**************************** 任务句柄 ********************************/
/* 
 * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
 * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
 * 这个句柄可以为NULL。
 */
static TaskHandle_t AppTaskCreate_Handle = NULL;/* 创建任务句柄 */
static TaskHandle_t LED_Task_Handle = NULL;/* LED任务句柄 */
static TaskHandle_t KEY_Task_Handle = NULL;/* KEY任务句柄 */
​
/********************************** 内核对象句柄 *********************************/
/*
 * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
 * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
 * 们就可以通过这个句柄操作这些内核对象。
 *
 * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
 * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
 * 来完成的
 * 
 */
/******************************* 全局变量声明 ************************************/
/*
 * 当我们在写应用程序的时候,可能需要用到一些全局变量。
 */
/*
*************************************************************************
*                             函数声明
*************************************************************************
*/
static void AppTaskCreate(void);/* 用于创建任务 */
static void LED_Task(void* pvParameters);/* LED_Task任务实现 */
static void KEY_Task(void* pvParameters);/* KEY_Task任务实现 */
static void BSP_Init(void);/* 用于初始化板载相关资源 */
​
/*****************************************************************
  * @brief  主函数
  * @param  无
  * @retval 无
  * @note   第一步:开发板硬件初始化 
            第二步:创建APP应用任务
            第三步:启动FreeRTOS,开始多任务调度
  ****************************************************************/
int main(void)
{   
    BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
​
    /* 开发板硬件初始化 */
    BSP_Init();
​
    printf("这是一个[野火]-STM32全系列开发板-FreeRTOS任务管理实验!\n\n");
    printf("按下KEY1挂起任务,按下KEY2恢复任务\n");
​
    /* 创建AppTaskCreate任务 */
    xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                          (const char*    )"AppTaskCreate",/* 任务名字 */
                          (uint16_t       )512,  /* 任务栈大小 */
                          (void*          )NULL,/* 任务入口函数参数 */
                          (UBaseType_t    )1, /* 任务的优先级 */
                          (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
    /* 启动任务调度 */           
    if(pdPASS == xReturn)
        vTaskStartScheduler();   /* 启动任务,开启调度 */
    else
        return -1;  
​
    while(1);   /* 正常不会执行到这里 */    
}
/***********************************************************************
  * @ 函数名  : AppTaskCreate
  * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
  * @ 参数    : 无  
  * @ 返回值  : 无
  **********************************************************************/
static void AppTaskCreate(void)
{
    BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
​
    taskENTER_CRITICAL();           //进入临界区
​
    /* 创建LED_Task任务 */
    xReturn = xTaskCreate((TaskFunction_t )LED_Task, /* 任务入口函数 */
                          (const char*    )"LED_Task",/* 任务名字 */
                          (uint16_t       )512,   /* 任务栈大小 */
                          (void*          )NULL,    /* 任务入口函数参数 */
                          (UBaseType_t    )2,       /* 任务的优先级 */
                          (TaskHandle_t*  )&LED_Task_Handle);/* 任务控制块指针 */
    if(pdPASS == xReturn)
        printf("创建LED_Task任务成功!\r\n");
    /* 创建KEY_Task任务 */
    xReturn = xTaskCreate((TaskFunction_t )KEY_Task,  /* 任务入口函数 */
                          (const char*    )"KEY_Task",/* 任务名字 */
                          (uint16_t       )512,  /* 任务栈大小 */
                          (void*          )NULL,/* 任务入口函数参数 */
                          (UBaseType_t    )3, /* 任务的优先级 */
                          (TaskHandle_t*  )&KEY_Task_Handle);/* 任务控制块指针 */ 
    if(pdPASS == xReturn)
        printf("创建KEY_Task任务成功!\r\n");
​
    vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
​
    taskEXIT_CRITICAL();            //退出临界区
}
​
/**********************************************************************
  * @ 函数名  : LED_Task
  * @ 功能说明: LED_Task任务主体
  * @ 参数    :   
  * @ 返回值  : 无
  ********************************************************************/
static void LED_Task(void* parameter)
{   
    while (1)
    {
        LED1_ON;
        printf("LED_Task Running,LED1_ON\r\n");
        vTaskDelay(500);   /* 延时500个tick */
        LED1_OFF;     
        printf("LED_Task Running,LED1_OFF\r\n");
        vTaskDelay(500);   /* 延时500个tick */
    }
}
​
/**********************************************************************
  * @ 函数名  : LED_Task
  * @ 功能说明: LED_Task任务主体
  * @ 参数    :   
  * @ 返回值  : 无
  ********************************************************************/
static void KEY_Task(void* parameter)
{   
    while (1)
    {
        if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )
        {/* K1 被按下 */
            printf("挂起LED任务!\n");
            vTaskSuspend(LED_Task_Handle);/* 挂起LED任务 */
            printf("挂起LED任务成功!\n");
        } 
        if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )
        {/* K2 被按下 */
            printf("恢复LED任务!\n");
            vTaskResume(LED_Task_Handle);/* 恢复LED任务! */
            printf("恢复LED任务成功!\n");
        }
        vTaskDelay(20);/* 延时20个tick */
    }
}
​
/***********************************************************************
  * @ 函数名  : BSP_Init
  * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
  * @ 参数    :   
  * @ 返回值  : 无
  *********************************************************************/
static void BSP_Init(void)
{
    /*
     * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
     * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
     * 都统一用这个优先级分组,千万不要再分组,切忌。
     */
    NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
    /* LED 初始化 */
    LED_GPIO_Config();
    /* 串口初始化    */
    Debug_USART_Config();
    /* 按键初始化    */
    Key_GPIO_Config();
}
​
/********************************END OF FILE****************************/

4.消息队列

4.1消息队列的基本概念

  • 队列又称消息队列,是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任务间传递信息,实现了任务接收来自其他任务或中断的不固定长度的消息,任务能够从队列里面读取消息,当队列中的消息是空时,读取消息的任务将被阻塞,用户还可以指定阻塞的任务时间 xTicksToWait,在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。

  • 当队列中有新消息时,被阻塞的任务会被唤醒并处理新消息;当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转为就绪态。

  • 消息队列是一种异步的通信方式。通过消息队列服务,任务或中断服务例程可以将一条或多条消息放入消息队列中。

  • 同样,一个或多个任务可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常是将先进入消息队列的消息先传给任务,也就是说,任务先得到的是最先进入消息队列的消息,即先进先出原则(FIFO),但是也支持后进先出原则(LIFO)

  • FreeRTOS 中使用队列数据结构实现任务异步通信工作,具有如下特性:

    • 消息支持先进先出方式排队,支持异步读写工作方式。

    • 读写队列均支持超时机制。

    • 消息支持后进先出方式排队,往队首发送消息(LIFO)。

    • 可以允许不同长度(不超过队列节点最大值)的任意类型消息。

    • 一个任务能够从任意一个消息队列接收和发送消息。

    • 多个任务能够从同一个消息队列接收和发送消息。

    • 当队列使用结束后,可以通过删除队列函数进行删除。

4.2消息队列的运作机制

  • 创建消息队列时 FreeRTOS 会先给消息队列分配一块内存空间,这块内存的大小等于消息队列控制块大小加上(单个消息空间大小与消息队列长度的乘积),接着再初始化消息队列,此时消息队列为空。

  • FreeRTOS 的消息队列控制块由多个元素组成,当消息队列被创建时,系统会为控制块分配对应的内存空间,用于保存消息队列的一些信息如消息的存储位置,头指针 pcHead、尾指针 pcTail、消息大小 uxItemSize 以及队列长度 uxLength 等。

  • 同时每个消息队列都与消息空间在同一段连续的内存空间中,在创建成功的时候,这些内存就被占用了,只有删除了消息队列的时候,这段内存才会被释放掉,创建成功的时候就已经分配好每个消息空间与消息队列的容量,无法更改,每个消息空间可以存放不大于消息大小 uxItemSize 的任意类型的数据,所有消息队列中的消息空间总数即是消息队列的长度,这个长度可在消息队列创建时指定。任务或者中断服务程序都可以给消息队列发送消息,当发送消息时,如果队列未满或者允许覆盖入队,FreeRTOS 会将消息拷贝到消息队列队尾,否则,会根据用户指定的阻塞超时时间进行阻塞,在这段时间中,如果队列一直不允许入队,该任务将保持阻塞状态以等待队列允许入队。

  • 当其它任务从其等待的队列中读取入了数据(队列未满),该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中还不允许入队,任务也会自动从阻塞态转移为就绪态,此时发送消息的任务或者中断程序会收到一个错误码 errQUEUE_FULL。发送紧急消息的过程与发送消息几乎一样,唯一的不同是,当发送紧急消息时,发送的位置是消息队列队头而非队尾,这样,接收者就能够优先接收到紧急消息,从而及时进行消息处理。当某个任务试图读一个队列时,其可以指定一个阻塞超时时间。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。

  • 当其它任务或中断服务程序往其等待的队列中写入了数据,该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。当消息队列不再被使用时,应该删除它以释放系统资源,一旦操作完成,消息队列将被永久性的删除。消息队列的运作过程具体见图

 

70153378f01430a75458f11b4a438241.png

4.3消息队列的阻塞机制

  • 很简单,因为 FreeRTOS 已经为我们做好了,我们直接使用就好了,每个对消息队列读写的函数,都有这种机制,我称之为阻塞机制。

  • 假设有一个任务 A 对某个队列进行读操作的时候(也就是我们所说的出队),发现它没有消息,那么此时任务 A 有 3 个选择

  • 第一个选择任务 A 扭头就走,既然队列没有消息,那我也不等了,干其它事情去,这样子任务 A 不会进入阻塞态;

  • 第二个选择任务 A 还是在这里等等吧,可能过一会队列就有消息,此时任务 A 会进入阻塞状态,在等待着消息的道来,而任务 A 的等待时间就由我们自己定义,比如设置 1000 个系统时钟节拍 tick 的等待,在这 1000 个 tick 到来之前任务 A 都是处于阻塞态,当阻塞的这段时间任务 A 等到了队列的消息,那么任务 A 就会从阻塞态变成就绪态,如果此时任务 A 比当前运行的任务优先级还高,那么,任务 A 就会得到消息并且运行;假如 1000 个 tick 都过去了,队列还没消息,那任务 A 就不等了,从阻塞态中唤醒,返回一个没等到消息的错误代码,然后继续执行任务 A 的其他代码;

  • 第三个选择,任务 A 死等,不等到消息就不走了,这样子任务 A 就会进入阻塞态,直到完成读取队列的消息。

  • 而在发送消息操作的时候,为了保护数据,当且仅当队列允许入队的时候,发送者才能成功发送消息;队列中无可用消息空间时,说明消息队列已满,此时,系统会根据用户指定的阻塞超时时间将任务阻塞,在指定的超时时间内如果还不能完成入队操作,发送消息的任务或者中断服务程序会收到一个错误码 errQUEUE_FULL,然后解除阻塞状态;当然,只有在任务中发送消息才允许进行阻塞状态,而在中断中发送消息不允许带有阻塞机制的,需要调用在中断中发送消息的 API 函数接口,因为发送消息的上下文环境是在中断中,不允许有阻塞的情况。假如有多个任务阻塞在一个消息队列中,那么这些阻塞的任务将按照任务优先级进行排序,优先级高的任务将优先获得队列的访问权

4.4消息队列的应用场景

  • 消息队列可以应用于发送不定长消息的场合,包括任务与任务间的消息交换,队列是FreeRTOS 主要的任务间通讯方式,可以在任务与任务间、中断和任务间传送信息,发送到队列的消息是通过拷贝方式实现的,这意味着队列存储的数据是原数据,而不是原数据的引用。

4.5消息队列控制块

FreeRTOS 的消息队列控制块由多个元素组成,当消息队列被创建时,系统会为控制块分配对应的内存空间,用于保存消息队列的一些信息如消息的存储位置,头指针 pcHead、尾指针 pcTail、消息大小 uxItemSize 以及队列长度 uxLength,以及当前队列消息个数uxMessagesWaiting 等

4.6消息队列常用函数讲解

  • 使用队列模块的典型流程如下:

    •  创建消息队列。

    •  写队列操作。

    •  读队列操作。

    •  删除队列。

4.6.1xQueueCreate()

  1. xQueueCreate()用于创建一个新的队列并返回可用于访问这个队列的队列句柄。队列句柄其实就是一个指向队列数据结构类型的指针。

  2. 队列就是一个数据结构,用于任务间的数据的传递。每创建一个新的队列都需要为其分配 RAM,一部分用于存储队列的状态,剩下的作为队列消息的存储区域。

  3. 使用xQueueCreate()创建队列时,使用的是动态内存分配,所以要想使用该函数必须在FreeRTOSConfig.h 中把 configSUPPORT_DYNAMIC_ALLOCATION 定义为 1 来使能,这是个用于使能动态内存分配的宏,通常情况下,在 FreeRTOS 中,凡是创建任务,队列,信号量和互斥量等内核对象都需要使用动态内存分配,所以这个宏默认在 FreeRTOS.h 头文件中已经使能(即定义为 1)。

  4. 如果想使用静态内存,则可以使用 xQueueCreateStatic() 函数来创建一个队列。

  5. 使用静态创建消息队列函数创建队列时需要的形参更多,需要的内存由编译的时候预先分配好,一般很少使用这种方法。xQueueCreate()函数原型具体见代码加粗部分,使用说明具体见图。

 

42e2745f3d4cb4795edd938e184b752f.png

  • 从函数原型中,我们可以看到,创建队列真正使用的函数是 xQueueGenericCreate(),消息队列创建函数,顾名思义,就是创建一个队列,与任务一样,都是需要先创建才能使用的东西,FreeRTOS 肯定不知道我们需要什么样的队列,比如队列的长度,消息的大小这些信息都是需要我们自己定义的,FreeRTOS 提供给我们这个创建函数,爱怎么搞都是我们自己来实现,下面来看看 xQueueGenericCreate()函数源码,具体见代码。

    #if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
    QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,
                                      const UBaseType_t uxItemSize,
                                      const uint8_t ucQueueType )
    {
        Queue_t *pxNewQueue;
        size_t xQueueSizeInBytes;
        uint8_t *pucQueueStorage;
        configASSERT( uxQueueLength > ( UBaseType_t ) 0 );
        if ( uxItemSize == ( UBaseType_t ) 0 ) {
            /* 消息空间大小为 0*/
            xQueueSizeInBytes = ( size_t ) 0; (1)
        } else {
            /* 分配足够消息存储空间,空间的大小为队列长度*单个消息大小 */
            xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize ); (2)
        }
        /* 向系统申请内存,内存大小为消息队列控制块大小+消息存储空间大小 */
        pxNewQueue=(Queue_t*)pvPortMalloc(sizeof(Queue_t)+xQueueSizeInBytes); (3)
    
        if ( pxNewQueue != NULL ) {
            /* 计算出消息存储空间的起始地址 */
            pucQueueStorage = ( ( uint8_t * ) pxNewQueue ) + sizeof( Queue_t ); (4
            #if( configSUPPORT_STATIC_ALLOCATION == 1 )
            {
                pxNewQueue->ucStaticallyAllocated = pdFALSE;
            }
            #endif
            prvInitialiseNewQueue( uxQueueLength, (5)
                                  uxItemSize,
                                  pucQueueStorage,
                                  ucQueueType,
                                  pxNewQueue );
        }
        return pxNewQueue;
    }
    
    #endif
    /*-----------------------------------------------------------*/
    1. 代码清单(1):如果 uxItemSize 为 0,也就是单个消息空间大小为 0,这样子就不需要申请内存了,那么 xQueueSizeInBytes 也设置为 0 即可,设置为 0 是可以的,用作信号量的时候这个就可以设置为 0。

    2. 代码清单(2):uxItemSize 并不是为 0,那么需要分配足够存储消息的空间,内存的大小为队列长度*单个消息大小。

    3. 代码清单 (3):FreeRTOS 调用 pvPortMalloc()函数向系统申请内存空间,内存大小为消息队列控制块大小加上消息存储空间大小,因为这段内存空间是需要保证连续的,具体见图 17-2。

     

    e34efccd310778f3a7c78a7e28a183b8.png

  1. 代码清单 (4):计算出消息存储内存空间的起始地址,因为(3)步骤中申请的内存是包含了消息队列控制块的内存空间,但是我们存储消息的内存空间在消息队列控制块后面。

  2. 代码清单(5):调用 prvInitialiseNewQueue()函数将消息队列进行初始化。其实xQueueGenericCreate()主要是用于分配消息队列内存的。

  3. 在创建消息队列的时候,是需要用户自己定义消息队列的句柄的,但是注意了,定义了队列的句柄并不等于创建了队列,创建队列必须是调用消息队列创建函数进行创建(可以是静态也可以是动态创建),否则,以后根据队列句柄使用消息队列的其它函数的时候会发生错误,创建完成会返回消息队列的句柄,用户通过句柄就可使用消息队列进行发送与读取消息队列的操作,如果返回的是 NULL 则表示创建失败,消息队列创建函数xQueueCreate()使用实例具体见。

  4. QueueHandle_t Test_Queue =NULL;
    #define QUEUE_LEN 4 /* 队列的长度,最大可包含多少个消息 */
    #define QUEUE_SIZE 4 /* 队列中每个消息大小(字节) */
    BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
    taskENTER_CRITICAL(); //进入临界区
    /* 创建 Test_Queue */ 
    Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,/* 消息队列的长度 */ 
                              (UBaseType_t ) QUEUE_SIZE);/* 消息的大小 */ 
    if (NULL != Test_Queue) 
        printf("创建 Test_Queue 消息队列成功!\r\n"); 
    taskEXIT_CRITICAL(); //退出临界区

4.6.2 xQueueCreateStatic()

xQueueCreateStatic()用于创建一个新的队列并返回可用于访问这个队列的队列句柄。队列句柄其实就是一个指向队列数据结构类型的指针。队列就是一个数据结构,用于任务间的数据的传递。每创建一个新的队列都需要为其分 配 RAM , 一 部 分 用 于 存 储 队 列 的 状 态 , 剩 下 的 作 为 队 列 的 存 储 区 。 使 用xQueueCreateStatic()创建队列时,使用的是静态内存分配,所以要想使用该函数必须在FreeRTOSConfig.h 中把 configSUPPORT_STATIC_ALLOCATION 定义为 1 来使能。这是个用于使能静态内存分配的宏,需要的内存在程序编译的时候分配好,由用户自己定义,其实创建过程与 xQueueCreate()都是差不多的,我们暂不深入讲解。

dab6cb43cc4d4cd7b3a49c805ac05b21.png

/*创建一个可以最多可以存储 10 个 64 位变量的队列 */
#define QUEUE_LENGTH 10
#define ITEM_SIZE sizeof( uint64_t )

/* 该变量用于存储队列的数据结构 */
static StaticQueue_t xStaticQueue;
/* 该数组作为队列的存储区域,大小至少有 uxQueueLength * uxItemSize 个字节 */
uint8_t ucQueueStorageArea[ QUEUE_LENGTH * ITEM_SIZE ]; 

void vATask( void *pvParameters )
{
    QueueHandle_t xQueue;

    /* 创建一个队列 */ 
    xQueue = xQueueCreateStatic( QUEUE_LENGTH, /* 队列深度 */ 
                                ITEM_SIZE, /* 队列数据单元的单位 */ 
                                ucQueueStorageArea,/* 队列的存储区域 */ 
                                &xStaticQueue ); /* 队列的数据结构 */ 
    /* 剩下的其他代码 */
}

4.6.3 vQueueDelete()

队列删除函数是根据消息队列句柄直接删除的,删除之后这个消息队列的所有信息都会被系统回收清空,而且不能再次使用这个消息队列了,但是需要注意的是,如果某个消息队列没有被创建,那也是无法被删除的,动脑子想想都知道,没创建的东西就不存在,怎么可能被删除。xQueue 是 vQueueDelete()函数的形参,是消息队列句柄,表示的是要删除哪个想队列。

#define QUEUE_LENGTH 5
#define QUEUE_ITEM_SIZE 4

int main( void )
{
    QueueHandle_t xQueue;
    /* 创建消息队列 */
    xQueue = xQueueCreate( QUEUE_LENGTH, QUEUE_ITEM_SIZE );

    if ( xQueue == NULL ) {
        /* 消息队列创建失败 */
    } else {
        /* 删除已创建的消息队列 */ 
        vQueueDelete( xQueue ); 
    }
}

4.6.4向消息队列发送消息函数

任务或者中断服务程序都可以给消息队列发送消息,当发送消息时,如果队列未满或者允许覆盖入队,FreeRTOS 会将消息拷贝到消息队列队尾,否则,会根据用户指定的阻塞超时时间进行阻塞,在这段时间中,如果队列一直不允许入队,该任务将保持阻塞状态以等待队列允许入队。当其它任务从其等待的队列中读取入了数据(队列未满),该任务将自动由阻塞态转为就绪态。当任务等待的时间超过了指定的阻塞时间,即使队列中还不允许入队,任务也会自动从阻塞态转移为就绪态,此时发送消息的任务或者中断程序会收到一个错误码 errQUEUE_FULL。发送紧急消息的过程与发送消息几乎一样,唯一的不同是,当发送紧急消息时,发送的位置是消息队列队头而非队尾,这样,接收者就能够优先接收到紧急消息,从而及时进行消息处理。其实消息队列发送函数有好几个,都是使用宏定义进行展开的,有些只能在任务调用,有些只能在中断中调用,具体见下面讲解。

4.6.4.1xQueueSend()与 xQueueSendToBack()

xQueueSend()是一个宏,宏展开是调用函数 xQueueGenericSend(),这个函数在后面会详细讲解其实现过程 。 该 宏 是 为 了 向 后 兼 容 没 有 包 含 xQueueSendToFront() 和xQueueSendToBack() 这 两 个 宏 的 FreeRTOS 版 本 。 xQueueSend() 等 同 于xQueueSendToBack()。xQueueSend()用于向队列尾部发送一个队列消息。消息以拷贝的形式入队,而不是以引用的形式。该函数绝对不能在中断服务程序里面被调用,中断中必须使用带有中断保护功能的 xQueueSendFromISR()来代替。xQueueSend()函数的具体说明见表格 17-3,应用实例具体见代码清单 17-12 加粗部分。

static void Send_Task(void* parameter)
{
    BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
    uint32_t send_data1 = 1;
    uint32_t send_data2 = 2;
    while (1) {
        if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) {
            /* K1 被按下 */
            printf("发送消息 send_data1!\n"); 
            xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */ 
                                 &send_data1,/* 发送的消息内容 */ 
                                 0 ); /* 等待时间 0 */ 
            if (pdPASS == xReturn) 
                printf("消息 send_data1 发送成功!\n\n"); 
        }
        if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) {
            /* K2 被按下 */
            printf("发送消息 send_data2!\n"); 
            xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */
                                 &send_data2,/* 发送的消息内容 */ 
                                 0 ); /* 等待时间 0 */ 
            if (pdPASS == xReturn) 
                printf("消息 send_data2 发送成功!\n\n"); 
        }
        vTaskDelay(20);/* 延时 20 个 tick */
    }
}                     

4.6.4.2xQueueSendFromISR()与 xQueueSendToBackFromISR()

xQueueSendFromISR()是一个宏,宏展开是调用函数 xQueueGenericSendFromISR()。该宏是 xQueueSend()的中断保护版本,用于在中断服务程序中向队列尾部发送一个队列消息,等价于 xQueueSendToBackFromISR()。xQueueSendFromISR()函数具体说明见表格 17-4,使用实例具体见代码清单 17-15 加粗部分。

 

7c14e91bf3b83fff97fe9826c8874970.png

void vBufferISR( void )
{
    char cIn;
    BaseType_t xHigherPriorityTaskWoken; 

    /* 在 ISR 开始的时候,我们并没有唤醒任务 */
    xHigherPriorityTaskWoken = pdFALSE; 

    /* 直到缓冲区为空 */
    do {
        /* 从缓冲区获取一个字节的数据 */
        cIn = portINPUT_BYTE( RX_REGISTER_ADDRESS );

        /* 发送这个数据 */ 
        xQueueSendFromISR( xRxQueue, &cIn, &xHigherPriorityTaskWoken ); 

    } while ( portINPUT_BYTE( BUFFER_COUNT ) );

    /* 这时候 buffer 已经为空,如果需要则进行上下文切换 */ 
    if ( xHigherPriorityTaskWoken ) { 
        /* 上下文切换,这是一个宏,不同的处理器,具体的方法不一样 */ 
        taskYIELD_FROM_ISR (); 
    } 
}

4.6.4.3xQueueSendToFront()

xQueueSendToFron() 是 一 个 宏 , 宏 展 开 也 是 调 用 函 数 xQueueGenericSend() 。xQueueSendToFront()用于向队列队首发送一个消息。消息以拷贝的形式入队,而不是以引用的形式。该函数绝不能在中断服务程序里面被调用,而是必须使用带有中断保护功能的xQueueSendToFrontFromISR ()来代替。xQueueSendToFron()函数的具体说明见表格 ,使用方式与 xQueueSend()函数一致。

 

eac351aaa333d755429221ab8e9cfbc2.png

4.6.4.4xQueueSendToFrontFromISR()

xQueueSendToFrontFromISR() 是 一 个 宏 , 宏 展 开 是 调 用 函 数xQueueGenericSendFromISR()。该宏是 xQueueSendToFront()的中断保护版本,用于在中断服务程序中向消息队列队首发送一个消息。xQueueSendToFromISR()函数具体说明见表格17-6,使用方式与 xQueueSendFromISR()函数一致

 

00742e98197df29d29a22c1ce2cdcc39.png

4.6.4.5 xQueueGenericSend()

上面看到的那些在任务中发送消息的函数都是 xQueueGenericSend()展开的宏定义,真正起作用的就是 xQueueGenericSend()函数,根据指定的参数不一样,发送消息的结果就不一样,下面一起看看任务级的通用消息队列发送函数的实现过程,具体见代码清单。

4.6.4.6xQueueGenericSendFromISR()

既然有任务中发送消息的函数,当然也需要有在中断中发送消息函数,其实这个函数跟 xQueueGenericSend() 函数很像,只不过是 执行的上下文环境是不一样的,xQueueGenericSendFromISR()函数只能用于中断中执行,是不带阻塞机制的。xQueueGenericSendFromISR()函数没有阻塞机制,只能用于中断中发送消息,代码简单了很多,当成功入队后,如果有因为等待出队而阻塞的任务,系统会将该任务解除阻塞,要注意的是,解除了任务并不是会马上运行的,只是任务会被挂到就绪列表中。在执行解除阻塞操作之前,会判断队列是否上锁。如果没有上锁,则可以解除被阻塞的任务,然后根据任务优先级情况来决定是否需要进行任务切换;如果队列已经上锁,则不能解除被阻塞的任务,只能是记录 xTxLock 的值,表示队列上锁期间消息入队的个数,也用来记录可以解除阻塞任务的个数,在队列解锁中会将任务解除阻塞。

4.6.5从消息队列读取消息函数

当任务试图读队列中的消息时,可以指定一个阻塞超时时间,当且仅当消息队列中有消息的时候,任务才能读取到消息。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务程序往其等待的队列中写入了数据,该任务将自动由阻塞态转为就绪态。当任务等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。

4.6.5.1xQueueReceive()与 xQueuePeek()

xQueueReceive() 是 一 个 宏 , 宏 展 开 是 调 用 函 数 xQueueGenericReceive() 。xQueueReceive()用于从一个队列中接收消息并把消息从队列中删除。接收的消息是以拷贝的形式进行的,所以我们必须提供一个足够大空间的缓冲区。具体能够拷贝多少数据到缓冲区,这个在队列创建的时候已经设定。该函数绝不能在中断服务程序里面被调用,而是必须使用带有中断保护功能的 xQueueReceiveFromISR ()来代替。xQueueReceive()函数的具体说明见表格 ,应用实例见代码清单。

 

9efbc0b58f5779c52ecaeece315e0713.png

static void Receive_Task(void* parameter)
{
    BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdPASS */
    uint32_t r_queue; /* 定义一个接收消息的变量 */
    while (1) {
        xReturn = xQueueReceive( Test_Queue, /* 消息队列的句柄 */ 
                                &r_queue, /* 发送的消息内容 */ 
                                portMAX_DELAY); /* 等待时间 一直等 */ 
        if (pdTRUE== xReturn) 
            printf("本次接收到的数据是:%d\n\n",r_queue); 
        else 
            printf("数据接收出错,错误代码: 0x%lx\n",xReturn); 
    }
}

看到这里,有人就问了如果我接收了消息不想删除怎么办呢?其实,你能想到的东西,FreeRTOS 看到也想到了,如果不想删除消息的话,就调用 xQueuePeek()函数。其实这个函数与 xQueueReceive()函数的实现方式一样,连使用方法都一样,只不过xQueuePeek()函数接收消息完毕不会删除消息队列中的消息而已。

4.6.5.2xQueueReceiveFromISR()与 xQueuePeekFromISR()

xQueueReceiveFromISR()是 xQueueReceive ()的中断版本,用于在中断服务程序中接收一个队列消息并把消息从队列中删除;xQueuePeekFromISR()是 xQueuePeek()的中断版本,用于在中断中从一个队列中接收消息,但并不会把消息从队列中移除。说白了这两个函数只能用于中断,是不带有阻塞机制的,并且是在中断中可以安全调用,函数说明具体见表格,函数的使用实例具体见代码清单。

 

7b49cf14380aea4b778bca482fca7f90.png

4.6.5.3xQueueGenericReceive()

由于在中断中接收消息的函数用的并不多,我们只讲解在任务中读取消息的函数——xQueueGenericReceive()。

4.6.6消息队列使用注意事项

在使用 FreeRTOS 提供的消息队列函数的时候,需要了解以下几点:

  • 使用 xQueueSend()、xQueueSendFromISR()、xQueueReceive()等这些函数之前应先创建需消息队列,并根据队列句柄进行操作。

  • 队列读取采用的是先进先出(FIFO)模式,会先读取先存储在队列中的数据。当然也 FreeRTOS 也支持后进先出(LIFO)模式,那么读取的时候就会读取到后进队列的数据。

  • 在获取队列中的消息时候,我们必须要定义一个存储读取数据的地方,并且该数据区域大小不小于消息大小,否则,很可能引发地址非法的错误。

  • 无论是发送或者是接收消息都是以拷贝的方式进行,如果消息过于庞大,可以将消息的地址作为消息进行发送、接收。

  • 队列是具有自己独立权限的内核对象,并不属于任何任务。所有任务都可以向同一队列写入和读出。一个队列由多任务或中断写入是经常的事,但由多个任务读出倒是用的比较少。

4.6.7消息队列实验

/**************************** 任务句柄 ********************************/
/* 
 * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
 * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
 * 这个句柄可以为NULL。
 */
static TaskHandle_t AppTaskCreate_Handle = NULL;/* 创建任务句柄 */
static TaskHandle_t Receive_Task_Handle = NULL;/* LED任务句柄 */
static TaskHandle_t Send_Task_Handle = NULL;/* KEY任务句柄 */
​
/********************************** 内核对象句柄 *********************************/
/*
 * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
 * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
 * 们就可以通过这个句柄操作这些内核对象。
 *
 * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
 * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
 * 来完成的
 * 
 */
QueueHandle_t Test_Queue =NULL;
​
/******************************* 全局变量声明 ************************************/
/*
 * 当我们在写应用程序的时候,可能需要用到一些全局变量。
 */
​
​
/******************************* 宏定义 ************************************/
/*
 * 当我们在写应用程序的时候,可能需要用到一些宏定义。
 */
#define  QUEUE_LEN    4   /* 队列的长度,最大可包含多少个消息 */
#define  QUEUE_SIZE   4   /* 队列中每个消息大小(字节) */
​
/*
*************************************************************************
*                             函数声明
*************************************************************************
*/
static void AppTaskCreate(void);/* 用于创建任务 */
​
static void Receive_Task(void* pvParameters);/* Receive_Task任务实现 */
static void Send_Task(void* pvParameters);/* Send_Task任务实现 */
​
static void BSP_Init(void);/* 用于初始化板载相关资源 */
​
/*****************************************************************
  * @brief  主函数
  * @param  无
  * @retval 无
  * @note   第一步:开发板硬件初始化 
            第二步:创建APP应用任务
            第三步:启动FreeRTOS,开始多任务调度
  ****************************************************************/
int main(void)
{   
    BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
​
    /* 开发板硬件初始化 */
    BSP_Init();
    printf("这是一个[野火]-STM32全系列开发板-FreeRTOS消息队列实验!\n");
    printf("按下KEY1或者KEY2发送队列消息\n");
    printf("Receive任务接收到消息在串口回显\n\n");
    /* 创建AppTaskCreate任务 */
    xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                          (const char*    )"AppTaskCreate",/* 任务名字 */
                          (uint16_t       )512,  /* 任务栈大小 */
                          (void*          )NULL,/* 任务入口函数参数 */
                          (UBaseType_t    )1, /* 任务的优先级 */
                          (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
    /* 启动任务调度 */           
    if(pdPASS == xReturn)
        vTaskStartScheduler();   /* 启动任务,开启调度 */
    else
        return -1;  
​
    while(1);   /* 正常不会执行到这里 */    
}
​
​
/***********************************************************************
  * @ 函数名  : AppTaskCreate
  * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
  * @ 参数    : 无  
  * @ 返回值  : 无
  **********************************************************************/
static void AppTaskCreate(void)
{
    BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
​
    taskENTER_CRITICAL();           //进入临界区
​
    /* 创建Test_Queue */
    Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,/* 消息队列的长度 */
                              (UBaseType_t ) QUEUE_SIZE);/* 消息的大小 */
    if(NULL != Test_Queue)
        printf("创建Test_Queue消息队列成功!\r\n");
​
    /* 创建Receive_Task任务 */
    xReturn = xTaskCreate((TaskFunction_t )Receive_Task, /* 任务入口函数 */
                          (const char*    )"Receive_Task",/* 任务名字 */
                          (uint16_t       )512,   /* 任务栈大小 */
                          (void*          )NULL,    /* 任务入口函数参数 */
                          (UBaseType_t    )2,       /* 任务的优先级 */
                          (TaskHandle_t*  )&Receive_Task_Handle);/* 任务控制块指针 */
    if(pdPASS == xReturn)
        printf("创建Receive_Task任务成功!\r\n");
​
    /* 创建Send_Task任务 */
    xReturn = xTaskCreate((TaskFunction_t )Send_Task,  /* 任务入口函数 */
                          (const char*    )"Send_Task",/* 任务名字 */
                          (uint16_t       )512,  /* 任务栈大小 */
                          (void*          )NULL,/* 任务入口函数参数 */
                          (UBaseType_t    )3, /* 任务的优先级 */
                          (TaskHandle_t*  )&Send_Task_Handle);/* 任务控制块指针 */ 
    if(pdPASS == xReturn)
        printf("创建Send_Task任务成功!\n\n");
​
    vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
​
    taskEXIT_CRITICAL();            //退出临界区
}
​
​
​
/**********************************************************************
  * @ 函数名  : Receive_Task
  * @ 功能说明: Receive_Task任务主体
  * @ 参数    :   
  * @ 返回值  : 无
  ********************************************************************/
static void Receive_Task(void* parameter)
{   
    BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdTRUE */
    uint32_t r_queue;   /* 定义一个接收消息的变量 */
    while (1)
    {
        xReturn = xQueueReceive( Test_Queue,    /* 消息队列的句柄 */
                                &r_queue,      /* 发送的消息内容 */
                                portMAX_DELAY); /* 等待时间 一直等 */
        if(pdTRUE == xReturn)
            printf("本次接收到的数据是%d\n\n",r_queue);
        else
            printf("数据接收出错,错误代码0x%lx\n",xReturn);
    }
}
​
/**********************************************************************
  * @ 函数名  : Send_Task
  * @ 功能说明: Send_Task任务主体
  * @ 参数    :   
  * @ 返回值  : 无
  ********************************************************************/
static void Send_Task(void* parameter)
{    
    BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
    uint32_t send_data1 = 1;
    uint32_t send_data2 = 2;
    while (1)
    {
        if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )
        {/* K1 被按下 */
            printf("发送消息send_data1!\n");
            xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */
                                 &send_data1,/* 发送的消息内容 */
                                 0 );        /* 等待时间 0 */
            if(pdPASS == xReturn)
                printf("消息send_data1发送成功!\n\n");
        } 
        if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )
        {/* K2 被按下 */
            printf("发送消息send_data2!\n");
            xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */
                                 &send_data2,/* 发送的消息内容 */
                                 0 );        /* 等待时间 0 */
            if(pdPASS == xReturn)
                printf("消息send_data2发送成功!\n\n");
        }
        vTaskDelay(20);/* 延时20个tick */
    }
}
​
/***********************************************************************
  * @ 函数名  : BSP_Init
  * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
  * @ 参数    :   
  * @ 返回值  : 无
  *********************************************************************/
static void BSP_Init(void)
{
    /*
     * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
     * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
     * 都统一用这个优先级分组,千万不要再分组,切忌。
     */
    NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
​
    /* LED 初始化 */
    LED_GPIO_Config();
​
    /* 串口初始化    */
    Debug_USART_Config();
​
    /* 按键初始化    */
    Key_GPIO_Config();
​
}
​
/********************************END OF FILE****************************/

5.信号量

同志们,回想一下,你是否在裸机编程中这样使用过一个变量:用于标记某个事件是否发生,或者标志一下某个东西是否正在被使用,如果是被占用了的或者没发生,我们就不对它进行操作。

5.1信号量基本概念

信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问,常用于协助一组相互竞争的任务来访问临界资源。在多任务系统中,各任务之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面的支持。抽象的来讲,信号量是一个非负整数,所有获取它的任务都会将该整数减一(获取它当然是为了使用资源),当该整数值为零时,所有试图获取它的任务都将处于阻塞状态。通常一个信号量的计数值用于对应有效的资源数,表示剩下的可被占用的互斥资源数。其值的含义分两种情况:

  1. 0:表示没有积累下来的释放信号量操作,且有可能有在此信号量上阻塞的任务。

  2. 正值,表示有一个或多个释放信号量操作。

5.2二值信号量

二值信号量既可以用于临界资源访问也可以用于同步功能。二值信号量和互斥信号量(以下使用互斥量表示互斥信号量)非常相似,但是有一些细微差别:互斥量有优先级继承机制,二值信号量则没有这个机制。这使得二值信号量更偏向应用于同步功能(任务与任务间的同步或任务和中断间同步),而互斥量更偏向应用于临界资源的访问。用作同步时,信号量在创建后应被置为空,任务 1 获取信号量而进入阻塞,任务 2 在某种条件发生后,释放信号量,于是任务 1 获得信号量得以进入就绪态,如果任务 1 的优先级是最高的,那么就会立即切换任务,从而达到了两个任务间的同步。同样的,在中断服务函数中释放信号量,任务 1 也会得到信号量,从而达到任务与中断间的同步。还记得我们经常说的中断要快进快出吗,在裸机开发中我们经常是在中断中做一个标记,然后在退出的时候进行轮询处理,这个就是类似我们使用信号量进行同步的,当标记发生了,我们再做其他事情。在 FreeRTOS 中我们用信号量用于同步,任务与任务的同步,中断与任务的同步,可以大大提高效率。可以将二值信号量看作只有一个消息的队列,因此这个队列只能为空或满(因此称为二值),我们在运用的时候只需要知道队列中是否有消息即可,而无需关注消息是什么。

5.3计数信号量

二进制信号量可以被认为是长度为 1 的队列,而计数信号量则可以被认为长度大于 1的队列,信号量使用者依然不必关心存储在队列中的消息,只需关心队列是否有消息即可。顾名思义,计数信号量肯定是用于计数的,在实际的使用中,我们常将计数信号量用于事件计数与资源管理。每当某个事件发生时,任务或者中断将释放一个信号量(信号量计数值加 1),当处理被事件时(一般在任务中处理),处理任务会取走该信号量(信号量计数值减 1),信号量的计数值则表示还有多少个事件没被处理。此外,系统还有很多资源,我们也可以使用计数信号量进行资源管理,信号量的计数值表示系统中可用的资源数目,任务必须先获取到信号量才能获取资源访问权,当信号量的计数值为零时表示系统没有可用的资源,但是要注意,在使用完资源的时候必须归还信号量,否则当计数值为 0的时候任务就无法访问该资源了。计数型信号量允许多个任务对其进行操作,但限制了任务的数量。比如有一个停车场,里面只有 100 个车位,那么能停的车只有 100 辆,也相当于我们的信号量有 100 个,假如一开始停车场的车位还有 100 个,那么每进去一辆车就要消耗一个停车位,车位的数量就要减一,对应的,我们的信号量在使用之后也需要减一,当停车场停满了 100 辆车的时候,此时的停车位为 0,再来的车就不能停进去了,否则将造成事故,也相当于我们的信号量为 0,后面的任务对这个停车场资源的访问也无法进行,当有车从停车场离开的时候,车位又空余出来了,那么,后面的车就能停进去了,我们信号量的操作也是一样的,当我们释放了这个资源,后面的任务才能对这个资源进行访问。

5.4互斥信号量

互斥信号量其实是特殊的二值信号量,由于其特有的优先级继承机制从而使它更适用于简单互锁,也就是保护临界资源(什么是优先级继承在后续相信讲解)。用作互斥时,信号量创建后可用信号量个数应该是满的,任务在需要使用临界资源时,(临界资源是指任何时刻只能被一个任务访问的资源),先获取互斥信号量,使其变空,这样其他任务需要使用临界资源时就会因为无法获取信号量而进入阻塞,从而保证了临界资源的安全。在操作系统中,我们使用信号量的很多时候是为了给临界资源建立一个标志,信号量表示了该临界资源被占用情况。这样,当一个任务在访问临界资源的时候,就会先对这个资源信息进行查询,从而在了解资源被占用的情况之后,再做处理,从而使得临界资源得到有效的保护。

5.5递归信号量

递归信号量,见文知义,递归嘛,就是可以重复获取调用的,本来按照信号量的特性,每获取一次可用信号量个数就会减少一个,但是递归则不然,对于已经获取递归互斥量的任务可以重复获取该递归互斥量,该任务拥有递归信号量的所有权。任务成功获取几次递归互斥量,就要返还几次,在此之前递归互斥量都处于无效状态,其他任务无法获取,只有持有递归信号量的任务才能获取与释放。

5.6二值信号量应用场景

在嵌入式操作系统中二值信号量是任务间、任务与中断间同步的重要手段,信号量使用最多的一般都是二值信号量与互斥信号量(互斥信号量在下一章讲解)。为什么叫二值信号量呢?因为信号量资源被获取了,信号量值就是 0,信号量资源被释放,信号量值就是 1,把这种只有 0 和 1 两种情况的信号量称之为二值信号量。在多任务系统中,我们经常会使用这个二值信号量,比如,某个任务需要等待一个标记,那么任务可以在轮询中查询这个标记有没有被置位,但是这样子做,就会很消耗 CPU资源并且妨碍其它任务执行,更好的做法是任务的大部分时间处于阻塞状态(允许其它任务执行),直到某些事件发生该任务才被唤醒去执行。可以使用二进制信号量实现这种同步,当任务取信号量时,因为此时尚未发生特定事件,信号量为空,任务会进入阻塞状态;当事件的条件满足后,任务/中断便会释放信号量,告知任务这个事件发生了,任务取得信号量便被唤醒去执行对应的操作,任务执行完毕并不需要归还信号量,这样子的 CPU 的效率可以大大提高,而且实时响应也是最快的。再比如某个任务使用信号量在等中断的标记的发生,在这之前任务已经进入了阻塞态,在等待着中断的发生,当在中断发生之后,释放一个信号量,也就是我们常说的标记,当它退出中断之后,操作系统会进行任务的调度,如果这个任务能够运行,系统就会把等待这个任务运行起来,这样子就大大提高了我们的效率。二值信号量在任务与任务中同步的应用场景:假设我们有一个温湿度的传感器,假设是 1s 采集一次数据,那么我们让他在液晶屏中显示数据出来,这个周期也是要 1s 一次的,如果液晶屏刷新的周期是 100ms 更新一次,那么此时的温湿度的数据还没更新,液晶屏根本无需刷新,只需要在 1s 后温湿度数据更新的时候刷新即可,否则 CPU 就是白白做了多次的无效数据更新,CPU的资源就被刷新数据这个任务占用了大半,造成 CPU 资源浪费,如果液晶屏刷新的周期是 10s更新一次,那么温湿度的数据都变化了 10 次,液晶屏才来更新数据,那拿这个产品有啥用,根本就是不准确的,所以,还是需要同步协调工作,在温湿度采集完毕之后,进行液晶屏数据的刷新,这样子,才是最准确的,并且不会浪费 CPU的资源。同理,二值信号量在任务与中断同步的应用场景:我们在串口接收中,我们不知道啥时候有数据发送过来,有一个任务是做接收这些数据处理,总不能在任务中每时每刻都在任务查询有没有数据到来,那样会浪费 CPU 资源,所以在这种情况下使用二值信号量是很好的办法,当没有数据到来的时候,任务就进入阻塞态,不参与任务的调度,等到数据到来了,释放一个二值信号量,任务就立即从阻塞态中解除,进入就绪态,然后运行的时候处理数据,这样子系统的资源就会很好的被利用起来。

5.7二值信号量运作机制

  • 创建信号量时,系统会为创建的信号量对象分配内存,并把可用信号量初始化为用户自定义的个数,二值信号量的最大可用信号量个数为 1。

  • 二值信号量获取,任何任务都可以从创建的二值信号量资源中获取一个二值信号量,获取成功则返回正确,否则任务会根据用户指定的阻塞超时时间来等待其它任务中断释放信号量。

  • 在等待这段时间,系统将任务变成阻塞态,任务将被挂到该信号量的阻塞等待列表中。在二值信号量无效的时候,假如此时有任务获取该信号量的话,那么任务将进入阻塞状态,具体见图

 

9094030aac8455cb4569e6e30cb11123.png

  • 假如某个时间中断任务释放了信号量,其过程具体见图 ,那么,由于获取无效信号量而进入阻塞态的任务将获得信号量并且恢复为就绪态,其过程具体见图

 

51a1c36c4810d5188c1ab72281ecc54a.png

 

c43f8e7db9717ea7e4a459671739f85f.png

5.8计数信号量运作机制

计数信号量可以用于资源管理,允许多个任务获取信号量访问共享资源,但会限制任务的最大数目。访问的任务数达到可支持的最大数目时,会阻塞其他试图获取该信号量的任务,直到有任务释放了信号量。这就是计数型信号量的运作机制,虽然计数信号量允许多个任务访问同一个资源,但是也有限定,比如某个资源限定只能有 3 个任务访问,那么第 4 个任务访问的时候,会因为获取不到信号量而进入阻塞,等到有任务(比如任务 1)释放掉该资源的时候,第 4 个任务才能获取到信号量从而进行资源的访问,其运作的机制具体见图。

 

976c75b8c37a7e690cfdfe06f7f5f66d.png

5.9信号量控制块

信号量 API 函数实际上都是宏,它使用现有的队列机制,这些宏定义在 semphr.h 文件中,如果使用信号量或者互斥量,需要包含 semphr.h 头文件。所以 FreeRTOS 的信号量控制块结构体与消息队列结构体是一模一样的,只不过结构体中某些成员变量代表的含义不一样而已,我们会具体讲解一下哪里与消息队列不一样。

5.10常用信号量函数接口讲解

5.10.1创建信号量函数

5.10.1.1xSemaphoreCreateBinary()

  1. xSemaphoreCreateBinary()用于创建一个二值信号量,并返回一个句柄。

  2. 其实二值信号量和互斥量都共同使用一个类型 SemaphoreHandle_t 的句柄(.h 文件 79 行),该句柄的原型是一个 void 型 的 指 针。

  3. 使 用 该 函数 创 建 的 二 值信 号 量 是 空的 , 在 使 用函 数xSemaphoreTake()获取之前必须先调用函数 xSemaphoreGive()释放后才可以获取。

  4. 如果是使用老式的函数 vSemaphoreCreateBinary()创建的二值信号量,则为 1,在使用之前不用先释放 。

  5. 要 想 使用该 函 数必须在 FreeRTOSConfig.h 中 把 宏configSUPPORT_DYNAMIC_ALLOCATION 定义为 1,即开启动态内存分配。其实该宏在FreeRTOS.h 中默认定义为 1,即所有 FreeRTOS 的对象在创建的时候都默认使用动态内存分配方案。

  6. SemaphoreHandle_t xSemaphore = NULL;
    void vATask( void * pvParameters )
    {
        /* 尝试创建一个信号量 */ 
        xSemaphore = xSemaphoreCreateBinary(); 
        if ( xSemaphore == NULL ) {
            /* 内存不足,创建失败 */
        } else {
            /* 信号量现在可以使用,句柄存在变量 xSemaphore 中
     这个时候还不能调用函数 xSemaphoreTake()来获取信号量
     因为使用 xSemaphoreCreateBinary()函数创建的信号量是空的
     在第一次获取之前必须先调用函数 xSemaphoreGive()先提交*/
        }
    }

5.10.1.2xSemaphoreCreateCounting()

  1. xSemaphoreCreateCounting ()用于创建一个计数信号量。

  2. 要想使用该函数必须在FreeRTOSConfig.h 中把宏 configSUPPORT_DYNAMIC_ALLOCATION 定义为 1,即开启动态内存分配。

  3. 其实该宏在 FreeRTOS.h 中默认定义为 1,即所有 FreeRTOS 的对象在创建的时候都默认使用动态内存分配方案。其 实 计 数 信 号 量 跟 二 值 信 号 量 的 创 建 过 程 都 差 不 多 , 其 实 也 是 间 接 调 用。

 

52820d723e78c4c3828dc3e03996d03d.png

void vATask( void * pvParameters )
{
    SemaphoreHandle_t xSemaphore;
    /* 创建一个计数信号量, 用于事件计数 */ 
    xSemaphore = xSemaphoreCreateCounting( 5, 5 ); 
    if ( xSemaphore != NULL ) {
        /* 计数信号量创建成功 */
    }
}

5.10.2信号量删除函数 vSemaphoreDelete()

  • vSemaphoreDelete()用于删除一个信号量,包括二值信号量,计数信号量,互斥量和递归互斥量。如果有任务阻塞在该信号量上,那么不要删除该信号量。

  • 删除信号量过程其实就是删除消息队列过程,因为信号量其实就是消息队列,只不过是无法存储消息的队列而已.

5.10.3信号量释放函数

  1. 与消息队列的操作一样,信号量的释放可以在任务、中断中使用,所以需要有不一样的 API 函数在不一样的上下文环境中调用。

  2. 在前面的讲解中,我们知道,当信号量有效的时候,任务才能获取信号量,那么,是什么函数使得信号量变得有效?其实有两个方式,一个是在创建的时候进行初始化,将它可用的信号量个数设置一个初始值;在二值信号量中,该初始值的范围是 0~1(旧版本的FreeRTOS 中创建二值信号量默认是有效的,而新版本则默认是无效),假如初始值为 1 个可用的信号量的话,被申请一次就变得无效了,那就需要我们释放信号量,FreeRTOS 提供了信号量释放函数,每调用一次该函数就释放一个信号量。

  3. 但是有个问题,能不能一直释放?很显然,这是不能的,无论是你的信号量是二值信号量还是计数信号量,都要注意可用信号量的范围,当用作二值信号量的时候,必须确保其可用值在 0~1 范围内;而用作计数信号量的话,其范围是由用户在创建时指定 uxMaxCount,其最大可用信号量不允许超出uxMaxCount,这代表我们不能一直调用信号量释放函数来释放信号量,其实一直调用也是无法释放成功的,在写代码的时候,我们要注意代码的严谨性罢了。

5.10.3.1xSemaphoreGive()

  • 是一个用于释放信号量的宏,真正的实现过程是调用消息队列通用发送函数,xSemaphoreGive()函数原型具体见代码清单 。释放的信号量对象必须是已经被创建的,可以用于二值信号量、计数信号量、互斥量的释放,但不能释放由函数xSemaphoreCreateRecursiveMutex()创建的递归互斥量。此外该函数不能在中断中使用。

#define xSemaphoreGive( xSemaphore ) \
xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), \
 NULL, \
 semGIVE_BLOCK_TIME, \
 queueSEND_TO_BACK )

从该宏定义可以看出释放信号量实际上是一次入队操作,并且是不允许入队阻塞,因为阻塞时间为 semGIVE_BLOCK_TIME,该宏的值为 0。通过消息队列入队过程分析,我们可以将释放一个信号量的过程简化:如果信号量未满,控制块结构体成员 uxMessageWaiting 就会加 1,然后判断是否有阻塞的任务,如果有的话就会恢复阻塞的任务,然后返回成功信息(pdPASS);如果信号量已满,则返回错误代码(err_QUEUE_FULL),具体的源码分析过程参考 17.6 章节。xSemaphoreGive()函数使用实例见代码清单加粗部分。

static void Send_Task(void* parameter)
{
    BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
    while (1) {
        /* K1 被按下 */
        if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) {
            xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量 
            if ( xReturn == pdTRUE ) 
                printf("BinarySem_Handle 二值信号量释放成功!\r\n"); 
            else 
                printf("BinarySem_Handle 二值信号量释放失败!\r\n"); 
        }
        /* K2 被按下 */
        if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) {
            xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量 
            if ( xReturn == pdTRUE ) 
                printf("BinarySem_Handle 二值信号量释放成功!\r\n"); 
            else 
                printf("BinarySem_Handle 二值信号量释放失败!\r\n"); 
        }
        vTaskDelay(20);
    }
}

5.10.3.2xSemaphoreGiveFromISR()

  • 用于释放一个信号量,带中断保护。被释放的信号量可以是二进制信号量和计数信号量。和普通版本的释放信号量 API 函数有些许不同,它不能释放互斥量,这是因为互斥量不可以在中断中使用,互斥量的优先级继承机制只能在任务中起作用,而在中断中毫无意义。

  • 带中断保护的信号量释放其实也是一个宏,真正调用的函数是 xQueueGiveFromISR ()。

  • 如果可用信号量未满,控制块结构体成员 xMessageWaiting 就会加 1,然后判断是否有阻塞的任务,如果有的话就会恢复阻塞的任务,然后返回成功信息(pdPASS),如果恢复的任务优先级比当前任务优先级高,那么在退出中断要进行任务切换一次;如果信号量满,则返回错误代码(err_QUEUE_FUL),表示信号量满,xQueueGiveFromISR()源码的实现过程在消息队列章节已经讲解,具体见 17.6.4 6 小节。

  • 一个或者多个任务有可能阻塞在同一个信号量上,调用函数 xSemaphoreGiveFromISR()可能会唤醒阻塞在该信号量上的任务,如果被唤醒的任务的优先级大于当前任务的优先级,那么形参 pxHigherPriorityTaskWoken 就会被设置为 pdTRUE,然后在中断退出前执行一次。

  • 上下文切换。从 FreeRTOS V7.3.0 版本开始, pxHigherPriorityTaskWoken 是一个可选的参数,可以设置为 NULL,xSemaphoreGiveFromISR()函数使用实例具体见代码清单。

void vTestISR( void )
{
    BaseType_t pxHigherPriorityTaskWoken;
    uint32_t ulReturn;
    /* 进入临界段,临界段可以嵌套 */
    ulReturn = taskENTER_CRITICAL_FROM_ISR();
    /* 判断是否产生中断 */
    {
        /* 如果产生中断,清除中断标志位 */
        //释放二值信号量,发送接收到新数据标志,供前台程序查询 
        xSemaphoreGiveFromISR(BinarySem_Handle,& 
                              pxHigherPriorityTaskWoken); 
        //如果需要的话进行一次任务切换,系统会判断是否需要进行切换 
        portYIELD_FROM_ISR(pxHigherPriorityTaskWoken); 
    }

    /* 退出临界段 */
    taskEXIT_CRITICAL_FROM_ISR( ulReturn );
}

5.10.4信号量获取函数

  • 与消息队列的操作一样,信号量的获取可以在任务、中断(中断中使用并不常见)中使用,所以需要有不一样的 API 函数在不一样的上下文环境中调用。

  • 与释放信号量对应的是获取信号量,我们知道,当信号量有效的时候,任务才能获取信号量,当任务获取了某个信号量的时候,该信号量的可用个数就减一,当它减到 0 的时候,任务就无法再获取了,并且获取的任务会进入阻塞态(假如用户指定了阻塞超时时间的话)。

  • 如果某个信号量中当前拥有 1 个可用的信号量的话,被获取一次就变得无效了,那么此时另外一个任务获取该信号量的时候,就会无法获取成功,该任务便会进入阻塞态,阻塞时间由用户指定。

5.10.4.1xSemaphoreTake()

  • xSemaphoreTake()函数用于获取信号量,不带中断保护。

  • 获取的信号量对象可以是二值信号量、计数信号量和互斥量,但是递归互斥量并不能使用这个 API 函数获取。

  • 其实获取信号量是一个宏,真正调用的函数是 xQueueGenericReceive ()。

  • 该宏不能在中断使用,而是必须由具体中断保护功能的 xQueueReceiveFromISR()版本代替。

  • 从该宏定义可以看出释放信号量实际上是一次消息出队操作,阻塞时间由用户指定BlockTime,当有任务试图获取信号量的时候,当且仅当信号量有效的时候,任务才能读获取到信号量。

  • 如果信号量无效,在用户指定的阻塞超时时间中,该任务将保持阻塞状态以等待信号量有效。

  • 当其它任务或中断释放了有效的信号量,该任务将自动由阻塞态转移为就绪态。

  • 当任务等待的时间超过了指定的阻塞时间,即使信号量中还是没有可用信号量,任务也会自动从阻塞态转移为就绪态。

  • 通过前面消息队列出队过程分析,我们可以将获取一个信号量的过程简化:如果有可用信号量,控制块结构体成员 uxMessageWaiting 就会减 1,然后返回获取成功信息(pdPASS);如果信号量无效并且阻塞时间为 0,则返回错误代码(errQUEUE_EMPTY);如果信号量无效并且用户指定了阻塞时间,则任务会因为等待信号量而进入阻塞状态,任务会被挂接到延时列表中。具体的源码分析过程参考 17.6 章节。(此处暂时未讲解互斥信号量)xSemaphoreTake()函数使用实例具体见代码清单

static void Receive_Task(void* parameter)
{
    BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
    while (1) {
        //获取二值信号量 xSemaphore,没获取到则一直等待 
        xReturn = xSemaphoreTake(BinarySem_Handle,/* 二值信号量句柄 */ 
                                 portMAX_DELAY); /* 等待时间 */ 
        if (pdTRUE == xReturn) 
            printf("BinarySem_Handle 二值信号量获取成功!\n\n"); 
        LED1_TOGGLE;
    }
}

5.10.4.2xSemaphoreTakeFromISR()

xSemaphoreTakeFromISR()是函数 xSemaphoreTake()的中断版本,用于获取信号量,是一个不带阻塞机制获取信号量的函数,获取对象必须由是已经创建的信号量,信号量类型可以是二值信号量和计数信号量,它与 xSemaphoreTake()函数不同,它不能用于获取互斥量,因为互斥量不可以在中断中使用,并且互斥量特有的优先级继承机制只能在任务中起作用,而在中断中毫无意义。

c8e5903886ae4028b2b3d6b9cb2daf5f.png

5.10.5信号量实验

5.10.5.1二值信号量同步实验

  • 信号量同步实验是在 FreeRTOS 中创建了两个任务,一个是获取信号量任务,一个是释放互斥量任务,两个任务独立运行,获取信号量任务是一直在等待信号量,其等待时间是 portMAX_DELAY,等到获取到信号量之后,任务开始执行任务代码,如此反复等待另外任务释放的信号量。

  • 释放信号量任务在检测按键是否按下,如果按下则释放信号量,此时释放信号量会唤醒获取任务,获取任务开始运行,然后形成两个任务间的同步,因为如果没按下按键,那么信号量就不会释放,只有当信号量释放的时候,获取信号量的任务才会被唤醒,如此一来就达到任务与任务的同步,同时程序的运行会在串口打印出相关信息。

/**************************** 任务句柄 ********************************/
/* 
 * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
 * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
 * 这个句柄可以为NULL。
 */
static TaskHandle_t AppTaskCreate_Handle = NULL;/* 创建任务句柄 */
static TaskHandle_t Receive_Task_Handle = NULL;/* LED任务句柄 */
static TaskHandle_t Send_Task_Handle = NULL;/* KEY任务句柄 */

/********************************** 内核对象句柄 *********************************/
/*
 * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
 * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
 * 们就可以通过这个句柄操作这些内核对象。
 *
 * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
 * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
 * 来完成的
 * 
 */
SemaphoreHandle_t BinarySem_Handle =NULL;

/******************************* 全局变量声明 ************************************/
/*
 * 当我们在写应用程序的时候,可能需要用到一些全局变量。
 */
/******************************* 宏定义 ************************************/
/*
 * 当我们在写应用程序的时候,可能需要用到一些宏定义。
 */
/*
*************************************************************************
*                             函数声明
*************************************************************************
*/
static void AppTaskCreate(void);/* 用于创建任务 */
static void Receive_Task(void* pvParameters);/* Receive_Task任务实现 */
static void Send_Task(void* pvParameters);/* Send_Task任务实现 */
static void BSP_Init(void);/* 用于初始化板载相关资源 */

/*****************************************************************
  * @brief  主函数
  * @param  无
  * @retval 无
  * @note   第一步:开发板硬件初始化 
            第二步:创建APP应用任务
            第三步:启动FreeRTOS,开始多任务调度
  ****************************************************************/
int main(void)
{	
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  
  /* 开发板硬件初始化 */
  BSP_Init();
	printf("这是一个[野火]-STM32全系列开发板-FreeRTOS二值信号量同步实验!\n");
  printf("按下KEY1或者KEY2进行任务与任务间的同步\n");
   /* 创建AppTaskCreate任务 */
  xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                        (const char*    )"AppTaskCreate",/* 任务名字 */
                        (uint16_t       )512,  /* 任务栈大小 */
                        (void*          )NULL,/* 任务入口函数参数 */
                        (UBaseType_t    )1, /* 任务的优先级 */
                        (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
  /* 启动任务调度 */           
  if(pdPASS == xReturn)
    vTaskStartScheduler();   /* 启动任务,开启调度 */
  else
    return -1;  
  
  while(1);   /* 正常不会执行到这里 */    
}
/***********************************************************************
  * @ 函数名  : AppTaskCreate
  * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
  * @ 参数    : 无  
  * @ 返回值  : 无
  **********************************************************************/
static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  taskENTER_CRITICAL();           //进入临界区
  /* 创建 BinarySem */
  BinarySem_Handle = xSemaphoreCreateBinary();	 
  if(NULL != BinarySem_Handle)
    printf("BinarySem_Handle二值信号量创建成功!\r\n");
  /* 创建Receive_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )Receive_Task, /* 任务入口函数 */
                        (const char*    )"Receive_Task",/* 任务名字 */
                        (uint16_t       )512,   /* 任务栈大小 */
                        (void*          )NULL,	/* 任务入口函数参数 */
                        (UBaseType_t    )2,	    /* 任务的优先级 */
                        (TaskHandle_t*  )&Receive_Task_Handle);/* 任务控制块指针 */
  if(pdPASS == xReturn)
    printf("创建Receive_Task任务成功!\r\n");
  /* 创建Send_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )Send_Task,  /* 任务入口函数 */
                        (const char*    )"Send_Task",/* 任务名字 */
                        (uint16_t       )512,  /* 任务栈大小 */
                        (void*          )NULL,/* 任务入口函数参数 */
                        (UBaseType_t    )3, /* 任务的优先级 */
                        (TaskHandle_t*  )&Send_Task_Handle);/* 任务控制块指针 */ 
  if(pdPASS == xReturn)
    printf("创建Send_Task任务成功!\n\n");
  vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
  taskEXIT_CRITICAL();            //退出临界区
}
/**********************************************************************
  * @ 函数名  : Receive_Task
  * @ 功能说明: Receive_Task任务主体
  * @ 参数    :   
  * @ 返回值  : 无
  ********************************************************************/
static void Receive_Task(void* parameter)
{	
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  while (1)
  {
    //获取二值信号量 xSemaphore,没获取到则一直等待
		xReturn = xSemaphoreTake(BinarySem_Handle,/* 二值信号量句柄 */
                              portMAX_DELAY); /* 等待时间 */
    if(pdTRUE == xReturn)
      printf("BinarySem_Handle二值信号量获取成功!\n\n");
		LED1_TOGGLE;
  }
}

/**********************************************************************
  * @ 函数名  : Send_Task
  * @ 功能说明: Send_Task任务主体
  * @ 参数    :   
  * @ 返回值  : 无
  ********************************************************************/
static void Send_Task(void* parameter)
{	 
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  while (1)
  {
    /* K1 被按下 */
    if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )
    {
      xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量
      if( xReturn == pdTRUE )
        printf("BinarySem_Handle二值信号量释放成功!\r\n");
      else
        printf("BinarySem_Handle二值信号量释放失败!\r\n");
    } 
    /* K2 被按下 */
    if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )
    {
      xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量
      if( xReturn == pdTRUE )
        printf("BinarySem_Handle二值信号量释放成功!\r\n");
      else
        printf("BinarySem_Handle二值信号量释放失败!\r\n");
    }
    vTaskDelay(20);
  }
}
/***********************************************************************
  * @ 函数名  : BSP_Init
  * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
  * @ 参数    :   
  * @ 返回值  : 无
  *********************************************************************/
static void BSP_Init(void)
{
	/*
	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
	 * 都统一用这个优先级分组,千万不要再分组,切忌。
	 */
	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
	/* LED 初始化 */
	LED_GPIO_Config();
	/* 串口初始化	*/
	Debug_USART_Config();
  /* 按键初始化	*/
  Key_GPIO_Config();
}
/********************************END OF FILE****************************/

5.10.5.2计数信号量实验

  • 计数型信号量实验是模拟停车场工作运行。

  • 在创建信号量的时候初始化 5 个可用的信号量,并且创建了两个任务:一个是获取信号量任务,一个是释放信号量任务,两个任务独立运行,获取信号量任务是通过按下 KEY1 按键进行信号量的获取,模拟停车场停车操作,其等待时间是 0,在串口调试助手输出相应信息。释放信号量任务则是信号量的释放,释放信号量任务也是通过按下 KEY2 按键进行信号量的释放,模拟停车场取车操作,在串口调试助手输出相应信息。

/**************************** 任务句柄 ********************************/
/* 
 * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
 * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
 * 这个句柄可以为NULL。
 */
static TaskHandle_t AppTaskCreate_Handle = NULL;/* 创建任务句柄 */
static TaskHandle_t Take_Task_Handle = NULL;/* Take_Task任务句柄 */
static TaskHandle_t Give_Task_Handle = NULL;/* Give_Task任务句柄 */

/********************************** 内核对象句柄 *********************************/
/*
 * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
 * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
 * 们就可以通过这个句柄操作这些内核对象。
 *
 * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
 * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
 * 来完成的
 * 
 */
SemaphoreHandle_t CountSem_Handle =NULL;

/******************************* 全局变量声明 ************************************/
/*
 * 当我们在写应用程序的时候,可能需要用到一些全局变量。
 */


/******************************* 宏定义 ************************************/
/*
 * 当我们在写应用程序的时候,可能需要用到一些宏定义。
 */


/*
*************************************************************************
*                             函数声明
*************************************************************************
*/
static void AppTaskCreate(void);/* 用于创建任务 */

static void Take_Task(void* pvParameters);/* Take_Task任务实现 */
static void Give_Task(void* pvParameters);/* Give_Task任务实现 */

static void BSP_Init(void);/* 用于初始化板载相关资源 */

/*****************************************************************
  * @brief  主函数
  * @param  无
  * @retval 无
  * @note   第一步:开发板硬件初始化 
            第二步:创建APP应用任务
            第三步:启动FreeRTOS,开始多任务调度
  ****************************************************************/
int main(void)
{	
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  
  /* 开发板硬件初始化 */
  BSP_Init();
  
  printf("这是一个[野火]-STM32全系列开发板-FreeRTOS计数信号量实验!\n");
  printf("车位默认值为5个,按下KEY1申请车位,按下KEY2释放车位!\n\n");
  
  /* 创建AppTaskCreate任务 */
  xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                        (const char*    )"AppTaskCreate",/* 任务名字 */
                        (uint16_t       )512,  /* 任务栈大小 */
                        (void*          )NULL,/* 任务入口函数参数 */
                        (UBaseType_t    )1, /* 任务的优先级 */
                        (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
  /* 启动任务调度 */           
  if(pdPASS == xReturn)
    vTaskStartScheduler();   /* 启动任务,开启调度 */
  else
    return -1;  
  
  while(1);   /* 正常不会执行到这里 */    
}


/***********************************************************************
  * @ 函数名  : AppTaskCreate
  * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
  * @ 参数    : 无  
  * @ 返回值  : 无
  **********************************************************************/
static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  
  taskENTER_CRITICAL();           //进入临界区
  
  /* 创建Test_Queue */
  CountSem_Handle = xSemaphoreCreateCounting(5,5);	 
  if(NULL != CountSem_Handle)
    printf("CountSem_Handle计数信号量创建成功!\r\n");

  /* 创建Take_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )Take_Task, /* 任务入口函数 */
                        (const char*    )"Take_Task",/* 任务名字 */
                        (uint16_t       )512,   /* 任务栈大小 */
                        (void*          )NULL,	/* 任务入口函数参数 */
                        (UBaseType_t    )2,	    /* 任务的优先级 */
                        (TaskHandle_t*  )&Take_Task_Handle);/* 任务控制块指针 */
  if(pdPASS == xReturn)
    printf("创建Take_Task任务成功!\r\n");
  
  /* 创建Give_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )Give_Task,  /* 任务入口函数 */
                        (const char*    )"Give_Task",/* 任务名字 */
                        (uint16_t       )512,  /* 任务栈大小 */
                        (void*          )NULL,/* 任务入口函数参数 */
                        (UBaseType_t    )3, /* 任务的优先级 */
                        (TaskHandle_t*  )&Give_Task_Handle);/* 任务控制块指针 */ 
  if(pdPASS == xReturn)
    printf("创建Give_Task任务成功!\n\n");
  
  vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
  
  taskEXIT_CRITICAL();            //退出临界区
}



/**********************************************************************
  * @ 函数名  : Take_Task
  * @ 功能说明: Take_Task任务主体
  * @ 参数    :   
  * @ 返回值  : 无
  ********************************************************************/
static void Take_Task(void* parameter)
{	
  BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */
  /* 任务都是一个无限循环,不能返回 */
  while (1)
  {
    //如果KEY1被单击
		if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )       
		{
			/* 获取一个计数信号量 */
      xReturn = xSemaphoreTake(CountSem_Handle,	/* 计数信号量句柄 */
                             0); 	/* 等待时间:0 */
			if ( pdTRUE == xReturn ) 
				printf( "KEY1被按下,成功申请到停车位。\n" );
			else
				printf( "KEY1被按下,不好意思,现在停车场已满!\n" );							
		}
		vTaskDelay(20);     //每20ms扫描一次		
  }
}

/**********************************************************************
  * @ 函数名  : Give_Task
  * @ 功能说明: Give_Task任务主体
  * @ 参数    :   
  * @ 返回值  : 无
  ********************************************************************/
static void Give_Task(void* parameter)
{	 
  BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */
  /* 任务都是一个无限循环,不能返回 */
  while (1)
  {
    //如果KEY2被单击
		if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )       
		{
			/* 获取一个计数信号量 */
      xReturn = xSemaphoreGive(CountSem_Handle);//给出计数信号量                  
			if ( pdTRUE == xReturn ) 
				printf( "KEY2被按下,释放1个停车位。\n" );
			else
				printf( "KEY2被按下,但已无车位可以释放!\n" );							
		}
		vTaskDelay(20);     //每20ms扫描一次	
  }
}
/***********************************************************************
  * @ 函数名  : BSP_Init
  * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
  * @ 参数    :   
  * @ 返回值  : 无
  *********************************************************************/
static void BSP_Init(void)
{
	/*
	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
	 * 都统一用这个优先级分组,千万不要再分组,切忌。
	 */
	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
	
	/* LED 初始化 */
	LED_GPIO_Config();
	
	/* 按键初始化	*/
  Key_GPIO_Config();

	/* 串口初始化	*/
	Debug_USART_Config();
  
}

/********************************END OF FILE****************************/

6.互斥量

6.1互斥量基本概念

  • 互斥量又称互斥信号量(本质是信号量),是一种特殊的二值信号量,它和信号量不同的是,它支持互斥量所有权、递归访问以及防止优先级翻转的特性,用于实现对临界资源的独占式处理。任意时刻互斥量的状态只有两种,开锁或闭锁。当互斥量被任务持有时,该互斥量处于闭锁状态,这个任务获得互斥量的所有权。

  • 当该任务释放这个互斥量时,该互斥量处于开锁状态,任务失去该互斥量的所有权。当一个任务持有互斥量时,其他任务将不能再对该互斥量进行开锁或持有。

  • 持有该互斥量的任务也能够再次获得这个锁而不被挂起,这就是递归访问,也就是递归互斥量的特性,这个特性与一般的信号量有很大的不同,在信号量中,由于已经不存在可用的信号量,任务递归获取信号量时会发生主动挂起任务最终形成死锁。

  • 如果想要用于实现同步(任务之间或者任务与中断之间),二值信号量或许是更好的选择,虽然互斥量也可以用于任务与任务、任务与中断的同步,但是互斥量更多的是用于保护资源的互锁

  • 用于互锁的互斥量可以充当保护资源的令牌,当一个任务希望访问某个资源时,它必须先获取令牌。

  • 当任务使用完资源后,必须还回令牌,以便其它任务可以访问该资源。是不是很熟悉,在我们的二值信号量里面也是一样的,用于保护临界资源,保证多任务的访问井然有序。

  • 当任务获取到信号量的时候才能开始使用被保护的资源,使用完就释放信号量,下一个任务才能获取到信号量从而可用使用被保护的资源。

  • 但是信号量会导致的另一个潜在问题,那就是任务优先级翻转(具体会在下文讲解)。而 FreeRTOS 提供的互斥量可以通过优先级继承算法,可以降低优先级翻转问题产生的影响,所以,用于临界资源的保护一般建议使用互斥量。

6.2互斥量的优先级继承机制

  • 在 FreeRTOS 操作系统中为了降低优先级翻转问题利用了优先级继承算法。

  • 优先级继承算法是指,暂时提高某个占有某种资源的低优先级任务的优先级,使之与在所有等待该资源的任务中优先级最高那个任务的优先级相等,而当这个低优先级任务执行完毕释放该资源时,优先级重新回到初始设定值。

  • 因此,继承优先级的任务避免了系统资源被任何中间优先级的任务抢占。互斥量与二值信号量最大的不同是:互斥量具有优先级继承机制,而信号量没有。

  • 也就是说,某个临界资源受到一个互斥量保护,如果这个资源正在被一个低优先级任务使用,那么此时的互斥量是闭锁状态,也代表了没有任务能申请到这个互斥量,如果此时一个高优先级任务想要对这个资源进行访问,去申请这个互斥量,那么高优先级任务会因为申请不到互斥量而进入阻塞态,那么系统会将现在持有该互斥量的任务的优先级临时提升到与高优先级任务的优先级相同,这个优先级提升的过程叫做优先级继承。

  • 这个优先级继承机制确保高优先级任务进入阻塞状态的时间尽可能短,以及将已经出现的“优先级翻转”危害降低到最小。没有理解?没问题,结合过程示意图再说一遍。我们知道任务的优先级在创建的时候就已经是设置好的,高优先级的任务可以打断低优先级的任务,抢占 CPU 的使用权。但是在很多场合中,某些资源只有一个,当低优先级任务正在占用该资源的时候,即便高优先级任务也只能乖乖的等待低优先级任务使用完该资源后释放资源。

  • 这里高优先级任务无法运行而低优先级任务可以运行的现象称为“优先级翻转”。为什么说优先级翻转在操作系统中是危害很大?因为在我们一开始创造这个系统的时候,我们就已经设置好了任务的优先级了,越重要的任务优先级越高。

  • 但是发生优先级翻转,对我们操作系统是致命的危害,会导致系统的高优先级任务阻塞时间过长。举个例子,现在有 3 个任务分别为 H 任务(High)、M 任务(Middle)、L 任务(Low),3 个任务的优先级顺序为 H 任务>M 任务>L 任务。正常运行的时候 H 任务可以打断 M任务与 L 任务,M 任务可以打断 L 任务,假设系统中有一个资源被保护了,此时该资源被 L 任务正在使用中,某一刻,H 任务需要使用该资源,但是 L 任务还没使用完,H任务则因为申请不到资源而进入阻塞态,L 任务继续使用该资源,此时已经出现了“优先级翻转”现象,高优先级任务在等着低优先级的任务执行,如果在 L 任务执行的时候刚好M 任务被唤醒了,由于 M 任务优先级比 L 任务优先级高,那么会打断 L 任务,抢占了CPU 的使用权,直到 M 任务执行完,再把 CUP 使用权归还给 L 任务,L 任务继续执行,等到执行完毕之后释放该资源,H 任务此时才从阻塞态解除,使用该资源。这个过程,本来是最高优先级的 H 任务,在等待了更低优先级的 L 任务与 M 任务,其阻塞的时间是 M任务运行时间+L 任务运行时间,这只是只有 3 个任务的系统,假如很多个这样子的任务打断最低优先级的任务,那这个系统最高优先级任务岂不是崩溃了,这个现象是绝对不允许出现的,高优先级的任务必须能及时响应。所以,没有优先级继承的情况下,使用资源保护,其危害极大。

  •  

    41ab61ef5b4ef9bbeb6161be37c89095.png

  • 图 1(1):L 任务正在使用某临界资源, H 任务被唤醒,执行 H 任务。但 L 任务并未执行完毕,此时临界资源还未释放。

  • 图 1(2):这个时刻 H 任务也要对该临界资源进行访问,但 L 任务还未释放资源,由于保护机制,H 任务进入阻塞态,L 任务得以继续运行,此时已经发生了优先级翻转现象。

  • (3):某个时刻 M 任务被唤醒,由于 M 任务的优先级高于 L 任务, M 任务抢占了 CPU 的使用权,M任务开始运行,此时 L 任务尚未执行完,临界资源还没被释放。

  • (4):M 任务运行结束,归还 CPU 使用权,L 任务继续运行。

  • (5):L任务运行结束,释放临界资源,H 任务得以对资源进行访问,H 任务开始运行。在这过程中,H 任务的等待时间过长,这对系统来说这是很致命的,所以这种情况不允许出现,而互斥量就是用来降低优先级翻转的产生的危害。

  • 假如有优先级继承呢?那么,在 H 任务申请该资源的时候,由于申请不到资源会进入阻塞态,那么系统就会把当前正在使用资源的 L 任务的优先级临时提高到与 H 任务优先级相同,此时 M 任务被唤醒了,因为它的优先级比 H 任务低,所以无法打断 L 任务,因为此时 L 任务的优先级被临时提升到 H,所以当 L 任务使用完该资源了,进行释放,那么此时 H 任务优先级最高,将接着抢占 CPU 的使用权, H 任务的阻塞时间仅仅是 L 任务的执行时间,此时的优先级的危害降到了最低,看!这就是优先级继承的优势,具体见图 。

  •  

    610128fcc6dc95ba339608c1aff52d02.png

  • 图 (1)**:L 任务正在使用某临界资源,L 任务正在使用某临界资源, H 任务被唤醒,执行 H 任务。但 L 任务并未执行完毕,此时临界资源还未释放。

  • 图 (2)**:某一时刻 H 任务也要对该资源进行访问,由于保护机制,H 任务进入阻塞态。此时发生优先级继承,系统将 L 任务的优先级暂时提升到与 H 任务优先级相同,L任务继续执行。

  • 图 (3)**:在某一时刻 M 任务被唤醒,由于此时 M 任务的优先级暂时低于 L 任务,所以 M 任务仅在就绪态,而无法获得 CPU 使用权。

  • (4):L任务运行完毕,H 任务获得对资源的访问权,H 任务从阻塞态变成运行态,此时 L 任务的优先级会变回原来的优先级。

  • 图 (5)**:当 H 任务运行完毕,M任务得到 CPU 使用权,开始执行。

  • 图 (6)**:系统正常运行,按照设定好的优先级运行。但是使用互斥量的时候一定需要注意:在获得互斥量后,请尽快释放互斥量,同时需要注意的是在任务持有互斥量的这段时间,不得更改任务的优先级。FreeRTOS 的优先级继承机制不能解决优先级反转,只能将这种情况的影响降低到最小,硬实时系统在一开始设计时就要避免优先级反转发生。

6.3互斥量应用场景

  • 互斥量的使用比较单一,因为它是信号量的一种,并且它是以锁的形式存在。

  • 在初始化的时候,互斥量处于开锁的状态,而被任务持有的时候则立刻转为闭锁的状态。

  • 互斥量更适合于:可能会引起优先级翻转的情况。

  • 递归互斥量更适用于:任务可能会多次获取互斥量的情况下。

  • 这样可以避免同一任务多次递归持有而造成死锁的问题。

  • 多任务环境下往往存在多个任务竞争同一临界资源的应用场景,互斥量可被用于对临界资源的保护从而实现独占式访问。

  • 另外,互斥量可以降低信号量存在的优先级翻转问题带来的影响。比如有两个任务需要对串口进行发送数据,其硬件资源只有一个,那么两个任务肯定不能同时发送啦,不然导致数据错误,那么,就可以用互斥量对串口资源进行保护,当一个任务正在使用串口的时候,另一个任务则无法使用串口,等到任务使用串口完毕之后,另外一个任务才能获得串口的使用权。

  • 另外需要注意的是互斥量不能在中断服务函数中使用,因为其特有的优先级继承机制只在任务起作用,在中断的上下文环境毫无意义

6.4互斥量运作机制

  • 多任务环境下会存在多个任务访问同一临界资源的场景,该资源会被任务独占处理。

  • 其他任务在资源被占用的情况下不允许对该临界资源进行访问,这个时候就需要用到FreeRTOS 的互斥量来进行资源保护,那么互斥量是怎样来避免这种冲突?

  • 用互斥量处理不同任务对临界资源的同步访问时,任务想要获得互斥量才能进行资源访问,如果一旦有任务成功获得了互斥量,则互斥量立即变为闭锁状态,此时其他任务会因为获取不到互斥量而不能访问这个资源,任务会根据用户自定义的等待时间进行等待,直到互斥量被持有的任务释放后,其他任务才能获取互斥量从而得以访问该临界资源,此时互斥量再次上锁,如此一来就可以确保每个时刻只有一个任务正在访问这个临界资源,保证了临界资源操作的安全性。

  •  

    5d030aab4c0621272cfc35be2a11e763.png

  • (1):因为互斥量具有优先级继承机制,一般选择使用互斥量对资源进行保护,如果资源被占用的时候,无论是什么优先级的任务想要使用该资源都会被阻塞。

  • (2):假如正在使用该资源的任务 1 比阻塞中的任务 2 的优先级还低,那么任务1 将被系统临时提升到与高优先级任务 2 相等的优先级(任务 1 的优先级从 L 变成 H)。

  • (3):当任务 1 使用完资源之后,释放互斥量,此时任务 1 的优先级会从 H 变回原来的 L。

  • (4)-(5):任务 2 此时可以获得互斥量,然后进行资源的访问,当任务 2 访问了资源的时候,该互斥量的状态又为闭锁状态,其他任务无法获取互斥量。

6.5互斥量控制块

  • 互斥量的 API 函数实际上都是宏,它使用现有的队列机制,这些宏定义在 semphr.h 文件中,如果使用互斥量,需要包含 semphr.h 头文件。

  • 所以 FreeRTOS 的互斥量控制块结构体与消息队列结构体是一模一样的,只不过结构体中某些成员变量代表的含义不一样而已,我们会具体讲解一下哪里与消息队列不一样。

  • 先来看看结构体控制块,体见代码清单加粗部分。

  • 注意:没说明的部分与消息队列一致

  • typedef struct QueueDefinition {
        int8_t *pcHead;
        int8_t *pcTail;
        int8_t *pcWriteTo;
    
        union {
            int8_t *pcReadFrom; 
            UBaseType_t uxRecursiveCallCount; (1)
        } u;
    
        List_t xTasksWaitingToSend;
        List_t xTasksWaitingToReceive;
    
        volatile UBaseType_t uxMessagesWaiting; (1) 
        UBaseType_t uxLength; (2) 
        UBaseType_t uxItemSize; (3) 
    
        volatile int8_t cRxLock;
        volatile int8_t cTxLock;
    
        #if( ( configSUPPORT_STATIC_ALLOCATION == 1 )
        && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
            uint8_t ucStaticallyAllocated;
        #endif
    
        #if ( configUSE_QUEUE_SETS == 1 )
        struct QueueDefinition *pxQueueSetContainer;
        #endif
    
        #if ( configUSE_TRACE_FACILITY == 1 )
        UBaseType_t uxQueueNumber;
        uint8_t ucQueueType;
        #endif
    
    } xQUEUE;
    
    typedef xQUEUE Queue_t;
  • 代码清单 (1): pcReadFrom 与 uxRecursiveCallCount 是一对互斥变量,使用联合体用来确保两个互斥的结构体成员不会同时出现。当结构体用于队列时,pcReadFrom 指向出队消息空间的最后一个,见文知义,就是读取消息时候是从 pcReadFrom 指向的空间读取消息内容。当结构体用于互斥量时,uxRecursiveCallCount 用于计数,记录递归互斥量被“调用”的次数。

  • 代码清单 (2):如果控制块结构体是用于消息队列:uxMessagesWaiting 用来记录当前消息队列的消息个数;如果控制块结构体被用于互斥量的时候,这个值就表示有效互斥量个数,这个值是 1则表示互斥量有效,如果是 0 则表示互斥量无效。

  • 代码清单 (3):如果控制块结构体是用于消息队列:uxLength 表示队列的长度,也就是能存放多少消息;如果控制块结构体被用于互斥量的时候,uxLength 表示最大的信号量可用个数,uxLength 最大为 1,因为信号量要么是有效的,要么是无效的。

  • 代码清单 (4):如果控制块结构体是用于消息队列:uxItemSize 表示单个消息的大小;如果控制块结构体被用于互斥量的时候,则无需存储空间,为 0 即可。

6.6互斥量函数接口讲解

6.6.1互斥量创建函数 xSemaphoreCreateMutex()

  • xSemaphoreCreateMutex()用于创建一个互斥量,并返回一个互斥量句柄。

  • 该句柄的原型是一个 void 型的指针,在使用之前必须先由用户定义一个互斥量句柄。

  • 要想使用该函数必须在 FreeRTOSConfig.h 中把宏 configSUPPORT_DYNAMIC_ALLOCATION 定义为 1,即开启动态内存分配,其实该宏在 FreeRTOS.h 中默认定义为 1,即所有 FreeRTOS 的对象在创建的时候都默认使用动态内存分配方案,同时还需在 FreeRTOSConfig.h 中把configUSE_MUTEXES 宏定义打开,表示使用互斥量。

  • 使用实例如下

  • SemaphoreHandle_t MuxSem_Handle;
    void vATask( void * pvParameters )
    {
        /* 创建一个互斥量 */ 
        MuxSem_Handle= xSemaphoreCreateMutex(); 
    
        if (MuxSem_Handle!= NULL ) { 
            /* 互斥量创建成功 */ 
        } 
    }

6.6.2 xSemaphoreCreateRecursiveMutex()

  • xSemaphoreCreateRecursiveMutex()用于创建一个递归互斥量,不是递归的互斥量由函数 xSemaphoreCreateMutex() 或 xSemaphoreCreateMutexStatic()创建(我们只讲解动态创建),且只能被同一个任务获取一次,如果同一个任务想再次获取则会失败。

  • 递归信号量则相反,它可以被同一个任务获取很多次,获取多少次就需要释放多少次。

  • 递归信号量与互斥量一样,都实现了优先级继承机制,可以降低优先级反转的危害。

  • 要 想 使用该 函 数 必 须 在 FreeRTOSConfig.h 中 把 宏configSUPPORT_DYNAMIC_ALLOCATION 和 configUSE_RECURSIVE_MUTEXES 均定义为 1。

  • 宏 configSUPPORT_DYNAMIC_ALLOCATION 定义为 1 即表示开启动态内存分配,其实该宏在 FreeRTOS.h 中默认定义为 1,即所有 FreeRTOS 的对象在创建的时候都默认使用 动 态 内 存 分 配 方 案 。 该 函 数 的 具 体 说 明 见 表 格 , 应 用 举 例 见 其 实xSemaphoreCreateRecursiveMutex()实际调用的函数就是 xQueueCreateMutex()函数,具体的创建过程也不再重复赘述,参考前一小节,下面来看看如何使用xSemaphoreCreateRecursiveMutex()函数,具体见代码清单。

  •  

    8427994df3082b11da71948d7e861864.png

  • 其实 xSemaphoreCreateRecursiveMutex()实际调用的函数就是 xQueueCreateMutex()函数,具 体 的 创 建 过 程 也 不 再 重 复 赘 述 , 参 考 前 一 小 节 , 下 面 来 看 看 如 何 使 用xSemaphoreCreateRecursiveMutex()函数,具体见代码清单。

  • SemaphoreHandle_t xMutex;
    
    void vATask( void * pvParameters )
    {
        /* 创建一个递归互斥量 */ 
        xMutex = xSemaphoreCreateRecursiveMutex(); 
    
        if ( xMutex != NULL ) { 
            /* 递归互斥量创建成功 */ 
        }
    }

6.6.3vSemaphoreDelete()

  • 互斥量的本质是信号量,直接调用 vSemaphoreDelete()函数进行删除即可,具体见18.6.2 信号量删除函数章节。

6.6.4xSemaphoreTake()

  • 我们知道,当互斥量处于开锁的状态,任务才能获取互斥量成功,当任务持有了某个互斥量的时候,其它任务就无法获取这个互斥量,需要等到持有互斥量的任务进行释放后,其他任务才能获取成功,任务通过互斥量获取函数来获取互斥量的所有权。

  • 任务对互斥量的所有权是独占的,任意时刻互斥量只能被一个任务持有,如果互斥量处于开锁状态,那么获取该互斥量的任务将成功获得该互斥量,并拥有互斥量的使用权;如果互斥量处于闭锁状态,获取该互斥量的任务将无法获得互斥量,任务将被挂起,在任务被挂起之前,会进行优先级继承,如果当前任务优先级比持有互斥量的任务优先级高,那么将会临时提升持有互斥量任务的优先级。

  • 互斥量的获取函数是一个宏定义,实际调用的函数就是xQueueGenericReceive(),具体见代码清单。

  • 使用实例:

  • static void HighPriority_Task(void* parameter)
    {
        BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为 pdTRUE */
        while (1) {
            printf("HighPriority_Task 获取信号量\n");
            //获取互斥量 MuxSem,没获取到则一直等待 
            xReturn = xSemaphoreTake(MuxSem_Handle,/* 互斥量句柄 */ 
                                     portMAX_DELAY); /* 等待时间 */ 
            if (pdTRUE == xReturn)
                printf("HighPriority_Task Runing\n");
            LED1_TOGGLE;
            //处理临界资源
            printf("HighPriority_Task 释放信号量!\r\n");
            xSemaphoreGive( MuxSem_Handle );//释放互斥量
            vTaskDelay(1000);
        }
    }    

6.6.5xSemaphoreTakeRecursive()

  • xSemaphoreTakeRecursive()是一个用于获取递归互斥量的宏,与互斥量的获取函数一样,xSemaphoreTakeRecursive()也是一个宏定义,它最终使用现有的队列机制,实际执行的函数是 xQueueTakeMutexRecursive() 。

  • 互 斥 量 之 前 必 须 由xSemaphoreCreateRecursiveMutex()这个函数创建。

  • 要注意的是该函数不能用于获取由函数xSemaphoreCreateMutex()创建的互斥量。

  • 要想使用该函数必须在头文件 FreeRTOSConfig.h 中把宏 configUSE_RECURSIVE_MUTEXES 定义为 1。

  •  

    3cc8b84e07fa69a6809e2888196d3bcc.png

  • SemaphoreHandle_t xMutex = NULL;
    /* 创建信号量的任务 */
    void vATask( void * pvParameters )
    {
        /* 创建一个递归互斥量,保护共享资源 */
        xMutex = xSemaphoreCreateRecursiveMutex();
    }
    /* 使用互斥量 */
    void vAnotherTask( void * pvParameters )
    {
        /* ... 做其他的事情 */
    
        if ( xMutex != NULL ) {
            /* 尝试获取递归信号量。 如果信号量不可用则等待 10 个 ticks */ 
            if(xSemaphoreTakeRecursive(xMutex,( TickType_t)10)==pdTRUE ) { 
                /* 获取到递归信号量,可以访问共享资源 */ 
                /* ... 其他功能代码 */ 
    
                /* 重复获取递归信号量 */ 
                xSemaphoreTakeRecursive( xMutex, ( TickType_t ) 10 ); 
                xSemaphoreTakeRecursive( xMutex, ( TickType_t ) 10 ); 
    
                /* 释放递归信号量,获取了多少次就要释放多少次 */
                xSemaphoreGiveRecursive( xMutex );
                xSemaphoreGiveRecursive( xMutex );
                xSemaphoreGiveRecursive( xMutex );
                /* 现在递归互斥量可以被其他任务获取 */
            } else {
                /* 没能成功获取互斥量,所以不能安全的访问共享资源 */
            }
        }
    }   

6.6.6互斥量释放函数 xSemaphoreGive()

  • 任务想要访问某个资源的时候,需要先获取互斥量,然后进行资源访问,在任务使用完该资源的时候,必须要及时归还互斥量,这样别的任务才能对资源进行访问。

  • 在前面的讲解中,我们知道,当互斥量有效的时候,任务才能获取互斥量,那么,是什么函数使得信号量变得有效呢?FreeRTOS 给我们提供了互斥量释放函数 xSemaphoreGive(),任务可以调用 xSemaphoreGive()函数进行释放互斥量,表示我已经用完了,别人可以申请使用,互斥量的释放函数与信号量的释放函数一致,都是调用 xSemaphoreGive()函数,但是要注意的是,互斥量的释放只能在任务中,不允许在中断中释放互斥量。

  • 使用该函数接口时,只有已持有互斥量所有权的任务才能释放它,当任务调用xSemaphoreGive()函数时会将互斥量变为开锁状态,等待获取该互斥量的任务将被唤醒。

  • 如果任务的优先级被互斥量的优先级翻转机制临时提升,那么当互斥量被释放后,任务的优先级将恢复为原本设定的优先级。

  • 我们知道互斥量、信号量的释放就是调用 xQueueGenericSend()函数,但是互斥量的处理还是有一些不一样的地方,因为它有优先级继承机制,在释放互斥量的时候我们需要恢复任务的初始优先级,所以,下面我们来看看具体在哪恢复任务的优先级,其实就是prvCopyDataToQueue()这个函数,该函数在 xQueueGenericSend()中被调用。

  • 简单总结一下互斥量释放的过程:被释放前的互斥量是处于无效状态,被释放后互斥量才变得有效,除了结构体成员变量 uxMessageWaiting 加 1 外,还要判断持有互斥量的任务是否有优先级继承,如果有的话,要将任务的优先级恢复到初始值。当然,该任务必须在没有持有其它互斥量的情况下,才能将继承的优先级恢复到原始值。然后判断是否有任务要获取互斥量并且进入阻塞状态,有的话解除阻塞,最后返回成功信息(pdPASS),下面看看互斥量释放函数是如何使用的,具体见代码清单

  • void vATask( void * pvParameters )
    {
        /* 创建一个互斥量用于保护共享资源 */
        xSemaphore = xSemaphoreCreateMutex();
    
        if ( xSemaphore != NULL ) {
            if ( xSemaphoreGive( xSemaphore ) != pdTRUE ) {
                /* 如果要释放一个互斥量,必须先有第一次的获取*/
            }
    
            /* 获取互斥量,不等待 */
            if ( xSemaphoreTake( xSemaphore, ( TickType_t ) 0 ) ) {
                /* 获取到互斥量,可以访问共享资源 */
    
                /* ... 访问共享资源代码 */
    
                /* 共享资源访问完毕,释放互斥量 */ 
                if ( xSemaphoreGive( xSemaphore ) != pdTRUE ) { 
                    /* 互斥量释放失败,这可不是我们希望的 */
                }
            }
        }
    }

6.6.7xSemaphoreGiveRecursive()

  • xSemaphoreGiveRecursive()是一个用于释放递归互斥量的宏。要想使用该函数必须在头文件 FreeRTOSConfig.h 把宏 configUSE_RECURSIVE_MUTEXES 定义为 1。

  • xSemaphoreGiveRecursive()函数用于释放一个递归互斥量。已经获取递归互斥量的任务可以重复获取该递归互斥量。使用 xSemaphoreTakeRecursive() 函数成功获取几次递归互斥量,就要使用 xSemaphoreGiveRecursive()函数返还几次,在此之前递归互斥量都处于无效状态,别的任务就无法获取该递归互斥量。使用该函数接口时,只有已持有互斥量所有权的任务才能释放它,每释放一次该递归互斥量,它的计数值就减 1。当该互斥量的计数值为 0 时(即持有任务已经释放所有的持有操作),互斥量则变为开锁状态,等待在该互斥量上的任务将被唤醒。如果任务的优先级被互斥量的优先级翻转机制临时提升,那么当互斥量被释放后,任务的优先级将恢复为原本设定的优先级,具体见代码清单。

  • 互斥量和递归互斥量的最大区别在于一个递归互斥量可以被已经获取这个递归互斥量的任务重复 获取, 而不会形成死锁。这个递归调用功能是通 过队列结构体成 员u.uxRecursiveCallCount 实现的,这个变量用于存储递归调用的次数,每次获取递归互斥量后,这个变量加 1,在释放递归互斥量后,这个变量减 1。只有这个变量减到 0,即释放和获取的次数相等时,互斥量才能变成有效状态,然后才允许使用 xQueueGenericSend()函数释放一个递归互斥量,xSemaphoreGiveRecursive()函数使用实例具体见代码清单。

  • SemaphoreHandle_t xMutex = NULL;
    
    void vATask( void * pvParameters )
    {
        /* 创建一个递归互斥量用于保护共享资源 */
        xMutex = xSemaphoreCreateRecursiveMutex();
    }
    
    void vAnotherTask( void * pvParameters )
    {
        /* 其他功能代码 */
    
        if ( xMutex != NULL ) {
            /* 尝试获取递归互斥量
     如果不可用则等待 10 个 ticks */
            if(xSemaphoreTakeRecursive(xMutex,( TickType_t ) 10 )== pdTRUE) {
                /* 获取到递归信号量,可以访问共享资源 */
                /* ... 其他功能代码 */
    
                /* 重复获取递归互斥量 */
                xSemaphoreTakeRecursive( xMutex, ( TickType_t ) 10 );
                xSemaphoreTakeRecursive( xMutex, ( TickType_t ) 10 );
    
                /* 释放递归互斥量,获取了多少次就要释放多少次 */ 
                xSemaphoreGiveRecursive( xMutex ); 
                xSemaphoreGiveRecursive( xMutex ); 
                xSemaphoreGiveRecursive( xMutex ); 
    
                /* 现在递归互斥量可以被其他任务获取 */
            } else {
                /* 没能成功获取互斥量,所以不能安全的访问共享资源 */
            }
        }
    }    

6.7互斥量实验

  • 模拟优先级翻转实验是在 FreeRTOS 中创建了三个任务与一个二值信号量,任务分别是高优先级任务,中优先级任务,低优先级任务,用于模拟产生优先级翻转。

  • 低优先级任务在获取信号量的时候,被中优先级打断,中优先级的任务执行时间较长,因为低优先级还未释放信号量,那么高优先级任务就无法取得信号量继续运行,此时就发生了优先级翻转,任务在运行中,使用串口打印出相关信息,具体见代码清单。

  • /**************************** 任务句柄 ********************************/
    /* 
     * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
     * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
     * 这个句柄可以为NULL。
     */
    static TaskHandle_t AppTaskCreate_Handle = NULL;/* 创建任务句柄 */
    static TaskHandle_t LowPriority_Task_Handle = NULL;/* LowPriority_Task任务句柄 */
    static TaskHandle_t MidPriority_Task_Handle = NULL;/* MidPriority_Task任务句柄 */
    static TaskHandle_t HighPriority_Task_Handle = NULL;/* HighPriority_Task任务句柄 */
    ​
    /********************************** 内核对象句柄 *********************************/
    /*
     * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
     * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
     * 们就可以通过这个句柄操作这些内核对象。
     *
     * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
     * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
     * 来完成的
     * 
     */
    SemaphoreHandle_t BinarySem_Handle =NULL;
    ​
    /******************************* 全局变量声明 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些全局变量。
     */
    ​
    ​
    /******************************* 宏定义 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些宏定义。
     */
    ​
    ​
    /*
    *************************************************************************
    *                             函数声明
    *************************************************************************
    */
    static void AppTaskCreate(void);/* 用于创建任务 */
    ​
    static void LowPriority_Task(void* pvParameters);/* LowPriority_Task任务实现 */
    static void MidPriority_Task(void* pvParameters);/* MidPriority_Task任务实现 */
    static void HighPriority_Task(void* pvParameters);/* MidPriority_Task任务实现 */
    ​
    static void BSP_Init(void);/* 用于初始化板载相关资源 */
    ​
    /*****************************************************************
      * @brief  主函数
      * @param  无
      * @retval 无
      * @note   第一步:开发板硬件初始化 
                第二步:创建APP应用任务
                第三步:启动FreeRTOS,开始多任务调度
      ****************************************************************/
    int main(void)
    {   
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      /* 开发板硬件初始化 */
      BSP_Init();
        printf("这是一个[野火]-STM32全系列开发板-FreeRTOS优先级翻转实验!\n");
       /* 创建AppTaskCreate任务 */
      xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                            (const char*    )"AppTaskCreate",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )1, /* 任务的优先级 */
                            (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
      /* 启动任务调度 */           
      if(pdPASS == xReturn)
        vTaskStartScheduler();   /* 启动任务,开启调度 */
      else
        return -1;  
      
      while(1);   /* 正常不会执行到这里 */    
    }
    ​
    ​
    /***********************************************************************
      * @ 函数名  : AppTaskCreate
      * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
      * @ 参数    : 无  
      * @ 返回值  : 无
      **********************************************************************/
    static void AppTaskCreate(void)
    {
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      taskENTER_CRITICAL();           //进入临界区
      
      /* 创建Test_Queue */
      BinarySem_Handle = xSemaphoreCreateBinary();   
      if(NULL != BinarySem_Handle)
        printf("BinarySem_Handle二值信号量创建成功!\r\n");
    ​
      xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量
    //  if( xReturn == pdTRUE )
    //    printf("释放信号量!\r\n");
        
      /* 创建LowPriority_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )LowPriority_Task, /* 任务入口函数 */
                            (const char*    )"LowPriority_Task",/* 任务名字 */
                            (uint16_t       )512,   /* 任务栈大小 */
                            (void*          )NULL,  /* 任务入口函数参数 */
                            (UBaseType_t    )2,     /* 任务的优先级 */
                            (TaskHandle_t*  )&LowPriority_Task_Handle);/* 任务控制块指针 */
      if(pdPASS == xReturn)
        printf("创建LowPriority_Task任务成功!\r\n");
      
      /* 创建MidPriority_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )MidPriority_Task,  /* 任务入口函数 */
                            (const char*    )"MidPriority_Task",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )3, /* 任务的优先级 */
                            (TaskHandle_t*  )&MidPriority_Task_Handle);/* 任务控制块指针 */ 
      if(pdPASS == xReturn)
        printf("创建MidPriority_Task任务成功!\n");
      
      /* 创建HighPriority_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )HighPriority_Task,  /* 任务入口函数 */
                            (const char*    )"HighPriority_Task",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )4, /* 任务的优先级 */
                            (TaskHandle_t*  )&HighPriority_Task_Handle);/* 任务控制块指针 */ 
      if(pdPASS == xReturn)
        printf("创建HighPriority_Task任务成功!\n\n");
      
      vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
      
      taskEXIT_CRITICAL();            //退出临界区
    }
    ​
    ​
    ​
    /**********************************************************************
      * @ 函数名  : LowPriority_Task
      * @ 功能说明: LowPriority_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void LowPriority_Task(void* parameter)
    {   
      static uint32_t i;
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      while (1)
      {
        printf("LowPriority_Task 获取信号量\n");
        //获取二值信号量 xSemaphore,没获取到则一直等待
            xReturn = xSemaphoreTake(BinarySem_Handle,/* 二值信号量句柄 */
                                  portMAX_DELAY); /* 等待时间 */
        if( xReturn == pdTRUE )
          printf("LowPriority_Task Runing\n\n");
        
        for(i=0;i<4000000;i++)//模拟低优先级任务占用信号量
            {
                taskYIELD();//发起任务调度
            }
        
        printf("LowPriority_Task 释放信号量!\r\n");
        xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量
    //    if( xReturn == pdTRUE )
    //      ;             /* 什么都不做 */
        
            LED1_TOGGLE;
        
        vTaskDelay(500);
      }
    }
    ​
    /**********************************************************************
      * @ 函数名  : MidPriority_Task
      * @ 功能说明: MidPriority_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void MidPriority_Task(void* parameter)
    {    
      while (1)
      {
       printf("MidPriority_Task Runing\n");
       vTaskDelay(500);
      }
    }
    ​
    /**********************************************************************
      * @ 函数名  : HighPriority_Task
      * @ 功能说明: HighPriority_Task 任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void HighPriority_Task(void* parameter)
    {   
      BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */
      while (1)
      {
        printf("HighPriority_Task 获取信号量\n");
        //获取二值信号量 xSemaphore,没获取到则一直等待
            xReturn = xSemaphoreTake(BinarySem_Handle,/* 二值信号量句柄 */
                                  portMAX_DELAY); /* 等待时间 */
        if(pdTRUE == xReturn)
          printf("HighPriority_Task Runing\n");
            LED1_TOGGLE;
        xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量
    //    if( xReturn == pdTRUE )
          //printf("HighPriority_Task 释放信号量!\r\n");
      
        vTaskDelay(500);
      }
    }
    ​
    ​
    /***********************************************************************
      * @ 函数名  : BSP_Init
      * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
      * @ 参数    :   
      * @ 返回值  : 无
      *********************************************************************/
    static void BSP_Init(void)
    {
        /*
         * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
         * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
         * 都统一用这个优先级分组,千万不要再分组,切忌。
         */
        NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
        
        /* LED 初始化 */
        LED_GPIO_Config();
    ​
        /* 串口初始化    */
        Debug_USART_Config();
      
      /* 按键初始化  */
      Key_GPIO_Config();
    ​
    }
    ​
    /********************************END OF FILE****************************/
    ​

6.8互斥量实验

  • 互斥量实验是基于优先级翻转实验进行修改的,目的是为了测试互斥量的优先级继承机制是否有效,具体见代码清单加粗部分。

  • /**************************** 任务句柄 ********************************/
    /* 
     * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
     * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
     * 这个句柄可以为NULL。
     */
    static TaskHandle_t AppTaskCreate_Handle = NULL;/* 创建任务句柄 */
    static TaskHandle_t LowPriority_Task_Handle = NULL;/* LowPriority_Task任务句柄 */
    static TaskHandle_t MidPriority_Task_Handle = NULL;/* MidPriority_Task任务句柄 */
    static TaskHandle_t HighPriority_Task_Handle = NULL;/* HighPriority_Task任务句柄 */
    /********************************** 内核对象句柄 *********************************/
    /*
     * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
     * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
     * 们就可以通过这个句柄操作这些内核对象。
     *
     * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
     * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
     * 来完成的
     * 
     */
    SemaphoreHandle_t MuxSem_Handle =NULL;
    
    /******************************* 全局变量声明 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些全局变量。
     */
    
    
    /******************************* 宏定义 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些宏定义。
     */
    
    
    /*
    *************************************************************************
    *                             函数声明
    *************************************************************************
    */
    static void AppTaskCreate(void);/* 用于创建任务 */
    
    static void LowPriority_Task(void* pvParameters);/* LowPriority_Task任务实现 */
    static void MidPriority_Task(void* pvParameters);/* MidPriority_Task任务实现 */
    static void HighPriority_Task(void* pvParameters);/* MidPriority_Task任务实现 */
    
    static void BSP_Init(void);/* 用于初始化板载相关资源 */
    
    /*****************************************************************
      * @brief  主函数
      * @param  无
      * @retval 无
      * @note   第一步:开发板硬件初始化 
                第二步:创建APP应用任务
                第三步:启动FreeRTOS,开始多任务调度
      ****************************************************************/
    int main(void)
    {	
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      /* 开发板硬件初始化 */
      BSP_Init();
    	printf("这是一个[野火]-STM32全系列开发板-FreeRTOS互斥量实验!\n");
       /* 创建AppTaskCreate任务 */
      xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                            (const char*    )"AppTaskCreate",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )1, /* 任务的优先级 */
                            (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
      /* 启动任务调度 */           
      if(pdPASS == xReturn)
        vTaskStartScheduler();   /* 启动任务,开启调度 */
      else
        return -1;  
      
      while(1);   /* 正常不会执行到这里 */    
    }
    
    
    /***********************************************************************
      * @ 函数名  : AppTaskCreate
      * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
      * @ 参数    : 无  
      * @ 返回值  : 无
      **********************************************************************/
    static void AppTaskCreate(void)
    {
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      taskENTER_CRITICAL();           //进入临界区
      
      /* 创建MuxSem */
      MuxSem_Handle = xSemaphoreCreateMutex();	 
      if(NULL != MuxSem_Handle)
        printf("MuxSem_Handle互斥量创建成功!\r\n");
    
      xReturn = xSemaphoreGive( MuxSem_Handle );//给出互斥量
    //  if( xReturn == pdTRUE )
    //    printf("释放信号量!\r\n");
        
      /* 创建LowPriority_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )LowPriority_Task, /* 任务入口函数 */
                            (const char*    )"LowPriority_Task",/* 任务名字 */
                            (uint16_t       )512,   /* 任务栈大小 */
                            (void*          )NULL,	/* 任务入口函数参数 */
                            (UBaseType_t    )2,	    /* 任务的优先级 */
                            (TaskHandle_t*  )&LowPriority_Task_Handle);/* 任务控制块指针 */
      if(pdPASS == xReturn)
        printf("创建LowPriority_Task任务成功!\r\n");
      
      /* 创建MidPriority_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )MidPriority_Task,  /* 任务入口函数 */
                            (const char*    )"MidPriority_Task",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )3, /* 任务的优先级 */
                            (TaskHandle_t*  )&MidPriority_Task_Handle);/* 任务控制块指针 */ 
      if(pdPASS == xReturn)
        printf("创建MidPriority_Task任务成功!\n");
      
      /* 创建HighPriority_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )HighPriority_Task,  /* 任务入口函数 */
                            (const char*    )"HighPriority_Task",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )4, /* 任务的优先级 */
                            (TaskHandle_t*  )&HighPriority_Task_Handle);/* 任务控制块指针 */ 
      if(pdPASS == xReturn)
        printf("创建HighPriority_Task任务成功!\n\n");
      
      vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
      
      taskEXIT_CRITICAL();            //退出临界区
    }
    
    
    
    /**********************************************************************
      * @ 函数名  : LowPriority_Task
      * @ 功能说明: LowPriority_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void LowPriority_Task(void* parameter)
    {	
      static uint32_t i;
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      while (1)
      {
        printf("LowPriority_Task 获取互斥量\n");
        //获取互斥量 MuxSem,没获取到则一直等待
    		xReturn = xSemaphoreTake(MuxSem_Handle,/* 互斥量句柄 */
                                  portMAX_DELAY); /* 等待时间 */
        if(pdTRUE == xReturn)
        printf("LowPriority_Task Runing\n\n");
        
        for(i=0;i<4000000;i++)//模拟低优先级任务占用互斥量
    		{
    			taskYIELD();//发起任务调度
    		}
        
        printf("LowPriority_Task 释放互斥量!\r\n");
        xReturn = xSemaphoreGive( MuxSem_Handle );//给出互斥量
          
    		LED1_TOGGLE;
        
        vTaskDelay(1000);
      }
    }
    
    /**********************************************************************
      * @ 函数名  : MidPriority_Task
      * @ 功能说明: MidPriority_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void MidPriority_Task(void* parameter)
    {	 
      while (1)
      {
       printf("MidPriority_Task Runing\n");
       vTaskDelay(1000);
      }
    }
    
    /**********************************************************************
      * @ 函数名  : HighPriority_Task
      * @ 功能说明: HighPriority_Task 任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void HighPriority_Task(void* parameter)
    {	
      BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */
      while (1)
      {
        printf("HighPriority_Task 获取互斥量\n");
        //获取互斥量 MuxSem,没获取到则一直等待
    		xReturn = xSemaphoreTake(MuxSem_Handle,/* 互斥量句柄 */
                                  portMAX_DELAY); /* 等待时间 */
        if(pdTRUE == xReturn)
          printf("HighPriority_Task Runing\n");
    		LED1_TOGGLE;
        
        printf("HighPriority_Task 释放互斥量!\r\n");
        xReturn = xSemaphoreGive( MuxSem_Handle );//给出互斥量
    
      
        vTaskDelay(1000);
      }
    }
    
    
    /***********************************************************************
      * @ 函数名  : BSP_Init
      * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
      * @ 参数    :   
      * @ 返回值  : 无
      *********************************************************************/
    static void BSP_Init(void)
    {
    	/*
    	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
    	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
    	 * 都统一用这个优先级分组,千万不要再分组,切忌。
    	 */
    	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
    	
    	/* LED 初始化 */
    	LED_GPIO_Config();
    
    	/* 串口初始化	*/
    	Debug_USART_Config();
      
      /* 按键初始化	*/
      Key_GPIO_Config();
    
    }
    
    /********************************END OF FILE****************************/
    

7.事件

7.1事件的基本概念

  • 事件是一种实现任务间通信的机制,主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输

  • 与信号量不同的是,它可以实现一对多,多对多的同步。

  • 即一个任务可以等待多个事件的发生:可以是任意一个事件发生时唤醒任务进行事件处理;也可以是几个事件都发生后才唤醒任务进行事件处理。同样,也可以是多个任务同步多个事件。每一个事件组只需要很少的 RAM 空间来保存事件组的状态。

  • 事件组存储在一个EventBits_t 类 型 的变量中 , 该变量在事件组结构体中定义

  • 如 果 宏configUSE_16_BIT_TICKS 定义为 1,那么变量 uxEventBits 就是 16 位的,其中有 8 个位用来存储事件组;而如果宏 configUSE_16_BIT_TICKS 定义为 0,那么变量 uxEventBits 就是32 位 的 , 其 中 有 24个位用来存储 事件组 。 在 STM32中, 我们一 般 将configUSE_16_BIT_TICKS 定义为 0,那么 uxEventBits 是 32 位的,有 24 个位用来实现事件标志组。每一位代表一个事件,任务通过“逻辑与”或“逻辑或”与一个或多个事件建立关联,形成一个事件组。

  • 事件的“逻辑或”也被称作是独立型同步**,指的是任务感兴趣的所有事件任一件发生即可被唤醒;

  • 事件“逻辑与”则被称为是关联型同步,指的是任务感兴趣的若干事件都发生时才被唤醒,并且事件发生的时间可以不同步。多任务环境下,任务、中断之间往往需要同步操作,一个事件发生会告知等待中的任务,即形成一个任务与任务、中断与任务间的同步。事件可以提供一对多、多对多的同步操作

  • 一对多同步模型:一个任务等待多个事件的触发,这种情况是比较常见的;

  • 多对多同步模型:多个任务等待多个事件的触发。

  • 任务可以通过设置事件位来实现事件的触发和等待操作。

  • FreeRTOS 的事件仅用于同步,不提供数据传输功能。

    • FreeRTOS 提供的事件具有如下特点:

    • 事件只与任务相关联,事件相互独立,一个 32 位的事件集合(EventBits_t 类型的变量,实际可用与表示事件的只有 24位),用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0 表示该事件类型未发生、1 表示该事件类型已经发生),一共 24 种事件类型。

    • 事件仅用于同步,不提供数据传输功能。

    • 事件无排队性,即多次向任务设置同一事件(如果任务还未来得及读走),等效于只设置一次。

    • 允许多个任务对同一事件进行读写操作。

    • 支持事件等待超时机制。

  • 在 FreeRTOS 事件中,每个事件获取的时候,用户可以选择感兴趣的事件,并且选择读取事件信息标记,它有三个属性,分别是逻辑与,逻辑或以及是否清除标记。当任务等待事件同步时,可以通过任务感兴趣的事件位和事件信息标记来判断当前接收的事件是否满足要求,如果满足则说明任务等待到对应的事件,系统将唤醒等待的任务;否则,任务会根据用户指定的阻塞超时时间继续等待下去。

7.2事件的应用场景

  • FreeRTOS 的事件用于事件类型的通讯,无数据传输,也就是说,我们可以用事件来做标志位,判断某些事件是否发生了,然后根据结果做处理,那很多人又会问了,为什么我不直接用变量做标志呢,岂不是更好更有效率?

  • 非也非也,若是在裸机编程中,用全局变量是最为有效的方法,这点我不否认,但是在操作系统中,使用全局变量就要考虑以下问题了:如何对全局变量进行保护呢,如何处理多任务同时对它进行访问?

    • 如何让内核对事件进行有效管理呢?使用全局变量的话,就需要在任务中轮询查看事件是否发送,这简直就是在浪费 CPU 资源啊,还有等待超时机制,使用全局变量的话需要用户自己去实现。

    • 所以,在操作系统中,还是使用操作系统给我们提供的通信机制就好了,简单方便还实用。

    • 在某些场合,可能需要多个时间发生了才能进行下一步操作,比如一些危险机器的启动,需要检查各项指标,当指标不达标的时候,无法启动,但是检查各个指标的时候,不能一下子检测完毕啊,所以,需要事件来做统一的等待,当所有的事件都完成了,那么机器才允许启动,这只是事件的其中一个应用。

    • 事件可使用于多种场合,它能够在一定程度上替代信号量,用于任务与任务间,中断与任务间的同步

    • 一个任务或中断服务例程发送一个事件给事件对象,而后等待的任务被唤醒并对相应的事件进行处理。

    • 但是它与信号量不同的是,事件的发送操作是不可累计的,而信号量的释放动作是可累计的。事件另外一个特性是,接收任务可等待多种事件,即多个事件对应一个任务或多个任务。

    • 同时按照任务等待的参数,可选择是“逻辑或”触发还是“逻辑与”触发。

    • 这个特性也是信号量等所不具备的,信号量只能识别单一同步动作,而不能同时等待多个事件的同步。

    • 各个事件可分别发送或一起发送给事件对象,而任务可以等待多个事件,任务仅对感兴趣的事件进行关注。当有它们感兴趣的事件发生时并且符合感兴趣的条件,任务将被唤醒并进行后续的处理动作。

7.3 事件运作机制

  • 接收事件时,可以根据感兴趣的参事件类型接收事件的单个或者多个事件类型

  • 事件接收成功后,必须使用 xClearOnExit 选项来清除已接收到的事件类型,否则不会清除已接收 到的事件 ,这样就需要用户显式清除事件位。

  • 用户可以自定义通过传入参数xWaitForAllBits 选择读取模式,是等待所有感兴趣的事件还是等待感兴趣的任意一个事件。

  • 设置事件时,对指定事件写入指定的事件类型,设置事件集合的对应事件位为 1,可以一次同时写多个事件类型,设置事件成功可能会触发任务调度。

  • 清除事件时,根据入参数事件句柄和待清除的事件类型,对事件对应位进行清 0 操作。

  • 事件不与任务相关联,事件相互独立,一个 32位的变量(事件集合,实际用于表示事件的只有 24 位),用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0 表示该事件类型未发生、1表示该事件类型已经发生),一共 24种事件类型具体见图20-1

 

0639a42965d357dee1d1ae1ab1cc761c.png

  • 事件唤醒机制,当任务因为等待某个或者多个事件发生而进入阻塞态,当事件发生的时候会被唤醒,其过程具体见图

  •  

    c15a23422a2dd10afc624eaa7013562d.png

  • 任务 1 对事件 3 或事件 5 感兴趣(逻辑或),当发生其中的某一个事件都会被唤醒,并且执行相应操作。而任务 2 对事件 3 与事件 5 感兴趣(逻辑与),当且仅当事件 3 与事件 5 都发生的时候,任务 2 才会被唤醒,如果只有一个其中一个事件发生,那么任务还是会继续等待事件发生。如果接在收事件函数中设置了清除事件位 xClearOnExit,那么当任务唤醒后将把事件 3 和事件 5 的事件标志清零,否则事件标志将依然存在。

7.4 事件控制块

  • 事件标志组存储在一个 EventBits_t 类型的变量中,该变量在事件组结构体中定义,具体见代码清单 20-1 加粗部分。

  • 如果宏 configUSE_16_BIT_TICKS 定义为 1,那么变量uxEventBits 就 是 16 位 的 , 其 中 有 8 个 位 用来存储 事 件 组 , 如 果 宏configUSE_16_BIT_TICKS 定义为 0,那么变量 uxEventBits 就是 32 位的,其中有 24 个位用来存储事件组,每一位代表一个事件的发生与否,利用逻辑或、逻辑与等实现不同事件的不同唤醒处理。

  • 在 STM32 中,uxEventBits 是 32 位的,所以我们有 24 个位用来实现事件组。除了事件标志组变量之外,FreeRTOS 还使用了一个链表来记录等待事件的任务,所有在等待此事件的任务均会被挂载在等待事件列表 xTasksWaitingForBits。

  • typedef struct xEventGroupDefinition {
        EventBits_t uxEventBits;
        List_t xTasksWaitingForBits;
    
        #if( configUSE_TRACE_FACILITY == 1 )
        UBaseType_t uxEventGroupNumber;
        #endif
    
        #if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) \
     && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
        uint8_t ucStaticallyAllocated;
        #endif
    } EventGroup_t;

7.5事件函数接口讲解

7.5.1 事件创建函数 xEventGroupCreate()

  • xEventGroupCreate()用于创建一个事件组,并返回对应的句柄。

  • 要想使用该函数必须在头文件 FreeRTOSConfig.h 定义宏 configSUPPORT_DYNAMIC_ALLOCATION 为 1(在FreeRTOS.h 中默认定义为 1)且需要把 FreeRTOS/source/event_groups.c 这个 C 文件添加到工程中。

  • 每一个事件组只需要很少的 RAM 空间来保存事件的发生状态。

  • 如果使用函数xEventGroupCreate()来创建一个事件,那么需要的 RAM 是动态分配的。

  • 如果使用函数xEventGroupCreateStatic()来创建一个事件,那么需要的 RAM 是静态分配的。

  • 我们暂时不讲解静态创建函数 xEventGroupCreateStatic()。

  • 事件创建函数,顾名思义,就是创建一个事件,与其他内核对象一样,都是需要先创建才能使用的资源,FreeRTOS 给我们提供了一个创建事件的函数 xEventGroupCreate(),当创建一个事件时,系统会首先给我们分配事件控制块的内存空间,然后对该事件控制块进行基本的初始化,创建成功返回事件句柄;创建失败返回 NULL。

7.5.2事件删除函数 vEventGroupDelete()

  • 在很多场合,某些事件只用一次的,就好比在事件应用场景说的危险机器的启动,假如各项指标都达到了,并且机器启动成功了,那这个事件之后可能就没用了,那就可以进行销毁了。想要删除事件怎么办?FreeRTOS 给我们提供了一个删除事件的函数——vEventGroupDelete(),使用它就能将事件进行删除了。当系统不再使用事件对象时,可以通过删除事件对象控制块来释放系统资源.

7.5.3事件组置位函数 xEventGroupSetBits()(任务)

  • xEventGroupSetBits()用于置位事件组中指定的位,当位被置位之后,阻塞在该位上的任务将会被解锁。

  • 使用该函数接口时,通过参数指定的事件标志来设定事件的标志位,然后遍历等待在事件对象上的事件等待列表,判断是否有任务的事件激活要求与当前事件对象标志值匹配,如果有,则唤醒该任务。简单来说,就是设置我们自己定义的事件标志位为 1,并且看看有没有任务在等待这个事件,有的话就唤醒它。

7.5.4 事件组置位函数 xEventGroupSetBitsFromISR()(中断)

  • xEventGroupSetBitsFromISR()是 xEventGroupSetBits()的中断版本,用于置位事件组中指定的位。

  • 置位事件组中的标志位是一个不确定的操作,因为阻塞在事件组的标志位上的任务的个数是不确定的。F

  • reeRTOS 是不允许不确定的操作在中断和临界段中发生的,所以xEventGroupSetBitsFromISR()给 FreeRTOS 的守护任务发送一个消息,让置位事件组的操作在守护任务里面完成,守护任务是基于调度锁而非临界段的机制来实现的。

  • 需要注意的地方:正如上文提到的那样,在中断中事件标志的置位是在守护任务(也叫软件定时器服务任务)中完成的。

  • 因此 FreeRTOS 的守护任务与其他任务一样,都是系统调度器根据其优先级进行任务调度的,但守护任务的优先级必须比任何任务的优先级都要高,保证在需要的时候能立即切换任务从而达到快速处理的目的,因为这是在中断中让事 件 标 志 位 置 位 , 其 优 先 级 由 FreeRTOSConfig.h 中 的 宏configTIMER_TASK_PRIORITY 来定义。

  • 其实 EventGroupSetBitsFromISR()函数真正调用的也是 xEventGroupSetBits()函数,只不过是在守护任务中进行调用的,所以它实际上执行的上下文环境依旧是在任务中。

  • 要想使用该函数,必须把 configUSE_TIMERS 和INCLUDE_xTimerPendFunctionCall 这些宏在 FreeRTOSConfig.h 中都定义为 1,并且把FreeRTOS/source/event_groups.c 这个 C文件添加到工程中编译。

7.5.5等待事件函数 xEventGroupWaitBits()

  • 既然标记了事件的发生,那么我怎么知道他到底有没有发生,这也是需要一个函数来获 取 事 件 是 否 已 经 发 生 , FreeRTOS 提 供 了 一 个 等 待 指 定 事 件 的 函 数 xEventGroupWaitBits(),通过这个函数,任务可以知道事件标志组中的哪些位,有什么事件发生了,然后通过 “逻辑与”、“逻辑或”等操作对感兴趣的事件进行获取,并且这个函数实现了等待超时机制,当且仅当任务等待的事件发生时,任务才能获取到事件信息。

  • 在这段时间中,如果事件一直没发生,该任务将保持阻塞状态以等待事件发生。

  • 当其它任务或中断服务程序往其等待的事件设置对应的标志位,该任务将自动由阻塞态转为就绪态。

  • 当任务等待的时间超过了指定的阻塞时间,即使事件还未发生,任务也会自动从阻塞态转移为就绪态。

  • 这样子很有效的体现了操作系统的实时性,如果事件正确获取(等待到)则返回对应的事件标志位,由用户判断再做处理,因为在事件超时的时候也会返回一个不能确定的事件值,所以需要判断任务所等待的事件是否真的发生。

  • EventGroupWaitBits()用于获取事件组中的一个或多个事件发生标志,当要读取的事件标志位没有 被置位时任务将进入阻塞等待状态 。 要想使用该函数必须把FreeRTOS/source/event_groups.c 这个 C 文件添加到工程中。

  • 下面简单分析处理过程:当用户调用这个函数接口时,系统首先根据用户指定参数和接收选项来判断它要等待的事件是否发生,如果已经发生,则根据参数 xClearOnExit 来决定是否清除事件的相应标志位,并且返回事件标志位的值,但是这个值并不是一个稳定的值,所以在等待到对应事件的时候,还需我们判断事件是否与任务需要的一致;

  • 如果事件没有发生,则把任务添加到事件等待列表中,任务感兴趣的事件标志值和等待选项填用列表项的值来表示,直到事件发生或等待时间超时,事件等待函数 xEventGroupWaitBits()具体用法见代码清单。

  • static void LED_Task(void* parameter)
    {
        EventBits_t r_event; /* 定义一个事件接收变量 */
        /* 任务都是一个无限循环,不能返回 */
        while (1) {
            /****************************************************************
     * 等待接收事件标志
     *
     * 如果 xClearOnExit 设置为 pdTRUE,那么在 xEventGroupWaitBits()返回之前,
     * 如果满足等待条件(如果函数返回的原因不是超时),那么在事件组中设置
     * 的 uxBitsToWaitFor 中的任何位都将被清除。
     * 如果 xClearOnExit 设置为 pdFALSE,
     * 则在调用 xEventGroupWaitBits()时,不会更改事件组中设置的位。
     *
     * xWaitForAllBits 如果 xWaitForAllBits 设置为 pdTRUE,则当 uxBitsToWaitFor 中
     * 的所有位都设置或指定的块时间到期时,xEventGroupWaitBits()才返回。
     * 如果 xWaitForAllBits 设置为 pdFALSE,则当设置 uxBitsToWaitFor 中设置的任何
     * 一个位置 1 或指定的块时间到期时,xEventGroupWaitBits()都会返回。
     * 阻塞时间由 xTicksToWait 参数指定。
     *********************************************************/
            r_event = xEventGroupWaitBits(Event_Handle, /* 事件对象句柄 */ 
                                          KEY1_EVENT|KEY2_EVENT,/* 接收任务感兴趣的事件 */ 
                                          pdTRUE, /* 退出时清除事件位 */ 
                                          pdTRUE, /* 满足感兴趣的所有事件 */ 
                                          portMAX_DELAY);/* 指定超时事件,一直等 */ 
    
            if ((r_event & (KEY1_EVENT|KEY2_EVENT)) == (KEY1_EVENT|KEY2_EVENT)) { 
                /* 如果接收完成并且正确 */ 
                printf ( "KEY1 与 KEY2 都按下\n");
                LED1_TOGGLE; //LED1 反转
            } else
                printf ( "事件错误!\n");
        }
    }

7.5.6 xEventGroupClearBits与 xEventGroupClearBitsFromISR

  • xEventGroupClearBits()与 xEventGroupClearBitsFromISR()都是用于清除事件组指定的位,如果在获取事件的时候没有将对应的标志位清除,那么就需要用这个函数来进行显式清除,xEventGroupClearBits()函数不能在中断中使用,而是由具有中断保护功能 的xEventGroupClearBitsFromISR() 来代替,中断清除事件标志位的操作在守护任务(也叫定时 器 服 务 任 务 ) 里 面 完 成 。

  • 守 护 进 程 的 优 先 级 由 FreeRTOSConfig.h 中 的 宏configTIMER_TASK_PRIORITY 来定义 。 要 想 使 用 该 函 数 必 须 把FreeRTOS/source/event_groups.c 这个 C 文件添加到工程中。

7.6事件实验

  • 事件标志组实验是在 FreeRTOS 中创建了两个任务,一个是设置事件任务,一个是等待事件任务,两个任务独立运行,设置事件任务通过检测按键的按下情况设置不同的事件标志位,等待事件任务则获取这两个事件标志位,并且判断两个事件是否都发生,如果是则输出相应信息,LED 进行翻转。等待事件任务的等待时间是 portMAX_DELAY,一直在等待事件的发生,等待到事件之后清除对应的事件标记位,具体见代码清单加粗部分。

  • /*
    *************************************************************************
    *                             包含的头文件
    *************************************************************************
    */ 
    /* FreeRTOS头文件 */
    #include "FreeRTOS.h"
    #include "task.h"
    #include "event_groups.h"
    /* 开发板硬件bsp头文件 */
    #include "bsp_led.h"
    #include "bsp_debug_usart.h"
    #include "bsp_key.h"
    /**************************** 任务句柄 ********************************/
    /* 
     * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
     * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
     * 这个句柄可以为NULL。
     */
    static TaskHandle_t AppTaskCreate_Handle = NULL;/* 创建任务句柄 */
    static TaskHandle_t LED_Task_Handle = NULL;/* LED_Task任务句柄 */
    static TaskHandle_t KEY_Task_Handle = NULL;/* KEY_Task任务句柄 */
    
    /********************************** 内核对象句柄 *********************************/
    /*
     * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
     * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
     * 们就可以通过这个句柄操作这些内核对象。
     *
     * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
     * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
     * 来完成的
     * 
     */
    static EventGroupHandle_t Event_Handle =NULL;
    
    /******************************* 全局变量声明 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些全局变量。
     */
    
    
    /******************************* 宏定义 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些宏定义。
     */
    #define KEY1_EVENT  (0x01 << 0)//设置事件掩码的位0
    #define KEY2_EVENT  (0x01 << 1)//设置事件掩码的位1
    
    /*
    *************************************************************************
    *                             函数声明
    *************************************************************************
    */
    static void AppTaskCreate(void);/* 用于创建任务 */
    
    static void LED_Task(void* pvParameters);/* LED_Task 任务实现 */
    static void KEY_Task(void* pvParameters);/* KEY_Task 任务实现 */
    
    static void BSP_Init(void);/* 用于初始化板载相关资源 */
    
    /*****************************************************************
      * @brief  主函数
      * @param  无
      * @retval 无
      * @note   第一步:开发板硬件初始化 
                第二步:创建APP应用任务
                第三步:启动FreeRTOS,开始多任务调度
      ****************************************************************/
    int main(void)
    {	
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      /* 开发板硬件初始化 */
      BSP_Init();
    	printf("这是一个[野火]-STM32全系列开发板-FreeRTOS事件标志组实验!\n");
       /* 创建AppTaskCreate任务 */
      xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                            (const char*    )"AppTaskCreate",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )1, /* 任务的优先级 */
                            (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
      /* 启动任务调度 */           
      if(pdPASS == xReturn)
        vTaskStartScheduler();   /* 启动任务,开启调度 */
      else
        return -1;  
      
      while(1);   /* 正常不会执行到这里 */    
    }
    
    
    /***********************************************************************
      * @ 函数名  : AppTaskCreate
      * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
      * @ 参数    : 无  
      * @ 返回值  : 无
      **********************************************************************/
    static void AppTaskCreate(void)
    {
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      taskENTER_CRITICAL();           //进入临界区
      
      /* 创建 Event_Handle */
      Event_Handle = xEventGroupCreate();	 
      if(NULL != Event_Handle)
        printf("Event_Handle 事件创建成功!\r\n");
        
      /* 创建LED_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )LED_Task, /* 任务入口函数 */
                            (const char*    )"LED_Task",/* 任务名字 */
                            (uint16_t       )512,   /* 任务栈大小 */
                            (void*          )NULL,	/* 任务入口函数参数 */
                            (UBaseType_t    )2,	    /* 任务的优先级 */
                            (TaskHandle_t*  )&LED_Task_Handle);/* 任务控制块指针 */
      if(pdPASS == xReturn)
        printf("创建LED_Task任务成功!\r\n");
      
      /* 创建KEY_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )KEY_Task,  /* 任务入口函数 */
                            (const char*    )"KEY_Task",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )3, /* 任务的优先级 */
                            (TaskHandle_t*  )&KEY_Task_Handle);/* 任务控制块指针 */ 
      if(pdPASS == xReturn)
        printf("创建KEY_Task任务成功!\n");
      
      vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
      
      taskEXIT_CRITICAL();            //退出临界区
    }
    
    
    
    /**********************************************************************
      * @ 函数名  : LED_Task
      * @ 功能说明: LED_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void LED_Task(void* parameter)
    {	
      EventBits_t r_event;  /* 定义一个事件接收变量 */
      /* 任务都是一个无限循环,不能返回 */
      while (1)
    	{
        /*******************************************************************
         * 等待接收事件标志 
         * 
         * 如果xClearOnExit设置为pdTRUE,那么在xEventGroupWaitBits()返回之前,
         * 如果满足等待条件(如果函数返回的原因不是超时),那么在事件组中设置
         * 的uxBitsToWaitFor中的任何位都将被清除。 
         * 如果xClearOnExit设置为pdFALSE,
         * 则在调用xEventGroupWaitBits()时,不会更改事件组中设置的位。
         *
         * xWaitForAllBits如果xWaitForAllBits设置为pdTRUE,则当uxBitsToWaitFor中
         * 的所有位都设置或指定的块时间到期时,xEventGroupWaitBits()才返回。 
         * 如果xWaitForAllBits设置为pdFALSE,则当设置uxBitsToWaitFor中设置的任何
         * 一个位置1 或指定的块时间到期时,xEventGroupWaitBits()都会返回。 
         * 阻塞时间由xTicksToWait参数指定。          
          *********************************************************/
        r_event = xEventGroupWaitBits(Event_Handle,  /* 事件对象句柄 */
                                      KEY1_EVENT|KEY2_EVENT,/* 接收线程感兴趣的事件 */
                                      pdTRUE,   /* 退出时清除事件位 */
                                      pdTRUE,   /* 满足感兴趣的所有事件 */
                                      portMAX_DELAY);/* 指定超时事件,一直等 */
                            
        if((r_event & (KEY1_EVENT|KEY2_EVENT)) == (KEY1_EVENT|KEY2_EVENT)) 
        {
          /* 如果接收完成并且正确 */
          printf ( "KEY1与KEY2都按下\n");		
          LED1_TOGGLE;       //LED1	反转
        }
        else
          printf ( "事件错误!\n");	
      }
    }
    
    /**********************************************************************
      * @ 函数名  : KEY_Task
      * @ 功能说明: KEY_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void KEY_Task(void* parameter)
    {	 
        /* 任务都是一个无限循环,不能返回 */
      while (1)
      {
        if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )       //如果KEY2被单击
    		{
          printf ( "KEY1被按下\n" );
    			/* 触发一个事件1 */
    			xEventGroupSetBits(Event_Handle,KEY1_EVENT);  					
    		}
        
    		if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )       //如果KEY2被单击
    		{
          printf ( "KEY2被按下\n" );	
    			/* 触发一个事件2 */
    			xEventGroupSetBits(Event_Handle,KEY2_EVENT); 				
    		}
    		vTaskDelay(20);     //每20ms扫描一次		
      }
    }
    
    /***********************************************************************
      * @ 函数名  : BSP_Init
      * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
      * @ 参数    :   
      * @ 返回值  : 无
      *********************************************************************/
    static void BSP_Init(void)
    {
    	/*
    	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
    	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
    	 * 都统一用这个优先级分组,千万不要再分组,切忌。
    	 */
    	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
    	
    	/* LED 初始化 */
    	LED_GPIO_Config();
    
    	/* 串口初始化	*/
    	Debug_USART_Config();
      
      /* 按键初始化	*/
      Key_GPIO_Config();
    
    }
    
    /********************************END OF FILE****************************/
    

8.软件定时器

8.1 软件定时器的基本概念

  • 定时器,是指从指定的时刻开始,经过一个指定时间,然后触发一个超时事件,用户可以自定义定时器的周期与频率。

  • 类似生活中的闹钟,我们可以设置闹钟每天什么时候响,还能设置响的次数,是响一次还是每天都响。

  • 定时器有硬件定时器和软件定时器之分:硬件定时器是芯片本身提供的定时功能。

  • 一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断。

  • 硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式。

  • 软件定时器,软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受硬件定时器资源限制的定时器服务,它实现的功能与硬件定时器也是类似的。

  • 使用硬件定时器时,每次在定时时间到达之后就会自动触发一个中断,用户在中断中处理信息;

  • 而使用软件定时器时,需要我们在创建软件定时器时指定时间到达后要调用的函数(也称超时函数/回调函数,为了统一,下文均用回调函数描述),在回调函数中处理信息。

  • 注意:软件定时器回调函数的上下文是任务,下文所说的定时器均为软件定时器。

  • 软件定时器在被创建之后,当经过设定的时钟计数值后会触发用户定义的回调函数。

  • 定时精度与系统时钟的周期有关。一般系统利用 SysTick 作为软件定时器的基础时钟,软件定时器的回调函数类似硬件的中断服务函数,所以,回调函数也要快进快出,而且回调函数中不能有任何阻塞任务运行的情况(软件定时器回调函数的上下文环境是任务),比如 vTaskDelay()以及其它能阻 塞任务运行的函数 ,两次触发回调函数的时间间隔xTimerPeriodInTicks 叫定时器的定时周期。

  • FreeRTOS 操作系统提供软件定时器功能,软件定时器的使用相当于扩展了定时器的数量,允许创建更多的定时业务。

  • FreeRTOS 软件定时器功能上支持:

    • 裁剪:能通过宏关闭软件定时器功能。

    • 软件定时器创建。

    • 软件定时器启动。

    • 软件定时器停止。

    • 软件定时器复位。

    • 软件定时器删除。

  • FreeRTOS 提供的软件定时器支持单次模式和周期模式,单次模式和周期模式的定时时间到之后都会调用软件定时器的回调函数,用户可以在回调函数中加入要执行的工程代码。

  • 单次模式:当用户创建了定时器并启动了定时器后,定时时间到了,只执行一次回调函数之后就将该定时器进入休眠状态,不再重新执行。

  • 周期模式:这个定时器会按照设置的定时时间循环执行回调函数,直到用户将定时器删除。

  •  

    91d99a0b71d7495e807facca366c5ec2.png

  • FreeRTOS 通过一个 prvTimerTask 任务(也叫守护任务 Daemon)管理软定时器,它是在启动调度器时自动创建的,为了满足用户定时需求。

  • prvTimerTask 任务会在其执行期间检查用户启动的时间周期溢出的定时器,并调用其回调函数。

  • 只有设置 FreeRTOSConfig.h 中的宏定义 configUSE_TIMERS 设置为 1 ,将相关代码编译进来,才能正常使用软件定时器相关功能。

8.2 软件定时器应用场景

  • 在很多应用中,我们需要一些定时器任务,硬件定时器受硬件的限制,数量上不足以满足用户的实际需求,无法提供更多的定时器,那么可以采用软件定时器来完成,由软件定时器代替硬件定时器任务。

  • 但需要注意的是软件定时器的精度是无法和硬件定时器相比的,而且在软件定时器的定时过程中是极有可能被其它中断所打断,因为软件定时器的执行上下文环境是任务。所以,软件定时器更适用于对时间精度要求不高的任务,一些辅助型的任务。

8.3 软件定时器的精度

  • 在操作系统中,通常软件定时器以系统节拍周期为计时单位。系统节拍是系统的心跳节拍,表示系统时钟的频率,就类似人的心跳,1s 能跳动多少下,系统节拍配置为configTICK_RATE_HZ,该宏在 FreeRTOSConfig.h 中有定义,默认是 1000。那么系统的时钟节拍周期就为 1ms(1s跳动 1000 下,每一下就为 1ms)。

  • 软件定时器的所定时数值必须是这个节拍周期的整数倍,例如节拍周期是 10ms,那么上层软件定时器定时数值只能是10ms,20ms,100ms 等,而不能取值为 15ms。由于节拍定义了系统中定时器能够分辨的精确度,系统可以根据实际系统 CPU 的处理能力和实时性需求设置合适的数值,系统节拍周期的值越小,精度越高,但是系统开销也将越大,因为这代表在 1 秒中系统进入时钟中断的次数也就越多。

8.4软件定时器的运作机制

  • 软件定时器是可选的系统资源,在创建定时器的时候会分配一块内存空间。当用户创建并启动一个软件定时器时, FreeRTOS 会根据当前系统时间及用户设置的定时确定该定时器唤醒时间,并将该定时器控制块挂入软件定时器列表,FreeRTOS 中采用两个定时器列表维护软件定时器,pxCurrentTimerList 与 pxOverflowTimerList 是列表指针,在初始化的时候分别指向 xActiveTimerList1 与 xActiveTimerList2,具体见代码清单。

  • PRIVILEGED_DATA static List_t xActiveTimerList1;
    PRIVILEGED_DATA static List_t xActiveTimerList2;
    PRIVILEGED_DATA static List_t *pxCurrentTimerList;
    PRIVILEGED_DATA static List_t *pxOverflowTimerList;
  • pxCurrentTimerList:系统新创建并激活的定时器都会以超时时间升序的方式插入到pxCurrentTimerList 列表中。

  • 系统在定时器任务中扫描 pxCurrentTimerList 中的第一个定时器,看是否已超时,若已经超时了则调用软件定时器回调函数。

  • 否则将定时器任务挂起,因为定时时间是升序插入软件定时器列表的,列表中第一个定时器的定时时间都还没到的话,那后面的定时器定时时间自然没到。

  • pxOverflowTimerList 列表是在软件定时器溢出的时候使用,作用与 xCurrentTimerList一致。

  • 同时,FreeRTOS 的软件定时器还有采用消息队列进行通信,利用“定时器命令队列”向软件定时器任务发送一些命令,任务在接收到命令就会去处理命令对应的程序,比如启动定时器,停止定时器等。假如定时器任务处于阻塞状态,我们又需要马上再添加一个软件定时器的话,就是采用这种消息队列命令的方式进行添加,才能唤醒处于等待状态的定时器任务,并且在任务中将新添加的软件定时器添加到软件定时器列表中,所以,在定时器启动函数中,FreeRTOS 是采用队列的方式发送一个消息给软件定时器任务,任务被唤醒从而执行接收到的命令。

  • 例如:系统当前时间 xTimeNow 值为 0,注意:xTimeNow 其实是一个局部变量,是根据 xTaskGetTickCount()函数获取的,实际它的值就是全局变量 xTickCount 的值,下文都采用它表示当前系统时间。

  • 在当前系统中已经创建并启动了 1 个定时器 Timer1;系统继续运行,当系统的时间 xTimeNow 为 20 的时候,用户创建并且启动一个定时时间为 100 的定时器 Timer2,此 时 Timer2 的溢出时间 xTicksToWait 就 为定 时时间 +系统 当前时间(100+20=120),然后将 Timer2 按 xTicksToWait 升序插入软件定时器列表中;假设当前系统时间 xTimeNow 为 40 的时候,用户创建并且启动了一个定时时间为 50 的定时器Timer3 , 那 么 此 时 Timer3 的 溢 出 时 间 xTicksToWait 就 为 40+50=90 , 同 样 安 装xTicksToWait 的数值升序插入软件定时器列表中,在定时器链表中插入过程具体见图 21-2。同理创建并且启动在已有的两个定时器中间的定时器也是一样的,具体见图

  •  

    ec51799ca6d4f608be27baa224178ede.png

  •  

    393e9eb4fccaab5349ad2c43f54b9140.png

  • 那么系统如何处理软件定时器列表?系统在不断运行,而 xTimeNow(xTickCount)随着 SysTick 的触发一直在增长(每一次硬件定时器中断来临,xTimeNow 变量会加 1),在软件定时器任务运行的时候会获取下一个要唤醒的定时器,比较当前系统时间xTimeNow 是否大于或等于下一个定时器唤醒时间 xTicksToWait,若大于则表示已经超时,定时器任务将会调用对应定时器的回调函数,否则将软件定时器任务挂起,直至下一个要唤醒的软件定时器时间到来或者接收到命令消息。

  • 以图 21-3 为例,讲解软件定时器调用回调函数的过程,在创建定 Timer1 并且启动后,假如系统经过了 50 个 tick, xTimeNow 从 0增长到 50,与 Timer1 的 xTicksToWait 值相等, 这时会触发与 Timer1 对应的回调函数,从而转到回调函数中执行用户代码,同时将 Timer1 从软件定时器列表删除,如果软件定时器是周期性的,那么系统会根据 Timer1 下一次唤醒时间重新将 Timer1 添加到软件定时器列表中,按照 xTicksToWait 的升序进行排列。同理,在 xTimeNow=40 的时候创建的 Timer3,在经过 130 个 tick 后(此时系统时间 xTimeNow 是 40,130 个 tick 就是系统时间xTimeNow 为 170 的时候),与 Timer3 定时器对应的回调函数会被触发,接着将 Timer3 从软件定时器列表中删除,如果是周期性的定时器,还会按照 xTicksToWait 升序重新添加到软件定时器列表中。

  • 使用软件定时器时候要注意以下几点:

    • 软件定时器的回调函数中应快进快出,绝对不允许使用任何可能引软件定时器起任务挂起或者阻塞的 API 接口,在回调函数中也绝对不允许出现死循环。

    • 软件定时器使用了系统的一个队列和一个任务资源,软件定时器任务的优先级默认为 configTIMER_TASK_PRIORITY,为了更好响应,该优先级应设置为所有任务中最高的优先级。

    • 创建单次软件定时器,该定时器超时执行完回调函数后,系统会自动删除该软件定时器,并回收资源。

    • 定时器任务的堆栈大小默认为 configTIMER_TASK_STACK_DEPTH 个字节

8.5软件定时器控制块

  • 软件定时器虽然不属于内核资源,但是也是 FreeRTOS 核心组成部分,是一个可以裁的功能模块,同样在系统中由一个控制块管理其相关信息,软件定时器的控制块中包含没 用 过 创 建 的 软 件 定 时 器 基 本 信 息 , 在 使 用 定 时 器 前 我 们 需 要 通 过xTimerCreate()/xTimerCreateStatic()函数创建一个软件定时器,在函数中,FreeRTOS 将向系统管理的内存申请一块软件定时器控制块大小的内存用于保存定时器的信息,下面来看看软件定时器控制块的成员变量

  • typedef struct tmrTimerControl {
        const char *pcTimerName; (1)
        ListItem_t xTimerListItem; (2)
        TickType_t xTimerPeriodInTicks; (3)
        UBaseType_t uxAutoReload; (4)
        void *pvTimerID; (5)
        TimerCallbackFunction_t pxCallbackFunction; (6)
        #if( configUSE_TRACE_FACILITY == 1 )
        UBaseType_t uxTimerNumber;
        #endif
    
        #if( ( configSUPPORT_STATIC_ALLOCATION == 1 )\
     && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
        uint8_t ucStaticallyAllocated; (7)
        #endif
    } xTIMER;
    typedef xTIMER Timer_t;
  • 代码清单 (1):软件定时器名字,这个名字一般用于调试的,RTOS 使用定时器是通过其句柄,并不是使用其名字。

  • 代码清单 (2)**:软件定时器列表项,用于插入定时器列表。

  • 代 码清单 (3)** :软件 定时器的周期,单位为系统节拍周期(即 tick) ,pdMS_TO_TICKS()可以把时间单位从 ms转换为系统节拍周期。

  • 代码清单 (4):软件定时器是否自动重置,如果该值为 pdFalse,那么创建的软件定时器工作模式是单次模式,否则为周期模式。

  • 代码清单 (5)**:软件定时器 ID,数字形式。该 ID 典型的用法是当一个回调函数分配给一个或者多个软件定时器时,在回调函数里面根据 ID 号来处理不同的软件定时器。

  • 代码清单 (6)**:软件定时器的回调函数,当定时时间到达的时候就会调用这个函数。

  • 代码清单 (7)**:标记定时器使用的内存,删除时判断是否需要释放内存。

8.6 软件定时器函数接口讲解

  • 软件定时器的功能是在定时器任务(或者叫定时器守护任务)中实现的。

  • 软件定时器的很多 API 函数通过一个名字叫“定时器命令队列”的队列来给定时器守护任务发送命令。该定时器命令队列由 RTOS 内核提供,且应用程序不能够直接访问,其消息队列的长度由宏 configTIMER_QUEUE_LENGTH 定义,下面就讲解一些常用的软件定时器函数接口。

8.6.1 软件定时器创建函数 xTimerCreate()

  • 软件定时器与 FreeRTOS 内核其他资源一样,需要创建才允许使用的,FreeRTOS 为我们提供了两种创建方式,一种是动态创建软件定时器 xTimerCreate(),另一种是静态创建方式 xTimerCreateStatic(),因为创建过程基本差不多,所以在这里我们只讲解动态创建方式。

  • xTimerCreate()用于创建一个软件定时器,并返回一个句柄。要想使用该函数函数必须在头 文件 FreeRTOSConfig.h中把宏 configUSE_TIMERS和configSUPPORT_DYNAMIC_ALLOCATION 均 定义为 1(configSUPPORT_DYNAMIC_ALLOCATION 在 FreeRTOS.h 中默认定义为 1),并且需要把 FreeRTOS/source/times.c 这个 C 文件添加到工程中。

  • 每一个软件定时器只需要很少的 RAM 空间来保存其的状态。如果使用函数xTimeCreate()来创建一个软件定时器,那么需要的 RAM 是动态分配的。

  • 如果使用函数xTimeCreateStatic()来创建一个事件组,那么需要的 RAM 是静态分配的软件定时器在创建成功后是处于休眠状态的,可以使用 xTimerStart()、xTimerReset()、xTimerStartFromISR() 、 xTimerResetFromISR() 、 xTimerChangePeriod() 和xTimerChangePeriodFromISR()这些函数将其状态转换为活跃态。xTimerCreate()函数源码具体见代码清单。

  • static TimerHandle_t Swtmr1_Handle =NULL; /* 软件定时器句柄 */
    static TimerHandle_t Swtmr2_Handle =NULL; /* 软件定时器句柄 */
    /* 周期模式的软件定时器 1,定时器周期 1000(tick)*/
    Swtmr1_Handle=xTimerCreate((const char*)"AutoReloadTimer", 
                               (TickType_t)1000,/* 定时器周期 1000(tick) */ 
                               (UBaseType_t)pdTRUE,/* 周期模式 */ 
                               (void* )1,/* 为每个计时器分配一个索引的唯一 ID */ 
                               (TimerCallbackFunction_t)Swtmr1_Callback); /* 回调函数 */ 
    if (Swtmr1_Handle != NULL)
    {
        /********************************************************************
     * xTicksToWait:如果在调用 xTimerStart()时队列已满,则以 tick 为单位指定调用任务应保持
     * 在 Blocked(阻塞)状态以等待 start 命令成功发送到 timer 命令队列的时间。
     * 如果在启动调度程序之前调用 xTimerStart(),则忽略 xTicksToWait。在这里设置等待时间为 0.
     **********************************************************************/
        xTimerStart(Swtmr1_Handle,0); //开启周期定时器
    }
    
    /* 单次模式的软件定时器 2,定时器周期 5000(tick)*/
    Swtmr2_Handle=xTimerCreate((const char* )"OneShotTimer", 
                               (TickType_t)5000,/* 定时器周期 5000(tick) */ 
                               (UBaseType_t )pdFALSE,/* 单次模式 */ 
                               (void*)2,/* 为每个计时器分配一个索引的唯一 ID */ 
                               (TimerCallbackFunction_t)Swtmr2_Callback); 
    if (Swtmr2_Handle != NULL)
    {
        xTimerStart(Swtmr2_Handle,0); //开启单次定时器
    }
    
    static void Swtmr1_Callback(void* parameter) 
    {
        /* 软件定时器的回调函数,用户自己实现 */
    }
    
    static void Swtmr2_Callback(void* parameter) 
    {
        /* 软件定时器的回调函数,用户自己实现 */
    }

8.6.2xTimerStart()

  • 如果是认真看上面 xTimerCreate()函数使用实例的同学应该就发现了,这个软件定时器启动函数 xTimerStart()在上面的实例中有用到过,前一小节已经说明了,软件定时器在创建完成的时候是处于休眠状态的,需要用 FreeRTOS 的相关函数将软件定时器活动起来,而 xTimerStart()函数就是可以让处于休眠的定时器开始工作。我们知道,在系统开始运行的时候,系统会帮我们自动创建一个软件定时器任务(prvTimerTask),在这个任务中,如果暂时没有运行中的定时器,任务会进入阻塞态等待命令,而我们的启动函数就是通过“定时器命令队列”向定时器任务发送一个启动命令,定时器任务获得命令就解除阻塞,然后执行启动软件定时器命令。

8.6.3xTimerStartFromISR()

  • 当 然 除 在任务启动软件定时器之外 ,还有在中断中启动软件定时器的函数xTimerStartFromISR()。

  • xTimerStartFromISR()是函数 xTimerStart()的中断版本,用于启动一个先前由函数 xTimerCreate() / xTimerCreateStatic()创建的软件定时器。

  • /* 这个方案假定软件定时器 xBacklightTimer 已经创建,
     定时周期为 5s,执行次数为一次,即定时时间到了之后
     就进入休眠态。
    程序说明:当按键按下,打开液晶背光,启动软件定时器,
     5s 时间到,关掉液晶背光*/
    
    /* 软件定时器回调函数 */
    void vBacklightTimerCallback( TimerHandle_t pxTimer )
    {
        /* 关掉液晶背光 */
        vSetBacklightState( BACKLIGHT_OFF );
    }
    
    
    /* 按键中断服务程序 */
    void vKeyPressEventInterruptHandler( void )
    {
        BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
        /* 确保液晶背光已经打开 */
        vSetBacklightState( BACKLIGHT_ON );
    
        /* 启动软件定时器 */
        if ( xTimerStartFromISR( xBacklightTimer, 
                                &xHigherPriorityTaskWoken ) != pdPASS ) { 
            /* 软件定时器开启命令没有成功执行 */ 
        } 
    
        /* ...执行其他的按键相关的功能代码 */
    
        if ( xHigherPriorityTaskWoken != pdFALSE ) { 
            /* 执行上下文切换 */ 
        }
    }

8.6.4软件定时器停止函数 xTimerStop()

  • xTimerStop() 用于停止一个已经启动的软件定时器,该函数的实现也是通过“定时器命令队列”发送一个停止命令给软件定时器任务,从而唤醒软件定时器任务去将定时器停止。要想使函数 xTimerStop()必须在头文件 FreeRTOSConfig.h 中把宏 configUSE_TIMERS定义为 1。

  • static TimerHandle_t Swtmr1_Handle =NULL; /* 软件定时器句柄 */
    
    /* 周期模式的软件定时器 1,定时器周期 1000(tick)*/
    Swtmr1_Handle=xTimerCreate((const char* )"AutoReloadTimer",
                               (TickType_t )1000,/* 定时器周期 1000(tick) */
                               (UBaseType_t )pdTRUE,/* 周期模式 */
                               (void*)1,/* 为每个计时器分配一个索引的唯一 ID */
                               (TimerCallbackFunction_t)Swtmr1_Callback); /* 回调函数 */
    if (Swtmr1_Handle != NULL)
    {
        /********************************************************************
     * xTicksToWait:如果在调用 xTimerStart()时队列已满,则以 tick 为单位指定调用任务应保持
     * 在 Blocked(阻塞)状态以等待 start 命令成功发送到 timer 命令队列的时间。
     * 如果在启动调度程序之前调用 xTimerStart(),则忽略 xTicksToWait。在这里设置等待时间为 0.
     *******************************************************************/
        xTimerStart(Swtmr1_Handle,0); //开启周期定时器 
    }
    
    static void test_task(void* parameter)
    {
        while (1) {
            /* 用户自己实现任务代码 */
            xTimerStop(Swtmr1_Handle,0); //停止定时器 
        }
    
    }

8.6.5xTimerStopFromISR()

  • xTimerStopFromISR()是函数 xTimerStop()的中断版本,用于停止一个正在运行的软件定时器,让其进入休眠态,实现过程也是通过“定时器命令队列”向软件定时器任务发送停止命令。

  • /* 这个方案假定软件定时器 xTimer 已经创建且启动。
     当中断发生时,停止软件定时器 */
    ​
    /* 停止软件定时器的中断服务函数*/
    void vAnExampleInterruptServiceRoutine( void )
    {
        BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    ​
        if (xTimerStopFromISR(xTimer,&xHigherPriorityTaskWoken)!=pdPASS ) { 
            /* 软件定时器停止命令没有成功执行 */
        }
        if ( xHigherPriorityTaskWoken != pdFALSE ) { 
            /* 执行上下文切换 */
        }
    }

8.6.6软件定时器删除函数 xTimerDelete()

  • xTimerDelete()用于删除一个已经被创建成功的软件定时器,删除之后就无法使用该定时器,并且定时器相应的资源也会被系统回收释放。要想使函数 xTimerStop()必须在头文件 FreeRTOSConfig.h 中把宏 configUSE_TIMERS 定义为 1。

8.7软件定时器实验

  • 软件定时器实验是在 FreeRTOS 中创建了两个软件定时器,其中一个软件定时器是单次模式,5000 个 tick 调用一次回调函数,另一个软件定时器是周期模式,1000 个 tick 调用一次回调函数,在回调函数中输出相关信息,具体见代码清单。

  • /*
    *************************************************************************
    *                             包含的头文件
    *************************************************************************
    */ 
    /* FreeRTOS头文件 */
    #include "FreeRTOS.h"
    #include "task.h"
    #include "event_groups.h"
    /* 开发板硬件bsp头文件 */
    #include "bsp_led.h"
    #include "bsp_debug_usart.h"
    #include "bsp_key.h"
    /**************************** 任务句柄 ********************************/
    /* 
     * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
     * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
     * 这个句柄可以为NULL。
     */
    static TaskHandle_t AppTaskCreate_Handle = NULL;/* 创建任务句柄 */
    ​
    /********************************** 内核对象句柄 *********************************/
    /*
     * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
     * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
     * 们就可以通过这个句柄操作这些内核对象。
     *
     * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
     * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
     * 来完成的
     * 
     */
    static TimerHandle_t Swtmr1_Handle =NULL;   /* 软件定时器句柄 */
    static TimerHandle_t Swtmr2_Handle =NULL;   /* 软件定时器句柄 */
    /******************************* 全局变量声明 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些全局变量。
     */
    static uint32_t TmrCb_Count1 = 0; /* 记录软件定时器1回调函数执行次数 */
    static uint32_t TmrCb_Count2 = 0; /* 记录软件定时器2回调函数执行次数 */
    ​
    /******************************* 宏定义 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些宏定义。
     */
    ​
    /*
    *************************************************************************
    *                             函数声明
    *************************************************************************
    */
    static void AppTaskCreate(void);/* 用于创建任务 */
    ​
    static void Swtmr1_Callback(void* parameter);
    static void Swtmr2_Callback(void* parameter);
    ​
    static void BSP_Init(void);/* 用于初始化板载相关资源 */
    ​
    /*****************************************************************
      * @brief  主函数
      * @param  无
      * @retval 无
      * @note   第一步:开发板硬件初始化 
                第二步:创建APP应用任务
                第三步:启动FreeRTOS,开始多任务调度
      ****************************************************************/
    int main(void)
    {   
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      /* 开发板硬件初始化 */
      BSP_Init();
      
        printf("这是一个[野火]-STM32全系列开发板-FreeRTOS软件定时器实验!\n");
    ​
      /* 创建AppTaskCreate任务 */
      xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                            (const char*    )"AppTaskCreate",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )1, /* 任务的优先级 */
                            (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
      /* 启动任务调度 */           
      if(pdPASS == xReturn)
        vTaskStartScheduler();   /* 启动任务,开启调度 */
      else
        return -1;  
      
      while(1);   /* 正常不会执行到这里 */    
    }
    ​
    ​
    /***********************************************************************
      * @ 函数名  : AppTaskCreate
      * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
      * @ 参数    : 无  
      * @ 返回值  : 无
      **********************************************************************/
    static void AppTaskCreate(void)
    {
      taskENTER_CRITICAL();           //进入临界区
        
      /************************************************************************************
       * 创建软件周期定时器
       * 函数原型
       * TimerHandle_t xTimerCreate(    const char * const pcTimerName,
                                    const TickType_t xTimerPeriodInTicks,
                                    const UBaseType_t uxAutoReload,
                                    void * const pvTimerID,
                    TimerCallbackFunction_t pxCallbackFunction )
        * @uxAutoReload : pdTRUE为周期模式,pdFALS为单次模式
       * 单次定时器,周期(1000个时钟节拍),周期模式
       *************************************************************************************/
      Swtmr1_Handle=xTimerCreate((const char*       )"AutoReloadTimer",
                                (TickType_t         )1000,/* 定时器周期 1000(tick) */
                                (UBaseType_t        )pdTRUE,/* 周期模式 */
                                (void*                )1,/* 为每个计时器分配一个索引的唯一ID */
                                (TimerCallbackFunction_t)Swtmr1_Callback); 
      if(Swtmr1_Handle != NULL)                          
      {
        /***********************************************************************************
         * xTicksToWait:如果在调用xTimerStart()时队列已满,则以tick为单位指定调用任务应保持
         * 在Blocked(阻塞)状态以等待start命令成功发送到timer命令队列的时间。 
         * 如果在启动调度程序之前调用xTimerStart(),则忽略xTicksToWait。在这里设置等待时间为0.
         **********************************************************************************/
        xTimerStart(Swtmr1_Handle,0);   //开启周期定时器
      }                            
      /************************************************************************************
       * 创建软件周期定时器
       * 函数原型
       * TimerHandle_t xTimerCreate(    const char * const pcTimerName,
                                    const TickType_t xTimerPeriodInTicks,
                                    const UBaseType_t uxAutoReload,
                                    void * const pvTimerID,
                    TimerCallbackFunction_t pxCallbackFunction )
        * @uxAutoReload : pdTRUE为周期模式,pdFALS为单次模式
       * 单次定时器,周期(5000个时钟节拍),单次模式
       *************************************************************************************/
        Swtmr2_Handle=xTimerCreate((const char*         )"OneShotTimer",
                                 (TickType_t            )5000,/* 定时器周期 5000(tick) */
                                 (UBaseType_t           )pdFALSE,/* 单次模式 */
                                 (void*                   )2,/* 为每个计时器分配一个索引的唯一ID */
                                 (TimerCallbackFunction_t)Swtmr2_Callback); 
      if(Swtmr2_Handle != NULL)
      {
       /***********************************************************************************
       * xTicksToWait:如果在调用xTimerStart()时队列已满,则以tick为单位指定调用任务应保持
       * 在Blocked(阻塞)状态以等待start命令成功发送到timer命令队列的时间。 
       * 如果在启动调度程序之前调用xTimerStart(),则忽略xTicksToWait。在这里设置等待时间为0.
       **********************************************************************************/   
        xTimerStart(Swtmr2_Handle,0);   //开启周期定时器
      } 
      
      vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
      
      taskEXIT_CRITICAL();            //退出临界区
    }
    ​
    /***********************************************************************
      * @ 函数名  : Swtmr1_Callback
      * @ 功能说明: 软件定时器1 回调函数,打印回调函数信息&当前系统时间
      *              软件定时器请不要调用阻塞函数,也不要进行死循环,应快进快出
      * @ 参数    : 无  
      * @ 返回值  : 无
      **********************************************************************/
    static void Swtmr1_Callback(void* parameter)
    {       
      TickType_t tick_num1;
    ​
      TmrCb_Count1++;                       /* 每回调一次加一 */
    ​
      tick_num1 = xTaskGetTickCount();  /* 获取滴答定时器的计数值 */
      
      LED1_TOGGLE;
      
      printf("Swtmr1_Callback函数执行 %d 次\n", TmrCb_Count1);
      printf("滴答定时器数值=%d\n", tick_num1);
    }
    ​
    /***********************************************************************
      * @ 函数名  : Swtmr2_Callback
      * @ 功能说明: 软件定时器2 回调函数,打印回调函数信息&当前系统时间
      *              软件定时器请不要调用阻塞函数,也不要进行死循环,应快进快出
      * @ 参数    : 无  
      * @ 返回值  : 无
      **********************************************************************/
    static void Swtmr2_Callback(void* parameter)
    {   
      TickType_t tick_num2;
    ​
      TmrCb_Count2++;                       /* 每回调一次加一 */
    ​
      tick_num2 = xTaskGetTickCount();  /* 获取滴答定时器的计数值 */
    ​
      printf("Swtmr2_Callback函数执行 %d 次\n", TmrCb_Count2);
      printf("滴答定时器数值=%d\n", tick_num2);
    }
    ​
    ​
    /***********************************************************************
      * @ 函数名  : BSP_Init
      * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
      * @ 参数    :   
      * @ 返回值  : 无
      *********************************************************************/
    static void BSP_Init(void)
    {
        /*
         * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
         * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
         * 都统一用这个优先级分组,千万不要再分组,切忌。
         */
        NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
        
        /* LED 初始化 */
        LED_GPIO_Config();
    ​
        /* 串口初始化    */
        Debug_USART_Config();
      
      /* 按键初始化  */
      Key_GPIO_Config();
    ​
    }
    ​
    /********************************END OF FILE****************************/
    ​

9.任务通知

9.1任务通知基本概念

  • FreeRTOS 从 V8.2.0 版本开始提供任务通知这个功能,每个任务都有一个 32 位的通知值,在大多数情况下,任务通知可以替代二值信号量、计数信号量、事件组,也可以替代长度为 1 的队列(可以保存一个 32位整数或指针值)。

  • 相对于以前使用 FreeRTOS 内核通信的资源,必须创建队列、二进制信号量、计数信号量或事件组的情况,使用任务通知显然更灵活。

  • 按照 FreeRTOS 官方的说法,使用任务通知比通过信号量等 ICP 通信方式解除阻塞的任务要快 45%,并且更加省 RAM 内存空间(使用 GCC 编译器,-o2 优化级别),任务通知的使用无需创建队列。

  • 想要使用任务通知,必须将 FreeRTOSConfig.h 中的宏定义 configUSE_TASK_NOTIFICATIONS 设置为 1,其实FreeRTOS 默认是为 1 的,所以任务通知是默认使能的。

  • FreeRTOS 提供以下几种方式发送通知给任务 :

    1. 发送通知给任务, 如果有通知未读,不覆盖通知值。

    2. 发送通知给任务,直接覆盖通知值。

    3. 发送通知给任务,设置通知值的一个或者多个位,可以当做事件组来使用。

    4. 发送通知给任务,递增通知值,可以当做计数信号量使用。通过对以上任务通知方式的合理使用,可以在一定场合下替代 FreeRTOS 的信号量,队列、事件组等。

    5. 当然,凡是都有利弊,不然的话 FreeRTOS 还要内核的 IPC 通信机制干嘛,消息通知虽然处理更快,RAM 开销更小。

    6. 但也有以下限制 :

      1. 只能有一个任务接收通知消息,因为必须指定接收通知的任务。

      2. 只有等待通知的任务可以被阻塞,发送通知的任务,在任何情况下都不会因为发送失败而进入阻塞态。

9.2 任务通知的运作机制

  • 顾名思义,任务通知是属于任务中附带的资源,所以在任务被创建的时候,任务通知也被初始化的,而在分析队列和信号量的章节中,我们知道在使用队列、信号量前,必须先创建队列和信号量,目的是为了创建队列数据结构。

  • 比如使用 xQueueCreate()函数创建队列,用 xSemaphoreCreateBinary()函数创建二值信号量等等。

  • 再来看任务通知,由于任务通知的数据结构包含在任务控制块中,只要任务存在,任务通知数据结构就已经创建完毕,可以直接使用,所以使用的时候很是方便。

  • 任务通知可以在任务中向指定任务发送通知,也可以在中断中向指定任务发送通知,FreeRTOS 的每个任务都有一个 32 位的通知值,任务控制块中的成员变量 ulNotifiedValue就是这个通知值。

  • 只有在任务中可以等待通知,而不允许在中断中等待通知

  • 如果任务在等待的通知暂时无效,任务会根据用户指定的阻塞超时时间进入阻塞状态,我们可以将等待通知的任务看作是消费者;其它任务和中断可以向等待通知的任务发送通知,发送通知的任务和中断服务函数可以看作是生产者,当其他任务或者中断向这个任务发送任务通知,任务获得通知以后,该任务就会从阻塞态中解除,这与 FreeRTOS 中内核的其他通信机制一致。

9.3任务通知的函数接口讲解

9.3.1发送任务通知函数 xTaskGenericNotify()

  • 我们先看一下发送通知 API 函数。这类函数比较多,有 6 个。

  • 但仔细分析会发现它们只能完成 3 种操作,每种操作有两个 API 函数,分别为带中断保护版本和不带中断保护版本。

  • FreeRTOS 将 API 细分为带中断保护版本和不带中断保护版本是为了节省中断服务程序处理时间,提升性能。

  • 通过前面通信机制的学习,相信大家都了解了 FreeRTOS 的风格,这里的任务通知发送函数也是利用宏定义来进行扩展的,所有的函数都是一个宏定义,在任务中发送任务通知的函数均是调用 xTaskGenericNotify()函数进行发送通知。

  • xTaskNotifyGive()是一个宏,宏展开是调用函数 xTaskNotify( ( xTaskToNotify ), ( 0 ), eIncrement ),即向一个任务发送通知,并将对方的任务通知值加 1。

  • 该函数可以作为二值信号量和计数信号量的一种轻量型的实现,速度更快,在这种情况下对象任务在等待任务通 知 的 时 候 应 该 是 使 用 函 数 ulTaskNotifyTake() 而不是 xTaskNotifyWait() 。xTaskNotifyGive() 不 能 在 中 断 里 面 使 用 , 而 是 使 用 具 有 中 断 保 护 功 能 的vTaskNotifyGiveFromISR()来代替

9.3.2vTaskNotifyGiveFromISR()

  • vTaskNotifyGiveFromISR()是 vTaskNotifyGive()的中断保护版本。用于在中断中向指定任务发送任务通知,并更新对方的任务通知值(加 1 操作),在某些场景中可以替代信号量操作,因为这两个通知都是不带有通知值的。

  • vTaskNotifyGiveFromISR()函数作用,每次调用该函数都会增加任务的通知值,任务通过接收函数返回值是否大于零,判断是否获取到了通知,任务通知值初始化为 0,(如果与信号量做对比)则对应为信号量无效。当中断调用vTaskNotifyGiveFromISR()通知函数给任务的时候,任务的通知值增加,使其大于零,使其表示的通知值变为有效,任务获取有效的通知值将会被恢复。

9.3.4xTaskNotify()

  • FreeRTOS 每个任务都有一个 32 位的变量用于实现任务通知,在任务创建的时候初始化为 0。

  • 这个 32 位的通知值在任务控制块 TCB 里面定义。

  • xTaskNotify()用于在任务中直接向另外一个任务发送一个事件,接收到该任务通知的任务有可能解锁。

  • 如果你想使用任务通知来实现二值信号量和计数信号量,那么应该使用更加简单的函数 xTaskNotifyGive() ,而不是使用 xTaskNotify(),xTaskNotify()函数在发送任务通知的时候会指定一个通知值,并且用户可以指定通知值发送的方式。

9.3.5xTaskNotifyFromISR()

  • xTaskNotifyFromISR()是 xTaskNotify()的中断保护版本,真正起作用的函数是中断发送任务通知通用函数 xTaskGenericNotifyFromISR()。

9.3.6xTaskNotifyAndQuery()

  • xTaskNotifyAndQuery()与 xTaskNotify()很像,都是调用通用的任务通知发送函数xTaskGenericNotify() 来 实 现 通 知 的 发 送 , 不 同 的 是 多 了 一 个 附 加 的 参 数pulPreviousNotifyValue 用于回传接收任务的上一个通知值,xTaskNotifyAndQuery()函数不能用在中断中,而是必须使用带中断保护功能的xTaskNotifyAndQuery()FromISR 来代替。

  •  

    6a8799bf4be433a396407facbe978bdc.png

9.3.7xTaskNotifyAndQueryFromISR()

  • xTaskNotifyAndQueryFromISR()是 xTaskNotifyAndQuery ()的中断版本,用于向指定的任务发送一个任务通知,并返回对象任务的上一个通知值,该函数也是一个宏定义,真正实现发送通知的是 xTaskGenericNotifyFromISR()。

  •  

    798e0f317df64e2cf2f2c5003ded69d4.png

9.4获取任务通知函数

  • 既然 FreeRTOS 中发送任务的函数有那么多个,那么任务怎么获取到通知呢?我们说了,任务通知在某些场景可以替代信号量、消息队列、事件等。

  • 获取任务通知函数只能用在任务中,没有带中断保护版本,因 此只有两 个 API 函 数: ulTaskNotifyTake() 和xTaskNotifyWait ()。

  • 前者是为代替二值信号量和计数信号量而专门设计的,它和发送通知API 函数 xTaskNotifyGive()、vTaskNotifyGiveFromISR()配合使用

  • 后者是全功能版的等待通知,可以根据不同的参数实现轻量级二值信号量、计数信号量、事件组和长度为 1 的队列。

  • 所有的获取任务通知 API 函数都带有指定阻塞超时时间参数,当任务因为等待通知而进入阻塞时,用来指定任务的阻塞时间,这些超时机制与 FreeRTOS 的消息队列、信号量、事件等的超时机制一致。

9.4.1ulTaskNotifyTake()

  • ulTaskNotifyTake()作为二值信号量和计数信号量的一种轻量级实现,速度更快

  • 如果FreeRTOS 中使用函数 xSemaphoreTake() 来获取信号量,这个时候则可以试试使用函数ulTaskNotifyTake()来代替。

  • 对于这个函数,任务通知值为 0,对应信号量无效,如果任务设置了阻塞等待,任务被阻塞挂起。

  • 当其他任务或中断发送了通知值使其不为 0 后,通知变为有效,等待通知的任务将获取到通知,并且在退出时候根据用户传递的第一个参数 xClearCountOnExit 选择清零通知值或者执行减一操作。

  • xTaskNotifyTake()在退出的时候处理任务的通知值的时候有两种方法:

  • 一种是在函数退出时将通知值清零,这种方法适用于实现二值信号量;

  • 另外一种是在函数退出时将通知值减 1,这种方法适用于实现计数信号量。

  • 当一个任务使用其自身的任务通知值作为二值信号量或者计数信号量时,其他任务应该使用函数 xTaskNotifyGive()或者 xTaskNotify( ( xTaskToNotify ), ( 0 ), eIncrement )来向其发送信号量。

  • 如果是在中断中,则应该使用他们的中断版本函数。该函数的具体说明见表格。

  •  

    918bf5f6c8385ba3a28d8e546f79f8d6.png

9.4.2xTaskNotifyWait()

  • xTaskNotifyWait()函数用于实现全功能版的等待任务通知,根据用户指定的参数的不同,可以灵活的用于实现轻量级的消息队列队列、二值信号量、计数信号量和事件组功能,并带有超时等待。

  •  

    d357c05b373636032d0e0953197ea607.png

  • /* 这个任务展示使用任务通知值的位来传递不同的事件
     这在某些情况下可以代替事件标志组。*/
    void vAnEventProcessingTask( void *pvParameters )
    {
        for ( ;; ) {
      /* 等待任务通知,无限期阻塞(没有超时,所以没有必要检查函数返回值)
     这个任务的任务通知值的位由标志事件发生的任务或者中断来设置*/
        xTaskNotifyWait( 0x00, /* 在进入的时候不清除通知值的任何位 */
                        ULONG_MAX, /* 在退出的时候复位通知值为 0 */
                        &ulNotifiedValue, /* 任务通知值传递到变量 ulNotifiedValue 中*/
                        portMAX_DELAY ); /* 无限期等待 */
            /* 根据任务通知值里面的各个位的值处理事情 */
            if ( ( ulNotifiedValue & 0x01 ) != 0 ) {
                /* 位 0 被置 1 */
                prvProcessBit0Event();
            }
    
            if ( ( ulNotifiedValue & 0x02 ) != 0 ) {
                /* 位 1 被置 1 */
                prvProcessBit1Event();
            }
    
            if ( ( ulNotifiedValue & 0x04 ) != 0 ) {
                /* 位 2 被置 1 */
                prvProcessBit2Event();
            }
            /* ... 等等 */
        }
    }

9.5任务通知实验

9.5.1 任务通知代替消息队列

  • 任务通知代替消息队列是在 FreeRTOS 中创建了三个任务,其中两个任务是用于接收任务通知,另一个任务发送任务通知。三个任务独立运行,发送消息任务是通过检测按键的按下情况来发送消息通知,另两个任务获取消息通知,在任务通知中没有可用的通知之前就一直等待消息,一旦获取到消息通知就把消息打印在串口调试助手里,具体见代码清单。

  • *************************************************************************
    *                             包含的头文件
    *************************************************************************
    */ 
    /* FreeRTOS头文件 */
    #include "FreeRTOS.h"
    #include "task.h"
    /* 开发板硬件bsp头文件 */
    #include "bsp_led.h"
    #include "bsp_debug_usart.h"
    #include "bsp_key.h"
    #include "limits.h"
    /**************************** 任务句柄 ********************************/
    /* 
     * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
     * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
     * 这个句柄可以为NULL。
     */
    static TaskHandle_t AppTaskCreate_Handle = NULL;/* 创建任务句柄 */
    static TaskHandle_t Receive1_Task_Handle = NULL;/* Receive1_Task任务句柄 */
    static TaskHandle_t Receive2_Task_Handle = NULL;/* Receive2_Task任务句柄 */
    static TaskHandle_t Send_Task_Handle = NULL;/* Send_Task任务句柄 */
    
    /********************************** 内核对象句柄 *********************************/
    /*
     * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
     * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
     * 们就可以通过这个句柄操作这些内核对象。
     *
     * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
     * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
     * 来完成的
     * 
     */
    
    
    /******************************* 全局变量声明 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些全局变量。
     */
    
    
    /******************************* 宏定义 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些宏定义。
     */
    #define  USE_CHAR  0  /* 测试字符串的时候配置为 1 ,测试变量配置为 0  */
    
    /*
    *************************************************************************
    *                             函数声明
    *************************************************************************
    */
    static void AppTaskCreate(void);/* 用于创建任务 */
    
    static void Receive1_Task(void* pvParameters);/* Receive1_Task任务实现 */
    static void Receive2_Task(void* pvParameters);/* Receive2_Task任务实现 */
    
    static void Send_Task(void* pvParameters);/* Send_Task任务实现 */
    
    static void BSP_Init(void);/* 用于初始化板载相关资源 */
    
    /*****************************************************************
      * @brief  主函数
      * @param  无
      * @retval 无
      * @note   第一步:开发板硬件初始化 
                第二步:创建APP应用任务
                第三步:启动FreeRTOS,开始多任务调度
      ****************************************************************/
    int main(void)
    {	
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      /* 开发板硬件初始化 */
      BSP_Init();
    	printf("这是一个[野火]-STM32全系列开发板-FreeRTOS任务通知代替消息队列实验!\n");
      printf("按下KEY1或者KEY2进行任务消息通知发送 \n");
       /* 创建AppTaskCreate任务 */
      xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                            (const char*    )"AppTaskCreate",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )1, /* 任务的优先级 */
                            (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
      /* 启动任务调度 */           
      if(pdPASS == xReturn)
        vTaskStartScheduler();   /* 启动任务,开启调度 */
      else
        return -1;  
      
      while(1);   /* 正常不会执行到这里 */    
    }
    
    
    /***********************************************************************
      * @ 函数名  : AppTaskCreate
      * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
      * @ 参数    : 无  
      * @ 返回值  : 无
      **********************************************************************/
    static void AppTaskCreate(void)
    {
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      taskENTER_CRITICAL();           //进入临界区
    
      /* 创建Receive1_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )Receive1_Task, /* 任务入口函数 */
                            (const char*    )"Receive1_Task",/* 任务名字 */
                            (uint16_t       )512,   /* 任务栈大小 */
                            (void*          )NULL,	/* 任务入口函数参数 */
                            (UBaseType_t    )2,	    /* 任务的优先级 */
                            (TaskHandle_t*  )&Receive1_Task_Handle);/* 任务控制块指针 */
      if(pdPASS == xReturn)
        printf("创建Receive1_Task任务成功!\r\n");
      
      /* 创建Receive2_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )Receive2_Task, /* 任务入口函数 */
                            (const char*    )"Receive2_Task",/* 任务名字 */
                            (uint16_t       )512,   /* 任务栈大小 */
                            (void*          )NULL,	/* 任务入口函数参数 */
                            (UBaseType_t    )3,	    /* 任务的优先级 */
                            (TaskHandle_t*  )&Receive2_Task_Handle);/* 任务控制块指针 */
      if(pdPASS == xReturn)
        printf("创建Receive2_Task任务成功!\r\n");
      
      /* 创建Send_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )Send_Task,  /* 任务入口函数 */
                            (const char*    )"Send_Task",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )4, /* 任务的优先级 */
                            (TaskHandle_t*  )&Send_Task_Handle);/* 任务控制块指针 */ 
      if(pdPASS == xReturn)
        printf("创建Send_Task任务成功!\n\n");
      
      vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
      
      taskEXIT_CRITICAL();            //退出临界区
    }
    
    
    
    /**********************************************************************
      * @ 函数名  : Receive_Task
      * @ 功能说明: Receive_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void Receive1_Task(void* parameter)
    {	
      BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */
    #if USE_CHAR
      char *r_char;
    #else
      uint32_t r_num;
    #endif
      while (1)
      {
        /* BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, 
                                      uint32_t ulBitsToClearOnExit, 
                                      uint32_t *pulNotificationValue, 
                                      TickType_t xTicksToWait ); 
         * ulBitsToClearOnEntry:当没有接收到任务通知的时候将任务通知值与此参数的取
           反值进行按位与运算,当此参数为Oxfffff或者ULONG_MAX的时候就会将任务通知值清零。
         * ulBits ToClearOnExit:如果接收到了任务通知,在做完相应的处理退出函数之前将
           任务通知值与此参数的取反值进行按位与运算,当此参数为0xfffff或者ULONG MAX的时候
           就会将任务通知值清零。
         * pulNotification Value:此参数用来保存任务通知值。
         * xTick ToWait:阻塞时间。
         *
         * 返回值:pdTRUE:获取到了任务通知。pdFALSE:任务通知获取失败。
         */
        //获取任务通知 ,没获取到则一直等待
    		xReturn=xTaskNotifyWait(0x0,			//进入函数的时候不清除任务bit
                                ULONG_MAX,	  //退出函数的时候清除所有的bit
    #if USE_CHAR
                                (uint32_t *)&r_char,		  //保存任务通知值
    #else
                                &r_num,		  //保存任务通知值
    #endif                        
                                portMAX_DELAY);	//阻塞时间
        if( pdTRUE == xReturn )
    #if USE_CHAR
          printf("Receive1_Task 任务通知消息为 %s \n",r_char);                      
    #else
          printf("Receive1_Task 任务通知消息为 %d \n",r_num);                      
    #endif  
         
        
    		LED1_TOGGLE;
      }
    }
    
    /**********************************************************************
      * @ 函数名  : Receive_Task
      * @ 功能说明: Receive_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void Receive2_Task(void* parameter)
    {	
      BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */
    #if USE_CHAR
      char *r_char;
    #else
      uint32_t r_num;
    #endif
      while (1)
      {
        /* BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, 
                                      uint32_t ulBitsToClearOnExit, 
                                      uint32_t *pulNotificationValue, 
                                      TickType_t xTicksToWait ); 
         * ulBitsToClearOnEntry:当没有接收到任务通知的时候将任务通知值与此参数的取
           反值进行按位与运算,当此参数为Oxfffff或者ULONG_MAX的时候就会将任务通知值清零。
         * ulBits ToClearOnExit:如果接收到了任务通知,在做完相应的处理退出函数之前将
           任务通知值与此参数的取反值进行按位与运算,当此参数为0xfffff或者ULONG MAX的时候
           就会将任务通知值清零。
         * pulNotification Value:此参数用来保存任务通知值。
         * xTick ToWait:阻塞时间。
         *
         * 返回值:pdTRUE:获取到了任务通知。pdFALSE:任务通知获取失败。
         */
        //获取任务通知 ,没获取到则一直等待
    		xReturn=xTaskNotifyWait(0x0,			//进入函数的时候不清除任务bit
                                ULONG_MAX,	  //退出函数的时候清除所有的bit
    #if USE_CHAR
                                (uint32_t *)&r_char,		  //保存任务通知值
    #else
                                &r_num,		  //保存任务通知值
    #endif                        
                                portMAX_DELAY);	//阻塞时间
        if( pdTRUE == xReturn )
    #if USE_CHAR
          printf("Receive2_Task 任务通知消息为 %s \n",r_char);                      
    #else
          printf("Receive2_Task 任务通知消息为 %d \n",r_num);                      
    #endif  
    		LED2_TOGGLE;
      }
    }
    
    /**********************************************************************
      * @ 函数名  : Send_Task
      * @ 功能说明: Send_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void Send_Task(void* parameter)
    {	 
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
    #if USE_CHAR
      char test_str1[] = "this is a mail test 1";/* 邮箱消息test1 */
      char test_str2[] = "this is a mail test 2";/* 邮箱消息test2 */
    #else
      uint32_t send1 = 1;
      uint32_t send2 = 2;
    #endif
      
    
      
      while (1)
      {
        /* KEY1 被按下 */
        if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )
        {
          /* 原型:BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, 
                                          uint32_t ulValue, 
                                          eNotifyAction eAction ); 
           * eNoAction = 0,通知任务而不更新其通知值。
           * eSetBits,     设置任务通知值中的位。
           * eIncrement,   增加任务的通知值。
           * eSetvaluewithoverwrite,覆盖当前通知
           * eSetValueWithoutoverwrite 不覆盖当前通知
           * 
           * pdFAIL:当参数eAction设置为eSetValueWithoutOverwrite的时候,
           * 如果任务通知值没有更新成功就返回pdFAIL。
           * pdPASS: eAction 设置为其他选项的时候统一返回pdPASS。
          */
          xReturn = xTaskNotify( Receive1_Task_Handle, /*任务句柄*/
    #if USE_CHAR 
                                 (uint32_t)&test_str1, /* 发送的数据,最大为4字节 */
    #else
                                  send1, /* 发送的数据,最大为4字节 */
    #endif
                                 eSetValueWithOverwrite );/*覆盖当前通知*/
          
          if( xReturn == pdPASS )
            printf("Receive1_Task_Handle 任务通知消息发送成功!\r\n");
        } 
        /* KEY2 被按下 */
        if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )
        {
          xReturn = xTaskNotify( Receive2_Task_Handle, /*任务句柄*/
    #if USE_CHAR 
                                 (uint32_t)&test_str2, /* 发送的数据,最大为4字节 */
    #else
                                  send2, /* 发送的数据,最大为4字节 */
    #endif
                                 eSetValueWithOverwrite );/*覆盖当前通知*/
          /* 此函数只会返回pdPASS */
          if( xReturn == pdPASS )
            printf("Receive2_Task_Handle 任务通知消息发送成功!\r\n");
        }
        vTaskDelay(20);
      }
    }
    /***********************************************************************
      * @ 函数名  : BSP_Init
      * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
      * @ 参数    :   
      * @ 返回值  : 无
      *********************************************************************/
    static void BSP_Init(void)
    {
    	/*
    	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
    	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
    	 * 都统一用这个优先级分组,千万不要再分组,切忌。
    	 */
    	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
    	
    	/* LED 初始化 */
    	LED_GPIO_Config();
    
    	/* 串口初始化	*/
    	Debug_USART_Config();
      
      /* 按键初始化	*/
      Key_GPIO_Config();
    
    }
    
    /********************************END OF FILE****************************/

9.5.2任务通知代替二值信号量

  • 任务通知代替消息队列是在 FreeRTOS 中创建了三个任务,其中两个任务是用于接收任务通知,另一个任务发送任务通知。三个任务独立运行,发送通知任务是通过检测按键的按下情况来发送通知,另两个任务获取通知,在任务通知中没有可用的通知之前就一直等待任务通知,获取到通知以后就将通知值清 0,这样子是为了代替二值信号量,任务同步成功则继续执行,然后在串口调试助手里将运行信息打印出来。

  • /*
    *************************************************************************
    *                             包含的头文件
    *************************************************************************
    */ 
    /* FreeRTOS头文件 */
    #include "FreeRTOS.h"
    #include "task.h"
    /* 开发板硬件bsp头文件 */
    #include "bsp_led.h"
    #include "bsp_debug_usart.h"
    #include "bsp_key.h"
    /**************************** 任务句柄 ********************************/
    /* 
     * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
     * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
     * 这个句柄可以为NULL。
     */
    static TaskHandle_t AppTaskCreate_Handle = NULL;/* 创建任务句柄 */
    static TaskHandle_t Receive1_Task_Handle = NULL;/* Receive1_Task任务句柄 */
    static TaskHandle_t Receive2_Task_Handle = NULL;/* Receive2_Task任务句柄 */
    static TaskHandle_t Send_Task_Handle = NULL;/* Send_Task任务句柄 */
    
    /********************************** 内核对象句柄 *********************************/
    /*
     * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
     * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
     * 们就可以通过这个句柄操作这些内核对象。
     *
     * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
     * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
     * 来完成的
     * 
     */
    
    
    /******************************* 全局变量声明 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些全局变量。
     */
    
    
    /******************************* 宏定义 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些宏定义。
     */
    
    
    /*
    *************************************************************************
    *                             函数声明
    *************************************************************************
    */
    static void AppTaskCreate(void);/* 用于创建任务 */
    
    static void Receive1_Task(void* pvParameters);/* Receive1_Task任务实现 */
    static void Receive2_Task(void* pvParameters);/* Receive2_Task任务实现 */
    
    static void Send_Task(void* pvParameters);/* Send_Task任务实现 */
    
    static void BSP_Init(void);/* 用于初始化板载相关资源 */
    
    /*****************************************************************
      * @brief  主函数
      * @param  无
      * @retval 无
      * @note   第一步:开发板硬件初始化 
                第二步:创建APP应用任务
                第三步:启动FreeRTOS,开始多任务调度
      ****************************************************************/
    int main(void)
    {	
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      /* 开发板硬件初始化 */
      BSP_Init();
    	printf("这是一个[野火]-STM32全系列开发板-FreeRTOS任务通知代替二值信号量实验!\n");
      printf("按下KEY1或者KEY2进行任务与任务间的同步\n");
       /* 创建AppTaskCreate任务 */
      xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                            (const char*    )"AppTaskCreate",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )1, /* 任务的优先级 */
                            (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
      /* 启动任务调度 */           
      if(pdPASS == xReturn)
        vTaskStartScheduler();   /* 启动任务,开启调度 */
      else
        return -1;  
      
      while(1);   /* 正常不会执行到这里 */    
    }
    
    
    /***********************************************************************
      * @ 函数名  : AppTaskCreate
      * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
      * @ 参数    : 无  
      * @ 返回值  : 无
      **********************************************************************/
    static void AppTaskCreate(void)
    {
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      taskENTER_CRITICAL();           //进入临界区
    
      /* 创建Receive1_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )Receive1_Task, /* 任务入口函数 */
                            (const char*    )"Receive1_Task",/* 任务名字 */
                            (uint16_t       )512,   /* 任务栈大小 */
                            (void*          )NULL,	/* 任务入口函数参数 */
                            (UBaseType_t    )2,	    /* 任务的优先级 */
                            (TaskHandle_t*  )&Receive1_Task_Handle);/* 任务控制块指针 */
      if(pdPASS == xReturn)
        printf("创建Receive1_Task任务成功!\r\n");
      
      /* 创建Receive2_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )Receive2_Task, /* 任务入口函数 */
                            (const char*    )"Receive2_Task",/* 任务名字 */
                            (uint16_t       )512,   /* 任务栈大小 */
                            (void*          )NULL,	/* 任务入口函数参数 */
                            (UBaseType_t    )3,	    /* 任务的优先级 */
                            (TaskHandle_t*  )&Receive2_Task_Handle);/* 任务控制块指针 */
      if(pdPASS == xReturn)
        printf("创建Receive2_Task任务成功!\r\n");
      
      /* 创建Send_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )Send_Task,  /* 任务入口函数 */
                            (const char*    )"Send_Task",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )4, /* 任务的优先级 */
                            (TaskHandle_t*  )&Send_Task_Handle);/* 任务控制块指针 */ 
      if(pdPASS == xReturn)
        printf("创建Send_Task任务成功!\n\n");
      
      vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
      
      taskEXIT_CRITICAL();            //退出临界区
    }
    
    
    
    /**********************************************************************
      * @ 函数名  : Receive_Task
      * @ 功能说明: Receive_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void Receive1_Task(void* parameter)
    {	
      while (1)
      {
        /* uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait ); 
         * xClearCountOnExit:pdTRUE 在退出函数的时候任务任务通知值清零,类似二值信号量
         * pdFALSE 在退出函数ulTaskNotifyTakeO的时候任务通知值减一,类似计数型信号量。
         */
        //获取任务通知 ,没获取到则一直等待
        ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
        
        printf("Receive1_Task 任务通知获取成功!\n\n");
        
    		LED1_TOGGLE;
      }
    }
    
    /**********************************************************************
      * @ 函数名  : Receive_Task
      * @ 功能说明: Receive_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void Receive2_Task(void* parameter)
    {	
      while (1)
      {
        /* uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait ); 
         * xClearCountOnExit:pdTRUE 在退出函数的时候任务任务通知值清零,类似二值信号量
         * pdFALSE 在退出函数ulTaskNotifyTakeO的时候任务通知值减一,类似计数型信号量。
         */
        //获取任务通知 ,没获取到则一直等待
        ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
        
        printf("Receive2_Task 任务通知获取成功!\n\n");
        
    		LED2_TOGGLE;
      }
    }
    
    /**********************************************************************
      * @ 函数名  : Send_Task
      * @ 功能说明: Send_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void Send_Task(void* parameter)
    {	 
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      while (1)
      {
        /* KEY1 被按下 */
        if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )
        {
          /* 原型:BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); */
          xReturn = xTaskNotifyGive(Receive1_Task_Handle);
          /* 此函数只会返回pdPASS */
          if( xReturn == pdTRUE )
            printf("Receive1_Task_Handle 任务通知发送成功!\r\n");
        } 
        /* KEY2 被按下 */
        if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )
        {
          xReturn = xTaskNotifyGive(Receive2_Task_Handle);
          /* 此函数只会返回pdPASS */
          if( xReturn == pdPASS )
            printf("Receive2_Task_Handle 任务通知发送成功!\r\n");
        }
        vTaskDelay(20);
      }
    }
    /***********************************************************************
      * @ 函数名  : BSP_Init
      * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
      * @ 参数    :   
      * @ 返回值  : 无
      *********************************************************************/
    static void BSP_Init(void)
    {
    	/*
    	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
    	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
    	 * 都统一用这个优先级分组,千万不要再分组,切忌。
    	 */
    	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
    	
    	/* LED 初始化 */
    	LED_GPIO_Config();
    
    	/* 串口初始化	*/
    	Debug_USART_Config();
      
      /* 按键初始化	*/
      Key_GPIO_Config();
    
    }
    
    /********************************END OF FILE****************************/
    

9.5.3任务通知代替计数信号量

  • 任务通知代替计数信号量是基于计数型信号量实验修改而来,模拟停车场工作运行。

  • 并且在 FreeRTOS 中创建了两个任务:一个是获取任务通知,一个是发送任务通知,两个任务独立运行,获取通知的任务是通过按下 KEY1 按键获取,模拟停车场停车操作,其等待时间是 0;发送通知的任务则是通过检测 KEY2 按键按下进行通知的发送,模拟停车场取车操作,并且在串口调试助手输出相应信息。

  • *************************************************************************
    *                             包含的头文件
    *************************************************************************
    */ 
    /* FreeRTOS头文件 */
    #include "FreeRTOS.h"
    #include "task.h"
    #include "queue.h"
    #include "semphr.h"
    /* 开发板硬件bsp头文件 */
    #include "bsp_led.h"
    #include "bsp_debug_usart.h"
    #include "bsp_key.h"
    /**************************** 任务句柄 ********************************/
    /* 
     * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
     * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
     * 这个句柄可以为NULL。
     */
    static TaskHandle_t AppTaskCreate_Handle = NULL;/* 创建任务句柄 */
    static TaskHandle_t Take_Task_Handle = NULL;/* Take_Task任务句柄 */
    static TaskHandle_t Give_Task_Handle = NULL;/* Give_Task任务句柄 */
    
    /********************************** 内核对象句柄 *********************************/
    /*
     * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
     * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
     * 们就可以通过这个句柄操作这些内核对象。
     *
     * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
     * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
     * 来完成的
     * 
     */
    SemaphoreHandle_t CountSem_Handle =NULL;
    
    /******************************* 全局变量声明 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些全局变量。
     */
    
    
    /******************************* 宏定义 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些宏定义。
     */
    
    
    /*
    *************************************************************************
    *                             函数声明
    *************************************************************************
    */
    static void AppTaskCreate(void);/* 用于创建任务 */
    
    static void Take_Task(void* pvParameters);/* Take_Task任务实现 */
    static void Give_Task(void* pvParameters);/* Give_Task任务实现 */
    
    static void BSP_Init(void);/* 用于初始化板载相关资源 */
    
    /*****************************************************************
      * @brief  主函数
      * @param  无
      * @retval 无
      * @note   第一步:开发板硬件初始化 
                第二步:创建APP应用任务
                第三步:启动FreeRTOS,开始多任务调度
      ****************************************************************/
    int main(void)
    {	
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      /* 开发板硬件初始化 */
      BSP_Init();
      
      printf("这是一个[野火]-STM32全系列开发板-FreeRTOS任务通知代替计数信号量实验!\n");
      printf("车位默认值为0个,按下KEY1申请车位,按下KEY2释放车位!\n\n");
      
      /* 创建AppTaskCreate任务 */
      xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                            (const char*    )"AppTaskCreate",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )1, /* 任务的优先级 */
                            (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
      /* 启动任务调度 */           
      if(pdPASS == xReturn)
        vTaskStartScheduler();   /* 启动任务,开启调度 */
      else
        return -1;  
      
      while(1);   /* 正常不会执行到这里 */    
    }
    
    
    /***********************************************************************
      * @ 函数名  : AppTaskCreate
      * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
      * @ 参数    : 无  
      * @ 返回值  : 无
      **********************************************************************/
    static void AppTaskCreate(void)
    {
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      taskENTER_CRITICAL();           //进入临界区
    
      /* 创建Take_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )Take_Task, /* 任务入口函数 */
                            (const char*    )"Take_Task",/* 任务名字 */
                            (uint16_t       )512,   /* 任务栈大小 */
                            (void*          )NULL,	/* 任务入口函数参数 */
                            (UBaseType_t    )2,	    /* 任务的优先级 */
                            (TaskHandle_t*  )&Take_Task_Handle);/* 任务控制块指针 */
      if(pdPASS == xReturn)
        printf("创建Take_Task任务成功!\r\n");
      
      /* 创建Give_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )Give_Task,  /* 任务入口函数 */
                            (const char*    )"Give_Task",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )3, /* 任务的优先级 */
                            (TaskHandle_t*  )&Give_Task_Handle);/* 任务控制块指针 */ 
      if(pdPASS == xReturn)
        printf("创建Give_Task任务成功!\n\n");
      
      vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
      
      taskEXIT_CRITICAL();            //退出临界区
    }
    
    
    
    /**********************************************************************
      * @ 函数名  : Take_Task
      * @ 功能说明: Take_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void Take_Task(void* parameter)
    {	
      uint32_t take_num = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */
      /* 任务都是一个无限循环,不能返回 */
      while (1)
      {
        //如果KEY1被单击
    		if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )       
    		{
          /* uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait ); 
           * xClearCountOnExit:pdTRUE 在退出函数的时候任务任务通知值清零,类似二值信号量
           * pdFALSE 在退出函数ulTaskNotifyTakeO的时候任务通知值减一,类似计数型信号量。
           */
          //获取任务通知 ,没获取到则不等待
          take_num=ulTaskNotifyTake(pdFALSE,0);//
          if(take_num > 0)
            printf( "KEY1被按下,成功申请到停车位,当前车位为 %d \n", take_num - 1);
    			else
            printf( "KEY1被按下,车位已经没有了,请按KEY2释放车位\n" );  
    		}
    		vTaskDelay(20);     //每20ms扫描一次		
      }
    }
    
    /**********************************************************************
      * @ 函数名  : Give_Task
      * @ 功能说明: Give_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void Give_Task(void* parameter)
    {	 
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      /* 任务都是一个无限循环,不能返回 */
      while (1)
      {
        //如果KEY2被单击
    		if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )       
    		{
          /* 原型:BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); */
    			/* 释放一个任务通知 */
          xTaskNotifyGive(Take_Task_Handle);//发送任务通知
          /* 此函数只会返回pdPASS */
    			if ( pdPASS == xReturn ) 
    				printf( "KEY2被按下,释放1个停车位。\n" );
    		}
    		vTaskDelay(20);     //每20ms扫描一次	
      }
    }
    /***********************************************************************
      * @ 函数名  : BSP_Init
      * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
      * @ 参数    :   
      * @ 返回值  : 无
      *********************************************************************/
    static void BSP_Init(void)
    {
    	/*
    	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
    	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
    	 * 都统一用这个优先级分组,千万不要再分组,切忌。
    	 */
    	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
    	
    	/* LED 初始化 */
    	LED_GPIO_Config();
    
    	/* 串口初始化	*/
    	Debug_USART_Config();
      
      /* 按键初始化	*/
      Key_GPIO_Config();
    
    }
    
    /********************************END OF FILE****************************/
    

9.5.4任务通知代替事件组

  • 任务通知代替事件组实验是在事件标志组实验基础上进行修改,实验任务通知替代事件实现事件类型的通信,该实验是在 FreeRTOS 中创建了两个任务,一个是发送事件通知任务,一个是等待事件通知任务,两个任务独立运行,发送事件通知任务通过检测按键的按下情况设置不同的通知值位,等待事件通知任务则获取这任务通知值,并且根据通知值判断两个事件是否都发生,如果是则输出相应信息,LED 进行翻转。等待事件通知任务的等待时间是 portMAX_DELAY,一直在等待事件通知的发生,等待获取到事件之后清除对应的任务通知值的位,具体见代码清单。

  • *************************************************************************
    *                             包含的头文件
    *************************************************************************
    */ 
    /* FreeRTOS头文件 */
    #include "FreeRTOS.h"
    #include "task.h"
    #include "event_groups.h"
    /* 开发板硬件bsp头文件 */
    #include "bsp_led.h"
    #include "bsp_debug_usart.h"
    #include "bsp_key.h"
    #include "limits.h"
    /**************************** 任务句柄 ********************************/
    /* 
     * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
     * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
     * 这个句柄可以为NULL。
     */
    static TaskHandle_t AppTaskCreate_Handle = NULL;/* 创建任务句柄 */
    static TaskHandle_t LED_Task_Handle = NULL;/* LED_Task任务句柄 */
    static TaskHandle_t KEY_Task_Handle = NULL;/* KEY_Task任务句柄 */
    
    /********************************** 内核对象句柄 *********************************/
    /*
     * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
     * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
     * 们就可以通过这个句柄操作这些内核对象。
     *
     * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
     * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
     * 来完成的
     * 
     */
    
    /******************************* 全局变量声明 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些全局变量。
     */
    
    
    /******************************* 宏定义 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些宏定义。
     */
    #define KEY1_EVENT  (0x01 << 0)//设置事件掩码的位0
    #define KEY2_EVENT  (0x01 << 1)//设置事件掩码的位1
    
    /*
    *************************************************************************
    *                             函数声明
    *************************************************************************
    */
    static void AppTaskCreate(void);/* 用于创建任务 */
    
    static void LED_Task(void* pvParameters);/* LED_Task 任务实现 */
    static void KEY_Task(void* pvParameters);/* KEY_Task 任务实现 */
    
    static void BSP_Init(void);/* 用于初始化板载相关资源 */
    
    /*****************************************************************
      * @brief  主函数
      * @param  无
      * @retval 无
      * @note   第一步:开发板硬件初始化 
                第二步:创建APP应用任务
                第三步:启动FreeRTOS,开始多任务调度
      ****************************************************************/
    int main(void)
    {	
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      /* 开发板硬件初始化 */
      BSP_Init();
    	printf("这是一个[野火]-STM32全系列开发板-FreeRTOS任务通知代替事件组实验!\n");
      printf("按下KEY1|KEY2发送任务事件通知!\n");
       /* 创建AppTaskCreate任务 */
      xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                            (const char*    )"AppTaskCreate",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )1, /* 任务的优先级 */
                            (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
      /* 启动任务调度 */           
      if(pdPASS == xReturn)
        vTaskStartScheduler();   /* 启动任务,开启调度 */
      else
        return -1;  
      
      while(1);   /* 正常不会执行到这里 */    
    }
    
    
    /***********************************************************************
      * @ 函数名  : AppTaskCreate
      * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
      * @ 参数    : 无  
      * @ 返回值  : 无
      **********************************************************************/
    static void AppTaskCreate(void)
    {
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      taskENTER_CRITICAL();           //进入临界区
      
        
      /* 创建LED_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )LED_Task, /* 任务入口函数 */
                            (const char*    )"LED_Task",/* 任务名字 */
                            (uint16_t       )512,   /* 任务栈大小 */
                            (void*          )NULL,	/* 任务入口函数参数 */
                            (UBaseType_t    )2,	    /* 任务的优先级 */
                            (TaskHandle_t*  )&LED_Task_Handle);/* 任务控制块指针 */
      if(pdPASS == xReturn)
        printf("创建LED_Task任务成功!\r\n");
      
      /* 创建KEY_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )KEY_Task,  /* 任务入口函数 */
                            (const char*    )"KEY_Task",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )3, /* 任务的优先级 */
                            (TaskHandle_t*  )&KEY_Task_Handle);/* 任务控制块指针 */ 
      if(pdPASS == xReturn)
        printf("创建KEY_Task任务成功!\n");
      
      vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
      
      taskEXIT_CRITICAL();            //退出临界区
    }
    
    
    
    /**********************************************************************
      * @ 函数名  : LED_Task
      * @ 功能说明: LED_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void LED_Task(void* parameter)
    {	
      uint32_t r_event = 0;  /* 定义一个事件接收变量 */
      uint32_t last_event = 0;/* 定义一个保存事件的变量 */
      BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdPASS */
      /* 任务都是一个无限循环,不能返回 */
      while (1)
    	{
        /* BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, 
                                      uint32_t ulBitsToClearOnExit, 
                                      uint32_t *pulNotificationValue, 
                                      TickType_t xTicksToWait ); 
         * ulBitsToClearOnEntry:当没有接收到任务通知的时候将任务通知值与此参数的取
           反值进行按位与运算,当此参数为Oxfffff或者ULONG_MAX的时候就会将任务通知值清零。
         * ulBits ToClearOnExit:如果接收到了任务通知,在做完相应的处理退出函数之前将
           任务通知值与此参数的取反值进行按位与运算,当此参数为0xfffff或者ULONG MAX的时候
           就会将任务通知值清零。
         * pulNotification Value:此参数用来保存任务通知值。
         * xTick ToWait:阻塞时间。
         *
         * 返回值:pdTRUE:获取到了任务通知。pdFALSE:任务通知获取失败。
         */
        //获取任务通知 ,没获取到则一直等待
    		xReturn = xTaskNotifyWait(0x0,			//进入函数的时候不清除任务bit
                                  ULONG_MAX,	  //退出函数的时候清除所有的bitR
                                  &r_event,		  //保存任务通知值                    
                                  portMAX_DELAY);	//阻塞时间
        if( pdTRUE == xReturn )
        { 
          last_event |= r_event;
          /* 如果接收完成并且正确 */      
          if(last_event == (KEY1_EVENT|KEY2_EVENT)) 
          {
            last_event = 0;     /* 上一次的事件清零 */
            printf ( "Key1与Key2都按下\n");		
            LED1_TOGGLE;       //LED1	反转 
          }
          else  /* 否则就更新事件 */
            last_event = r_event;   /* 更新上一次触发的事件 */
        }
        
      }
    }
    
    /**********************************************************************
      * @ 函数名  : KEY_Task
      * @ 功能说明: KEY_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void KEY_Task(void* parameter)
    {	 
        /* 任务都是一个无限循环,不能返回 */
      while (1)
      {
        if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )       
    		{
          printf ( "KEY1被按下\n" );
          /* 原型:BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, 
                                          uint32_t ulValue, 
                                          eNotifyAction eAction ); 
           * eNoAction = 0,通知任务而不更新其通知值。
           * eSetBits,     设置任务通知值中的位。
           * eIncrement,   增加任务的通知值。
           * eSetvaluewithoverwrite,覆盖当前通知
           * eSetValueWithoutoverwrite 不覆盖当前通知
           * 
           * pdFAIL:当参数eAction设置为eSetValueWithoutOverwrite的时候,
           * 如果任务通知值没有更新成功就返回pdFAIL。
           * pdPASS: eAction 设置为其他选项的时候统一返回pdPASS。
          */
    			/* 触发一个事件1 */
    			xTaskNotify((TaskHandle_t	)LED_Task_Handle,//接收任务通知的任务句柄
                      (uint32_t		)KEY1_EVENT,			//要触发的事件
                      (eNotifyAction)eSetBits);			//设置任务通知值中的位
    									
    		}
        
    		if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )       
    		{
          printf ( "KEY2被按下\n" );	
    			/* 触发一个事件2 */
    			xTaskNotify((TaskHandle_t	)LED_Task_Handle,//接收任务通知的任务句柄
                      (uint32_t		)KEY2_EVENT,			//要触发的事件
                      (eNotifyAction)eSetBits);			//设置任务通知值中的位				
    		}
    		vTaskDelay(20);     //每20ms扫描一次		
      }
    }
    
    /***********************************************************************
      * @ 函数名  : BSP_Init
      * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
      * @ 参数    :   
      * @ 返回值  : 无
      *********************************************************************/
    static void BSP_Init(void)
    {
    	/*
    	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
    	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
    	 * 都统一用这个优先级分组,千万不要再分组,切忌。
    	 */
    	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
    	
    	/* LED 初始化 */
    	LED_GPIO_Config();
    
    	/* 串口初始化	*/
    	Debug_USART_Config();
      
      /* 按键初始化	*/
      Key_GPIO_Config();
    
    }
    
    /********************************END OF FILE****************************/
    

10.内存管理

10.1 内存管理的基本概念

  • 在计算系统中,变量、中间数据一般存放在系统存储空间中,只有在实际使用时才将它们从存储空间调入到中央处理器内部进行运算。

  • 通常存储空间可以分为两种:内部存储空间和外部存储空间

  • 内部存储空间访问速度比较快,能够按照变量地址随机地访问,也就是我们通常所说的 RAM(随机存储器),或电脑的内存;而外部存储空间内所保存的内容相对来说比较固定,即使掉电后数据也不会丢失,可以把它理解为电脑的硬盘。

  • 在这一章中我们主要讨论内部存储空间(RAM)的管理——内存管理。

  • FreeRTOS 操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些内存管理函数是如何实现的,所以在 FreeRTOS 中提供了多种内存分配算法(分配策略),但是上层接口(API)却是统一的。

  • 这样做可以增加系统的灵活性:用户可以选择对自己更有利的内存管理策略,在不同的应用场合使用不同的内存分配策略。

  • 在嵌入式程序设计中内存分配应该是根据所设计系统的特点来决定选择使用动态内存分配还是静态内存分配算法,一些可靠性要求非常高的系统应选择使用静态的,而普通的业务系统可以使用动态来提高内存使用效率。

  • 静态可以保证设备的可靠性但是需要考虑内存上限,内存使用效率低,而动态则是相反。

  • FreeRTOS 内存管理模块管理用于系统中内存资源,它是操作系统的核心模块之一

  • 主要包括内存的初始化、分配以及释放。很多人会有疑问,什么不直接使用 C 标准库中的内存管理函数呢?在电脑中我们可以用 malloc()和 free()这两个函数动态的分配内存和释放内存。但是,在嵌入式实时操作系统中,调用 malloc()和 free()却是危险的,

  • 原因有以下几点:

    • 这些函数在小型嵌入式系统中并不总是可用的,小型嵌入式设备中的 RAM 不足。

    • 它们的实现可能非常的大,占据了相当大的一块代码空间。

    • 他们几乎都不是安全的。

    • 它们并不是确定的,每次调用这些函数执行的时间可能都不一样。

    • 它们有可能产生碎片。

    • 这两个函数会使得链接器配置得复杂。

    • 如果允许堆空间的生长方向覆盖其他变量占据的内存,它们会成为 debug 的灾难。

    • 在一般的实时嵌入式系统中,由于实时性的要求,很少使用虚拟内存机制。

    • 所有的内存都需要用户参与分配,直接操作物理内存,所分配的内存不能超过系统的物理内存,所有的系统堆栈的管理,都由用户自己管理。

    • 同时,在嵌入式实时操作系统中,对内存的分配时间要求更为苛刻,分配内存的时间必须是确定的。

    • 一般内存管理算法是根据需要存储的数据的长度在内存中去寻找一个与这段数据相适应的空闲内存块,然后将数据存储在里面。

    • 而寻找这样一个空闲内存块所耗费的时间是不确定的,因此对于实时系统来说,这就是不可接受的,实时系统必须要保证内存块的分配过程在可预测的确定时间内完成,否则实时任务对外部事件的响应也将变得不可确定。

    • 而在嵌入式系统中,内存是十分有限而且是十分珍贵的,用一块内存就少了一块内存,而在分配中随着内存不断被分配和释放,整个系统内存区域会产生越来越多的碎片,因为在使用过程中,申请了一些内存,其中一些释放了,导致内存空间中存在一些小的内存块,它们地址不连续,不能够作为一整块的大内存分配出去,所以一定会在某个时间,系统已经无法分配到合适的内存了,导致系统瘫痪。

    • 其实系统中实际是还有内存的,但是因为小块的内存的地址不连续,导致无法分配成功,所以我们需要一个优良的内存分配算法来避免这种情况的出现。

    • 不同的嵌入式系统具有不同的内存配置和时间要求。

    • 所以单一的内存分配算法只可能适合部分应用程序。

    • 因此,FreeRTOS 将内存分配作为可移植层面(相对于基本的内核代码部分而言),FreeRTOS 有针对性的提供了不同的内存分配管理算法,这使得应用于不同场景的设备可以选择适合自身内存算法。

    • FreeRTOS 对内存管理做了很多事情,FreeRTOS 的 V9.0.0 版本为我们提供了 5 种内存管理算法,分别是 heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c,源文件存放于FreeRTOS\Source\portable\MemMang 路径下,在使用的时候选择其中一个添加到我们的工程中去即可。

    • FreeRTOS 的内存管理模块通过对内存的申请、释放操作,来管理用户和系统对内存的使用,使内存的利用率和使用效率达到最优,同时最大限度地解决系统可能产生的内存碎片问题。

10.2 内存管理的应用场景

  • 首先,在使用内存分配前,必须明白自己在做什么,这样做与其他的方法有什么不同,特别是会产生哪些负面影响,在自己的产品面前,应当选择哪种分配策略。

  • 内存管理的主要工作是动态划分并管理用户分配好的内存区间,主要是在用户需要使用大小不等的内存块的场景中使用,当用户需要分配内存时,可以通过操作系统的内存申请函数索取指定大小内存块,一旦使用完毕,通过动态内存释放函数归还所占用内存,使之可以重复使用(heap_1.c 的内存管理除外)。

  • 例如我们需要定义一个 float 型数组:floatArr[];但是,在使用数组的时候,总有一个问题困扰着我们:数组应该有多大?在很多的情况下,你并不能确定要使用多大的数组,可能为了避免发生错误你就需要把数组定义得足够大。

  • 即使你知道想利用的空间大小,但是如果因为某种特殊原因空间利用的大小有增加或者减少,你又必须重新去修改程序,扩大数组的存储范围。

  • 这种分配固定大小的内存分配方法称之为静态内存分配。这种内存分配的方法存在比较严重的缺陷,在大多数情况下会浪费大量的内存空间,在少数情况下,当你定义的数组不够大时,可能引起下标越界错误,甚至导致严重后果。我们用动态内存分配就可以解决上面的问题。

  • 所谓动态内存分配就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。

  • 动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。

10.3内存管理方案详解

  • FreeRTOS 规定了内存管理的函数接口,具体见,但是不管其内部的内存管理方案是怎么实现的,所以,FreeRTOS 可以提供多个内存管理方案,下面,就一起看看各个内存管理方案的区别。

  • 1 void *pvPortMalloc( size_t xSize ); //内存申请函数
    2 void vPortFree( void *pv ); //内存释放函数
    3 void vPortInitialiseBlocks( void ); //初始化内存堆函数
    4 size_t xPortGetFreeHeapSize( void ); //获取当前未分配的内存堆大小
    5 size_t xPortGetMinimumEverFreeHeapSize( void ); //获取未分配的内存堆历史最小值
  • FreeRTOS 提供的内存管理都是从内存堆中分配内存的。

  • 从前面学习的过程中,我们也知道,创建任务、消息队列、事件等操作都使用到分配内存的函数,这是系统中默认使用内存管理函数从内存堆中分配内存给系统核心组件使用。

  • 对于 heap_1.c、heap_2.c 和 heap_4.c 这三种内存管理方案,内存堆实际上是一个很大的数 组 , 定 义 为 static uint8_t ucHeap[ configTOTAL_HEAP_SIZE] , 而 宏 定 义configTOTAL_HEAP_SIZE 则表示系统管理内存大小,单位为字,在 FreeRTOSConfig.h 中由用户设定。

  • 对于 heap_3.c 这种内存管理方案,它封装了 C 标准库中的 malloc()和 free()函数,封装后的 malloc()和 free()函数具备保护,可以安全在嵌入式系统中执行。因此,用户需要通过编译器或者启动文件设置堆空间。

  • heap_5.c 方案允许用户使用多个非连续内存堆空间,每个内存堆的起始地址和大小由用户定义。这种应用其实还是很大的,比如做图形显示、GUI 等,可能芯片内部的 RAM是不够用户使用的,需要外部 SDRAM,那这种内存管理方案则比较合适。

10.3.1 heap_1

  • cheap_1.c 管理方案是 FreeRTOS 提供所有内存管理方案中最简单的一个,它只能申请内存而不能进行内存释放,并且申请内存的时间是一个常量,这样子对于要求安全的嵌入式设备来说是最好的,因为不允许内存释放,就不会产生内存碎片而导致系统崩溃,但是也有缺点,那就是内存利用率不高,某段内存只能用于内存申请的地方,即使该内存只使用一次,也无法让系统回收重新利用实际上,大多数的嵌入式系统并不会经常动态申请与释放内存,一般都是在系统完成的时候,就一直使用下去,永不删除,所以这个内存管理方案实现简洁、安全可靠,使用的非常广泛。

  • heap1.c 方案具有以下特点:

    • 1、 用于从不删除任务、队列、信号量、互斥量等的应用程序(实际上大多数使用FreeRTOS 的应用程序都符合这个条件)。

    • 2、 函数的执行时间是确定的并且不会产生内存碎片。

    • heap_1.c 管理方案使用两个静态变量对系统管理的内存进行跟踪内存分配,具体见代码清单静态变量

    • 2 static uint8_t *pucAlignedHeap = NULL;
    • 变量 xNextFreeByte 用来定位下一个空闲的内存堆位置。

    • 真正的运作过程是记录已经被分配的内存大小,在每次申请内存成功后,都会增加申请内存的字节数目。

    • 因为内存堆实际上是一个大数组,我们只需要知道已分配内存的大小,就可以用它作为偏移量找到未分配内存的起始地址。

    • 静态变量 pucAlignedHeap 是一个指向对齐后的内存堆起始地址,我们使用一个数组作为堆内存,但是数组的起始地址并不一定是对齐的内存地址,所以我们需要得到FreeRTOS 管理的内存空间对齐后的起始地址,并且保存在静态变量 pucAlignedHeap 中。

    • 为什么要对齐?这是因为大多数硬件访问内存对齐的数据速度会更快。为了提高性能,FreeRTOS 会进行对齐操作,不同的硬件架构的内存对齐操作可能不一样,对于 Cortex-M3架构,进行 8 字节对齐。

10.3.2heap_2.c

  • heap_2.c 方案与 heap_1.c 方案采用的内存管理算法不一样,它采用一种最佳匹配算法(best fit algorithm),比如我们申请 100 字节的内存,而可申请内存中有三块对应大小 200 字节, 500 字节和 1000 字节大小的内存块,按照算法的最佳匹配,这时候系统会把 200 字节大小的内存块进行分割并返回申请内存的起始地址,剩余的内存则插回链表留待下次申请。Heap_2.c 方案支持释放申请的内存,但是它不能把相邻的两个小的内存块合成一个大的内存块,对于每次申请内存大小都比较固定的,这个方式是没有问题的,而对于每次申请并不是固定内存大小的则会造成内存碎片,后面要讲解的 heap_4.c 方案采用的内存管理算法能解决内存碎片的问题,可以把这些释放的相邻的小的内存块合并成一个大的内存块。同样的 ,内存分配时需 要的总的内存堆 空间由文件 reeRTOSConfig.h 中的宏configTOTAL_HEAP_SIZE 配置,单位为字。通过调用函数 xPortGetFreeHeapSize() 我们可以知道还剩下多少内存没有使用,但是并不包括内存碎片,这样一来我们可以实时的调整和优化 configTOTAL_HEAP_SIZE 的大小。

  • heap_2.c 方案具有以下特点:

    • 可以用在那些反复的删除任务、队列、信号量、等内核对象且不担心内存碎片的

    • 应用程序。如果我们的应用程序中的队列、任务、信号量、等工作在一个不可预料的顺序,这样子也有可能会导致内存碎片。

    • 具有不确定性,但是效率比标准 C 库中的 malloc 函数高得多。

    • 不能用于那些内存分配和释放是随机大小的应用程序。

  • heap_2.c 方案与 heap_1 方案在内存堆初始化的时候操作都是一样的,在内存中开辟了一个静态数组作为堆的空间,大小由用户定义,然后进行字节对齐处理。

  • heap_2.c 方案采用链表的数据结构记录空闲内存块,将所有的空闲内存块组成一个空闲内存块链表,FreeRTOS 采用 2 个 BlockLink_t 类型的局部静态变量 xStart、xEnd 来标识空闲内存块链表的起始位置与结束位置,空闲内存块链表结构体具体见代码清单。

  • 1 typedef struct A_BLOCK_LINK {
    2 struct A_BLOCK_LINK *pxNextFreeBlock;
    3 size_t xBlockSize;
    4 } BlockLink_t;
    //pxNextFreeBlock 成员变量是指向下一个空闲内存块的指针。
    //xBlockSize 用于记录申请的内存块的大小,包括链表结构体大小。

10.3.3heap_3.c

  • heap_3.c 方案只是简单的封装了标准 C 库中的 malloc()和 free()函数,并且能满足常用的编译器。重新封装后的 malloc()和 free()函数具有保护功能,采用的封装方式是操作内存前挂起调度器、完成后再恢复调度器。

  • heap_3.c 方案具有以下特点:

    • 需要链接器设置一个堆,malloc()和 free()函数由编译器提供。

    • 具有不确定性。

    • 很可能增大 RTOS 内核的代码大小。

  • 要 注 意 的 是 在 使 用 heap_3.c 方 案 时 , FreeRTOSConfig.h 文 件 中 的configTOTAL_HEAP_SIZE 宏定义不起作用。

  • 在 STM32 系列的工程中,这个由编译器定义

  • 的堆都在启动文件里面设置,单位为字节,我们具体以 STM32F10x 系列为例,具体见图。而其它系列的都差不多。

  •  

    d27551b07d8d57add21e55bd62de66b5.png

10.3.4 heap_4.c

  • heap_4.c 方案与 heap_2.c 方案一样都采用最佳匹配算法来实现动态的内存分配,但是不一样的是 heap_4.c 方案还包含了一种合并算法,能把相邻的空闲的内存块合并成一个更大的块,这样可以减少内存碎片。

  • heap_4.c 方案特别适用于移植层中可以直接使用pvPortMalloc()和 vPortFree()函数来分配和释放内存的代码。

  • 内 存 分 配 时 需 要 的 总 的 堆 空 间 由 文 件 FreeRTOSConfig.h 中 的 宏configTOTAL_HEAP_SIZE 配置,单位为字。

  • 通过调用函数 xPortGetFreeHeapSize() 我们可以知道还剩下多少内存没有使用,但是并不包括内存碎片。

  • 这样一来我们可以实时的调整和优化 configTOTAL_HEAP_SIZE 的大小。

  • heap_4.c 方案的空闲内存块也是以单链表的形式连接起来的,BlockLink_t 类型的局部静态变量 xStart 表示链表头,但 heap_4.c 内存管理方案的链表尾部则保存在内存堆空间最后位置,并使用 BlockLink_t 指针类型局部静态变量 pxEnd 指向这个区域(而 heap_2.c 内存管理方案则使用 BlockLink_t 类型的静态变量 xEnd 表示链表尾)heap_4.c 内存管理方案的空闲块链表不是以内存块大小进行排序的,而是以内存块起始地址大小排序,内存地址小的在前,地址大的在后,因为 heap_4.c 方案还有一个内存合并算法,在释放内存的时候,假如相邻的两个空闲内存块在地址上是连续的,那么就可以合并为一个内存块,这也是为了适应合并算法而作的改变。

  • heap_4.c 方案具有以下特点:

    • 1、可用于重复删除任务、队列、信号量、互斥量等的应用程序

    • 2、可用于分配和释放随机字节内存的应用程序,但并不像 heap2.c 那样产生严重的内存碎片。

    • 3、具有不确定性,但是效率比标准 C 库中的 malloc 函数高得多。

10.3.5 heap_5.c

  • heap_5.c 方案在实现动态内存分配时与 heap4.c 方案一样,采用最佳匹配算法和合并算法,并且允许内存堆跨越多个非连续的内存区,也就是允许在不连续的内存堆中实现内存分配,比如用户在片内 RAM 中定义一个内存堆,还可以在外部 SDRAM 再定义一个或多个内存堆,这些内存都归系统管理。

  • heap_5.c 方案通过调用 vPortDefineHeapRegions()函数来实现系统管理的内存初始化,在内存初始化未完成前不允许使用内存分配和释放函数。

  • 如创建 FreeRTOS 对象(任务、队列、信号量等)时会隐式的调用 pvPortMalloc()函数,因此必须注意:使用 heap_5.c 内存管理方案创建任何对象前,要先调用 vPortDefineHeapRegions()函数将内存初始化

10.4内存管理的实验

  • 内存管理实验使用 heap_4.c 方案进行内存管理测试,创建了两个任务,分别是 LED 任务与内存管理测试任务,内存管理测试任务通过检测按键是否按下来申请内存或释放内存,当申请内存成功就像该内存写入一些数据,如当前系统的时间等信息,并且通过串口输出相关信息;LED 任务是将 LED 翻转,表示系统处于运行状态。在不需要再使用内存时,注意要及时释放该段内存,避免内存泄露,源码具体见代码清单.

  • *************************************************************************
    *                             包含的头文件
    *************************************************************************
    */ 
    /* FreeRTOS头文件 */
    #include "FreeRTOS.h"
    #include "task.h"
    /* 开发板硬件bsp头文件 */
    #include "bsp_led.h"
    #include "bsp_debug_usart.h"
    #include "bsp_key.h"
    /**************************** 任务句柄 ********************************/
    /* 
     * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
     * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
     * 这个句柄可以为NULL。
     */
    static TaskHandle_t AppTaskCreate_Handle = NULL;/* 创建任务句柄 */
    static TaskHandle_t LED_Task_Handle = NULL;/* LED_Task任务句柄 */
    static TaskHandle_t Test_Task_Handle = NULL;/* Test_Task任务句柄 */
    
    
    
    /******************************* 全局变量声明 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些全局变量。
     */
    uint8_t *Test_Ptr = NULL;
    
    
    /*
    *************************************************************************
    *                             函数声明
    *************************************************************************
    */
    static void AppTaskCreate(void);/* 用于创建任务 */
    
    static void LED_Task(void* pvParameters);/* LED_Task任务实现 */
    static void Test_Task(void* pvParameters);/* Test_Task任务实现 */
    
    static void BSP_Init(void);/* 用于初始化板载相关资源 */
    
    /*****************************************************************
      * @brief  主函数
      * @param  无
      * @retval 无
      * @note   第一步:开发板硬件初始化 
                第二步:创建APP应用任务
                第三步:启动FreeRTOS,开始多任务调度
      ****************************************************************/
    int main(void)
    {	
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      /* 开发板硬件初始化 */
      BSP_Init();
    	printf("这是一个[野火]-STM32全系列开发板-FreeRTOS内存管理实验\n");
      printf("按下KEY1申请内存,按下KEY2释放内存\n");
       /* 创建AppTaskCreate任务 */
      xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                            (const char*    )"AppTaskCreate",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )1, /* 任务的优先级 */
                            (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
      /* 启动任务调度 */           
      if(pdPASS == xReturn)
        vTaskStartScheduler();   /* 启动任务,开启调度 */
      else
        return -1;  
      
      while(1);   /* 正常不会执行到这里 */    
    }
    
    
    /***********************************************************************
      * @ 函数名  : AppTaskCreate
      * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
      * @ 参数    : 无  
      * @ 返回值  : 无
      **********************************************************************/
    static void AppTaskCreate(void)
    {
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      taskENTER_CRITICAL();           //进入临界区
    
      /* 创建LED_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )LED_Task, /* 任务入口函数 */
                            (const char*    )"LED_Task",/* 任务名字 */
                            (uint16_t       )512,   /* 任务栈大小 */
                            (void*          )NULL,	/* 任务入口函数参数 */
                            (UBaseType_t    )2,	    /* 任务的优先级 */
                            (TaskHandle_t*  )&LED_Task_Handle);/* 任务控制块指针 */
      if(pdPASS == xReturn)
        printf("创建LED_Task任务成功\n");
      
      /* 创建Test_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )Test_Task,  /* 任务入口函数 */
                            (const char*    )"Test_Task",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )3, /* 任务的优先级 */
                            (TaskHandle_t*  )&Test_Task_Handle);/* 任务控制块指针 */ 
      if(pdPASS == xReturn)
        printf("创建Test_Task任务成功\n\n");
      
      vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
      
      taskEXIT_CRITICAL();            //退出临界区
    }
    
    
    
    /**********************************************************************
      * @ 函数名  : LED_Task
      * @ 功能说明: LED_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void LED_Task(void* parameter)
    {	
      while (1)
      {
        LED1_TOGGLE;
        vTaskDelay(1000);/* 延时1000个tick */
      }
    }
    
    /**********************************************************************
      * @ 函数名  : Test_Task
      * @ 功能说明: Test_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void Test_Task(void* parameter)
    {	 
      uint32_t g_memsize;
      while (1)
      {
        if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )
        {
          /* KEY1 被按下 */
          if(NULL == Test_Ptr)
          {
                      
            /* 获取当前内存大小 */
            g_memsize = xPortGetFreeHeapSize();
            printf("系统当前内存大小为 %d 字节,开始申请内存\n",g_memsize);
            Test_Ptr = pvPortMalloc(1024);
            if(NULL != Test_Ptr)
            {
              printf("内存申请成功\n");
              printf("申请到的内存地址为%#x\n",(int)Test_Ptr);
    
              /* 获取当前内剩余存大小 */
              g_memsize = xPortGetFreeHeapSize();
              printf("系统当前内存剩余存大小为 %d 字节\n",g_memsize);
                      
              //向Test_Ptr中写入当数据:当前系统时间
              sprintf((char*)Test_Ptr,"当前系统TickCount = %d \n",xTaskGetTickCount());
              printf("写入的数据是 %s \n",(char*)Test_Ptr);
            }
          }
          else
          {
            printf("请先按下KEY2释放内存再申请\n");
          }
        } 
        if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )
        {
          /* KEY2 被按下 */
          if(NULL != Test_Ptr)
          {
            printf("释放内存\n");
            vPortFree(Test_Ptr);	//释放内存
            Test_Ptr=NULL;
            /* 获取当前内剩余存大小 */
            g_memsize = xPortGetFreeHeapSize();
            printf("系统当前内存大小为 %d 字节,内存释放完成\n",g_memsize);
          }
          else
          {
            printf("请先按下KEY1申请内存再释放\n");
          }
        }
        vTaskDelay(20);/* 延时20个tick */
      }
    }
    
    /***********************************************************************
      * @ 函数名  : BSP_Init
      * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
      * @ 参数    :   
      * @ 返回值  : 无
      *********************************************************************/
    static void BSP_Init(void)
    {
    	/*
    	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
    	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
    	 * 都统一用这个优先级分组,千万不要再分组,切忌。
    	 */
    	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
    	
    	/* LED 初始化 */
    	LED_GPIO_Config();
    
    	/* 串口初始化	*/
    	Debug_USART_Config();
      
      /* 按键初始化	*/
      Key_GPIO_Config();
    
    }
    
    /********************************END OF FILE****************************/
    

11.中断管理

11.1 异常与中断的基本概念

  • 异常是导致处理器脱离正常运行转向执行特殊代码的任何事件,如果不及时进行处理,轻则系统出错,重则会导致系统毁灭性瘫痪。

  • 所以正确地处理异常,避免错误的发生是提高软件鲁棒性(稳定性)非常重要的一环,对于实时系统更是如此。

  • 异常是指任何打断处理器正常执行,并且迫使处理器进入一个由有特权的特殊指令执行的事件。

  • 异常通常可以分成两类:同步异常和异步异常。

  • 由内部事件(像处理器指令运行产生的事件)引起的异常称为同步异常,例如造成被零除的算术运算引发一个异常,又如在某些处理器体系结构中,对于确定的数据尺寸必须从内存的偶数地址进行读和写操作。从一个奇数内存地址的读或写操作将引起存储器存取一个错误事件并引起一个异常(称为校准异常)。

  • 异步异常主要是指由于外部异常源产生的异常,是一个由外部硬件装置产生的事件引起的异步异常。

  • 同步异常不同于异步异常的地方是事件的来源,同步异常事件是由于执行某些指令而从处理器内部产生的,而异步异常事件的来源是外部硬件装置。例如按下设备某个按钮产生的事件。

  • 同步异常与异步异常的区别还在于,同步异常触发后,系统必须立刻进行处理而不能够依然执行原有的程序指令步骤

  • 异步异常则可以延缓处理甚至是忽略,例如按键中断异常,虽然中断异常触发了,但是系统可以忽略它继续运行(同样也忽略了相应的按键事件)

  • 中断,中断属于异步异常。

  • 所谓中断是指中央处理器 CPU 正在处理某件事的时候,外部发生了某一事件,请求 CPU 迅速处理,CPU暂时中断当前的工作,转入处理所发生的事件,处理完后,再回到原来被中断的地方,继续原来的工作,这样的过程称为中断

  • 中断能打断任务的运行,无论该任务具有什么样的优先级,因此中断一般用于处理比较紧急的事件,而且只做简单处理,例如标记该事件,在使用 FreeRTOS 系统时,一般建议使用信号量、消息或事件标志组等标志中断的发生,将这些内核对象发布给处理任务,处理任务再做具体处理。通过中断机制,在外设不需要 CPU 介入时,CPU 可以执行其他任务,而当外设需要CPU 时通过产生中断信号使 CPU 立即停止当前任务转而来响应中断请求。这样可以使CPU 避免把大量时间耗费在等待、查询外设状态的操作上,因此将大大提高系统实时性以及执行效率。

  • 此处读者要知道一点,FreeRTOS 源码中有许多处临界段的地方,临界段虽然保护了关键代码的执行不被打断,但也会影响系统的实时,任何使用了操作系统的中断响应都不会比裸机快。比如,某个时候有一个任务在运行中,并且该任务部分程序将中断屏蔽掉,也就是进入临界段中,这个时候如果有一个紧急的中断事件被触发,这个中断就会被挂起,不能得到及时响应,必须等到中断开启才可以得到响应,如果屏蔽中断时间超过了紧急中断能够容忍的限度,危害是可想而知的。

  • 所以,操作系统的中断在某些时候会有适当的中断延迟,因此调用中断屏蔽函数进入临界段的时候,也需快进快出。

  • 当然 FreeRTOS 也能允许一些高优先级的中断不被屏蔽掉,能够及时做出响应,不过这些中断就不受系统管理,也不允许调用 FreeRTOS 中与中断相关的任何 API 函数接口。

  • FreeRTOS 的中断管理支持:

    • 开/关中断。

    • 恢复中断。

    • 中断使能。

    • 中断屏蔽。

    • 可选择系统管理的中断优先级。

11.1.1中断的介绍

  • 中断相关的硬件可以划分为三类:外设、中断控制器、CPU 本身

  • 外设:当外设需要请求 CPU 时,产生一个中断信号,该信号连接至中断控制器。

  • 中断控制器:中断控制器是 CPU 众多外设中的一个,它一方面接收其他外设中断信号的输入,另一方面,它会发出中断信号给 CPU。可以通过对中断控制器编程实现对中断源的优先级、触发方式、打开和关闭源等设置操作。在 Cortex-M 系列控制器中常用的中断控制器是 NVIC(内嵌向量中断控制器 Nested Vectored Interrupt Controller)。

  • CPU:CPU 会响应中断源的请求,中断当前正在执行的任务,转而执行中断处理程序。NVIC 最多支持 240个中断,每个中断最多 256 个优先级。

11.1.2 和中断相关的名词解释

  • 中断号:每个中断请求信号都会有特定的标志,使得计算机能够判断是哪个设备提出的中断请求,这个标志就是中断号。

  • 中断请求:“紧急事件”需向 CPU 提出申请,要求 CPU 暂停当前执行的任务,转而处理该“紧急事件”,这一申请过程称为中断请求。

  • 中断优先级:为使系统能够及时响应并处理所有中断,系统根据中断时间的重要性和紧迫程度,将中断源分为若干个级别,称作中断优先级。

  • 中断处理程序:当外设产生中断请求后,CPU 暂停当前的任务,转而响应中断申请,即执行中断处理程序。

  • 中断触发:中断源发出并送给 CPU 控制信号,将中断触发器置“1”,表明该中断源产生了中断,要求 CPU 去响应该中断,CPU 暂停当前任务,执行相应的中断处理程序。

  • 中断触发类型:外部中断申请通过一个物理信号发送到 NVIC,可以是电平触发或边沿触发。中断向量:中断服务程序的入口地址。

  • 中断向量表:存储中断向量的存储区,中断向量与中断号对应,中断向量在中断向量表中按照中断号顺序存储。

  • 临界段:代码的临界段也称为临界区,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即开中断。

11.2 中断管理的运作机制

  • 当中断产生时,处理机将按如下的顺序执行:

    1. 保存当前处理机状态信息

    1. 载入异常或中断处理函数到 PC寄存器

    1. 把控制权转交给处理函数并开始执行

    1. 当处理函数执行完成时,恢复处理器状态信息

    1. 从异常或中断中返回到前一个程序执行点

  • 中断使得 CPU 可以在事件发生时才给予处理,而不必让 CPU 连续不断地查询是否有相应的事件发生。

  • 通过两条特殊指令:关中断和开中断可以让处理器不响应或响应中断,在关闭中断期间,通常处理器会把新产生的中断挂起,当中断打开时立刻进行响应,所以会有适当的延时响应中断,故用户在进入临界区的时候应快进快出。

  • 中断发生的环境有两种情况:在任务的上下文中,在中断服务函数处理上下文中。

  • 任务在工作的时候,如果此时发生了一个中断,无论中断的优先级是多大,都会打断当前任务的执行,从而转到对应的中断服务函数中执行,其过程具体见图。

  • 图 (1)、(3):在任务运行的时候发生了中断,那么中断会打断任务的运行,那么操作系统将先保存当前任务的上下文环境,转而去处理中断服务函数。

  • 图 (2)、(4):当且仅当中断服务函数处理完的时候才恢复任务的上下文环境,继续运行任务

 

7f5480aa62105fe936f3ecd92f8d1293.png

  • 在执行中断服务例程的过程中,如果有更高优先级别的中断源触发中断,由于当前处于中断处理上下文环境中,根据不同的处理器构架可能有不同的处理方式,比如新的中断等待挂起直到当前中断处理离开后再行响应;或新的高优先级中断打断当前中断处理过程,而去直接响应这个更高优先级的新中断源。后面这种情况,称之为中断嵌套。在硬实时环境中,前一种情况是不允许发生的,不能使响应中断的时间尽量的短。

  • 而在软件处理(软实时环境)上,FreeRTOS 允许中断嵌套,即在一个中断服务例程期间,处理器可以响应另外一个优先级更高的中断,过程如图所示。

  • 图 (1):当中断 1 的服务函数在处理的时候发生了中断 2,由于中断 2 的优先级比中断 1 更高,所以发生了中断嵌套,那么操作系统将先保存当前中断服务函数的上下文环境,并且转向处理中断 2,当且仅当中断 2 执行完的时候图 (2),才能继续执行中断 1。

 

12227b90df4ec13f68380b089bb571b7.png

11.3 中断延迟的概念

  • 即使操作系统的响应很快了,但对于中断的处理仍然存在着中断延迟响应的问题,我们称之为中断延迟(Interrupt Latency) 。

  • 中断延迟是指从硬件中断发生到开始执行中断处理程序第一条指令之间的这段时间。也就是:系统接收到中断信号到操作系统作出响应,并完成换到转入中断服务程序的时间。

  • 也可以简单地理解为:(外部)硬件(设备)发生中断,到系统执行中断服务子程序(ISR)的第一条指令的时间。

  • 中断的处理过程是:外界硬件发生了中断后,CPU 到中断处理器读取中断向量,并且查找中断向量表,找到对应的中断服务子程序(ISR)的首地址,然后跳转到对应的 ISR去做相应处理。这部分时间,我称之为:识别中断时间。在允许中断嵌套的实时操作系统中,中断也是基于优先级的,允许高优先级中断抢断正在处理的低优先级中断,所以,如果当前正在处理更高优先级的中断,即使此时有低优先级的中断,也系统不会立刻响应,而是等到高优先级的中断处理完之后,才会响应。

  • 而即使在不支持中断嵌套,即中断是没有优先级的,中断是不允许被中断的,所以,如果当前系统正在处理一个中断,而此时另一个中断到来了,系统也是不会立即响应的,而只是等处理完当前的中断之后,才会处理后来的中断。此部分时间,我称其为:等待中断打开时间。

  • 在操作系统中,很多时候我们会主动进入临界段,系统不允许当前状态被中断打断,故而在临界区发生的中断会被挂起,直到退出临界段时候打开中断。此部分时间,我称其为:关闭中断时间。

  • 中断延迟可以定义为,从中断开始的时刻到中断服务例程开始执行的时刻之间的时间段。

  • 中断延迟 = 识别中断时间 + [等待中断打开时间] + [关闭中断时间]。注意:“[ ]”的时间是不一定都存在的,此处为最大可能的中断延迟时间。

11.4 中断管理的应用场景

  • 中断在嵌入式处理器中应用非常之多,没有中断的系统不是一个好系统,因为有中断,才能启动或者停止某件事情,从而转去做另一间事情。

  • 我们可以举一个日常生活中的例子来说明,假如你正在给朋友写信,电话铃响了,这时你放下手中的笔去接电话,通话完毕再继续写信。

  • 这个例子就表现了中断及其处理的过程:电话铃声使你暂时中止当前的工作,而去处理更为急需处理的事情——接电话,当把急需处理的事情处理完毕之后,再回过头来继续原来的事情。

  • 在这个例子中,电话铃声就可以称为“中断请求”,而你暂停写信去接电话就叫作“中断响应”,那么接电话的过程就是“中断处理”。

  • 由此我们可以看出,在计算机执行程序的过程中,由于出现某个特殊情况(或称为“特殊事件”),使得系统暂时中止现行程序,而转去执行处理这一特殊事件的程序,处理完毕之后再回到原来程序的中断点继续向下执行。

  • 为什么说吗没有中断的系统不是好系统呢?我们可以再举一个例子来说明中断的作用。假设有一个朋友来拜访你,但是由于不知何时到达,你只能在门口等待,于是什么事情也干不了;但如果在门口装一个门铃,你就不必在门口等待而可以在家里去做其他的工作,朋友来了按门铃通知你,这时你才中断手中的工作去开门,这就避免了不必要的等待。

  • CPU 也是一样,如果时间都浪费在查询的事情上,那这个 CPU啥也干不了,要他何用。

  • 在嵌入式系统中合理利用中断,能更好利用 CPU 的资源。

11.5 中断管理讲解

  • ARM Cortex-M 系列内核的中断是由硬件管理的,而 FreeRTOS 是软件,它并不接管由硬件管理的相关中断(接管简单来说就是,所有的中断都由 RTOS 的软件管理,硬件来了中断时,由软件决定是否响应,可以挂起中断,延迟响应或者不响应),只支持简单的开关中断等,所以 FreeRTOS 中的中断使用其实跟裸机差不多的,需要我们自己配置中断,并且使能中断,编写中断服务函数,在中断服务函数中使用内核 IPC 通信机制,一般建议使用信号量、消息或事件标志组等标志事件的发生,将事件发布给处理任务,等退出中断后再由相关处理任务具体处理中断。

  • 用 户 可 以 自 定 义 配 置 系 统 可 管 理 的 最 高 中 断 优 先 级 的 宏 定 义configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY , 它 是 用 于 配 置 内 核 中 的basepri 寄存器的,当 basepri 设置为某个值的时候,NVIC 不会响应比该优先级低的中断,而优先级比之更高的中断则不受影响。

  • 就是说当这个宏定义配置为 5 的时候,中断优先级数值在 0、1、2、3、4 的这些中断是不受 FreeRTOS 屏蔽的,也就是说即使在系统进入临界段的时候,这些中断也能被触发而不是等到退出临界段的时候才被触发,当然,这些中断服务函数中也不能调用 FreeRTOS 提供的 API 函数接口,而中断优先级在 5 到 15 的这些中断是可以被屏蔽的,也能安全调用 FreeRTOS 提供的 API 函数接口。

  • ARM Cortex-M NVIC 支持中断嵌套功能:当一个中断触发并且系统进行响应时,处理器硬件会将当前运行的部分上下文寄存器自动压入中断栈中,这部分的寄存器包括 PSR,R0,R1,R2,R3 以及 R12 寄存器。

  • 当系统正在服务一个中断时,如果有一个更高优先级的中断触发,那么处理器同样的会打断当前运行的中断服务例程,然后把老的中断服务例程上下文的 PSR,R0,R1,R2,R3 和 R12 寄存器自动保存到中断栈中。

  • 这些部分上下文寄存器保存到中断栈的行为完全是硬件行为,这一点是与其他 ARM 处理器最大的区别(以往都需要依赖于软件保存上下文)。

  • 另外,在 ARM Cortex-M 系列处理器上,所有中断都采用中断向量表的方式进行处理,即当一个中断触发时,处理器将直接判定是哪个中断源,然后直接跳转到相应的固定位置进行处理。

  • 而在 ARM7、ARM9 中,一般是先跳转进入 IRQ 入口,然后再由软件进行判断是哪个中断源触发,获得了相对应的中断服务例程入口地址后,再进行后续的中断处理。

  • ARM7、ARM9 的好处在于,所有中断它们都有统一的入口地址,便于 OS 的统一管理。

  • 而ARM Cortex-M 系列处理器则恰恰相反,每个中断服务例程必须排列在一起放在统一的地址上(这个地址必须要设置到 NVIC 的中断向量偏移寄存器中)。

  • 中断向量表一般由一个数组定义(或在起始代码中给出),在 STM32 上,默认采用起始代码给出:具体详见中断向量表。

  • FreeRTOS 在 Cortex-M 系列处理器上也遵循与裸机中断一致的方法,当用户需要使用自定义的中断服务例程时,只需要定义相同名称的函数覆盖弱化符号即可。所以,FreeRTOS 在 Cortex-M 系列处理器的中断控制其实与裸机没什么差别。

11.6 中断管理实验

  • 中断管理实验是在 FreeRTOS 中创建了两个任务分别获取信号量与消息队列,并且定义了两个按键 KEY1 与 KEY2 的触发方式为中断触发,其触发的中断服务函数则跟裸机一样,在中断触发的时候通过消息队列将消息传递给任务,任务接收到消息就将信息通过串口调试助手显示出来。

  • 而且中断管理实验也实现了一个串口的 DMA 传输+空闲中断功能,当串口接收完不定长的数据之后产生一个空闲中断,在中断中将信号量传递给任务,任务在收到信号量的时候将串口的数据读取出来并且在串口调试助手中回显,具体见代码清单

  • *************************************************************************
    *                             包含的头文件
    *************************************************************************
    */ 
    /* FreeRTOS头文件 */
    #include "FreeRTOS.h"
    #include "task.h"
    #include "queue.h"
    #include "semphr.h"
    ​
    /* 开发板硬件bsp头文件 */
    #include "bsp_led.h"
    #include "bsp_debug_usart.h"
    #include "bsp_key.h"
    #include "bsp_exti.h"
    ​
    /* 标准库头文件 */
    #include <string.h>
    ​
    /**************************** 任务句柄 ********************************/
    /* 
     * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
     * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
     * 这个句柄可以为NULL。
     */
    static TaskHandle_t AppTaskCreate_Handle = NULL;/* 创建任务句柄 */
    static TaskHandle_t LED_Task_Handle = NULL;/* LED任务句柄 */
    static TaskHandle_t KEY_Task_Handle = NULL;/* KEY任务句柄 */
    ​
    /********************************** 内核对象句柄 *********************************/
    /*
     * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
     * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
     * 们就可以通过这个句柄操作这些内核对象。
     *
     * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
     * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
     * 来完成的
     * 
     */
    QueueHandle_t Test_Queue =NULL;
    SemaphoreHandle_t BinarySem_Handle =NULL;
    ​
    /******************************* 全局变量声明 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些全局变量。
     */
     
    extern char Usart_Rx_Buf[USART_RBUFF_SIZE];
     
     
    /******************************* 宏定义 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些宏定义。
     */
    #define  QUEUE_LEN    4   /* 队列的长度,最大可包含多少个消息 */
    #define  QUEUE_SIZE   4   /* 队列中每个消息大小(字节) */
    ​
    ​
    /*
    *************************************************************************
    *                             函数声明
    *************************************************************************
    */
    static void AppTaskCreate(void);/* 用于创建任务 */
    ​
    static void LED_Task(void* pvParameters);/* LED_Task任务实现 */
    static void KEY_Task(void* pvParameters);/* KEY_Task任务实现 */
    ​
    static void BSP_Init(void);/* 用于初始化板载相关资源 */
    ​
    /*****************************************************************
      * @brief  主函数
      * @param  无
      * @retval 无
      * @note   第一步:开发板硬件初始化 
                第二步:创建APP应用任务
                第三步:启动FreeRTOS,开始多任务调度
      ****************************************************************/
    int main(void)
    {   
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      /* 开发板硬件初始化 */
      BSP_Init();
      
        printf("这是一个[野火]-STM32全系列开发板-FreeRTOS中断管理实验!\n");
      printf("按下KEY1 | KEY2触发中断!\n");
      printf("串口发送数据触发中断,任务处理数据!\n");
      
       /* 创建AppTaskCreate任务 */
      xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                            (const char*    )"AppTaskCreate",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )1, /* 任务的优先级 */
                            (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
      /* 启动任务调度 */           
      if(pdPASS == xReturn)
        vTaskStartScheduler();   /* 启动任务,开启调度 */
      else
        return -1;  
    ​
      while(1);/* 正常不会执行到这里 */    
    }
    ​
    ​
    /***********************************************************************
      * @ 函数名  : AppTaskCreate
      * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
      * @ 参数    : 无  
      * @ 返回值  : 无
      **********************************************************************/
    static void AppTaskCreate(void)
    {
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      taskENTER_CRITICAL();           //进入临界区
      
      /* 创建Test_Queue */
      Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,/* 消息队列的长度 */
                                (UBaseType_t ) QUEUE_SIZE);/* 消息的大小 */
      
        if(NULL != Test_Queue)
        printf("Test_Queue消息队列创建成功!\n");
        
      /* 创建 BinarySem */
      BinarySem_Handle = xSemaphoreCreateBinary();   
      
        if(NULL != BinarySem_Handle)
        printf("BinarySem_Handle二值信号量创建成功!\n");
        
      /* 创建LED_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )LED_Task, /* 任务入口函数 */
                            (const char*    )"LED_Task",/* 任务名字 */
                            (uint16_t       )512,   /* 任务栈大小 */
                            (void*          )NULL,  /* 任务入口函数参数 */
                            (UBaseType_t    )2,     /* 任务的优先级 */
                            (TaskHandle_t*  )&LED_Task_Handle);/* 任务控制块指针 */
      if(pdPASS == xReturn)
        printf("创建LED_Task任务成功!\n");
      /* 创建KEY_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )KEY_Task,  /* 任务入口函数 */
                            (const char*    )"KEY_Task",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )3, /* 任务的优先级 */
                            (TaskHandle_t*  )&KEY_Task_Handle);/* 任务控制块指针 */ 
      if(pdPASS == xReturn)
        printf("创建KEY_Task任务成功!\n");
      
      vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
      
      taskEXIT_CRITICAL();            //退出临界区
    }
    ​
    ​
    ​
    /**********************************************************************
      * @ 函数名  : LED_Task
      * @ 功能说明: LED_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void LED_Task(void* parameter)
    {   
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      uint32_t r_queue; /* 定义一个接收消息的变量 */
      while (1)
      {
        /* 队列读取(接收),等待时间为一直等待 */
        xReturn = xQueueReceive( Test_Queue,    /* 消息队列的句柄 */
                                 &r_queue,      /* 发送的消息内容 */
                                 portMAX_DELAY); /* 等待时间 一直等 */
                                    
            if(pdPASS == xReturn)
            {
                printf("触发中断的是 KEY%d !\n",r_queue);
            }
            else
            {
                printf("数据接收出错\n");
            }
            
        LED1_TOGGLE;
      }
    }
    ​
    /**********************************************************************
      * @ 函数名  : LED_Task
      * @ 功能说明: LED_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void KEY_Task(void* parameter)
    {   
        BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      while (1)
      {
        //获取二值信号量 xSemaphore,没获取到则一直等待
            xReturn = xSemaphoreTake(BinarySem_Handle,/* 二值信号量句柄 */
                                  portMAX_DELAY); /* 等待时间 */
        if(pdPASS == xReturn)
        {
          LED2_TOGGLE;
          printf("收到数据:%s\n",Usart_Rx_Buf);
          memset(Usart_Rx_Buf,0,USART_RBUFF_SIZE);/* 清零 */
        }
      }
    }
    ​
    /***********************************************************************
      * @ 函数名  : BSP_Init
      * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
      * @ 参数    :   
      * @ 返回值  : 无
      *********************************************************************/
    static void BSP_Init(void)
    {
        /*
         * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
         * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
         * 都统一用这个优先级分组,千万不要再分组,切忌。
         */
        NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
        
        /* LED 初始化 */
        LED_GPIO_Config();
    ​
        /* DMA初始化   */
        USARTx_DMA_Config();
        
        /* 串口初始化    */
        Debug_USART_Config();
      
         /* 按键初始化   */
        Key_GPIO_Config();
        
        /* 按键初始化    */
        EXTI_Key_Config();
        
    }
    ​
    /********************************END OF FILE****************************/
    /*********************************************************************************
      * @ 函数名  : KEY1_IRQHandler
      * @ 功能说明: 中断服务函数
      * @ 参数    : 无  
      * @ 返回值  : 无
      ********************************************************************************/
    void KEY1_IRQHandler(void)
    {
        BaseType_t pxHigherPriorityTaskWoken;
      //确保是否产生了EXTI Line中断
      uint32_t ulReturn;
      /* 进入临界段,临界段可以嵌套 */
      ulReturn = taskENTER_CRITICAL_FROM_ISR();
      
        if(EXTI_GetITStatus(KEY1_INT_EXTI_LINE) != RESET) 
        {
        /* 将数据写入(发送)到队列中,等待时间为 0  */
            xQueueSendFromISR(Test_Queue, /* 消息队列的句柄 */
                                                &send_data1,/* 发送的消息内容 */
                                                &pxHigherPriorityTaskWoken);
            
            //如果需要的话进行一次任务切换
            portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
            
            //清除中断标志位
            EXTI_ClearITPendingBit(KEY1_INT_EXTI_LINE);     
        }  
      
      /* 退出临界段 */
      taskEXIT_CRITICAL_FROM_ISR( ulReturn );
    }
    ​
    /*********************************************************************************
      * @ 函数名  : KEY1_IRQHandler
      * @ 功能说明: 中断服务函数
      * @ 参数    : 无  
      * @ 返回值  : 无
      ********************************************************************************/
    void KEY2_IRQHandler(void)
    {
        BaseType_t pxHigherPriorityTaskWoken;
      uint32_t ulReturn;
      /* 进入临界段,临界段可以嵌套 */
      ulReturn = taskENTER_CRITICAL_FROM_ISR();
      
      //确保是否产生了EXTI Line中断
        if(EXTI_GetITStatus(KEY2_INT_EXTI_LINE) != RESET) 
        {
        /* 将数据写入(发送)到队列中,等待时间为 0  */
            xQueueSendFromISR(Test_Queue, /* 消息队列的句柄 */
                                                &send_data2,/* 发送的消息内容 */
                                                &pxHigherPriorityTaskWoken);
            
            //如果需要的话进行一次任务切换
            portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
            
            //清除中断标志位
            EXTI_ClearITPendingBit(KEY2_INT_EXTI_LINE);     
        }  
      
      /* 退出临界段 */
      taskEXIT_CRITICAL_FROM_ISR( ulReturn );
    }
    ​
    /*********************************************************************************
      * @ 函数名  : DEBUG_USART_IRQHandler
      * @ 功能说明: 串口中断服务函数
      * @ 参数    : 无  
      * @ 返回值  : 无
      ********************************************************************************/
    void DEBUG_USART_IRQHandler(void)
    {
      uint32_t ulReturn;
      /* 进入临界段,临界段可以嵌套 */
      ulReturn = taskENTER_CRITICAL_FROM_ISR();
    ​
        if(USART_GetITStatus(DEBUG_USART,USART_IT_IDLE)!=RESET)
        {       
            Uart_DMA_Rx_Data();       /* 释放一个信号量,表示数据已接收 */
            USART_ReceiveData(DEBUG_USART); /* 清除标志位 */
        }    
      
      /* 退出临界段 */
      taskEXIT_CRITICAL_FROM_ISR( ulReturn );
    }

12.CPU 使用率统计

12.1 CPU 利用率的基本概念

  • CPU 使用率其实就是系统运行的程序占用的 CPU 资源,表示机器在某段时间程序运行的情况,如果这段时间中,程序一直在占用 CPU 的使用权,那么可以人为 CPU 的利用率是 100%。

  • CPU 的利用率越高,说明机器在这个时间上运行了很多程序,反之较少。

  • 利用率的高低与 CPU 强弱有直接关系,就像一段一模一样的程序,如果使用运算速度很慢的CPU,它可能要运行 1000ms,而使用很运算速度很快的 CPU 可能只需要 10ms,那么在1000ms 这段时间中,前者的 CPU 利用率就是 100%,而后者的 CPU 利用率只有 1%,因为1000ms 内前者都在使用 CPU 做运算,而后者只使用 10ms 的时间做运算,剩下的时间CPU 可以做其他事情。

  • FreeRTOS 是多任务操作系统,对 CPU 都是分时使用的:比如 A 任务占用 10ms,然后 B 任务占用 30ms,然后空闲 60ms,再又是 A任务占 10ms,B 任务占 30ms,空闲 60ms;

  • 如果在一段时间内都是如此,那么这段时间内的利用率为 40%,因为整个系统中只有 40%

    的时间是 CPU 处理数据的时间。

12.2 CPU 利用率的作用

  • 一个系统设计的好坏,可以使用 CPU 使用率来衡量,一个好的系统必然是能完美响应急需的处理,并且系统的资源不会过于浪费(性价比高)。

  • 举个例子,假设一个系统的CPU 利用率经常在 90%~100%徘徊,那么系统就很少有空闲的时候,这时候突然有一些事情急需 CPU 的处理,但是此时 CPU 都很可能被其他任务在占用了,那么这个紧急事件就有可能无法被相应,即使能被相应,那么占用 CPU 的任务又处于等待状态,这种系统就是不够完美的,因为资源处理得太过于紧迫;反过来,假如 CPU 的利用率在 1%以下,那么我们就可以认为这种产品的资源过于浪费,搞一个那么好的 CPU 去干着没啥意义的活(大部分时间处于空闲状态),使用,作为产品的设计,既不能让资源过于浪费,也不能让资源过于紧迫,这种设计才是完美的,在需要的时候能及时处理完突发事件,而且资源也不会过剩,性价比更高。

12.3 CPU 利用率统计

  • FreeRTOS 是一个很完善很稳定的操作系统,当然也给我们提供测量各个任务占用CPU 时间的函数接口,我们可以知道系统中的每个任务占用 CPU 的时间,从而得知系统设计的是否合理,出于性能方面的考虑,有的时候,我们希望知道 CPU 的使用率为多少,进而判断此 CPU 的负载情况和对于当前运行环境是否能够“胜任工作”。所以,在调试的时候很有必要得到当前系统的 CPU 利用率相关信息,但是在产品发布的时候,就可以把CPU 利用率统计这个功能去掉,因为使用任何功能的时候,都是需要消耗系统资源的,FreeRTOS 是使用一个外部的变量进行统计时间的,并且消耗一个高精度的定时器,其用于定时的精度是系统时钟节拍的 10-20 倍,比如当前系统时钟节拍是 1000HZ,那么定时器的计数节拍就要是 10000-20000HZ。而且 FreeRTOS 进行 CPU 利用率统计的时候,也有一定缺陷,因为它没有对进行 CPU 利用率统计时间的变量做溢出保护,我们使用的是 32 位变量来系统运行的时间计数值,而按 20000HZ 的中断频率计算,每进入一中断就是 50us,变量加一,最大支持计数时间:2^32 * 50us / 3600s =59.6 分钟,运行时间超过了 59.6 分钟后统计的结果将不准确,除此之外整个系统一直响应定时器 50us 一次的中断会比较影响系统的性能。用户想要使用使用 CPU 利 用 率统 计 的 话 , 需 要 自 定 义 配 置 一 下 ,首 先 在FreeRTOSConfig.h 配置与系统运行时间和任务状态收集有关的配置选项,并且实现portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() 与portGET_RUN_TIME_COUNTER_VALUE()这两个宏定义,具体见代码清单

  • /********************************************************************
     FreeRTOS 与运行时间和任务状态收集有关的配置选项
     **********************************************************************/
    ​
    //启用运行时间统计功能 
    #define configGENERATE_RUN_TIME_STATS 1 
    ​
    //启用可视化跟踪调试 
    #define configUSE_TRACE_FACILITY 1 
    ​
    /* 与宏 configUSE_TRACE_FACILITY 同时为 1 时会编译下面 3 个函数
     * prvWriteNameToBuffer()
     * vTaskList(),
     * vTaskGetRunTimeStats()
     */
    #define configUSE_STATS_FORMATTING_FUNCTIONS 1
    ​
    extern volatile uint32_t CPU_RunTime; 
    ​
    #define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() (CPU_RunTime = 0ul) 
    #define portGET_RUN_TIME_COUNTER_VALUE() CPU_RunTime
  • 然后需要实现一个中断频率为 20000HZ 定时器,用于系统运行时间统计,其实很简单,只需将 CPU_RunTime 变量自加即可,这个变量是用于记录系统运行时间的,中断服务函数具体见代码清单。

  • /* 用于统计运行时间 */
    volatile uint32_t CPU_RunTime = 0UL;
    ​
    void BASIC_TIM_IRQHandler (void)
    {
        if ( TIM_GetITStatus( BASIC_TIM, TIM_IT_Update) != RESET ) {
            CPU_RunTime++; 
            TIM_ClearITPendingBit(BASIC_TIM , TIM_FLAG_Update);
        }
    }
  • 然后我们就可以在任务中调用 vTaskGetRunTimeStats()和 vTaskList()函数获得任务的相关信息与 CPU 使用率的相关信息,然后打印出来即可,具体见代码清单加粗部分。

  • 关于 vTaskGetRunTimeStats()和 vTaskList()函数的具体实现过程就不讲解了,有兴趣可以看看源码。

  • memset(CPU_RunInfo,0,400); //信息缓冲区清零
    ​
    vTaskList((char *)&CPU_RunInfo); //获取任务运行时间信息 
    ​
    printf("---------------------------------------------\r\n");
    printf("任务名 任务状态 优先级 剩余栈 任务序号\r\n");
    printf("%s", CPU_RunInfo);
    printf("---------------------------------------------\r\n");
    ​
    memset(CPU_RunInfo,0,400); //信息缓冲区清零
    ​
    vTaskGetRunTimeStats((char *)&CPU_RunInfo); 
    ​
    printf("任务名 运行计数 使用率\r\n");
    printf("%s", CPU_RunInfo);
    printf("---------------------------------------------\r\n\n");

12.4CPU 利用率统计实验

  • CPU 利用率实验是是在 FreeRTOS 中创建了三个任务,其中两个任务是普通任务,另一个任务用于获取 CPU 利用率与任务相关信息并通过串口打印出来。具体见代码清单。

  • /*
    *************************************************************************
    *                             包含的头文件
    *************************************************************************
    */ 
    /* FreeRTOS头文件 */
    #include "FreeRTOS.h"
    #include "task.h"
    /* 开发板硬件bsp头文件 */
    #include "bsp_led.h"
    #include "bsp_debug_usart.h"
    #include "./tim/bsp_basic_tim.h"
    #include "string.h"
    /**************************** 任务句柄 ********************************/
    /* 
     * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
     * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
     * 这个句柄可以为NULL。
     */
     /* 创建任务句柄 */
    static TaskHandle_t AppTaskCreate_Handle = NULL;
    /* LED任务句柄 */
    static TaskHandle_t LED1_Task_Handle = NULL;
    static TaskHandle_t LED2_Task_Handle = NULL;
    static TaskHandle_t CPU_Task_Handle = NULL;
    /********************************** 内核对象句柄 *********************************/
    /*
     * 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
     * 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
     * 们就可以通过这个句柄操作这些内核对象。
     *
     * 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
     * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
     * 来完成的
     * 
     */
    ​
    ​
    /******************************* 全局变量声明 ************************************/
    /*
     * 当我们在写应用程序的时候,可能需要用到一些全局变量。
     */
    ​
    ​
    /*
    *************************************************************************
    *                             函数声明
    *************************************************************************
    */
    static void AppTaskCreate(void);/* 用于创建任务 */
    ​
    static void LED1_Task(void* pvParameters);/* LED1_Task任务实现 */
    static void LED2_Task(void* pvParameters);/* LED2_Task任务实现 */
    static void CPU_Task(void* pvParameters);/* CPU_Task任务实现 */
    static void BSP_Init(void);/* 用于初始化板载相关资源 */
    ​
    /*****************************************************************
      * @brief  主函数
      * @param  无
      * @retval 无
      * @note   第一步:开发板硬件初始化 
                第二步:创建APP应用任务
                第三步:启动FreeRTOS,开始多任务调度
      ****************************************************************/
    int main(void)
    {   
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
    ​
      /* 开发板硬件初始化 */
      BSP_Init();
      printf("这是一个[野火]-STM32全系列开发板-FreeRTOS-CPU利用率统计实验!\r\n");
       /* 创建AppTaskCreate任务 */
      xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                            (const char*    )"AppTaskCreate",/* 任务名字 */
                            (uint16_t       )512,  /* 任务栈大小 */
                            (void*          )NULL,/* 任务入口函数参数 */
                            (UBaseType_t    )1, /* 任务的优先级 */
                            (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
      /* 启动任务调度 */           
      if(pdPASS == xReturn)
        vTaskStartScheduler();   /* 启动任务,开启调度 */
      else
        return -1;  
      
      while(1);   /* 正常不会执行到这里 */    
    }
    ​
    ​
    /***********************************************************************
      * @ 函数名  : AppTaskCreate
      * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
      * @ 参数    : 无  
      * @ 返回值  : 无
      **********************************************************************/
    static void AppTaskCreate(void)
    {
      BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
      
      taskENTER_CRITICAL();           //进入临界区
      
      /* 创建LED_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )LED1_Task, /* 任务入口函数 */
                            (const char*    )"LED1_Task",/* 任务名字 */
                            (uint16_t       )512,   /* 任务栈大小 */
                            (void*          )NULL,  /* 任务入口函数参数 */
                            (UBaseType_t    )2,     /* 任务的优先级 */
                            (TaskHandle_t*  )&LED1_Task_Handle);/* 任务控制块指针 */
      if(pdPASS == xReturn)
        printf("创建LED1_Task任务成功!\r\n");
      
      /* 创建LED_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )LED2_Task, /* 任务入口函数 */
                            (const char*    )"LED2_Task",/* 任务名字 */
                            (uint16_t       )512,   /* 任务栈大小 */
                            (void*          )NULL,  /* 任务入口函数参数 */
                            (UBaseType_t    )3,     /* 任务的优先级 */
                            (TaskHandle_t*  )&LED2_Task_Handle);/* 任务控制块指针 */
      if(pdPASS == xReturn)
        printf("创建LED2_Task任务成功!\r\n");
    ​
      /* 创建LED_Task任务 */
      xReturn = xTaskCreate((TaskFunction_t )CPU_Task, /* 任务入口函数 */
                            (const char*    )"CPU_Task",/* 任务名字 */
                            (uint16_t       )512,   /* 任务栈大小 */
                            (void*          )NULL,  /* 任务入口函数参数 */
                            (UBaseType_t    )4,     /* 任务的优先级 */
                            (TaskHandle_t*  )&CPU_Task_Handle);/* 任务控制块指针 */
      if(pdPASS == xReturn)
        printf("创建CPU_Task任务成功!\r\n");
      
      vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
      
      taskEXIT_CRITICAL();            //退出临界区
    }
    ​
    ​
    ​
    /**********************************************************************
      * @ 函数名  : LED_Task
      * @ 功能说明: LED_Task任务主体
      * @ 参数    :   
      * @ 返回值  : 无
      ********************************************************************/
    static void LED1_Task(void* parameter)
    {   
      while (1)
      {
        LED1_ON;
        vTaskDelay(500);   /* 延时500个tick */
        printf("LED1_Task Running,LED1_ON\r\n");
        LED1_OFF;     
        vTaskDelay(500);   /* 延时500个tick */             
        printf("LED1_Task Running,LED1_OFF\r\n");
    ​
      }
    }
    ​
    static void LED2_Task(void* parameter)
    {   
      while (1)
      {
        LED2_ON;
        vTaskDelay(500);   /* 延时500个tick */
        printf("LED2_Task Running,LED1_ON\r\n");
        
        LED2_OFF;     
        vTaskDelay(500);   /* 延时500个tick */             
        printf("LED2_Task Running,LED1_OFF\r\n");
      }
    }
    ​
    static void CPU_Task(void* parameter)
    {   
      uint8_t CPU_RunInfo[400];     //保存任务运行时间信息
      
      while (1)
      {
        memset(CPU_RunInfo,0,400);              //信息缓冲区清零
        
        vTaskList((char *)&CPU_RunInfo);  //获取任务运行时间信息
        
        printf("---------------------------------------------\r\n");
        printf("任务名      任务状态 优先级   剩余栈 任务序号\r\n");
        printf("%s", CPU_RunInfo);
        printf("---------------------------------------------\r\n");
        
        memset(CPU_RunInfo,0,400);              //信息缓冲区清零
        
        vTaskGetRunTimeStats((char *)&CPU_RunInfo);
        
        printf("任务名       运行计数         使用率\r\n");
        printf("%s", CPU_RunInfo);
        printf("---------------------------------------------\r\n\n");
        vTaskDelay(1000);   /* 延时500个tick */        
      }
    }
    ​
    /***********************************************************************
      * @ 函数名  : BSP_Init
      * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
      * @ 参数    :   
      * @ 返回值  : 无
      *********************************************************************/
    static void BSP_Init(void)
    {
        /*
         * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
         * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
         * 都统一用这个优先级分组,千万不要再分组,切忌。
         */
        NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
        
        /* LED 初始化 */
        LED_GPIO_Config();
    ​
        /* 串口初始化    */
        Debug_USART_Config();
      
      /* 基本定时器初始化   */
        TIMx_Configuration();
      
    }
    ​
    /********************************END OF FILE****************************/
    ​

 

### RT-DETRv3 网络结构分析 RT-DETRv3 是一种基于 Transformer 的实时端到端目标检测算法,其核心在于通过引入分层密集正监督方法以及一系列创新性的训练策略,解决了传统 DETR 模型收敛慢和解码器训练不足的问题。以下是 RT-DETRv3 的主要网络结构特点: #### 1. **基于 CNN 的辅助分支** 为了增强编码器的特征表示能力,RT-DETRv3 引入了一个基于卷积神经网络 (CNN) 的辅助分支[^3]。这一分支提供了密集的监督信号,能够与原始解码器协同工作,从而提升整体性能。 ```python class AuxiliaryBranch(nn.Module): def __init__(self, in_channels, out_channels): super(AuxiliaryBranch, self).__init__() self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1) self.bn = nn.BatchNorm2d(out_channels) def forward(self, x): return F.relu(self.bn(self.conv(x))) ``` 此部分的设计灵感来源于传统的 CNN 架构,例如 YOLO 系列中的 CSPNet 和 PAN 结构[^2],这些技术被用来优化特征提取效率并减少计算开销。 --- #### 2. **自注意力扰动学习策略** 为解决解码器训练不足的问题,RT-DETRv3 提出了一种名为 *self-att 扰动* 的新学习策略。这种策略通过对多个查询组中阳性样本的标签分配进行多样化处理,有效增加了阳例的数量,进而提高了模型的学习能力和泛化性能。 具体实现方式是在训练过程中动态调整注意力权重分布,确保更多的高质量查询可以与真实标注 (Ground Truth) 进行匹配。 --- #### 3. **共享权重解编码器分支** 除了上述改进外,RT-DETRv3 还引入了一个共享权重的解编码器分支,专门用于提供密集的正向监督信号。这一设计不仅简化了模型架构,还显著降低了参数量和推理时间,使其更适合实时应用需求。 ```python class SharedDecoderEncoder(nn.Module): def __init__(self, d_model, nhead, num_layers): super(SharedDecoderEncoder, self).__init__() decoder_layer = nn.TransformerDecoderLayer(d_model=d_model, nhead=nhead) self.decoder = nn.TransformerDecoder(decoder_layer, num_layers=num_layers) def forward(self, tgt, memory): return self.decoder(tgt=tgt, memory=memory) ``` 通过这种方式,RT-DETRv3 实现了高效的目标检测流程,在保持高精度的同时大幅缩短了推理延迟。 --- #### 4. **与其他模型的关系** 值得一提的是,RT-DETRv3 并未完全抛弃经典的 CNN 技术,而是将其与 Transformer 结合起来形成混合架构[^4]。例如,它采用了 YOLO 系列中的 RepNCSP 模块替代冗余的多尺度自注意力层,从而减少了不必要的计算负担。 此外,RT-DETRv3 还借鉴了 DETR 的一对一匹配策略,并在此基础上进行了优化,进一步提升了小目标检测的能力。 --- ### 总结 综上所述,RT-DETRv3 的网络结构主要包括以下几个关键组件:基于 CNN 的辅助分支、自注意力扰动学习策略、共享权重解编码器分支以及混合编码器设计。这些技术创新共同推动了实时目标检测领域的发展,使其在复杂场景下的表现更加出色。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值