当前位置: 首页 > news >正文

02 FreeRTOS 任务

1、创建任务函数

1.1 动态内存的使用

        在之前我们如果要创建一个与学生有关的任务,我们会定义:

//打印50个学生的信息
char name[50][100];
int age[50];
int sex[50];	//1表示男,0表示女
int score[50];

         如果之后要对其进行修改会非常麻烦,因此我们要引入面对对象的编程思想,这个对象就是student,那么我们就定义一个相关的结构体来表示一个学生:

struct Student{char name[100];int age;int sex;int score;struct Student *next;    //这里定义一个指针,可以使用链表把学生管理起来
};

        通过这种编程思想,我们在FressRTOS中对任务也要构造出一个结构体,之前我们是通过xTaskCreate函数动态创建了任务,xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1),同时在创建时指定了栈的大小为100*4个字节,这个TaskHandle_t * const则是指向了typedef struct tskTaskControlBlock * TaskHandle_t,也就是TCB_t *这个结构体,所以说我们创建任务时所返回的handle,就只是这个TCB_t 的指针,另外取了一个名字。

        在xTaskCreate中有一个TCB_t *结构体,同时可以看出这里用了malloc从动态内存,也就是从堆里面来做分配

1.2 静态创建任务

        使用xTaskCreateStatic函数静态创建任务,要事先分配好TCB结构体,栈。

        如果要使用这个函数,还要在FreeRTOSconfig.h中定义"configSUPPORT_STATIC_ALLOCATION""为1,并且实现vApplicationGetldleTaskMemory函数。

//关键代码
void Task1Function( void * param)
{while(1){printf("1");}
}void Task2Function( void * param)
{while(1){printf("2");}
}void Task3Function( void * param)
{while(1){printf("3");}
}/*-----------------------------------------------------------*/
StackType_t xTask3Stack[100];StaticTask_t xTask3TCB;StackType_t xIdleTaskStack[100];	//定义空闲任务栈
StaticTask_t xIdleTask3TCB;			//定义空闲任务TCB//申请获得空闲任务内存
//要提供空闲任务的TCBBuffer,空闲任务的栈Buffer,空闲任务的栈大小
void vApplicationGetIdleTaskMemory(StaticTask_t ** ppxIdleTaskTCBBuffer, StackType_t ** ppxIdleTaskStackBuffer,uint32_t * pulIdleTaskStackSize)
{*ppxIdleTaskTCBBuffer = &xIdleTask3TCB;*ppxIdleTaskStackBuffer = xIdleTaskStack;*pulIdleTaskStackSize = 100;
}//main.c
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);    //动态创建
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);    //动态创建
xTaskCreateStatic(Task3Function, "Task3", 100, NULL, 1, xTask3Stack, &xTask3TCB);    //静态创建

1.3 进一步实验

1.3.1 优先级实验

        在FreeRTOS中,优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。

        在之前的代码中添加修改:

static int task1flagrun = 0;
static int task2flagrun = 0;
static int task3flagrun = 0;void Task1Function( void * param)
{while(1){task1flagrun = 1; task2flagrun = 0;task3flagrun = 0;printf("1");}
}void Task2Function( void * param)
{while(1){task1flagrun = 0; task2flagrun = 1;task3flagrun = 0;printf("2");}
}void Task3Function( void * param)
{while(1){task1flagrun = 0; task2flagrun = 0;task3flagrun = 1;printf("3");}
}

        然后再main函数中打断点,打开调试,运行到断点,然后将task1flagrun、task2flagrun、task3flagrun这三个变量添加到逻辑分析仪中去运行一段时间,我们会发现,同优先级的3个任务,不会同时进行。

        如果我们将Task1Function的优先级设置为2,其它仍为1,运行时候我们就会发现这时候,只打印1,没有执行另外两个任务。由此可以看出,对于FreeRTOS来说,在默认的调度下面,高优先级的任务先执行,如果高优先级的任务没有主动放弃运行,其它低优先级的任务无法执行。

1.3.2 删除任务

        我们在创建任务的时候传入了一个handle,以后想要引用这个任务就必须要通过这个handle执行。如果

