前言
在上一章我们完成了工程的创建后面代码都会基于这个模板来编写,本章就学习一下实时操作系统给我们带来最直观的优势,多任务执行;
任务创建
xTaskCreate((TaskFunction_t )led0_task, (const char* )"led0_task", (uint16_t )256, (void* )NULL, (UBaseType_t )4, (TaskHandle_t* )&led0_task_handle);
在上一章节我们通过这个函数创建了一个任务,我们跳转进去看一下这个函数的原型
portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode, const signed portCHAR * const pcName, unsigned portSHORT usStackDepth, void *pvParameters, unsigned portBASE_TYPE uxPriority, xTaskHandle *pxCreatedTask );
- pdTASK_CODE pvTaskCode :是一个指向任务的实现函数的指针,任务只是一个不会退出的子函数,通常我们会写一个死循环来实现;
- const signed portCHAR * const pcName:任务名,用于辅助调试FreeRTOS并不会使用他;
- unsigned portSHORT usStackDepth:当任务创建时,内核会分为每个任务分配属于任务自己的唯一状态,该值用于指定分配多大的栈空间,传入的值表示的是栈空间可以保存多少个字(word),而不是多少个字节(byte)。比如说,如果是 32 位宽的栈空间,传入的 usStackDepth值为 100,则将会分配 400 字节的栈空间(100 * 4bytes)。栈深度乘以栈宽度的结果千万不能超过一个 size_t 类型变量所能表达的最大值,大部分时间用户赋予较合理的数值即可(小了就调大,大了就调小尽量保证空间的不浪费)
- void *pvParameters:传入我们任务函数中的值,一般为NULL
- unsigned portBASE_TYPE uxPriority:指定任务执行的优先级。优先级的取值范围可以从最低优先级 0 到最高优先级(configMAX_PRIORITIES – 1)
- xTaskHandle *pxCreatedTask:传入任务句柄,如果该任务不会被应用程序用到也可以设置为NULL;
- 该函数有两个可能的返回值:
pdTRUE: 表明任务创建成功。
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:由于内存堆空间不足,FreeRTOS 无法分配足够的空间来保存任务结构数据和任务栈,因此无法创建任务
当我们创建完成一个任务,我们的任务状态还是处于就绪状态,还未开启任务调度器,也没创建空闲任务与定时器任务任务调度器只启动一次,之后就不会再次执行了,FreeRTOS 中启动任务调度器的函数是 vTaskStartScheduler(),并且启动任务调度器的时候就不会返回,从此任务管理都由 FreeRTOS管理,如果这里我们卸载vTaskStartScheduler后面的语句执行了也就说明我们的任务启动失败这时候我们需要根据情况特殊处理(这里需要注意在实时操作系统中我们的任务看似是可以共同执行,但实际过程中他们也是有一个切换过程的,在任何时刻只可能有一个任务处于运行态。所以一个任务进入运行态后(切入)另一个任务就会进入非运行态(切出),更多理论上的就不多赘述了网上有很多大神写过,或者查看官方文档了解freertos的任务调度算法;
任务函数
static void led1_task(void *par) { while(1) { HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin); vTaskDelay(200); /* 延时500个tick */ printf("hello FreeRTOS! I am led1_task\r\n"); } }
这是我们上一章的测试代码,其实他就是个普通的函数,在内部实现一个死循环,如果任务的具体实现会跳出上面的死循环,则此任务必须在函数运行完之前删除。传入NULL参数表示删除的是当前任务
static void led1_task(void *par) { int i = 10; while(i--) { HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin); vTaskDelay(200); /* 延时500个tick */ printf("hello FreeRTOS! I am led1_task\r\n"); } vTaskDelete( NULL ); }
任务传参
在创建任务的过程中我们有一个参数是可以传入给我们的任务函数的,具体使用方法如下
/* 创建led0任务 */ xTaskCreate((TaskFunction_t )led0_task, /* 任务入口函数 */ (const char* )"led0_task", /* 任务名字 */ (uint16_t )256, /* 任务栈大小 */ (void* )"hello FreeRTOS! I am led0_task\r\n", /* 任务入口函数参数 */ (UBaseType_t )4, /* 任务的优先级 */ (TaskHandle_t* )&led0_task_handle); /* 任务控制块指针 */ static void led0_task(void *par) { char *pcTaskName; pcTaskName = ( char * ) par; // 根据我们要传入的值的类型进行转换 while(1) { HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin); vTaskDelay(500); /* 延时500个tick */ printf(pcTaskName); } vTaskDelete( NULL ); }
多个任务创建
/* 创建led0任务 */ xTaskCreate((TaskFunction_t )led0_task, /* 任务入口函数 */ (const char* )"led0_task", /* 任务名字 */ (uint16_t )256, /* 任务栈大小 */ (void* )"hello FreeRTOS! I am led0_task\r\n", /* 任务入口函数参数 */ (UBaseType_t )4, /* 任务的优先级 */ (TaskHandle_t* )&led0_task_handle); /* 任务控制块指针 */ /* 创建led1任务 */ xTaskCreate((TaskFunction_t )led1_task, /* 任务入口函数 */ (const char* )"led1_task", /* 任务名字 */ (uint16_t )256, /* 任务栈大小 */ (void* )"hello FreeRTOS! I am led1_task\r\n", /* 任务入口函数参数 */ (UBaseType_t )2, /* 任务的优先级 */ (TaskHandle_t* )&led1_task_handle); /* 任务控制块指针 */ /* 开启任务调度 */ vTaskStartScheduler();
任务优先级
xTaskCreate() API 函数的参数 uxPriority 为创建的任务赋予了一个初始优先级。这个侁先级可以在调度器启动后调用 vTaskPrioritySet() API 函数进行修改。
应用程序在文件 FreeRTOSConfig.h 中设定的编译时配置常量configMAX_PRIORITIES的值,即是最多可具有的优先级数目。FreeRTOS本身并没有限定这个常量的最大值,但这个值越大,则内核花销的内存空间就越多。所以总是建议将此常量设为能够用到的最小值。
对于如何为任务指定优先级,FreeRTOS并没有强加任何限制。任意数量的任务可以共享同一个优先级以保证最大设计弹性。当然,如果需要的话,你也可以为每个任务指定唯一的优先级(就如同某些调度算法的要求一样),但这不是强制要求的。低优先级号表示任务的优先级低,优先级号 0 表示最低优先级。有效的优先级号范围从 0 到(configMAX_PRIORITES – 1)
调度器保证总是在所有可运行的任务中选择具有最高优先级的任务,并使其进入运行态。如果被选中的优先级上具有不止一个任务,调度器会让这些任务轮流执行,每个任务都执行一个”时间片”,任务在时间片起始时刻进入运行态,在时间片结束时刻又退出运行态。
要能够选择下一个运行的任务,调度器需要在每个时间片的结束时刻运行自己本身。一个称为心跳tick(也就是我们的系统时钟中断systick),中断的周期性中断用于此目的。时间片的长度通过心跳中断的频率进行设定,心跳中断频率由FreeRTOSConfig.h中的编译时配置常量 configTICK_RATE_HZ 进行配置。比如说,如果configTICK_RATE_HZ 设为100(HZ),则时间片长度为 10ms。
通过上面的引文我们可以知道优先级低的任务总会被先执行,那如果我们把我们之前任务的优先级改一下我们来看看效果
可以看到我们的高优先级led0_task和低优先级led1_task还是会交替运行的,这是因为我们的任务函数中有vTaskDelay这个函数,这个函数是让该任务进入挂起状态,然后去执行其他任务函数,如果我们将两个任务函数中的vTaskDelay函数替换成while延时再试试看;
可以看到我们只有优先级高的led0的任务还在执行,而led1的任务函数就无法执行了,这种情况我们称为任务 1 的执行时间被任务 2”饿死(starved)”了,但不是说每个任务都是需要加入vTaskDelay这个函数才会交替运行的,如果他们的运行优先级在同一等级的时候系统还是会让两个任务进行交替调度的,例如下面这种情况(我们子函数还是不做更改);
任务优先级和堆栈空间大小在实时操作系统中是非常重要的,需要大家多多实践;
任务的暂停和启动
在实际开发中很少情况会用到,但是在有用到的情况我们还是需要掌握用法;
static void led0_task(void *par) { char *pcTaskName; pcTaskName = ( char * ) par; while(1) { HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin); vTaskDelay(200); /* 延时500个tick */ printf(pcTaskName); } vTaskDelete( NULL ); } static void led1_task(void *par) { char *pcTaskName; int i = 0; pcTaskName = ( char * ) par; while(1) { HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin); vTaskDelay(200); /* 延时500个tick */ printf(pcTaskName); if(i++ == 5) { // 判断led0任务是否是挂起状态,如果不是则挂起我们的led0任务 if(eTaskGetState(led0_task_handle) != eSuspended) vTaskSuspend(led0_task_handle); }else if(i == 20){ // 判断led0任务是否是运行状态,如果不是则运行我们的led0任务 if(eTaskGetState(led0_task_handle) != eRunning) vTaskResume(led0_task_handle); } } vTaskDelete( NULL ); }
任务删除
以下代码我们实现第二个任务中循环十次让led0的任务停止运行,在上面的任务子函数编写中我们可以看到vTaskDelete这个函数,他的作用就是删除任务,当我们传入的参数为NULL时表示删除本任务,当我们传入任务句柄他就会删除该任务句柄下的任务;
结尾
我是凉开水白菜,我们下文见~