目录
一、什么是任务
二、创建任务---xTaskCreate函数
三、任务的删除
四、任务优先级
1.阻塞状态(Blocked)
2.暂停状态(Suspended)
3.就绪状态(Ready)
五、Delay
六、调度算法
在FreeRTOS中,任务就是一个函数,原型如下:
void ATaskFunction( void *pvParameters );
要注意的是:
这个函数不能返回
同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数
函数内部,尽量使用局部变量:
每个任务都有自己的栈
每个任务运行这个函数时
任务A的局部变量放在任务A的栈里、任务B的局部变量放在任务B的栈里
不同任务的局部变量,有自己的副本
任务示例如下:
void ATaskFunction( void *pvParameters )
{
/* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */
int32_t lVariableExample = 0;
/* 任务函数通常实现为一个无限循环 */
for( ;; )
{
/* 任务的代码 */
}
/* 如果程序从循环中退出,一定要使用vTaskDelete删除自己
* NULL表示删除的是自己
*/
vTaskDelete( NULL );
/* 程序不会执行到这里, 如果执行到这里就出错了 */
}
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数
const char * const pcName, // 任务的名字
const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节
void * const pvParameters, // 调用任务函数时传入的参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务
参数描述:
pvTaskCode
函数指针,可以简单地认为任务就是一个C函数。
它稍微特殊一点:永远不退出,或者退出时要调用"vTaskDelete(NULL)"
pcName
任务的名字,FreeRTOS内部不使用它,仅仅起调试作用。
长度为:configMAX_TASK_NAME_LEN
usStackDepth
每个任务都有自己的栈,这里指定栈大小。
单位是word,比如传入100,表示栈大小为100 word,也就是400字节。
最大值为uint16_t的最大值。
怎么确定栈的大小,并不容易,很多时候是估计。
精确的办法是看反汇编码。
pvParameters
调用pvTaskCode函数指针时用到:pvTaskCode(pvParameters)
uxPriority
优先级范围:0~(configMAX_PRIORITIES – 1)
数值越小优先级越低,:更高优先级的、或者后面创建的任务先运行。
如果传入过大的值,xTaskCreate会把它调整为(configMAX_PRIORITIES – 1)
pxCreatedTask
用来保存xTaskCreate的输出结果:task handle。
以后如果想操作这个任务,比如修改它的优先级,就需要这个handle。
如果不想使用该handle,可以传入NULL。
返回值
成功:pdPASS;
失败:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足)
注意:返回值是pdFAIL不对。
pdFAIL是0,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY是-1。
多个任务可以使用同一个函数;
void vTaskFunction( void *pvParameters )
{
const char *pcTaskText = pvParameters;
volatile uint32_t ul; /* volatile用来避免被优化掉 */
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务的信息 */
printf(pcTaskText);
/* 延迟一会(比较简单粗暴) */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
}
}
}
static const char *pcTextForTask1 = "T1 run\r\n";
static const char *pcTextForTask2 = "T2 run\r\n";
int main( void )
{
prvSetupHardware();
xTaskCreate(vTaskFunction, "Task 1", 1000, (void *)pcTextForTask1, 1, NULL);
xTaskCreate(vTaskFunction, "Task 2", 1000, (void *)pcTextForTask2, 1, NULL);
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
删除任务时使用的函数如下:
void vTaskDelete( TaskHandle_t xTaskToDelete );
pvTaskCode
任务句柄,使用xTaskCreate创建任务时可以得到一个句柄。
也可传入NULL,这表示删除自己。
自杀: vTaskDelete(NULL)
被杀:别的任务执行vTaskDelete(pvTaskCode) ,pvTaskCode是自己的句柄
杀人:执行vTaskDelete(pvTaskCode) ,pvTaskCode是别的任务的句柄
FreeRTOS一天一个小知识之任务延时函数vTaskDelay-CSDN博客https://blog.csdn.net/simplemethane/article/details/116998825以下是这篇文章中谈到的延迟的内容:
Dealy的延时,是通过CPU做循环的方式来延时,CPU在延时中是做不了其他东西的,大大浪费了CPU的效率!而且非常危险!
所以大家在裸机中如果要需要很长时间延时的话,建议用定时器来延时。
void vTaskDelay( const TickType_t xTicksToDelay )
{
BaseType_t xAlreadyYielded = pdFALSE;
/* A delay time of zero just forces a reschedule. */
if( xTicksToDelay > ( TickType_t ) 0U )
{
configASSERT( uxSchedulerSuspended == 0 );
vTaskSuspendAll();
{
traceTASK_DELAY();
/* A task that is removed from the event list while the
* scheduler is suspended will not get placed in the ready
* list or removed from the blocked list until the scheduler
* is resumed.
*
* This task cannot be in an event list as it is the currently
* executing task. */
prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
}
xAlreadyYielded = xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* Force a reschedule if xTaskResumeAll has not already done so, we may
* have put ourselves to sleep. */
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
FreeRTOS这个任务执行是这样的。首先TASK1创建,然后在创建TASK2
TASK先执行, 执行到GPIO_SetBits(GPIOC,GPIO_Pin_2); 下一句vTaskDelay(500); 延时500ms,其实就是任务挂起500ms,CPU此时不会执行TASK的任务,去执行处于就绪态的TASK2, 当TASK2的GPIO_ResetBits(GPIOC,GPIO_Pin_3); 执行好了之后执行下一条 vTaskDelay(200);此时TASK1延时500ms,TASK延时200ms。
这时候FreeRTOS是没有执行处于就绪态的任务的,只有执行空闲任务 。此时由于TASK2是延时200ms,比TASK2延时的500ms要快,所以TASK2比TASK1更早进入就绪态,此时CPU执行 GPIO_SetBits(GPIOC,GPIO_Pin_3); 这一语句,执行好了之后TASK2又延时800ms,进入挂起态。当TASK1延时500ms到,TASK1进入就绪态,
执行GPIO_ResetBits(GPIOC,GPIO_Pin_2); i++;语句,执行完之后,TASK1又进入500ms的延时,进入挂起态~
【
任务堆栈
任务堆栈用来保存任务现场(CPU寄存器值),创建任务的时候需要指定任务堆栈,任务堆栈的变量类型为StackType_t,再次运行任务时会从上次中断的地方开始运行
】
所以在FreeRTOS中的延时函数,只是任务挂起和任务恢复而已
//任务一
void vTask1( void *pvParameters )
{
const TickType_t xDelay100ms = pdMS_TO_TICKS( 100UL );
BaseType_t ret;
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务的信息 */
printf("Task1 is running\r\n");
ret = xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
if (ret != pdPASS)
printf("Create Task2 Failed\r\n");
// 如果不休眠的话, Idle任务无法得到执行
// Idel任务会清理任务2使用的内存
// 如果不休眠则Idle任务无法执行, 最后内存耗尽
vTaskDelay( xDelay100ms );
}
//任务二
void vTask2( void *pvParameters )
{
/* 打印任务的信息 */
printf("Task2 is running and about to delete itself\r\n");
// 可以直接传入参数NULL, 这里只是为了演示函数用法
vTaskDelete(xTask2Handle);
}
//main函数
int main( void )
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
main函数中创建任务1,优先级为1。任务1运行时,它创建任务2,任务2的优先级是2。
任务2的优先级最高,它马上执行。
任务2打印一句话后,就删除了自己。
任务2被删除后,任务1的优先级最高,轮到任务1继续运行,它调用vTaskDelay() 进入Block状
态
任务1 Block期间,轮到Idle任务执行:它释放任务2的内存(TCB、栈)
时间到后,任务1变为最高优先级的任务继续执行。
如此循环。
在任务1的函数中,如果不调用vTaskDelay,则Idle任务用于没有机会执行,它就无法释放创建任务2是分配的内存。而任务1在不断地创建任务,不断地消耗内存,最终内存耗尽再也无法创建新的任务。
高优先级的任务先运行。
优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。
FreeRTOS会确保最高优先级的、可运行的任务,马上就能执行
对于相同优先级的、可运行的任务,轮流执行
举例子:
厨房着火了,当然优先灭火
喂饭、回复信息同样重要,轮流做
对于同优先级的任务,它们“轮流”执行。怎么轮流?你执行一会,我执行一会。
"一会"怎么定义?
人有心跳,心跳间隔基本恒定。
FreeRTOS中也有心跳,它使用定时器产生固定间隔的中断。这叫Tick、滴答,比如每10ms发生一次时钟中断。
假设t1、t2、t3发生时钟中断
两次中断之间的时间被称为时间片(time slice、tick period)
时间片的长度由configTICK_RATE_HZ 决定,假设configTICK_RATE_HZ为100,那么时间片长度就是10ms
相同优先级的任务怎么切换呢?请看下图:
任务2从t1执行到t2
在t2发生tick中断,进入tick中断处理函数:
选择下一个要运行的任务
执行完中断处理函数后,切换到新的任务:任务1
任务1从t2执行到t3
从图中可以看出,任务运行的时间并不是严格从t1,t2,t3哪里开始
在FreeRTOS中,系统时钟节拍的特点就是周期性中断,既然要产生中断那就需要定时器,所以在这里就是使用了一个24位的定时器,采用向下计数的方式,然后可以产生周期性的中断。
系统在使用的时候,一般是在FreeRTOSConfig.h里面进行配置
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
这是一个常用的配置,系统的节拍频率设置为1000,也就是说系统的节拍周期为1ms,这也是最为典型的一种设置。
vTaskDelay(2); // 等待2个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待20ms
// 还可以使用pdMS_TO_TICKS宏把ms转换为tick
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms
注意,基于Tick实现的延时并不精确,比如vTaskDelay(2) 的本意是延迟2个Tick周期,有可能经过1个Tick多一点就返回了。
使用vTaskDelay函数时,建议以ms为单位,使用pdMS_TO_TICKS把时间转换为Tick。
将毫秒数换算成了tick数
#define pdMS_TO_TICKS( xTimeInMs ) ( ( TickType_t ) ( ( ( TickType_t ) ( xTimeInMs ) * ( TickType_t ) configTICK_RATE_HZ ) / ( TickType_t ) 1000U ) )
SysTick 定时器被捆绑在 NVIC 中,用于产生 SysTick 异常(异常号: 15), 滴答定时器是一个 24 位的递减计数器,支持中断。
使用比较简单, 专门用于给操作系统提供时钟节拍。
FreeRTOS 的系统时钟节拍可以在配置文件 FreeRTOSConfig.h 里面设置:
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
如上所示的宏定义配置表示系统时钟节拍是 1KHz,即 1ms。
void vTask3( void *pvParameters )
{
const TickType_t xDelay3000ms = pdMS_TO_TICKS( 3000UL );
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务的信息 */
printf("T3\r\n");
// 如果不休眠的话, 其他任务无法得到执行
vTaskDelay( xDelay3000ms );
}
}
修改优先级:
使用uxTaskPriorityGet来获得任务的优先级:
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );
使用参数xTask来指定任务,设置为NULL表示获取自己的优先级。
使用vTaskPrioritySet 来设置任务的优先级:
void vTaskPrioritySet( TaskHandle_t xTask,
UBaseType_t uxNewPriority );
使用参数xTask来指定任务,设置为NULL表示设置自己的优先级;
参数uxNewPriority表示新的优先级,取值范围是0~(configMAX_PRIORITIES – 1)。
五、任务状态
void vTask1( void *pvParameters )
{
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务的信息 */
printf("T1\r\n");
}
}
void vTask2( void *pvParameters )
{
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务的信息 */
printf("T2\r\n");
}
}
void vTask3( void *pvParameters )
{
const TickType_t xDelay3000ms = pdMS_TO_TICKS( 3000UL );
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
/* 打印任务的信息 */
printf("T3\r\n");
// 如果不休眠的话, 其他任务无法得到执行
vTaskDelay( xDelay3000ms );
}
}
/***********************************************/
//main函数:
int main()
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);
xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL);
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
如果把任务3中的vTaskDelay调用注释掉,那么任务1、任务2根本没有执行的机会,任务1、任务2被"饿死"了(starve)。
在实际产品中,我们不会让一个任务一直运行,而是使用"事件驱动"的方法让它运行:
任务要等待某个事件,事件发生后它才能运行
在等待事件过程中,它不消耗CPU资源
在等待事件的过程中,这个任务就处于阻塞状态(Blocked)
在阻塞状态的任务,它可以等待两种类型的事件:
时间相关的事件
可以等待一段时间:我等2分钟
也可以一直等待,直到某个绝对时间:我等到下午3点
同步事件:这事件由别的任务,或者是中断程序产生
例子1:任务A等待任务B给它发送数据
例子2:任务A等待用户按下按键
在等待一个同步事件时,可以加上超时时间。
比如等待队里数据,超时时间设为10ms:
10ms之内有数据到来:成功返回
10ms到了,还是没有数据:超时返回
在日常生活的例子中,母亲正在电脑前跟同事沟通,母亲可以暂停:
好烦啊,我暂停一会
领导说:你暂停一下
FreeRTOS中的任务也可以进入暂停状态,唯一的方法是通过vTaskSuspend函数。函数原型如下:
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
参数xTaskToSuspend表示要暂停的任务,如果为NULL,表示暂停自己。
要退出暂停状态,只能由别人来操作:
别的任务调用:vTaskResume
中断程序调用:xTaskResumeFromISR
实际开发中,暂停状态用得不多。
这个任务完全准备好了,随时可以运行:只是还轮不到它。这时,它就处于就绪态(Ready)。
有两个Delay函数:
vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。
void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: 等待多少给
Tick */
/* pxPreviousWakeTime: 上一次被唤醒的时间
* xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement)
* 单位都是Tick Count
*/
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement );
使用vTaskDelay(n)时,进入、退出vTaskDelay的时间间隔至少是n个Tick中断
使用xTaskDelayUntil(&Pre, n)时,前后两次退出xTaskDelayUntil的时间至少是n个Tick中断退出xTaskDelayUntil时任务就进入的就绪状态,一般都能得到执行机会,所以可以使用xTaskDelayUntil来让任务周期性地运行
int main( void )
{
prvSetupHardware();
/* Task1的优先级更高, Task1先执行 */
xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
void vTask1( void *pvParameters )
{
const TickType_t xDelay50ms = pdMS_TO_TICKS( 50UL );
TickType_t xLastWakeTime;
int i;
/* 获得当前的Tick Count */
xLastWakeTime = xTaskGetTickCount();
for( ;; )
{
flag = 1;
/* 故意加入多个循环,让程序运行时间长一点 */
for (i = 0; i <5; i++)
printf( "Task 1 is running\r\n" );
##if 1
vTaskDelay(xDelay50ms);
##else
vTaskDelayUntil(&xLastWakeTime, xDelay50ms);
##endif
}
}
void vTask2( void *pvParameters )
{
for( ;; )
{
flag = 0;
printf( "Task 2 is running\r\n" );
}
}
使用Keil的逻辑分析观察flag变量的bit波形,如下:
flag为1时表示Task1在运行,flag为0时表示Task2在运行,也就是Task1处于阻塞状态
vTaskDelay:指定的是阻塞的时间
vTaskDelayUntil:指定的是任务执行的间隔、周期
static volatile int flagIdleTaskrun = 0; // 空闲任务运行时flagIdleTaskrun=1
static volatile int flagTask1run = 0; // 任务1运行时flagTask1run=1
static volatile int flagTask2run = 0; // 任务2运行时flagTask2run=1
static volatile int flagTask3run = 0; // 任务3运行时flagTask3run=1
int main( void )
{
prvSetupHardware();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 0, NULL);
xTaskCreate(vTask2, "Task 2", 1000, NULL, 0, NULL);
xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL);
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}
void vTask1( void *pvParameters )
{
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
flagIdleTaskrun = 0;
flagTask1run = 1;
flagTask2run = 0;
flagTask3run = 0;
/* 打印任务的信息 */
printf("T1\r\n");
}
}
void vTask2( void *pvParameters )
{
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
flagIdleTaskrun = 0;
flagTask1run = 0;
flagTask2run = 1;
flagTask3run = 0;
/* 打印任务的信息 */
printf("T2\r\n");
}
}
void vTask3( void *pvParameters )
{
const TickType_t xDelay5ms = pdMS_TO_TICKS( 5UL );
/* 任务函数的主体一般都是无限循环 */
for( ;; )
{
flagIdleTaskrun = 0;
flagTask1run = 0;
flagTask2run = 0;
flagTask3run = 1;
/* 打印任务的信息 */
printf("T3\r\n");
// 如果不休眠的话, 其他任务无法得到执行
vTaskDelay( xDelay5ms );
}
}
提供了一个空闲任务的钩子函数:
void vApplicationIdleHook(void)
{
flagIdleTaskrun = 1;
flagTask1run = 0;
flagTask2run = 0;
flagTask3run = 0;
/* 故意加入打印让flagIdleTaskrun变为1的时间维持长一点 */
printf("Id\r\n");
}
抢占时:高优先级任务就绪时,就可以马上执行
不抢占时:优先级失去意义了,既然不能抢占就只能协商了,图中任务1一直在运行(一点都没有协商精神),其他任务都无法执行。即使任务3的vTaskDelay 已经超时、即使它的优先级更高,都没办法执行。