Jialong's Blog
沉潜 自由 追寻幸福
FreeRTOS操作系统总结

本文来总结一些FreeRTOS的基本概念和API的使用,部分内容参考自:

AN0025 应用笔记FreeRTOS on AT32 MCU - 雅特力

FreeRTOS中断优先级管理

AT32/STM32中断配置

一般配置NVIC_PriorityGroup_4,只使用中断优先级寄存器的高四位,一共可以表示2^4=16级中断优先级,NVIC_PriorityGroup_4只有16级抢占优先级,没有子优先级。

FreeRTOS中断配置

一般配置用户可以在抢占优先级为3-15的中断里面调用FreeRTOS的API函数。

中断优先级和任务优先级

二者没有关系,中断的优先级永远高于任何任务的优先级。

临界段保护

临界段代码也成为临界区,指哪些必须完整运行不能被打断的代码。例如一些外设的初始化,需要遵循严格的时序要求。FreeRTOS采用的是在进入临界区代码后关闭中断,退出临界区的时候打开中断。FreeRTOS系统很多程序段都加了临界区保护,在写应用程序的时候也有很多地方需要用到临界区保护。

与临界段代码保护有关的函数有以下4个:

  • taskENTER_CRITICAL()

进入临界区,关中断

  • taskEXIT_CRITICAL()

退出临界区,开中断

taskENTER_CRITICAL_FROM_ISR()

进入临界区,关中断(ISR中使用)

任务管理

裸机与带RTOS的区别

  • 裸机系统

一个大循环顺序执行,每一部分的操作都不是实时性的。为了解决这个问题,引入了中断,紧急事件可以放到中断中执行。

  • RTOS运行流程

调度器使用调度算法来决定当前要执行的任务,从而实现多任务系统。这里所说的多任务系统同一时刻只能有一个任务可以运行,只能通过调度器决策,看起来像所有任务同时运行一样。

FreeRTOS任务状态

  • 运行态 - Running
  • 就绪态 - Ready
  • 阻塞态 - Blocked
  • 挂起态 - Suspended

FreeRTOS任务状态转换关系

阻塞态和挂起态的区别:

  • 阻塞指的是进程由于发生某事件(例如I/O请求、申请缓冲区失败)暂时无法继续执行,引起进程调度,把CPU让给其他就绪进程。挂起是由于系统和用户的需要引入的操作,进程被挂起意味着该进程处于静止状态,如果进程正在执行,它将暂停执行,若原本处于就绪状态,则该进程此时暂不接受调度。
  • 二者的共同点是进程都暂停执行,并且都释放了CPU,即两个过程都涉及上下文切换
  • 不同点是:
    • 对系统资源占用不同
    • 发生时机和恢复时机不同

FreeRTOS空闲任务

空闲任务是系统任务,是必须要执行的,一个RTOS每时每刻都要有一个任务执行,这个空闲任务还可以做一些其他工作,例如进入低功耗等。

任务调度

FreeRTOS共有三种任务调度方式:合作式、抢占式、时间片式。

合作式调度

主要用于资源很紧张的设备,现在已经很少使用。不作详细介绍。

抢占式调度

调度器:使用相关的调度算法决定当前需要执行的任务。

所有调度器有一个共同的特性:

  • 调度器可以区分就绪态任务和挂起任务
  • 调度器可以选择就绪态中的一个任务,然后激活他。
  • 不同调度器的最大区别就是如何分配就绪态任务的完成时间。

抢占式调度就是调度器算法的一种。实际应用中,不同的任务需要不同的响应时间。

时间片调度

最常用的时间片调度算法就是Round-robin调度算法。

消息队列

消息队列基本概念

通过RTOS内核提供的服务,任务或中断服务子程序可以将一个消息放入到队列;同样,一个或多个任务可以通过RTOS内核服务从队列中得到消息。

在裸机编程中,相比于消息队列,使用全局数组主要有如下问题:

  • 使用消息队列

