序:

  1. 在上一章节中, 任务体内的延时使用的是软件延时,即还是让 CPU 空等来达到延时的效果。
  2. 使用 RTOS 的很大优势就是榨干 CPU 的性能,永远不能让它闲着, 任务如果需要延时也就不能再让 CPU 空等来实现延时的效果
  3. RTOS 中的延时叫阻塞延时,即任务需要延时的时候, 任务会放弃 CPU 的使用权, CPU 可以去干其它的事情,当任务延时时间到,重新获取 CPU 使用权, 任务继续运行。
  4. 当任务需要延时,进入阻塞状态,那 CPU 又去干什么事情了?如果没有其它任务可以运行, RTOS 都会为 CPU 创建一个空闲任务,这个时候 CPU 就运行空闲任务。 在FreeRTOS 中,空闲任务是系统在【启动调度器】 的时候创建的优先级最低的任务,空闲任务主体主要是做一些系统内存的清理工作。
  5. 我们本章实现的空闲任务只是对一个全局变量进行计数。

6.1 实现空闲任务

目前我们在创建任务时使用的栈和 TCB 都使用的是静态的内存,即需要预先定义好内存,空闲任务也不例外。有关空闲任务的栈和 TCB 需要用到的内存空间均在 main.c 中定义。

6.1.1 定义空闲任务栈

空闲任务的栈在我们在 main.c 中定义、

  1. #define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )
  2. StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];

就是定义一个数组作为栈。

6.1.2 定义空闲任务的任务控制块

任务控制块是每一个任务必须的,空闲任务的的任务控制块我们在 main.c 中定义,是一个全局变量。

  1. /* 定义空闲任务的任务控制块 */
  2. TCB_t IdleTaskTCB;
  1. // portable.h
  2. #define portTASK_FUNCTION( vFunction, pvParameters ) void vFunction( void *pvParameters )
  3. // task.c
  4. static portTASK_FUNCTION( prvIdleTask, pvParameters )
  5. {
  6. /* 防止编译器的警告 */
  7. ( void ) pvParameters;
  8. for(;;)
  9. {
  10. /* 空闲任务暂时什么都不做 */
  11. }
  12. }

6.1.3 创建空闲任务

当定义好空闲任务的栈, 任务控制块后,就可以创建空闲任务。空闲任务在调度器启动函数 vTaskStartScheduler()中创建。

  1. extern TCB_t IdleTaskTCB;
  2. void vTaskStartScheduler( void )
  3. {
  4. TCB_t *pxIdleTaskTCBBuffer = NULL; /* 用于指向空闲任务控制块 */
  5. StackType_t *pxIdleTaskStackBuffer = NULL; /* 用于空闲任务栈起始地址 */
  6. uint32_t ulIdleTaskStackSize;
  7. /* 获取空闲任务的内存:任务栈和任务 TCB */
  8. vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, //1
  9. &pxIdleTaskStackBuffer,
  10. &ulIdleTaskStackSize );
  11. xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任务入口 */
  12. (char *)"IDLE", /* 任务名称,字符串形式 */
  13. (uint32_t)ulIdleTaskStackSize , /* 任务栈大小,单位为字 */
  14. (void *) NULL, /* 任务形参 */
  15. (StackType_t *)pxIdleTaskStackBuffer, /* 任务栈起始地址 */
  16. (TCB_t *)&pxIdleTaskTCBBuffer ); /* 任务控制块 */
  17. //将空闲任务插入到就绪列表的开头。 在下一章我们会支持优先级,
  18. //空闲任务默认的优先级是最低的,即排在就绪列表的开头
  19. vListInsertEnd( &( pxReadyTasksLists[0] ), &( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem) );
  20. /***********************************************空闲任务创建完成*******************************/
  21. /* 手动指定第一个运行的任务 */
  22. pxCurrentTCB = &Task1TCB;
  23. if ( xPortStartScheduler() != pdFALSE)
  24. {
  25. /* 调度器启动成功,则不会返回,即不会来到这里 */
  26. }
  27. }
  1. 获 取 空 闲 任 务 的 内 存 , 即 将 pxIdleTaskTCBBuffer 和pxIdleTaskStackBuffer 这两个接下来要作为形参传到 xTaskCreateStatic()函数的指针分别指向空闲任务的 TCB 和栈的起始地址,这个操作由函数 vApplicationGetIdleTaskMemory()来实现,该函数需要用户自定义,目前我们在 main.c 中实现
    1. #define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )
    2. StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
    3. /* 定义空闲任务的任务控制块 */
    4. TCB_t IdleTaskTCB;
    5. void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
    6. StackType_t **ppxIdleTaskStackBuffer,
    7. uint32_t *pulIdleTaskStackSize )
    8. {
    9. *ppxIdleTaskTCBBuffer=&IdleTaskTCB;
    10. *ppxIdleTaskStackBuffer=IdleTaskStack;
    11. *pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
    12. }

