作者:阿润菜菜
假如你是一位母亲是不是会经常遇到这样的情况:你要一边给小孩喂饭,一边加班跟同事微信交流,但是你无法一心多用,只能不停地切换注意力,导致效率低下,还容易出错?
如果你是一个软件开发者,你可能会想:有没有什么办法可以让我像电脑一样,可以同时运行多个任务,而不影响彼此的执行呢?
答案是:有!那就是使用操作系统(OS)。
操作系统是一种软件,它可以管理和调度多个程序或者任务(task)的运行,让它们看起来像是同时执行一样。操作系统可以根据任务的优先级、时间片、事件等因素来决定哪个任务应该先运行,哪个任务应该后运行,或者哪个任务应该暂停或者继续运行。
操作系统有很多种类,例如我们常用的Windows、Linux、Mac OS等,它们被称为通用操作系统(general-purpose OS),因为它们可以运行在各种类型的计算机上,并且支持各种类型的应用程序。
但是在一些专用的电子设备中,例如电梯、汽车、飞机、医疗仪器等,通用操作系统就不太适合了。因为这些设备通常使用的是微控制器(microcontroller)或者小型微处理器(microprocessor),它们的内存和处理能力都很有限。而且这些设备对于实时性(real-time)有很高的要求,也就是说它们必须在规定的时间内完成指定的任务,否则就会造成严重的后果。
例如,在电梯系统中,你按住开门键时如果没有即刻反应,即使只是慢个1
秒,也会夹住人。
为了满足这些设备的需求,就出现了一种特殊的操作系统:实时操作系统(real-time operating system,RTOS)。RTOS是一种为实时应用设计的操作系统,它可以在有限的资源下保证任务的及时响应和正确执行。
今天我们要介绍的就是一种开源的、免费的、广泛使用的RTOS:FreeRTOS。
FreeRTOS是一个实时操作系统内核(kernel),它可以在多种微控制器和处理器上运行,提供了丰富的任务调度、同步、通信、内存管理等功能。FreeRTOS是开源的,可以免费使用,也可以根据需要进行修改和定制。
那FreeRTOS和Linux的区别是什么?
1.FreeRTOS中没有进程和线程的区分:
FreeRTOS中的任务(Task)和线程(Thread)是相同的概念,每个任务就是一个线程,有着自己的一个程序函数。FreeRTOS可以创建、删除、挂起、恢复、优先级设置等多个任务,任务之间可以通过任务调度器根据优先级进行切换。
2.FreeRTOS和Linux都是操作系统,但是有很多区别,主要有以下几点:
FreeRTOS可以让你在微控制器或者小型微处理器上实现多任务的并发执行,从而提高你的系统的性能和效率。
FreeRTOS可以让你根据任务的优先级、时间片、事件等因素来灵活地调度任务的运行,从而保证你的系统的实时性和正确性。
FreeRTOS可以让你使用队列、信号量、互斥锁、事件组等机制来实现任务之间的同步和通信,从而保证你的系统的稳定性和可靠性。
FreeRTOS可以让你使用静态或者动态的方式来分配任务的内存空间,从而保证你的系统的灵活性和可扩展性。
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提供了多种创建任务的函数,除了xTaskCreate
之外,还有xTaskCreateStatic、xTaskCreateRestricted、xTaskCreateRestrictedStatic
等。这些函数的区别主要在于是否使用静态分配或者是否使用MPU。静态分配意味着TCB和栈空间都是由用户提供的,而不是从堆中分配的。MPU意味着可以为每个任务设置不同的内存访问权限,以提高系统的安全性和稳定性。
创建任务的函数都有一些共同的参数,例如:
例如,下面的代码使用xTaskCreate函数创建了一个名为"Hello",优先级为1,栈大小为1000字节,没有传递任何参数的任务,并将其句柄存储在xHandle变量中。
TaskHandle_t xHandle = NULL;
xTaskCreate(vTaskHello, "Hello", 1000, NULL, 1, &xHandle);
如果要使用静态分配,可以使用xTaskCreateStatic函数,并提供两个额外的参数:
例如,下面的代码使用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函数,并提供一个结构体类型的参数:
例如,下面的代码使用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
函数实现固定频率的周期性任务。这个函数需要传递一个指向上次唤醒时间的变量的指针,和一个以系统时钟节拍为单位的周期时间。这个函数会根据上次唤醒时间和周期时间计算出下次唤醒时间,并挂起当前任务直到下次唤醒时间到达。这样可以避免累积误差,保证任务按照固定频率执行。
本节完