FreeRtos中任务的有关理解

创建任务的接口函数

osThreadNew 和 xTaskCreate 都是用于创建任务(线程)的函数,但它们属于不同的接口和框架。

接口层级:

    • xTaskCreate: 是 FreeRTOS 的原生 API 直接用于创建任务,属于 FreeRTOS 的核心函数。这是一个更低级的接口,直接与 FreeRTOS 的调度器交互。
    • osThreadNew: 是根据 CMSIS-RTOS API 规范定义的接口,目的是提供一个更高层次的、硬件无关的 RTOS 接口。CMSIS-RTOS API 可以被视为在 FreeRTOS 之上提供了一层抽象,使得代码更具可移植性。
  • 使用 xTaskCreate 是一个针对 FreeRTOS 的直接接口,提供了更底层的控制。
  • 使用 osThreadNew 则是一个更加标准化和可移植的方式,适合需要与不同 RTOS 系统兼容的项目。

 freertos中创建任务时使用的函数:(静态和动态)

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 内部不使用。pcName 的长度为 configMAX_TASK_NAME_LEN。
usStackDepth 每个任务都有自己的栈,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)。如果以后需要对该任务进行操作,如修改优先级,则需要使用此句柄。如果不需要使用该句柄,可以传入 NULL。
返回值 成功时返回 pdPASS,失败时返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因是内存不足)。请注意,文档中提到的失败返回值是 pdFAIL 是不正确的。pdFAIL 的值为 0,而 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 的值为 -1。
TaskHandle_t xTaskCreateStatic ( 
    TaskFunction_t pxTaskCode,   // 函数指针, 任务函数
    const char * const pcName,   // 任务的名字
    const uint32_t ulStackDepth, // 栈大小,单位为word,10表示40字节
    void * const pvParameters,   // 调用任务函数时传入的参数
    UBaseType_t uxPriority,      // 优先级
    StackType_t * const puxStackBuffer, // 静态分配的栈,就是一个buffer
    StaticTask_t * const pxTaskBuffer // 静态分配的任务结构体的指针,用它来操作这个任务
);

相比于使用动态分配内存创建任务的函数,最后2个参数不一样:

参数描述
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)
puxStackBuffer 静态分配的栈内存,比如可以传入一个数组, 它的大小是usStackDepth*4。
pxTaskBuffer 静态分配的StaticTask_t结构体的指针
返回值 成功:返回任务句柄; 失败:NULL

 那么动态和静态分别适合在什么场景下使用呢?

如果系统有固定的、确定的任务需求,并且对内存有严格的使用要求,建议使用静态创建任务。另一方面,如果任务数量和优先级在运行时可以动态变化,动态创建任务会是更灵活的选择。

编程实例1

在02_freertos_first_app中,默认任务运行LCD程序,显示字符,自己使用xTaskCreate函数创建的任务中运行LED闪烁程序。LCD程序和LED闪烁程序内部都是死循环实现,在裸机中是不可能同时运行的,但是在freertos中,通过任务调度,可以让用户看到两个程序同时运行的现象。(本质上是交替执行的)

void Led_Test(void)
{
    Led_Init();

    while (1)
    {
        Led_Control(LED_GREEN, 1);
        HAL_Delay(500);
        Led_Control(LED_GREEN, 0);
        HAL_Delay(500);
    }
}

void OLED_Test(void)
{
    uint16_t cnt=0;
    OLED_Init();
    // 清屏
    OLED_Clear();
    
    while (1)
    {
        // 在(0, 0)打印'A'
        OLED_PutChar(0, 0, 'A');
        // 在(1, 0)打印'Y'
        OLED_PutChar(1, 0, 'Y');
        // 在第0列第2页打印一个字符串"Hello World!"
        OLED_PrintString(0, 2, "Hello World!");
        OLED_PrintSignedVal(0, 4, cnt++);
    }
}

//默认任务
defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);

//led任务
xTaskCreate(MyTask,"myfirsttask",128,NULL,osPriorityNormal,NULL);

void StartDefaultTask(void *argument)
{
  /* USER CODE BEGIN StartDefaultTask */
  /* Infinite loop */
  for(;;)
  {
    //osDelay(1);
    //Led_Test();
    LCD_Test();
  }
}

void MyTask(void *argument)
{
    while(1)
    {
        Led_Test();
    }
}

在创建任务时,FreeRTOS会在堆中开辟出一块空间,用于存放任务的控制信息TCB块和栈区Stack用于储存任务相关的变量,每一个任务都有自己的栈。

在任务切换时,FreeRTOS 需要保存当前任务的上下文,以便将来能够恢复。而后,切换到新任务时,FreeRTOS 需要加载新的任务上下文。这一过程称为上下文切换。保存现场保存在哪里呢,就保存在栈里。保存的什么呢,包括寄存器的值,pc,sp等。

  • 做何事

 

  • 优先级

freertos任务调度策略:

高优先级的先执行,若是优先级相同的任务,这些任务会以轮询的方式进行调度,即时间片轮转。时间片的长度由系统的节拍频率来决定,FreeRTOSConfig.h 文件中configTICK_RATE_HZ 宏定义来确定节拍频率。在轮转过程中如果某个任务进入就绪状态(例如,任务主动阻塞等待某个事件或延迟),调度器会选择运行的下一个任务。一旦有更高优先级的任务就绪,会马上得到运行。