6.2 实现阻塞延时

6.2.1 vTaskDealy() 函数

阻塞延时的阻塞是指任务调用该延时函数后, 任务会被剥离 CPU 使用权,然后进入阻塞状态,直到延时结束, 任务重新获取 CPU 使用权才可以继续运行。在任务阻塞的这段时间, CPU 可以去执行其它的任务,如果其它的任务也在延时状态,那么 CPU 就将运行空闲任务。阻塞延时函数在 task.c 中定义。

  1. // 阻塞延时
  2. void vTaskDealy( const TickType_t xTicksToDelay )
  3. {
  4. TCB_t *pxTCB = NULL;
  5. /* 获取当前任务的 TCB */
  6. pxTCB = pxCurrentTCB;
  7. /* 设置延时时间 */
  8. pxTCB->xTicksToDelay = xTicksToDelay;
  9. /* 任务切换 */
  10. taskYIELD();
  11. }
  1. xTicksToDelay 是任务控制块的一个成员,用于记录任务需要延时的时间,单位为 SysTick 的中断周期。比如我们本书当中 SysTick 的中断周期为 10ms,调用 vTaskDelay( 2 )则完成 2*10ms 的延时。

    1. // freertos.h文件
    2. typedef struct tskTaskControlBlock
    3. {
    4. volatile StackType_t *pxTopOfStack; /* 栈顶 */
    5. ListItem_t xStateListItem; /* 任务节点 */
    6. StackType_t *pxStack; /* 任务栈起始地址 */
    7. /* 任务名称,字符串形式 */
    8. char pcTaskName[ configMAX_TASK_NAME_LEN ];
    9. TickType_t xTicksToDelay; /* 用于延时 */ // 新增部分
    10. } tskTCB;
    11. typedef tskTCB TCB_t;

6.2.2 修改vTaskSwitchContext() 函数

任务切换。 调用 tashYIELD()会产生 PendSV中断,在 PendSV中断服务函数中会调用上下文切换函数 vTaskSwitchContext(),该函数的作用是寻找最高优先级的就绪任务,然后更新 pxCurrentTCB。 上一章我们只有两个任务, 则 pxCurrentTCB 不是指向任务 1 就是指向任务 2,本章节开始我们多增加了一个空闲任务,则需要让pxCurrentTCB 在这三个任务中切换, 算法需要改变。

  1. void vTaskSwitchContext( void )
  2. {
  3. /* 如果当前任务是空闲任务,那么就去尝试执行任务 1 或者任务 2,
  4. 看看他们的延时时间是否结束,如果任务的延时时间均没有到期,
  5. 那就返回继续执行空闲任务 */
  6. if( pxCurrentTCB == &IdleTaskTCB )
  7. {
  8. if(Task1TCB.xTicksToDelay == 0)
  9. {
  10. pxCurrentTCB = &Task1TCB;
  11. }
  12. else if (Task2TCB.xTicksToDelay == 0)
  13. {
  14. pxCurrentTCB = &Task2TCB;
  15. }
  16. else
  17. {
  18. return;
  19. }
  20. }
  21. else /* 当前任务不是空闲任务则会执行到这里 */
  22. {
  23. /*如果当前任务是任务 1 或者任务 2 的话,检查下另外一个任务,
  24. 如果另外的任务不在延时中,就切换到该任务
  25. 否则,判断下当前任务是否应该进入延时状态,
  26. 如果是的话,就切换到空闲任务。否则就不进行任何切换 */
  27. if(pxCurrentTCB == &Task1TCB)
  28. {
  29. if(Task2TCB.xTicksToDelay == 0)
  30. {
  31. pxCurrentTCB = &Task2TCB;
  32. }
  33. else if (pxCurrentTCB->xTicksToDelay != 0)
  34. {
  35. pxCurrentTCB = &IdleTaskTCB;
  36. }
  37. else
  38. {
  39. return;
  40. }
  41. }
  42. if(pxCurrentTCB == &Task2TCB)
  43. {
  44. if(Task1TCB.xTicksToDelay == 0)
  45. {
  46. pxCurrentTCB = &Task1TCB;
  47. }
  48. else if (pxCurrentTCB->xTicksToDelay != 0)
  49. {
  50. pxCurrentTCB = &IdleTaskTCB;
  51. }
  52. else
  53. {
  54. return;
  55. }
  56. }
  57. }
  58. }

