序:
- 在上一章节中, 任务体内的延时使用的是软件延时,即还是让 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.c
static 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 )
#endif
void 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 128
StackType_t Task1Stack[TASK1_STACK_SIZE];
TCB_t Task1TCB;
TaskHandle_t Task2_Handle;
#define TASK2_STACK_SIZE 128
StackType_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 0
flag1 = 1;
delay( 100 );
flag1 = 0;
delay( 100 );
/* 线程切换,这里是手动切换 */
portYIELD();
#else
flag1 = 1;
vTaskDelay( 2 );
flag1 = 0;
vTaskDelay( 2 );
#endif
}
}
/* 任务2 */
void Task2_Entry( void *p_arg )
{
for( ;; )
{
#if 0
flag2 = 1;
delay( 100 );
flag2 = 0;
delay( 100 );
/* 线程切换,这里是手动切换 */
portYIELD();
#else
flag2 = 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