什么是FreeRTOS?

FreeRTOS(Free Real-Time Operating System)是一种开源的实时操作系统(RTOS),专为嵌入式系统设计。它提供了基本的任务管理功能,使得在有限资源的硬件平台上实现多任务并行处理变得可行。

FreeRTOS中什么是任务?任务的基本状态有哪些?

在FreeRTOS中,任务(Task) 是操作系统调度的基本单位。任务通常是一个独立的执行单元,由操作系统的调度器管理,并且可以并发执行。每个任务拥有自己的执行上下文,包括堆栈、优先级等,任务通过调度器进行切换和执行。任务可以根据需要进行创建、删除、暂停、恢复等操作。

基本状态:

  1. 就绪: 就绪状态表示任务已经准备好,可以运行。任务处于就绪状态时,等待操作系统的调度器将其选中执行。
  2. 运行: 任务处于运行状态时,它正在被处理器执行。每个时刻只能有一个任务处于运行状态,调度器会根据任务的优先级来选择运行任务。
  3. 阻塞: 任务进入阻塞状态时,表示它正在等待某个事件或者条件的发生(如等待信号量、等待消息、等待时间等)。在阻塞状态下,任务不会被调度器选中执行,直到等待的条件满足时,任务才会被唤醒并进入就绪状态。
  4. 挂起: 任务可以被显式地挂起,处于挂起状态的任务不会被调度器选中运行。挂起状态的任务不参与任何调度,直到被显式地恢复。挂起通常是为了暂停任务的执行,或者某些特殊情况下需要停止任务。
  5. 删除: 删除状态是任务生命周期的结束。当任务被删除时,它会释放所有的资源,包括堆栈、任务控制块等,并且从任务列表中移除,不能再被调度执行。

如何创建一个FreeRTOS任务?

/* START_TASK 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define START_TASK_PRIO     1           /* 任务优先级 */
#define START_STK_SIZE      128         /* 任务堆栈大小 */
TaskHandle_t StartTask_Handler;         /* 任务句柄 */
void start_task(void *pvParameters);    /* 任务函数 */

void lvgl_demo(void)
{
    xTaskCreate((TaskFunction_t )start_task,            /* 任务函数 */
                (const char*    )"start_task",          /* 任务名称 */
                (uint16_t       )START_STK_SIZE,        /* 任务堆栈大小 */
                (void*          )NULL,                  /* 传递给任务函数的参数 */
                (UBaseType_t    )START_TASK_PRIO,       /* 任务优先级 */
                (TaskHandle_t*  )&StartTask_Handler);   /* 任务句柄 */

    vTaskStartScheduler();                              /* 开启任务调度 */
}

FreeRTOS任务的优先级是如何定义的?

在FreeRTOS中,任务优先级是用来决定哪个任务先执行的机制。每个任务都有一个与之关联的优先级,优先级数值较大的任务会优先执行。FreeRTOS支持优先级调度,任务调度器会根据任务的优先级来选择下一个要运行的任务。在创建任务的时候可以指定任务优先级。也可以在任务调度器开启之后使用vTaskPrioritySet()改变某个任务的优先级。

最大优先级在FreeRTOSConfig.h中有定义:

#define configMAX_PRIORITIES                            32                      /* 定义最大优先级数, 最大优先级=configMAX_PRIORITIES-1, 无默认需定义 */
#define configUSE_PORT_OPTIMISED_TASK_SELECTION         1                       /* 1: 使用硬件计算下一个要运行的任务, 0: 使用软件算法计算下一个要运行的任务, 默认: 0 */

当开启第二个宏定义的时候,使用硬件计算下一个要运行的任务,能够加速计算,需要硬件支持计算前导零指令CLZ(Count Leading Zeros)。由于stm32是32位的,因此使用硬件最多只能支持32个优先级。

什么是任务堆栈?堆栈大小应该如何设置?

在FreeRTOS中,任务堆栈是为每个任务分配的内存区域,用于存储任务执行期间的局部变量、函数调用的返回地址、保存的寄存器值等。每个任务都有独立的堆栈空间,以确保任务之间的执行互不干扰。

堆栈大小的确定需要考虑任务函数使用的局部变量数量、函数调用深度等因素。可以开启栈溢出钩子函数来处理栈溢出的情况。

FreeRTOS中的时间片轮转调度是什么?在什么情况下会发生?

在FreeRTOS中,时间片轮转调度是一种调度策略,旨在公平地分配CPU时间给同一优先级的任务。在这种策略下,所有相同优先级的任务轮流执行,每个任务在其分配的时间片内运行,时间片用完后,调度器会自动切换到下一个任务。

时间片轮转调度需要参与调度的任务是同一优先级,否则将触发抢占式调度,且要满足以下条件之一:

  1. 时间片到期
  2. 任务主动让出CPU