调度方法:使用链表进行理解

 在创建一个任务之后,它就处于就绪状态,根据优先级挂载在就绪链表中,如上图所示,startDefaultTask、Led_Test、ColorLED_Test三个任务都是默认优先级24,他们根据创建顺序依次挂载在ReadyList[24]中,pxCurrentTCB指向当前任务TCB(pxCurrentTCB根据创建的任务,依次指向startDefaultTask、Led_Test、ColorLED_Test)。那么系统开始运行时,ColorLED_Test任务会率先开始运行,运行一个时间片之后,系统遍历就绪链表,找到第一个非空链表(即优先级最高的),然后pxCurrentTCB就指向startDefaultTask,这个任务得到运行。

如果运行过程中,某一个任务发生阻塞,那么就把它从就绪链表删除,移动到阻塞链表,阻塞解除之后,再放回来。当调用vTaskSuspend时,会将任务挂载在挂起链表中。

 

 当发生tick中断,也就是要切换任务时,会发生:

  • cnt++
  • 判断阻塞链表中的任务是否可以恢复到就绪链表
  • 便利就绪链表进行调度

创建任务之使用任务参数(多个任务使用同一个函数,传递不同的参数)

要传的参数

typedef struct TaskPrintInfo{
    uint8_t x;
    uint8_t y;
    char name[16];
}LcdPrintInfo;

//要传的参数
static LcdPrintInfo g_Task1Info = {0,0,"Task1"};
static LcdPrintInfo g_Task2Info = {0,3,"Task2"};
static LcdPrintInfo g_Task3Info = {0,6,"Task3"};

任务函数

void LcdPrintTask(void *params)
{
    LcdPrintInfo *pInfo = params; //指针赋值 
    uint32_t cnt = 0;
    uint8_t len = 0;
    while(1)
    {
        len = LCD_PrintString(pInfo->x,pInfo->y,pInfo->name);
        len+= LCD_PrintString(len,pInfo->y,":");
        LCD_PrintSignedVal(len,pInfo->y,cnt++);
    }
}

创建任务

xTaskCreate(LcdPrintTask,"LCDTask1",128,&g_Task1Info,osPriorityAboveNormal,NULL);
xTaskCreate(LcdPrintTask,"LCDTask2",128,&g_Task2Info,osPriorityAboveNormal,NULL);
xTaskCreate(LcdPrintTask,"LCDTask3",128,&g_Task3Info,osPriorityAboveNormal,NULL);

要注意LCD基于IIC传输,属于互斥资源,在这里我们先使用一个简单的全局变量来保护互斥资源

static int g_LCDCanUse = 1;
void LcdPrintTask(void *params)
{
    LcdPrintInfo *pInfo = params; //指针赋值 
    uint32_t cnt = 0;
    uint8_t len = 0;
    while(1)
    {
        if(g_LCDCanUse)
        {
            g_LCDCanUse = 0;
            len = LCD_PrintString(pInfo->x,pInfo->y,pInfo->name);
            len+= LCD_PrintString(len,pInfo->y,":");
            LCD_PrintSignedVal(len,pInfo->y,cnt++);
            g_LCDCanUse = 1;
        }
    }
}

此时运行程序,我们发现只有task3在运行,task1和task2都无法运行,这是因为,task3首先运行后,任务调度过程中,切换另一个任务的时刻大概率发生在耗时较长的打印语句中,被切换后,全局变量仍然为0,因此其他任务不能够运行。这说明这种方法是不可靠的。更可靠的方法见下一节。

任务删除

void vTaskDelete( TaskHandle_t xTaskToDelete );
参数描述
pvTaskCode 任务句柄,使用xTaskCreate创建任务时可以得到一个句柄。 也可传入NULL,这表示删除自己。

怎么删除任务?

  • 自杀:vTaskDelete(NULL)
  • 被杀:别的任务执行vTaskDelete(pvTaskCode),pvTaskCode是自己的句柄
  • 杀人:执行vTaskDelete(pvTaskCode),pvTaskCode是别的任务的句柄

优先级与阻塞(任务四状态)

创建三个任务,task1:音乐播放任务 task2:LED闪烁任务 task3:红外接收任务  创建任务时设置同一优先级,运行后发现task1音乐播放比较卡顿,这是因为本质上这三个任务是交替执行的。

解决方法:提高task1的优先级,并进行vTaskDelay延时。  

void vTaskDelay( const TickType_t xTicksToDelay )
BaseType_t vTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
                            const TickType_t xTimeIncrement ); //起始时间和增量时间
vTaskDelay(2);  // 等待2个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待20ms

如果想要暂停音乐播放,可以使用vTaskSuspend将任务挂起,与此相对应的是vTaskResume。

void vTaskSuspend( TaskHandle_t xTaskToSuspend );

在阻塞状态的任务,它可以等待两种类型的事件:

  • 时间相关的事件
    • 可以等待一段时间:我等2分钟(vTaskDelay)
    • 也可以一直等待,直到某个绝对时间:我等到下午3点(vTaskDelayUntil)
  • 同步事件:这事件由别的任务,或者是中断程序产生
    • 例子1:任务A等待任务B给它发送数据
    • 例子2:任务A等待用户按下按键
    • 同步事件的来源有很多(这些概念在后面再了解):
      • 队列(queue)
      • 二进制信号量(binary semaphores)
      • 计数信号量(counting semaphores)
      • 互斥量(mutexes)
      • 递归互斥量、递归锁(recursive mutexes)
      • 事件组(event groups)
      • 任务通知(task notifications)

空闲任务

空闲任务在启动调度器时自动创建,拥有最低优先级(0),其作用是:在所有其他任务都处于阻塞状态或者就绪状态时运行,并可以释放被删除的任务的内存。空闲任务运行时调用一个钩子函数,vApplicationIdleHook()钩子函数中可以执行一些低优先级的、后台的、需要连续执行的函数,用户可自定义。

posted @ 2024-11-05 10:10  要是天天吃鱼就好了  阅读(1090)  评论(0)    收藏  举报