FreeRTOS(创建任务)
任务三要素:
下面我们来试试两种创建任务方式:动态分配/静态分配
用到的函数:
//动态分配xTaskCreate();//静态分配xTaskCreateStatic();两个函数的参数:
下面是代码示例:
/* USER CODE BEGIN Variables *///全局变量staticStackType_t g_pucStackofColorTask[128];//定义堆大小staticStaticTask_t g_TCBofColorTask;//定义TCB结果staticTaskHandle_t xColorTaskHandle;//定义句柄/* USER CODE END Variables *//* USER CODE BEGIN Init *///局部变量TaskHandle_t xLightTaskHandle;BaseType_t ret;//接收返回值/* USER CODE END Init *//* USER CODE BEGIN RTOS_THREADS *//* add threads, ... *//*创建任务:光*/ret=xTaskCreate(Led_Test,"LightTask",128,NULL,osPriorityNormal,&xLightTaskHandle);/*创建任务:色*/xColorTaskHandle=xTaskCreateStatic(ColorLED_Test,"ColorLEDTask",128,NULL,osPriorityNormal,g_pucStackofColorTask,&g_TCBofColorTask);/* USER CODE END RTOS_THREADS */估算栈的大小:
栈的大小由这三个决定:
现场:就是切换任务时会在栈内分配一块新的内存(PSR)来保存这个任务的全部寄存器(16个),所以占16*4=64字节
局部变量:由定义的变量大小决定
返回地址:由这个调用关系决定
可以看到假设这个是五级调用,每级要保存的寄存器一般为9个,所以返回地址的大小为594=180字节大小
例子:
进到结构体看看有多少个成员:7个
所以这个任务的栈大概250字节左右,而我们设置的是128(uint_32t)即128*4=512,足够了
同一函数创建多个任务:
任务代码如下:
/* Private function prototypes -----------------------------------------------*//* USER CODE BEGIN FunctionPrototypes *///定义参数结构体structTaskPtintInfo{uint8_tx;uint8_ty;charName[16];};//定义参数staticstructTaskPtintInfoTask1={0,0,"Task1:"};staticstructTaskPtintInfoTask2={0,3,"Task2:"};staticstructTaskPtintInfoTask3={0,6,"Task3:"};staticintg_LCDCanUse=1;voidLcdPrintTask(void*parms){LCD_Init();//初始任务被我注释掉了,里面包括了lcd初始化和清屏函数,所以这里再调用一下LCD_Clear();structTaskPtintInfo*pInfo=parms;int32_tcnt=0;intlen=0;while(1){if(g_LCDCanUse==1){g_LCDCanUse=0;len=LCD_PrintString(pInfo->x,pInfo->y,pInfo->Name);//赋值字符串长度给lenLCD_PrintSignedVal(len,pInfo->y,cnt++);g_LCDCanUse=1;}mdelay(500);}}/* USER CODE END FunctionPrototypes */
g_LCDCanUse变量是用来保护每个任务的,从之前的学习中,我们知道每个任务能运行的时间是固定的,假设在我们这个显示OLED任务运行到一半时被切换到另外的一个任务,那么很有可能在I2C传输这里被切换,一个完整的I2C传输被打断会出现问题的,所以我们使用g_LCDCanUse变量来保证这个任务能接上。这里的实现代码是:当Task3运行时,将g_LCDCanUse设置为0,所以切换到Task2和Task1时,因为g_LCDCanUse==0,无法进入if内部任务,所以只有当切换到Task3运行完任务将g_LCDCanUse设置为1时,才能到下一个任务正常运行(不过这个方法不是万无一失的,那怎么解决这个互斥访问LCD呢?后面我们在解决)
这里设置一个mdelay(500)是为了能保证其它任务正常执行,假设没有这个延迟函数,那当Task3结束if()语句任务时,因为把g_LCDCanUse设置为1,所以它会再次进入if()语句里面,那Task3就会一直卡在if()里面,其它任务就一直得不到执行,所以设置一个延迟函数是为了在运行到延迟函数时能切换到其它任务来执行
创建任务代码如下:
/*创建文件:同一函数多个任务*/xTaskCreate(LcdPrintTask,"LcdTask1",128,&Task1,osPriorityNormal,NULL);xTaskCreate(LcdPrintTask,"LcdTask2",128,&Task2,osPriorityNormal,NULL);xTaskCreate(LcdPrintTask,"LcdTask3",128,&Task3,osPriorityNormal,NULL);看过我第一节理论课应该都知道,这里能同一个函数创建不同任务是因为每个任务都有属于自己的栈
删除任务:
代码如下:
voidStartDefaultTask(void*argument){/* USER CODE BEGIN StartDefaultTask *//* Infinite loop */LCD_Init();LCD_Clear();staticTaskHandle_t xPlayMusicHandle=NULL;uint8_tdev,data;intlen;IRReceiver_Init();LCD_PrintString(0,0,"Waitting Conctol");while(1){if(IRReceiver_Read(&dev,&data)==0){/*创建播放音乐任务*/if(data==0xa8){LCD_ClearLine(0,0);externvoidPlayMusic(void*params);//防止创建多个任务if(xPlayMusicHandle==NULL){LCD_PrintString(0,0,"Play Music");xTaskCreate(PlayMusic,"playmusci",128,NULL,osPriorityNormal,&xPlayMusicHandle);}}/*删除播放音乐任务*/if(data==0xa2){//判断有没有创建任务if(xPlayMusicHandle!=NULL){LCD_ClearLine(0,0);LCD_PrintString(0,0,"Stop Music");vTaskDelete(xPlayMusicHandle);xPlayMusicHandle=NULL;}}}}/* USER CODE END StartDefaultTask */}我在初始任务这里来实现创建任务和删除任务
注意static TaskHandle_t xPlayMusicHandle = NULL;这里定义句柄要初始化,因为局部变量是随机的,如果你要判定的话,很大可能是创建不了任务的,同时会拿垃圾句柄去删除任务,这就会导致按play键没反应,按power键有一次反应
提高蜂鸣器优先级:
我们在上小节源代码基础上添加一些小改动:
可以看到我们提高了播放音乐任务的优先级,但如果我们只做这个改动会导致,这个任务优先级最高,一直占用了CPU资源,这就会导致其它任务的停止,并且不能删除这任务,怎么处理呢?如下:
只要将该任务进入阻塞状态,那么其它任务就能正常运行
使任务进入暂停状态来控制音乐的暂停和播放:
状态转换图:
控制音乐播放代码如下:
voidStartDefaultTask(void*argument){/* USER CODE BEGIN StartDefaultTask *//* Infinite loop */LCD_Init();LCD_Clear();staticTaskHandle_t xPlayMusicHandle=NULL;uint8_tdev,data;intlen;IRReceiver_Init();LCD_PrintString(0,0,"Waitting Conctol");uint8_tTaskState_Flag=1;//0表示任务暂停,1表示任务进行while(1){if(IRReceiver_Read(&dev,&data)==0){/*创建播放音乐任务*/if(data==0xa8){LCD_ClearLine(0,0);externvoidPlayMusic(void*params);//防止创建多个任务if(xPlayMusicHandle==NULL){LCD_PrintString(0,0,"Play Music");xTaskCreate(PlayMusic,"playmusci",128,NULL,osPriorityNormal+1,&xPlayMusicHandle);}else//再次按下play键,如果已经创建任务{if(TaskState_Flag==1){/*使任务进入暂停状态*/vTaskSuspend(xPlayMusicHandle);PassiveBuzzer_Control(0);//暂停蜂鸣器LCD_PrintString(0,0,"Stop Music");TaskState_Flag=0;}elseif(TaskState_Flag==0){/*使任务恢复就绪状态*/vTaskResume(xPlayMusicHandle);LCD_PrintString(0,0,"Ready Music");TaskState_Flag=1;}}}/*删除播放音乐任务*/if(data==0xa2){if(xPlayMusicHandle!=NULL){LCD_ClearLine(0,0);LCD_PrintString(0,0,"Delect Music");vTaskDelete(xPlayMusicHandle);PassiveBuzzer_Control(0);//暂停蜂鸣器xPlayMusicHandle=NULL;}}}}任务的管理和调度:
还记得我们的说过的默认任务调度策略吗?如下:
这些任务调度核心是链表:
我们来到CubeMX里面看到FreeTROS的配置参数中可以看到,任务优先级有56个
我们来到定义这个优先级宏内部看看:
可以看到这三种链表,而pxReadyTasksLists链表就是管理不同优先级处于就绪态的任务链表,以我们的代码为例:
当我们创建这些任务的使用,会按这些任务的优先级放到就绪链表对应优先级链表中,而我们设置的默认优先级就是24:
可以看到在24优先级链表中链接着三个任务的结构体,其它链表都是空的,那它是怎么调度的呢?来到main.c文件中可以看到该函数:
osKernelStart();//启动内核vTaskStartScheduler();//启动调度器,在启动内核函数内部
在启动调度器函数里面可以看到,还会创建一个空闲任务,这个空闲任务的优先级是0
在添加进就绪链表函数中里面有一个全局指针pxCurrent
如上,当我们创建第一个任务StartDefaultTasks时,没有其它任务,它是全局最高的优先级任务,所以全局指针会指向它,然后创建第二个任务时,它又会指向第二个任务,以此类推,最终它会指向最后一个任务,所以这也是为什么创建任务一、二、三的时候,调度器会先调用任务三
从图一时间轴,我们可以知道,在我们设置好的时间中(我们这里设置的是每1m)会触发一次中断,触发Tick中断后,cnt++,然后会触发调度,怎么调度呢?从图二可以知道,它会从链表中从上到下遍历这些优先级链表,看看哪个不是空的(这里假设25优先级链表还没有任务),当它到24优先级链表时就会找到StartDefaultTasks任务,然后就让pxCurrent指向这个任务,于是它找到了下一个要运行的任务,然后启动它(这也是为什么任务三启动后,会到任务一启动)
那假设我设置一个更高的优先级任务呢?如图二,当创建这个优先级更高的任务时,系统会立马停止并保存当前任务,去执行这个优先级更高的任务,而且我们知道如果这个任务不调用vTaskDelay函数让它进入阻塞状态,那它就会一直占用CPU资源,那调用vTaskDelay函数会发生什么?
还记得我们进入启动调度器函数内部有三种链表吗?如下
当调用vTaskDelay函数时,系统会将该任务从RredyLists移到DelayedTaskList链表中(这里面有一个Value记录延迟2个Tick),接着回到时间轴
我们可以看到当最高优先级任务进入阻塞状态时,会触发调度,那它又会回到就绪链表,从上往下遍历这些链表,回到24优先级链表里面时,会有一个index记录项,这个index会指向上一次运行的任务(即第一个任务),它会根据这个index找出下一个任务执行(即任务二)
回到时间轴,我们可以看到当经过两个Tick时间,触发Tick中断时,它不仅会正常计数cnt++和调度,还会去判断一下Delay链表里面的任务时间有没有到(这是发生在调度里面的),当Delay链表里面的任务时间到了以后,会将它重新放回就绪链表中,然后发起调度,最后又会先执行它
最后还是回到时间轴
假设我们调用vTaskSusoend函数呢?那系统会将它放到第三钟链表中
因为在触发Tick中断中,并没有到这个链表判断的操作,所以要从这个链表将任务拿出来,只能调用vTaskResume函数将它放回到就绪链表中
最后总结一下,有一个系统时钟,Tick中断,当发生Tick中断时,它会做三件事情:累加计数、判断一下Delay链表里面的任务时间有没有到、发起调度
空闲函数:
从这个图我们可以知道,当一个任务没有进入死循环,而且设置一个时间,当这个时间到了以后,不经过其它处理,直接返回的话,会返回到prvTaskExitError函数,这是个错误函数,这个函数会关闭所有中断,我们知道所有任务都是依靠中断来执行的,所有这会导致所有任务停止
所以任务退出只有两种方式:“自杀或者他杀”
我们了解过在删除任务的时候,调用删除任务的函数来释放被删除任务的TCB和栈空间,拿自杀呢?之前的FreeRTOS(纯理论)那节时的疑惑可以在这里解除了,自杀会由空闲函数来帮你释放TCB和栈空间的任务,但空闲任务的优先级是最低了(0),我们可以知道其它任务优先级是远远大于空闲任务的,所以这些任务的执行,会导致空闲任务永远无法给这些自杀任务“收尸”,那怎么办呢?
第一:事件驱动,第二尽量使用延时函数(vTaskDelay、vTaskDelayuint),不要用死循环
我们可以进入mdelay,udelay函数内部可以看到,它们就是死循环
所以我们只要把它们改成阻塞函数(vTaskDelay、vTaskDelayuint),那么这些任务做完自己的事情时就会进入阻塞状态,不参与调度,就让出了CPU资源,这样空闲函数就有机会执行了
那么有同学疑惑了,假如这些任务轮流退出进入阻塞状态,那空闲函数不也一直得不到执行吗?
我们分两种情况来看:
情况一:两个任务交替阻塞/就绪,但总有任务处于就绪态
假设任务 A 和任务 B 都是普通优先级,A 刚进入阻塞态,B 刚好退出阻塞态。此时 B 处于就绪态,调度器会直接切换到 B 运行,空闲任务不会被选中。等 B 也进入阻塞态后,如果此时 A 还没到延时结束的时间,就绪链表为空,空闲任务才会被调度。
所以在这种“你方唱罢我登场”的交替中,空闲任务确实可能长时间得不到运行——但这没关系,因为 CPU 并没有闲着,它一直在执行有用的任务(A 或 B)。空闲任务本来就是在“无事可做”时才运行的,它不运行恰恰说明系统很忙、很健康。
情况二:所有任务都进入阻塞态
只有当所有任务都阻塞(比如都在vTaskDelay等待中)时,就绪链表才为空,调度器才会选择空闲任务运行。此时空闲任务会执行,直到某个任务的延时到期、重新进入就绪态,调度器又会立刻切走空闲任务。
所以你的担心其实反了:空闲任务得不到运行,恰恰说明系统里一直有任务在干活,这是好事。真正要担心的是某个高优先级任务一直处于就绪态(比如死循环不阻塞),那才会把 CPU 占死,让其它任务和空闲任务都得不到运行。
总结:
| 场景 | 空闲任务是否运行 |
|---|---|
| 有任务处于就绪态 | 不运行(CPU 被该任务占用) |
| 所有任务都阻塞 | 运行(就绪链表为空) |
| 高优先级任务死循环不阻塞 | 不运行(且其它任务也被饿死) |
所以只要任务都会主动进入阻塞态,空闲任务总有机会运行——哪怕它被“挤”了很久,但只要所有任务都阻塞的那一刻到来,它就会立刻被调度。
所以我们不用担心空闲函数得不到运行的问题。
最后提醒一下:空闲函数里面还有一个钩子函数可以提供我们写代码逻辑
两个Delay函数:
xTaskDelay()函数时间轴:
xTaskDelayunitl()函数时间轴:
可以看到这两个阻塞函数的时间轴是不一样的,第一个任务与任务之间的时间间隔是不固定的,第二个则是有周期性的,下面我们通过代码来看一下
xTaskDelay代码:
uint64_tt1,t2;voidLcdPrintTask(void*parms){//Time=;LCD_Init();LCD_Clear();structTaskPtintInfo*pInfo=parms;int32_tcnt=0;intlen=0;while(1){if(g_LCDCanUse==1){g_LCDCanUse=0;len=LCD_PrintString(pInfo->x,pInfo->y,pInfo->Name);//赋值字符串长度给lenLCD_PrintSignedVal(len,pInfo->y,cnt++);g_LCDCanUse=1;mdelay(cnt&0x3);//随机取几个数}t1=system_get_ns();vTaskDelay(500);//500000000t2=system_get_ns();LCD_ClearLine(pInfo->x,pInfo->y);LCD_PrintSignedVal(pInfo->x,pInfo->y+2,t2-t1);}}现象:
xTaskDelayunitl代码如下:
staticintg_LCDCanUse=1;TickType_t Time_Test;uint64_tt1,t2;voidLcdPrintTask(void*parms){Time_Test=xTaskGetTickCount();LCD_Init();LCD_Clear();structTaskPtintInfo*pInfo=parms;int32_tcnt=0;intlen=0;while(1){if(g_LCDCanUse==1){g_LCDCanUse=0;len=LCD_PrintString(pInfo->x,pInfo->y,pInfo->Name);//赋值字符串长度给lenLCD_PrintSignedVal(len,pInfo->y,cnt++);g_LCDCanUse=1;mdelay(cnt&0x3);//随机取几个数}t1=system_get_ns();vTaskDelayUntil(&Time_Test,500);//500000000t2=system_get_ns();LCD_ClearLine(pInfo->x,pInfo->y);LCD_PrintSignedVal(pInfo->x,pInfo->y+2,t2-t1);}}现象:
区别如上
