FreeRTOS保姆级教程:STM32任务与协程详解及实战代码示例

目录

一、任务和协程概述:

(1)任务的特点:

(2)协程的特点:

(3)注意事项: 

 二、任务:

(1)任务状态:

a.任务状态说明:

b.有效任务状态转换:

c.作用说明: 

 三、任务优先级:

(1)任务优先级说明:

优先级范围:

优先级值:

调度器行为:

相同优先级的任务:

时间片轮询调度:

优先级设置:

优先级注意事项:

 (2)API函数:

特点:

在FreeRTOS中的API函数:

 (3)作用说明:

 四、任务调度:

(1)默认RTOS调度策略(单核):

(2)避免任务饥饿:

(3)配置RTOS调度策略:

(4)FreeRTOS AMP调度策略:

(5)FreeRTOS SMP调度策略:

(6)配置SMPRTOS调度策略:

(7)作用总结:

 五、任务实现:

在FreeRTOS中实现和使用任务:

(1)任务函数的结构:

(2)事件驱动型任务:

(3)任务创建和删除:

(4)任务创建宏:

(5)函数原型:

(6)总结:

六、协程:

(1)协程状态:

(2)注意事项: 

 七、协程实现:

(1)协程的结构:

(2)协程的创建:

(3)注意事项:

(4)协程的调度:

(5)协程的状态:

(6)协程的限制:

(7)总结:

 八、协程优先级:

(1)协程优先级的范围和定义:

(2)协程优先级的特性:

(3)协程与任务的优先级比较:

(4)协程优先级的应用:

(5)协程优先级设置的注意事项:

(6)总结:

九、 协程调度:

(1)调度协程:

(2)混合任务和协程:

(3)实现示例:

(4)总结:

十、 局限和限制:

(1)共享堆栈:

(2)阻塞调用的位置:

(3)switch语句中的阻塞调用:

(4)协程的限制和复杂性:

(5)总结:

十一、协程示例:

(1)创建一个简单的协程来闪烁 LED:

(2)调度协程:

(3)创建协程并启动 RTOS 调度器:

 (4)示例扩展:使用索引参数:

 十二、空闲任务:

(1)空闲任务的作用:

(2)空闲任务的特点:

(3)空闲任务钩子:

(4)空闲任务钩子的使用场景:

(5)示例代码:

(6)注意事项:

(7)总结:

 十三、线程本地存储指针:

(1)线程本地存储指针:

(2)线程本地整数:

(3)线程本地结构体:

(4)注意事项:

(5)总结

十四、以STM32F103C8T6为例的实际任务和协程操作:

(1)通过任务调度,闪烁一个LED:

 (2)通过协程调度,闪烁一个LED:

十五、FreeRTOS移植所需文件:


一、任务和协程概述:

在FreeRTOS操作系统中,任务(Task)和协程(Coroutine)是两种不同的并发执行单位,它们各自有不同的特点和适用场景。

