freertos实时操作系统空闲任务阻塞延时示例解析
阻塞态:如果一个任务当前正在等待某个外部事件,则称它处于阻塞态。
rtos中的延时叫阻塞延时,即任务需要延时的时候,会放弃CPU的使用权,进入阻塞状态。在任务阻塞的这段时间,CPU可以去执行其它的任务(如果其它的任务也在延时状态,那么 CPU 就将运行空闲任务),当任务延时时间到,重新获取 CPU 使用权,任务继续运行。
空闲任务:处理器空闲的时候,运行的任务。当系统中没有其他就绪任务时,空闲任务开始运行,空闲任务的优先级是最低的。
空闲任务
定义空闲任务:
#define portSTACK_TYPE uint32_ttypedef portSTACK_TYPE StackType_t; /*定义空闲任务的栈*/#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE]; /*定义空闲任务的任务控制块*/TCB_t IdleTaskTCB;
创建空闲任务:在vTaskStartScheduler调度器启动函数中创建。
/*任务控制块的结构体 */typedef struct tskTaskControlBlock{ volatile StackType_t*pxTopOfStack; /* 栈顶 */ ListItem_txStateListItem; /* 任务节点 */StackType_t*pxStack; /* 任务栈起始地址 */ charpcTaskName[ configMAX_TASK_NAME_LEN ]; /* 任务名称,字符串形式 */TickType_t xTicksToDelay; /* 用于延时 */} tskTCB; typedef tskTCB TCB_t; /*获取获取空闲任务的内存:任务控制块、任务栈起始地址、任务栈大小*/void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize ){*ppxIdleTaskTCBBuffer=&IdleTaskTCB; //空闲任务的任务控制块*ppxIdleTaskStackBuffer=IdleTaskStack; //空闲任务的任务栈*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE; //栈的大小}void vTaskStartScheduler( void ){/*创建空闲任务start*/TCB_t *pxIdleTaskTCBBuffer = NULL; /* 用于指向空闲任务控制块 */StackType_t *pxIdleTaskStackBuffer = NULL; /* 用于空闲任务栈起始地址 */uint32_t ulIdleTaskStackSize; /* 获取:任务控制块、任务栈起始地址、任务栈大小 */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 ){/* 调度器启动成功,则不会返回,即不会来到这里 */}}//下面是空闲任务的任务入口,看到,里面什么都没做//这个我用debug发现一直卡到这个for不动了。//通过单步运行,发生了中断,程序也无法进入中断。static portTASK_FUNCTION( prvIdleTask, pvParameters ){ /* 防止编译器的警告 */ ( void ) pvParameters; for(; ; ){/* 空闲任务暂时什么都不做 */}}
阻塞延时 任务函数如下:延时函数由软件延时替代为阻塞延时。
void Task1_Entry( void *p_arg ){ for( ; ; ) {#if 0flag1 = 1; delay( 100 ); /*软件延时*/flag1 = 0; delay( 100 ); /* 线程切换,这里是手动切换 */portYIELD(); #elseflag1 = 1; vTaskDelay( 2 ); /*阻塞延时*/flag1 = 0; vTaskDelay( 2 ); #endif }}
任务函数里面调用了vTaskDelay阻塞延时函数,如下。
/*阻塞延时函数的定义 */void vTaskDelay( const TickType_t xTicksToDelay ){TCB_t *pxTCB = NULL; /* 获取当前任务的任务控制块 */pxTCB = pxCurrentTCB; /* 设置延时时间:xTicksToDelay个SysTick延时周期 */pxTCB->xTicksToDelay = xTicksToDelay; /* 任务切换 */taskYIELD(); }
然后vTaskDelay里面调用了taskYIELD函数,如下。目的是产生PendSV中断,进入PendSV中断服务函数。
/* Interrupt control and state register (SCB_ICSR):0xe000ed04 * Bit 28 PENDSVSET: PendSV set-pending bit */#define portNVIC_INT_CTRL_REG( * ( ( volatile uint32_t * ) 0xe000ed04 ) )#define portNVIC_PENDSVSET_BIT( 1UL << 28UL )#define portSY_FULL_READ_WRITE( 15 )/* Scheduler utilities. */#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 ); \}
PendSV中断服务函数如下,里面调用了vTaskSwitchContext上下文切换函数,目的是寻找最高优先级的就绪任务,然后更新pxCurrentTCB。
__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} 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 nop}
vTaskSwitchContext上下文切换函数如下。
任务需要延时的时候,会放弃CPU的使用权,进入阻塞状态。在任务阻塞的这段时间,CPU可以去执行其它的任务(如果其它的任务也在延时状态,那么 CPU 就将运行空闲任务),当任务延时时间到,重新获取 CPU 使用权,任务继续运行。
void vTaskSwitchContext( void ){ if( pxCurrentTCB == &IdleTaskTCB )//如果当前线程是空闲线程 {if(Task1TCB.xTicksToDelay == 0)//如果线程1延时时间结束{pxCurrentTCB =&Task1TCB; //切换到线程1}else if(Task2TCB.xTicksToDelay == 0)//如果线程2延时时间结束(线程1在延时中){pxCurrentTCB =&Task2TCB; //切换到线程2}else{return; /* 线程延时均没有到期则返回,继续执行空闲线程 */} } else//当前任务不是空闲任务 {if(pxCurrentTCB == &Task1TCB)//如果当前线程是线程1{if(Task2TCB.xTicksToDelay == 0)//如果线程2不在延时中{pxCurrentTCB =&Task2TCB; //切换到线程2}else if(pxCurrentTCB->xTicksToDelay != 0)//如果线程1进入延时状态(线程2也在延时中){pxCurrentTCB = &IdleTaskTCB; //切换到空闲线程}else {return; /* 返回,不进行切换 */}}else if(pxCurrentTCB == &Task2TCB)//如果当前线程是线程2{if(Task1TCB.xTicksToDelay == 0)//如果线程1不在延时中{pxCurrentTCB =&Task1TCB; //切换到线程1}else if(pxCurrentTCB->xTicksToDelay != 0)//如果线程2进入延时状态(线程1也在延时中){pxCurrentTCB = &IdleTaskTCB; //切换到空闲线程}else {return; /* 返回,不进行切换*/}} }}
由上面代码可知,vTaskSwitchContext上下文切换函数通过看xTicksToDelay是否为零,来判断任务已经就绪or继续延时。
xTicksToDelay以什么周期递减,在哪递减。这个周期由SysTick中断提供。
SysTick SysTick是系统定时器,重装载数值寄存器的值递减到0的时候,系统定时器就产生一次中断,以此循环往复。
下面是SysTick的初始化。
//main函数里面/* 启动调度器,开始多任务调度,启动成功则不返回 */vTaskStartScheduler(); //task.c里面调用了xPortStartScheduler函数void vTaskStartScheduler( void ){//.....省略部分代码/* 启动调度器 */if( xPortStartScheduler() != pdFALSE ){/* 调度器启动成功,则不会返回,即不会来到这里 */}}//port.c里面//xPortStartScheduler调度器启动函数,里面调用了vPortSetupTimerInterrupt函数初始化SysTickBaseType_t xPortStartScheduler( void ){/* 配置PendSV 和 SysTick 的中断优先级为最低 */ portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; /* 初始化SysTick */vPortSetupTimerInterrupt(); /* 启动第一个任务,不再返回 */ prvStartFirstTask(); /* 不应该运行到这里 */ return 0; }//system_ARMCM4.c文件#defineXTAL(50000000UL)/* Oscillator frequency */#defineSYSTEM_CLOCK(XTAL / 2U)//FreeRTOSConfig.h文件//系统时钟大小#define configCPU_CLOCK_HZ( ( unsigned long ) 25000000 ) //SysTick每秒中断多少次,配置成100,10ms中断一次#define configTICK_RATE_HZ( ( TickType_t ) 100 )//下面初始化SysTick/* 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//configSYSTICK_CLOCK_HZ=configCPU_CLOCK_HZ /* 确保SysTick的时钟与内核时钟一致 */ #define portNVIC_SYSTICK_CLK_BIT ( 1UL << 2UL )//无符号长整形32位二进制,左移两位#else #define portNVIC_SYSTICK_CLK_BIT ( 0 )#endif#define portNVIC_SYSTICK_INT_BIT( 1UL << 1UL )#define portNVIC_SYSTICK_ENABLE_BIT( 1UL << 0UL )//初始化SysTick的函数如下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,下面看看SysTick的中断服务函数。
现在就明白了,xTicksToDelay是以SysTick的中断周期递减的。
// port.c文件,SysTick中断服务函数//里面调用了xTaskIncrementTick函数更新系统时基void xPortSysTickHandler( void ){ /* 关中断 进入临界段*/vPortRaiseBASEPRI(); /* 更新系统时基 */xTaskIncrementTick(); /* 开中断 退出临界段*/vPortClearBASEPRIFromISR(); }//task.c文件,static volatile TickType_t xTickCount= ( TickType_t ) 0U; void xTaskIncrementTick( void ){TCB_t *pxTCB = NULL; BaseType_t i = 0; /* 更新系统时基计数器xTickCount,xTickCount是一个在port.c中定义的全局变量 */const TickType_t xConstTickCount = xTickCount + 1; xTickCount = xConstTickCount; //把xTickCount加1/* 扫描就绪列表中所有线程的xTicksToDelay,如果不为0,则减1 */ for(i=0; ixTicksToDelay > 0){pxTCB->xTicksToDelay --; } }/* 任务切换 */portYIELD(); }
实验现象 这个里面就可以看到,高电平时间是20ms,刚好是阻塞延时的20ms。而且两个任务波形相同,好像是CPU在同时做两件事。这就是阻塞延时的好处。
为什么呢,
一开始,所有任务都没有进入延时。
当一个任务放弃CPU后(进入延时),这一瞬间,CPU立即转向运行另一个任务(另一个任务也立即进入延时)。这是因为uvTaskDelay阻塞延时函数里面调用了taskYIELD()任务切换函数。所以产生PendSV中断,进入PendSV中断服务函数xPortPendSVHandler。
在那个PendSV中断服务函数里面,调用vTaskSwitchContext上下文切换函数,由于现在两个任务都在延时过程中,就开始切到空闲任务。
等到重装载数值寄存器的值递减到0的时候,系统定时器就产生一次中断,进入系统定时器的中断函数中,改变xTicksToDelay,然后再次调用任务切换函数portYIELD()。目的是产生PendSV中断,进入PendSV中断服务函数。
然后再次调用vTaskSwitchContext上下文切换函数,判断现在两个任务是否还在延时,如果任务1不在延时,那么立即切到任务1,任务1里面又调用uvTaskDelay阻塞延时函数,再次套娃重复上面的活动。
所以波形上几乎同步。
文章图片
之前用软件延时在任务函数里面写delay(100),这就属于cpu一直跑这个delay,跑完了才进行任务切换,如下图所示,一个任务高低电平全搞完,才切到下一个任务。
文章图片
文章图片
【freertos实时操作系统空闲任务阻塞延时示例解析】以上就是freertos实时操作系统空闲任务阻塞延时示例解析的详细内容,更多关于freertos空闲任务阻塞延时的资料请关注脚本之家其它相关文章!
推荐阅读
- FreeRTOS实时操作系统特点介绍
- 操作系统的线程和进程的区别_面试官(你熟悉多线程嘛(线程跟进程有什么区别?...))
- 操作系统6----进程和线程
- python中的多线程
- 操作系统|面试必考 | 进程和线程的区别
- 实时流处理与分布式存储过程中对文件的操作
- 操作系统|完美解决Win11共享打印机连接错误0x00000709教程
- 操作系统|操作系统 --- 文件操作和IO
- 操作系统|操作系统 ---多线程(进阶)
- 操作系统|回顾2016年(我们见证了一场存储行业的厮杀)