FreeRTOS的延时函数有哪些?如何使用?

  1. vTaskDelay()vTaskDelay() 是最常用的延时函数,用于让任务进入阻塞状态一段时间。该函数会将任务挂起一段指定的tick时间。vTaskDelay() 会依据系统时钟频率(tick rate)来计算延时的时间。
  2. vTaskDelayUntil()vTaskDelayUntil() 函数类似于 vTaskDelay(),但它提供了一种相对固定的周期延时机制,用于创建周期性任务。这个函数确保任务以固定的时间间隔运行,基于任务启动时的时间戳。
#include "FreeRTOS.h"
#include "task.h"

void vTaskFunction(void *pvParameters)
{
    for (;;)
    {
        // 执行任务代码

        // 延时 100 毫秒(假设 FreeRTOS tick rate 为 1000Hz)
        vTaskDelay(pdMS_TO_TICKS(100));

        // 执行其他代码
    }
}
#include "FreeRTOS.h"
#include "task.h"

void vTaskFunction(void *pvParameters)
{
    TickType_t xLastWakeTime;
    const TickType_t xFrequency = pdMS_TO_TICKS(100);  // 设置周期为100ms

    // 初始化上次唤醒时间为当前系统时间
    xLastWakeTime = xTaskGetTickCount();

    for (;;)
    {
        // 执行任务代码

        // 延时,确保任务按固定的周期执行
        vTaskDelayUntil(&xLastWakeTime, xFrequency);

        // 任务执行完成后,继续执行其他代码
    }
}

FreeRTOS中如何处理中断?

在 FreeRTOS 中,ISR 必须遵循一定的规则,因为 ISR 是在中断上下文中执行的。ISR 不能直接调用可能会导致阻塞的函数,例如 vTaskDelay()xQueueSend() 等。因为中断服务程序是异步的,它需要快速执行并且不能阻塞。

为了在中断中与任务进行交互,FreeRTOS 提供了一些专门的函数,可以在 ISR 中调用。常见的 ISR-safe API 包括:

  1. xQueueSendFromISR() / xQueueReceiveFromISR(): 这些函数用于从 ISR 向队列中发送数据或者从队列中接收数据。它们是专门为 ISR 上下文设计的,不会阻塞。
  2. xSemaphoreGiveFromISR() / xSemaphoreTakeFromISR(): 这些函数用于在 ISR 中释放信号量或获取信号量。
  3. portYIELD_FROM_ISR(): 这个宏用于请求调度器切换上下文,通常用于中断处理完成后,指示系统可能需要切换到其他任务执行。

同时,要注意到FreeRTOS会对一部分优先级较低的中断进行屏蔽,因此在设置中断的时候要注意把优先级设置的高于这个值。下面就是把优先级不高于5的中断全部屏蔽了(数值越低优先级越高,和FreeRTOS的优先级相反)。

#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY         15                  /* 中断最低优先级 */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY    5                   /* FreeRTOS可管理的最高中断优先级 */

FreeRTOS通过设置M3内核的BASEPRI寄存器达到屏蔽部分中断的效果,而taskENTER_CRITICAL()是通过把BASEPRI设置为configMAX_SYSCALL_INTERRUPT_PRIORITY屏蔽了所有的可屏蔽中断。(其实configMAX_SYSCALL_INTERRUPT_PRIORITY就是0)。

#define configMAX_SYSCALL_INTERRUPT_PRIORITY            ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

在这里插入图片描述

FreeRTOS中什么是Idle任务?它的作用是什么?

在FreeRTOS中,空闲任务(Idle Task) 是系统中优先级最低的任务,其主要作用是在没有其他任务需要执行时占用CPU资源,防止处理器进入空转状态。

空闲任务的主要作用包括:

  1. 资源回收与清理: 当系统中删除任务或任务结束时,空闲任务负责清理和释放相关资源,如任务控制块(TCB)和堆栈空间。 这有助于防止内存泄漏,确保系统资源得到有效管理。
  2. 执行低优先级的后台任务: 开发者可以在空闲任务中添加钩子函数,以执行一些低优先级的后台任务。
  3. 防止处理器空转

FreeRTOS的systick的作用是什么?如何影响任务调度?

FreeRTOSConfig.h中有下面几个宏定义,通过设置这些宏定义可以改变systick中断的频率:

#define configCPU_CLOCK_HZ                              SystemCoreClock         /* 定义CPU主频, 单位: Hz, 无默认需定义 */
#define configSYSTICK_CLOCK_HZ                          (configCPU_CLOCK_HZ / 8)/* 定义SysTick时钟频率,当SysTick时钟频率与内核时钟频率不同时才可以定义, 单位: Hz, 默认: 不定义 */
#define configTICK_RATE_HZ                              1000                    /* 定义系统时钟节拍频率, 单位: Hz, 无默认需定义 */

在systick的中断服务函数中调用xPortSysTickHandler()来触发FreeRTOS的任务调度。

void SysTick_Handler(void)
{
    HAL_IncTick();
    if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) /* OS开始跑了,才执行正常的调度处理 */
    {
        xPortSysTickHandler();
    }
}

因此systick的作用就是定时调用xPortSysTickHandler()函数,使任务调度器正常工作。中断频率越高,调度越频繁,同时调度所消耗的资源也越多;中断频率越低,调度越不频繁,有的对实时性要求高的任务可能不会得到及时的调度。因此需要综合考虑。一般情况下设置1kHz就行。