(1)任务的特点:

  • 独立性:每个任务在FreeRTOS中是独立的,拥有自己的执行逻辑和上下文,包括寄存器状态和堆栈。
  • 优先级:任务支持优先级调度,高优先级的任务可以抢占低优先级任务的执行。
  • 周期性和响应性:任务可以周期性执行或响应外部事件,如中断、信号量释放等。
  • 上下文切换:任务切换由RTOS调度器管理,确保任务上下文在切换时得以保存和恢复。
  • 抢占式机制:任务支持完全抢占式机制,按优先顺序排列,确保高优先级任务得到及时响应。
  • 堆栈使用:每个任务保留自己的堆栈,可能会增加RAM的使用率。
  • 重入问题:如果使用抢占式机制,需要考虑重入问题,确保任务的互斥和数据一致性。
  • (2)协程的特点:

  • 堆栈使用:所有协程共用一个堆栈,相比任务,协程可以大大减少RAM的使用。
  • 调度和优先级:协程间使用协同调度,可以在抢占式任务的应用程序中使用,但通常不包含在现代RTOS的调度中。
  • 宏实现:协程通常通过宏实现,与任务的调度方式不同。
  • 使用限制:协程的构造受到严格限制,以减少RAM使用,但在现代RTOS中使用较少。
  • (3)注意事项: 

            在选择使用任务还是协程时,需要考虑应用程序的具体需求。如果应用程序对RAM资源有严格的限制,且任务之间的同步和通信需求较低,协程可能是一个合适的选择。然而,如果应用程序需要复杂的同步机制、优先级调度和抢占式行为,任务可能是更好的选择。

            在实际应用中,任务是FreeRTOS中更常用的并发执行单位,而协程由于其限制和RTOS的发展,使用较少。在设计RTOS应用程序时,通常推荐使用任务作为主要的并发执行单元,并利用FreeRTOS提供的任务调度、同步和通信机制来实现应用程序逻辑。如果需要类似协程的行为,可以通过手动管理任务的执行和挂起状态来模拟,但这通常比使用真正的协程要复杂得多。

     二、任务:

    (1)任务状态:

    在FreeRTOS操作系统中,任务可以处于以下几种状态,并且可以在这些状态之间转换。

    a.任务状态说明:

  • 运行态(Running):

  • 描述:当任务实际在CPU上执行时,它处于运行态。在单核处理器上,任何时刻只能有一个任务处于运行态。
  • 转换:任务可以通过调用taskYIELD()vTaskDelay()等函数,或者由于更高优先级任务的启动而从运行态转换到就绪态或阻塞态。
  • 就绪态(Ready):

  • 描述:就绪态的任务已经准备好执行,但因为其他同等或更高优先级的任务正在运行,所以暂时没有被调度执行。
  • 转换:任务可以从阻塞态或挂起态通过超时或事件触发而进入就绪态。当运行态的任务完成或被挂起时,就绪态的任务可能会被调度器选中进入运行态。
  • 阻塞态(Blocked):

  • 描述:任务因为等待某些事件(如等待队列、信号量、事件组、任务通知或延时等)而暂时无法执行,处于阻塞态。
  • 转换:任务在等待的事件得到满足或超时后,会从阻塞态转换回就绪态。例如,等待的信号量被释放或调用vTaskDelay()后延时结束。
  • 挂起态(Suspended):

  • 描述:挂起态的任务不会参与调度,即使它们具备执行条件。这种状态通常用于暂停任务的执行,而不释放其持有的资源。
  • 转换:任务可以通过调用vTaskSuspend()进入挂起态,通过调用xTaskResume()vTaskResumeFromISR()从挂起态转换回就绪态。
  • b.有效任务状态转换:

  • 运行态 → 就绪态:通过任务主动放弃CPU或被更高优先级任务抢占。
  • 运行态/就绪态 → 阻塞态:通过任务调用延时函数、等待信号量或事件等。
  • 阻塞态 → 就绪态:等待的事件得到满足或延时结束。
  • 运行态/就绪态/阻塞态 → 挂起态:通过任务调用vTaskSuspend()
  • 挂起态 → 就绪态:通过任务调用xTaskResume()vTaskResumeFromISR()
  • c.作用说明: 

    这些状态和转换是FreeRTOS调度器管理任务执行的关键机制,确保任务可以根据优先级和事件触发进行合理的调度和执行。

     三、任务优先级:

    在FreeRTOS中,任务是执行的最小单位,每个任务都被分配了一个优先级,这个优先级决定了任务的调度顺序。

    (1)任务优先级说明:

  • 优先级范围:

  • 任务的优先级范围是从0到configMAX_PRIORITIES - 1,其中configMAX_PRIORITIES是在FreeRTOSConfig.h中定义的。
  • 如果配置了使用“前导零计数”优化任务选择的移植机制,并且configUSE_PORT_OPTIMISED_TASK_SELECTIONFreeRTOSConfig.h中设置为1,则configMAX_PRIORITIES通常不超过32。
  • 优先级值:

  • 优先级值越小,表示任务的优先级越低。
  • 空闲任务(Idle Task)的优先级为0,这是最低的优先级。
  • 调度器行为:

  • FreeRTOS调度器确保在任何时候,就绪或运行状态下的任务总是比同样处于就绪状态下的更低优先级任务先获得CPU时间。
  • 这意味着,当前正在运行的任务总是当前可运行的最高优先级任务。
  • 相同优先级的任务:

  • 可以有任意数量的任务具有相同的优先级。
  • 如果configUSE_TIME_SLICING未定义或者设置为0,相同优先级的任务将按照它们成为就绪状态的顺序执行,直到它们被更高优先级的任务抢占。
  • 如果configUSE_TIME_SLICING设置为1,相同优先级的任务将通过时间片轮询调度方案共享CPU时间。这意味着每个任务将获得一个时间片,在时间片用完后,如果任务仍然处于就绪状态,它将被放回就绪队列的末尾,让其他同优先级的任务获得执行机会。
  • 时间片轮询调度:

  • 时间片轮询调度确保所有相同优先级的任务都能得到公平的CPU时间分配。
  • 时间片的长度可以通过configTICK_RATE_HZ(定义了tick的频率)和configMAX_PRIORITIES来调整。
  • 优先级设置:

  • 任务的优先级在创建时通过xTaskCreate()xTaskCreateStatic()函数的uxPriority参数设置。
  • 任务优先级可以在运行时通过API函数如vTaskPrioritySet()进行动态调整。
  • 优先级注意事项:

  • 设定优先级时,需要考虑系统的实时性要求和任务间的相互关系,避免优先级反转问题。
  • 应尽量使用抢占式调度,以确保高优先级任务能够及时响应。
  •  (2)API函数:

    API函数(应用程序编程接口函数)是指一组预定义的函数,允许程序员与软件库、操作系统或其他服务进行交互。API函数提供了一种标准化的方式,使得开发者能够使用特定的功能,而无需了解其内部实现细节。

    特点:

  • 封装性:API函数封装了复杂的操作,开发者只需调用这些函数即可完成特定任务。
  • 标准化:API函数通常遵循一定的命名规则和参数格式,便于学习和使用。
  • 跨平台性:许多API函数设计为跨平台使用,使得开发者可以在不同的操作系统或环境中使用相同的代码。
  • 文档化:API函数通常伴随详细的文档,描述其功能、参数、返回值及使用示例。
  • 在FreeRTOS中的API函数:

    在FreeRTOS中,API函数用于管理任务、队列、信号量等。

    以下是一些常用的FreeRTOS API函数示例:

  • 任务管理:

  • xTaskCreate(): 创建一个新的任务。
  • vTaskDelete(): 删除指定的任务。
  • vTaskDelay(): 使当前任务延迟一段时间。
  • 任务优先级:

  • vTaskPrioritySet(): 设置指定任务的优先级。
  • uxTaskPriorityGet(): 获取指定任务的优先级。
  • 队列管理:

  • xQueueCreate(): 创建一个新的队列。
  • xQueueSend(): 向队列发送数据。
  • xQueueReceive(): 从队列接收数据。
  • 信号量管理:

  • xSemaphoreCreateBinary(): 创建一个二进制信号量。
  • xSemaphoreTake(): 获取信号量。
  • xSemaphoreGive(): 释放信号量。
  •  (3)作用说明:

    通过合理配置和使用任务优先级,可以确保FreeRTOS系统能够高效、公平地调度多个任务,满足实时性要求。

     四、任务调度:

    FreeRTOS是一个可预占的实时操作系统内核,它提供了多种调度策略,可以应用于单核、非对称多核(AMP)和对称多核(SMP)处理器架构。

    (1)默认RTOS调度策略(单核):

    FreeRTOS默认使用固定优先级的抢占式调度策略,并且对同等优先级的任务执行时间切片轮询调度:

  • 固定优先级:任务的优先级不会永久改变,但可能会因为优先级继承而暂时提高。
  • 抢占式:调度器总是选择最高优先级的任务来执行,即使当前任务的时间片还没有用完。
  • 轮询调度:相同优先级的任务会轮流执行。
  • 时间切片:调度器在每个时钟节拍(tick)中断上切换相同优先级的任务。
  • (2)避免任务饥饿:

    高优先级任务可能会永久性地剥夺低优先级任务的执行时间,这就是为什么通常最好创建事件驱动型任务的原因。事件驱动型任务在等待事件时会进入阻塞状态,从而允许低优先级任务运行。

    (3)配置RTOS调度策略:

    FreeRTOSConfig.h中,可以通过以下宏来配置调度策略:

  • configUSE_PREEMPTION:如果设置为0,则关闭抢占,只有在任务主动放弃CPU时才会进行调度。
  • configUSE_TIME_SLICING:如果设置为0,则关闭时间切片,相同优先级的任务不会轮流执行。
  • (4)FreeRTOS AMP调度策略:

    在非对称多处理(AMP)配置中,每个处理器核心运行自己的FreeRTOS实例,核心之间可以通过共享内存和核间通信原语(如流缓冲区或消息缓冲区)进行通信。

    (5)FreeRTOS SMP调度策略:

    在对称多处理(SMP)配置中,一个FreeRTOS实例可以跨多个处理器核心调度任务。每个核心必须具有相同的处理器架构并共用相同的内存空间。SMP调度策略允许同时在多个核心上运行多个任务。

    (6)配置SMPRTOS调度策略:

    在SMP配置中,可以使用以下配置选项:

  • configRUN_MULTIPLE_PRIORITIES:如果设置为0,则只有当多个任务具有相同优先级时,调度器才会同时运行多个任务。
  • configUSE_CORE_AFFINITY:如果设置为1,则可以使用vTaskCoreAffinitySet() API函数定义任务可以在哪些核心上运行,从而避免同时执行假设了自身执行顺序的两个任务。
  • (7)作用总结:

    FreeRTOS提供了灵活的调度策略,可以适应不同的处理器架构和应用需求。通过合理配置和使用这些调度策略,可以确保系统资源得到有效利用,同时满足实时性要求。在设计RTOS应用程序时,需要考虑任务的优先级、调度策略以及多核处理器的特定特性,以实现高效和可靠的系统。

     五、任务实现:

    在FreeRTOS中,任务是执行的最小单位,每个任务都是一个无限循环的函数,它定义了任务的行为和功能。

    在FreeRTOS中实现和使用任务:

    (1)任务函数的结构:

  • 任务函数通常具有以下结构:
  • void vATaskFunction( void *pvParameters )
    {
        for( ;; )
        {
            // 任务应用代码在这里。
        }
    
        // 任务不应该尝试从其实现函数返回或以其他方式退出。
        // 在较新的FreeRTOS版本中,如果尝试这样做,将调用configASSERT()(如果定义)。
        // 如果需要任务退出,则应让任务调用vTaskDelete(NULL)以确保其退出是干净的。
        vTaskDelete( NULL );
    }

    任务函数必须永不返回,因此通常实现为一个无限循环。任务函数的参数pvParameters可以用来传递任何类型的信息给任务。

    (2)事件驱动型任务:

    为了避免低优先级任务饥饿,最好创建事件驱动型任务。任务在等待事件时应该进入阻塞状态,而不是忙等待。这可以通过使用FreeRTOS提供的通信和同步原语来实现,例如:

    void vATaskFunction( void *pvParameters )
    {
        for( ;; )
        {
            if( WaitForEvent( EventObject, TimeOut ) == pdPASS )
            {
                // 处理事件。
            }
            else
            {
                // 超时后的错误处理。
            }
        }
    
        // 任务退出。
        vTaskDelete( NULL );
    }

    在这里,WaitForEvent()是一个伪代码,代表等待事件的调用,可以是xQueueReceive()ulTaskNotifyTake()xEventGroupWaitBits()或其他FreeRTOS通信和同步原语。

    (3)任务创建和删除:

  • 创建任务:使用xTaskCreate()xTaskCreateStatic()函数创建新任务。
  • 删除任务:当任务不再需要时,可以调用vTaskDelete()来删除任务。
  • 在FreeRTOS中,任务的创建和删除是通过特定的API函数进行的。

  • 创建任务:

  • 可以使用xTaskCreate()xTaskCreateStatic()函数来创建任务。

  • 使用xTaskCreate()创建任务: xTaskCreate()函数动态地创建任务,由FreeRTOS的内存管理器分配任务控制块和堆栈。

    TaskHandle_t xTaskCreate(
        TaskFunction_t pxTaskCode,   // 任务函数
        const char * const pcName,   // 任务名称
        uint32_t ulStackDepth,      // 堆栈深度,单位为字(取决于处理器架构)
        void * const pvParameters,  // 传递给任务的参数
        UBaseType_t uxPriority,     // 任务优先级
        TaskHandle_t * const pxCreatedTask // 任务句柄的指针,可选参数
    );

    示例代码:

    // 任务函数
    void vTaskFunction(void *pvParameters)
    {
        // 任务代码
    }
    
    // 创建任务
    TaskHandle_t xHandle = NULL;
    xTaskCreate(vTaskFunction, "Task1", 500, NULL, 1, &xHandle);
  • 使用xTaskCreateStatic()创建任务:

  • xTaskCreateStatic()函数静态地创建任务,需要预先提供任务控制块和堆栈。

    BaseType_t xTaskCreateStatic(
        TaskFunction_t pxTaskCode,   // 任务函数
        const char * const pcName,   // 任务名称
        uint32_t ulStackDepth,      // 堆栈深度,单位为字(取决于处理器架构)
        void * const pvParameters,  // 传递给任务的参数
        UBaseType_t uxPriority,     // 任务优先级
        StackType_t *pxStackBuffer, // 提供的堆栈空间
        TaskHandle_t *pxTaskBuffer  // 提供的任务控制块
    );

    示例代码:

    // 任务函数
    void vTaskFunction(void *pvParameters)
    {
        // 任务代码
    }
    
    // 静态任务创建所需的堆栈和任务控制块
    StackType_t xStack[STACK_SIZE];
    TaskHandle_t xTaskBuffer;
    
    // 创建任务
    BaseType_t xStatus = xTaskCreateStatic(vTaskFunction, "Task1", STACK_SIZE, NULL, 1, xStack, &xTaskBuffer);
  • 任务删除后,其占用的资源(如堆栈和任务控制块)不会自动释放。如果使用xTaskCreate()创建任务,FreeRTOS会在内部释放任务的堆栈和任务控制块。如果使用xTaskCreateStatic(),则需要手动管理这些资源。
  • 删除任务前,确保任务已经完成所有必要的清理工作,以避免资源泄漏或其他问题。
  • 在任务删除后,不应再使用与该任务相关联的任何句柄或资源。
  • (4)任务创建宏:

    FreeRTOS提供了portTASK_FUNCTIONportTASK_FUNCTION_PROTO宏,这些宏允许将编译器特定的语法添加到函数定义和原型中。这些宏的使用取决于特定的硬件和编译器。通常,如果端口相关文档中没有特别说明,就不需要使用这些宏。

    (5)函数原型:

    任务函数的原型可以简单地写为:

    void vATaskFunction( void *pvParameters );

    或者使用宏:

    portTASK_FUNCTION_PROTO( vATaskFunction, pvParameters );

    函数定义也可以使用宏:

    portTASK_FUNCTION( vATaskFunction, pvParameters )
    {
        for( ;; )
        {
            // 任务应用代码在这里。
        }
    }

    (6)总结:

    在FreeRTOS中实现任务时,需要遵循特定的结构和模式,以确保任务能够正确地与RTOS内核交互。任务通常是事件驱动的,并且在不再需要时应该能够自我删除。通过使用FreeRTOS提供的API和宏,可以简化任务的创建和管理。

    六、协程:

    (1)协程状态:

    在FreeRTOS中,协程是一种轻量级的任务,它们在内存受限的极小处理器上非常有用,但对于现代32位微控制器来说,它们通常不是首选。

    协程可以存在于以下几种状态中:

  • 运行(Running):当协程实际执行时,它处于运行状态,此时正在使用处理器。

  • 就绪(Ready):就绪的协程是那些能够执行(未阻塞)但目前未执行的协程。协程可能处于就绪状态的原因包括:另一个具有相同或更高优先级的协程已处于运行状态,或者任务处于运行状态(仅当应用程序同时使用任务和协程时才会出现这种情况)。

  • 阻塞(Blocked):如果协程当前正在等待时间事件或外部事件,则该协程处于阻塞状态。例如,如果协程调用crDELAY(),它将阻塞,直到延迟期结束。阻塞的协程不可用于调度。

  •         与任务不同,当前没有等同于任务挂起状态的协程。有效的协程状态转换包括从就绪状态到运行状态,以及从运行状态到阻塞状态。协程在阻塞时会自动让出CPU资源,这有助于提高系统的并发性和响应速度,避免了CPU资源的浪费。

            在FreeRTOS中,协程的调度是通过重复调用vCoRoutineSchedule()来实现的,这通常在空闲任务钩子中完成。如果应用程序只使用协程,那么在空闲任务中调用vCoRoutineSchedule()是必要的,因为空闲任务在调度程序启动时会自动创建。

    (2)注意事项: 

            随着FreeRTOS的发展,协程的支持可能已经在后续版本中被移除或不再推荐使用。在早期版本的FreeRTOS中,通过设置configUSE_CO_ROUTINES宏来决定是否包含协程的相关代码。如果该宏被设置为1,则FreeRTOS会包含协程的相关代码,并允许在系统中使用协程;如果设置为0,则协程相关的代码不会被包含,从而节省内存空间。在最新版本的FreeRTOS中,由于协程在现代实时操作系统中并不常用,且其实现可能不如任务调度那样高效和灵活,因此协程的支持可能已经被完全移除。因此,在编写基于FreeRTOS的应用程序时,推荐使用任务作为主要的并发执行单元。

     七、协程实现:

    在FreeRTOS中,协程是一种比任务更轻量级的并发执行单位,它们通常用于内存资源非常有限的系统中。协程的实现和使用有其特定的模式和要求。

    (1)协程的结构:

    协程的实现通常遵循以下结构:

    void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
    {
        crSTART( xHandle );
    
        for( ;; )
        {
            // 协程应用代码在这里。
        }
    
        crEND();
    }

    在这个结构中:

  • crSTART() 宏在协程开始执行时调用,它负责设置协程的初始状态。
  • crEND() 宏在协程结束时调用,它标志着协程的结束。
  • (2)协程的创建:

    协程是通过调用xCoRoutineCreate()函数创建的。这个函数接受协程函数、协程的优先级和协程索引作为参数,并返回一个协程句柄,该句柄在后续的操作中用于控制协程。

    (3)注意事项:

  • 每个协程函数都必须以crSTART()开始,并以crEND()结束。这两个宏是FreeRTOS协程实现的一部分,用于管理协程的执行和退出。
  • 协程函数不应返回任何值,并且通常实现为一个无限循环。这是因为协程的设计目的是持续执行,直到显式地被停止或阻塞。
  • 可以通过单个协程函数创建多个协程实例,每个实例通过uxIndex参数区分。这个索引参数在协程函数内部可以用来存储与特定协程实例相关的数据或状态。
  • (4)协程的调度:

    协程的调度是通过调用vCoRoutineSchedule()函数手动进行的。这个函数通常在空闲任务中调用,或者在任何其他需要显式调度协程的地方调用。协程调度是协作式的,意味着协程需要显式地让出控制权,通常是通过调用如crDELAY()这样的宏来实现。

    (5)协程的状态:

    协程主要有以下几种状态:

  • 运行态(Running):协程正在执行。
  • 就绪态(Ready):协程准备好执行,但当前没有运行,可能是因为其他协程正在运行。
  • 阻塞态(Blocked):协程正在等待某个事件或资源,因此暂时不执行。
  • (6)协程的限制:

  • 协程共享相同的堆栈空间,这意味着它们不能有太多的局部变量或大的堆栈需求。
  • 协程不能像任务那样被抢占,它们的调度完全依赖于协程本身的协作。
  • 协程的阻塞操作必须在协程函数内部进行,不能在协程调用的其他函数中进行。
  • (7)总结:

    FreeRTOS协程是一种轻量级的并发执行机制,适用于资源受限的环境。它们通过特定的宏和函数进行创建和管理,并且需要程序员显式地进行调度和状态管理。协程的使用可以减少系统的内存占用,但同时也带来了一些限制和复杂性。在现代的FreeRTOS版本中,协程的使用已经不如任务普遍,因此在选择使用协程之前,需要仔细考虑应用程序的具体需求和约束。

     八、协程优先级:

    在FreeRTOS中,协程的优先级管理是一个关键的概念,它决定了协程之间的调度顺序。

    (1)协程优先级的范围和定义:

  • 每个协程都被分配了一个从0到configMAX_CO_ROUTINE_PRIORITIES - 1的优先级,其中configMAX_CO_ROUTINE_PRIORITIES是在FreeRTOSConfig.h中定义的。
  • 优先级的范围可以根据应用程序的需求进行设置,但通常数字较低表示协程的优先级较高。
  • (2)协程优先级的特性:

  • 相对性:协程的优先级是相对的,仅在协程之间比较有效。这意味着协程的优先级不会影响任务的调度,也不会被任务的优先级影响。
  • 优先级继承:如果协程等待某个同步机制(如信号量),它可能会继承持有该同步机制的任务的优先级。这是为了防止优先级反转问题。
  • 优先级天花板:在某些情况下,协程的优先级可能会被提升到一个预设的“天花板”优先级,以确保能够获得必要的资源。
  • (3)协程与任务的优先级比较:

  • 当在同一应用程序中混用任务和协程时,任务的优先级总是高于协程的优先级。
  • 这意味着如果一个高优先级的任务就绪,它将抢占所有协程的执行,无论协程的优先级如何。
  • (4)协程优先级的应用:

  • 在设计系统时,应该根据协程的职责和对响应时间的要求来分配优先级。
  • 通常,对实时性要求较高的协程应该赋予较高的优先级。
  • 在协程之间,优先级较高的协程会优先获得调度,但它们必须主动让出CPU(例如通过调用crDELAY())来允许优先级较低的协程运行。
  • (5)协程优先级设置的注意事项:

  • 协程的优先级设置应该谨慎进行,以避免优先级反转和饥饿问题。
  • 在可能的情况下,应该使用时间片轮转来公平地分配CPU时间给具有相同优先级的协程。
  • 协程的优先级设置不应该影响系统的实时性要求和任务的调度。
  • (6)总结:

    协程优先级是FreeRTOS中协程调度的一个重要方面。正确地设置和管理协程优先级对于确保系统的有效运行至关重要。在设计系统时,开发者需要考虑协程之间的相对优先级以及协程和任务之间的优先级关系,以确保系统的实时性和效率。由于FreeRTOS的版本更新,协程的使用已经不如以前普遍,因此在考虑使用协程时,应该评估是否有更合适的任务或线程机制可以满足系统的需求。

    九、 协程调度:

    在FreeRTOS中,协程的调度是通过协作式调度机制实现的,这意味着协程需要显式地让出CPU以便其他协程运行。

    (1)调度协程:

  • 调度函数:vCoRoutineSchedule()是FreeRTOS提供的用于调度协程的函数。它应该在系统的某个固定点被周期性调用,以确保所有就绪状态的协程都有机会被执行。
  • 调度位置:调用vCoRoutineSchedule()的最佳位置是在空闲任务钩子(vApplicationIdleHook())中。即使应用程序仅使用协程,空闲任务也会在调度器启动后自动创建,因此在这个钩子中调用vCoRoutineSchedule()可以保证协程得到调度。
  • 调度机制:协程调度是协作式的,协程必须在适当的时候调用crDELAY()或其他阻塞调用,以允许其他协程运行。这种调度方式减少了上下文切换的开销,但需要程序员更仔细地管理协程的执行。
  • (2)混合任务和协程:

  • 空闲任务调度:在混合任务和协程的应用程序中,协程通常从空闲任务中调度。这意味着只有在没有更高优先级的任务需要执行时,协程才会运行。
  • 优先级考虑:任务的优先级总是高于协程的优先级。因此,如果有高优先级的任务就绪,它将抢占协程的执行。
  • 系统资源利用:在没有任务需要执行时,空闲任务会运行,这允许系统在这段时间内执行协程。这样可以更有效地利用系统资源,特别是在任务之间存在空闲时间的情况下。
  • (3)实现示例:

    // 空闲任务钩子函数
    void vApplicationIdleHook( void )
    {
        // 调用协程调度器
        vCoRoutineSchedule();
    }
    
    // 创建协程的示例
    void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
    {
        crSTART( xHandle );
    
        for( ;; )
        {
            // 协程应用代码
            // ...
    
            // 让出CPU,允许其他协程运行
            crDELAY( xHandle, 100 );
        }
    
        crEND();
    }
    
    // 主函数中创建协程
    int main( void )
    {
        // 创建协程
        xCoRoutineCreate( vACoRoutineFunction, 0, 0 );
    
        // 启动任务调度器
        vTaskStartScheduler();
    
        // 如果调度器启动成功,以下代码不会执行
        for( ;; );
    }

    (4)总结:

    在FreeRTOS中,协程的调度需要程序员的显式管理,通常通过在空闲任务钩子中调用vCoRoutineSchedule()来实现。在混合任务和协程的应用程序中,协程通常在系统空闲时运行,这要求开发者在设计系统时仔细考虑任务和协程之间的优先级和资源分配。通过合理地调度协程,可以提高系统的整体效率和响应性,尤其是在资源受限的嵌入式系统中。

    十、 局限和限制:

    协程在FreeRTOS中提供了一种轻量级的并发执行方式,但它们也有一些局限性和使用上的限制。以下是协程在使用时需要注意的一些关键点:

    (1)共享堆栈:

  • 堆栈维持问题:协程在阻塞时不会像任务那样保存其堆栈,这意味着在堆栈上分配的局部变量可能会丢失其值。
  • 静态变量:为了在阻塞调用中保持变量值,需要将这些变量声明为static。这样,即使协程阻塞,它们的值也会在协程恢复时保持不变。
  • 共享堆栈的问题和解决方案:

  • void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
    {
        static char c = 'a'; // 静态变量,用于在阻塞后保持其值
    
        crSTART( xHandle );
    
        for( ;; )
        {
            c = 'b'; // 设置变量值
            crDELAY( xHandle, 10 ); // 阻塞调用,协程让出控制权
            // 此处变量 c 仍然为 'b',因为它是静态的
        }
    
        crEND();
    }

    在这个例子中,变量c被声明为static,这样即使协程在crDELAY()调用中阻塞,它的值也会在协程恢复时保持不变。

    (2)阻塞调用的位置:

  • 直接调用阻塞函数:协程函数中可以直接调用导致阻塞的API函数,如crDELAY()
  • 间接调用限制:不能在协程调用的其他函数中进行阻塞调用。这意味着所有可能导致阻塞的操作都应该在协程函数的直接上下文中进行。
  • 阻塞调用的位置限制:

  • void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
    {
        crSTART( xHandle );
    
        for( ;; )
        {
            crDELAY( xHandle, 10 ); // 正确的使用:直接在协程函数中调用阻塞函数
    
            vACalledFunction(); // 错误的使用:不能在协程调用的函数中调用阻塞函数
        }
    
        crEND();
    }
    
    void vACalledFunction( void )
    {
        // 不能在此处调用阻塞函数,如 crDELAY()
    }

    在这个例子中,crDELAY()可以直接在协程函数中调用,但不能在vACalledFunction()中调用,因为这可能会导致协程的执行流程出现问题。

    (3)switch语句中的阻塞调用:

  • 限制使用:在FreeRTOS的默认协程实现中,不允许在switch语句中进行阻塞调用。这是因为switch语句中的代码路径可能不会立即返回到协程函数,从而破坏了协程的执行流程。
  • switch语句中不允许阻塞调用:

  • void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
    {
        crSTART( xHandle );
    
        for( ;; )
        {
            crDELAY( xHandle, 10 ); // 正确的使用:直接在协程函数中调用阻塞函数
    
            int aVariable = 1;
            switch( aVariable )
            {
                case 1:
                    // 不能在此处调用阻塞函数,如 crDELAY()
                    break;
                default:
                    // 也不行
                    break;
            }
        }
    
        crEND();
    }

    在这个例子中,switch语句内不能进行阻塞调用,因为这可能会破坏协程的调度机制。

    (4)协程的限制和复杂性:

  • 使用限制:协程的使用受到更多限制,例如不能在switch语句中进行阻塞调用,也不能在协程函数调用的其他函数中进行阻塞调用。
  • 复杂性:与任务相比,协程的使用更复杂,需要程序员更加注意协程的调度和执行流程。
  • (5)总结:

    尽管协程在内存使用上更为高效,但它们在FreeRTOS中使用时需要注意以下几点:

  • 协程的堆栈在阻塞时不会保持,需要使用static变量来维持状态。
  • 阻塞调用必须在协程函数的直接上下文中进行,不能在协程调用的其他函数中进行。
  • switch语句中不允许进行阻塞调用。
  • 协程的使用比任务更复杂,需要更多的注意和设计考虑。
  • 由于这些限制,协程通常只在特定的、对内存使用有严格要求的场景中使用。在大多数情况下,任务是更可取的选择,因为它们提供了更多的灵活性和更少的限制。在设计系统时,应该根据实际需求和资源限制来选择使用任务还是协程。

    十一、协程示例:

    (1)创建一个简单的协程来闪烁 LED:

    void vFlashCoRoutine( CoRoutineHandle_t xHandle,
                          UBaseType_t uxIndex )
    {
       //协例程必须从调用crSTART()开始。
        crSTART( xHandle );
    
        for( ;; )
        {
           //指定时间延迟。
            crDELAY( xHandle, 10 );
    
            //闪烁LED
            vParTestToggleLED( 0 );
        }
    
        //协例程必须以调用crEND()结束。
        crEND();
    }
    

    (2)调度协程:

    通过重复调用 vCoRoutineSchedule() 来调度协程。执行这一操作的最佳位置是 空闲任务内部,通过编写空闲任务钩子函数来完成。首先,请确保 configUSE_IDLE_HOOK 在 FreeRTOSConfig.h中设置为 1。然后编写空闲任务钩子函数,如下所示:

    void vApplicationIdleHook( void )
    {
        vCoRoutineSchedule( void );
    }
    

    如果空闲任务没有执行任何其他函数,那按以下方式在循环中调用 vCoRoutineSchedule() 效率会更高:

    void vApplicationIdleHook( void )
    {
        for( ;; )
        {
            vCoRoutineSchedule( void );
        }
    }
    

    (3)创建协程并启动 RTOS 调度器:

  • 协程可在 main() 中创建:
  • #include "task.h"
    #include "croutine.h"
    
    #define PRIORITY_0 0
    
    void main( void )
    {
        //在这种情况下,索引不被使用,并作为0传入。
        xCoRoutineCreate( vFlashCoRoutine, PRIORITY_0, 0 );
    
        //注意:任务也可以在这里创建!
    
        //启动RTOS调度器
        vTaskStartScheduler();
    }
    

    (4)示例扩展:使用索引参数:

    现在假设我们要从同一函数中创建 8 个这样的协程。每个协程将 以不同速度闪烁不同的 LED。索引参数可用于在协程函数中 区分协程。

    这一次,我们将创建 8 个协程,并向每个协程传递不同的索引。

    #include "task.h"
    #include "croutine.h"
    
    #define PRIORITY_0        0
    #define NUM_COROUTINES    8
    
    void main( void )
    {
        int i;
    
        for( i = 0; i < NUM_COROUTINES; i++ )
        {
            //这次i作为索引传入。
            xCoRoutineCreate( vFlashCoRoutine, PRIORITY_0, i );
        }
    
        //注意:任务也可以在这里创建!
    
        //启动RTOS调度器
        vTaskStartScheduler();
    }
    

     协程函数也被扩展,因此每个协程使用的 LED 和闪烁速度都不同。

    const int iFlashRates[ NUM_COROUTINES ] = { 10, 20, 30, 40, 50, 60, 70, 80 };
    const int iLEDToFlash[ NUM_COROUTINES ] = { 0, 1, 2, 3, 4, 5, 6, 7 }
    
    void vFlashCoRoutine( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
    {
        //协同例程必须从调用crSTART()开始。
        crSTART( xHandle );
    
        for( ;; )
        {
            //指定时间延迟。uxIndex用于索引到iFlashRates。
            //因为每个协同程序都是由不同的索引值,每个会延迟不同的时间。
            crDELAY( xHandle, iFlashRate[ uxIndex ] );
    
               //闪烁LED。uxIndex再次用作数组索引,
               //这次定位应该被切换的LED。
            vParTestToggleLED( iLEDToFlash[ uxIndex ] );
        }
    
             //协例程必须以调用crEND()结束。
        crEND();
    }
    

     十二、空闲任务:

    在FreeRTOS中,空闲任务(Idle Task)是系统启动时自动创建的特殊任务,它具有最低的优先级。

    (1)空闲任务的作用:

  • 内存回收:空闲任务负责释放已被删除任务的内存,确保系统资源得到合理利用。
  • CPU资源管理:当没有更高优先级的任务需要执行时,CPU时间由空闲任务占用。
  • 节能模式:空闲任务通常用于将微控制器CPU设置为低功耗模式,以节省能源。
  • (2)空闲任务的特点:

  • 最低优先级:空闲任务的优先级最低,确保只有在没有其他任务运行时,它才会执行。
  • 自动创建:RTOS启动时自动创建,无需开发者手动创建。
  • 空闲任务钩子:提供了一个名为vApplicationIdleHook()的钩子函数,允许开发者在空闲任务的每次循环中执行自定义代码。
  • (3)空闲任务钩子:

  • 定义钩子:在FreeRTOSConfig.h中将configUSE_IDLE_HOOK设置为1,以启用空闲任务钩子。
  • 实现钩子函数:定义vApplicationIdleHook(void)函数,实现需要在空闲任务中执行的代码。
  • (4)空闲任务钩子的使用场景:

  • 节能模式:在空闲钩子中实现代码,将CPU设置为低功耗模式。
  • 监控:执行系统监控任务,如温度传感器读数或系统状态检查。
  • 维护操作:执行一些定期的维护操作,如内存碎片整理或日志记录。
  • (5)示例代码:

    以下是如何实现和使用空闲任务钩子的示例:

    // 在 FreeRTOSConfig.h 中启用空闲钩子
    #define configUSE_IDLE_HOOK 1
    
    // 空闲任务钩子函数
    void vApplicationIdleHook( void )
    {
        // 将CPU设置为低功耗模式
        // 例如,使用特定的微控制器指令或库函数
        EnterLowPowerMode();
    }

    在这个示例中,EnterLowPowerMode()是一个假设的函数,用于将微控制器CPU设置为低功耗模式。实际的实现将取决于特定的硬件平台和其提供的低功耗管理功能。

    (6)注意事项:

  • 避免阻塞:在空闲任务钩子中避免调用任何可能导致任务阻塞的API函数,如vTaskDelay()
  • 共享优先级:应用程序任务可以与空闲任务共享优先级,但这通常不推荐,因为它可能会影响空闲任务的性能。
  • 合理使用:合理使用空闲任务钩子,避免在其中执行复杂或耗时的操作,以免影响系统的响应性。
  • (7)总结:

    FreeRTOS的空闲任务是系统不可或缺的一部分,它在没有其他任务运行时执行,并提供了一个钩子函数,允许开发者执行自定义的操作。通过合理利用空闲任务钩子,可以提高系统的效率和性能,特别是在需要节能的应用场景中。

     十三、线程本地存储指针:

    在FreeRTOS中,线程本地存储(Thread Local Storage,TLS)是一种机制,允许每个任务拥有自己的数据副本,这对于管理任务特定的数据非常有用。

    (1)线程本地存储指针:

    FreeRTOS为每个任务提供了一个线程本地存储指针数组,可以通过configNUM_THREAD_LOCAL_STORAGE_POINTERSFreeRTOSConfig.h中配置数组的大小。这些指针可以用于存储任务特定的数据。

  • 设置和获取线程本地存储指针:

  • vTaskSetThreadLocalStoragePointer():用于设置数组中特定索引的值。
  • pvTaskGetThreadLocalStoragePointer():用于获取数组中特定索引的值。
  • (2)线程本地整数:

    如果需要存储小于或等于void*大小的值,可以直接存储在线程本地存储指针数组中。例如,如果void*是32位的,可以直接存储32位的整数。

    示例代码:

    uint32_t ulVariable;
    
    // 将32位值存储在调用任务的线程本地存储数组的索引1中
    vTaskSetThreadLocalStoragePointer(NULL, 1, (void *)0x12345678);
    
    // 将32位变量ulVariable的值存储在调用任务的线程本地存储数组的索引0中
    ulVariable = ERROR_CODE;
    vTaskSetThreadLocalStoragePointer(NULL, 0, (void *)ulVariable);
    
    // 从调用任务的线程本地存储数组的索引5中读取值到ulVariable
    ulVariable = (uint32_t)pvTaskGetThreadLocalStoragePointer(NULL, 5);

    (3)线程本地结构体:

    线程本地存储指针数组也可以用来存储指向结构体的指针,这些结构体可以存储更复杂的数据。

    示例代码:

    typedef struct
    {
        uint32_t ulValue1;
        uint32_t ulValue2;
    } xExampleStruct;
    
    xExampleStruct *pxStruct;
    
    // 为任务创建一个结构体
    pxStruct = pvPortMalloc(sizeof(xExampleStruct));
    
    // 设置结构体成员
    pxStruct->ulValue1 = 0;
    pxStruct->ulValue2 = 1;
    
    // 将结构体的指针存储在调用任务的线程本地存储数组的索引0中
    vTaskSetThreadLocalStoragePointer(NULL, 0, (void *)pxStruct);
    
    // 从调用任务的线程本地存储数组的索引0中读取结构体的位置
    pxStruct = (xExampleStruct *)pvTaskGetThreadLocalStoragePointer(NULL, 0);

    (4)注意事项:

  • 当使用NULL作为任务句柄调用vTaskSetThreadLocalStoragePointer()pvTaskGetThreadLocalStoragePointer()时,这些函数将操作调用它们的任务的线程本地存储。
  • 确保分配给线程本地存储的结构体或数据在任务的生命周期内有效,避免悬挂指针。
  • 使用pvPortMalloc()为结构体分配内存时,确保内存在任务间是独立的,以防止数据冲突。
  • (5)总结:

    FreeRTOS的线程本地存储提供了一种灵活的机制,允许任务存储和管理自己的数据。通过合理使用线程本地存储指针,可以避免任务间的相互干扰,确保数据的隔离性和安全性。

    十四、以STM32F103C8T6为例的实际任务和协程操作:

    (1)通过任务调度,闪烁一个LED:

    #include "stm32f10x.h"                 
    #include "Delay.h"                    
    #include "FreeRTOS.h"                  // 包含FreeRTOS实时操作系统的头文件,用于多任务管理。
    #include "task.h"                      // 包含任务相关函数的头文件,用于任务创建和管理。
    
    TaskHandle_t myTaskHandler; // 定义一个任务句柄变量,用于跟踪任务。
    
    // 定义任务函数,用于控制LED的闪烁。
    void myTask1(void *arg)
    {
        while(1) // 无限循环,任务会一直运行。
        {
            GPIO_ResetBits(GPIOC,GPIO_Pin_13); // 将GPIOC的第13脚位设置为低,通常用于熄灭LED。
            vTaskDelay(500);                  // 任务延迟500个时钟节拍,大约半秒(具体时间取决于时钟配置)。
            GPIO_SetBits(GPIOC,GPIO_Pin_13);   // 将GPIOC的第13脚位设置为高,通常用于点亮LED。
            vTaskDelay(500);                  // 再次延迟500个时钟节拍。
        }
    }
    
    // 主函数
    int main(void)
    {
        // 启动GPIOC端口的时钟,以便能够使用该端口的引脚。
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
    
        // 定义GPIO初始化结构体变量。
        GPIO_InitTypeDef GPIO_InitStructure;
        // 设置GPIO模式为推挽输出。
        GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
        // 设置要初始化的GPIO引脚为第13脚。
        GPIO_InitStructure.GPIO_Pin=GPIO_Pin_13;      
        // 设置GPIO速度为50MHz。
        GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
        // 根据上面定义的参数初始化GPIOC的第13脚。
        GPIO_Init(GPIOC,&GPIO_InitStructure);
        
        // 创建名为"led1"的任务,栈大小为64字节,优先级为2,任务函数为myTask1,不传递参数给任务。
        xTaskCreate(myTask1, "led1", 64, NULL, 2, &myTaskHandler);
        // 启动任务调度器,这是FreeRTOS开始执行任务的地方。
        vTaskStartScheduler();  
        
        while(1) // 通常,主循环中不需要任何代码,因为任务调度器接管了CPU。
        {
        }
    }

    通过任务调度,闪烁一个LED

  • 初始化GPIO,创建一个任务来控制LED的闪烁,然后启动任务调度器。在任务调度器运行后,主循环通常保持空转,因为所有的工作都在任务中完成。
  • (2)通过协程调度,闪烁一个LED:

    #include "stm32f10x.h"                              
    #include "FreeRTOS.h"                   // 包含FreeRTOS实时操作系统的头文件,用于多任务管理。
    #include "task.h"                       // 包含任务相关函数的头文件,用于任务创建和管理。
    #include "croutine.h"                   // 包含协程相关函数的头文件,用于协程创建和管理。
    
    #define PRIORITY_0 0                    // 定义优先级0,通常用于最低优先级的任务或协程。
    
    // 协程函数原型
    void vFlashCoRoutine( CoRoutineHandle_t xHandle, UBaseType_t uxIndex );
    
    // 空闲任务钩子函数,用于调度协程
    void vApplicationIdleHook( void )
    {
        vCoRoutineSchedule();               // 调用协程调度函数,以确保协程得到执行。
    }
    
    // 协程函数,用于控制LED闪烁
    void vFlashCoRoutine( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
    {
        // 协程必须从调用crSTART()开始。
        crSTART( xHandle );
    
        for( ;; )                         // 无限循环,协程会一直运行,直到手动停止或复位。
        {
            // 点亮LED
            GPIO_SetBits(GPIOC, GPIO_Pin_13); // 将GPIOC的第13脚位设置为高,通常用于点亮LED。
            crDELAY( xHandle, 500 );        // 延迟500个时钟节拍,大约半秒(具体时间取决于时钟配置)。
    
            // 熄灭LED
            GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 将GPIOC的第13脚位设置为低,通常用于熄灭LED。
            crDELAY( xHandle, 500 );        // 再次延迟500个时钟节拍。
        }
    
        // 协程必须以调用crEND()结束。
        crEND();
    }
    
    int main(void)
    {
        // 启动GPIOC端口的时钟
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // 使能GPIOC端口的时钟。
    
        // 定义GPIO初始化结构体变量
        GPIO_InitTypeDef GPIO_InitStructure;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 设置为推挽输出模式。
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;       // 初始化第13脚。
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置GPIO速度为50MHz。
        GPIO_Init(GPIOC, &GPIO_InitStructure);          // 根据初始化结构体配置GPIOC的第13脚。
    
        // 启动协程
        xCoRoutineCreate( vFlashCoRoutine, PRIORITY_0, 0 ); // 创建协程,传递协程函数、优先级和索引。
    
        // 启动任务调度器
        vTaskStartScheduler();                           // 启动FreeRTOS的任务调度器。
    
        // 如果调度器启动成功,以下代码不会执行
        for( ;; );                                       // 无限循环,防止main函数返回。
    }

    在实际应用协程调度时,闪烁一个LED时,报错:

    .\Objects\Project.axf: Error: L6218E: Undefined symbol vCoRoutineAddToDelayedList (referred from main.o).
    .\Objects\Project.axf: Error: L6218E: Undefined symbol vCoRoutineSchedule (referred from main.o).
    .\Objects\Project.axf: Error: L6218E: Undefined symbol xCoRoutineCreate (referred from main.o).
    Not enough information to list image symbols.
    Not enough information to list load addresses in the image map.
    Finished: 2 information, 0 warning and 3 error messages.
    ".\Objects\Project.axf" - 3 Error(s), 0 Warning(s).
    Target not created.

    解决办法:

  • FreeRTOS 协程相关的源文件没有包含在项目中:确保项目包含了实现这些函数的 FreeRTOS 协程相关的源文件。通常这些文件包括 croutine.c

  • FreeRTOS 配置问题:检查FreeRTOSConfig.h 文件,确保已经定义了 configUSE_CO_ROUTINES 为 1,以启用协程的支持。如果没有定义或者定义为0,协程相关的函数将不会被包含在编译中。

  •  个人看法:

            在较新版本的FreeRTOS中,协程(co-routines)的支持可能已经被移除或不再推荐使用。这是因为协程在现代实时操作系统中并不常用,且其实现可能不如任务(task)调度那样高效和灵活。如果正在使用的FreeRTOS版本中找不到关于协程的相关函数定义,这可能是因为协程的支持已经不再包含在该版本中。

            在编写基于FreeRTOS的应用程序时,推荐使用任务作为主要的并发执行单元,并利用FreeRTOS提供的任务调度、同步和通信机制来实现应用程序逻辑。如果您需要类似协程的行为,可能需要通过手动管理任务的执行和挂起状态来模拟,但这通常比使用真正的协程要复杂得多。

            如果项目确实需要使用协程,并且使用的FreeRTOS版本还支持协程,那么您需要确保在FreeRTOSConfig.h中定义了configUSE_CO_ROUTINES为1,以启用协程的相关代码。同时,确保项目包含了实现协程功能的FreeRTOS源文件,如croutine.c

    实践发现,在FreeRTOSConfig.h中添加:

    #define configUSE_IDLE_HOOK         1
    #define configUSE_CO_ROUTINES       1
    #define configMAX_CO_ROUTINE_PRIORITIES    10

    即可解决该报错问题。 

    十五、FreeRTOS 协程补充说明:

    FreeRTOS 协程是 FreeRTOS 实时操作系统中的一个轻量级任务机制,它们比传统的任务更简单,通常用于实现有限状态机或简单的通信协议。协程在 FreeRTOS 中通常用于实现简单的、协作式的、非抢占式的任务。

    以下是关于 FreeRTOS 协程的一些关键点:

  • 协程与任务的区别:

  • 任务是抢占式的,而协程是非抢占式的,即协程的执行完全由编程逻辑控制,没有时间片的概念。
  • 协程通常用于实现简单的有限状态机或处理简单的事件,而任务则用于更复杂的功能。
  • 配置:

  • 要在 FreeRTOS 中使用协程,需要在 FreeRTOSConfig.h 文件中定义 configUSE_CO_ROUTINES 为 1。
  • 如果需要在空闲任务中调度协程,还需要定义 configUSE_IDLE_HOOK 为 1。
  • 示例文件:

  • crflash.c 是一个示例文件,它使用协程来控制 LED 的闪烁,而不是使用任务。
  • crhook.c 演示了如何将数据从中断传递到协程。
  • 替换任务为协程:

  • 要将任务替换为协程,需要在项目中包含 croutine.c 文件,并在 main.c 中包含 croutine.h 头文件。
  • 需要将创建任务的函数调用(如 vStartLEDFlashTasks())替换为创建协程的函数调用(如 vStartFlashCoRoutines(n))。
  • 调度协程:

  • 协程需要手动调度,通常在空闲钩子函数中调用 vCoRoutineSchedule() 来调度协程。
  • 空闲钩子函数:

  • 在 main.c 中添加或修改空闲钩子函数,以便在系统空闲时调度协程:
    void vApplicationIdleHook( void )
    {
        vCoRoutineSchedule( void );
    }
  • 内存优化:

  • 使用协程可以减少堆栈的需求,因为协程通常不需要像任务那样大的堆栈空间。
  • 如果项目 RAM 有限,可以通过减少 FreeRTOSConfig.h 中的 portTOTAL_HEAP_SPACE 定义来优化内存使用。
  • 注意事项:

  • 协程不会像任务那样自动调度,因此需要程序员手动管理协程的执行顺序。
  • 协程不会响应中断,因此不适合需要快速响应的实时处理。
  • 十六、FreeRTOS移植所需文件:

    通过网盘分享的文件:FreeRTOSv202212.01.zip
    链接: https://pan.baidu.com/s/1I5QGQsoFgaGMDOVduHaaGA?pwd=38u9 提取码: 38u9 

    作者:The_xz

    物联沃分享整理
    物联沃-IOTWORD物联网 » FreeRTOS保姆级教程:STM32任务与协程详解及实战代码示例

    发表回复