上一篇我们介绍了延时函数。讲到了因为FreeRTOS是多任务的,因此我们使用延时函数可以将当前任务挂起,就可以让CPU去执行其他任务,等到延时时间结束,再接着执行之前的任务,这样可以充分利用CPU的性能。
那么既然ESP-IDF FreeRTOS是多任务的,那么我们必然是可以创建任务的,之前一直用的app_main是ESP-IDF FreeRTOS默认创建的main任务。
创建任务可用的函数有俩,如果是多核的ESP32,那么可以使用四个(不过本质上还是两个,这个后面再说)
static inline BaseType_t xTaskCreate(TaskFunction_t pxTaskCode, const char *const pcName, const configSTACK_DEPTH_TYPE usStackDepth, void *const pvParameters, UBaseType_t uxPriority, TaskHandle_t *const pxCreatedTask)
上面这个函数就可以创建出一个新的任务并且添加到准备运行的任务列表中,也就是说我们用这个函数一旦创建了任务,那么马上就会准备开始执行。
第一个参数传入函数指针,这个函数里的内容就是我们这个任务要执行的内容。需要注意的是这个任务无返回值,可以接受一个void*类型的参数(也可以不接收)。另外这个函数要么内部是死循环,要么需要自己把自己杀死,否则编译报错。具体如何杀死自己,后面会介绍。
我们可以看看参数一的类型,看看这个任务函数的原型是什么。
类型很简单,就是无返回值,可以有个void*类型参数的函数。
第二个参数传入一个字符串表示这个任务的名字,这个随便起,不懂起名字的小伙伴可以和自己的任务函数的函数名一致(记得是传入字符串)。
第三个参数传入这个任务函数的栈大小,单位是字(word),我们ESP32是32位的,因此一个字是4个Byte,这个单位换算要清楚。每个任务函数都有各自独立的栈,我们需要各自指定栈的大小,这个栈的大小我们大概的估计一下就行,不用很精确。一般来说不确定就给个1024,多余总比不够用好,不过还是要具体问题具体分析。
如果这个栈大小给的少了,那么当执行这个任务的时候,会导致我们ESP32不停地重启。
第四个参数传入给任务函数传的参数,没有就填NULL。
第五个是优先级,数值越小优先级越小(注意这个,这边和STM32中的优先级不一样)。优先级用于任务的调度,也就是说多个任务理论上是可以看成是并行的,这个我们后面会验证。
优先级的数值我们可以根据宏定义configMAX_PRIORITIES来判断,这个宏定义就是我们能用的最大的优先级+1(优先级从0开始,一共有configMAX_PRIORITIES这么多个的等级)。
第六个是任务句柄,是传出参数,我们可以先定义一个任务句柄,然后在创建任务的时候传进去。任务句柄主要是用来删除任务的,当然删除任务并不一定是要使用到任务句柄,也可以让任务自杀,这个后面会说。
多个任务可以共用同一个函数。可以使用传入的参数来区分是哪个任务调用的。
那么下面就简单举个例子。
下面创建了两个任务,调用的是同一个函数,通过参数的不同来打印不同的内容。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
void test(void* who){
int* val = who;
printf("this is test%d\r\n",*val);
while(1){
printf("hello%d\r\n",*val);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void){
TaskHandle_t test1_handle,test2_handle;
int t1 = 1,t2 = 2;
xTaskCreate(test,"test1",1024*2,&t1,configMAX_PRIORITIES/2,&test1_handle);
xTaskCreate(test,"test2",1024*2,&t2,configMAX_PRIORITIES/2,&test2_handle);
printf("this is main\r\n");
while(1){
printf("world\r\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
上面这个算是我们用的最多的创建任务的函数了,它会帮我们自动分配内存,当然我们也可以使用其他函数来自己给任务分配内存。
static inline TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode, const char *const pcName, const uint32_t ulStackDepth, void *const pvParameters, UBaseType_t uxPriority, StackType_t *const puxStackBuffer, StaticTask_t *const pxTaskBuffer)
前面几个参数和自动分配内存的函数的参数是一样的,不一样的是最后两个。
分别是我们给它静态分配的栈,最后一个是一个结构体,它保存的是任务的数据结构。
且这个函数的返回值是任务的句柄,而我们动态分配的函数的任务句柄是通过最后一个传出参数给我们的,也就是说静态分配比动态分配要多出两个参数。
静态分配内存比较复杂,且我们大多情况使用动态分配内存的函数创建任务就够了,所以这边就贴上官方的示例代码,感兴趣的小伙伴看看就行。
// Dimensions of the buffer that the task being created will use as its stack.
// NOTE: This is the number of words the stack will hold, not the number of
// bytes. For example, if each stack item is 32-bits, and this is set to 100,
// then 400 bytes (100 * 32-bits) will be allocated.
#define STACK_SIZE 200
// Structure that will hold the TCB of the task being created.
StaticTask_t xTaskBuffer;
// Buffer that the task being created will use as its stack. Note this is
// an array of StackType_t variables. The size of StackType_t is dependent on
// the RTOS port.
StackType_t xStack[ STACK_SIZE ];
// Function that implements the task being created.
void vTaskCode( void * pvParameters ){
// The parameter value is expected to be 1 as 1 is passed in the
// pvParameters value in the call to xTaskCreateStatic().
configASSERT( ( uint32_t ) pvParameters == 1UL );
for( ;; )
{
// Task code goes here.
}
}
// Function that creates a task.
void vOtherFunction( void ){
TaskHandle_t xHandle = NULL;
// Create the task without using any dynamic memory allocation.
xHandle = xTaskCreateStatic(
vTaskCode, // Function that implements the task.
"NAME", // Text name for the task.
STACK_SIZE, // Stack size in words, not bytes.
( void * ) 1, // Parameter passed into the task.
tskIDLE_PRIORITY,// Priority at which the task is created.
xStack, // Array to use as the task's stack.
&xTaskBuffer ); // Variable to hold the task's data structure.
// puxStackBuffer and pxTaskBuffer were not NULL, so the task will have
// been created, and xHandle will be the task's handle. Use the handle
// to suspend the task.
vTaskSuspend( xHandle );
}
细心的小伙伴可能发现了,上面出现了一个我们没见过的函数vTaskSuspend。
我们先简单介绍一下任务的几种状态。
首先我们通过ESP-IDF FreeRTOS可以创建多个任务,正常情况下任务是“并行”的(看起来是并行的,实际上是通过分割时间片来轮流运行,因为太快了所以在我们看来就是并行的),但严格说来,在一个时间点上,CPU只能执行一个任务(多核除外,多核是每个核都可以执行一个任务),那么正在执行的任务就处在运行状态,而其他没执行的,在等待执行的任务就处在就绪状态。
如果有个任务执行了vTaskDelay或者xTaskDelayUntil,那么它会暂时挂起,也就是阻塞状态,当然除了这俩函数会让任务阻塞,其他还有很多办法,这个我们后面再说。
如果有个任务,我现在不想让它执行了,那我可以删除它,可我万一以后还需要它,我只要它暂时消失怎么办呢?那么我们可以让它处于暂停状态,等我要它了,我再唤醒它,让它再次处于就绪状态。
而vTaskSuspend就是让任务处于暂停状态的函数。
void vTaskSuspend(TaskHandle_t xTaskToSuspend)
传入任务句柄即可将此任务暂停,如果传入的是NULL,那么是暂停自己(调用这个函数的任务)。
当我们需要它了,我们用下面这个函数再次唤醒它。
void vTaskResume (TaskHandle_t xTaskToResume)
传入任务句柄即可唤醒对应任务,但是需要注意的是,不能通过传入NULL来自己唤醒自己,这个应该不难理解。
接下来来个小例子示范一下这俩函数。
在主函数里计数,计到3秒的时候把test任务暂停(打印hello),计到六秒的时候再继续。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
void test(void*){
printf("this is test\r\n");
while(1){
printf("hello\r\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void){
TaskHandle_t test1_handle;
xTaskCreate(test,"test1",1024*2,NULL,configMAX_PRIORITIES/2,&test1_handle);
printf("this is main\r\n");
int count = 0;
TickType_t curTime = xTaskGetTickCount();
while(1){
printf("count is %d\r\n",++count);
if(count == 3) vTaskSuspend(test1_handle);
else if(count ==6) vTaskResume(test1_handle);
xTaskDelayUntil(&curTime,pdMS_TO_TICKS(1000));
}
}
关于任务状态,我们再介绍一个函数。
void vTaskDelete(TaskHandle_t xTaskToDelete)
传入任务句柄来删除对应任务,如果传入NULL,那么当前任务自己删除自己(紫砂)。
关于创建任务,ESP-IDF FreeRTOS还提供了另外两个函数。
BaseType_t xTaskCreatePinnedToCore(TaskFunction_t pxTaskCode, const char *const pcName, const uint32_t ulStackDepth, void *const pvParameters, UBaseType_t uxPriority, TaskHandle_t *const pxCreatedTask, const BaseType_t xCoreID)
TaskHandle_t xTaskCreateStaticPinnedToCore(TaskFunction_t pxTaskCode, const char *const pcName, const uint32_t ulStackDepth、void *const pvParameters、UBaseType_t uxPriority、StackType_t *const puxStackBuffer、 StaticTask_t *const pxTaskBuffer, const BaseType_t xCoreID)
简单来说这两个函数就是乐鑫为了适配多核系统给整出来的,也就是说这俩函数只能在ESP-IDF FreeRTOS里使用。它们相较于上面动态分配和静态分配的创建任务的函数就多了一个参数,这个参数来指定我们这个任务固定到哪个核心上去。
但我们平常使用不用专门去记这两个,第一是普适性不强,只能在ESP-IDF FreeRTOS中,并且是双核的ESP32才能使用。第二是就算我们使用了xTaskCreate,如果我们的芯片支持双核,那么它底层还是会帮我们调用多核版本的。
默认帮我设置的核心是不固定核心,可以在任意一个核心上运行,这也算是自动分配了。
我们可以使用下面这个函数获取当前内核管理的任务数。
UBaseType_t uxTaskGetNumberOfTasks(void)