🍀作者:阿润菜菜
目录
- 一、通过故事介绍FreeRTOS
- 1.什么是FreeRTOS?
- 2.FreeRTOS能做什么?
- 二、如何使用FreeRTOS? --- 第1个FreeRTOS程序
- 三、FreeRTOS的堆和栈
- 1.堆和栈的概念
- 2.堆和栈的分配方式
- 3.堆和栈的溢出检测
- 四、创建任务函数及任务管理
一、通过故事介绍FreeRTOS
假如你是一位母亲是不是会经常遇到这样的情况:你要一边给小孩喂饭,一边加班跟同事微信交流,但是你无法一心多用,只能不停地切换注意力,导致效率低下,还容易出错?
如果你是一个软件开发者,你可能会想:有没有什么办法可以让我像电脑一样,可以同时运行多个任务,而不影响彼此的执行呢?
答案是:有!那就是使用操作系统(OS)。
操作系统是一种软件,它可以管理和调度多个程序或者任务(task)的运行,让它们看起来像是同时执行一样。操作系统可以根据任务的优先级、时间片、事件等因素来决定哪个任务应该先运行,哪个任务应该后运行,或者哪个任务应该暂停或者继续运行。
操作系统有很多种类,例如我们常用的Windows、Linux、Mac OS等,它们被称为通用操作系统(general-purpose OS),因为它们可以运行在各种类型的计算机上,并且支持各种类型的应用程序。
但是在一些专用的电子设备中,例如电梯、汽车、飞机、医疗仪器等,通用操作系统就不太适合了。因为这些设备通常使用的是微控制器(microcontroller)或者小型微处理器(microprocessor),它们的内存和处理能力都很有限。而且这些设备对于实时性(real-time)有很高的要求,也就是说它们必须在规定的时间内完成指定的任务,否则就会造成严重的后果。
例如,在电梯系统中,你按住开门键时如果没有即刻反应,即使只是慢个1
秒,也会夹住人。
为了满足这些设备的需求,就出现了一种特殊的操作系统:实时操作系统(real-time operating system,RTOS)。RTOS是一种为实时应用设计的操作系统,它可以在有限的资源下保证任务的及时响应和正确执行。
今天我们要介绍的就是一种开源的、免费的、广泛使用的RTOS:FreeRTOS。
1.什么是FreeRTOS?
FreeRTOS是一个实时操作系统内核(kernel),它可以在多种微控制器和处理器上运行,提供了丰富的任务调度、同步、通信、内存管理等功能。FreeRTOS是开源的,可以免费使用,也可以根据需要进行修改和定制。
那FreeRTOS和Linux的区别是什么?
1.FreeRTOS中没有进程和线程的区分:
FreeRTOS中的任务(Task)和线程(Thread)是相同的概念,每个任务就是一个线程,有着自己的一个程序函数。FreeRTOS可以创建、删除、挂起、恢复、优先级设置等多个任务,任务之间可以通过任务调度器根据优先级进行切换。
2.FreeRTOS和Linux都是操作系统,但是有很多区别,主要有以下几点:
- FreeRTOS是一个实时操作系统(RTOS),它要求快速地处理任务,保证实时性和可靠性。Linux是一个通用操作系统(GPOS),它要求提供丰富的功能和服务,保证用户体验和兼容性。
- FreeRTOS是一个迷你的操作系统内核,只包含了基本的功能,如任务管理、时间管理、信号量、消息队列、内存管理等。Linux是一个完整的操作系统,包含了内核和用户空间,有很多组件和模块,如文件系统、网络协议栈、设备驱动、图形界面、shell等。
- FreeRTOS可以在资源有限的微控制器中运行,占用的内存和存储空间很小。Linux需要较多的资源,一般运行在处理器性能较强的平台上。
- FreeRTOS主要用于嵌入式领域,如工业控制、物联网、智能家居等。Linux主要用于桌面、服务器、移动设备等领域。
2.FreeRTOS能做什么?
-
FreeRTOS可以让你在微控制器或者小型微处理器上实现多任务的并发执行,从而提高你的系统的性能和效率。
-
FreeRTOS可以让你根据任务的优先级、时间片、事件等因素来灵活地调度任务的运行,从而保证你的系统的实时性和正确性。
-
FreeRTOS可以让你使用队列、信号量、互斥锁、事件组等机制来实现任务之间的同步和通信,从而保证你的系统的稳定性和可靠性。
-
FreeRTOS可以让你使用静态或者动态的方式来分配任务的内存空间,从而保证你的系统的灵活性和可扩展性。
-
FreeRTOS可以让你使用各种工具和方法来检测和调试你的系统,例如栈溢出检测、断言、跟踪分析等,从而保证你的系统的质量和安全性。
二、如何使用FreeRTOS? — 第1个FreeRTOS程序
要使用FreeRTOS,首先你需要选择一个合适的硬件平台和软件环境,例如微控制器型号、编译器类型、开发板规格等。然后你需要下载FreeRTOS的源代码,并根据你的平台选择相应的移植文件(port file)。移植文件是一些针对不同平台的特殊代码,用来实现一些基本的功能,例如时钟配置、中断处理、任务切换等。接着你需要配置FreeRTOSConfig.h文件,这个文件是一个头文件,用来设置一些FreeRTOS相关的宏定义,例如任务数量、堆大小、调试选项等。最后你就可以开始编写你自己的应用程序了。
要创建一个FreeRTOS程序,首先需要包含FreeRTOS的头文件,并定义一些必要的宏和变量。例如:
#include "FreeRTOS.h"
#include "task.h"
#define mainDELAY_LOOP_COUNT 100000UL
static void prvTask1(void *pvParameters);
static void prvTask2(void *pvParameters);
然后需要创建任务,并启动调度器。例如:
int main(void)
{
xTaskCreate(prvTask1, "Task 1", 1000, NULL, 1, NULL);
xTaskCreate(prvTask2, "Task 2", 1000, NULL, 1, NULL);
vTaskStartScheduler();
return 0;
}
这里创建了两个任务,分别执行prvTask1和prvTask2函数,每个任务分配了1000个字节的栈空间,优先级都为1,没有传递任何参数。然后调用vTaskStartScheduler函数启动调度器,这个函数会创建一个空闲任务,并开始按照优先级和时间片轮转调度各个就绪状态的任务。
每个任务都是一个无限循环的函数,可以在其中执行一些操作,并调用一些FreeRTOS提供的API函数。例如:
static void prvTask1(void *pvParameters)
{
unsigned long ul;
for(;;)
{
for(ul = 0; ul < mainDELAY_LOOP_COUNT; ul++)
{
// do something
}
vTaskDelay(250 / portTICK_PERIOD_MS);
// toggle LED 1
}
}
static void prvTask2(void *pvParameters)
{
unsigned long ul;
for(;;)
{
for(ul = 0; ul < mainDELAY_LOOP_COUNT; ul++)
{
// do something
}
vTaskDelay(250 / portTICK_PERIOD_MS);
// toggle LED 2
}
}
这里每个任务都执行了一个延时循环,并在每次循环后调用vTaskDelay函数挂起自己一段时间,然后切换到另一个任务。vTaskDelay函数的参数是以系统时钟节拍为单位的延时时间,portTICK_PERIOD_MS是一个宏,表示每个节拍的毫秒数,这个值取决于系统时钟的频率和配置。在每次延时结束后,每个任务都会切换一个LED的状态,以此来观察任务的运行情况。
这个程序可以在不同的硬件平台上运行,只需要根据不同的平台选择合适的FreeRTOS移植文件,并编写相应的硬件初始化和LED控制代码。FreeRTOS提供了多种平台的移植文件,可以在官网或者GitHub上找到。
三、FreeRTOS的堆和栈
1.堆和栈的概念
- 堆(heap)是一块动态分配的内存区域,可以在程序运行时根据需要申请和释放。
- 栈(stack)是一块静态分配的内存区域,用来存储函数调用时的局部变量、参数、返回地址等。
- FreeRTOS中每个任务都有自己的栈空间,用来保存任务的上下文(context),即任务运行时的寄存器值、状态等。
- FreeRTOS中还有一个任务控制块(TCB),用来保存任务的基本信息,例如任务名、优先级、状态、堆栈指针等。
2.堆和栈的分配方式
- FreeRTOS提供了多种堆管理方案,可以在FreeRTOSConfig.h文件中选择使用哪种方案。
- heap_1:只支持静态分配,即在程序开始时就分配好所有任务的TCB和栈空间,不支持动态创建和删除任务。
- heap_2:支持动态分配,即在程序运行时可以创建和删除任务,但不支持内存回收,即删除任务后不会释放其占用的内存空间。
- heap_3:支持动态分配和内存回收,使用标准C库的malloc和free函数来管理堆空间,但可能存在内存碎片问题,即堆空间被分割成很多小块,导致无法分配足够大的连续空间。
- heap_4:支持动态分配和内存回收,并且可以合并相邻的空闲块,减少内存碎片问题,但需要更多的代码空间和执行时间。
- heap_5:在heap_4的基础上增加了多个堆区域的支持,可以将不同大小或者不同属性的内存区域作为堆来使用,提高了内存利用率。
3.堆和栈的溢出检测
- 堆溢出(heap overflow)是指申请的堆空间超过了可用的堆空间,导致无法分配成功。
- 栈溢出(stack overflow)是指任务的栈空间不足以存储所需的数据,导致覆盖了其他内存区域。
- FreeRTOS提供了多种栈溢出检测方法,可以在FreeRTOSConfig.h文件中选择使用哪种方法。
- method 1:在栈顶和栈底设置哨兵值(sentinel value),并在每次任务切换时检查哨兵值是否被破坏。这种方法简单易用,但可能存在误报或者漏报的情况。
- method 2:使用MPU(内存保护单元)来保护栈区域不被非法访问。这种方法需要硬件支持,并且需要使用特殊的函数来创建任务。这种方法可以精确地检测到栈溢出,并且可以防止栈溢出造成的系统崩溃。
- method 3:结合method 1和method 2,既使用哨兵值又使用MPU来检测栈溢出。这种方法可以提高检测的准确性和安全性,但也增加了复杂度和开销。
四、创建任务函数及任务管理
FreeRTOS提供了多种创建任务的函数,除了xTaskCreate
之外,还有xTaskCreateStatic、xTaskCreateRestricted、xTaskCreateRestrictedStatic
等。这些函数的区别主要在于是否使用静态分配或者是否使用MPU。静态分配意味着TCB和栈空间都是由用户提供的,而不是从堆中分配的。MPU意味着可以为每个任务设置不同的内存访问权限,以提高系统的安全性和稳定性。
创建任务的函数都有一些共同的参数,例如:
- pvTaskCode:指向任务函数的指针。
- pcName:指向任务名字的指针。
- usStackDepth:指定任务栈的大小,以字为单位。
- pvParameters:指向传递给任务函数的参数的指针。
- uxPriority:指定任务的优先级,数值越大优先级越高。
- pxCreatedTask:指向接收任务句柄的变量的指针。
例如,下面的代码使用xTaskCreate函数创建了一个名为"Hello",优先级为1,栈大小为1000字节,没有传递任何参数的任务,并将其句柄存储在xHandle变量中。
TaskHandle_t xHandle = NULL;
xTaskCreate(vTaskHello, "Hello", 1000, NULL, 1, &xHandle);
如果要使用静态分配,可以使用xTaskCreateStatic函数,并提供两个额外的参数:
- puxStackBuffer:指向分配给任务栈的内存区域的指针。
- pxTaskBuffer:指向分配给TCB的内存区域的指针。
例如,下面的代码使用xTaskCreateStatic函数创建了一个名为"World",优先级为2,栈大小为1000字节,没有传递任何参数的任务,并将其句柄存储在xHandle变量中。同时,它使用了两个静态数组来分配任务栈和TCB所需的内存空间。
#define STACK_SIZE 1000
static StackType_t xStack[STACK_SIZE];
static StaticTask_t xTaskBuffer;
TaskHandle_t xHandle = NULL;
xHandle = xTaskCreateStatic(vTaskWorld, "World", STACK_SIZE, NULL, 2, xStack, &xTaskBuffer);
如果要使用MPU,可以使用xTaskCreateRestricted或者xTaskCreateRestrictedStatic函数,并提供一个结构体类型的参数:
- xTaskParameters:包含了创建任务所需的所有信息和MPU设置。
例如,下面的代码使用xTaskCreateRestricted函数创建了一个名为"MPU",优先级为3,栈大小为1000字节,没有传递任何参数的任务,并将其句柄存储在xHandle变量中。同时,它使用了一个结构体变量来设置任务的MPU属性,例如允许访问RAM区域和FLASH区域等。
#define STACK_SIZE 1000
static const TaskParameters_t xTaskParameters = {
.pvTaskCode = vTaskMPU,
.pcName = "MPU",
.usStackDepth = STACK_SIZE,
.pvParameters = NULL,
.uxPriority = 3,
.puxStackBuffer = NULL,
.xRegions = {
{RAM_START_ADDRESS, RAM_LENGTH, portMPU_REGION_READ_WRITE},
{FLASH_START_ADDRESS, FLASH_LENGTH, portMPU_REGION_READ_ONLY},
{0, 0, 0}
}
};
TaskHandle_t xHandle = NULL;
xHandle = xTaskCreateRestricted(&xTaskParameters, NULL);
创建任务的函数都会返回一个句柄(handle),这是一个指向TCB的指针,可以用来对任务进行管理。例如,可以使用vTaskDelete
函数删除一个任务,可以使用vTaskSuspend和vTaskResume函数挂起和恢复一个任务,可以使用vTaskPrioritySet
函数改变一个任务的优先级,可以使用xTaskNotify和xTaskNotifyWait函数给一个任务发送和接收通知等。
例如,下面的代码使用vTaskDelete函数删除了之前创建的"Hello"任务,并使用vTaskPrioritySet函数将"World"任务的优先级提高到4。
vTaskDelete(xHandle);
vTaskPrioritySet(xHandle, 4);
除了使用句柄来管理任务之外,还可以使用任务名或者任务标识符(task number)。每个任务都有一个唯一的标识符,可以使用uxTaskGetTaskNumber和vTaskSetTaskNumber
函数获取和设置。每个任务也可以有一个最多8个字符的名字,可以使用pcTaskGetName
函数获取。这些信息可以用来在调试或者跟踪时识别不同的任务。
例如,下面的代码使用uxTaskGetTaskNumber
函数获取了当前任务的标识符,并使用pcTaskGetName
函数获取了当前任务的名字,并打印出来。
UBaseType_t xTaskNumber = uxTaskGetTaskNumber(NULL);
char *pcTaskName = pcTaskGetName(NULL);
printf("The task number is %d, the task name is %s\n", xTaskNumber, pcTaskName);
另外我们还可以使用一些其他的任务管理功能,比如:
我们可以使用vTaskDelayUntil
函数实现固定频率的周期性任务。这个函数需要传递一个指向上次唤醒时间的变量的指针,和一个以系统时钟节拍为单位的周期时间。这个函数会根据上次唤醒时间和周期时间计算出下次唤醒时间,并挂起当前任务直到下次唤醒时间到达。这样可以避免累积误差,保证任务按照固定频率执行。
本节完