什么是任务通知(Task Notifications)?如何使用?

在 FreeRTOS 中,每一个任务都有两个用于任务通知功能的数组,分别为任务通知数组和任务通知状态数组。其中任务通知数组中的每一个元素都是一个 32 位无符号类型的通知值;而任务通知状态数组中的元素则表示与之对应的任务通知的状态。

任务通知数组中的 32 位无符号通知值,用于任务到任务或中断到任务发送通知的“媒介”。当通知值为 0 时,表示没有任务通知;当通知值不为 0 时,表示有任务通知,并且通知值就是通知的内容。

任务通知状态数组中的元素,用于标记任务通知数组中通知的状态,任务通知有三种状态,分别为未等待通知状态、等待通知状态和等待接收通知状态。其中未等待通知状态为任务通知的复位状态;当任务在没有通知的时候接收通知时,在任务阻塞等待任务通知的这段时间内,任务所等待的任务通知就处于等待通知状态;当有其他任务向任务发送通知,但任务还未接收这一通知的这段期间内,任务通知就处于等待接收通知状态。

任务通知功能所使用到的任务通知数组和任务通知状态数组为任务控制块中的成员变量,因此任务通知的传输是直接传出到任务中的,不同通过任务的通讯对象(队列、事件标志组和信号量就属于通讯对象)这个间接的方式。

下面的代码是用任务通知模拟计数型信号量,xTaskNotifyGive((TaskHandle_t)Task2Task_Handler)会将task2的任务通知数组加1,ulTaskNotifyTake((BaseType_t )pdFALSE, (TickType_t )portMAX_DELAY)会将task2的任务通知数组减1。如果pdFALSE改成pdTRUE的话就是读取一次直接清零了。

void task1(void *pvParameters)
{
    uint8_t key = 0;
    
    while (1)
    {
        key = key_scan(0);
        
        if (Task2Task_Handler != NULL)
        {
            switch (key)
            {
                case KEY0_PRES:                                         /* 发送任务通知 */
                {
                    xTaskNotifyGive((TaskHandle_t)Task2Task_Handler);   /* 接收任务通知的任务句柄 */
                    break;
                }
                default:
                {
                    break;
                }
            }
        }
        
        vTaskDelay(10);
    }
}

void task2(void *pvParameters)
{
    uint32_t notify_val = 0;
    uint32_t task2_num  = 0;
    
    while (1)
    {
        notify_val = ulTaskNotifyTake((BaseType_t   )pdFALSE,           /* 通知值在函数退出时递减,类似计数型信号量 */
                                      (TickType_t   )portMAX_DELAY);    /* 等待时间 */
        
        lcd_show_xnum(166, 111, notify_val - 1, 2, 16, 0, BLUE);        /* 在LCD上显示任务通知值 */
        lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]);     /* LCD区域刷新 */
        
        vTaskDelay(1000);
    }
}

下面的代码是用任务通知模拟消息邮箱,xTaskNotify给task2的任务通知数组赋值,xTaskNotifyWait取值并在取值完成的时候将任务通知数组清零。如果把xTaskNotifyeSetValueWithOverwrite换成eSetBits,则可以起到设置位的效果,二进制表示的key为1的位对应的任务通知数组的位会被置1。

void task1(void *pvParameters)
{
    uint8_t key = 0;
    
    while (1)
    {
        key = key_scan(0);
        
        if ((Task2Task_Handler != NULL) && (key != 0))
        {
            xTaskNotify((TaskHandle_t   )Task2Task_Handler,         /* 接收任务通知的任务句柄 */
                        (uint32_t       )key,                       /* 要更新的bit位 */
                        (eNotifyAction  )eSetValueWithOverwrite);   /* 更新方式为覆写 */
        }
        
        vTaskDelay(10);
    }
}

void task2(void *pvParameters)
{
    uint32_t    notify_val  = 0;
    uint32_t    task2_num   = 0;
    
    while (1)
    {
        xTaskNotifyWait((uint32_t     )0x00000000,      /* 进入函数时,不清除任务通知值 */
                        (uint32_t     )0xFFFFFFFF,      /* 函数退出时,清零任务通知值 */
                        (uint32_t*    )&notify_val,     /* 接收到的通知值 */
                        (TickType_t   )portMAX_DELAY);  /* 等待时间 */
        
        switch (notify_val)
        {
            case KEY0_PRES:                             /* LCD区域刷新 */
            {
                lcd_fill(6, 126, 233, 313, lcd_discolor[++task2_num % 11]);
                break;
            }
            case KEY1_PRES:                             /* LED0闪烁 */
            {
                LED0_TOGGLE();
                break;
            }
            default:
            {
                break;
            }
        }
    }
}

FreeRTOS中的队列(Queue)是什么?它的应用场景有哪些?

队列是一种 FIFO(先进先出)结构,任务或中断通过队列发送或接收数据。发送数据的任务会将数据放入队列的尾部,而接收数据的任务则从队列的头部取出数据。

