第1章 FreeRTOS引入及堆栈
1.1 FreeRTOS学习三阶段
1)、理解RTOS总原理,会移植官方Demo,会使用。
2)、知道内部机制,源码还没看!
3)、知道内部实现,能看懂源码!并能轻松移植任何单片机。
1.2 RTOS操作系统 与 裸机开发(前后系统)区别
RTOS:根据任务需要 人为的为任务切换CPU资源,有的任务可能不能完整运行完,并对运行环境进行保护管理,相当虚拟了多个CPU。
裸机开发(前后系统):每个任务只能顺序获得CPU资源。一个任务运行一遍了才轮到下个任务。
1.3 中断服务函数与调用函数的区别
中断服务函数 | 调用函数 |
---|---|
硬件触发 | 软件触发 |
何时发生可能不可预料,需要实现设定发生条件 | 何时发生可预料,在需要的地方调用 |
入口地址相对固定 | 入口地址可随意设置 |
不能传参 | 不能传参 |
没有返回值 | 没有返回值 |
在执行调度之前,都保存现场 |
1.4 堆栈
堆 | 栈(通常说堆栈) | |
---|---|---|
相同点 | 一种数据结构,被管理起来的内存 | |
管理方式不同 | 程序员手工控制分配/释放 | 编译器自动管理 |
分配方式不同 | 动态分配 | 2种分配方式: 静态分配是编译器完成的,比如局部变量的分配; 动态分配由alloca函数分配,编译器进行释放; |
空间大小不同 | 没有限制 | 有一定空间大小限制 |
产生碎片不同 | 频繁分配/释放会造成内存空间的不连续 | 因为栈是先进后出的队列,所以不会存在碎片问题 |
分配效率不同 | 堆是分配一块内存,按照一定的算法实现 | 栈是分配专门的寄存器,所以栈的效率比较高 |
生长方向不同 | 向上生长,向着内存地址增加的方向增长; | 向下生长,向着内存地址减小的方向增长; |
内存泄漏 | 溢出 | |
作用不同 | 程序员使用的大块空间 | 保护运行环境现场(中断、函数调用、任务切换) |
数组 | |
---|---|
数组是全局变量 | 被分配在静态存储区 |
数组是局部变量 | 被分配在栈上 |
数组是动态分配 | 被分配在堆上 |
1.5 中断服务函数、调用函数、操作系统任务 对栈操作区别
中断服务函数 | 调用函数 | 操作系统任务 |
---|---|---|
1、硬件保存一部寄存器到栈;(例如M3, 会有些约定,R0~2用来运算) 2、软件保存用到的部分寄存器或临时变量到栈; | 1、软件保存用到的部分寄存器或临时变量;有些寄存器不用保存到栈;(例如M3, 会有些约定,R0~2用来传参,所以不用保存) | 1,要软件全部保存寄存器到栈; |
1.6 FreeRTOS与STM32启动文件 中的堆栈区别
堆 | ||
---|---|---|
STM32启动文件 | FreeRTOS | |
来源 | 用户用malloc函数(或其他分配函数)分配的内存。若用户没有用分配函数分配空间,则大小可设置为0。要注意调用的第三方库或函数是否使用,例如USB、SD、网络等。 | FreeRTOS在配置文件中定义了一个总堆大小,其实就是一个数组,然后再该数组上为各个任务分配栈,剩下的空间用作堆。用户用分配函数分配内存。 |
作用 | 在大数据存储中,有时候我们需要在一定的时间内需要大存储空间,由于内存有限,希望该内存能被重复利用。堆正好可以满足这一灵活需求。堆可以用作局部临时存储,也可以静态存储。而我们通常在内存中定义的静态或全局的数组是不能释放重复利用的,这样在内存需求量大的情况下内存就无法满足了! | |
管理方式 | 用户分配/释放 | |
管理函数种类 | 标准C库内存管理函数; 自定义内存管理函数; | FreeRTOS中的内存管理函数; 标准C库内存管理函数; 自定义内存管理函数; |
第2章 任务
2.1 什么是任务?
任务: 运行的函数
句柄: 结构体指针
2.2 创建任务
2.2.1 创建任务所需参数
TCB结构体: 任务控制块。主要有函数指针,SP位置,优先级,参数等。
任务句柄(TaskHandle_t ): 就是TCB结构体指针。
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, //函数指针指向任务函数(一个死循环函数)
const char * const pcName, //任务的名字,FreeRTOS内部不使用它,仅仅起调试作用。长度为:configMAX_TASK_NAME_LEN
const configSTACK_DEPTH_TYPE usStackDepth, //任务栈大小,每个任务都有自己的栈。单位u32,输入10表示是40个字节
void * const pvParameters, //调用任务函数时传入的参数
UBaseType_t uxPriority, //优先级
TaskHandle_t * const pxCreatedTask ); //传出的任务句柄, 以后使用它来操作这个任务
2.2.2 创建任务时参数去向
1)、分配了TCB机构体,将任务名、任务优先级赋给TCB结构体。
2)、分配了栈。在栈里写入了函数地址、参数指针。
2.3 任务调度机制
2.3.1 任务优先级
-
高优先级的任务,优先执行,可抢占低优先级任务
-
高优先级的任务不停止,低优先级任务永远无法执行
-
同等优先级任务,轮流执行(或叫时间片轮转)
优先级的取值范围 | 优先级越高 | 任务运行顺序 |
---|---|---|
0~(configMAX_PRIORITIES – 1) | 数值越大 | 高优先级先运行;同优先级,可运行任务,轮流执行 |
调度器方法 | configMAX_PRIORITIES 大小 | 方法使能 | |
---|---|---|---|
使用C语言实现时 | 对所有的架构都是同样的代码 | 没有限制,但取值越大越浪费 内存 和 时间。 | configUSE_PORT_OPTIMISED_TASK_SELECTION被定义为 0 |
使用汇编实现时 | 针对架构相关的优化。 | 取值不能超过 32 | configUSE_PORT_OPTIMISED_TASK_SELECTION被定义为 1 |
优先级有关配置:
配置项说明 | 配置项 | 配置方式 | ||||
---|---|---|---|---|---|---|
A | B | C | D | E | ||
--- | --- | --- | --- | --- | --- | --- |
常用 | 很少用 | 很少用 | 很少用 | 几乎不用 | ||
是否 可抢占 | configUSE_PREEMPTION | 1 | 1 | 1 | 1 | 0 |
是否 时间片轮转 | configUSE_TIME_SLICING | 1 | 1 | 0 | 0 | x |
是否 空闲任务让步 | configIDLE_SHOULD_YIELD | 1 | 0 | 1 | 0 | x |
关闭Tick中断来实现省电 | configUSE_TICKLESS_IDLE | 0 | 0 | 0 | 0 | 0 |
2.3.2 任务状态
运行(Running): 任务正在执行,此时占用处理器。例如,汽车正在行驶。
就绪(Ready): 随时可以运行的任务,但还没轮到它。新创建的任务为就绪态。
阻塞(Blocked): 等待某个事件(时间延时,事件等)。例如,汽车等待绿灯。
暂停(Suspended)(挂起): 不等待,直接去休息了。
2.3.3 怎么管理
FreeRTOS通过三种链表(就绪任务链表、阻塞任务链表、暂停任务链表)来管理任务。就绪任务链表 有多少个优先级就有多少个 就绪任务链表;阻塞任务链表、暂停任务链表 各1个。
1、从高优先级 就绪任务链表 向低优先级 就绪任务链表 中找到就绪任务,获取任务句柄给到运行指针, 运行它。
2、若有同级任务,排队轮流执行,链表前面的先运行,运行1个tick后排到队伍后面。
3、若某个任务阻塞,则将该任务从 就绪任务链表移到 阻塞任务链表 中;阻塞解除,则反之。
4、若某个任务暂停,则将该任务从 就绪任务链表移到 暂停任务链表 中;解除暂停,则反之。
2.3.4 怎么进行调度
TICK定时中断,在Tick中断函数中会进行任务切换(链表种取出合适的任务运行)。每次中断会检查有哪些延时任务到期,将其从延时任务列表里移除并加入到就绪列表里。如果就绪任务的优先级都相同,如果开启时间片轮询,就会每个tick执行一个任务,轮询执行。
Tick中断做了什么: 1)、找到最高优先级的就绪任务
2)、保存当前任务的信息,并将其插入就绪任务链表末端
3)、恢复新的任务
第3章 同步与互斥
3.1 同步与互斥的概念
同步: 任务之间的依赖,比如A任务的运行依赖于B任务产生的数据。例如:在团队活动里,同事A写完报表,经理B才能拿去向领导汇报。经理B必须等同事A完成报表,AB之间有依赖。
互斥: 对资源同时访问时,只能有一个获得资源,一个使用完后才能轮到下一个,往往需要进行互斥访问。例如:打印机同一时间内只能打印一个任务;A、B两人抢厕所,A先一步占用了,B只能等A用完再用。
3.2 同步与互斥的概实现方法
能实现同步、互斥的内核方法有:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)。
它们都有类似的操作方法:获取/释放、阻塞/唤醒、超时。
队列(queue)可以用于"任务到任务"、"任务到中断"、"中断到任务"直接传输信息。
内核对象 | 应用场景 | 生产者 | 消费者 | 数据/状态 | 说明 |
---|---|---|---|---|---|
队列 | 传递任意多个数据 | ALL | ALL | 任务、中断都可写数据; 任务、中断都可读数据; | 用来传递数据, 发送者、接收者无限制, 一个数据只能唤醒一个接收者 |
计数信号量 | 计数资源数量 | ALL | ALL | 核心是"计数" 任务、中断都可释放信号量(计数加1); 任务、中断都可获得信号量(计数减1); | 用来维持资源的个数, 生产者、消费者无限制, 1个资源只能唤醒1个接收者: 像停车场,出来一个,才能进去一个 |
二值信号量 | 二值信号量通常用于资源的互斥访问或同步 | ALL | ALL | 核心是"计数" 任务、中断都可释放信号量(数值1); 任务、中断都可获得信号量(数值0); | 用来维持资源的个数, 生产者、消费者无限制, 1个资源只能唤醒1个接收者: 像停车场,出来一个,才能进去一个 |
互斥量 | 优先级继承的二值信号量 | A上锁 | A开锁 | 数值只有0或1 任务释放信号量(数值1); 任务获得信号量(数值0); | 就像一个空厕所, 谁使用谁上锁, 也只能由他开锁 |
事件组 | ALL | ALL | 一个事件用一bit表示: 1表示事件已发生, 0表示事件没发生; | 用来传递事件, 可以是N个事件, 发送者、接受者无限制, 可以唤醒多个接收者:像广播 | |
任务通知 | ALL | 只有我 | 数据、状态都可以传输,核心是任务的TCB里的数值,会被覆盖; 发通知给谁?必须指定接收任务; 只能由接收任务本身获取该通知; | N对1的关系: 发送者无限制, 接收者只能是这个任务 |
第3章 队列
3.1 队列
队列是为了任务与任务、任务与中断之间传递不定长消息。可将一条或多条消息写入队列中;也可从队列中读取一条或多条消息。uCOS传递消息是传递指针方式,而FreeRTOS采用是复制消息方式,不过也可实现传递指针方式(复制数据指针就可以实现)。
队列核心操作:1-关中断,2-操作队列缓冲区,3-链表(记录因发送/接收(2种链表)而进入阻塞的任务)
特性:
- 读写队列支持超时机制。
- 消息支持先进先出原则(FIFO),但是也支持后进先出原则(LIFO)。
- 可以允许不同长度的任意类型消息(不超过队列节点最大值)。
- 一个任务能够从任意一个消息队列接收和发送消息。
- 多个任务能够从同一个消息队列接收和发送消息。
- 当队列使用结束后,可以通过删除队列函数进行删除。
3.2 消息邮箱
消息邮箱就是消息队列长度为 1 的情况。
第4章 信号量
信号是一种标志,如信号灯;量用于计数。顾名思义,就是标志计数。信号量功能本质就是计数。信号量其实就是只有一个队列项的队列,该队列项不是用来传递消息,而是用来计数。一般用来进行资源管理和任务同步,实现任务之间同步或临界资源的互斥访问。
信号量主要实现两个功能:
- 两个任务之间或者中断函数跟任务之间的同步功能,这个与事件标志组是类似的。其实就是共享资源为 1 的时候。
- 多个共享资源的管理。正在使用的资源有多少,信号量就减多少。
4.1 计数信号量
计数信号量用于事件发生次数计数或者对资源数量的管理,计数无限制。在任务和中断中可使用。
事件计数: 事件产生—释放信号量(信号量+1); 事件处理—获取信号量(信号量-1),减到零表示所有事件处理完毕。
资源管理: 信号量值代表当前资源的可用数量,比如停车场当前剩余的停车位数量。信号量值为 0 时说明没有资源了。
优先级翻转现象: 在很多场合中,某些资源只有一个,当低优先级任务正在占用该资源的时候,即便高优先级任务也只能乖乖的等待低优先级任务使用完该资源后释放资源。这里高优先级任务无法运行而低优先级任务可以运行的现象称为“优先级翻转”。
4.2 二值信号量
- 信号量资源被获取,信号量值是 0;信号量资源被释放,信号量值是 1,只有 0 和 1 两种情况的信号量称之为二值信号量。
- 类似一个标志位,事件产生置1,事件处理后置0。
- 可以在任务和中断中使用
4.3 互斥信号量
互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或中断与任务之间的同步)二值信号量最适合。
优先级继承:当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。优先级继承尽可能的降低了高优先级任务处于阻塞态的时间,并且将已经出现的 “优先级翻转”的影响降到最低。
可以在任务中使用,不可以在中断中使用。
互斥信号量不能用于中断服务函数中, 原因如下:
- 互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
- 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。
4.4 递归信号量
递归互斥信号量,其实就是互斥信号量里面嵌套 互斥信号量。
第5章 事件组
事件是一种实现任务间通信的机制,主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输。与信号量不同的是,它可以实现一对多,多对多的同步。 即一个任务可以等待多个事件的发生:可以是任意一个事件发生时唤醒任务进行事件处理;也可以是几个事件都发生后才唤醒任务进行事件处理。同样,也可以是多个任务同步多个事件。(可以在任务和中断中使用)
特性:
- 事件只与任务相关联,事件相互独立,一个 32 位的事件集合(EventBits_t 类型的变量,实际可用与表示事件的只有 24位),用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0 表示该事件类型未发生、1 表示该事件类型已经发生),一共 24 种事件类型。
- 事件仅用于同步,不提供数据传输功能。
- 事件无排队性,即多次向任务设置同一事件(如果任务还未来得及读走),等效于 只设置一次。
- 允许多个任务对同一事件进行读写操作。
- 支持事件等待超时机制。
第6章 任务通知
通过任务通知方式可以实现 消息邮箱, 计数信号量,二值信号量,事件标志组。
在使用队列、信号量前,必须先创建队列和信号量,目的是为了创建队列数据结构。而由于任务通知的数据结构包含在任务控制块中,只要任务存在,任务通知数据结构就已经创建完毕,可以直接使用。任务通知处理更快,RAM 开销更小。
任务通知可以在任务中向指定任务发送通知,也可以在中断中向指定任务发送通知,FreeRTOS 的每个任务都有一个 32 位的通知值,任务控制块中的成员变量 ulNotifiedValue 就是这个通知值。
只有在任务中可以等待通知,而不允许在中断中等待通知。
任务通知限制:
- 任务通知方式仅可以用在只有一个任务等待信号量,消息邮箱或者事件标志组的情况。
- 使用任务通知方式实现的消息邮箱替代消息队列时,发送消息的任务不支持超时等待,即消息队列中的数据已经满了,可以等待消息队列有空间可以存新的数据,而任务通知方式实现的消息邮箱不支持超时等待。
FreeRTOS 提供以下几种方式发送通知给任务 :
- 发送通知给任务, 如果有通知未读,不覆盖通知值。
- 发送通知给任务,直接覆盖通知值。
- 发送通知给任务,设置通知值的一个或者多个位 ,可以当做事件组来使用。
- 发送通知给任务,递增通知值,可以当做计数信号量使用
第7章 软件定时器
FreeRTOS软件定时器是基于 Tick 硬件定时器中断来运行的,其他操作系统也是,不过其他操作系统是在硬件定时器里调用软件定时器函数,而FreeRTOS不是。FreeRTOS它认为如果定时器函数很耗时,会影响整个系统,所以FreeRTOS是通过任务来处理软件定时器。
当 FreeRTOS 的配置项 configUSE_TIMERS 被设置为 1 时,在启动调度器时,会自动创建 RTOS Damemon Task(守护任务)。Tick 硬件定时器中断函数发现某个时间到了,写命令队列,守护任务里读定时器命令队列,有数据就处理,没数据就休眠。
第8章 中断机制
FreeRTOS把MCU硬件中断分为两类,高优先级中断一类;低优先级中断一类。高优先级中断中不能使用FreeRTOS的API函数,它用于处理更紧急的事情,不依赖于FreeRTOS。低优先级中断可以在中断中使用FreeRTOS的API函数。
第9章 临界资源
临界资源:一次仅允许一个进程或任务使用的共享资源。例如: 硬件有打印机、磁带机等;软件有消息缓冲队列、变量互斥量、变量、数组、缓冲区等。
临界资源的互斥访问通过 屏蔽/使能中断、暂停/恢复调度器 来实现。这样访问临界资源就可以不受干扰。
临界区:每个进程或任务中,访问临界资源的那段代码。也就是屏蔽/使能中断之间代码、暂停/恢复调度器之间代码。