前言:在本专栏《FreeRTOS》中已经为读者朋友详细介绍了FreeRTOS以及关于FreeRTOS于STM32下的手动移植。从今天开始将带领大家系统学习FreeRTOS,这款常见的轻量化小型实时操作系统。当然,考虑到FreeRTOS并不局限于STM32这一款MCU,后续文章的实验也可能使用其他MCU。言归正传,本文将从较为简单的任务创建开始学习。

一、什么是任务

        在裸机系统中,系统的主体就是 main 函数里面顺序执行的无限循环,这个无限循环里面 CPU 按照顺序完成各种事情。在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务。

任务的大概形式具体代码如下所示:

void task_entry (void *parg)
{
    /* 任务主体,无限循环且不能返回 */
    for (;;)
    {
        /* 任务主体代码 */
    }
}

二、任务的实现过程

2.1 定义任务栈

        在裸机系统中,他们统统放在一个叫栈的地方,栈是单片机 RAM 里面一段连续的内存空间,栈的大小一般在启动文件(STM32中的start_up文件中)或者链接脚本里面指定,最后由 C 库函数 main 进行初始化。        

        但是,在多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于 RAM 中

补补充知识——堆与栈的区别(常见面试题):

堆(Heap)与栈(Stack)是开发人员必须面对的两个概念,在理解这两个概念时,需要放到具体的场景下,因为不同场景下,堆与栈代表不同的含义。一般情况下,有两层含义:
(1)程序内存布局场景下,堆与栈表示两种内存管理方式;
(2)数据结构场景下,堆与栈表示两种常用的数据结构。

        例如本小节我们要实现两个 LED 按照一定的频率轮流的翻转,每个 LED 对应一个任务,那么就需要定义两个任务栈,具体代码如下所示。在多任务系统中,有多少个任务就需要定义多少个任务栈。

#define TASK1_STACK_SIZE 128
StackType_t Task1Stack[TASK1_STACK_SIZE];
 
#define TASK2_STACK_SIZE 128
StackType_t Task2Stack[TASK2_STACK_SIZE];

任务栈其实就是一个预先定义好的全局数组,数据类型为StackType_t,大小由TASK1_STACK_SIZE 这个宏来定义,默认为 128,单位为字,即 512 字节,这也是 FreeRTOS 推荐的最小的任务栈

在 FreeRTOS 中,凡是涉及到数据类型的地方,FreeRTOS 都会将标准的 C 数据类型用 typedef重新取一个类型名。这些经过重定义的数据类型放在 portmacro.h 这个头文件。

2.2 定义任务函数

        任务是一个独立的函数,函数主体无限循环且不能返回。本章我们在 main.c中定义的两个任务具体代码如下所示:

//任务 1 函数
void task1_task(void *pvParameters)
{
    while(1)
    {
        LED1=0;
        vTaskDelay(200);
        LED1=1;
        vTaskDelay(800);
    }
}
//任务 2 函数
void task2_task(void *pvParameters)
{
    while(1)
    {
        LED2=0;
        vTaskDelay(800);
        LED2=1;
        vTaskDelay(200);
    }
}

特别说明:任务里面的延时函数必须使用 FreeRTOS 里面提供的延时函数,并不能使用我们裸机编程中的那种延时。这两种的延时的区别是 FreeRTOS 里面的延时是阻塞延时,即调用 vTaskDelay()函数的时候,当前任务会被挂起,调度器会切换到其它就绪的任务,从而实现多任务。。

FreeRTOS 任务的状态:

(1)运行态

        当一个任务正在运行时, 那么就说这个任务处于运行态, 处于运行态的任务就是当前正在使用处理器的任务。 如果使用的是单核处理器的话那么不管在任何时刻永远都只有一个任务处于运行态。

(2)就绪态

        处于就绪态的任务是那些已经准备就绪(这些任务没有被阻塞或者挂起), 可以运行的任务,但是处于就绪态的任务还没有运行,因为有一个同优先级或者更高优先级的任务正在运行!

(3)阻塞态

        如果一个任务当前正在等待某个外部事件的话就说它处于阻塞态, 比如说如果某个任务调用了函数 vTaskDelay()的话就会进入阻塞态, 直到延时周期完成。任务在等待队列、信号量、事件组、通知或互斥信号量的时候也会进入阻塞态。任务进入阻塞态会有一个超时时间,当超过这个超时时间任务就会退出阻塞态,即使所等待的事件还没有来临!

(4)挂起态

        像阻塞态一样,任务进入挂起态以后也不能被调度器调用进入运行态, 但是进入挂起态的任务没有超时时间。任务进入和退出挂起态通过调用函数 vTaskSuspend()和xTaskResume()。

总结:FreeRTOS利用这些状态使任务更加灵活的运行,更加节约CPU资源,实时性更强。这些状态都可以由相对应的API函数来控制任务变成哪种状态!!!

2.3 定义任务控制块

        在裸机系统中,程序的主体是 CPU 按照顺序执行的。而在多任务系统中,任务的执行是由系统调度的。系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块,这个任务控制块(TCB)就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈指针,任务名称,任务的形参等。有了这个任务控制块之后,以后系统对任务的全部操作都可以通过这个任务控制块来实现。定义一个任务控制块需要一个新的数据类型,该数据类型在 task.c 这 C 文件中声明,具体代码如下所示,使用它可以为每个任务都定义一个任务控制块实体。

typedef struct tskTaskControlBlock
{
    volatile StackType_t *pxTopOfStack; /* 栈顶 */
    ListItem_t xStateListItem; /* 任务节点 */
    StackType_t *pxStack; /* 任务栈起始地址 */
    /* 任务名称,字符串形式 */(4)
    char pcTaskName[ configMAX_TASK_NAME_LEN ];
} tskTCB;
typedef tskTCB TCB_t;

       我们在 main.c 文件中为两个任务定义的任务控制块,具体 代码如下所示:

StaticTask_t Task1TaskTCB;
StaticTask_t Task2TaskTCB;

三、 FreeRTOS 的任务

        FreeRTOS 最基本的功能就是任务管理,而任务管理最基本的操作就是创建和删除任务,官方都给我们提供的相对应的API函数来创建和删除任务。 

函数 描述
xTaskCreate()

使用动态的方法创建一个任务。

xTaskCreateStatic() 使用静态的方法创建一个任务。
xTaskCreateRestricted() 创建一个使用 MPU 进行限制的任务,相关内存使用动态内存分配。
vTaskDelete() 删除一个任务。