常用的操作有:

  1. xQueueSend(),xQueueSendFromISR()
  2. xQueueReceive(),xQueueReceiveFromISR()
  3. xQueuePeek():查看队列头部的项,但不将其移除。
  4. xQueueReset():重置队列,将所有项移除。
  5. uxQueueMessagesWaiting():查询队列中待接收的数据项数量。
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();           /* 进入临界区 */
    /* 创建队列 */
    xQueue = xQueueCreate(QUEUE_LENGTH, QUEUE_ITEM_SIZE);
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t )task1,
                (const char*    )"task1",
                (uint16_t       )TASK1_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK1_PRIO,
                (TaskHandle_t*  )&Task1Task_Handler);
    /* 创建任务2 */
    xTaskCreate((TaskFunction_t )task2,
                (const char*    )"task2",
                (uint16_t       )TASK2_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK2_PRIO,
                (TaskHandle_t*  )&Task2Task_Handler);
    vTaskDelete(StartTask_Handler); /* 删除开始任务 */
    taskEXIT_CRITICAL();            /* 退出临界区 */
}

void task1(void *pvParameters)
{
    uint8_t key = 0;
    
    while (1)
    {
        key = key_scan(0);
        
        if (key != 0)
        {
            xQueueSend(xQueue, &key, portMAX_DELAY);    /* 将键值作为消息发送到队列中 */
        }
        
        vTaskDelay(10);
    }
}

void task2(void *pvParameters)
{
    uint8_t     queue_recv  = 0;
    uint32_t    task2_num   = 0;
    
    while (1)
    {
        xQueueReceive(xQueue, &queue_recv, portMAX_DELAY);
        
        switch (queue_recv)
        {
            case KEY0_PRES:                             /* LCD区域刷新 */
            {
                lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]);
                break;
            }
            case KEY1_PRES:                             /* LED0闪烁 */
            {
                LED0_TOGGLE();
                break;
            }
            default:
            {
                break;
            }
        }
    }
}

如何通过FreeRTOS信号量(Semaphore)实现任务同步?

FreeRTOS的信号量有二值型信号量、计数型信号量。二值信号量的使用范例如下:

void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();           /* 进入临界区 */
    /* 创建二值信号量 */
    BinarySemaphore = xSemaphoreCreateBinary();
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t )task1,
                (const char*    )"task1",
                (uint16_t       )TASK1_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK1_PRIO,
                (TaskHandle_t*  )&Task1Task_Handler);
    /* 创建任务2 */
    xTaskCreate((TaskFunction_t )task2,
                (const char*    )"task2",
                (uint16_t       )TASK2_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK2_PRIO,
                (TaskHandle_t*  )&Task2Task_Handler);
    vTaskDelete(StartTask_Handler); /* 删除开始任务 */
    taskEXIT_CRITICAL();            /* 退出临界区 */
}
void task1(void *pvParameters)
{
    uint8_t key = 0;
    
    while (1)
    {
        key = key_scan(0);
        
        switch (key)
        {
            case KEY0_PRES:
            {
                xSemaphoreGive(BinarySemaphore);                    /* 释放二值信号量 */
                break;
            }
            default:
            {
                break;
            }
        }
        
        vTaskDelay(10);
    }
}
void task2(void *pvParameters)
{
    uint32_t task2_num = 0;
    
    while (1)
    {
        xSemaphoreTake(BinarySemaphore, portMAX_DELAY);             /* 获取二值信号量 */
        
        lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]); /* LCD区域刷新 */
    }
}

信号量的常用操作函数有:

  1. xSemaphoreCreateBinary():创建二值信号量。
  2. xSemaphoreCreateCounting(最大计数值, 初始计数值):创建计数型信号量。
  3. xSemaphoreGive():V操作。
  4. xSemaphoreTake(xSemaphore, 等待时间):P操作。
  5. uxSemaphoreGetCount():获取信号量的值。

获取信号量的常用格式:

if (xSemaphoreTake(xSemaphore, 等待时间) == pdTRUE) {
    // 获取成功,执行临界区代码
} else {
    // 获取失败,执行相应处理
}

portMAX_DELAY是一直等待的意思,如果使用了一直等待,则不需要处理else的情况。

什么是互斥量(Mutex)?FreeRTOS中的Mutex与Semaphore有何不同?

互斥量(Mutex) 是一个用于保护共享资源的锁,用于确保在同一时刻只有一个任务或线程能够访问共享资源,从而防止竞争条件和数据不一致的发生。它常用于多线程或多任务环境中,确保对共享资源的互斥访问。

Mutex的一个关键特性是它的所有权。一个线程/任务获取了Mutex后,只有该线程/任务能够释放它。如果其他线程尝试释放它,系统会报错。而信号量的值不依赖于持有者,任何线程或任务都可以释放信号量(即使它没有获取信号量)。信号量不管理任务的所有权。

