FreeRTOS
前言
FreeRTOS是一款开源的实时操作系统,小巧,稳定,移植性强可运行在资源有限的嵌入式设备上,因此成为单片机系统的选择。
官方文档:FreeRTOS documentation - FreeRTOS™
实时性:Unix 操作系统给每个任务分配同样的运行时间,时间到了就切换到下一个任务。 RTOS 的任务调度器被设计为可预测的,优先级高的任务可以打断优先级低的任务得到及时执行。
系统文件
系统主要由内核和组件组成,而内核的源文件主要内容如下,portable
文件夹下存放的是针对不同平台的移植文件和动态内存分配文件。
名称 | 描述 |
---|---|
include (文件夹) | 内包含了 FreeRTOS 的头文件 |
portable (文件夹) | 内包含了 FreeRTOS 的移植文件 |
croutine.c | 协程相关文件 |
event_groups.c | 事件相关文件 |
list.c | 列表相关文件 |
queue.c | 队列相关文件 |
stream_buffer.c | 流式缓冲区相关文件 |
tasks.c | 任务相关文件 |
timers.c | 软件定时器相关文件 |
系统移植
系统移植需要将上述文件添到工程目录,需要注意的是portable
文件夹中移植文件的选择,针对不同的硬件平台需要选用不同的移植文件。
硬件平台 | 移植文件 |
---|---|
STM32F1(ARMCC) | RVDS/ARM_CM3 |
STM32F4/ STM32G4(ARMCC) | RVDS/ARM_CM4F |
STM32F7(ARMCC) | RVDS/ARM_CM7/r0p1 |
STM32H7(ARMCC) | RVDS/ARM_CM7/r0p1 |
STM32H5(ARMClang) | GCC/ ARM_CM33_NTZ |
同时在MemMang
文件夹下还存放着五个动态内存管理的文件,只需要选择一个使用即可。
文件 | 功能 |
---|---|
heap1.c | 最简单的方法,不允许释放内存。 |
heap2.c | 允许释放内存,但不合并相邻的空闲块。 |
heap3.c | 简单包装标准 malloc() 和 free() 以确保线程安全。 |
heap4.c | 合并相邻的空闲块以避免碎片化。包括绝对地址放置选项。 |
heap5.c | 在heap4.c的基础上,能够将堆跨越多个不相邻的内存区域。 |
系统配置
FreeRTOS支持通过宏定义设置通过预处理来“裁剪”系统,进而满足不同项目对系统的不同需求,这些宏的定义位置在FreeRTOSConfig.h
文件内。FreeRTOSConfig.h
这个文件需要自行配置,可以参照官方demo中的配置文件和官方文档中宏的作用自行修改。
配置文件:Customization - FreeRTOS™
中断管理
ARM Cortex-M系列的MPU都有如下三个用于屏蔽中断的寄存器:
寄存器名称 | 作用 |
---|---|
PRIMASK | 只有0位可读可写,设置为1能够屏蔽除 NMI 和 HardFault 外的所有异常和中断。 |
FAULTMASK | 只有0位可读可写,设置为 1 用于屏蔽除 NMI 外的所有异常和中断。 |
BASEPRI | 只有低 8 位[7:0]可读可写,屏蔽优先级低于其设置的值的中断。 |
FreeRTOS就是通过BASEPRI
寄存器来管理受其管理的中断的(优先级高于BASEPRI
寄存器设置的值的中断不受管理)
我们知道ARM Cortex-M系列有256个中断,其中16个为系统中断,240个为外部中断,有两个重要的系统中断PendSV
和 SysTick
,需要设置为最低优先级。
- SysTick: 系统滴答定时器中断,这个中断每隔1ms就触发一次用于判断当前所执行的任务是否为最高优先级是否需要进行任务切换,调用PenSV进行上下问切换。
- PendSV: 可挂起的系统调用中断,这个中断的服务函数主要进行执行任务的切换。
临界区:
临界区是指那些必须完整运行的区域,在临界区中的代码必须完整运行不能被打断。在运行临界区的代码的时候就需要将Free RTOS管理的中断关闭,也就是向BASEPRI
寄存器写入受FreeRTOS管理的最高优先级的值,进而将受管理的中断屏蔽。
上述操作通过下列四个宏定义进行:
宏 | 作用 |
---|---|
taskENTER_CRITICAL() | 用于在非中断中进入临界区 |
taskENTER_CRITICAL_FROM_ISR() | 用于从中断中进入临界区 |
taskEXIT_CRITICAL() | 用于从非中断中退出临界区 |
taskEXIT_CRITICAL_FROM_ISR(x) | 用于从中断中退出临界区 |
PS.从中断中进入临界区的区别在于相关函数会先读取BASEPRI
的值,并在函数的最后返回这个值,这是为了在后续从中断中退出临界区时恢复BASEPRI
寄存器的值。
列表与列表项
就是链表的应用
任务管理
创建任务分为静态创建和动态创建,静态创建堆栈大小固定编译前就已经确定,动态通过malloc函数创建任务堆栈。
动态创建任务的函数定义堆栈大小的参数单位为字
任务挂起函数并不支持嵌套,不论使用此函数重复挂起任务多少次,只需调用一次恢复任务的函数,那么任务就不再被挂起。
启动流程
消息队列
队列
队列用于消息在任务与任务,任务与中断之间的传递。队列中消息以环形缓冲区的形式存储,缓存区的大小在队列创建时确定,每种队列只能存储一种类型的消息。
函数 | 描述 |
---|---|
xQueueCreate() | 动态方式创建队列 |
xQueueCreateStatic() | 静态方式创建队列 |
xQueueSend() | 往队列的尾部写入消息 |
xQueueSendToBack() | 同 xQueueSend() |
xQueueSendToFront() | 往队列的头部写入消息 |
xQueueOverwrite() | 覆写队列消息(只用于队列长度为 1 的情况) |
xQueueSendFromISR() | 在中断中往队列的尾部写入消息 |
xQueueSendToBackFromISR() | 同 xQueueSendFromISR() |
xQueueSendToFrontFromISR() | 在中断中往队列的头部写入消息 |
xQueueOverwriteFromISR() | 在中断中覆写队列消息(只用于队列长度为 1 的情况) |
xQueueReceive() | 从队列头部读取消息,并删除消息 |
xQueuePeek() | 从队列头部读取消息 |
xQueueReceiveFromISR() | 在中断中从队列头部读取消息,并删除消息 |
xQueuePeekFromISR() | 在中断中从队列头部读取消息 |
prvLockQueue() | 队列上锁的函数 |
prvUnlockQueue() | 队列解锁的函数 |
队列集
一个队列只允许传递同一种数据类型,如果需要传递不同类型的数据就需要设置多个队列,然而对每一个队列都进行轮询效率低下,因此可以通过监听队列集的方式来监视所有被添加到队列集中的队列。任务在监听队列集时,一旦队列集中有任何队列中存在可读消息xQueueSelectFromSet()
函数都会返回这个可读队列,进而决定是否要读取这个队列。注意使用队列集功能,需要在 FreeRTOSConfig.h 文件中将配置项 configUSE_QUEUE_SETS 配置为 1。
函数 | 描述 |
---|---|
xQueueCreateSet() | 创建队列集 |
xQueueAddToSet() | 将队列添加到队列集中,被添加队列必须为空 |
xQueueRemoveFromSet() | 从队列集中移除队列,被移除队列必须为空 |
xQueueSelectFromSet() | 获取队列集中有有效消息的队列 |
xQueueSelectFromSetFromISR() | 在中断中获取队列集中有有效消息的队列 |
信号量
信号量用于保证任务之间的同步,以及公共资源的有序访问。
为了避免多个任务通过全局变量传递消息而造成公共资源的访问冲突,使用信号量来保证公共资源的有序访问,只有获得空闲信号量的任务才能访问公共资源,申请获取信号量而未获取到的任务将处于阻塞状态直至获取到信号量或者阻塞超时。通过给共享资源建立标志位的方法来实现上述效果,该标志表示该共享资源被占用情况。当一个任务在访问共享资源之前,就可以先对这个标志进行查询,从而在了解资源被占用的情况之后,再来决定自己的行为,这个标志位就是信号量。信号量是通过队列实现有许多种类:
- 二值信号量
- 计数型信号量
- 互斥信号量
- 递归互斥信号量
二值信号量
二值信号量是长度为1的队列,队列只有为空和满两种情况,对应信号量空闲和占有两种状态,也就对应标志位真假两种状态。
函数 | 描述 |
---|---|
xSemaphoreCreateBinary() | 使用动态方式创建二值信号量 |
xSemaphoreCreateBinaryStatic() | 使用静态方式创建二值信号量 |
xSemaphoreTake() | 获取信号量 |
xSemaphoreTakeFromISR(). | 在中断中获取信号量 |
xSemaphoreGive() | 释放信号量 |
xSemaphoreGiveFromISR() | 在中断中释放信号量 |
vSemaphoreDelete() | 删除信号量 |
计数信号量
计数信号量是长度大于1的队列,用于做多个资源的标志位或者事件计数。
函数 | 描述 |
---|---|
xSemaphoreCreateCounting() | 使用动态方式创建计数型信号量 |
xSemaphoreCreateCountingStatic() | 使用静态方式创建计数型信号量 |
xSemaphoreTake() | 获取信号量 |
xSemaphoreTakeFromISR() | 在中断中获取信号量 |
xSemaphoreGive() | 释放信号量 |
xSemaphoreGiveFromISR() | 在中断中释放信号量 |
vSemaphoreDelete() | 删除信号量 |
uxSemaphoreGetCount() | 获取计数型信号量资源数 |
互斥信号量
互斥信号量和二值信号量一样都是长度为一的队列,不同的是互斥信号量拥有优先级继承的机制,当底优先级的任务获取到信号量之后,高优先级任务再去获取信号量会被阻塞,同时低优先级的任务的优先级会被提升到和高优先级任务一样的程度,这样的操作叫做优先级的继承。这样做的目的是为了最大程度上避免优先级反转,即低优先级任务在获取信号量之后被中等优先级的任务抢占执行,导致高优先级的任务一直无法获取到信号量而被一直阻塞。
需要注意的是中断中不能申请互斥信号量,因为中断的优先级不受操作系统的管制。
函数 | 描述 |
---|---|
xSemaphoreCreateMutex() | 使用动态方式创建互斥信号量 |
xSemaphoreCreateMutexStatic() | 使用静态方式创建互斥信号量 |
xSemaphoreTake() | 获取信号量 |
xSemaphoreGive() | 释放信号量 |
vSemaphoreDelete() | 删除信号量 |
递归互斥信号量
递归互斥信号量是特殊的互斥信号量,在具备互斥信号量的优先级继承特性的同时它允许信号的持有任务多次申请获取信号量,在释放信号量的时候也需要释放相同次数才能真正释放信号量。
函数 | 描述 |
---|---|
xSemaphoreCreateRecursiveMutex() | 使用动态方式创建递归互斥信号量 |
xSemaphoreCreateRecursiveMutexStatic() | 使用静态方式创建递归互斥信号量 |
xSemaphoreTakeRecursive() | 获取递归互斥信号量 |
xSemaphoreGiveRecursive() | 释放递归互斥信号量 |
vSemaphoreDelete() | 删除信号量 |