FreeRTOS学习笔记
(这是我自己学习FreeRTOS整理的笔记,仅供参考)
第一部分:实现FreeRTOS内核
变量名:
定义变量时往往会把变量的类型当作前缀加在变量上
变量类型 | 前缀 |
---|---|
char型 | c |
short型 | s |
long型 | l |
portBASE_TYPE类型 | x |
如数据结构、任务句柄、队列句柄等定义 | x |
无符号型 | u |
指 针变量 | p |
无符号的char型 | uc |
char型的指针变量 | pc |
函数名:
函数名包含了函数返回值的类型、函数所在的文件名和函数的功能,如果是私有的函数,则会加一个prv(private)的前缀。
eg:vTaskPrioritySet()函数的返回值为void型,在task.c文件中定义。
宏:
均由大写字母表示,并配有小写字母的前缀,前缀用于表示该宏在哪个头文件定义。
注:信号量的函数都是一个宏定义,但是其函数的命 名方法是遵循函数的命名方法而不是宏定义的方法。
格式:
1个Tab键等于4个空格键。在编程时最好使用空格键。
portmacro.h文件中的数据类型:
1 #ifndef PORTMACRO_H
2 #define PORTMACRO_H
3
4 #include "stdint.h"
5 #include "stddef.h"
6
7
8 /* 数据类型重定义 */
9 #define portCHAR char
10 #define portFLOAT float
11 #define portDOUBLE double
12 #define portLONG long
13 #define portSHORT short
14 #define portSTACK_TYPE uint32_t
15 #define portBASE_TYPE long
16
17 typedef portSTACK_TYPE StackType_t;
18 typedef long BaseType_t;
19 typedef unsigned long UBaseType_t;
20
21 #if( configUSE_16_BIT_TICKS == 1 )(1)
22 typedef uint16_t TickType_t;
23 #define portMAX_DELAY ( TickType_t ) 0xffff
24 #else
25 typedef uint32_t TickType_t;
26 #define portMAX_DELAY ( TickType_t ) 0xffffffffUL
27 #endif
28
29 #endif/* PORTMACRO_H */
任务的定义与任务切换:
FreeRTOS的任务可认为是一系列独立任务的集合。每个任务在自己的环境中运行。在任何时刻,只有一个任务得到运行, FreeRTOS调度器决定运行哪个任务。
任务栈
其实就是一个预先定义好的全局数组,数据类型为StackType_t,大小由TASK1_STACK_SIZE这个宏来定 义,默认为128,单位为字,即512字节,这也是FreeRTOS推荐的最小的任务栈。
任务
是一个独立的函数,函数主体无限循环且不能返回。
每一个任务有一个对应的
任务控制块
,使得系统能够顺利的调度任务,数据类型为tsk
TCB
任务创建函数xTaskCreateStatic()
:将任务的栈、任务的函数实体以及任务的控制块最终需要联系起来,由系统进行统一调度。
将任务
插入就绪列表
中,就是通过将任务控制块的
xStateListItem
节点(数据类型为ListItem_t)插入就绪列表中来实现的。
调度器
是由几个全局变量和一些可以实现任务切换的函数组成。
注:系统调度的优先级要低于系 统的其他硬件中断优先级,即优先响应系统中的外部硬件中断,所以 SysTick和PendSV的中断优先级配置为最低。
RTOS中的延时叫作
阻塞延时
,即任务需要延时时,会放弃CPU的 使用权,CPU可以去做其他的事情,当任务延时时间到,重新获取CPU 使用权,任务继续运行。当任务需要延时而进入阻塞状态时,如果没有其他任务可以运行,RTOS都会为CPU创建一个空闲任务,这个时候CPU就运行空闲任务(优先级最低的任务)。在实际应用中,可在空闲任务中让单片机进入休眠或者低功耗等操作。
FreeRTOS中,数字越小,逻辑优先级也越小,0优先级最低(空闲任务)。
BASEPRI寄存器
:用于屏蔽系统可管理的中断,优先级
低于
该宏定义的中断
不受FreeRTOS管理
,
不可被屏蔽
,
大于等于
BASEPRI的值的中断会被
屏蔽
。
uxTopReadyPriority
在优化方法中担任的是一个
优先级位图表
的角色,即该变量的每个位对应任务的优先级,如果任务就绪,则将对应的位置1,反之清零。由uxTopReadyPriority的前导零个数就算找到了就绪任务的最高优先级。
任务延时列表
FreeRTOS定义了两个任务延时列表,任务延时列表维护着一条双向链表,每个节点代表了正在延时的任务,节点按照延时时间大小做升序排列。
当任务需要延时时,则先将任务挂起,即先将任务从就绪列表中删除,然后插入任务延时列表,同时更新下一个任务的解锁时刻变量xNextTaskUnblockTime的值。xNextTaskUnblockTime的值等于系统时基计数器的值xTickCount 加上任务需要延时的值xTicksToDelay。当系统时基计数器
xTickCount 的值与xNextTaskUnblockTime相等
时,就表示有
任务延时到期
了,需要将该
任务就绪
。
时间片
,就是同一个优先级下可以有多个任务,每个任务轮流享有相 的CPU时间,
享有CPU的时间
叫作时间片。在RTOS中,最小的时间单位为一个tick,即SysTick的中断周期。
注:taskRESET_READY_PRIORITY()函数的妙处在于当清除优先级位图 表uxTopReady-Priority中相应的位时,会先判断当前优先级链表下是否还有其他任务,如果有则不清零。
第二部分、FreeRTOS内核应用开发
**任务中必须使用FreeRTOS提供的延时函数, 不能使用裸机编程中的延时:**这两种延时的区别是:FreeRTOS中的延时是阻塞延时,即调用vTaskDelay()函数时,当前任务会被挂起,调度器会切换到其他就绪的任务,从而实现多任务。如果还是使用裸机编程中的那种延时,那么整个任务就成了一个死循环,如果恰好该任 务的优先级是最高的,那么系统永远都是在这个任务中运行,比它优先级更低的任务无法运行,根本无法实现多任务。
对于
控制频率要求较高的较重要
的任务,
阻塞的时间较短
。
“万事俱备,只欠东风”法
:
1、硬件初始化
2、RTOS系统初始化
3、创建各种任务
4、启动RTOS调度器,开始任务调度
注:任务实体通常是一个
不带返回值
的无限循环的C函数,函数体
必须有阻塞
的情况出现
“小心翼翼,十分谨慎”法:
1、硬件初始化
2、RTOS系统初始化
3、创建一个初始任务,然后在这个初始任务中创建各种应用任务,当所有任务都创建成功后,启动任务把自己删除
4、启动RTOS调度器,开始任务调度
在Cortex-M3架构中,FreeRTOS为了任务启动和任务切换使用了3 个异常:SVC、PendSV和SysTick:
-
SVC
(系统服务调用,简称系统调用):用于任务启动,用户程序使用SVC发出对系统服务函数的呼叫请求,以这种方 法调用它们来间接访问硬件,它就会产生一个SVC异常。 -
PendSV
(可挂起系统调用):用于完成任务切换,它是可以像普通中断一样被挂起的,其最大特性是如果当前有优先级比它高的中断在运行,PendSV会延迟执行,直到高优先级中断执行完毕,这样产生的 PendSV中断就不会打断其他中断的运行。 - **SysTick:**用于产生系统节拍时钟,提供一个时间片,如果多个任务共享同一个优先级,则每次SysTick中断,下一个任务将获得一个时间片。
任务状态通常分为以下4种:
- **就绪(ready)态:**该任务在就绪列表中,就绪的任务已经具备执行能力,只等待调度器进行调度,新创建的任务会初始化为就绪态。
- **运行(running)态:**该状态表明任务正在执行,此时它占用处理器,FreeRTOS调度器选择运行的永远是处于最高优先级的就绪态任务,当任务开始运行的一刻,其任务状态就变成了运行态。
- **阻塞(blocked)态:**如果任务当前正在等待某个时序或外部中 断,我们就说这个任务处于阻塞态,该任务不在就绪列表中,包含任务挂起、任务延时、任务正在等待信号量、读写队列或者等待读写事件等。
-
挂起(suspended)态:
处于挂起态的任务对调度器而言是不可见的,让一个任务进入挂起态的唯一办法就是调用
vTaskSuspend()函数
;而恢复一个挂起态的任务的唯一途径是调用
vTaskResume()或 vTaskResumeFromISR()函数
。
常用的任务函数
任务挂起函数:
-
vTaskSuspend()
:用于
挂起指定任务
。被挂起的任务绝不会得 到CPU的使用权,不管该任务具有什么优先级。
(注:要使用vTaskSuspend(),则必须将宏定义INCLUDE_vTaskSuspend配置为1)
-
vTaskSuspendAll()
:可将
所有任务挂起
,直接将调度器锁定,使uxScheduler-Suspended变量
加1
(注:恢复调度器可以调用xTaskResumeAll()函数,调用了多少次 vTaskSuspendAll(),就要调用多少次xTaskResumeAll()进行恢复)
任务恢复函数:
-
vTaskResume()
:
任务恢复
就是让挂起的任务重新进入就绪态,恢复的任务会保留挂起前的状态信息,在 恢复时根据挂起时的状态继续运行。
(注:无论任务在挂起时调用过多少次vTaskSuspend()函数,只需要调 用一次vTaskResume()函数即可将任务恢复运行)
-
xTaskResumeFromISR()
:
(危险)
同样用于
恢复
被挂起的任务,专门用在
中断服务程序
中 -
xTaskResumeAll()
:
恢复调度器
,使uxScheduler-Suspended变量
减1
任务删除函数:
-
vTaskDelete()
:用于
删除
一个任务,当一个任务
删除另一个任务
时,形参为要删除的任务创建时返回的
任务句柄
。如果是
删除自身
,则形参为
NULL
,删除自身需要到空闲任务中
(注:删除任务时,只会自动释放内核本身分配给任务的内存。应用程序(而不是内核)分配给任务的内存或其他资源必须是删除任务时由 应用程序显式释放。)
任务延时函数:
-
vTaskDelay()
:
相对延时函数
,调用该函数后,任务将进入阻塞状态,单位为系统节拍周期。 在
任务主体之后
调用。
(注:vTaskDelay()并
不适用于周期性
执行任务的场合。此外,其他任务和中断活动也会影响到vTaskDelay()的调用)
-
vTaskDelayUntil()
:
绝对延时函数
,常用于较
精确的周期
运行任务,比如希望某个任务以固定频率定期执行,而不受外部的影响,任务从上一次运行开始到下一次运行开始的时间间隔是绝对的,而不是相对的。在
任务主体之前
调用。
(注:xTimeIncrement:任务周期时间。 pxPreviousWakeTime:上一次唤醒任务的时间点。 xTimeToWake:本次要唤醒任务的时间点。 xConstTickCount:进入延时的时间点。)
中断服务函数:
中断服务程序最好保持
精简短小
、快进快出,一般在中断服务函数中
只标记事件
的发生,然后通知任务,让对应任务去执行相关处理
(注:一般来说处理时间更短的任务,其优先级应设置得更高一些。)
消息队列
消息队列是
异步
通信,常用于
任务间通信
,可以在任务与任务间、中断与任务间传递信息
任务或者中断
服务程序都可以给消息队列发送消息到
队尾
;
紧急
消息发送到
队头
队列存储的数据是
原始数据
,而不是原始数据的引用。
使用队列模块的典型流程:
- 创建消息队列
- 写队列操作
- 读队列操作
- 删除队列
常用的消息队列函数:
- **xQueueCreate() **:消息队列动态创建函数,用于创建一个新的队列并返回可用于访问这个队 列的队列句柄
- xQueueCreateStatic() :消息队列静态创建函数(很少用)
-
vQueueDelete()
: 消息队列
删除
函数
消息发送函数:
-
xQueueSend()
: 消息队列
发送
函数 ,等同于xQueueSendToBack(),向队尾发送一个队列消息。 *(注:该函数绝对不能在中断服务程序中被调用,
中断
中必须使用带有中断保护功能的**xQueueSendFromISR()*
来代替)
-
xQueueSendFromISR()
:用于在
中断服务程序
中向队列尾部
发送
一个队列消息 -
xQueueSendToFront()
: 用于向队列
队首发送
一个消息。 *(注:该函数绝对不能在中断服务程序中被调用,
中断
中必须使用带有中断保护功能的**xQueueSendToFrontFromISR()*
来代替)
-
xQueueGenericSendFromISR()
: 消息队列发送函数,只能在
中断
中执行,是
不带阻塞机制
的
消息读取函数:
-
xQueueReceive()
: 从一个队列中
接收消息
并把消息从队列中
删除
,中断中用xQueue-ReceiveFromISR()来代替 -
xQueuePeek()
: 和xQueueReceive()几乎一样,但接收后
不删除
消息 -
xQueueReceiveFromISR()
: 中断中接收消息,不带阻塞机制 -
xQueuePeekFromISR()
: 中断中接收消息,不带阻塞机制 -
xQueueGenericReceive()
: 在
任务中读取
消息的函数
信号量
信号量
(semaphore)是一种实现任务间通信的机制,是一个非负整数
二值信号量
:既可以用于临界资源访问,也可以用于同步功能。更适用于
同步
互斥信号量
:有优先继承机制,更适用于
临界资源的访问
计数信号量
:可以用于资源管理,允许多个任务获取信号量访问共享资源,但限制任务最大数目。
常用的信号量函数
信号量创建函数:
-
xSemaphoreCreateBinary()
:创建一个
二值信号量
,并返回一 个句柄。 -
xSemaphoreCreateCounting()
:创建一个
计数信号量
-
xSemaphoreCreateMutex()
:创建一个互斥量,并返回一个互斥量句柄 -
xSemaphoreCreateRecursiveMutex()
:创建一个
递归互斥量
信号量删除函数:
-
vSemaphoreDelete()
:删除一个信号量,包括二值信号量、计 数信号量、互斥量和递归互斥量。
(注:如果有任务阻塞在该信号量上,那 么不要删除该信号量)
信号量释放函数:
(注:释放信号量实际上是一次
入队
操作,并且不允许入队阻塞)
-
xSemaphoreGive()(任务)
:不能在中断中使用,不能释放由函数 xSemaphoreCreateRecursiveMutex()创建的递归互斥量 -
xSemaphoreGiveFromISR()(中断)
:释放一个信号量,带中断保护
(注:
互斥量的释放
只能在
任务中进行
)
-
xSemaphoreGiveRecursive()
:释放
递归
互斥量
信号量获取函数:
(注:获取信号量实际上是一次消息
出队
操作,阻塞时间xBlockTime由用户指定)
-
xSemaphoreTake()(任务)
:获取信号量,不带中断保护 -
xSemaphoreTakeFromISR()(中断)
:中断中使用,不带阻塞机制 -
xSemaphoreTakeRecursive()
:获取
递归
互斥量
优先级翻转现象
: 高优先级任务在等待低优先级的任务执行
优先级继承
:系统会将现在持有该互斥量的任务的优先级临时提升到与高优先级任务的优先级相 同,这个优先级提升的过程叫作优先级继承
(注:不能解决优先级翻转问题,只能将这种情况的影响降到最低)
递归互斥量
更适用于任务可能多次获取互斥量的情况,这样可以避免同一任务多次递归持有而造成死锁的问题。
互斥量不能在中断服务函数中使用
事件
FreeRTOS提供的事件具有如下特点:
- 事件只与任务相关联,事件相互独立
- 事件仅用于同步,不提供数据传输功能
- 事件无排队性,即多次向任务设置同一事件(如果任务还未来得及读取),等效于只设置一次
- 允许多个任务对同一事件进行读写操作
- 支持事件等待超时机制
事件信息标记有3个属性,分别是
逻辑与
、
逻辑或
以及
是否清除标记
事件
接收成功后
,必须使用
xClearOnExit选项清除已接收
的事件类型,否则不会清除已接收的事件。
可以自定义通过传入参数
xWaitForAllBits选择读取模式
——是等待所有感兴趣的事件还是等待感兴趣的任意一个事件。
常用事件函数
事件创建函数:
-
xEventGroupCreate()
:创建一个事件组,并返回对应的句柄
事件删除函数:
-
vEventGroupDelete()
:将事件删除
事件组置位函数:
-
xEventGroupSetBits()(任务)
:置位事件组中指定的位,当位被置位之后,阻塞在该位上的任务将会被解锁。
(注:一般操作我们是用宏定义
*#define EVENT(0x01<<x)*
来实现,“<<x”表示写入事件组的bit x)
-
xEventGroupSetBitsFromISR()(中断)
:真正调用的也是 xEventGroupSetBits()函数,只不过是在守护任务中进行调用的,所以它真正执行的上下文环境依旧是在任务中
事件等待函数:
-
xEventGroupWaitBits()
: 用于获取事件组中的一个或多个事件发生标志,当要读取的事件标志位没有被置位时,任务将进入阻塞等待状态。
事件组清除指定位函数:
-
xEventGroupClearBits()
(任务) -
xEventGroupClearBitsFromISR()
(中断)
软件定时器
软件定时器回调函数的上下文是任务,回调函数也要快进快出,而且回调函数中不能有任何阻塞任务运行的情况
(注:为了更好地响应,软件定时器任务的优先级应设置为所有任务中
最高
的。)
软件定时器用到的列表 :
-
pxCurrentTimerList
:系统新创建并激活的定时器都会以超时时间
升序
的方式插入pxCurrentTimerList列表中。 -
pxOverflowTimerList
:此列表是在软件定时器
溢出时使用
,作 用与pxCurrentTimerList一致。
常用软件定时器函数:
软件定时器创建函数:
-
xTimerCreate()
:用于创建一个软件定时器,并返回一个句柄。
软件定时器启动函数:
(注:软件定时器在创建完成时是处于休眠状态的,需要用相关函数将软件定时器激活)
-
xTimerStart() (任务)
: 让处于休眠状态的定时器开始工作。 -
xTimerStartFromISR() (中断)
: 在中断中启动软件定时器
软件定时器停止函数:
-
xTimerStop() (任务)
: 停止一个已经启动的软件定时器 -
xTimerStopFromISR() (中断)
:中断中停止一个已经启动的软件定时器
软件定时器删除函数:
-
xTimerDelete()
: 删除一个已经被创建成功的软件定时器(要在软件定时器任务中进行)
任务通知
任务通知的限制:
-
只能有一个任务接收通知消息,因为必须
指定接收通知的任务
。 -
只有等待通知的任务可以被阻塞
,发送通知的任务在任何情况下都不会因为发送失败而进入阻塞态。
常用任务通知函数
发送任务通知函数:
-
xTaskNotifyGive()
:向一个任务发送通知,并将对方的任务通知值加1。该函数可以作为二值信号量和计数信号量的一种轻量型的实现,速度更快。对象任务在等待任务通知时应该
使用函数ulTaskNotifyTake() 而不是xTaskNotifyWait()
。 -
vTaskNotifyGiveFromISR()
:中断保护版本 -
xTaskNotify()
:用于在任务中直接向另外一个任务发送一个事件,接收到该任务通知的任务有可能解锁。 -
xTaskNotifyFromISR()
:中断保护版本 -
xTaskGenericNotifyFromISR()
:中断中发送任务通知
通用
函数,xTaskNotify-FromISR()、xTaskNotifyAndQueryFromISR()等函数都是以此函数为基础 -
xTaskNotifyAndQuery()
:与xTaskNotify()很像,多了一个附加的参数pulPreviousNotifyValue用于
回传
接收任务的上一个通知值 -
xTaskNotifyAndQueryFromISR()
:中断保护版本
获取任务通知函数:
(注:只能在
任务中
,所有的获取任务通知API函数都带有指定
阻塞超时
时间参数,当任务因为等待通知而进入阻塞时,用来指定任务的阻塞时间,这些超时机制与FreeRTOS的消息队列、信号量、事件等的超时机制一致。)
-
ulTaskNotifyTake()
:为代替二值信号量和计数信号量而专门设计的 -
xTaskNotifyWait()
: 用于实现全功能版的等待任务通知,根据用户指定的参数的不同,可以灵活地用于实现轻量级的消息队列、二值信号量、计数信号量和事件组功能,并带有超时等待。
内存(RAM)管理
heap1.c方案的特点:
- 用于从不删除任务、队列、信号量、互斥量等的应用程序(实际上大多数使用Free-RTOS的应用程序都符合这个条件)。
- 函数的执行时间是确定的,并且不会产生内存碎片。
变量
xNextFreeByte
用来定位下一个空闲的内存堆位置。
静态变量
pucAlignedHeap
是一个指向对齐后的内存堆的起始地址
heap2.c方案的特点:
- 可以用于那些反复删除任务、队列、信号量等内核对象且不担心内存碎片的应用程序。
- 如果我们的应用程序中的队列、任务、信号量等的顺序不可预测,也有可能导致内存碎片。
- 具有不确定性,但是效率比标准C库中的malloc()函数高得多。
- 不能用于内存分配和释放是随机大小的应用程序。
heap3.c方案的特点:
- 需要链接器设置一个堆,malloc()和free()函数由编译器提供
- 具有不确定性
- 很可能增大RTOS内核的代码大小
heap4.c方案的特点:
能将相邻的两个空闲内存块
合并
起来
(注:heap4.c内存管理方案的空闲块链表不是以内存块的大小进行排序的,而是以
内存块起始地址大小排序
,小的在前)
- 可用于重复删除任务、队列、信号量、互斥量等的应用程序。
- 可用于分配和释放随机字节内存的应用程序
- 具有不确定性,但效率比标准C库中的malloc()函数高
heap5.c方案的特点:
在实现动态内存分配时与heap4.c方案一样,采用最佳匹配算法和合并算法,并且允许内存
堆跨越多个非连续的内存区
。
中断管理
异常分为
同步异常
和
异步异常
由
内部事件
引起的异常称为
同步异常
异步异常
主要指由
外部异常源
产生的异常,
中断
属于异步异常
参考文献:FreeRTOS内核实现与应用开发实战指南:基于STM32 (刘火良, 杨森)