基于STM32的实时操作系统FreeRTOS(待更新……)

摘要:RTOS很简单,听起来叫做实时操作系统,有一点吓唬人。但是学起来真的很简单,你不要把他想象的太复杂。这玩意其实就是一个任务调度器,在裸机中程序只有一个死循环,但是使用了RTOS程序中就有了多个死循环,RTOS就是调度每个死循环依次执行,执行的速度很快,看起来就相当于并行执行。

一、FreeRTOS的入门

学习一个RTOS,搞懂它的编程的风格很重要,这可以大大提供我们阅读代码的效率。下面我们就以FreeRTOS里面的数据类型、变量名、函数名和宏这几个方面做简单介绍。

1、数据类型

在FreeRTOS中,使用的数据类型虽然都是标准C里面的数据类型,但是针对不同的处理器,对标准C的数据类型又进行了重定义,给它们取了一个新的名字,比如char重新定义了一个名字 porCHAR,这里面的port表示接口的意思,就是FreeRTOS要移植到这些处理器上需要这些接口文件来把它们连接在一起。但是用户在写程序的时候并非一定要遵循 FreeRTOS的风格,我们还是可以直接用C语言的标准类型。在FreeRTOS中,int型从不使用,只使用short和long型。在Cortex-M内核的MCU中,short为16位,long为32位。
在这里插入图片描述

FreeRtOS中的数据类型重定义
/* Type definitions. */
#define portCHAR          char
#define portFLOAT         float
#define portDOUBLE        double
#define portLONG          long
#define portSHORT         short
#define portSTACK_TYPE    uint32_t
#define portBASE_TYPE     long

typedef portSTACK_TYPE   StackType_t;
typedef long             BaseType_t;
typedef unsigned long    UBaseType_t;

#if ( configUSE_16_BIT_TICKS == 1 )
    typedef uint16_t     TickType_t;
    #define portMAX_DELAY              ( TickType_t ) 0xffff
#else
    typedef uint32_t     TickType_t;
    #define portMAX_DELAY              ( TickType_t ) 0xffffffffUL

/* 32-bit tick type on a 32-bit architecture, so reads of the tick count do
* not need to be guarded with a critical section. */
    #define portTICK_TYPE_IS_ATOMIC    1
#endif

/*------------------------------------------------------*/

2、变量名

在FreeRTOS中,定义变量的时候往往会把变量的类型当作前缀加在变量上,这样的好处是让用户一看到这个变量就知道该变量的类型。比如char型变量的前缀是c,short型变量的前缀是s,long型变量的前缀是l,portBASE_TYPE类型变量的前缀是x。还有其他的数据类型,比如数据结构,任务句柄,队列句柄等定乂的变量名的前缀也是ⅹ。如果一个变量是无符号型的那么会有一个前缀u,如果是一个指针变量则会有一个前缀p。因此,当我们定义一个无符号的char型变量的时候会加一个u前缀,当定义一个char型的指针变量的时候会有一个pc前缀。

3、函数名

函数名包含了函数返回值的类型、函数所在的文件名和函数的功能,如果是私有的函数则会加一个prv(private)的前缀。特别的,在函数名中加入了函数所在的文件名,这大大的帮助了用户提高寻找函数定义的效率和了解函数作用的目的,具体的举例如下

  • vTaskPrioritySet()函数的返回值为void型,在task.c这个文件中定义。
  • xQueueReceive()函数的返回值为portBASE_TYPE型(long),在queue.c这个文件中定义。
  • vSmaphoreCreateBinary()函数的返回值为void型,在semper.h这个文件中定义。

4、宏

宏均是由大写字母表示,并配有小写字母的前缀,前缀用于表示该宏在哪个头文件定义,部分举例具体见下表请添加图片描述
这里有个地方要注意的是信号量的函数都是一个宏定义,但是它的函数的命名方法是遵循函数的命名方法而不是宏定义的方法。

在贯穿FreeRTOS的整个代码中,还有几个通用的宏定义我们也要注意下,都是表示0和1的宏请添加图片描述

