序:
- 在上一章节中, 任务体内的延时使用的是软件延时,即还是让 CPU 空等来达到延时的效果。
- 使用 RTOS 的很大优势就是榨干 CPU 的性能,永远不能让它闲着, 任务如果需要延时也就不能再让 CPU 空等来实现延时的效果
- RTOS 中的延时叫阻塞延时,即任务需要延时的时候, 任务会放弃 CPU 的使用权, CPU 可以去干其它的事情,当任务延时时间到,重新获取 CPU 使用权, 任务继续运行。
- 当任务需要延时,进入阻塞状态,那 CPU 又去干什么事情了?如果没有其它任务可以运行, RTOS 都会为 CPU 创建一个空闲任务,这个时候 CPU 就运行空闲任务。 在FreeRTOS 中,空闲任务是系统在【启动调度器】 的时候创建的优先级最低的任务,空闲任务主体主要是做一些系统内存的清理工作。
- 我们本章实现的空闲任务只是对一个全局变量进行计数。
6.1 实现空闲任务
目前我们在创建任务时使用的栈和 TCB 都使用的是静态的内存,即需要预先定义好内存,空闲任务也不例外。有关空闲任务的栈和 TCB 需要用到的内存空间均在 main.c 中定义。
6.1.1 定义空闲任务栈
空闲任务的栈在我们在 main.c 中定义、
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
就是定义一个数组作为栈。
6.1.2 定义空闲任务的任务控制块
任务控制块是每一个任务必须的,空闲任务的的任务控制块我们在 main.c 中定义,是一个全局变量。
/* 定义空闲任务的任务控制块 */TCB_t IdleTaskTCB;
// portable.h#define portTASK_FUNCTION( vFunction, pvParameters ) void vFunction( void *pvParameters )// task.cstatic portTASK_FUNCTION( prvIdleTask, pvParameters ){/* 防止编译器的警告 */( void ) pvParameters;for(;;){/* 空闲任务暂时什么都不做 */}}
6.1.3 创建空闲任务
当定义好空闲任务的栈, 任务控制块后,就可以创建空闲任务。空闲任务在调度器启动函数 vTaskStartScheduler()中创建。
extern TCB_t IdleTaskTCB;void vTaskStartScheduler( void ){TCB_t *pxIdleTaskTCBBuffer = NULL; /* 用于指向空闲任务控制块 */StackType_t *pxIdleTaskStackBuffer = NULL; /* 用于空闲任务栈起始地址 */uint32_t ulIdleTaskStackSize;/* 获取空闲任务的内存:任务栈和任务 TCB */vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, //1&pxIdleTaskStackBuffer,&ulIdleTaskStackSize );xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任务入口 */(char *)"IDLE", /* 任务名称,字符串形式 */(uint32_t)ulIdleTaskStackSize , /* 任务栈大小,单位为字 */(void *) NULL, /* 任务形参 */(StackType_t *)pxIdleTaskStackBuffer, /* 任务栈起始地址 */(TCB_t *)&pxIdleTaskTCBBuffer ); /* 任务控制块 *///将空闲任务插入到就绪列表的开头。 在下一章我们会支持优先级,//空闲任务默认的优先级是最低的,即排在就绪列表的开头vListInsertEnd( &( pxReadyTasksLists[0] ), &( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem) );/***********************************************空闲任务创建完成*******************************//* 手动指定第一个运行的任务 */pxCurrentTCB = &Task1TCB;if ( xPortStartScheduler() != pdFALSE){/* 调度器启动成功,则不会返回,即不会来到这里 */}}
- 获 取 空 闲 任 务 的 内 存 , 即 将 pxIdleTaskTCBBuffer 和pxIdleTaskStackBuffer 这两个接下来要作为形参传到 xTaskCreateStatic()函数的指针分别指向空闲任务的 TCB 和栈的起始地址,这个操作由函数 vApplicationGetIdleTaskMemory()来实现,该函数需要用户自定义,目前我们在 main.c 中实现
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];/* 定义空闲任务的任务控制块 */TCB_t IdleTaskTCB;void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,StackType_t **ppxIdleTaskStackBuffer,uint32_t *pulIdleTaskStackSize ){*ppxIdleTaskTCBBuffer=&IdleTaskTCB;*ppxIdleTaskStackBuffer=IdleTaskStack;*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;}
6.2 实现阻塞延时
6.2.1 vTaskDealy() 函数
阻塞延时的阻塞是指任务调用该延时函数后, 任务会被剥离 CPU 使用权,然后进入阻塞状态,直到延时结束, 任务重新获取 CPU 使用权才可以继续运行。在任务阻塞的这段时间, CPU 可以去执行其它的任务,如果其它的任务也在延时状态,那么 CPU 就将运行空闲任务。阻塞延时函数在 task.c 中定义。
// 阻塞延时void vTaskDealy( const TickType_t xTicksToDelay ){TCB_t *pxTCB = NULL;/* 获取当前任务的 TCB */pxTCB = pxCurrentTCB;/* 设置延时时间 */pxTCB->xTicksToDelay = xTicksToDelay;/* 任务切换 */taskYIELD();}
xTicksToDelay 是任务控制块的一个成员,用于记录任务需要延时的时间,单位为 SysTick 的中断周期。比如我们本书当中 SysTick 的中断周期为 10ms,调用 vTaskDelay( 2 )则完成 2*10ms 的延时。
// freertos.h文件typedef struct tskTaskControlBlock{volatile StackType_t *pxTopOfStack; /* 栈顶 */ListItem_t xStateListItem; /* 任务节点 */StackType_t *pxStack; /* 任务栈起始地址 *//* 任务名称,字符串形式 */char pcTaskName[ configMAX_TASK_NAME_LEN ];TickType_t xTicksToDelay; /* 用于延时 */ // 新增部分} tskTCB;typedef tskTCB TCB_t;
6.2.2 修改vTaskSwitchContext() 函数
任务切换。 调用 tashYIELD()会产生 PendSV中断,在 PendSV中断服务函数中会调用上下文切换函数 vTaskSwitchContext(),该函数的作用是寻找最高优先级的就绪任务,然后更新 pxCurrentTCB。 上一章我们只有两个任务, 则 pxCurrentTCB 不是指向任务 1 就是指向任务 2,本章节开始我们多增加了一个空闲任务,则需要让pxCurrentTCB 在这三个任务中切换, 算法需要改变。
void vTaskSwitchContext( void ){/* 如果当前任务是空闲任务,那么就去尝试执行任务 1 或者任务 2,看看他们的延时时间是否结束,如果任务的延时时间均没有到期,那就返回继续执行空闲任务 */if( pxCurrentTCB == &IdleTaskTCB ){if(Task1TCB.xTicksToDelay == 0){pxCurrentTCB = &Task1TCB;}else if (Task2TCB.xTicksToDelay == 0){pxCurrentTCB = &Task2TCB;}else{return;}}else /* 当前任务不是空闲任务则会执行到这里 */{/*如果当前任务是任务 1 或者任务 2 的话,检查下另外一个任务,如果另外的任务不在延时中,就切换到该任务否则,判断下当前任务是否应该进入延时状态,如果是的话,就切换到空闲任务。否则就不进行任何切换 */if(pxCurrentTCB == &Task1TCB){if(Task2TCB.xTicksToDelay == 0){pxCurrentTCB = &Task2TCB;}else if (pxCurrentTCB->xTicksToDelay != 0){pxCurrentTCB = &IdleTaskTCB;}else{return;}}if(pxCurrentTCB == &Task2TCB){if(Task1TCB.xTicksToDelay == 0){pxCurrentTCB = &Task1TCB;}else if (pxCurrentTCB->xTicksToDelay != 0){pxCurrentTCB = &IdleTaskTCB;}else{return;}}}}
6.3 SysTick中断函数
在任务上下文切换函数 vTaskSwitchContext ()中,会判断每个任务的任务控制块中的延时成员 xTicksToDelay 的值是否为 0,如果为 0就要将对应的任务就绪, 如果不为 0 就继续延时。如果一个任务要延时,一开始 xTicksToDelay 肯定不为 0,当 xTicksToDelay 变为0 的时候表示延时结束,那么 xTicksToDelay 是以什么周期在递减? 在哪里递减? 在FreeRTOS 中, 这个周期由 SysTick 中断提供,操作系统里面的最小的时间单位就是SysTick 的中断周期,我们称之为一个 tick, SysTick 中断服务函数在 port.c.c 中实现。
void xPortSysTickHandler( void ){/* 关中断 */vPortRaiseBASEPRI();/* 更新系统时基 */xTaskIncrementTick();/* 开中断 */vPortClearBASEPRIFromISR();}
6.3.1 xTaskIncrementTick()函数
xTaskIncrementTick()函数。
void xTaskIncrementTick( void ){TCB_t *pxTCB = NULL;BaseType_t i = 0;/* 更新系统时基计数器 xTickCount, xTickCount 是一个在 port.c 中定义的全局变量 */const BaseType_t xConstTickCount = xTickCount + 1;xTickCount = xConstTickCount;/* 扫描就绪列表中所有任务的 xTicksToDelay,如果不为 0,则减 1 */for ( i = 0; i < configMAX_PRIORITIES; i++){pxTCB = (TCB_t *)listGET_OWNER_OF_HEAD_ENTRY( ( &pxReadyTasksLists[i] ) );if (pxTCB->xTicksToDelay > 0){pxTCB->xTicksToDelay--;}}/* 任务切换 */portYIELD();}
6.4 SysTick初始化函数
SysTick 的中断服务函数要想被顺利执行,则 SysTick 必须先初始化。 SysTick 初始化函数在 port.c 中定义:
// 滴答时钟/* SysTick 控制寄存器 */#define portNVIC_SYSTICK_CTRL_REG (*((volatile uint32_t *) 0xe000e010 ))/* SysTick 重装载寄存器寄存器 */#define portNVIC_SYSTICK_LOAD_REG (*((volatile uint32_t *) 0xe000e014 ))/* SysTick 时钟源选择 */#ifndef configSYSTICK_CLOCK_HZ#define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ/* 确保 SysTick 的时钟与内核时钟一致 */#define portNVIC_SYSTICK_CLK_BIT ( 1UL << 2UL )#else#define portNVIC_SYSTICK_CLK_BIT ( 0 )#endifvoid vPortSetupTimerInterrupt( void ){/* 设置重装载寄存器的值 */portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;/* 设置系统定时器的时钟等于内核时钟使能SysTick 定时器中断使能SysTick 定时器 */portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT |portNVIC_SYSTICK_INT_BIT |portNVIC_SYSTICK_ENABLE_BIT );}
SysTick 初 始 化 函 数 vPortSetupTimerInterrupt() , 在xPortStartScheduler()中被调用
BaseType_t xPortStartScheduler( void ){/* 配置 PendSV 和 SysTick 的中断优先级为最低 */portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;/* 初始化 SysTick */vPortSetupTimerInterrupt() // 这个/* 启动第一个任务,不再返回 */prvStartFirstTask();/* 不应该运行到这里 */return 0;}
设置重装载寄存器的值, 决定 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 中定义
- SysTick 每秒中断多少次,目前配置为 100,即每 10ms 中断一次。
6.5 小总结
void vTaskStartScheduler( void ) 也就是任务开始调度的时候,创建空闲任务,然后添加到任务列表的[0]位置中。然后开始调度。
然后调到任务1,然后通过系统给出的delay函数void vTaskDelay( const TickType_t xTicksToDelay )来设置延迟的时间,也就是xTicksToDelay。然后调用任务切换
然后调用portYIELD(),进行真正的任务切换,也就是挂起pendSV
在pendSV中断函数中,保存上问,然后切换 调用vTaskSwitchContext函数,来更改pxCurrentTCB。在这个函数中,判断是否有任务在延时,根据任务是否有延时在3个任务之间切换。这个切换是发生在有任务要睡眠的时候发生的切换,然后就切换其它任务了。那么这么切换回去?
这个设置的xTicksToDelay的数值如何更改?并且如何切换回去的呢?答案是通过systick的中断,设置中断时间为10ms,然后在中断函数中(关中断),然后循环遍历所有的任务,将他们的延时减去1,然后调用portYIELD()进行任务切换,这里就是一个10ms的周期过去之后,看看是否有任务等待时间完成了。如果等待完成,然后取调用。
6.6 main函数
/*************************************************************************** @file main.c* @author fire* @version V1.0* @date 2018-xx-xx* @brief 《FreeRTOS内核实现与应用开发实战指南》书籍例程* 新建FreeRTOS工程—软件仿真************************************************************************* @attention** 实验平台:野火 STM32 系列 开发板** 官网 :www.embedfire.com* 论坛 :http://www.firebbs.cn* 淘宝 :https://fire-stm32.taobao.com**************************************************************************//*************************************************************************** 包含的头文件**************************************************************************/#include "FreeRTOS.h"#include "task.h"/*************************************************************************** 全局变量**************************************************************************/portCHAR flag1;portCHAR flag2;extern List_t pxReadyTasksLists[ configMAX_PRIORITIES ];/*************************************************************************** 任务控制块 & STACK**************************************************************************/TaskHandle_t Task1_Handle;#define TASK1_STACK_SIZE 128StackType_t Task1Stack[TASK1_STACK_SIZE];TCB_t Task1TCB;TaskHandle_t Task2_Handle;#define TASK2_STACK_SIZE 128StackType_t Task2Stack[TASK2_STACK_SIZE];TCB_t Task2TCB;/*************************************************************************** 函数声明**************************************************************************/void delay (uint32_t count);void Task1_Entry( void *p_arg );void Task2_Entry( void *p_arg );/************************************************************************** main函数*************************************************************************/int main(void){/* 硬件初始化 *//* 将硬件相关的初始化放在这里,如果是软件仿真则没有相关初始化代码 *//* 初始化与任务相关的列表,如就绪列表 */prvInitialiseTaskLists();/* 创建任务 */Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */(char *)"Task1", /* 任务名称,字符串形式 */(uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */(void *) NULL, /* 任务形参 */(StackType_t *)Task1Stack, /* 任务栈起始地址 */(TCB_t *)&Task1TCB ); /* 任务控制块 *//* 将任务添加到就绪列表 */vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );Task2_Handle = xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */(char *)"Task2", /* 任务名称,字符串形式 */(uint32_t)TASK2_STACK_SIZE , /* 任务栈大小,单位为字 */(void *) NULL, /* 任务形参 */(StackType_t *)Task2Stack, /* 任务栈起始地址 */(TCB_t *)&Task2TCB ); /* 任务控制块 *//* 将任务添加到就绪列表 */vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );/* 启动调度器,开始多任务调度,启动成功则不返回 */vTaskStartScheduler();for(;;){/* 系统启动成功不会到达这里 */}}/*************************************************************************** 函数实现**************************************************************************//* 软件延时 */void delay (uint32_t count){for(; count!=0; count--);}/* 任务1 */void Task1_Entry( void *p_arg ){for( ;; ){#if 0flag1 = 1;delay( 100 );flag1 = 0;delay( 100 );/* 线程切换,这里是手动切换 */portYIELD();#elseflag1 = 1;vTaskDelay( 2 );flag1 = 0;vTaskDelay( 2 );#endif}}/* 任务2 */void Task2_Entry( void *p_arg ){for( ;; ){#if 0flag2 = 1;delay( 100 );flag2 = 0;delay( 100 );/* 线程切换,这里是手动切换 */portYIELD();#elseflag2 = 1;vTaskDelay( 2 );flag2 = 0;vTaskDelay( 2 );#endif}}/* 获取空闲任务的内存 */StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];TCB_t IdleTaskTCB;void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,StackType_t **ppxIdleTaskStackBuffer,uint32_t *pulIdleTaskStackSize ){*ppxIdleTaskTCBBuffer = &IdleTaskTCB;*ppxIdleTaskStackBuffer = IdleTaskStack;*pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;}
6.7 实验现象
出了一个BUG
在创建IDEL任务的时候
xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任务入口 */(char *)"IDLE", /* 任务名称,字符串形式 */(uint32_t)ulIdleTaskStackSize , /* 任务栈大小,单位为字 */(void *) NULL, /* 任务形参 */(StackType_t *)pxIdleTaskStackBuffer, /* 任务栈起始地址 *///这里的取地址符号没加(TCB_t *) pxIdleTaskTCBBuffer ); /* 任务控制块 */// 可以看出,这样强制转换,系统不会报错,但是给调试带来了很大的难度。// 强制转换出错的结果 需要程序员自己承担xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任务入口 */(char *)"IDLE", /* 任务名称,字符串形式 */(uint32_t)ulIdleTaskStackSize , /* 任务栈大小,单位为字 */(void *) NULL, /* 任务形参 */(StackType_t *)pxIdleTaskStackBuffer, /* 任务栈起始地址 */(TCB_t *)&pxIdleTaskTCBBuffer ); /* 任务控制块 */

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