为了防止优先级反转(Priority Inversion),大多数RTOS(包括FreeRTOS)对互斥量提供优先级继承机制。如果一个高优先级任务正在等待一个低优先级任务释放互斥量,低优先级任务会临时继承高优先级任务的优先级,以防止长时间的优先级反转。而Semaphore(包括二值型信号量)通常不涉及优先级继承,因此它会导致优先级反转的问题。

  1. xSemaphoreCreateMutex():创建互斥量。
  2. xSemaphoreTake(MutexSemaphore):获取互斥量。
  3. xSemaphoreGive(MutexSemaphore):释放互斥量。

什么是任务优先级反转?如何在FreeRTOS中避免?

优先级反转(Priority Inversion)是一种在多任务操作系统中,低优先级的任务持有锁或资源,导致高优先级任务被阻塞的现象,进而使得高优先级任务的执行优先级被“反转”。这种情况可能导致系统的实时性性能下降,甚至影响任务的实时响应性。

优先级反转的发生过程:
假设有三个任务:低优先级任务(L),中等优先级任务(M),和高优先级任务(H)。其中,高优先级任务H在等待低优先级任务L释放某个资源或锁时,发生优先级反转的情况如下:

  1. 任务L(低优先级任务)获得了共享资源(如互斥锁)并进入执行状态。
  2. 任务H(高优先级任务)此时需要访问相同的共享资源,因此它被阻塞,等待任务L释放资源。
  3. 任务M(中等优先级任务)开始执行,任务M的优先级介于L和H之间,任务M现在在执行,但它不需要访问共享资源。
  4. 由于任务M的优先级比任务H高,任务H不能继续执行,直到任务L释放锁。

发生这种情况就是优先级反转。使用互斥量可以改变这种情况,因为它提供了优先级继承机制,会临时把低优先级的任务提高到高优先级,进而导致L任务能够抢占M任务。

什么是事件组(Event Groups)?如何使用事件组?

事件组(Event Groups)是实时操作系统(RTOS)中的一种同步机制,用于在多个任务之间进行通信和协作。它是一种轻量级的机制,通常由操作系统提供,可以让多个任务在多个事件发生时进行同步。

虽然说事件标志组使用了 32 位无符号的数据类型变量来存储事件标志,但这并不意味着,一个EventBits_t 数据类型的变量能够存储 32 个事件标志,FreeRTOS 将这个 EventBits_t 数据类型的变量拆分成两部分,其中低 24 位[23:0](configUSE_16_BIT_TICKS 配置位 1 时,是低 8 位[7:0])用于存储事件标志,而高 8 位[31:24](configUSE_16_BIT_TICKS 配置位 1 时,依然是高 8 位[15:8])用作存储事件标志组的一些控制信息,也就是说一个事件组最多可以存储 24 个事件标志。

事件标志组相关函数:
在这里插入图片描述

void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();           /* 进入临界区 */
    /* 创建事件标志组 */
    EventGroupHandler = xEventGroupCreate();
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t )task1,
                (const char*    )"task1",
                (uint16_t       )TASK1_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK1_PRIO,
                (TaskHandle_t*  )&Task1Task_Handler);
    /* 创建任务2 */
    xTaskCreate((TaskFunction_t )task2,
                (const char*    )"task2",
                (uint16_t       )TASK2_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK2_PRIO,
                (TaskHandle_t*  )&Task2Task_Handler);
    /* 创建任务3 */
    xTaskCreate((TaskFunction_t )task3,
                (const char*    )"task3",
                (uint16_t       )TASK3_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK3_PRIO,
                (TaskHandle_t*  )&Task3Task_Handler);
    vTaskDelete(StartTask_Handler); /* 删除开始任务 */
    taskEXIT_CRITICAL();            /* 退出临界区 */
}

void task1(void *pvParameters)
{
    uint8_t key = 0;
    
    while (1)
    {
        key = key_scan(0);
        
        switch (key)
        {
            case KEY0_PRES:
            {
                xEventGroupSetBits((EventGroupHandle_t  )EventGroupHandler, /* 待操作的事件标志组句柄 */
                                   (EventBits_t         )EVENTBIT_0);       /* 待设置的bit位 */
                break;
            }
            case KEY1_PRES:
            {
                xEventGroupSetBits((EventGroupHandle_t  )EventGroupHandler, /* 待操作的事件标志组句柄 */
                                   (EventBits_t         )EVENTBIT_1);       /* 待设置的bit位 */
                break;
            }
            default:
            {
                break;
            }
        }
        
        vTaskDelay(10);
    }
}

void task2(void *pvParameters)
{
    uint32_t task2_num = 0;
    
    while (1)
    {
        xEventGroupWaitBits((EventGroupHandle_t )EventGroupHandler, /* 等待的事件标志组句柄 */
                            (EventBits_t        )EVENTBIT_ALL,      /* 等待的事件 */
                            (BaseType_t         )pdTRUE,            /* 函数退出时清零等待的事件 */
                            (BaseType_t         )pdTRUE,            /* 等待等待的事件中的所有事件 */
                            (TickType_t         )portMAX_DELAY);    /* 等待时间 */
        
        lcd_fill(6, 131, 233, 313, lcd_discolor[++task2_num % 11]); /* LCD区域刷新 */
        
        vTaskDelay(10);
    }
}