6.3 SysTick中断函数

在任务上下文切换函数 vTaskSwitchContext ()中,会判断每个任务的任务控制块中的延时成员 xTicksToDelay 的值是否为 0,如果为 0就要将对应的任务就绪, 如果不为 0 就继续延时。如果一个任务要延时,一开始 xTicksToDelay 肯定不为 0,当 xTicksToDelay 变为0 的时候表示延时结束,那么 xTicksToDelay 是以什么周期在递减? 在哪里递减? 在FreeRTOS 中, 这个周期由 SysTick 中断提供,操作系统里面的最小的时间单位就是SysTick 的中断周期,我们称之为一个 tick, SysTick 中断服务函数在 port.c.c 中实现。

  1. void xPortSysTickHandler( void )
  2. {
  3. /* 关中断 */
  4. vPortRaiseBASEPRI();
  5. /* 更新系统时基 */
  6. xTaskIncrementTick();
  7. /* 开中断 */
  8. vPortClearBASEPRIFromISR();
  9. }

6.3.1 xTaskIncrementTick()函数

xTaskIncrementTick()函数。

  1. void xTaskIncrementTick( void )
  2. {
  3. TCB_t *pxTCB = NULL;
  4. BaseType_t i = 0;
  5. /* 更新系统时基计数器 xTickCount, xTickCount 是一个在 port.c 中定义的全局变量 */
  6. const BaseType_t xConstTickCount = xTickCount + 1;
  7. xTickCount = xConstTickCount;
  8. /* 扫描就绪列表中所有任务的 xTicksToDelay,如果不为 0,则减 1 */
  9. for ( i = 0; i < configMAX_PRIORITIES; i++)
  10. {
  11. pxTCB = (TCB_t *)listGET_OWNER_OF_HEAD_ENTRY( ( &pxReadyTasksLists[i] ) );
  12. if (pxTCB->xTicksToDelay > 0)
  13. {
  14. pxTCB->xTicksToDelay--;
  15. }
  16. }
  17. /* 任务切换 */
  18. portYIELD();
  19. }


6. 空闲任务与阻塞延时实现 - 图1

6.4 SysTick初始化函数

SysTick 的中断服务函数要想被顺利执行,则 SysTick 必须先初始化。 SysTick 初始化函数在 port.c 中定义:

  1. // 滴答时钟
  2. /* SysTick 控制寄存器 */
  3. #define portNVIC_SYSTICK_CTRL_REG (*((volatile uint32_t *) 0xe000e010 ))
  4. /* SysTick 重装载寄存器寄存器 */
  5. #define portNVIC_SYSTICK_LOAD_REG (*((volatile uint32_t *) 0xe000e014 ))
  6. /* SysTick 时钟源选择 */
  7. #ifndef configSYSTICK_CLOCK_HZ
  8. #define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ
  9. /* 确保 SysTick 的时钟与内核时钟一致 */
  10. #define portNVIC_SYSTICK_CLK_BIT ( 1UL << 2UL )
  11. #else
  12. #define portNVIC_SYSTICK_CLK_BIT ( 0 )
  13. #endif
  14. void vPortSetupTimerInterrupt( void )
  15. {
  16. /* 设置重装载寄存器的值 */
  17. portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
  18. /* 设置系统定时器的时钟等于内核时钟
  19. 使能SysTick 定时器中断
  20. 使能SysTick 定时器 */
  21. portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT |
  22. portNVIC_SYSTICK_INT_BIT |
  23. portNVIC_SYSTICK_ENABLE_BIT );
  24. }
  1. SysTick 初 始 化 函 数 vPortSetupTimerInterrupt() , 在xPortStartScheduler()中被调用

    1. BaseType_t xPortStartScheduler( void )
    2. {
    3. /* 配置 PendSV 和 SysTick 的中断优先级为最低 */
    4. portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
    5. portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
    6. /* 初始化 SysTick */
    7. vPortSetupTimerInterrupt() // 这个
    8. /* 启动第一个任务,不再返回 */
    9. prvStartFirstTask();
    10. /* 不应该运行到这里 */
    11. return 0;
    12. }
  2. 设置重装载寄存器的值, 决定 SysTick 的中断周期。

