在《刘火良 FreeRTOS内核实现与应用之2------任务的定义与切换》的基础上构建了:
- 空闲任务:
a. 修改了任务控制块,增加了一个用于延时的变量:TickType_t xTicksToDelay;
b.
- 任务的阻塞延时函数:void vTaskDelay( const TickType_t xTicksToDelay )
a. 增加了SysTick中断服务函数;
b. 修改了portYIELD()函数;
c. 修改了vT
空闲任务构建
和普通任务创建的步骤类似,如下。
定义空闲任务的栈
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
定义空闲任务的控制块
TCB_t IdleTaskTCB;
创建空闲任务
和普通任务的区别:
a. 普通任务直接在main函数中创建;
b. 空闲任务在main函数调用vTaskStartScheduler()函数,在vTaskStartScheduler()中创建;

void vTaskStartScheduler( void )
{
/*==========创建空闲任务start===============================*/
TCB_t * pxIdleTaskTCBBuffer = NULL; /* 用于指向空闲任务控制块 */
StackType_t * pxIdleTaskStackBuffer = NULL; /* 用于空闲任务栈起始地址 */
uint32_t ulIdleTaskStackSize;
/* 获取空闲任务的内存:任务栈和任务TCB */
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer,
&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 ) );
/*======创建空闲任务end==============================*/
/* 手动指定第一个运行的任务 */
pxCurrentTCB = &Task1TCB;
/* 初始化系统时基计数器 */
xTickCount = ( TickType_t ) 0U;
/* 启动调度器 */
if( xPortStartScheduler() != pdFALSE )
{
/* 调度器启动成功,则不会返回,即不会来到这里 */
}
}
任务的阻塞延时实现
void vTaskDelay( const TickType_t xTicksToDelay )实现
a. 任务调用该延时函数后,任务会剥夺CPU使用权,然后进入阻塞状态,直到延时结束,任务重新获取CPU使用权才可以继续运行。
b. 在该任务阻塞的这段时间,CPU可以去执行其他任务,若果其他任务也处于延时状态,那么CPU将运行空闲任务。
位置:在task.c文件中定义;
void vTaskDelay( const TickType_t xTicksToDelay )
{
TCB_t *pxTCB = NULL;
/* 获取当前任务的TCB */
pxTCB = pxCurrentTCB;
/* 设置延时时间 */
pxTCB->xTicksToDelay = xTicksToDelay;
/* 任务切换 */
taskYIELD();
}
本函数调用了taskYIELD函数,在FreeRTOS的调度系统中触发任务切换,确保能安全地交出当前任务的控制权给调度程序。
使用 PendSV 中断来实现调度,使任务切换变得高效。
#define portYIELD() \
{ \
/* 设置 PendSV 的中断挂起位,产生上下文切换 */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
\
/* Barriers are normally not required but do ensure the code is completely \
within the specified behaviour for the architecture. */ \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
void vTaskSwitchContext( void )修改
taskYIELD函数会产生PendSV中断,在PendSV中断函数中会调用上下文切换函数vTaskSwitchContext。该函数的功能:寻找最高优先级的就绪任务;更新pxCurrentTCB。
位置:在task.c文件中定义;
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; /* 返回,不进行切换,因为两个线程都处于延时中 */
}
}
else if(pxCurrentTCB == &Task2TCB)
{
if(Task1TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task1TCB;
}
else if(pxCurrentTCB->xTicksToDelay != 0)
{
pxCurrentTCB = &IdleTaskTCB;
}
else
{
return; /* 返回,不进行切换,因为两个线程都处于延时中 */
}
}
}
}
PendSV中断函数
位置:在port.c文件中定义;
/*
*************************************************************************
* PendSV_Handler
*************************************************************************
*/
__asm void xPortPendSVHandler( void )
{
// extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
/* 当进入PendSVC Handler时,上一个任务运行的环境即:
xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
这些CPU寄存器的值会自动保存到任务的栈中,剩下的r4~r11需要手动保存 */
/* 获取任务栈指针到r0 */
mrs r0, psp
isb
ldr r3, =pxCurrentTCB /* 加载pxCurrentTCB的地址到r3 */
ldr r2, [r3] /* 加载pxCurrentTCB到r2 */
stmdb r0!, {r4-r11} /* 将CPU寄存器r4~r11的值存储到r0指向的地址 */
str r0, [r2] /* 将任务栈的新的栈顶指针存储到当前任务TCB的第一个成员,即栈顶指针 */
stmdb sp!, {r3, r14} /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,
调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护;
R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护 */
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY /* 进入临界段 */
msr basepri, r0
dsb
isb
bl vTaskSwitchContext /* 调用函数vTaskSwitchContext,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */
mov r0, #0 /* 退出临界段 */
msr basepri, r0
ldmia sp!, {r3, r14} /* 恢复r3和r14 */
ldr r1, [r3]
ldr r0, [r1] /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
ldmia r0!, {r4-r11} /* 出栈 */
msr psp, r0
isb
bx r14 /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、
使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,
然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,
当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/
nop
}