void task3(void *pvParameters)
{
    EventBits_t event_val = 0;
    
    while (1)
    {
        event_val = xEventGroupGetBits((EventGroupHandle_t)EventGroupHandler);  /* 获取的事件标志组句柄 */
        
        lcd_show_xnum(182, 110, event_val, 1, 16, 0, BLUE);                     /* 在LCD上显示事件值 */
        
        vTaskDelay(10);
    }
}

如何在FreeRTOS中使用定时器(Timer)?与任务延时的区别是什么?

在 FreeRTOS 中,定时器(Timer)是一种强大的软件机制,允许任务在指定的时间间隔内执行回调函数,而不需要创建额外的任务。相比任务延时(vTaskDelay),定时器更适合处理周期性或延迟执行的任务,并且不会阻塞任务。

FreeRTOS 提供的软件定时器还能够根据需要设置成单次定时器和周期定时器。当单次定时器定时超时后,不会自动启动下一个周期的定时,而周期定时器在定时超时后,会自动地启动下一个周期的定时。

需要注意的是,在定时器的回调函数中,不能存在可能导致任务阻塞的函数。

定时器相关函数:
在这里插入图片描述

void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();           /* 进入临界区 */
    /* 定时器1创建为周期定时器 */
    Timer1Timer_Handler = xTimerCreate((const char*  )"Timer1",                 /* 定时器名 */
                                      (TickType_t   )1000,                      /* 定时器超时时间 */
                                      (UBaseType_t  )pdTRUE,                    /* 周期定时器 */
                                      (void*        )1,                         /* 定时器ID */
                                      (TimerCallbackFunction_t)Timer1Callback); /* 定时器回调函数 */
    /* 定时器2创建为单次定时器 */
    Timer2Timer_Handler = xTimerCreate((const char*  )"Timer2",                 /* 定时器名 */
                                     (TickType_t    )1000,                      /* 定时器超时时间 */
                                     (UBaseType_t   )pdFALSE,                   /* 单次定时器 */
                                     (void*         )2,                         /* 定时器ID */
                                     (TimerCallbackFunction_t)Timer2Callback);  /* 定时器回调函数 */
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t )task1,
                (const char*    )"task1",
                (uint16_t       )TASK1_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK1_PRIO,
                (TaskHandle_t*  )&Task1Task_Handler);
    vTaskDelete(StartTask_Handler); /* 删除开始任务 */
    taskEXIT_CRITICAL();            /* 退出临界区 */
}

void task1(void *pvParameters)
{
    uint8_t key = 0;
    
    while (1)
    {
        if ((Timer1Timer_Handler != NULL) && (Timer2Timer_Handler != NULL))
        {
            key = key_scan(0);
            
            switch (key)
            {
                case KEY0_PRES:
                {
                    xTimerStart((TimerHandle_t  )Timer1Timer_Handler,   /* 待启动的定时器句柄 */
                                (TickType_t     )portMAX_DELAY);        /* 等待系统启动定时器的最大时间 */
                    xTimerStart((TimerHandle_t  )Timer2Timer_Handler,   /* 待启动的定时器句柄 */
                                (TickType_t     )portMAX_DELAY);        /* 等待系统启动定时器的最大时间 */
                    break;
                }
                case KEY1_PRES:
                {
                    xTimerStop((TimerHandle_t  )Timer1Timer_Handler,    /* 待停止的定时器句柄 */
                                (TickType_t     )portMAX_DELAY);        /* 等待系统停止定时器的最大时间 */
                    xTimerStop((TimerHandle_t  )Timer2Timer_Handler,    /* 待停止的定时器句柄 */
                                (TickType_t     )portMAX_DELAY);        /* 等待系统停止定时器的最大时间 */
                    break;
                }
                default:
                {
                    break;
                }
            }
        }
        
        vTaskDelay(10);
    }
}

void Timer1Callback(TimerHandle_t xTimer)
{
    static uint32_t timer1_num = 0;
    
    lcd_fill(6, 131, 114, 313, lcd_discolor[++timer1_num % 11]);    /* LCD区域刷新 */
    lcd_show_xnum(79, 111, timer1_num, 3, 16, 0x80, BLUE);          /* 显示定时器1超时次数 */
}

void Timer2Callback(TimerHandle_t xTimer)
{
    static uint32_t timer2_num = 0;
    
    lcd_fill(126, 131, 233, 313, lcd_discolor[++timer2_num % 11]);  /* LCD区域刷新 */
    lcd_show_xnum(199, 111, timer2_num, 3, 16, 0x80, BLUE);         /* 显示定时器2超时次数 */
}

与任务延时的区别:

  1. 在 FreeRTOS 中,所有的定时器都由一个专门的定时器服务任务(Timer Service Task)处理,不管你创建多少个定时器,所有定时器的回调函数都会在同一个定时器服务任务中执行。而如果采用任务延时的方法需要创建多个任务。
  2. 软件定时器的回调函数共用定时器的栈,而采用任务延时需要独立的栈。
  3. 软件定时器支持单次和循环定时,而任务延时的方法只能循环。