如 果 没 有 定 义 configSYSTICK_CLOCK_HZ 那 么configSYSTICK_CLOCK_HZ 就 等 于 configCPU_CLOCK_HZ ,
configSYSTICK_CLOCK_HZ 确 实 没 有 定 义 , 则 configSYSTICK_CLOCK_HZ 由 在
FreeRTOSConfig.h 中定义的 configCPU_CLOCK_HZ 决定, 同时 configTICK_RATE_HZ 也在 FreeRTOSConfig.h 中定义

  1. SysTick 每秒中断多少次,目前配置为 100,即每 10ms 中断一次。

6.5 小总结

  1. void vTaskStartScheduler( void ) 也就是任务开始调度的时候,创建空闲任务,然后添加到任务列表的[0]位置中。然后开始调度。

  2. 然后调到任务1,然后通过系统给出的delay函数void vTaskDelay( const TickType_t xTicksToDelay )来设置延迟的时间,也就是xTicksToDelay。然后调用任务切换

  3. 然后调用portYIELD(),进行真正的任务切换,也就是挂起pendSV

  4. 在pendSV中断函数中,保存上问,然后切换 调用vTaskSwitchContext函数,来更改pxCurrentTCB。在这个函数中,判断是否有任务在延时,根据任务是否有延时在3个任务之间切换。这个切换是发生在有任务要睡眠的时候发生的切换,然后就切换其它任务了。那么这么切换回去?

  5. 这个设置的xTicksToDelay的数值如何更改?并且如何切换回去的呢?答案是通过systick的中断,设置中断时间为10ms,然后在中断函数中(关中断),然后循环遍历所有的任务,将他们的延时减去1,然后调用portYIELD()进行任务切换,这里就是一个10ms的周期过去之后,看看是否有任务等待时间完成了。如果等待完成,然后取调用。

