协程是freeRTOS提供的另外一种用以实现用户任务的一种机制,主要使用在RAM较小的处理器上,在32位的处理器上几乎不使用。本文包括:协程状态、协程优先级、实现协程、协程与任务混合使用、不足之处、demo。

状态

协程相对于任务状态只有三种,没有挂起态:

  1. 运行态

    与任务的运行态意义相同。

  2. 就绪态

    与任务的就绪态意义相同,不过条件不同,除了因为此时有更高优先级的协程在运行导致无法执行外,在任务与协程混用的系统中,任何一个任务处在运行态都会导致协程无法执行。

  3. 阻塞态

    与任务类似,协程在等待时间或者外部事件,会进入阻塞态,与vTaskDelay()类似,协程使用crDELAY()来等待一段时间。

协程的状态转换图:

Valid co-routine state transitions

优先级

与任务类似,每个协程都有一个从0到configMAX_CO_ROUTINE_PRIORITIES - 1的优先级,共享优先级,且优先级之相对于协程来说。意思是,即使协程的优先级比任务的优先级高,系统仍然会优先调度到任务上,而非协程。

可以概括为:高优先级任务 > 低优先级任务 > 高等级协程 > 低等级协程

实现协程

与任务类似(…标准的开头语…),freeRTOS也要求用户定义固定形式的协程,如下:

  1. void vACoRoutineFunction( CoRoutineHandle_t xHandle,
  2. UBaseType_t uxIndex )
  3. {
  4. crSTART( xHandle );
  5. for( ;; )
  6. {
  7. -- Co-routine application code here. --
  8. }
  9. crEND();
  10. }

CoRoutineHandle_t xHandleUBaseType_t uxIndex 协程所接收的参数,关于这两个参数的含义和用法,后面会有介绍,带着疑问往下看吧。

值得注意的几点:

  • 所有的协程必须通过调用crSTART()crEND()来开始和结束。
  • 与任务相同,不允许返回,是一个死循环。
  • 多个协程可以通过同一个协程”模板”创建,彼此之间通过uxIdex区分。

调度

如任务不类似,协程的调度是才用重复调用vCoRountinueSChedule()来实现的,最佳的调用方式是在空闲任务的钩子函数中调用,这是因为即使你只使用协程,空闲任务仍然会自动创建当调度器启动的时候。

在空闲任务的钩子函数中调度协程,会让协程在与任务混合使用的系统中,总是在所有的任务执行完之后才会执行。因此,建议有使用混合任务与协程需求的小伙伴,把重要性低且占用时间短,对实时性要求不高的事情放在协程中处理

缺点

虽然相比同等数量的任务,协程所占用的RAM比较少,在低内存的处理器上更加适合,但是同样协程也有很多限制,同时使用上也比任务复杂。

协程中的变量

协程的堆栈不会在协程阻塞的时候保持,这意味着协程在栈上所申请的变量可能会丢失它们的值,为了解决这点,协程中需要保持数据的变量必须定义位静态的static

  1. void vACoRoutineFunction( CoRoutineHandle_t xHandle,
  2. UBaseType_t uxIndex )
  3. {
  4. static char c = 'a';
  5. // Co-routines must start with a call to crSTART().
  6. crSTART( xHandle );
  7. for( ;; )
  8. {
  9. // 如果我们在这里将c的值设为'b' ...
  10. c = 'b';
  11. // ... 然后阻塞协程 ...
  12. crDELAY( xHandle, 10 );
  13. // ... c的值只有在申明成静态类型才会一定等于'b'
  14. // (as it is here).
  15. }
  16. // Co-routines must end with a call to crEND().
  17. crEND();
  18. }

协程中的阻塞API调用

另一个问题因此导致的问题就是,所有能导致协程阻塞的API调用,只能由协程本身调用,不能由其内部调用的函数来调用。

  1. void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
  2. {
  3. // Co-routines must start with a call to crSTART().
  4. crSTART( xHandle );
  5. for( ;; )
  6. {
  7. // It is fine to make a blocking call here,
  8. crDELAY( xHandle, 10 );
  9. // but a blocking call cannot be made from within
  10. // vACalledFunction().
  11. vACalledFunction();
  12. }
  13. // Co-routines must end with a call to crEND().
  14. crEND();
  15. }
  16. void vACalledFunction( void )
  17. {
  18. // Cannot make a blocking call here!
  19. }

