目录
4.2.2 vTaskDelay()软件延时函数(task.c中实现)
能理解这节的前提是看懂了前面的"任务定义及切换""链表"及"临界段"这三个内容;(这节内容请务必先下载后面提供的程序,对照着程序看,不然容易理不清)
要知道使用freertos操作系统的目的是为了榨干cpu的性能,所以我们不能让cpu闲着,要让它一直执行任务。但要是任务中有延时函数,这就会导致cpu因等待延时而闲置,所以这里我们要自己实现一个软件延时函数,当任务需要延时时调用此延时函数,此时cpu会放弃当前任务的控制权转而去干其他事情,当任务延时计时完毕后,任务就重新拿回了cpu的使用权(阻塞延时)。在启动调度器时,rtos会创建一个“空闲任务”,它的任务优先级是最低的。在实际应用中,当系统进入空闲任务时,可在空闲任务中让单片机进入休眠或低功耗模式,这会降低单片机运行时的功率。
4.空闲任务与阻塞延时
4.1空闲任务的创建
其实就创建普通任务的过程一样,只不过它是在启动调度器时创建的;
4.1.1定义空闲任务栈
在main.c中定义了一个空闲任务的栈数组以及其栈大小,如下所示:
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
TCB_t IdleTaskTCB;
其中空闲任务栈大小的宏定义是在FreeRtosConfig.h头文件中:
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )
4.1.2获取空闲任务内存(main.c中实现)
在正式创建空闲任务之前,首先讲解一下vApplicationGetIdleTaskMemory"获取空闲任务内存"函数,后面创建空闲任务时马上会用到:
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize )
{
*ppxIdleTaskTCBBuffer=&IdleTaskTCB;
*ppxIdleTaskStackBuffer=IdleTaskStack;
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}
可以看到其形参分别是:"指向空闲任务控制块的指针的地址","指向空闲任务栈的指针的地址","存储任务栈大小变量的地址";(这里指针和变量都需传入其地址,为的是在函数中直接操纵指针和变量)
vApplicationGetIdleTaskMemory函数做了以下操作:
1.将指向任务控制块的指针->空闲任务的任务控制块;(由于形参接受的是指针地址,故形参引用一次之后才是指针本身,这时才能将空闲任务控制块的地址传入指针本身)
2.将指向空闲任务栈的指针->空闲任务栈数组的首位;
3.赋予传入的栈大小变量空闲任务栈大小;
可以看到,这个函数就是在获取后面创建空闲任务时所需的信息;
4.1.3创建空闲任务(task.c中实现)
空闲任务是在调度器函数中创建的,具体如下:
static volatile TickType_t xTickCount = ( TickType_t ) 0U;
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 */
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 )
{
/* 调度器启动成功,则不会返回,即不会来到这里 */
}
}
调度器中具体执行了以下操作:
1.创建三个变量,分别用于后面接收空闲任务要创建时的空闲任务的信息;
2.通过获取到的空闲任务信息,静态创建空闲任务;(xTaskCreateStatic静态创建任务函数在“任务定义及切换”那里分析过了,这里不再赘述)
3.将空闲任务插入到就绪列表的第一个根节点下;(vListInsertEnd插入根节点函数在“链表”那节分析过)
4.指定当前任务指针->任务1的任务控制块;(通过前几节的分析可以知道,当前任务指针指向哪个任务控制块,那么调度器启动后就会执行哪个任务)
5.xPortStartScheduler启动调度器,开始执行当前任务指针指向的那个任务,也就是任务1;( xPortStartScheduler内的操作在“任务定义及切换”那里分析过了,这里不再赘述);
4.2实现阻塞延时
首先介绍下TCB任务控制块,因为要实现阻塞延,所以我们在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;
typedef tskTCB TCB_t;
4.2.1systick滴答时钟寄存器配置
配置这个寄存器的目的是让系统按我们的规定好的时间计时,计时一到systick就会触发systick中断(就是单片机中的定时中断),这个寄存器的配置是在启动调度器时配置的,这里的调用顺序是:
vTaskStartScheduler()->vTaskStartScheduler()->vPortSetupTimerInterrupt()(在port.c中定义)
/* SysTick 配置寄存器,port.c中定义 */
#define portNVIC_SYSTICK_CTRL_REG ( * ( ( volatile uint32_t * ) 0xe000e010 ) )
#define portNVIC_SYSTICK_LOAD_REG ( * ( ( volatile uint32_t * ) 0xe000e014 ) )
#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
/* 时钟频率宏,FreeRTOSConfig.h中定义 */
#define configCPU_CLOCK_HZ ( ( unsigned long ) 25000000 )
#define configTICK_RATE_HZ ( ( TickType_t ) 100 )
/* SysTick 配置函数,port.c中定义 */
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 );
}
配置过程:
1.可以看到systick滴答时钟的重装计数器被配置为了0.025MHZ-1;(以现在系统的时钟频率25MHZ来看,计满0.025MHZ的数要用1ms)
2.配置systick寄存器:将计数时钟频率改为和系统时钟频率一致,启动定时中断,并使能定时器;
(systick配置完毕后,系统就开始计时,并且计时一到就进入systick中断,按现在的配置是1ms进入一次systick中断)
配置完systick寄存器后,再来看看systick中执行的操作:
void xPortSysTickHandler( void )
{
/* 关中断 */
vPortRaiseBASEPRI();
/* 更新系统时基 */
xTaskIncrementTick();
/* 开中断 */
vPortClearBASEPRIFromISR();
}
可以看到在systick中断中,它先进入了临界段(因为systick的优先级在前面被配置为了最低,防止它打断其它重要的中断(系统调度优先级应该最小保证系统实时性),但在进入systick中断后,也不能允许其他中断来打断systick中断,不然会导致延时计数不准确)。
systick中断进入临界段后,开始调用xTaskIncrementTick函数,具体如下:
void xTaskIncrementTick( void )
{
TCB_t *pxTCB = NULL;
BaseType_t i = 0;
/* 更新系统时基计数器xTickCount,xTickCount是一个在port.c中定义的全局变量 */
const TickType_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();
}
xTaskIncrementTick函数具体做了以下操作:
1.可以看到自systick配置完毕以来,每过1ms全局变量xTickCount就+1,所以它是作为系统的时基表示系统自systick配置以来系统一共运行了多长时间;(从下面仿真中也可以看出来,这个变量的值/100后和系统运行时间一致)
2.通过for循环搜寻现在每个根节点下是否有在执行阻塞延时的任务(这节在创建任务的时候,就直接挂载在了就绪列表的各个根节点下),若有则先将这个任务内的计时项xTicksToDelay -1表示已经过去了1ms,最后执行任务切换,并在pendsv中断的vTaskSwitchContext函数中找到下一次要执行的合适任务;(这个情况下的任务切换,是为了找有没有可以执行的任务存在(也就是xTicksToDelay=0),若有则转而去执行那个任务,没有则执行空闲任务(后面分析“阻塞延时函数”是会具体介绍))
4.2.2 vTaskDelay()软件延时函数(task.c中实现)
知道滴答定时器的配置之后,现在来看vTaskDelay()软件延时函数:
void vTaskDelay( const TickType_t xTicksToDelay )
{
TCB_t *pxTCB = NULL;
/* 获取当前任务的TCB */
pxTCB = pxCurrentTCB;
/* 设置延时时间 */
pxTCB->xTicksToDelay = xTicksToDelay;
/* 任务切换 */
taskYIELD();
}
vTaskDelay()软件延时函数做了以下操作:
1.设置当前任务指针指向的任务控制块软件延时的时间;
2.开始任务切换;(这个函数中会设置 SCB_ICSR寄存器的pendsv中断位,从而开启pendsv中断,具体在“任务定义及切换”分析过)
开启pendsv中断后,程序将进入pendsv中断中:
__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
}
pendsv中断中这些汇编代码在“任务定义及切换”中已经分析过了,里面主要做的就是切换任务,这里不再详细赘述;(只需要注意vTaskSwitchContext这个函数(它在这节有一个新的实现),它的功能就是寻找合适的新的要运行的任务,在阻塞延时中这个实现是为了配合任务延时函数来进行相应的任务切换的)下面将介绍这个函数:
4.2.2vTaskSwitchContext任务切换函数(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; /* 返回,不进行切换,因为两个线程都处于延时中 */
}
}
}
}
vTaskSwitchContext函数做了以下操作:(分析vTaskSwitchContext函数之前,要先知道main函数中任务调用软件延时的具体情况,这样我们通才可以通过程序执行来分析vTaskSwitchContext函数)
void Task1_Entry( void *p_arg )
{
for( ;; )
{
flag1 = 1;
vTaskDelay( 2 );
flag1 = 0;
vTaskDelay( 2 );
}
}
/* 任务2 */
void Task2_Entry( void *p_arg )
{
for( ;; )
{
flag2 = 1;
vTaskDelay( 2 );
flag2 = 0;
vTaskDelay( 2 );
}
}
1.我们在前面最开始启动调度器时,pxcurrent当前任务控制块指针指向的是任务1的TCB,所以调度器调度完之后,程序是跳转到了任务1的函数中去执行任务1中的程序;
2.如上所示执行完任务1的第一个语句flag1=1后,第一次调用vTaskDelay()函数,即进入到vTaskDelay()函数中,这时pxcurrent指向的是Task1TCB。在vTaskDelay函数中有一个任务切换的操作(它最终是在pendsv中断中执行的,在pendsv中断中vTaskSwitchContext时任务切换时选择任务的关键函数),所以我们来到vTaskSwitchContext函数中,进入到函数中即执行第2个if判断(因为此时pxcurrent指针指向的是Task1TCB1),又由于此时任务2还没有调用阻塞延时,所以它的延时量xTicksToDelay =0,故经过第二个if的嵌套判断后现在pxcurrent指向的是Task1TCB2;(vTaskDelay函数中的“任务切换”语句,是为了让cpu在任务1的阻塞延时期间去执行其他任务)
3.由于pxcurrent指向Task2TCB,所以在退出pendv中断之后开始执行的是任务2,首先执行的是flag2=1,之后任务2开始执行阻塞延时2ms;
4.由于调用vTaskDelay函数延时也是为了寻找其他可以执行的任务,并切换到那个任务,这时进入vTaskSwitchContext进入的是第3个if(因为这时pxcurrent指向的是Task2TCB),进入第3个if后,显然现在有两种情况:
第1种:任务1和2的延时还没有结束,这时返回到调用之前的地方,去执行空闲任务;
第2种:任务1延时结束,这时在systick中断中,会把将系统切换到任务1去运行;
通过仿真知道,情况属于第1种具体如下所示:
任务2中的延时函数执行完后,进入了“空闲任务中”;
系统就这样通过调用阻塞延时函数,让cpu在任务之间往复执行,所以cpu现在一刻也没有闲着,一直在忙着执行系统调度和任务,通过在仿真的逻辑分析仪中查看两个任务执行的标志位,我们可以看到如下图所示的情况:
虽然只有单核,但两个任务就像并行在执行一样;
4.3仿真程序
理解上面所有的函数操作后,一步步运行程序:
程序是由野火提供的例程:
链接:https://pan.baidu.com/s/1iy9ETH5A1DAYh_6KJKKV9g?pwd=1234
提取码:1234
本人为初学菜鸟,文章如有错误地方,感谢指正!!