5、格式

TAB键盘等于四个空格键。我们在编程的时候最好使用空格键而不是使用TAB键,当两个编译器的TAB键设置的大小不一样的时候,代码移植的时候代码的格式就会变乱,而使用空格键则不会出现这种问题。

6、函数

在文件作用域范围的函数前缀为 prv(一般定义是 static)
API 函数的前缀为它们的返回类型,当返回为空时,前缀为 v
返回值类型 + 所在文件 + 功能名称。比如:
vTaskDelete 该函数返回值为 void 型,定义在 tasks.c,作用是 delete。
vTaskPrioritySet()函数的返回值为 void 型,定义在 tasks.c,函数作用是PrioritySet 设置优先级。
xQueueReceive()函数的返回值为 portBASE_TYPE 型,在 queue.c 这个文件中定义,函数作用是 receive 接收。
vSemaphoreCreateBinary()函数的返回值为 void 型,在 Semaphore.h 这个文件中定义,函数作用是 CreateBinary。

RTOS的简单介绍

RTOS很简单,听起来叫做实时操作系统,有一点吓唬人。但是学起来真的很简单,你不要把他想象的太复杂。这玩意其实就是一个任务调度器,在裸机中程序只有一个死循环,但是使用了RTOS程序中就有了多个死循环,RTOS就是调度每个死循环依次执行,执行的速度很快,看起来就相当于并行执行。

RTOS的特点:

1、OS要做的事情很简单,核心就是任务管理(线程管理)在RTOS这种简单OS中,任务指的就是线程。

2、控制硬件时,应用代码直接调用底层代码,不经过OS的转换,RTOS只进行任务管理和存储管理

请添加图片描述
任务与进程、线程的关系:

在复杂OS中任务就是进程,但是在RTOS这种简单实时OS中不存在进程这个东西,有的只是线程,因此在RTOS中任务就是线程。操作系統是以进程为单位执行任务,进程与线程的关系:进程是线程的容器。
进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。
线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位。

我们生活中有许许多多关于进程与线程的小栗子,比如:1.我们使用打开一个微信软件,这个时候就开启了一个进程,
当我们在微信里面进行各种操作(查看朋友圈,扫一扫…),这么多的操作就是线程。
所以我们可以说“进程”是包含“线程”的,“线程”是“进程”的一个子集。
*

RTOS的本质

1、其实一个线程管理器:

RTOS其实就是一个线程管理器,所有的线程并发运行,RTOS的“任务管理”会负责线程的调度,因此RTOS就是一个简单的线程管理器,事实上线程的实现原理并不复杂。

2、RTOS线程的实现原理:

每一个线程本质就是函数,当该函数在没有被注册为线程时就是一个普通的函数,我们可以以普通函数的方式去调用,当注册为线程之后就是一个线程函数,线程的特定就是并发运行。

所谓并发运行就是某个线程函数运行了一段时间后就会切换运行其它线程,然再切换运行其它线程,然后再切换回原来的线程接着运行,由于每个线程运行的时间片很短,而且切换的速度又非常快,因此在宏观上我们会感觉到所有的线程都在同时运行,这就是并发运行。

从A线程切换运行其它线程时(vTaskStartScheduler()就是用来开启任务调度的),为了保证能够再一次切换回A线程上,切换时必须保存A线程被中断处的信息(TCB任务控制块来保存),然后才能通过中断信息返回,实际上我们调用普通函数时,为能够返回到调用处(中断处),也必须保存被调用中断处的信息,道理其实都是一样的。

请添加图片描述
定时器所定的时间片到后,PC就会指向下一个线程的中断处(或者最开始处),CPU开始切换运行该线程的指令,OS所用的定时器就是我们以前课程所介绍过的systick定时器,systick定时器就是专门用来给RTOS干这个事情的。

