实时操作系统,要求一个高的实时性,就不是像在一个死循环中放俩函数了。而是创建俩任务,也叫做俩进程,高速的轮流执行,提高实时性。
堆栈的申请是任务的基础。
创建任务又两种方式,
第一种是动态创建任务,使用
BaseType_t xTaskCreate(TaskFunction_t pvTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask)
在动态创建任务的时候,指定好任务栈的大小后,系统使用函数xTaskCreate去为任务自动申请空间
第二种是静态创建
TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode,
const char *const pcName,
const configSTACK_DEPTH_TYPE ulStackDepth,
void *const pvParameters,
UBaseType_t uxPriority,
StackType_t *const puxStackBuffer,
StaticTask_t *const pxTaskBuffer)
静态为任务申请空间,空间地址要手动申请,定义一个数组,传入数组头指针就算申请了空间
优先级和时间片是两个概念,平级才有时间片。平级的时候用滴答时间时间片进行任务轮转,只有不同级别的时候才用任务优先级。
优先执行高优先级任务,要是高优先级的任务没有放弃任务权限,低优先级的任务得不到执行。
使用下面的函数,参数是任务的句柄,TaskHandle_t xTask,删除哪个任务就把哪个人物的句柄放进去。
void vTaskDelete(TaskHandle_t xTask)//这是在任务一中删除任务2,需要加任务句柄,他杀所用
vTaskDelete(TNULL)//在自己的任务中删除自己,就可以用参数NULL,自杀所用
每个申请的任栈空间,都是由头和长度组成。任务栈空间分配的大小,要足够的支撑任务的运行,任务变量运行中和定义的变量大小不能超过,栈申请的空间大小。
那么如何确定一个任务需要多大的栈空间呢
多任务的交替执行如何实现,不是两个,是多个
任务切换的基础是,滴答tick中断,如果优先级相同的三个任务,只要滴答中断一到,就切换任务。就会产生时钟中断。
一到点,到到时间,就会发生Tick_IST中断,Tick基本上上是1ms,可以进行配置,配置成任何想要的时间。
runing :运行状态:在跑着的任务
ready :准备状态:已经创建的任务,等待运行,可以随时运行,等待runing跑完,就要他们跑 了
blocked:阻塞状态:等待着去执行某个任务,可能某个任务的事情还没做完,又要执行了,比如说通信,一个信号还没发送完,又要发送了,应该就需要阻塞了。
suspended:暂停状态:主动休息了,被动休息了,被刮起了,挂起函数。
阻塞和暂停状态有啥不同呢:阻塞是等待某事情的发生,比如信号没法送完,自己任务中有个延时函数也起到了阻塞作用vTaskDelay(10);,这个任务里面有个中断需要触发,一直没有触发,就阻塞住了,一旦中断来了(延时够了),那这个阻塞的任务又可以进行了。
暂停状态是纯粹的休息,是主动和被动休息,需要运行的任务去重新唤醒这个阻塞的。
四种状态如何转换呢?
有三个任务被创建出来后,三个任务都会在read状态,当启用任务调度器的时候,主管任务调度,拉出来一个优先级高的任务去执行,runing状态,执行后然后执行下一个任务,这个read起来。
也可以挂起一个任务,用任务函数。
转换如下
内存上,不同的任务放在一个链表上,通过头指针找到任务的头。
创建任务,就是把这个任务放在一个,ready链表中
挂起任务,把这个任务放在了SUSpended列表中了
实验运行,讲解四种状态
几个重要函数:
xTaskGetTickCount()得到当前滴答的个数,发生了几次滴答
vTaskSuspend(xHandleTask3);挂起任务
vTaskResume(xHandleTask3);恢复任务
void Task2Function(void * param)
{
while (1)
{
task1flagrun = 0;
task2flagrun = 1;
task3flagrun = 0;
printf("2");
vTaskDelay(10); //延时阻塞的任务2
}
}
VTaskDel ay和vTaskDelayUntil
vTaskDelayUntil这个延时函数有啥不一样的呢,就是用这个延时函数,保持每次任务执行的时间是一致的,不是说一会任务时间长,一会任务时间短。
vTaskDelayUntil相当于任劳任怨的延时函数,给弄得好好,把任务时间补齐,弄得规规整整的,城墙上的守卫者。
空闲任务,就是系统自带的优先级最低的任务函数,在这个优先级最低的任务函数中,要做的是一些清理工作。
可是两个任务互相执行,空闲任务得不到执行的话,内存会 爆炸掉吗?
vTaskDelete(NULL);杀死任务可以清理任务内存,可是能这个杀死只能是别人杀死,别人清理尸体,不能自己清理自己的实体。
面试重点:
我们可以在空闲任务中实现一些不重要的函数,不重要的东西,比如说清理尸体啥的,就需要钩子函数。就是不破化这个Freertos的源码,去改造空闲函数,用钩子钩住点东西。
使用钩子任务也有一些条件和约束:
钩子函数永远不能进入阻塞和暂停状态。
使用钩子函数方法:
前面个讲了如何创建任务,现在到了处理任务后事的时候了。
优先级高的任务先执行,优先级低的任务交替执行
1、进入阻塞任务,的方式有好几种,等待任务的执行,任务进入阻塞的方式很重要,比如等待消息,消息阻塞有好几种方式
2、可以配置算法调度,看是否可以抢占,实际任务中要支持抢占
所谓的抢占,就是高优先级可以打断低优先级任务进行运行
如果抢占不了低优先级任务的话,那低优先级任务没有认出自己的任务的话,低优先级任务就要一直执行了。
3、同优先级的任务可以交替执行,也可以设置为先到先得
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 0 //这个设置为0的话,低优先级的,在高优先级进行阻塞后也不进行轮转了,就只是先到先得了。
#define configIDLE_SHOULD_YIELD 1
4、空闲任务需要卑微的让出任务权限
#define configIDLE_SHOULD_YIELD 1
这个配置,配置空闲任务是否礼让别人,空闲任务运行的时候,可以让别人占有自己。
1是礼让,0是不礼让,都是0优先级的时候和其他任务 ,共同差不多的时候。
调度策略
允许抢占的话,高优先级的任务先进性执行
不允许抢占的话,大家平等,哪个任务执行,就一直执行,除非自己让出执行权限
允许的话,同优先级的任务交替执行
不允许的话,谁先运行谁就一直运行,除非主动让出 CPU和被抢占
如果让步的话,如果礼让,就空闲任务执行的时间少,用户任务优先执行
如果不礼让,那就会和用户同等级的0等级的的任务进行时间片轮转执行
也叫开辟的一块内存空间叫做FIFO,多个任务可以共同访问这个内存空间实现通信过程。
使用队列传输数据时有两种方法:
拷贝:把数据、把变量的值复制进队列里
引用:把数据、把变量的地址复制进队列里
FreeRTOS使用拷贝值的方法,这更简单:
局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据 无需分配buffer来保存数据,队列中有buffer 局部变量可以马上再次使用 发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据 如果数据实在太大,你还是可以使用队列传输它的地址 队列的空间有FreeRTOS内核分配,无需任务操心 对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个 地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把 数据复制出队列。
队列的阻塞访问
任务读写队列时,简单地说:如果读写不成功,则阻塞;可以指定超时时间。口语化地说,就是可以定 个闹钟:如果能读写了就马上进入就绪态,否则就阻塞直到超时。
某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞状态:还可以指定阻塞的时间。如果队 列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。 既然读取队列的任务个数没有限制,那么当多个任务读取空队列时,这些任务都会进入阻塞状态:有多 个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态? 优先级最高的任务 如果大家的优先级相同,那等待时间最久的任务会进入就绪态
数据的同步是指,如何能通知任务新数据的到来并同时提高CPU的利用率。假设一个简单的生产者消费者模型--有两个任务,一个任务是数据的生产者(Producer)任务,一个任务是数据的消费者(Consumer)任务,消费者任务等待生产者任务产生的数据并进行处理。按照正常的思路,消费者任务和生产者任务会轮流执行,但是如果在消费者任务执行的时候数据还没有产生的话,消费者任务的运行就是无用的,会降低CPU的使用效率。为此,FreeRTOS引入了信号量(Semaphore)概念,通过信号量的同步机制可以使消费者任务在数据还没到达的时候进入阻塞状态,并让出CPU资源给其他任务。信号量是一种同步机制,可以起到消息通知和提高CPU利用率的作用。对于信号量的操作有两种,获取信号量(taking a semaphore)和给予信号量(giving a semaphore)。在生产者消费者模型中,生产者生产数据后给予信号量,消费者获取信号量后可以处理数据。信号量又分为二进制信号量(binary semaphore)和计数信号量(counting semaphore)。二进制信号量中信号量的数目最多为1,即最多只能通知解锁一个任务;计数信号量中信号量的数目可以自定义设定为多个,即可以通知解锁多个任务。
资源的保护是指,如何保护资源不被多个任务同时操作。如果一个保存数据的全局变量在被一个任务操作的过程中,又被多个更高优先级的任务抢占并处理,那么最后这个支离破碎的数据将会毫无意义,破坏了数据的独立完整性;如果一个任务在使用uart串口发送数据的时候,又被多个更高优先级的任务抢占串口资源并发送数据,那么最后串口发送的数据将只是一堆乱码毫无意义,会导致系统的不稳定。
我理解啊,一个任务正在操作全局变量的时候不如说计算c=a+b,这个还没计算完才读取了了a的数据,另一个任务出现了吧b的数据改了,然后再回头计算的时候c就计算错了。那互斥量应该就是保证这个任务活这个代码运行的时候不被别人打断。
互斥量是二进制信号量的一个变种,开启互斥量需要在头文件FreeRTOSConfig.h设置configUSE_MUTEXES 为1。互斥量和信号量的主要区别如下
互斥量操作的相关函数
SemaphoreHandle_t xSemaphoreCreateMutex( void )
xSemaphoreCreateMutex()函数用于创建互斥量
首先申明个互斥量的全局变量
SemaphoreHandle_t xMutex;
然后在main函数中创建互斥量
int main(void)
{
...
xMutex = xSemaphoreCreateMutex();
...
}
void Function_Resource(void const * argument)
{
//要保护的资源函数
...
xSemaphoreTake( xMutex, portMAX_DELAY );
{
//对资源的处理
...
}
xSemaphoreGive( xMutex );
...
}
在上段代码中,如果有多个任务要调用资源函数的话,通过资源函数内部的互斥量机制可以保护资源不被其他任务打断。
1、信号量的初始值是0,互斥量的初始值是1
2、优先级倒置问题
3、返还的意思是啥,就是在谁的任务中取走这个1,就要在这个任务中用完互斥量后,再把1返还吗。
时间组又叫事件位,每一位表示一个事件,这个位发生了就是表示1,没有发生就是标志0;
相当于事件发生的标志位发生了就变化1,没发生就还是0;
事件组:事件发生时,会唤醒所有符号条件的任务,简单地说它有"广播"的作用
事件组和队列、信号量等不太一样,主要集中在2个地方:
唤醒谁?
队列、信号量:事件发生时,只会唤醒一个任务
事件组:事件发生时,会唤醒所有符号条件的任务,简单地说它有"广播"的作用
是否清除事件?
队列、信号量:是消耗型的资源,队列的数据被读走就没了;
信号量被获取后就减少了 事件组:被唤醒的任务有两个选择,可以让事件保留不动,也可以清除事件
事件组的常规操作如下:
先创建事件组 任务C、D等待事件: 等待什么事件?可以等待某一位、某些位中的任意一个,也可以等待多位。简单地说就 是"或"、"与"的关系。
得到事件时,要不要清除?可选择清除、不清除。 任务A、B产生事件:设置事件组里的某一位、某些位
/* 设置事件组中的位
* xEventGroup: 哪个事件组
* uxBitsToSet: 设置哪些位?
* 如果uxBitsToSet的bitX, bitY为1, 那么事件组中的bitX, bitY被设置为1
* 可以用来设置多个位,比如 0x15 就表示设置bit4, bit2, bit0
* pxHigherPriorityTaskWoken: 有没有导致更高优先级的任务进入就绪态? pdTRUE-有,
pdFALSE-没有
* 返回值: pdPASS-成功, pdFALSE-失败
*/
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t * pxHigherPriorityTaskWoken );
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait )
就是说吧,就是相当于一个定时器中断,时间到了就执行中断函数。但是有一些需要注意的事情。
定时器类型,一次使用定时器,还是循环使用软件定时器。两个不同的定时器,就是需要多次提醒吗?
3回调函数
定时器的回调函数的原型如下:
void ATimerCallback( TimerHandle_t xTimer );
定时器的回调函数是在守护任务中被调用的,守护任务不是专为某个定时器服务的,它还要处理其他定 时器。
所以,定时器的回调函数不要影响其他人:
回调函数要尽快实行,不能进入阻塞状态
不要调用会导致阻塞的API函数,比如 vTaskDelay()
可以调用 xQueueReceive() 之类的函数,但是超时时间要设为0:即刻返回,不可阻塞
参考文章:
韦东山freeRTOS系列教程之【第一章】FreeRTOS概述与体验_freertos教程_韦东山的博客-CSDN博客
韦东山freeRTOS快速入门 (100ask.net)//视频教程
FreeRTOS 从入门到精通10--资源管理(互斥量与信号量) - 知乎