实时操作系统(RTOS-Real Time Operating System)是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统做出快速响应,调度一切可利用的资源完成实时任务,并控制所有实时任务协调一致运行的操作系统。提供及时响应和高可靠性是其主要特点。
FreeRTOS是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需要。
由于RTOS需占用一定的系统资源(尤其是RAM资源),只有μC/OS-II、embOS、salvo、FreeRTOS等少数实时操作系统能在小RAM单片机上运行。相对μC/OS-II、embOS等商业操作系统,FreeRTOS操作系统是完全免费的操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植到各种单片机上运行。
首先是源码的下载,链接在此FreeRTOS源码下载,另附上官网地址。
下载后可以看到有几个文件。
其中FreeRTOS文件夹内文件包含源码和各种单片机的Demo,而FreeRTOS-Plus文件夹包含的则是FreeRTOS与FatFs、TCP和UDP等组件进行融合的程序代码。
我们暂时需要的是FreeRTOS文件夹的内容,打开它会发现文件包含以下几个文件。
Demo文件夹包含的是各种单片机与FreeRTOS融合的Demo,License文件夹则为字面意思,Source就是FreeRTOS的源码。
该文件是croutine.c的头文件,它们共同实现FreeRTOS协同程序功能。具体可看协程的作用。
关于进程-线程-协程的关系如下。
该头文件字面意思是弃用的定义。下面是文档里面对应的描述。
每个FreeRTOS端口都有一个唯一的portmacro.h头文件。最初,使用预处理器定义来确保预处理器为所使用的端口找到正确的portmacro.h文件。该方案不赞成设置编译器的include路径,以便找到正确的portmacro.h文件——不需要常量,并允许将portmacro.h文件放在与所使用端口相关的任何位置。下面的定义保留在代码中,仅用于向后兼容。新项目不应该使用它们。
其实就是通过define相对的单片机来声明与单片机相契合的portmacro.h头文件。事实上如前所述,直接将需要的portmacro.h放到自己的工程即可。
该文件是event_groups.c的头文件,提供事件组功能,它允许与事件通信任务。
该头文件提供FreeRTOS的配置选项,文件内都是关于系统的宏定义(#ifdef-#define-#endif),通过与FreeRTOSConfig.h配合进行FreeRTOS的配置,通过合理配置可实现FreeRTOS的裁剪。详见FreeRTOS 之 全配置项详解、裁剪(FreeRTOSConfig.h)。
该文件是list.c的头文件,列表的实现主要是为了调度器(scheduler),但也可以由应用程序代码使用。list_ts只能存储指向list_item_ts的指针。每个ListItem_t包含一个数值(xItemValue)。大多数情况下,列表按递减项值顺序排序。已经创建了包含一个列表项的列表。该项的值是可以存储的最大值,因此它总是在列表的末尾,充当标记。列表成员pxHead总是指向这个标记—即使它位于列表的末尾。这是因为尾部包含一个回滚指针,指向列表的真正头部。除了它的值之外,每个列表项还包含指向列表中下一项的指针(pxNext)、指向它所在列表的指针(pxContainer)和返回包含它的对象的指针。后面的两个指针用于提高列表操作的效率。在包含列表项的对象和列表项本身之间存在有效的双向链接。
该头文件为消息缓冲区,它可实现直接到任务通知,它利用流缓冲区(stream_buffer)进行数据的传输。详见RTOS Message Buffers 。
该头文件包含的是关于MPU的API函数原型声明。
当使用MPU时,标准(非MPU) API函数被映射到启动“MPU_”的等效函数,该函数的原型在这个头文件中定义。这将导致应用程序代码调用MPU_版本,该版本包装了具有特权的非mpu版本,提升然后降级代码,因此内核代码总是运行于完整的特权下。
该头文件主要作用是将标准task .h API函数映射到相应的MPU。
该头文件包含的是可移植层的API,它为每个端口定义对应的函数。
该头文件定义了任务函数必须遵循的原型。
该文件是queue.c的头文件,定义了队列的相关函数。详见队列。
在FreeRTOS实现内部,队列使用两个内存块。第一个块用于保存队列的数据结构。第二个块用于保存放置到队列中的项。如果使用xQueueCreate()创建队列,则在xQueueCreate()函数中自动动态分配这两个内存块。(详见Memory Management)。如果使用xQueueCreateStatic()创建队列,那么应用程序编写器必须提供队列将使用的内存。因此,xQueueCreateStatic()允许在不使用任何动态内存分配的情况下创建队列。
该头文件定义了关于二进制信号量/互斥量的函数原型。详见信号量。
在许多使用场景中,使用直接到任务的通知代替二进制信号量会更快、更节省内存!
该头文件包含了关于堆栈的宏定义。
如果正在交换的任务的堆栈当前溢出,或者看起来可能在过去溢出,则调用堆栈溢出钩子函数。将configCHECK_FOR_STACK_OVERFLOW设置为1将导致宏只检查当前堆栈状态——将当前堆栈顶部值与堆栈限制进行比较。将configCHECK_FOR_STACK_OVERFLOW设置为大于1也会导致检查最后几个堆栈字节,以确保在创建任务时设置的字节的值没有被覆盖。注意,第二个测试并不保证总是能够识别溢出堆栈。
该头文件是stack_macros.h的旧版本,如果使用此头文件会报警告。
#ifndef _MSC_VER /* Visual Studio doesn't support #warning. */
#warning The name of this file has changed to stack_macros.h. Please update your code accordingly. This source file (which has the original name) will be removed in future released.
#endif
该文件为stream_buffer.c的头文件,定义关于流缓冲区的函数原型。详见流缓冲区。
流缓冲区用于将连续的数据流从一个任务或中断发送到另一个任务。它们的实现是轻量级的,这使得它们特别适合于中断到任务和核心到核心的通信场景。
独特FreeRTOS对象之间流缓冲区实现(也消息缓冲区实现,如消息缓冲区是建立在流缓冲区)的假定只有一个任务或中断,将写入缓冲区(the writer编写器),且只有一个任务或中断,从缓冲区读取(the reader阅读器)。对于编写器和读取器来说,不同的任务或中断是安全的,但是与其他FreeRTOS对象不同,拥有多个不同的编写器或多个不同的读取器是不安全的。如果有多个不同的编写器,那么应用程序编写器必须将每个调用放置到临界区中的一个写入API函数(如xStreamBufferSend())中,并将发送块时间设置为0。同样,如果要有多个不同的读取器,那么应用程序编写器必须将每个调用放置到读取API函数(如xStreamBufferRead())的临界部分中,并将接收块时间设置为0。
该文件为tasks.c的头文件,定义了关于任务(task)的创建、删除等操作函数。详见任务创建。
该文件为timers.c的头文件,定义了关于软件定时器的函数。其中软件定时器用于在将来的某个固定时间或定期以固定的频率调度函数的执行。软件定时器执行的函数称为软件定时器的回调函数。
详见软件定时器。
在FreeRTOS实现内部,软件定时器使用一块内存,其中存储定时器数据结构。如果使用xTimerCreate()创建软件计时器,则在xTimerCreate()函数中自动动态分配所需的内存。(见内存管理)。如果使用xTimerCreateStatic()创建软件计时器,则应用程序编写器必须提供软件计时器将使用的内存。因此,xTimerCreateStatic()允许在不使用任何动态内存分配的情况下创建软件计时器。计时器是在休眠状态下创建的。可以使用xTimerStart()、xTimerReset()、xTimerStartFromISR()、xTimerResetFromISR()、xTimerChangePeriod()和xtimerchangefromisr() API函数将计时器转换为活动状态。
该文件夹包含了关于不同编译器环境下的单片机端口的定义、内存管理等文件。
如在keil编译器下则需要到RVDS文件夹内获取关于端口的文件,它主要包含两个文件port.c和portmacro.h,前者为端口实现portable.h中定义的函数,而后者为给定的硬件和编译器正确配置FreeRTOS。假如要使用STM32F10x单片机,则选择portable\RVDS\ARM_CM3中所包含的文件。
而MemMang文件夹则包含五种堆的管理方式,详细的说明如下表所示。详见内存管理。
文件名 | 功能说明 |
---|---|
heap1.c | 最简单的方式,不允许释放内存 |
heap2.c | 允许释放内存,但不会合并相邻的空闲块。(容易造成内存碎片) |
heap3.c | 简单地包装标准的malloc()和free()以保证线程安全 |
heap4.c | 邻近空闲块的合并以避免碎片。包括绝对地址放置选项 |
heap5.c | 根据heap_4,能够跨多个非相邻内存区域跨越堆 |
下面介绍适合它们使用的环境。
这是所有方式中最简单的实现。它不容许一旦被分配到被释放的内存。
它的实现:
该方案使用最佳拟合算法,并且与方案1不同,允许释放先前分配的块。它并不相邻的空闲块合并成一个单一的大的块。请参阅heap_4.c以获取执行无融合块的实现。
它的实现:
即使应用程序反复删除任务,队列,信号量,互斥量等,也可以使用以下有关内存碎片的警告。
如果不是,如果被分配和释放内存使用是随机的大小。例如:
如果您的应用程序以不可预测的顺序排队,任务,信号量,互斥锁等,则可能导致内存碎片问题。几乎所有应用都不太可能,但应该牢记这一点。
不是确定性的 - 但是大多数标准C库malloc实现的效率要高得多。
这实现了标准C库malloc()和free()函数的简单包装,在大多数情况下,这些函数将与您选择的编译器一起提供。包装器只是使malloc()和free()函数线程安全。
它的实现:
该方案使用第一拟合算法,并且与方案2不同,它将相邻的空闲存储块组合成单个大块(它确实包括合并算法)。
可用堆空间总量由configTOTAL_HEAP_SIZE设置 - 它在FreeRTOSConfig.h中定义。提供configAPPLICATION_ALLOCATED_HEAP FreeRTOSConfig.h配置常量以允许将堆放置在内存中的特定地址。xPortGetFreeHeapSize()API函数返回在调用函数时保持未分配的堆空间总量,xPortGetMinimumEverFreeHeapSize()API函数返回FreeRTOS应用程序引导的系统中存在的最小可用堆空间量。这两个函数都没有提供有关如何将未分配的内存分段为更小的块的信息。
它的实现:
此方案使用与heap_4相同的第一个拟合和内存合并算法,并允许堆跨越多个非相邻(非连续)内存区域。Heap_5通过调用vPortDefineHeapRegions()来初始化,并且在vPortDefineHeapRegions()执行之后才能使用。创建RTOS对象(任务,队列,信号量等)将隐式调用pvPortMalloc(),因此在使用heap_5时,必须在创建任何此类对象之前调用vPortDefineHeapRegions()。
下面以FreeRTOS移植到STM32F103ZET6为例。
将FreeRTOS/Source文件夹转移到项目中并在C/C++处添加头文件对应的路径,如下图所示。
然后会报错说不能打开FreeRTOSConfig.h头文件,这是我们需要到Demo文件夹处找到与我们单片机型号相同或相似的Demo,这里在CORTEX_STM32F103_Keil文件夹内可找到FreeRTOSConfig.h文件,将它移动到include文件夹内然后在编译项目就ok了。至于如何创建任务可根据任务创建来进行。或者根据下面给出的示例进行。
#include "bsp.h"
#define LED0 PCout(13)
#define LED1 PCout(0)
//任务优先级
#define START_TASK_PRIO 1
//任务堆栈大小
#define START_STK_SIZE 128
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);
//任务优先级
#define LED0_TASK_PRIO 2
//任务堆栈大小
#define LED0_STK_SIZE 50
//任务句柄
TaskHandle_t LED0Task_Handler;
//任务函数
void led0_task(void *pvParameters);
//任务优先级
#define LED1_TASK_PRIO 3
//任务堆栈大小
#define LED1_STK_SIZE 50
//任务句柄
TaskHandle_t LED1Task_Handler;
//任务函数
void led1_task(void *pvParameters);
int main(void)
{
BSP_init();
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
}
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
xTaskCreate((TaskFunction_t )led0_task, //创建LED0任务
(const char* )"led0_task",
(uint16_t )LED0_STK_SIZE,
(void* )NULL,
(UBaseType_t )LED0_TASK_PRIO,
(TaskHandle_t* )&LED0Task_Handler);
xTaskCreate((TaskFunction_t )led1_task, //创建LED1任务
(const char* )"led1_task",
(uint16_t )LED1_STK_SIZE,
(void* )NULL,
(UBaseType_t )LED1_TASK_PRIO,
(TaskHandle_t* )&LED1Task_Handler);
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
//LED0任务函数
void led0_task(void *pvParameters)
{
while(1)
{
LED0=~LED0;
vTaskDelay(500);
}
}
//LED1任务函数
void led1_task(void *pvParameters)
{
while(1)
{
LED1=0;
vTaskDelay(200);
LED1=1;
vTaskDelay(800);
}
}
可以看出和UCOS-II的创建任务很相似,大概是像人一样看起来都差不多,但是内涵不同吧(UCOS不是免费的呀)。希望大家使用愉快。
当我将FreeRTOS移植到STM32F407VGT6时出现几十个错误,如下图。
可以看出大多是标识符未定义,问题大多是出现task.h和list.h头文件中。之后可以看到这两个文件有一个共同点,都含有下面这句话,它的意思是FreeRTOS.h必须在包含task.h之前被包含,其实对于其他文件也是一样,FreeRTOS.h头文件必须先被包含进来。所以我们可以通过移动一下头文件的位置来解决。
#ifndef INC_FREERTOS_H
#error "include FreeRTOS.h must appear in source files before include task.h"
#endif