不过为了让任务能够处理实时性的事件,凡是处理实时性事件的高优先级任务,可以不等当前线程的时间片到而直接抢占CPU运行,总之高优先级的线程(任务)可以抢占低优先级的CPU资源,以保证能够实时的处理实时性事件。

CPU从当前线程切换运行其它线程时,到底切换到哪一个线程,以及高优先级线程如何实现抢占,这都是由RTOS的任务管理来实现的,所以说RTOS的本质其实就是一个线程(任务)管理器。

线程ID:
每个线程(任务)都有一个唯一识别号,这个识别号就是线程ID,任务管理器就是通过这个ID来识别和管理线程的。

二、FreeRTOS移植

1、下载 FreeRTOS 源码 FreeRTOS官网

在这里插入图片描述

点击下载
在这里插入图片描述
下载完成后解压缩
在这里插入图片描述

2、介绍FreeRTOS文件夹

在这里插入图片描述
Demo: 例程和内核源码
License: 这里面只有一个许可文件“license.txt”,用 FreeRTOS 做产品的话就需要看看这个文
件,但是我们是学习 FreeRTOS,所以暂时不需要理会这个文件。
Source: 文件夹里面包含的是 FreeRTOS 内核的源代码

3、正式移植:

普通的库函数版本工程
在这里插入图片描述
新建名称叫FreeRTOS文件夹
在这里插入图片描述
打开FreeRTOS文件夹,并在里面创建src文件夹
在这里插入图片描述
在刚刚下载的FreeRTOS源码文件夹下,FreeRTOS/Source 文件夹选中的部分全部复制到工程文件夹的FreeRTOS/src中
在这里插入图片描述
在这里插入图片描述
在工程文件夹下的FreeRTOS中建立port文件夹
在这里插入图片描述
将源码中FreeRTOS/Source中的include文件夹复制到工程文件夹FreeRTOS/port中,同时在源码FreeRTOS/Source/portable中的RVDS和MemMang文件夹复制到工程文件夹FreeRTOS/port中。
在这里插入图片描述
在源码 .\FreeRTOSv202112.00\FreeRTOS\Demo\CORTEX_STM32F103_Keil文件夹中,找到FreeRTOSConfig.h并复制到工程下的User文件夹。
在这里插入图片描述
在这里插入图片描述
用Keil5打开移植好的工程,创建工程文件夹FreeRTOS/src和FreeRTOS/port
在这里插入图片描述
在工程下的\FreeRTOS\src文件夹添加全部的.c文件
在工程下的\FreeRTOS\port\RVDS\ARM_CM3文件夹添加port.c
在工程下的\FreeRTOS\port\MemMang文件夹添加heap_4.c
在工程下的User下添加FreeRTOSConfig.h
在这里插入图片描述
最后指定头文件路径
在这里插入图片描述
最后编译一下,会看到出现一个错误
在这里插入图片描述
在错误报告中看到xTaskGetCurrentTaskHandle() 函数未定义,在FreeRTOS.h中搜索xTaskGetCurrentTaskHandle
在这里插入图片描述
在这里插入图片描述
看到206行的位置将0改为1即可,最后编译
在这里插入图片描述
完美通过!!!
在这里插入图片描述
最后工程移植好了,剩下的就是代码的事情了。

/* 板级外设头文件 */
#include "stm32f10x.h"
#include "sys.h"
#include "bsp_led.h"
#include "bsp_usart.h"
/* FreeRTOS 相关头文件 */
#include "FreeRTOS.h"
#include "task.h"

/* 任务句柄 */
static TaskHandle_t AppTaskCreate_Handle = NULL;
static TaskHandle_t LED1_Task_Handle = NULL;
static TaskHandle_t LED2_Task_Handle = NULL;

// 声明函数
static void LED1_Task(void *parameter);
static void LED2_Task(void *parameter);
static void AppTaskCreate(void);
static void BSP_Init(void);

