韦东山老师《FreeRTOS入门与工程实践(基于DshanMCU-103)》书籍获取:https://gitee.com/jingcheng11/embedded-data.git
任务
任务的三大要素:函数、栈&TCB结构体、优先级
创建任务方式
创建任务时可以使用 2 个函数:动态分配内存、静态分配内存。
动态分配内存
动态分配内存函数会分配栈和TCB结构体
句柄理解
静态分配内存
静态分配内存函数需要事先指定栈和TCB结构体
使用一个函数创建不同的任务
多个任务可以使用同一个函数,怎么体现它们的差别?
- 栈不同
- 创建任务时可以传入不同的参数
我们创建 3 个任务,使用同一个函数,但是在 LCD 上打印不一样的信息。
在具体的任务中,要注意的点:
在OLED显示时,如果删掉下图中包含的“延时函数”,OLED就会只显示一行(只会显示Task3部分)。其实其他任务已经在执行了,只是可能没有进入到打印就被切换Task3了(因为打印的时间比较久,所以很大概率会被这个时间切换,而一旦被切换,全局变量g_LCDCanUse被置为0,所以TASK2、3永远打印不出来)。但是加上延时之后,会有很大概率在执行延时函数的时候被切换(这个时候全局变量g_LCDCanUse是1,所以能执行打印;另外的Task)。
提问:为什么只显示Task3部分?
答:Task3是被最后创建的,会先被执行。具体解释看下图。
任务调度这里韦东山老师讲的特别清楚!
另外,为什么要添加全局变量?因为在LCD打印的时候,虽然使用了三行函数,但是实际上打印出来的是一行话。所以不能被打断。所以,使用全局变量g_LCDCanUse能实现互斥访问LCD,但不是万无一失的。(注意:虽然能实现,但这里的方法不太好,只是解释代码,后面会逐步改进)
创建&删除音乐播放任务
怎么删除任务?
- 自杀:vTaskDelete(NULL)
- 被杀:别的任务执行 vTaskDelete(pvTaskCode),pvTaskCode 是自己的句柄
- 杀人:执行 vTaskDelete(pvTaskCode),pvTaskCode 是别的任务的句柄
删除任务时使用的函数如下:void vTaskDelete( TaskHandle_t xTaskToDelete );
上述代码音乐播放有卡顿,播放和停止播放效果。
改进音乐播放效果(优先级)
- FreeRTOS 会确保最高优先级的、可运行的任务,马上就能执行
- 对于相同优先级的、可运行的任务,轮流执行
想让音乐播放效果更好(比较快,不卡顿)可以通过改变优先级和vTaskDelay函数来实现。
音乐不卡顿
osPriorityNormal+1 //只需要将优先级 +1 即可
仅仅做这一步,在实际实验过程中会发现,音乐确实不卡顿了,但是在按下“停止播放”按钮后丝毫无反应!这是因为我们创建音乐播放任务优先级高,且内部死循环,所以它一直抢占CPU资源,导致停止播放任务一直执行不了。
那如何解决呢?
修改延时函数
下面的mdelay函数一直内部死循环,将这行的延时函数改成vTaskDelay就可以解决上述 “暂停不了音乐” 的问题。
vTaskDelay(MusicSpeed/Music_Lone_Brave[i][2]);
通过这一操作,既保持了好的音乐播放效果,也不影响其他任务
通过上述操作,可以实现音乐播放和删除任务,但是存在一个问题:音乐每次都是从头开始播放的,那么如何使得音乐播放是从上次暂停的位置开始播放呢? ,这就涉及到任务状态转换。
任务状态
可以分为运行状态和非运行状态;非运行状态又可分为三种。总体可以分为:
- 运行状态(Running)
- 阻塞状态(Blocked)
- 暂停状态(Suspended)
- 就绪状态(Ready)
音乐暂停&接着播放
我们需要实现按下"Play"键播放,再次按"Play"键暂停,再次按"Play"键播放…这样的功能。具体函数,我们来分析一下。当第一次按play,音乐播放任务处于运行(Running)状态,该函数内部有函数vTaskDelay,因此可能会进入阻塞状态;在这两种状态下,如图所示需要通过vTaskSuspend()函数进入暂停状态;当再次按play键,我们需要音乐接着播放,此时,需要跳出暂停状态回到ready状态,需要通过vTaskResume()函数实现;当在ready状态时,由于“音乐播放任务”的优先级比较高,因此很快进入Running状态实现再次播放音乐。
那如何进行状态之间的切换呢?我们需要一个标志位。具体代码如下图所示:
//部分函数定义与初始化不做展示了
int FlagMusic = 0;
while (1)
{
/*读取红外遥控器*/
if(0 == IRReceiver_Read(&dev, &data)){
if(data == 0xa8){
/*创建音乐播放任务*/
extern void PlayMusic(void *params);
if(xSoundTaskHandle == NULL){
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Create task");
ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, osPriorityNormal+1, &xSoundTaskHandle);
FlagMusic = 1;
}
else
{
if(FlagMusic)
{
PassiveBuzzer_Control(0);
LCD_PrintString(0, 0, "Suspend task");
vTaskSuspend(xSoundTaskHandle);
FlagMusic = 0;
}
else
{
//PassiveBuzzer_Control(1);
LCD_PrintString(0, 0, "Resume task");
vTaskResume(xSoundTaskHandle);
FlagMusic = 1;
}
}
}else if(data == 0xa2){
/*删除音乐播放任务*/
if(xSoundTaskHandle != NULL){
PassiveBuzzer_Control(0);
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Delete task");
vTaskDelete(xSoundTaskHandle);
xSoundTaskHandle = NULL;
}
}
}
}
任务管理与调度
韦东山老师任务调度,在视频20多分钟的时候,讲了详细的任务执行过程。
任务管理与任务调动的核心是链表操作,能够发生任务调动依赖于Tick中断。
Tick
系统时钟使用定时器产生固定间隔的中断:产生Tick中断。
中断函数会做三件事:
- cnt++,累加计数
- 判断DelayedTaskList里面的任务是否可恢复,如果可恢复会将该任务移到ReadyList中去
- 发起调度:
去高优先级到低优先级去遍历ReadyList链表,直到找到非空项,找到非空链表之后,取出下一个要运行的任务去运行(这里会有相应索引找到下一个要运行的任务,比如已经运行完Task1和Task2了,此时就会运行Task3)。
两次中断之间的时间被称为时间片。程序中设置阻塞和暂停的目的是充分利用时间片资源。
空闲函数
介绍
空闲任务(Idle 任务)的作用之一:释放被删除的任务的内存。
除了上述目的之外,为什么必须要有空闲任务?一个良好的程序,它的任务都是事件驱
动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。
在使用vTaskStartScheduler()函数来创建、启动调度器时,这个函数内部会创建空闲任务:
- 空闲任务优先级为 0:它不能阻碍用户任务运行
- 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
空闲任务的优先级为 0,这意味着一旦某个用户的任务变为就绪态,那么空闲任务马上
被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)了空闲任务,这是由调度器实现的。要注意的是:如果使用vTaskDelete()来删除任务,那么你就要确保空闲任务有机会执行,否则就无法释放被删除任务的内存。
可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循
环每执行一次,就会调用一次钩子函数。钩子函数可以:测量系统的空闲时间、让系统进入省电模式、执行一些低优先级的、后台的、需要连续执行的函数等等。
任务退出
- 自杀:vTaskDelete(NULL)
- 他杀:vTaskDelete(handle)
不设置死循环,退出任务
系统瘫痪的写法:
正确写法:
加上 vTaskDelete(NULL),也就是”自杀“,自杀后由空闲任务 “收尸”(做一些任务清除工作,如释放TCB结构体、释放栈)。
两个delay 函数
- vTaskDelay() 非周期性运行,等待指定个数的Tick Interrupt才能变为就绪状态。
- vTaskDelayUntil() 使任务周期性运行,等待到指定的绝对时刻,才能变为就绪态。
使用该函数,需要传入参数:pxPreviousWakeTime(上一次被唤醒的时间 )和xTimeIncrement(要阻塞到(pxPreviousWakeTime + xTimeIncrement)),也就是起始时间和增量时间。