注意在操作系统中实现中断服务程序与裸机是不同的,主要有以下几点要注意:

  • 如果FreeRTOS工程中的中断函数没有调用FreeRTOS的消息队列API函数,那么就与裸机编程是一样的。
  • 如果调用了FreeRTOS的消息队列API函数,退出时要检测是否有高优先级任务就绪,如果有则需要在退出中断后进行任务切换。
  • 在使用AT32芯片时将中断优先级分组设置为组4,即NVIC_PriorityGroup_4
  • 要在FreeRTOS多任务开启前就设置好优先级分组,一旦设置好就不可修改。

消息队列相关API

xQueueCreate()

创建一个消息队列,返回该队列的句柄。

xQueueSend()

向指定消息队列发送一个消息,返回值为发送消息成功与否的标志。

xQueueSendFromISR()

在中断处理函数中,发送消息的函数,返回值为发送消息成功与否标志。

xQueueReceive()

从指定消息队列内接受消息,返回值为接受消息成功与否标志。

xQueueReceiveFromISR()

在中断处理函数中从指定消息队列内接受消息。

信号量

FreeRTOS信号量的概念

给共享资源建立一个标志,该标志表示该共享资源被占用的情况。

实际中使用信号量主要来实现两个功能:

  • 两个任务之间或中断函数和任务之间的同步功能,就是共享资源为1的时候
  • 多个共享资源的管理

二值信号量

常用于互斥访问或同步。

信号量API函数也允许设置一个阻塞时间,阻塞时间指的是当任务获取信号量的时候由于信号量无效而进入阻塞状态的最大时间节拍数。

二值信号量API

xSemaphoreCreateBinary()

创建一个二值信号量,返回值为创建的二值信号量的句柄SemaphoreHandle_t

xSemaphoreCreateBinaryStatic()

静态创建一个二值信号量

vSemaphoreDelete()

删除一个信号量,无返回值。

xSemaphoreGive()

释放一个信号量,返回值为释放成功与否标志。

xSemaphoreFromISR()

内释放一个信号量(中断级)

xSemaphoreTake()

获取一个信号量,返回值为获取成功与否标志。

xSemaphoreTakeFromISR()

获取一个信号量(中断级)

二值信号量例程

计数型信号量

又称为数值信号量,是可以大于1的信号量。信号量的本质就是队列,只是不用关注队列中存放了什么消息,只需关注队列是否满即可。计数型信号量的应用场景:

  • 事件计数:每次事件发生前就在事件处理函数中释放信号量(增加信号量的计数值),其他任务事件会获取信号值,获取一次就在任务事件处理函数中对信号量减1操作。该场合下创建的计数信号量的初始值为0。
  • 资源管理:该场景下,信号量代表当前资源的可用数量。任务事件获取一次资源后信号量的值就减1操作,任务事件释放一次资源后信号量加1操作。该场合中信号量的初始值为资源的总数量。

计数型信号量API

xSemaphoreCreateCounting()

创建一个计数型信号量,返回创建的计数型信号量的句柄。函数的两个参数为该信号量允许的最大值和信号量的初始值。

vSemaphoreDelete()

xSemaphoreGive()

xSemaphoreTake()

uxSemaphoreGetCount()

计数型信号量例程

测试一下

互斥信号量

优先级翻转

使用二值信号量的时候很有可能碰到优先级翻转的问题,这在可剥夺型内核中很常见。但实时系统中不允许出现这样的情况,这回打乱系统的预期执行顺序,导致低优先级的任务先运行,违背了实时性的初衷。

互斥信号量介绍

为了资源互斥访问而设计。

互斥信号量采取了措施尽量回避优先级翻转的问题。当一个高优先级任务想要获取互斥信号量但该信号量被某低优先级的任务所持有,此时高优先级任务会进入阻塞态,在进入阻塞态之前此高优先级的任务会将持有互斥信号量的低优先级的任务的优先级提高到和高优先级任务相同。

这样持有互斥信号量的低优先级任务就不会被其他中间等级优先级的任务所抢占CPU的使用权,尽可能地缩短了高优先级任务的响应时间。

互斥信号量API