int main()
{
    BaseType_t xReturn = pdPASS;	// 可以理解为任务ID
    BSP_Init();                     // 开发板硬件初始化
    printf("这是一个[野火]-STM32霸道开发板-FreeRTOS-动态创建多任务!\r\n");
    
    xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,           /* 任务入口函数 */
                          (const char*    )"AppTaskCreate",         /* 任务名字 */
                          (uint16_t       )512,                     /* 任务栈大小 */
                          (void*          )NULL,                    /* 任务入口函数参数 */
                          (UBaseType_t    )2,                       /* 任务的优先级 */
                          (TaskHandle_t*  )&AppTaskCreate_Handle);  /* 任务控制块指针 */
    if(pdPASS == xReturn)
        vTaskStartScheduler();      // 开启调度器
    else
        return -1;
    
    while(1);
}


static void AppTaskCreate(void)
{
    BaseType_t xReturn = pdPASS;    // 任务ID
    taskENTER_CRITICAL();           // 进入临界区
    
    /* 创建LED1_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");
    
    /* 创建LED2_Task任务 */
    xReturn = xTaskCreate((TaskFunction_t )LED2_Task,           /* 任务入口函数 */
                          (const char*    )"LED2_Task",         /* 任务名字 */
                          (uint16_t       )512,                 /* 任务栈大小 */
                          (void*          )NULL,                /* 任务入口函数参数 */
                          (UBaseType_t    )2,                   /* 任务的优先级 */
                          (TaskHandle_t*  )&LED2_Task_Handle);  /* 任务控制块指针 */
    if(pdPASS == xReturn)
        printf("创建LED1_Task任务成功!\r\n");
    
    vTaskDelete(AppTaskCreate_Handle);        // 删除AppTaskCreate任务
    taskEXIT_CRITICAL();                      // 退出临界区
}


static void LED1_Task(void *parameter)
{
    while(1)
    {
        LED1_ON;            // LED1 高电平
        vTaskDelay(500);    // 阻塞500ms
        printf("LED_Task Running,LED1_ON\r\n");
        LED1_OFF;           // LED1 低电平
        vTaskDelay(500);    // 阻塞500ms
        printf("LED_Task Running,LED1_OFF\r\n");
    }
}    

static void LED2_Task(void *parameter)
{
    while(1)
    {
        LED2_ON;            // LED1 高电平
        vTaskDelay(500);    // 阻塞500ms
        printf("LED_Task Running,LED2_ON\r\n");
        LED2_OFF;           // LED1 低电平
        vTaskDelay(500);    // 阻塞500ms
        printf("LED_Task Running,LED2_OFF\r\n");
    }
}

static void BSP_Init(void)
{
    /*
     * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
     * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
     * 都统一用这个优先级分组,千万不要再分组,切忌。
     */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
    
    // LED初始化
    LED_GPIO_Config();
    
    // 串口初始化
    USART_Config();
}

三、任务

1、基本概念

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

每个任务都是在自己权限范围内的一个小程序。其具有程序入口,通常会运行在一个死循环中,也不会退出。FreeRTOS 任务不允许以任何方式从实现函数中返回——它们绝不能有一条”return”语句,也不能执行到函数末尾。如果一个任务不再需要,可以显式地将其删除。

FreeRTOS 中的任务是抢占式调度机制,高优先级的任务可打断低优先级任务,低优先
级任务必须在高优先级任务阻塞或结束后才能得到调度。同时 FreeRTOS 也支持时间片轮
转调度方式,只不过时间片的调度是不允许抢占任务的 CPU 使用权。

什么是任务,在 FreeRTOS 中,任务就是一个函数

void Task1(void *parameter)
{
	while(1)
	{
		/* 任务主体 */
		vTaskDelay(20);		// 阻塞函数,阻塞20ms
	}
}

注意:

  • 每个任务都是独立于其它任务运行,互不影响
  • 这个函数不能有返回值
  • 同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数。例如上面例程的AppTaskCreate() 函数。
  • 函数内部,尽量使用局部变量:
  • 每个 FreeRTOS 任务都需要有自己的栈空间,任务越多需要的RAM空间就越大
  • 任务与任务之间,任务与中断之间是可以通信的
  • 如果一个任务不再需要,需要调用FreeRTOS 中的任务删除 API 函数接口将其删除,并释放内存。
  • 任务的返回必须是void,而且带有一个void指针参数