void Task2Function( void * param)
{int i = 0;while(1){task1flagrun = 0; task2flagrun = 1;task3flagrun = 0;printf("2");//删除任务一if(i++ == 100){vTaskDelete(xHandleTask1);}//自杀if(i == 200){vTaskDelete(NULL);}}
}

        通过观察实验结果可以发现,经过修改的程序,在运行之初会打印1、2、3,运行一会儿后只打印2、3,再之后就只打印3了。

        使用vTaskDelete既可以删除动态创建的任务,也可以删掉静态创建的任务,但是要想删除任务,必须要记录创建任务时的返回值handle。

        FreeRTOS在xTaskCreate中分配TCB和栈,但是并不是在vTaskDelete中释放TCB和栈,而是在空闲任务中进行这些清理工作,如果连续不断的调用xTaskCreate和vTaskDelete,最终会导致内存耗光。 

1.3.3 使用同一个任务函数创建多个任务

void TaskGenericFunction( void * param)
{int val = (int)param;while(1){		printf("%d", val);}
}xTaskCreate(TaskGenericFunction, "Task4", 100, (void *)4, 1, NULL);
xTaskCreate(TaskGenericFunction, "Task5", 100, (void *)5, 1, NULL);

        通过结果发现4和5都成功打印出来了,证明可以在同一个任务函数中创建多个任务。

        使用同一个任务会产生不同的效果,是因为它们的栈是不一样的,传入的参数都保存在不同的栈里面,运行的时候互不影响。

1.3.4 栈大小实验

void Task1Function( void * param)
{//在创建任务时,申请了100*4的空间,这里故意定义500,耗尽栈的空间,从高地址向下增长,破坏下面的空间//这样程序运行的时候是完全不可控的,程序会崩溃volatile char buf[500];	//使用关键字,不允许没有使用的空间被优化掉int i;while(1){task1flagrun = 1; task2flagrun = 0;task3flagrun = 0;printf("1");for(i = 0; i < 500; i++){buf[i] = 0;}}
}

        程序按上面修改之后运行,直接崩溃了,在申请的栈空间以及TCB空间的前面会有一个头来存储有关的信息,便于程序返回等操作,一旦栈的空间使用不当,冲破了空间限制,把前面头部的信息更改了,程序就会发生不可控的问题。

2、任务状态

2.1 任务切换的基础:tick中断

        在FreeRTOS系统中有个定时器,这个定时器每隔一段时间会产生一个中断,这个间隔就称为tick,发生中断时,它会将发生中断的次数记录下来,初始值为0,每发生一次中断就累加1,这个中断值就成为tick count,这将是RTOS的时钟基准。

        在发生中断时,tick中断处理函数被调用,在这个函数中,它会判断是否要切换任务,如果要切换任务就会去切换任务。而运行任务的基准时间(周期),可以在源码的FreeRTOSConfig.h中去配置,我们也可以指定每个任务每次执行几个tick:

#define configTICK_RATE_HZ        ((TickType_t)1000)

        在使用keil模拟器中的逻辑分析仪时,如果时间不准确,要确认代码中设置时钟时用的频率和Options中Xtal的频率相同。

        在main函数中创建多个任务时,为什么后面创建的任务反而先运行?因为后面的任务插入链表时,pxCurrentTCB先执行它。创建任务时代码如下,后创建的最高优先级任务先执行:

2.2 有哪些任务状态?状态切换图

        正在运行的任务:Running状态

        可以随时运行但是现在还没轮到:Ready状态

        阻塞,等待某些事情发生才能继续执行:Blocked状态

        暂停,主动/被动休息:Suspended状态

        状态转换图:

2.3 怎么管理不同状态的任务:放在不同的链表里

        将不同状态的任务放在不同的链表里,当有需要执行时,就从对应链表中挑出一个来执行。

2.4 阻塞状态(Blocked)举例:vTaskDelay函数

        使用vTaskDelay时,如果想要延时若干毫秒,那么可以自己把毫秒换算成Tick数,或者使用宏把毫秒换算成Tick数。有一个宏:pdMS_TO_TICKS(ms),可以把毫秒转换成Tick数。

void Task2Function( void * param)
{int i = 0;while(1){task1flagrun = 0; task2flagrun = 1;task3flagrun = 0;printf("2");//让任务2进入阻塞状态vTaskDelay(10);}
}

2.5 暂停状态(Suspended)举例:vTaskSuspend/vTaskResume

void Task1Function( void * param)
{//获得TickCount的值TickType_t tStart = xTaskGetTickCount();TickType_t t;int flag = 0;while(1){t = xTaskGetTickCount();	//在运行的时候获取当前时间task1flagrun = 1; task2flagrun = 0;task3flagrun = 0;printf("1");//命令任务3进入暂停状态,如果想让自己主动休息,可以传入NULL//对于进入暂停状态的任务必须由别人来唤醒if(!flag && t > tStart + 10){vTaskSuspend(xHandleTask3);flag = 1;	//设置一个标志位,不要让重复让任务3进入暂停状态}if(t > tStart + 30){//命令任务3恢复运行状态vTaskResume(xHandleTask3);}}
}

3、实现周期性的任务

3.1 vTaskDelay

        至少等待指定个数的Tick Interrupt才能变成就绪状态。

static int rands[] = {2, 18, 64, 121, 9};void Task1Function( void * param)
{//获得TickCount的值TickType_t tStart = xTaskGetTickCount();int i =0;int j =0;while(1){task1flagrun = 1; task2flagrun = 0;task3flagrun = 0;for(i = 0; i < rands[j]; i++){printf("1");		}j++;if(j == 5){j = 0;}vTaskDelay(20);	//延时20个tick}
}

        通过结果图可以看出,延迟的时间的一致的。

3.2 vTaskDelayUntil

        等待到指定的绝对时刻,才能变为就绪状态。

        vTaskDelayUntil是老版本,它没有返回值,在新版本中可以用xTaskDelayUntil函数,二者传入的参数是一样的,只是新的有返回值。

static int rands[] = {2, 18, 64, 121, 9};void Task1Function( void * param)
{//获得TickCount的值TickType_t tStart = xTaskGetTickCount();int i =0;int j =0;while(1){task1flagrun = 1; task2flagrun = 0;task3flagrun = 0;for(i = 0; i < rands[j]; i++){printf("1");		}j++;if(j == 5){j = 0;}//设置个开关
#if 0vTaskDelay(20);	//延时20个tick
#elsevTaskDelayUntil(&tStart, 20);	//延时到(tStart+20tick)时刻,同时更新tStart=tStart+20tick
#endif}
}

        通过结果图可以看出,每两次开始执行之间的时间是一致的。

4、空闲任务及其钩子函数

4.1 空闲任务        

        在xTaskCreate中分配TCB和栈,但是并不一定是在vTaskDelete中释放TCB和栈,对于自杀的任务,由空闲任务来清理内存,对于他杀的任务,由凶手来清理。

        我们在任务1中创建任务2,并且将任务一的优先级设置为1,任务二的优先级设置为2,在任务二中打印语句之后自杀。这样我们在程序中会有3个任务,任务一、任务二和空闲任务,空闲任务的优先级最低,为0。这样执行之后,程序很快就崩溃了,因为可分配的内存不够了,堆不够了。

void Task2Function( void * param);/*-----------------------------------------------------------*/void Task1Function( void * param)
{TaskHandle_t xHandleTask2;BaseType_t xReturn;while(1){printf("1");xReturn = xTaskCreate(Task2Function, "Task2", 1024, NULL, 2, &xHandleTask2);if(xReturn != pdPASS){printf("xTaskCreate err\r\n");}}
}void Task2Function( void * param)
{while(1){printf("2");//vTaskDelay(2);vTaskDelete(NULL);}
}

        如果是在任务一中杀死任务二,那么程序现象会一直执行。

void Task2Function( void * param);/*-----------------------------------------------------------*/void Task1Function( void * param)
{TaskHandle_t xHandleTask2;BaseType_t xReturn;while(1){printf("1");xReturn = xTaskCreate(Task2Function, "Task2", 1024, NULL, 2, &xHandleTask2);if(xReturn != pdPASS){printf("xTaskCreate err\r\n");}vTaskDelete(xHandleTask2);}
}void Task2Function( void * param)
{while(1){printf("2");vTaskDelay(2);}
}

4.2 钩子函数         

        使用空闲任务不仅可以帮我们清理自杀的任务,还可以执行一些低优先级、后台的、需要连续执行的函数、测量系统的空闲时间、让系统进入省电模式等。如果要做到这些,我们可以通过修改空闲任务的函数来实现,但是如果直接修改会破坏FreeRTOS的核心文件,因此提供了一个钩子函数,可以先配置规定的宏,来告诉程序你要使用这个钩子函数。

        对钩子函数,空闲任务对其也会有一些限制:不能导致空闲任务进入阻塞状态、暂停状态;如果你会使用vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务一直卡在钩子函数里的话,它就无法释放内存。

        使用钩子函数的前提:

                在FreeRTOSConfig.h中把这个宏定义为1:configUSE_IDLE_HOOK

                实现vApplicationIdleHook函数

void Task2Function( void * param);/*-----------------------------------------------------------*/
static int task1flagrun = 0;
static int task2flagrun = 0;
static int taskidleflagrun = 0;void Task1Function( void * param)
{TaskHandle_t xHandleTask2;BaseType_t xReturn;while(1){task1flagrun = 1; task2flagrun = 0;taskidleflagrun = 0;printf("1");xReturn = xTaskCreate(Task2Function, "Task2", 1024, NULL, 2, &xHandleTask2);if(xReturn != pdPASS){printf("xTaskCreate err\r\n");}vTaskDelete(xHandleTask2);}
}void Task2Function( void * param)
{while(1){task1flagrun = 0; task2flagrun = 1;taskidleflagrun = 0;printf("2");vTaskDelay(2);}
}void vApplicationIdleHook(void)
{task1flagrun = 0; task2flagrun = 0;taskidleflagrun = 1;printf("0");
}//main函数里
xTaskCreate(Task1Function, "Task1", 100, NULL, 0, &xHandleTask1);

        在这个程序中,任务一先执行,在任务一中创建任务二,此时任务二的优先级最高,因此任务二先执行,之后杀掉任务二,任务一和空闲任务的优先级相同,二者交替执行,在执行空闲任务时会用到钩子函数。

5、任务调度算法

5.1 状态与事件

        正在运行的任务,被称为"正在使用处理器",它处于运行状态。在单处理器系统中,任何时间里只能有一个任务处于运行状态。

        非运行状态的任务,它处于这3种状态之一:

                阻塞(Blocked)

                暂停(Suspended)

                就绪(Ready)

        就绪态的任务,可以被调度器挑选出来切换为运行状态,调度器永远都是挑选最高优先级的就绪态任务并让它进入运行状态。

        阻塞状态的任务,它在等待"事件",当事件发生时任务就会进入就绪状态。

        事件分为两类:

                时间相关的事件:就是设置超时时间,在指定时间内阻塞,时间到了就进入就绪状态。使用时间相关的事件,可以实现周期性的功能、可以实现超时功能。

                同步事件:同步事件就是某个任务在等待某些信息,别的任务或者中断服务程序会给它发送信息。怎么"发送信息"的方法有很多,比如任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)等。这些方法用来发送同步信息,比如表示某个外设得到了数据。

5.2 调度策略

5.2.1 可否抢占?

        高优先级的任务能否优先执行(配置项: configUSE_PREEMPTION)

        如果可以:被称作"可抢占调度"(Pre-emptive),高优先级的就绪任务马上执行,下面再细化。

        如果不可以:不能抢就只能协商了,被称作"合作调度模式"(Co-operative Scheduling)。当前任务执行时,更高优先级的任务就绪了也不能马上运行,只能等待当前任务主动让出CPU资源。其他同优先级的任务也只能等待:更高优先级的任务都不能抢占,平级的更应该老实点

2.2.2 可抢占的前提下,同优先级的任务是否轮流执行(配置项: configUSE_TIME_SLICING)?

        轮流执行:被称为"时间片轮转"(Time Slicing),同优先级的任务轮流执行,你执行一个时间片、我再执行一个时间片

        不轮流执行:英文为"without Time Slicing",当前任务会一直执行,直到主动放弃、或者被高优先级任务抢占。

5.2.3 允许抢占、允许时间片轮转时,空闲任务是否让步?

        在"可抢占"+"时间片轮转"的前提下,进一步细化:空闲任务是否让步于用户任务(配置项:configlDLE_SHOULD_YIELD)。如果让步,则空闲任务低人一等,每执行一次循环,就看看是否主动让位给用户任务。如果不让步,空闲任务跟用户任务一样,大家轮流执行,没有谁更特殊。

相关文章:

  • PyCharm面板ctrl+鼠标滚轮放大缩小代码
  • 基于Pytorch框架的深度学习ShufflenetV2神经网络十七种猴子动物识别分类系统源码
  • three.js官方案例webgl_loader_fbx.html学习
  • Docker打包之后如何将进行变成压缩包进行传输和使用?
  • XSKY CTO 在英特尔存储技术峰会的演讲:LLM 存储,架构至关重要
  • 人脸识别技术的前沿技术和应用场景
  • 华为交换机、路由器开局(基础配置及远程登录)
  • OrangePi AIpro评测 - 基础操作篇
  • OpenHarmony 实战开发——内核对象队列之算法详解
  • 使用RAG和文本转语音功能,我构建了一个 QA 问答机器人
  • 【Paddle2ONNX】为Paddle2ONNX升级Opset版本到18
  • 免费,Python蓝桥杯等级考试真题--第12级(含答案解析和代码)
  • el-table 实现嵌套表格的思路及完整功能代码
  • “胖东来”超市商业模式,为何被誉为中国零售业是神一般的存在?
  • C++的第一道门坎:类与对象(一)
  • Android框架之Volley
  • CAP 一致性协议及应用解析
  • docker容器内的网络抓包
  • javascript 总结(常用工具类的封装)
  • java取消线程实例
  • js继承的实现方法
  • js面向对象
  • learning koa2.x
  • MySQL主从复制读写分离及奇怪的问题
  • nginx 负载服务器优化
  • windows下mongoDB的环境配置
  • 翻译:Hystrix - How To Use
  • 力扣(LeetCode)56
  • 手写双向链表LinkedList的几个常用功能
  • 探索 JS 中的模块化
  • 腾讯大梁:DevOps最后一棒,有效构建海量运营的持续反馈能力
  • 优化 Vue 项目编译文件大小
  • 在GitHub多个账号上使用不同的SSH的配置方法
  • 最近的计划
  • ​queue --- 一个同步的队列类​
  • #pragma pack(1)
  • #基础#使用Jupyter进行Notebook的转换 .ipynb文件导出为.md文件
  • #使用清华镜像源 安装/更新 指定版本tensorflow
  • (delphi11最新学习资料) Object Pascal 学习笔记---第13章第1节 (全局数据、栈和堆)
  • (第9篇)大数据的的超级应用——数据挖掘-推荐系统
  • (二)fiber的基本认识
  • (附源码)php新闻发布平台 毕业设计 141646
  • (十五)使用Nexus创建Maven私服
  • (收藏)Git和Repo扫盲——如何取得Android源代码
  • (学习日记)2024.01.19
  • (一)RocketMQ初步认识
  • (已更新)关于Visual Studio 2019安装时VS installer无法下载文件,进度条为0,显示网络有问题的解决办法
  • (原創) 物件導向與老子思想 (OO)
  • (转)重识new
  • ***检测工具之RKHunter AIDE
  • *1 计算机基础和操作系统基础及几大协议
  • .Net CoreRabbitMQ消息存储可靠机制
  • .NET I/O 学习笔记:对文件和目录进行解压缩操作
  • .NET 反射 Reflect
  • .Net(C#)常用转换byte转uint32、byte转float等