xSemaphoreCreateMutex()

创建一个互斥信号量,返回创建的互斥信号量的句柄。

vSemaphoreDelete()

xSemaphoreGive()

xSemaphoreTake()

uxSemaphoreGetCount()

互斥信号量例程

递归互斥信号量

是一个特殊的互斥信号量,已经获取了互斥信号量的任务就不能再次获取这个互斥信号量了,而递归互斥信号量则不同,在同一个任务中,此任务可以多次获得互斥递归信号量。一旦一个任务获得了递归互斥信号量,那么其他任务便不能获得此信号量,只能本身再次获取此递归互斥信号量。

互斥递归信号量API

xSemaphoreCreateRecursiveMutex()

xSemaphoreCreateRecursiveMutexStatic()

vSemaphoreDelete()

xSemaphoreGiveRecursive()

xSemaphoreTakeRecursive()

事件标志组

事件标志组与消息队列、信号量一样,都是FreeRTOS内核提供的一种内核服务,在任务绒布中应用非常广泛。

EventGroup概念

事件标志组是FreeRTOS内核提供的一种服务,是实现多任务的有效机制之一。Event Group实质上是内核管理的一个变量,任务通过设置这个变量的不同BIT位,达到同步的效果。

使用全局变量相比于事件标志组主要有以下问题:

  • 使用Event Group可以让RTOS内核有效地管理任务,而全局变量无法做到,任务的超时等机制需要用户自己实现
  • 使用了全局变量就要防止多任务的访问冲突,而Event Group就不需要担心该问题
  • 使用Event Group可以解决中断服务程序和任务之间的同步问题

EventGroup API

xEventGroupCreate()

返回创建的事件标志组的句柄

xEventGroupSetBits()

设置一个事件标志组对应的BIT位,返回事件标志组的值

xEventGroupWaitBits()

等待事件标志组的对应BIT位,返回事件标志组的值

软件定时器组

FreeRTOS软件定时器概念

内核提供的一种服务。FreeRTOS的软件定时器组的时基是基于系统时钟节拍实现的,它的实现不需要使用任何硬件定时器,而且可以创建多个。

在硬件定时器中,我们是在定时器中断中实现需要的功能,而使用软件定时器时,我们是在创建软件定时器时指定软件定时器的回调函数,在回调函数中实现相应的功能。

FreeRTOS的软件定时器支持单次模式和周期性模式。

定时事件到后会调用定时器的回调函数,用户可以在回调函数中加入需要执行的工程代码。

FreeRTOS为软件定时器专门创建了一个任务,称其为软件定时器的守护进程(Daemon Task)。该任务在系统是能了软件定时器组的功能后自动创建。

用户应用程序和内核管理软件定时器程序之间通过消息队列实现通信功能。

软件定时器组API

xTimerCreate()

xTimerStart()

xTimerStop()

pcTimerGetName()

pvTimerGetTimerID()

vTimerSetTimerID()

FreeRTOS低功耗模式

FreeRTOS提供了一种Tickless机制来管理低功耗。

Tickless机制

tickless可理解为无滴答时钟,即滴答时钟节拍停止运行的情况。

FreeRTOS中,当用户任务都被挂起或阻塞时,最低优先级的空闲任务会得到执行,我们就可以把睡眠模式放到空闲任务中。进入空闲模式后,首先要计算而可执行低功耗的最大时间,即求出下一个要执行的高优先级任务的剩余事件。然后把低功耗的唤醒时间设置为这个求出的时间,到时间系统会从低功耗模式被唤醒,继续执行多任务。

实现tickless模式最麻烦的是低功耗可执行时间的获取,这个问题FreeRTOS已经做好了。

用户只用在FreeRTOSConfig.h配置文件中配置宏定义configUSE_TICKLESS_IDLE为1即可。另外如果将该参数配置为2,那么用户可以自定义tickless低功耗模式的实现。当该宏定义为1且系统运行满足以下条件时,系统内核会自动调用低功耗宏定义函数portSUPPRESS_TICKS_AND_SLEEP():

  • 当前空闲任务正在运行,所有其他任务处于挂起或阻塞状态
  • 根据用户配置configEXPECTED_IDLE_TIME_BEFORE_SLEEP的大小,只有当系统可运行于低功耗模式的时钟节拍数大于等于这个参数时,系统才可以进入到低功耗模式。用户可以重新定义该宏。