2、创建任务

BaseType_t xTaskCreate(TaskFunction_t pxTaskCode,                   // 函数指针, 任务函数
                       const char * const pcName,                   // 任务的名字
                       const configSTACK_DEPTH_TYPE usStackDepth,   // 栈大小,单位为 word,10 表示40 字节
                       void * const pvParameters,                   // 调用任务函数时传入的参数
                       UBaseType_t uxPriority,                      // 优先级
                       TaskHandle_t * const pxCreatedTask );        // 任务句柄, 以后使用它来操作这个任务

参数说明:

参数描述
pvTaskCode函数指针,可以简单地认为任务就是一个 C 函数。它稍微特殊一点:永远不退出,或者退出时要调用"vTaskDelete(NULL)"
pcName任务的名字,FreeRTOS 内部不使用它,仅仅起调试作用。长度为:configMAX_TASK_NAME_LEN
usStackDepth每个任务都有自己的栈,这里指定栈大小。单位是 word,比如传入 100,表示栈大小为 100 word,也就是 400 字节。最大值为 uint16_t 的最大值。怎么确定栈的大小,并不容易,很多时候是估计。精确的办法是看反汇编码。
pvParameters调用 pvTaskCode 函数指针时用到:pvTaskCode(pvParameters)
uxPriority优先级范围:0~(configMAX_PRIORITIES – 1)数值越小优先级越低,如果传入过大的值,xTaskCreate 会把它调整为(configMAX_PRIORITIES –1)
pxCreatedTask用来保存 xTaskCreate 的输出结果:task handle。以后如果想操作这个任务,比如修改它的优先级,就需要这个 handle。如果不想使用该 handle,可以传入 NULL。
返回值成功:pdPASS;失败:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足)注意:文档里都说失败时返回值是 pdFAIL,这不对。pdFAIL 是 0,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 是-1。

示例:

// 任务1:
void Task1(void *parameter)
{
	const char *pcTaskName = "T1 run!\n";
	volatile uint32_t ul;		// volatile 用来避免被优化掉
	
	while(1)
	{
		printf(pcTaskName);
		vTaskDelay(1000);		// 阻塞1000ms
	}
}
// 任务2:
void Task2(void *parameter)
{
	const char *pcTaskName = "T2 run!\n";
	volatile uint32_t ul;		// volatile 用来避免被优化掉
	
	while(1)
	{
		printf(pcTaskName);
		vTaskDelay(1000);		// 阻塞1000ms
	}
}

main() 函数

int main()
{
    BSP_Init();
    
	xTaskCreate(vTask1, "Task 1", 512, NULL, 1, NULL);
	xTaskCreate(vTask2, "Task 2", 512, NULL, 1, NULL);
	
	/* 启动调度器 */
	vTaskStartScheduler();
	
	
	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	while(1);
}

运行结果:
在这里插入图片描述
注意:

  • task2 先运行!
  • 在main函数中第一步创建了task1任务,task1进入就绪态。然后创建task2任务,task2进入就绪态。之后启动调度器,因为task1和task2是相同优先级所以按照顺序来执行,在任务栈中task1在下面,task2在上面,所以根据栈的后进先出规则调度器会先执行task2任务
    在这里插入图片描述

3、删除任务

void vTaskDelete( TaskHandle_t xTaskToDelete );

参数说明:

参数描述
pvTaskCode任务句柄,使用 xTaskCreate 创建任务时可以得到一个句柄。也可传入 NULL,这表示删除自己。

怎么删除任务?举个不好的例子:

  • 自杀:vTaskDelete(NULL)
  • 被杀:别的任务执行 vTaskDelete(pvTaskCode),pvTaskCode 是自己的句柄
  • 杀人:执行 vTaskDelete(pvTaskCode),pvTaskCode 是别的任务的句柄