FreeRTOSConfig.h中,有下面的宏定义,可以启用或禁用软件定时器、更改栈的大小等。

configTIMER_QUEUE_LENGTH 设置了定时器服务任务可以接收的定时器事件队列的最大长度。每当定时器超时或定时器回调函数需要被调用时,相关的事件会被放入队列中。定时器服务任务会从队列中取出这些事件并执行相应的回调函数。

/* 软件定时器相关定义 */
#define configUSE_TIMERS                                1                               /* 1: 使能软件定时器, 默认: 0 */
#define configTIMER_TASK_PRIORITY                       ( configMAX_PRIORITIES - 1 )    /* 定义软件定时器任务的优先级, 无默认configUSE_TIMERS为1时需定义 */
#define configTIMER_QUEUE_LENGTH                        5                               /* 定义软件定时器命令队列的长度, 无默认configUSE_TIMERS为1时需定义 */
#define configTIMER_TASK_STACK_DEPTH                    ( configMINIMAL_STACK_SIZE * 2) /* 定义软件定时器任务的栈空间大小, 无默认configUSE_TIMERS为1时需定义 */

如何调试和监控FreeRTOS中的任务状态和堆栈使用情况?

任务状态监控

FreeRTOS 提供了 eTaskGetState() 函数,可以用来获取任务的状态。这些状态值包括:

  1. eRunning:任务正在运行。
  2. 任务正在运行:任务就绪,等待 CPU 时间片。
  3. eBlocked:任务被阻塞,等待某些事件或资源。
  4. eSuspended:任务被挂起,不能运行。
  5. eDeleted:任务已删除。
TaskHandle_t xTaskHandle;
eTaskState taskState;

taskState = eTaskGetState(xTaskHandle);
if (taskState == eRunning) {
    // 任务正在运行
} else if (taskState == eBlocked) {
    // 任务被阻塞
}

堆栈使用情况

FreeRTOS 提供了 uxTaskGetStackHighWaterMark() 函数来获取任务堆栈的使用情况。该函数返回任务堆栈的“高水位”,即堆栈空闲的最小空间。若该值为零,则表示堆栈已满,可能存在溢出的风险。

UBaseType_t uxHighWaterMark;

uxHighWaterMark = uxTaskGetStackHighWaterMark(xTaskHandle);
printf("Task %s high water mark: %u\n", pcTaskGetName(xTaskHandle), uxHighWaterMark);

除此之外,还有堆栈溢出钩子函数可以监控和处理堆栈溢出的情况。

FreeRTOS是如何实现任务调度的?描述其调度器的工作原理。

任务调度模式

FreeRTOS 支持两种主要的调度模式:

  • 抢占式调度(Preemptive Scheduling):在抢占式模式下,任务可以被更高优先级的任务抢占。即使当前任务没有让出 CPU,调度器也会在系统节拍到来时检查是否有更高优先级的任务需要执行。
  • 协作式调度(Cooperative Scheduling):在协作式模式下,任务主动让出 CPU(如调用 taskYIELD()vTaskDelay())时,才会发生任务切换。任务之间不会相互抢占,调度器不会主动中断当前任务,只有当任务自愿放弃 CPU 时,调度器才会进行任务切换。

FreeRTOS 默认为 抢占式调度,如果想使用协作式调度,可以通过修改 configUSE_PREEMPTION 配置项来禁用抢占式调度。

调度器的核心实现

调度器的核心是通过 Tick Timer(系统节拍)任务就绪队列 来完成的:

  • Tick Timer:FreeRTOS 使用一个系统定时器(通常是硬件定时器)来产生系统节拍(Tick)。每当系统节拍到来时,调度器会检查所有任务的状态,执行任务切换。
  • 就绪队列:每个任务会被放入一个按优先级排序的就绪队列中。调度器会从队列中选择优先级最高的任务执行。
任务调度器的工作流程
  1. 初始化阶段:FreeRTOS 初始化时,调度器开始运行并启用系统节拍(Tick)。此时,FreeRTOS 会根据任务优先级和任务状态决定第一个要执行的任务。
  2. 任务就绪:当任务调用 xTaskCreate() 被创建时,它会进入就绪状态并被放入就绪队列中。调度器会选择优先级最高的任务执行。
  3. 任务切换:当系统节拍到来时,或者当前任务主动让出 CPU,调度器会检查是否有更高优先级的任务可以执行。如果有,调度器会发生任务切换,当前运行任务会被挂起,新的高优先级任务会被调度执行。
  4. 任务挂起与阻塞:当任务等待某个事件或资源时,它会进入阻塞状态,直到条件满足任务才能再次进入就绪队列。
  5. 任务终止与删除:当任务完成时,它会被删除,调度器会清理相关资源,并选择其他任务执行。

FreeRTOS中如何使用静态内存分配?它有哪些优点和限制?

在 FreeRTOS 中,静态内存分配是一种将任务、队列、信号量、定时器等对象的内存分配在编译时进行,而不是在运行时动态分配的方式。通过静态内存分配,开发者可以更精确地控制内存的使用,避免运行时的内存碎片问题,提高系统的稳定性,尤其适用于内存资源有限的嵌入式系统。

