前言
在上一节中,任务中延时函数使用的软件延时,即让CPU等待来达到延时的效果,但这样与裸机编程是一样,还没有达到RTOS的效果,使用RTOS的目的就是充分发挥RTOS的性能,永远不让它闲着。如果任务需要延时,不能以CPU空等的方式来实现。RTOS中的延时叫做阻塞延时,即任务需要延时时,会放弃CPU的使用权,CPU可以去做其他事情,当任务延时时间时,重新获取CPU使用权,继续执行任务。FreeRTOS中至少有一个任务叫做空闲任务,如果没有可以执行的任务,CPU就执行空闲任务,空闲任务是优先级最低的任务。
一、实现空闲任务
空闲任务的创建和普通任务的创建是一致的,只是空闲任务优先级最低。
1.定于空闲任务的栈
/* Idle Task */
#define confgiMINIMAL_STACK_SIZE 128
StackType_t IdleTasKStack[confgiMINIMAL_STACK_SIZE];
2.定义任务控制块
TCB_t IdleTaskTCB;
3.创建空闲任务
由于空闲任务是必须有的,所以空闲任务的创建放在vTaskStartScheduler()函数中。在vTaskStartScheduler()继续添加代码:
void vTaskStartScheduler(void)
{
/* 创建空闲任务 */
TCB_t *pxIdleTaskTCBBuffer = NULL; /* 用于指向空闲任务的控制块 */
StackType_t *pxIdleTaskStackBuffer = NULL; /* 用于空闲任务的栈起始地址 */
uint32_t ulIdleTaskStackSize;
/* 获取空闲任务的内存:任务栈和任务TCB */
vApplicationGetIdleTaskMemory(&pxIdleTaskTCBBuffer,&pxIdleTaskStackBuffer,&ulIdleTaskStackSize);
/* 创建空闲任务 */
TaskHandle_t xIdleTaskHandle =
xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任务入口 */
(char*)"IDLE", /* 任务名称,字符串形式 */
(uint32_t)ulIdleTaskStackSize, /* 任务栈大小,单位为字 */
(void*)NULL, /* 任务形参 */
(StackType_t*)pxIdleTaskStackBuffer, /* 任务栈起始地址 */
(TCB_t*)pxIdleTaskTCBBuffer); /* 任务控制块 */
/* 将任务添加到就序列表 */
vListInsertEnd(&(pxReadyTasksLists[0]),&(((TCB_t*)pxIdleTaskStackBuffer)->xStateListItem));
/* 手动指定第一个任务 */
pxCurrentTCB = &Task1TCB;
/* 启动调度器 */
if(xPortStartScheduler() != pdFALSE)
{
/* 调度器启动成功则不会来到这里 */
}
}
空闲任务prvIdleTask()这里是是一个空的死循环函数。vApplicationGetIdleTaskMemory()函数的实现如下:
void vApplicationGetIdleTaskMemory(TCB_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize)
{
*ppxIdleTaskTCBBuffer = &IdleTaskTCB;
*ppxIdleTaskStackBuffer = IdleTasKStack;
*pulIdleTaskStackSize = confgiMINIMAL_STACK_SIZE;
}
二、实现阻塞延时
1.vTaskDelay()函数
阻塞延时的阻塞指的是任务调用该延时函数之后,任务会被剥夺CPU的使用权,然后进入阻塞状态,直到延时结束,任务重新获取CPU使用权才可以继续运行。
void vTaskDelay(const TickType_t xTicksToDelay)
{
TCB_t *pxTCB = NULL;
/* 获取当前任务的TCB */
pxTCB = pxCurrentTCB;
/* 设置延时时间 */
pxTCB->xTicksToDelay = xTicksToDelay;
/* 任务切换 */
taskYIELD();
}
这个函数有两个功能,一是设置当前函数的延时时间,而是进行任务切换。在TCB结构体中新增成员xTicksToDelay用来延时,如下:
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 栈顶 */
ListItem_t xStateListItem; /* 任务节点 */
StackType_t *pxStack; /* 任务栈起始地址 */
char pcTaskName[configMAX_TASK_NAME_LEN]; /* 任务名称,为字符串形式 */
TickType_t xTicksToDelay; /* 延时时间 */
}tskTCB;
2.修改vTaskSwitchContext()函数
在之前该函数的作用仅是切换任务1和任务2,不考虑阻塞和优先级情况,由于目前已经加入了一个空闲任务以及阻塞延时,因此任务切换时应该考虑任务1和任务2的延时情况,比如如果任务1在处于延时状态,那么就执行任务2,如果任务2也处于延时状态,那么就执行空闲任务。
void vTaskSwitchContext(void)
{
/* 如果当前任务是空闲任务,那么就尝试去执行任务1或者任务2,看它们的延时是否已经结束,
如果任务的延时还没有到期,则返回,继续执行空闲任务 */
if(pxCurrentTCB == &IdleTaskTCB)
{
if(Task1TCB.xTicksToDelay == 0)
{
pxCurrentTCB = &Task1TCB;
}
else if(Task2TCB.xTicksToDelay == 0)
{
pxCurrentTCB = &Task2TCB;
}
else
{
return;
}
}
else /* 当前任务不是空闲任务 */
{
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;
}
}
}
}
代码实现无非就是寻找一个可以执行的任务来执行,很容易理解。
3.SysTick中断服务函数
在任务上下文切换函数中,会判断每个任务的任务控制块中的延时成员xTicksToDelay的值是否为0,如果为0就要将对应的任务就绪,如果不为0就继续延时。延时肯定是要有时基的,RTOS一般用SysTick来提供时基,当然也可以用普通定时器来提供时基。具体是在SysTick的中断服务函数中对延时值xTicksToDelay递减,每中断一次,xTicksToDelay就递减一次,因此任务的延时时间的最小间隔是由定时器的中断频率来决定,一般1ms中断一次。SysTick中断服务函数的内容如下:
/* 在port.c中实现 */
void xPortSysTickHandler(void)
{
/* 关中断 */
vPortRaiseBASEPRI();
/* 更新系统时基 */
xTaskIncrementTick();
/* 开中断 */
vPortClearBASEPRIFromISR();
}
这个函数的作用就是更新延时时间,更新系统时间的xTaskIncrementTick()函数的代码如下:
/* 在task.c中实现 */
void xTaskIncrementTick(void)
{
TCB_t *pxTCB = NULL;
BaseType_t i=0;
/* 更新系统时基计数器xTickCount,xTickCount是一个早port.c中定义的全局变量 */
const TickType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;
/* 扫描就序列表中所有任务的xTicksDelay,如果不为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();
}
如果任务的xTicksToDelay值不为0,就进行减1,然后产生PendSV中断进行任务切换。
4.SysTick初始化
/* SysTick Initialization */
/* 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
#define portNVIC_SYSTICK_INT_BIT (1UL << 1UL)
#define portNVIC_SYSTICK_ENABLE_BIT (1UL << 0UL)
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);
}
其中的configSYSTICK_CLOCK_HZ
设置时钟频率,这里用软件仿真,设置为25MHz;configTICK_RATE_HZ是定时器的中断频率,这里设置为 100Hz,即10ms中断一次。代码定义如下:
#define configCPU_CLOCK_HZ ((unsigned long)25000000)
#define configTICK_RATE_HZ ((TickType_t)100)
三、实验验证
将之前的两个任务中的延时函数替换为阻塞延时,代码如下:
/* 任务1 */
void Task1_Entry(void *p_arg)
{
for(;;)
{
#if 0
flag1 = 1;
delay(100);
flag1 = 0;
delay(100);
#else
flag1 = 1;
vTaskDelay(2);
flag1 = 0;
vTaskDelay(2);
#endif
taskYIELD();
}
}
/* 任务2 */
void Task2_Entry(void *p_arg)
{
for(;;)
{
#if 0
flag2 = 1;
delay(100);
flag2 = 0;
delay(100);
#else
flag2 = 1;
vTaskDelay(2);
flag2 = 0;
vTaskDelay(2);
#endif
taskYIELD();
}
}
运行结果:
可以看出,flag1和flag2几乎是同步变化的,已经看到了RTOS的影子了,这就是RTOS想到达到的效果。