注意:

  1. 任务可以使用 API 函数 vTaskDelete()删除自己或其它任务。 任务被删除后就不复存在,也不会再进入运行态。
  2. 空闲任务的责任是要将分配给已删除任务的内存释放掉。因此有一点很重要,那就是使用vTaskDelete() API函数的任务千万不能把空闲任务的执行时间饿死。
  3. 需要说明一点,只有内核为任务分配的内存空间才会在任务被删除后自动回收。任务自己占用的内存或资源需要由应用程序自己显式地释放。

例程:
 创建任务 1:任务 1 的大循环里,创建任务 2,然后休眠一段时间
 任务 2:打印一句话,然后就删除自己

// task1
void vTask1( void *pvParameters )
{
	const TickType_t xDelay100ms = pdMS_TO_TICKS(100UL);
	BaseType_t ret;
	/* 任务函数的主体一般都是无限循环 */
	while(1)
	{
		/* 打印任务的信息 */
		printf("Task1 is running\r\n");
		ret = xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
		if (ret != pdPASS)
		printf("Create Task2 Failed\r\n");
		// 如果不休眠的话, Idle 任务无法得到执行
		// Idel 任务会清理任务 2 使用的内存
		// 如果不休眠则 Idle 任务无法执行, 最后内存耗尽
		vTaskDelay( xDelay100ms );
}
// task2
void vTask2( void *pvParameters ) 
{
	while(1)
	{
		/* 打印任务的信息 */
		printf("Task2 is running and about to delete itself\r\n");
		// 可以直接传入参数 NULL, 这里只是为了演示函数用法
		vTaskDelete(xTask2Handle);
		vTaskDelay(2);	// 阻塞2ms
	}
}
// main
int main( void ) 
{
	BSP_Init();
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
	/* 启动调度器 */
	vTaskStartScheduler();
	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	while(1);
}

在这里插入图片描述
注意:

  1. 在任务 1 的函数中,如果不调用 vTaskDelay,则空闲任务用于没有机会执行,它就无法释放创建任务 2 是分配的内存。
  2. 而任务 1 在不断地创建任务,不断地消耗内存,最终内存耗尽再也无法创建新的任务。

4、任务优先级

  • FreeRTOS 会确保最高优先级的、可运行的任务,马上就能执行。对于相同优先级的、可运行的任务,轮流执行,这无需记忆
  • 优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。configMAX_PRIORITIES在文件FreeRTOSConfig.h中有定义
  • 最低优先级的是空闲任务,空闲任务是不可被删除、挂起的。空闲任务主要负责释放已被删除任务的内存。
  • configMAX_PRIORITIES 的取值理论上可以随便取,但是现实中RAM内存是有限的,最好不能超过 32。

一句话:

  • FreeRTOS 会确保最高优先级的、可运行的任务,马上就能执行
  • 对于相同优先级的、可运行的任务,轮流执行

使用 uxTaskPriorityGet 来获得任务的优先级:

UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );
参数:xTask 如果为NULL表示获取自己当前任务的优先级

使用 vTaskPrioritySet 来设置任务的优先级:

void vTaskPrioritySet(TaskHandle_t xTask, UBaseType_t uxNewPriority);
参数:
xTask:为NULL时表示设置自己当前的任务优先级
uxNewPriority:取值范围是 0~(configMAX_PRIORITIES – 1)

5、任务的状态