使用静态内存分配需要手动为任务、队列、信号量、定时器等对象分配内存。具体方法是使用 FreeRTOS 提供的带有静态内存分配支持的 API,例如:

  • xTaskCreateStatic()
#include "FreeRTOS.h"
#include "task.h"

// 定义一个任务堆栈和任务控制块
StackType_t xTaskStack[configMINIMAL_STACK_SIZE];
StaticTask_t xTaskControlBlock;

void vTaskFunction(void *pvParameters)
{
    // 任务代码
}

void main(void)
{
    // 使用静态分配创建任务
    xTaskCreateStatic(vTaskFunction, 
                      "Task1", 
                      configMINIMAL_STACK_SIZE, 
                      NULL, 
                      tskIDLE_PRIORITY, 
                      xTaskStack, 
                      &xTaskControlBlock);
    
    // 启动调度器
    vTaskStartScheduler();
}
  • xQueueCreateStatic()
QueueHandle_t xQueue;
StaticQueue_t xQueueBuffer;
uint8_t ucQueueStorage[QUEUE_LENGTH * sizeof(QueueItemType)];

void main(void)
{
    // 创建一个静态队列
    xQueue = xQueueCreateStatic(QUEUE_LENGTH, sizeof(QueueItemType), ucQueueStorage, &xQueueBuffer);
}

FreeRTOS中如何高效管理多任务之间的共享资源?

  1. 信号量
  2. 互斥量
  3. 队列
  4. 事件组
  5. 任务通知

如何在FreeRTOS中实现一个生产者-消费者模型?

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include <stdio.h>

#define QUEUE_LENGTH 10           // 队列长度
#define ITEM_SIZE sizeof(int)     // 队列元素大小

// 定义队列句柄
QueueHandle_t xQueue;

// 生产者任务函数
void vProducerTask(void *pvParameters)
{
    int itemToProduce = 0;   // 生产的项目数据

    while (1)
    {
        // 生成数据
        itemToProduce++;

        // 尝试将数据发送到队列
        if (xQueueSend(xQueue, &itemToProduce, portMAX_DELAY) != pdTRUE)
        {
            // 如果队列满了,打印日志
            printf("队列已满\n");
        }

        // 模拟生产过程的延时,1秒生产一次数据
        vTaskDelay(pdMS_TO_TICKS(1000)); 
    }
}

// 消费者任务函数
void vConsumerTask(void *pvParameters)
{
    int itemReceived;   // 接收到的数据

    while (1)
    {
        // 从队列中取出数据
        if (xQueueReceive(xQueue, &itemReceived, portMAX_DELAY) == pdTRUE)
        {
            // 处理接收到的数据
            printf("消费了数据: %d\n", itemReceived);
        }

        // 模拟消费过程的延时,1.5秒消费一次数据
        vTaskDelay(pdMS_TO_TICKS(1500));
    }
}

// 主函数
void main(void)
{
    // 创建队列,队列长度为 QUEUE_LENGTH,元素大小为 ITEM_SIZE
    xQueue = xQueueCreate(QUEUE_LENGTH, ITEM_SIZE);

    if (xQueue != NULL)  // 检查队列是否创建成功
    {
        // 创建生产者任务
        xTaskCreate(vProducerTask, "Producer", 1000, NULL, 1, NULL);

        // 创建消费者任务
        xTaskCreate(vConsumerTask, "Consumer", 1000, NULL, 2, NULL);

        // 启动调度器
        vTaskStartScheduler();
    }
    else
    {
        // 创建队列失败
        printf("队列创建失败\n");
    }
}

FreeRTOS支持的时间精度有多高?如何优化Tick Rate来满足系统需求?

精度高低看Tick Rate的高低,频率过高会占用较多资源,但响应实时性较好;频率较低占用资源少,但实时性较差。一般应用1000Hz即可。

如果需要将FreeRTOS移植到一个新的微控制器平台,你需要做哪些工作?

  1. 选择合适的端口文件(port):这些文件实现了 FreeRTOS 操作系统与具体硬件平台的交互。每个硬件平台都有对应的端口文件,这些文件通常位于 FreeRTOS/Source/portable 目录中。
  2. 设置 FreeRTOS 配置:包括时钟、Tick Rate、任务优先级等。
  3. 移植中断管理:实现定时器中断、任务上下文切换和中断优先级管理。
  4. 实现上下文切换:保存和恢复任务上下文,管理任务堆栈。
  5. 移植系统 Tick 和时间管理:确保定时器产生系统 Tick 来驱动任务调度。
  6. 移植外设驱动和内存管理:根据硬件平台编写外设驱动程序并实现内存分配函数。
  7. 测试和调试:确保系统稳定性,调试任务调度和硬件接口。

描述一个你使用FreeRTOS的项目,遇到的最大挑战是什么?你是如何解决的?

描述FreeRTOS与裸机开发的主要区别,选择FreeRTOS的理由是什么?

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