6.6 main函数

  1. /**
  2. ************************************************************************
  3. * @file main.c
  4. * @author fire
  5. * @version V1.0
  6. * @date 2018-xx-xx
  7. * @brief 《FreeRTOS内核实现与应用开发实战指南》书籍例程
  8. * 新建FreeRTOS工程—软件仿真
  9. ************************************************************************
  10. * @attention
  11. *
  12. * 实验平台:野火 STM32 系列 开发板
  13. *
  14. * 官网 :www.embedfire.com
  15. * 论坛 :http://www.firebbs.cn
  16. * 淘宝 :https://fire-stm32.taobao.com
  17. *
  18. ************************************************************************
  19. */
  20. /*
  21. *************************************************************************
  22. * 包含的头文件
  23. *************************************************************************
  24. */
  25. #include "FreeRTOS.h"
  26. #include "task.h"
  27. /*
  28. *************************************************************************
  29. * 全局变量
  30. *************************************************************************
  31. */
  32. portCHAR flag1;
  33. portCHAR flag2;
  34. extern List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
  35. /*
  36. *************************************************************************
  37. * 任务控制块 & STACK
  38. *************************************************************************
  39. */
  40. TaskHandle_t Task1_Handle;
  41. #define TASK1_STACK_SIZE 128
  42. StackType_t Task1Stack[TASK1_STACK_SIZE];
  43. TCB_t Task1TCB;
  44. TaskHandle_t Task2_Handle;
  45. #define TASK2_STACK_SIZE 128
  46. StackType_t Task2Stack[TASK2_STACK_SIZE];
  47. TCB_t Task2TCB;
  48. /*
  49. *************************************************************************
  50. * 函数声明
  51. *************************************************************************
  52. */
  53. void delay (uint32_t count);
  54. void Task1_Entry( void *p_arg );
  55. void Task2_Entry( void *p_arg );
  56. /*
  57. ************************************************************************
  58. * main函数
  59. ************************************************************************
  60. */
  61. int main(void)
  62. {
  63. /* 硬件初始化 */
  64. /* 将硬件相关的初始化放在这里,如果是软件仿真则没有相关初始化代码 */
  65. /* 初始化与任务相关的列表,如就绪列表 */
  66. prvInitialiseTaskLists();
  67. /* 创建任务 */
  68. Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */
  69. (char *)"Task1", /* 任务名称,字符串形式 */
  70. (uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */
  71. (void *) NULL, /* 任务形参 */
  72. (StackType_t *)Task1Stack, /* 任务栈起始地址 */
  73. (TCB_t *)&Task1TCB ); /* 任务控制块 */
  74. /* 将任务添加到就绪列表 */
  75. vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
  76. Task2_Handle = xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */
  77. (char *)"Task2", /* 任务名称,字符串形式 */
  78. (uint32_t)TASK2_STACK_SIZE , /* 任务栈大小,单位为字 */
  79. (void *) NULL, /* 任务形参 */
  80. (StackType_t *)Task2Stack, /* 任务栈起始地址 */
  81. (TCB_t *)&Task2TCB ); /* 任务控制块 */
  82. /* 将任务添加到就绪列表 */
  83. vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );
  84. /* 启动调度器,开始多任务调度,启动成功则不返回 */
  85. vTaskStartScheduler();
  86. for(;;)
  87. {
  88. /* 系统启动成功不会到达这里 */
  89. }
  90. }
  91. /*
  92. *************************************************************************
  93. * 函数实现
  94. *************************************************************************
  95. */
  96. /* 软件延时 */
  97. void delay (uint32_t count)
  98. {
  99. for(; count!=0; count--);
  100. }
  101. /* 任务1 */
  102. void Task1_Entry( void *p_arg )
  103. {
  104. for( ;; )
  105. {
  106. #if 0
  107. flag1 = 1;
  108. delay( 100 );
  109. flag1 = 0;
  110. delay( 100 );
  111. /* 线程切换,这里是手动切换 */
  112. portYIELD();
  113. #else
  114. flag1 = 1;
  115. vTaskDelay( 2 );
  116. flag1 = 0;
  117. vTaskDelay( 2 );
  118. #endif
  119. }
  120. }
  121. /* 任务2 */
  122. void Task2_Entry( void *p_arg )
  123. {
  124. for( ;; )
  125. {
  126. #if 0
  127. flag2 = 1;
  128. delay( 100 );
  129. flag2 = 0;
  130. delay( 100 );
  131. /* 线程切换,这里是手动切换 */
  132. portYIELD();
  133. #else
  134. flag2 = 1;
  135. vTaskDelay( 2 );
  136. flag2 = 0;
  137. vTaskDelay( 2 );
  138. #endif
  139. }
  140. }
  141. /* 获取空闲任务的内存 */
  142. StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
  143. TCB_t IdleTaskTCB;
  144. void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
  145. StackType_t **ppxIdleTaskStackBuffer,
  146. uint32_t *pulIdleTaskStackSize )
  147. {
  148. *ppxIdleTaskTCBBuffer = &IdleTaskTCB;
  149. *ppxIdleTaskStackBuffer = IdleTaskStack;
  150. *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
  151. }

6.7 实验现象

出了一个BUG
在创建IDEL任务的时候

  1. xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任务入口 */
  2. (char *)"IDLE", /* 任务名称,字符串形式 */
  3. (uint32_t)ulIdleTaskStackSize , /* 任务栈大小,单位为字 */
  4. (void *) NULL, /* 任务形参 */
  5. (StackType_t *)pxIdleTaskStackBuffer, /* 任务栈起始地址 */
  6. //这里的取地址符号没加
  7. (TCB_t *) pxIdleTaskTCBBuffer ); /* 任务控制块 */
  8. // 可以看出,这样强制转换,系统不会报错,但是给调试带来了很大的难度。
  9. // 强制转换出错的结果 需要程序员自己承担
  10. xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任务入口 */
  11. (char *)"IDLE", /* 任务名称,字符串形式 */
  12. (uint32_t)ulIdleTaskStackSize , /* 任务栈大小,单位为字 */
  13. (void *) NULL, /* 任务形参 */
  14. (StackType_t *)pxIdleTaskStackBuffer, /* 任务栈起始地址 */
  15. (TCB_t *)&pxIdleTaskTCBBuffer ); /* 任务控制块 */

image.png
放大还是能看出来不是同时的,相差几个us
image.png
可查看到,保持一个电平的时间大概是20ms
image.png