FreeRTOS中的任务永远处于下面几个状态中的某一个:

  1. 运行态:当一个任务正在运行时,那么就说这个任务处于运行态,处于运行态的任务就是当前正在使用处理器的任务。如果使用的是单核处理器的话那么不管在任何时刻永远都只有一个任务处于运行态。
  2. 就绪态:处于就绪态的任务是那些已经准备就绪(这些任务没有被阻塞或者挂起),可以运行的任务,但是处于就绪态的任务还没有运行,因为有一个同优先级或者更高优先级的任务正在运行!
  3. 阻塞态:如果一个任务当前正在等待某个外部事件的话就说它处于阻塞态,比如说如果某个任务调用了函数
    vTaskDelay()的话就会进入阻塞态,直到延时周期完成。任务在等待队列、信号量、事件组、通知或互斥信号量的时候也会进入阻塞态。任务进入阻塞态会有一个超时时间,当超过这个超时时间任务就会退出阻塞态,即使所等待的事件还没有来临!
  4. 挂起态:像阻塞态一样,任务进入挂起态以后也不能被调度器调用进入运行态,但是进入挂起态的任务没有超时时间。任务进入和退出挂起态通过调用函数VTaskSuspendo和xTask_Resumed

在这里插入图片描述

结合上图:

  • 创建任务→就绪态(Ready):任务创建完成后进入就绪态,表明任务已准备就绪,随时可以运行,只等待调度器进行调度。
  • 就绪态→运行态(Running):发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运行态。
  • 运行态→就绪态:有更高优先级任务创建或者恢复后,会发生任务调度,刻就绪列表中最高优先级任务变为运行态,那么原先运行的任务由运行态变为就绪态,依然在就绪列表中,等待最高优先级的任务运行完毕继续运行原来的任务(此处可以看做是CPU 使用权被更高优先级的任务抢占了)。
  • 运行态→阻塞态(Blocked):正在运行的任务发生阻塞(挂起、延时、读信号量等待)时,该任务会从就绪列表中删除,任务状态由运行态变成阻塞态,然后发生任务切换,运行就绪列表中当前最高优先级任务。
  • 阻塞态→就绪态:阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态;如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。
  • 就绪态、阻塞态、运行态→挂起态(Suspended):任务可以通过调用 vTaskSuspend() API函数都可以将处于任何状态的任务挂起,被挂起的任务得不到CPU 的使用权,也不会参与调度,除非它从挂起态中解除。
  • 挂起态→就绪态:把 一 个 挂 起 状态 的 任 务 恢复的 唯 一 途 径 就 是调用 vTaskResume() 或
    vTaskResumeFromISR() API函数,如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。

阻塞态:

vTaskDelay():		至少等待指定个数的 Tick Interrupt 才能变为就绪状态,叫相对延时
vTaskDelayUntil():	等待到指定的绝对时刻,才能变为就绪态,叫绝对延时
通过阻塞实现同步事件的来源:
 队列					(queue)
 二进制信号量			(binary semaphores)
 计数信号量				(counting semaphores)
 互斥量					(mutexes)
 递归互斥量、递归锁		(recursive mutexes)
 事件组					(event groups)
 任务通知				(task notifications)

6、挂起任务

有时候我们需要暂停某个任务的运行,过一段时间以后在重新运行。这个时候要是使用任务删除和重建的方法的话那么任务中变量保存的值肯定丢失了!

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

void vTaskSuspend( TaskHandle_t xTaskToSuspend )
参数描述
xTaskToSuspend挂起指定任务的任务句柄,任务必须为已创建的任务,可以通过传递 NULL 来挂起任务自己。

挂起所有任务的函数

void vTaskSuspendAll( void )
参数描述
void挂起所有任务实际上就是将调度器锁定,调度器被挂起后则不能进行上下文切换,但是中断还是使能的。 当调度器被挂起的时候,如果有中断需要进行上下文切换, 那么这个任务将会被挂起,在调度器恢复之后才执行切换任务。 调度器恢复可以调 用 xTaskResumeAll() 函数,调 用了多少次 的 vTaskSuspendAll() 就 要调用多少 次xTaskResumeAll()进行恢复

注意:
无论任务是什么状态都可以被挂起,只要调用了 vTaskSuspend()这个函数就会挂起成功,不论是挂起其他任务还是挂起任务自身。
挂起任务之前是什么状态,都会被系统保留下来,在恢复的瞬间,继续执行。

7、恢复任务

如果想要使用任务恢复函数 vTaskResume()则必须将宏定义INCLUDE_vTaskSuspend 配置为 1