协程中的switch语句

freeRTOS的默认协程实现中不能使用switch语句

  1. void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
  2. {
  3. // Co-routines must start with a call to crSTART().
  4. crSTART( xHandle );
  5. for( ;; )
  6. {
  7. // It is fine to make a blocking call here,
  8. crDELAY( xHandle, 10 );
  9. switch( aVariable )
  10. {
  11. case 1 : // Cannot make a blocking call here!
  12. break;
  13. default: // Or here!
  14. }
  15. }
  16. // Co-routines must end with a call to crEND().
  17. crEND();
  18. }

一个简单的栗子

下面的例子是演示如何使用协程来是LED闪烁,在这要说明的是,不要以为所有的LED闪烁都可以放在协程中来做,别如电能表的脉冲控制,对脉冲宽度有要求的,如果放在协程里,很有可能导致忽慢忽快。协程的实时性要差点,最好放在这里的任务都是些对实时性要求不高的。

  • 简单的LED闪烁代码
  1. void vFlashCoRoutine( CoRoutineHandle_t xHandle,
  2. UBaseType_t uxIndex )
  3. {
  4. // Co-routines must start with a call to crSTART().
  5. crSTART( xHandle );
  6. for( ;; )
  7. {
  8. // Delay for a fixed period.
  9. crDELAY( xHandle, 10 );
  10. // Flash an LED.
  11. vParTestToggleLED( 0 );
  12. }
  13. // Co-routines must end with a call to crEND().
  14. crEND();
  15. }
  • 调度协程

    协程的调度通过重复调用vCoRoutineSchedule()实现,最好放在空闲任务的钩子函数中,因此首先需要使能钩子函数,设置configUSE_IDLE_HOOK为1,接着写空闲任务的钩子函数:
  1. void vApplicationIdleHook( void )
  2. {
  3. vCoRoutineSchedule( void );
  4. }

如果,空闲任务的钩子函数不包含其他功能,建议这样写,效率会高点:

  1. void vApplicationIdleHook( void )
  2. {
  3. for( ;; )
  4. {
  5. vCoRoutineSchedule( void );
  6. }
  7. }
  • 创建协程并启动调用器

    1. #include "task.h"
    2. #include "croutine.h"
    3. #define PRIORITY_0 0
    4. void main( void )
    5. {
    6. // In this case the index is not used and is passed
    7. // in as 0.
    8. xCoRoutineCreate( vFlashCoRoutine, PRIORITY_0, 0 );
    9. // NOTE: Tasks can also be created here!
    10. // Start the RTOS scheduler.
    11. vTaskStartScheduler();
    12. }
  • 使用index来实现多个LED闪烁

    如前所述,协程函数的申明实际上类似申明了一个”模板”,可以通过这个”模板”声明多个协程,每个协程之间使用index区分:

  1. #include "task.h"
  2. #include "croutine.h"
  3. #define PRIORITY_0 0
  4. #define NUM_COROUTINES 8
  5. void main( void )
  6. {
  7. int i;
  8. for( i = 0; i < NUM_COROUTINES; i++ )
  9. {
  10. /* 通过index区分不同协程. */
  11. xCoRoutineCreate( vFlashCoRoutine, PRIORITY_0, i );
  12. }
  13. // 除了协程,这里仍然可以创建任务!
  14. // 启动调度器
  15. vTaskStartScheduler();
  16. }
  17. The co-routine function is also extended so each uses a different LED and flash rate.
  18. const int iFlashRates[ NUM_COROUTINES ] = { 10, 20, 30, 40, 50, 60, 70, 80 };
  19. const int iLEDToFlash[ NUM_COROUTINES ] = { 0, 1, 2, 3, 4, 5, 6, 7 }
  20. void vFlashCoRoutine( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
  21. {
  22. // Co-routines must start with a call to crSTART().
  23. crSTART( xHandle );
  24. for( ;; )
  25. {
  26. // 不同的协程阻塞时间不同,从而实现闪烁频率不同
  27. crDELAY( xHandle, iFlashRate[ uxIndex ] );
  28. // 自然,每个协程可根据index选择不同的LED
  29. vParTestToggleLED( iLEDToFlash[ uxIndex ] );
  30. }
  31. // Co-routines must end with a call to crEND().
  32. crEND();
  33. }