基础知识
1.任务状态
总共有四种:运行态,就绪态,阻塞态,挂起态。只有就绪态才可转变运行态。其他任务想运行,就必须先转变成就绪态。
2.堆和栈
堆
堆是一块用于动态分配内存的区域,用于存储程序运行时动态创建的对象。堆的大小可以在程序运行时动态调整。
堆的分配和释放由程序员手动控制的。堆的分配和释放顺序是可以任意的,不需要遵循先进先出的原则,堆的分配和释放需要调用相应的函数。
栈
概念介绍
栈是一种用于存储局部变量和函数调用信息的数据结构。栈的大小在程序编译时确定,通常较小。
栈的分配和释放是由编译器自动控制的。栈的分配和释放遵循先进先出的原则。栈的分配和释放无需调用函数,由编译器自动完成。
通常用于跳转函数前,保存相关局部变量。
保存现场:就是保存被打断前的寄存器的值。
可简单理解为任务 = 函数 + 它本身的栈。
保存现场的几种情况:
函数嵌套,不用保存全部寄存器的值
硬件中断,F103,M3,M4硬件也会保存一部分。
任务切换:保存所有的寄存器值。
堆和栈的区别
分配方式
堆的分配是动态的,由程序员手动控制,堆的分配可以在任意时刻进行,不需要遵循特定的顺序。
栈的分配是静态的,由编译器自动控制,栈的分配在编译时确定,无法在运行时改变。
栈还用于中断时的保存现场和恢复现场。
分配效率
堆的分配效率较低,需要在运行时动态分配和释放。
栈的分配效率较高,由编译器在编译时确定分配大小。
空间管理
堆的空间管理由程序员手动控制,需要注意内存泄漏和内存碎片的问题。
栈的空间管理由编译器自动控制,无效程序员关注。
FreeRTOS中的任务栈
- 任务栈的大小由创建任务时指定,通过由编译器自动计算;
- 任务栈的空间管理由Free RTOS内核自动控制,无需程序员关注;
- 任务栈的分配由Free RTOS内核自动控制,无效程序员手动分配;
- 任务栈的分配效率较高,由编译器在编译时确定分配大小。
- 每个任务都有一个自己的栈
3.FreeRTOS中的调度算法
优先级抢占式调度算法
算法原理:任务优先级越高,被调度的机会就越大,任务优先级相同时,采用时间片轮转调度。
优点:可以满足实时系统的响应时间要求,可以实现任务的优先级管理。
缺点:低优先级任务可能会被长时间阻塞。
在FreeRTOS中,优先级数字越小。优先级越低。
时间片轮转调度算法
算法原理:每个任务被分配一个时间片,时间片用完后,任务被挂起,等待下一次调度。
优点:公平地分配CPU时间片,可以避免低优先级任务长时间阻塞。
缺点:无法满足实时系统的响应时间要求。
时间片调度
时间片调度是指将CPU的执行时间划分为固定长度的时间片,每个任务在一个时间片内执行一定的指令。当多个优先级相同的任务处于就绪状态时,它们将通过时间片调度方式轮流执行。
同优先级的情况下,我们一般是后创建的那个任务先执行。(首次执行的情况)
默认情况下采用优先级调度和时间片调度
当由多个优先级相同的任务处于就绪状态时,它们将通过时间片调度方式轮流执行。当由较高优先级的任务处于就绪状态时,高优先级任务会被优先执行而不受时间片调度的限制。
调度机制概述
优先级不同
- 高优先级的任务,优先执行,可以抢占低优先级的任务。
- 高优先级的任务不停止,低优先级的任务永远无法执行。
- 同等优先级的任务,轮流执行,也叫做时间片流转。
如何管理?取出要运行的任务
- 找到最高优先级的运行态、就绪态任务,运行它
-
如果大家平级,轮流执行:排队,链表前面的先运行,运行1个tick后乖乖地去链表尾部排队。
谁来调度?
- Tick中断(一般1ms一次)
优先级高先执行。优先级相同、后来创建的先执行。空闲任务由调度器创建。
深入理解调度机制
可抢占:高优先级的任务先运行
时间片轮转:同优先级的任务轮流运行
空闲任务礼让:如果由同是优先级为0的其他就绪任务,空闲任务主动放弃一次运行机会。
4.动态任务和静态任务
如果任务数量固定且资源需求稳定,静态创建任务是一个简单且高效的选择。如果需要在运行时动态创建和撤销任务,或者任务的数量和资源需求不确定,动态创建任务则提供了更大的灵活性和管理性。一般而言,动态创建更为常用。
5.任务状态
就绪态
任务已经创建,但还未被调度执行。处于就绪态的任务可以被调度器选择执行。
就绪态的任务就放入就绪链表
运行态
被调度器选中执行的任务处于运行态。正在使用CPU资源执行任务代码。
运行态的任务放入就运行链表
阻塞态
任务因为某些原因无法继续执行而被阻塞。可能的原因包括等待某个事件的发生或者等待资源的释放。
运行状态的任务可以调用特定函数进入阻塞态,如vTaskDelay()时间延迟函数。
也可由进程间的通信时请求信号量,队列等事件进入阻塞态。
处于阻塞态的任务会被放入阻塞链表
事件的分类
时间相关事件:
- 时间相关事件,就是设置超时时间,在指定时间内阻塞,时间到了就进入就绪状态。
- 使用时间相关的事件,可以实现周期性的功能、可以实现超时功能。
同步事件:
- 同步事件就是某个任务在等待某些信息,别的任务或者中断服务程序会给它发送消息。
- 发送消息的方式有很多:任务通知,队列,事件组,信号量,互斥量等。这些方法可以用来发送同步信息,比如某个外设得到了数据。
挂起态
调用vTaskSuspend()暂停任务。调用vTaskResume()恢复任务
一个任务暂停后如果想要恢复,需要通过别的任务调用vTaskResume()函数才能恢复这个任务。
句柄为NULL(或自身句柄)则操作自己,句柄为其他任务,则操作其他任务。
注意:挂起后无法自己恢复自己。
FreeRTOS代码以及源码
FreeRTOSConfig.h文件FreeRTOS的配置文件,包含了许多的宏定义,与CubeMx中的设置一一对应。
freertos.c是主要用于创建任务。
RTOS源码在Middlewares文件夹中。
数据类型
变量名
函数名
宏定义的规律(不用刻意去记):
config开头的都是些配置项(本质是一些数字值)
INCLUDE开头的通常为“函数使能开关”是数值0或1
任务创建和删除的API函数
- xTaskCreate();动态创建任务
- xTaskCreateStatic();静态创建任务
- vTaskDelete();删除任务
动态创建任务函数解析
函数指针:该函数的地址。
在任务栈中存储了函数地址和参数值,以便任务切换。
静态创建任务函数解析
和动态创建相比,调用的API函数不一样。
空闲任务和软件定时器需要手动配置。动态创建是自动配置。
空闲任务:当没有任务执行时,CPU会执行空闲任务。
调用两个接口函数,完成空闲任务和定时器的配置
任务删除函数
细节补充:
启动任务只需要执行一次,用完就删除。
处于临界区的代码不会被中断打断(因为临界区把中断关了)
调用FreeRTOS的时间延迟函数,才能进入阻塞态。 因为有vTaskDelay()函数把高优先级任务阻塞掉,所以其他优先级任务才能够执行。
HAL库和自定义的延迟函数不会阻塞任务。
自己删自己,空闲函数收尸。别人删自己,别人给收尸。
任务挂起和恢复
from ISR 在中断使用的函数。
任务挂起后,无论该任务的优先级多高,都无法再执行。
中断管理
简单介绍
如果要在中断里使用的函数,后缀必须带有ISR。
屏蔽中断的等级可以自行设置
可以设置一个阈值,低于这个优先级的,都被Free RTOS管理。只看高4位,0x50则表示0-15被RTOS管理,0-4则不受RTOS控制。
一般情况下只设置抢占优先级。
注意:响应优先级通常也称为子优先级。任何一个中断都可以打断任意级别的任务。
Free RTOS的开关中断
进入临界区其实就是关中断,所以临界区的代码不会被中断打断。而退出临界区就是开启中断。
中断函数里也可以设置临界区,防止某个中断不被别的中断打断。
时间片流转的切换上下文依靠的是滴答定时器中断,想要完整执行完任务后再切换则需要设置临界区。临界区可以保护任务不被滴答定时器中断导致切换。
默认软件定时器优先级最高,空闲任务优先级最低
Free RTOS的中断机制
ISR函数中不切换任务,只是进行记录(唤醒)。
一般情况下,Systick和Pendsv的中断优先级设为最低。
FreeRTOS任务相关API函数
获取系统中任务的数量时,被删除掉的任务也会包含在内。注意会算上空闲任务和软件定时器任务(如果开启)。
任务删掉后就不会再显示任务统计时间。
Free RTOS时间管理
vTaskDelay():相对延时。从执行到vTaskDelay()函数开始,真到指定延时的时间结束。
xTaskDelayUntil():绝对延时。将整个任务的运行周期视为一个整体,适用于需要以固定频率定期执行的任务。
图片vTaskDelayUntil()少了个l
vTaskDelay()一般日常使用较多。
FreeRTOS的消息队列
概念介绍
FreeRTOS中的队列是一种用于任务之间通信的机制,它允许一个任务发送信息给另一个任务。队列是线程安全的数据结构,任务可以通过队列在彼此之间传递数据。
写队列和读队列都是采用复制的方式将数据复制过去使用。
消息队列读写API函数
创建队列
写消息
往队列发送:往尾部,往头部。写的方式:正常新增,覆盖写。
从队列读取:从头部读;读的方式:receive(剪切)、peek(复制)
读取消息
注意:队列里面的每个数据大小可以不一样;创建队列,要在任务开始前创建好。
读取队列数据时,会让任务进入阻塞。
C语言基础补充
表示字符串的方法:1.字符指针,2.字符数组。
字符指针
char * p = "hello world";
p的值是字符串首字符的地址,&p是指针变量p的地址,
字符数组
char a[] = "hello world";
a本身自己没有地址,只有首字符的地址。&a,a,&a[0]都是首字符的地址
信号量
概念介绍
Free RTOS中的信号量是一种用于任务间同步和资源管理的机制。信号量可以是二进制的0,1也可以是计数型。信号量的基本操作包括“获取”和“释放”。
信号量与队列的区别
信号量小于队列,信号量底层其实还是队列。信号量是大小为1的队列。
二值信号量相关函数
信号量刚创建完,会默认释放一次(+1),如果不想要,可以先主动获取一次(-1)。
计数信号量
概念
计数型信号量相关API函数
动态方便。静态灵活,但需要手动配置一些参数。
往队列发送数据的原理
二值信号量的实现原理
xSemaphoreCreateBinary();X开头的,不会主动释放信号量。还有返回值
vSemaphoreCreateBinary();V开头的,会主动释放一次信号量。
创建的队列:长度为1,队列项大小为0,类型为二值信号量。
释放信号量:往队列发送NULL值。
获取信号量:......获得NULL值
计数型信号量的实现原理
创建的队列:长度为计数最大值,队列项大小为0,类型为计数型信号量。
释放信号量: 发送一个NULL值,计数变量值 + 1。
获取信号量:计数变量值 - 1。
优先级翻转(倒反天罡)
解决办法:采用互斥信号量的优先级继承机制。只能消除部分影响。释放完信号量后,恢复原来的优先级。
注意:在中断中不能使用互斥信号量。
互斥信号量(Mutex)
互斥信号量就是加了优先级继承机制的二值信号量。
相关API函数
优先级继承:从旧的就绪链表中移除,放入新的就绪链表,提高优先级。释放完Mutex后再恢复回原先的优先级。
队列集
概念介绍
相关API函数
先创建队列,再添加到队列集。
事件标志组
概念介绍
事件标志组与信号量的区别
相关API函数
事件组的内部机制
事件的含义由自己定义。
关闭调度器,但不会关闭中断,所以不可以在中断中使用事件组。
任务同步的方式选择
队列:两个任务之间传递数据。
信号量:简单需求,例如指示功能。
事件标志位:多个任务,多种条件
细节:信号量发送方不用阻塞,接收方需要阻塞。
总结
任务通知
概念介绍
1.通知状态:
- 初始化,没有在等待;
- 正在等:任务调用了接收通知的函数(xxxTake\xxxWait)。
- 已收到:发送方调用了发送通知的函数,其实是发送方已发送。
2.通知值
从10.4.0版本,是一个组,每一个都是32位的值,可以实现多对一。
关于发送的重要细节
- 临界区操作
- 设置接收任务的通知值,通知状态
- 将 接收任务 放入就绪列表、主动触发任务切换
接收函数 xxxTake
- 如果通知值为=0,将自己的任务状态设置为waiting 中
- 将 自己 放入阻塞列表、主动触发任务切换
- 如果第二个参数 = true ,直接清零(对标二值信号量);如果 = false,-1(对标计数型信号量)
接收函数 xxxWait
- 执行 接收前清理(根据配置)
- 将 自己 的任务状态 设置为 waiting 中
- 将 自己 放入阻塞列表、主动触发任务切换
- 保存一下现在的通知值
- 如果状态 = 已接收(发送方已发送),执行清理(根据配置)
任务通知是一对一,两个任务之间进行;事件组可以是多对一(多个任务对应一个事件组)
任务通知相关API函数
注意:无法在中断里接收任务通知,因为ISR没有TCB
进行多个任务通知时,任务优先级高的建议放在前面
FreeRTOS软件定时器
概念介绍
软件:代码提供的,参考FreeRTOS系统时钟节拍,Systick = 1ms
- 节省硬件资源
- Free RTOS源码中,会把systick设为最低优先级15,容易被打断。
- 适用于对时间精度要求不高的场景
分类:
- 单次:第一次超时,调用回调函数之后,就结束了
- 周期:每次超时,调用回调函数之后,会自动重置继续下一次的超时
硬件:基本、通用、高级
软件定时器任务(启动调度器时创建的):负责管理软件定时器
- 负责判断软件定时器是否超时(超时时间是我们传的)
- 超时后,调用 超时回调函数(类似硬件定时器中断处理函数,但它不是中断,只是一个函数)
- 不要在超时回调函数里定义阻塞的代码;为了不影响其他软件定时器调用超时回调函数。
- 处理定时器命令队列
- 也是一个Free RTOS的任务,优先级默认最高优先级31
- 可选的,configUse_TIMERS == 0 默认关闭 , 1打开。
软件定时器的状态
软件定时器的相关API函数
空闲任务
空闲任务的状态
只有两种状态:运行状态或者就绪状态。
自己删自己,空闲函数收尸。别人删自己,别人给收尸。
空闲任务的钩子函数。
低功耗
概念介绍
不影响系统运行又想降低功耗,可以在空闲任务执行的期间,让MCU进入相应的低功耗模式。
低功耗的实现
依赖于睡眠模式
- 利用__WFI进入睡眠模模式
- 唤醒:Systick中断
依赖于空闲任务
会调整systick的周期、做补偿
低功耗模式的相关配置
内存管理算法
heap1
heap2
heap4
heap4主要流程
分配流程
- 堆栈初始化
- 插入新空闲块的逻辑
释放流程
- 释放指定的内存块
- 释放成功后,执行 插入新空闲块的逻辑(合并相邻的空闲内存块)
都会用到:挂起调度器 操作 恢复调度器,确保操作期间不被干扰。
相关API函数(熟悉即可,一般由系统自动配置)
FreeRTOS的编码(熟悉)
RTOS任务与中断服务程序
中断是MCU的硬件特性。优先级数字越低,中断优先级越高。RTOS中的高优先级的一组中断不受FreeRTOS的管理,称为FreeRTOS不可屏蔽中断。低优先级的一组中断是FreeRTOS的可屏蔽中断。
任务与中断的关系(重要)
中断是MCU的硬件特性,由硬件事件或者软件信号引起中断,运行哪个ISR由硬件决定。中断的优先级数字越低,优先级越高,所以中断的最高优先级为0.
FreeRTOS的任务是一个纯软件的概念,与硬件系统无关。任务的优先级数字越低,表示的优先级越低,所以任务的最低优先级是0。由Free RTOS的任务调度算法决定哪个任务处于运行状态。
任务只有在没有ISR运行的时候才能运行,即使优先级最低的中断也可以抢占高优先级任务的执行。而任务不能抢占ISR的运行。
在实际的软件设计中,一般要尽可能地简化ISR函数的功能,使其尽量少占用CPU的时间,否则会导致程序响应变迟钝。
任务通知
不能向ISR发送通知。只能是任务或者ISR函数向任务发送通知。
任务通知不能进行消息广播。
任务通知一次只能发送或者接收一个uint32_t类型的数据。
实操应用
CubeMx配置
基础配置(基于STM32G431开发板)
Debug + Timebase Source设置
补充知识
汇编相关
读取LDR
写STR
加ADD
减SUB
PUSH入栈(可理解为写内存)
POP出栈(可理解为读内存)
LR返回地址
FreeRTOS的链表
uxNumberOfitems:链表中有多少个item
pxIndex:指向当前正在使用的item,这个pxIndex被用来遍历链表
pvOwner:包含ITEM的结构体
pxContainer:链表
void * 可以代表任何类型的指针
Container的概念引入
person里含有node,person就是node的容器。
dog里含有node,dog就是node的容器。
核心在于如何根据node找到container
方法1:把node放在container的最前面。
方法2:在结构体中算出container的位置
方法3:在node中保存container的地址
不知道栈空间应该给多少时,可以先给个较大的栈空间,再查询高水位,进行适当的调整。