FreeRTOS内存管理方式

FreeRTOS内核提供了5中内存管理机制,我们可以根据应用具体需求选择合适的内存管理方式,源程序路径为:FreeRTOS/portable/MemMang

方式一

只能分配内存,不能释放内存。FreeRTOS的内存管理策略还会使用字节对齐。

方式二

使用最佳匹配算法,允许释放之前已分配的内存,但不会把相邻的内存块合并为一个大的内存块。

方式三

简单地封装了标准库的malloc()和free()函数,采用的封装策略是操作内存前挂起调度器,完成后再恢复调度器,封装后的malloc()和free()函数具备线程保护机制。

方式四

与第二种策略比较类型,增加了一个合并算法,将相邻的空闲内存块合并成一个大块。

方式五

允许内存堆跨越多个连续的内存区,除此之外其他操作都和第四种内存管理方式相似。

FreeRTOS流缓存

FreeRTOS消息缓存

FreeRTOS任务通知

FreeRTOS编码风格

FreeRTOS的核心源代码遵从MISRA编码标准指南。

  • uint32_t类型的变量使用前缀ul,表示unsigned long
volatile uint32_t ulNotifiedValue;
  • uint16_t类型的变量使用前缀us,表示unsigned short
uint16_t usStackDepth;
  • uint8_t类型使用前缀uc,表示unsigned char
volatile uint8_t ucNotifyState;
  • 非stdint类型的变量使用前缀x,比如基本的Type_t和TickType_t类型;非stdint类型的无符号变量使用前缀ux,比如UbaseType_t
BaseType_t xReturn;
  • size_t类型的变量使用前缀x;枚举类型变量使用前缀e
size_t xBlockSize;
typedef enum {
	eAbortSleep = 0,
	eStandardSleep,
	eNoTasksWaitingTimeout
} eSleepModeStatus;
eSleepModeStatus eReturn = eStandardSleep;

如上eReturn变量的定义,e开头表示这个变量的类型是enum。

  • 指针类型的变量在类型基础上附加前缀p,比如指向uint32_t的指针变量的前缀为pul
uint32_t *pulTopOfStack, ulTemp;
  • char类型变量仅被允许保存ASCII字符,前缀为c;
char cStatus;
volatile int8_t cRxLock;

函数命名风格

  • 在文件作用域范围的函数前缀位prv
static void prvDeleteTCB(TCB_t *pxTCB)
{
	...
}

可以理解为就是static函数。

  • API函数的前缀为它们的返回类型,当返回类型为空时,前缀为v
void vTaskDelete(TaskHandle_t xTaskToDelete)
{
	...
}

API函数提供给其他模块调用,可以理解为全局函数。

BaseType_t xTaskResumeAll(void)
{
    ...
}

xTaskResumeAll(void)函数,函数前面的x代表返回值的类型,这里是BaseType_t类型。

  • API函数名字起始部分为该函数所在的文件名
QueueHandle_t xQueueGenericCreate()
{
    ...
}

xQueueGenericCreate()函数定义在Queue.c中,x表示函数的返回值是QueueHandle_t类型。

宏命名风格

宏的名字起始部分为该宏定义所在的文件名的一部分。比如configUSE_PREEMPTION定义在FreeRTOSConfig.h文件中。除了前缀,宏剩下的字母全部为大写,两个单词间用下划线_隔开。

在FreeRTOS/Source/inclue/queue.h中:

/* For internal use only. */
 #define queueSEND_TO_BACK       ( ( BaseType_t ) 0 )
#define queueSEND_TO_FRONT      ( ( BaseType_t ) 1 )
#define  queueOVERWRITE         ( ( BaseType_t ) 2 )

最后修改于 2022-07-11