FreeRTOS学习资料:
安富莱论坛FreeRTOS教程
FreeRTOS源码下载链接
第1章 为什么选用FreeRTOS
1.1 FreeRTOS优势
FreeRTOS优势 最大的优势就是开源免费,商业使用的话不需要用户公开源代码,也不存在任何版权问题,是当前小型嵌入式操作系统市场使用率最高的。 去年的全球嵌入式市场分析报告中,FreeRTOS 占据了 20%。
1.2 移植的难易程度
FreeRTOS 的移植比较简单,只需要用户添加需要的源码文件,不需要做任何的底层工作,再添加三
个宏定义即可,详情可以看第 5 章 FreeRTOS 操作系统移植。
1.3 上手难易程度
作为开源免费的 RTOS,官方配套的手册在 RTOS 的基础知识说明、API 函数说明及其举例方面做的
都非常好,用户上手比较容易。随着后面章节的学习,大家会体会到这一点。
第2章 嵌入式实时操作系统介绍
2.1 嵌入式系统定义
一般定义
以应用为中心、以计算机技术为基础、软件硬件可裁剪、功能、可靠性、成本、体积、功耗严格要求
的专用计算机系统。
2.2 实时和分时操作系统区别
按对外部事件的响应能力来分类,嵌入式操作系统有分时操作系统和实时操作系统。
- 如果操作系统能使计算机系统及时的响应外部事件请求,并能控制所有实时设备和实时任务协调运行,且能在一个规定的时间内完成对事件的处理,那么这种系统就称为实时操作系统(RTOS)。
- 按时间的正确程度来分,实时操作系统又分为硬件的实时操作系统和软件的实时操作系统。系统必须在极其严格的时间内完成的任务叫做硬件的实时操作系统,如果不是很严格的话就是软件的实时操作系统。
- 分时操作系统就是按时间片轮转完成各个任务。
第7章 FreeRTOSConfig 配置文件详解
- 7.2.7 configMINIMAL_STACK_SIZE
此参数用于定义空闲任务的栈空间大小,单位字,即 4 字节。 - 7.2.8 configTOTAL_HEAP_SIZE
定义堆大小,FreeRTOS 内核,用户动态内存申请,任务栈,任务创建,信号量创建,消息队列创建等都需要用这个空间。
第10章 FreeRTOS 任务管理
10.1 单任务系统
学习多任务系统之前,我们先来回顾下单任务系统的编程框架,即裸机时的编程框架。裸机编程主要是采用超级循环(super-loops)系统,又称前后台系统。应用程序是一个无限的循环,循环中调用相应的函数完成相应的操作,这部分可以看做后台行为;中断服务程序处理异步事件,这部分可以看做是前台行为。后台也可以叫做任务级,前台也叫作中断级。
对于前后台系统的编程思路主要有以下两种方式:
10.1.1 查询方式
10.1.2 中断方式
对于查询方式无法有效执行紧急任务的情况,采用中断方式就有效地解决了这个问题,下面是中断方式简单的流程图
采用中断和查询结合的方式可以解决大部分裸机应用,但随着工程的复杂,裸机方式的缺点就暴露出来了:
1)必须在中断(ISR)内处理时间关键运算:
- ISR 函数变得非常复杂,并且需要很长执行时间。
- ISR 嵌套可能产生不可预测的执行时间和堆栈需求。
2)超级循环和 ISR 之间的数据交换是通过全局共享变量进行的:
- 应用程序的程序员必须确保数据一致性。
3)超级循环可以与系统计时器轻松同步,但:
- 如果系统需要多种不同的周期时间,则会很难实现。
- 超过超级循环周期的耗时函数需要做拆分。
- 增加软件开销,应用程序难以理解。
4)超级循环使得应用程序变得非常复杂,因此难以扩展:
- 一个简单的更改就可能产生不可预测的副作用,对这种副作用进行分析非常耗时。
- 超级循环概念的这些缺点可以通过使用实时操作系统 (RTOS) 来解决。
10.2 多任务系统
针对这些情况,使用多任务系统就可以解决这些问题了。下面是一个多任务系统的流程图:
多任务系统或者说 RTOS 的实现,重点就在这个调度器上,而调度器的作用就是使用相关的调度算法来决定当前需要执行的任务。如上图所示的那样,创建了任务并完成 OS 初始化后,就可以通过调度器来决定任务 A,任务 B 和任务 C 的运行,从而实现多任务系统。另外需要初学者注意的是,这里所说的多任务系统同一时刻只能有一个任务可以运行,只是通过调度器的决策,看起来像所有任务同时运行一样。
FreeRTOS 就是一款支持多任务运行的实时操作系统,具有时间片,抢占式和合作式三种调度方法。
通过 FreeRTOS 实时操作系统可以将程序函数分成独立的任务,并为其提供合理的调度方式。
10.3 FreeRTOS 的任务栈设置
不管是裸机编程还是 RTOS 编程,栈的分配大小都非常重要。局部变量,函数调用时的现场保护和返回地址,函数的形参,进入中断函数前和中断嵌套等都需要栈空间,栈空间定义小了会造成系统崩溃。
不同于裸机编程,在 RTOS 下,每个任务都有自己的栈空间。对于 FreeRTOS 来说,任务栈空间是在任务创建的时候从FreeRTOSConfig.h 文件中定义的 heap 空间中申请的#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) )
具体每个任务的栈大小是在创建 FreeRTOS 的任务时进行设置的
10.4 FreeRTOS 的系统栈设置
任务栈不使用任务的栈空间,哪里使用这里的栈空间呢?答案就在中断函数和中断嵌套。
- 由于 Cortex-M3 和 M4 内核具有双堆栈指针,MSP 主堆栈指针和 PSP 进程堆栈指针,或者叫 PSP任务堆栈指针也是可以的。在 FreeRTOS 操作系统中**,主堆栈指针 MSP 是给系统栈空间使用的,进程堆栈指针 PSP 是给任务栈使用的**。也就是说,在 FreeRTOS 任务中,所有栈空间的使用都是通过
PSP 指针进行指向的。一旦进入了中断函数以及可能发生的中断嵌套都是用的 MSP 指针。这个知识点要记住它,当前可以不知道这是为什么,但是一定要记住。 - 实际应用中系统栈空间分配多大,主要是看可能发生的中断嵌套层数,下面我们就按照最坏执行情况进行考虑,所有的寄存器都需要入栈,此时分为两种情况:
64 字节
对于 Cortex-M3 内核和未使用 FPU(浮点运算单元)功能的 Cortex-M4 内核在发生中断时需要将 16 个通用寄存器全部入栈,每个寄存器占用 4 个字节,也就是 16*4 = 64 字节的空间。
可能发生几次中断嵌套就是要 64 乘以几即可。当然,这种是最坏执行情况,也就是所有的寄存器都入栈。
(注:任务执行的过程中发生中断的话,有 8 个寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余寄存器入栈以及发生中断嵌套都是用的系统栈)
200 字节
对于具有 FPU(浮点运算单元)功能的 Cortex-M4 内核,如果在任务中进行了浮点运算,那么在发生中断的时候除了 16 个通用寄存器需要入栈,还有 34 个浮点寄存器也是要入栈的,也就是(16+34)*4 = 200 字节的空间。当然,这种是最坏执行情况,也就是所有的寄存器都入栈。
(注:任务执行的过程中发送中断的话,有 8 个通用寄存器和 18 个浮点寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余通用寄存器和浮点寄存器入栈以及发生中断嵌套都是用的系统栈
10.5FreeRTOS 的任务状态
FreeRTOS 的运行支持以下四种状态:
Running—运行态
当任务处于实际运行状态被称之为运行态,即 CPU 的使用权被这个任务占用。
Ready—就绪态
处于就绪态的任务是指那些能够运行(没有被阻塞和挂起),但是当前没有运行的任务,因为同优先
级或更高优先级的任务正在运行。
Blocked—阻塞态
由于等待信号量,消息队列,事件标志组等而处于的状态被称之为阻塞态,另外任务调用延迟函数也
会处于阻塞态。
Suspended—挂起态
类似阻塞态,通过调用函数 vTaskSuspend()对指定任务进行挂起,挂起后这个任务将不被执行,只
有调用函数 xTaskResume()才可以将这个任务从挂起态恢复。
10.6FreeRTOS 启动
使用如下函数即可启动 FreeRTOS:
vTaskStartScheduler()
10.7FreeRTOS 的任务创建
使用如下函数可以实现 FreeRTOS 的任务创建:
xTaskCreate()
10.8FreeRTOS 的任务删除
使用如下函数可以实现 FreeRTOS 的任务删除:
vTaskDelete()
10.9FreeRTOS 的任务挂起
使用如下函数可以实现 FreeRTOS 的任务挂起:
xTaskSuspend()
10.10 FreeRTOS 的任务恢复
使用如下函数可以实现 FreeRTOS 的任务恢复:
xTaskResume()
10.11 FreeRTOS 的任务恢复(中断方式)
使用如下函数可以实现 FreeRTOS 的任务恢复(中断方式):
xTaskResumeFromISR()
10.12 FreeRTOS 的空闲任务
几乎所有的小型 RTOS 中都会有一个空闲任务,空闲任务属于系统任务,是必须要执行的,用户程
序不能将其关闭。不光小型系统中有空闲任务,大型的系统里面也有的,比如 WIN7
第11章 FreeRTOS 任务栈大小确定及其溢出检测
11.1任务栈大小的确定
在基于 RTOS 的应用设计中,每个任务都需要自己的栈空间,应用不同,每个任务需要的栈大小也是
不同的。将如下的几个选项简单的累加就可以得到一个粗略的栈大小:
- 函数的嵌套调用,针对每一级函数用到栈空间的有如下四项:
- 函数局部变量。
- **函数形参,一般情况下函数的形参是直接使用的 CPU 寄存器,不需要使用栈空间,但是这个函数中如果还嵌套了一个函数的话,这个存储了函数形参的 CPU 寄存器内容是要入栈的。**所以建议大家也把这部分算在栈大小中。
- 函数返回地址,针对 M3 和 M4 内核的 MCU,一般函数的返回地址是专门保存到 LR(Link Register)寄存器里面的,如果这个函数里面还调用了一个函数的话,这个存储了函数返回地址的 LR 寄存器内容是要入栈的。所以建议大家也把这部分算在栈大小中。
- 函数内部的状态保存操作也需要额外的栈空间。
- 任务切换,任务切换时所有的寄存器都需要入栈,对于带 FPU 浮点处理单元的 M4 内核 MCU 来说,FPU 寄存器也是需要入栈的。
- 针对 M3 内核和 M4 内核的 MCU 来说,在任务执行过程中,如果发生中断:
- M3 内核的 MCU 有 8 个寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余寄存器入栈以及发生中断嵌套都是用的系统栈。
- M4 内核的 MCU 有 8 个通用寄存器和 18 个浮点寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余通用寄存器和浮点寄存器入栈以及发生中断嵌套都是用的系统栈。
- 进入中断以后使用的局部变量以及可能发生的中断嵌套都是用的系统栈,这点要注意。
函数栈大小确定
函数的栈大小计算起来是比较麻烦的,那么有没有简单的办法来计算呢?有的,一般 IDE 开发环境都有这样的功能,比如 MDK 会生成一个 htm 文件,通过这个文件用户可以知道每个被调用函数的最大栈需求以及各个函数之间的调用关系。但是 MDK 无法确定通过函数指针实现函数调用时的栈需求。另外,发生中断或中断嵌套时的现场保护需要的栈空间也不会统计。
11.2什么是栈溢出
略
11.3FreeRTOS 的栈溢出检测机制
FreeRTOS 提供了两种栈溢出检测机制,这两种检测都是在任务切换时才会进行:
- 方法一
在任务切换时检测任务栈指针是否过界了,如果过界了,在任务切换的时候会触发栈溢出钩子函数。
void vApplicationStackOverflowHook( TaskHandle_t xTask, signed char *pcTaskName ); - 方法二
任务创建的时候将任务栈所有数据初始化为 0xa5,任务切换时进行任务栈检测的时候会检测末尾的 16 个字节是否都是 0xa5,通过这种方式来检测任务栈是否溢出了。
使用方法二需要用户在 FreeRTOSConfig.h 文件中配置如下宏定义:
#define configCHECK_FOR_STACK_OVERFLOW 2
第12章 FreeRTOS 中断优先级配置(重要)
12.1NVIC 基础知识
NVIC 的全称是 Nested vectored interrupt controller,即嵌套向量中断控制器。
对于 M3 和 M4 内核的 MCU,每个中断的优先级都是用寄存器中的 8 位来设置的。8 位的话就可以设置 2^8 = 256 级中断,实际中用不了这么多,所以芯片厂商根据自己生产的芯片做出了调整。比如 ST的 STM32F1xx 和 F4xx 只使用了这个 8 位中的高四位[7:4],低四位取零,这样 2^4=16,只能表示 16级中断嵌套。
12.2使用 FreeRTOS 时如何配置外设 NVIC
强烈推荐用户将 Cortex-M3 内核的 STM32F103 和 Cortex-M4 内核的 STM32F407 以及STM32F429 的 NVIC 优先级分组设置为 4,即:NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);这样中断优先级的管理将非常方便。这个也是官方强烈建议的。此函数在 bsp_Init 中第一个被调用
第13章 FreeRTOS 任务优先级修改及其分配方案
13.3中断优先级和任务优先级区别
部分初学者也容易在这两个概念上面出现问题。简单的说,这两个之间没有任何关系,不管中断的优先级是多少,中断的优先级永远高于任何任务的优先级,即任务在执行的过程中,中断来了就开始执行中断服务程序。
另外对于 STM32F103,F407 和 F429 来说,中断优先级的数值越小,优先级越高。而 FreeRTOS的任务优先级是,任务优先级数值越小,任务优先级越低。
第14章 任务调度—抢占式,时间片和合作式
14.2FreeRTOS 支持的调度方式
FreeRTOS 操作系统支持三种调度方式:抢占式调度,时间片调度和合作式调度。实际应用主要是抢占式调度和时间片调度,合作式调度用到的很少。
- 抢占式调度
每个任务都有不同的优先级,任务会一直运行直到被高优先级任务抢占或者遇到阻塞式的 API 函数,比如 vTaskDelay。 - 时间片调度
每个任务都有相同的优先级,任务会运行固定的时间片个数或者遇到阻塞式的 API 函数,比如vTaskDelay,才会执行同优先级任务之间的任务切换。
第15章 FreeRTOS 临界段和开关中断
15.1临界段
代码的临界段也称为临界区,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即开中断。
FreeRTOS 源码中就有多处临界段的处理,跟 FreeRTOS 一样,uCOS-II 和 uCOS-III 源码中都是有临界段的,而 RTX 的源码中不存在临界段。另外,除了 FreeRTOS 操作系统源码所带的临界段以外,用户写应用的时候也有临界段的问题,比如以下两种:
- 读取或者修改变量(特别是用于任务间通信的全局变量)的代码,一般来说这是最常见的临界代码。
- 调用公共函数的代码,特别是不可重入的函数,如果多个任务都访问这个函数,结果是可想而知的。
总之,对于临界段要做到执行时间越短越好,否则会影响系统的实时性。
* 一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
FreeRTOS 任务代码中临界段的进入和退出主要是通过操作寄存器 basepri 实现的。进入临界段前操作寄存器 basepri 关闭了所有小于等于宏定义 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY所定义的中断优先级,这样
**临界段代码就不会被中断干扰到,而且实现任务切换功能的 PendSV 中断和滴答定时器中断是最低优先级中断,所以此任务在执行临界段代码期间是不会被其它高优先级任务打断的。**退出临界段时重新操作 basepri 寄存器,即打开被关闭的中断(这里我们不考虑不受 FreeRTOS 管理的更高优先级中断)。FreeRTOS 进入和退出临界段的函数如下:
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
上面这两个函数是供用户调用的,其中函数 taskENTER_CRITICAL 是进入临界段,函数taskEXIT_CRITICAL 是退出临界段。进一步跟踪宏定义的实现如下:
#define portENTER_CRITICAL() vPortEnterCritical()
#define portEXIT_CRITICAL() vPortExitCritical()
第16章 FreeRTOS 调度锁,任务锁和中断锁
16.1调度锁
调度锁就是 RTOS 提供的调度器开关函数,如果某个任务调用了调度锁开关函数,处于调度锁开和调度锁关之间的代码在执行期间是不会被高优先级的任务抢占的,即任务调度被禁止。这一点要跟临界段的作用区分开,调度锁只是禁止了任务调度,并没有关闭任何中断,中断还是正常执行的。而临界段进行了开关中断操作。
16.2中断锁
中断锁就是 RTOS 提供的开关中断函数,FreeRTOS 没有专门的中断锁函数,使用 15.3 小节里面介绍的中断服务程序临界段处理函数就可以实现同样效果。
16.3任务锁
简单的说,为了防止当前任务的执行被其它高优先级的任务打断而提供的锁机制就是任务锁。FreeRTOS 也没有专门的任务锁函数,但是使用 FreeRTOS 现有的功能有两种实现方法:
- 通过给调度器加锁实现
- 通过关闭任务切换中断 PendSV 和系统时钟节拍中断 Systick
第17章 FreeRTOS 系统时钟节拍和时间管理
17.1FreeRTOS 的时钟节拍
任何操作系统都需要提供一个时钟节拍,以供系统处理诸如延时、超时等与时间相关的事件。
时钟节拍是特定的周期性中断,这个中断可以看做是系统心跳。中断之间的时间间隔取决于不同的应
用,一般是 1ms – 100ms。时钟的节拍中断使得内核可以将任务延迟若干个时钟节拍,以及当任务等待事件发生时,提供等待超时等依据。时钟节拍率越快,系统的额外开销就越大。
对于 Cortex-M3 内核的 STM32F103 和 Cortex-M4 内核的 STM32F407 以及 F429,教程配套的例
子都是用滴答定时器来实现系统时钟节拍的。
滴答定时器 Systick
SysTick 定时器被捆绑在 NVIC 中,用于产生 SysTick 异常(异常号:15),滴答定时器是一个 24 位
的递减计数器,支持中断。使用比较简单,专门用于给操作系统提供时钟节拍。
FreeRTOS 的系统时钟节拍可以在配置文件 FreeRTOSConfig.h 里面设置:
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
如上所示的宏定义配置表示系统时钟节拍是 1KHz,即 1ms。
17.2.1时间延迟
FreeRTOS 中的时间延迟函数主要有以下两个作用:
- 为周期性执行的任务提供延迟。
- 对于抢占式调度器,让高优先级任务可以通过时间延迟函数释放 CPU 使用权,从而让低优先级任务
可以得到执行。
17.2.2FreeRTOS 的时间相关函数
FreeRTOS 时间相关的函数主要有以下 4 个:
- vTaskDelay () //函数 vTaskDelay 用于任务的延迟。
- vTaskDelayUntil () //函数 vTaskDelayUntil 用于周期性延迟。
- xTaskGetTickCount() //函数 xTaskGetTickCount 用于获取系统当前运行的时钟节拍数。
- xTaskGetTickCountFromISR() //函数 xTaskGetTickCountFromISR 用于获取系统当前运行的时钟节拍数。此函数用于在中断服务程序里面调用,如果在任务里面调用的话,需要使用函数xTaskGetTickCount,这两个函数切不可混用。
第18章 FreeRTOS 事件标志组
18.1事件标志组
18.1.1为什么要使用事件标志
事件标志组是实现多任务同步的有效机制之一。也许有不理解的初学者会问采用事件标志组多麻烦,
搞个全局变量不是更简单?其实不然,在裸机编程时,使用全局变量的确比较方便,但是在加上 RTOS 后就是另一种情况了。使用全局变量相比事件标志组主要有如下三个问题:
- 使用事件标志组可以让 RTOS 内核有效地管理任务,而全局变量是无法做到的,任务的超时等机制需要用户自己去实现。
- 使用了全局变量就要防止多任务的访问冲突,而使用事件标志组则处理好了这个问题,用户无需担心。
- 使用事件标志组可以有效地解决中断服务程序和任务之间的同步问题。