void vTaskResume( TaskHandle_t xTaskToResume )
参数 :描述
xTaskToResume恢复指定任务的任务句柄。恢复的任务进入就绪态,如果任务是最高优先级则进入运行态,这一切都是由调度器来决定

注意:

  • 因为任务挂起只能通过调用 vTaskSuspend()函数进行挂起 ,没挂起的任务无需恢复
  • 需要恢复的任务必须存在,如果不存在,调用恢复任务函数没有任何意义。
  • 无论任务在挂起时候调用过多少次这个vTaskSuspend()函数,也只需调用一次 vTaskResume ()函数即可将任务恢复运行
  • 无论调用多少次vTaskResume() 函数,也只在任务是挂起态的时候才进行恢复

8、调度器

基本概念:
FreeRTOS 中提供的任务调度器是基于优先级的全抢占式调度:在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的。在系统中,当有比当前任务优先级更高的任务就绪时,当前任务将立刻被换出,高优先级任务抢占处理器运行。
在这里插入图片描述

四、同步互斥与通信

团队成员之间要协调工作进度(同步)、争用会议室(互斥)、沟通(通信)。

再举一个例子。在团队活动里,同事 A 先写完报表,经理 B 才能拿去向领导汇报。经理 B 必须等同事 A 完成报表,AB 之间有依赖,B 必须放慢脚步,被称为同步。在团队活动中,同事 A 已经使用会议室了,经理 B 也想使用,即使经理 B 是领导,他也得等着,这就叫互斥。经理 B 跟同事 A 说:你用完会议室就提醒我。这就是使用"同步"来实现"互斥"。

能实现同步、互斥的内核方法有:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)。

它们都有类似的操作方法:获取/释放、阻塞/唤醒、超时。比如:
 A 获取资源,用完后 A 释放资源
 A 获取不到资源则阻塞,B 释放资源并把 A 唤醒
 A 获取不到资源则阻塞,并定个闹钟;A 要么超时返回,要么在这段时间内因为 B 释放资源而被唤醒。

在这里插入图片描述
队列:
 里面可以放任意数据,可以放多个数据
 任务、ISR 都可以放入数据;任务、ISR 都可以从中读出数据
事件组:
 一个事件用一 bit 表示,1 表示事件发生了,0 表示事件没发生
 可以用来表示事件、事件的组合发生了,不能传递数据
 有广播效果:事件或事件的组合发生了,等待它的多个任务都会被唤醒
信号量:
 核心是"计数值"
 任务、ISR 释放信号量时让计数值加 1
 任务、ISR 获得信号量时,让计数值减 1
任务通知:
 核心是任务的 TCB 里的数值
 会被覆盖
 发通知给谁?必须指定接收任务
 只能由接收任务本身获取该通知
互斥量:
 数值只有 0 或 1
 谁获得互斥量,就必须由谁释放同一个互斥量

在这里插入图片描述

五、消息队列

1、基本概念

队列又称消息队列,是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任务间传递信息,实现了任务接收来自其他任务或中断的不固定长度的消息,任务能够从队列里面读取消息,当队列中的消息是空时,读取消息的任务将被阻塞,用户还可以指定阻塞的任务时间 xTicksToWait,在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当队列中有新消息时,被阻塞的任务会被唤醒并处理新消息;当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转为就绪态。消息队列是一种异步的通信方式。

特性:

  • 数据存储,队列可以保存有限个具有确定长度的数据单元。队列可以保存的最大单元数目被称为队列的“深度”。在队列创建时需要设定其深度和每个单元的大小。
  • 可被多任务存取,队列是具有自己独立权限的内核对象,并不属于或赋予任何任务。所有任务都可以向同一队列写入和读出。一个队列由多方写入是经常的事,但由多方读出倒是很少遇到。
  • 读队列时阻塞,当某个任务试图读一个队列时,其可以指定一个阻塞超时时间。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务例程往其等待的队列中写入了数据,该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。
  • 44
    点赞
  • 211
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值