在裸机运行中,我们是使用软件延时来实现延时的功能(delay()),即是让CPU空等来达到延时的目的。使用RTOS的很大优势就是榨干CPU性能,永远不让他闲着,任务需要延时也就不需要让CPU空等来实现延时的效果。
RTOS中的延时叫做阻塞延时,即任务需要延时的时候,任务会放弃CPU的使用权,CPU可以去干其他的事情,当任务延时时间到,重新获取CPU使用权,任务继续运行。这样就可以充分利用CPU资源了。
当任务进入阻塞状态,如果没有其他任务可以运行,RTOS都会为CPU创建一个空闲任务,这个时候CPU就去运行空闲任务。在FreeRTOS中,空闲任务是系统在【启动调度器】的时候创建的优先级最低的任务,空闲任务主要做一些系统内存清理的任务。
一、实现空闲任务
目前我们在创建任务时使用的栈和TCB都是使用的静态内存,即预先定义好的内存,空闲任务也不例外。
1、定义空闲任务的栈
空闲任务的栈在main.c 中定义:
/* 定义空闲任务的栈 */
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) (2)
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE]; (1)
(1):空闲任务的栈是一个定义好的数组,大小由FreeRTOSConfig.h 中
定义的宏 configMINIMAL_STACK_SIZE 控制,大小为128,单位是字。
2、定义空闲任务的任务控制块
空闲任务的任务控制块在main.c中定义:
/* 定义空闲任务的任务控制块 */
TCB_t IdleTaskTCB;
3、定义空闲任务的任务控制块
空闲任务在调度器启动函数vTaskStartScheduler()中创建:
extern TCB_t IdleTaskTCB;
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize );
void vTaskStartScheduler( void )
{
/*=======================创建空闲任务 start=======================*/
TCB_t *pxIdleTaskTCBBuffer = NULL; /* 用于指向空闲任务控制块 */
StackType_t *pxIdleTaskStackBuffer = NULL; /* 用于空闲任务栈起始地址 */
uint32_t ulIdleTaskStackSize;
/* 获取空闲任务的内存:任务栈和任务 TCB */ (1)
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer,
&pxIdleTaskStackBuffer,
&ulIdleTaskStackSize );
/* 创建空闲任务 */ (2)
xIdleTaskHandle =
xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任务入口 */
(char *)"IDLE", /* 任务名称,字符串形式 */
(uint32_t)ulIdleTaskStackSize , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(StackType_t *)pxIdleTaskStackBuffer, /* 任务栈起始地址 */
(TCB_t *)pxIdleTaskTCBBuffer ); /* 任务控制块 */
/* 将任务添加到就绪列表开头 */ (3)
vListInsertEnd( &( pxReadyTasksLists[0] ),&( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem ) );
/*==========================创建空闲任务 end=====================*/
/* 手动指定第一个运行的任务 */
pxCurrentTCB = &Task1TCB;
/* 启动调度器 */
if ( xPortStartScheduler() != pdFALSE )
{
/* 调度器启动成功,则不会返回,即不会来到这里 */
}
}
(1):获 取 空 闲 任 务 的 内 存 , 即 将 pxIdleTaskTCBBuffer 和pxIdleTaskStackBuffer 这两个接下来要作为形参传到xTaskCreateStatic()函数的指针分别指向空闲任务的 TCB 和栈的起始地址,这个操作由函数 vApplicationGetIdleTaskMemory()来实现:
vApplicationGetIdleTaskMemory():
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize )
{
*ppxIdleTaskTCBBuffer=&IdleTaskTCB;
*ppxIdleTaskStackBuffer=IdleTaskStack;
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}
(2):调用 xTaskCreateStatic()函数创建空闲任务。
(3):将空闲任务插入到就绪列表的开头。
二、实现阻塞延时
1、vTaskDelay ()函数
阻塞延时的阻塞是指任务调用该延时函数后,任务会被剥离CPU使用权,然后进入阻塞状态,直到延时结束,任务重新获取CPU使用权才可以继续运行。在任务阻塞这段时间,CPU可以去执行其他的任务,如果其他任务也处于阻塞状态,CPU就会执行空闲任务。
void vTaskDelay( const TickType_t xTicksToDelay )
{
TCB_t *pxTCB = NULL;
/* 获取当前任务的 TCB */
pxTCB = pxCurrentTCB; (1)
/* 设置延时时间 */
pxTCB->xTicksToDelay = xTicksToDelay; (2)
/* 任务切换 */
taskYIELD(); (3)
}
(1):获取当前任务控制块。
(2):xTicksToDelay是任务控制块的一个成员,用于记录需要延时的时间,单位为SysTick的中断周期。
xTicksToDelay定义:
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 栈顶 */
ListItem_t xStateListItem; /* 任务节点 */
StackType_t *pxStack; /* 任务栈起始地址 */
char pcTaskName[ configMAX_TASK_NAME_LEN ];/* 任务名称,字符串形式 */
TickType_t xTicksToDelay; /* 用于延时 */
} tskTCB;
(3):任务切换。调用taskYIELD()会产生PendSV中断,在PendSV中断服务函数终会调用任务切换函数vTaskSwitchContext(),该任务的作用是寻找最高优先级的就绪任务,然后更新pxCurrentTCB.因为这一章比上一章多一个空闲任务,需要让pxCurrentTCB 在这三个任务中切换, 算法需要改变,vTaskSwitchContext()函数修改如下:
void vTaskSwitchContext( void )
{
/* 如果当前任务是空闲任务,那么就去尝试执行任务 1 或者任务 2,
看看他们的延时时间是否结束,如果任务的延时时间均没有到期,
那就返回继续执行空闲任务 */
if ( pxCurrentTCB == &IdleTaskTCB ) (1)
{
if (Task1TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task1TCB;
}
else if (Task2TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task2TCB;
}
else
{
return; /* 任务延时均没有到期则返回,继续执行空闲任务 */
}
}
else /* 当前任务不是空闲任务则会执行到这里 */ (2)
{
/*如果当前任务是任务 1 或者任务 2 的话,检查下另外一个任务,
如果另外的任务不在延时中,就切换到该任务
否则,判断下当前任务是否应该进入延时状态,
如果是的话,就切换到空闲任务。否则就不进行任何切换 */
if (pxCurrentTCB == &Task1TCB)
{
if (Task2TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task2TCB;
}
else if (pxCurrentTCB->xTicksToDelay != 0)
{
pxCurrentTCB = &IdleTaskTCB;
}
else
{
return; /* 返回,不进行切换,因为两个任务都处于延时中 */
}
}
else if (pxCurrentTCB == &Task2TCB)
{
if (Task1TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task1TCB;
}
else if (pxCurrentTCB->xTicksToDelay != 0)
{
pxCurrentTCB = &IdleTaskTCB;
}
else
{
return; /* 返回,不进行切换,因为两个任务都处于延时中 */
}
}
}
}
三、SysTick 中断服务函数
1、SysTick 中断服务函数
在任务上下文切换函数 vTaskSwitchContext ()中,会判断每个任务的任务控制块中的延时成员xTicksToDelay的值是否为0,如果为0就要将对应的任务就绪,如果不为0就继续延时。如果一个任务延时,一开始xTicksToDelay不为0,当xTicksToDelay变为0后表示延时结束。在FreeRTOS中,操作系统中的最小时间单位是SysTick的中断周期,我们称之为一个tick ,xTicksToDelay就是以tick为周期递减的。
SysTick 中断服务函数:
void xPortSysTickHandler( void )
{
/* 进入临界段,关中断 */
vPortRaiseBASEPRI();
/* 更新系统时基 */
xTaskIncrementTick();
/* 退出临界中断,开中断 */
vPortClearBASEPRIFromISR();
}
xTaskIncrementTick()函数
void xTaskIncrementTick( void )
{
TCB_t *pxTCB = NULL;
BaseType_t i = 0;
/* 更新系统时基计数器 xTickCount, xTickCount 是一个在 port.c 中定义的全局变量 */(1)
const TickType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;
/* 扫描就绪列表中所有任务的 xTicksToDelay,如果不为 0,则减 1 */(2)
for (i=0; i<configMAX_PRIORITIES; i++)
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &pxReadyTasksLists[i] ) );
if (pxTCB->xTicksToDelay > 0)
{
pxTCB->xTicksToDelay --;
}
}
/* 执行一次任务切换 */(3)
portYIELD();
}
2、SysTick 初始化函数
SysTick 的中断服务函数要想被顺利执行,则 SysTick 必须先初始化SysTick 初始化函数在 port.c 中定义。
vPortSetupTimerInterrupt()函数:
/* SysTick 控制寄存器 */ (1)
#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
#define portNVIC_SYSTICK_INT_BIT ( 1UL << 1UL )
#define portNVIC_SYSTICK_ENABLE_BIT ( 1UL << 0UL )
void vPortSetupTimerInterrupt( void ) (2)
{
/* 设置重装载寄存器的值 */ (2)-①
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
/* 设置系统定时器的时钟等于内核时钟 (2)-②
使能 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;
}
(2)-①:设置重装载寄存器的值,决定SysTick 的中断周期。
configCPU_CLOCK_HZ 与 configTICK_RATE_HZ 宏定义:
#define configCPU_CLOCK_HZ (( unsigned long ) 25000000) (1)
#define configTICK_RATE_HZ (( TickType_t ) 100) (2)
(1):系统时钟大小,目前是软件仿真,需要配置成与system_ARMCM3.c文件中的 SYSTEM_CLOCK的一样, 即等于 25M。如果有具体的硬件,则配置成与硬件系统时钟一样。
(2):SysTick 每秒中断多少次,目前配置为 100,即每 10ms 中断一次。
(2)-②:设置系统定时器的时钟等于内核时钟,使能 SysTick 定时器中断,使能 SysTick 定时器。
参考:[野火®]《FreeRTOS 内核实现与应用开发实战—基于STM32》