江苏分销网站建设,网站建设的目的及功能,企业咨询顾问服务协议,电商如何从零做起freertos空闲任务、阻塞延时空闲任务阻塞延时SysTick实验现象阻塞态#xff1a;如果一个任务当前正在等待某个外部事件#xff0c;则称它处于阻塞态。
rtos中的延时叫阻塞延时#xff0c;即任务需要延时的时候#xff0c;会放弃CPU的使用权,进入阻塞状态。在任务阻塞的这段…
freertos空闲任务、阻塞延时空闲任务阻塞延时SysTick实验现象阻塞态如果一个任务当前正在等待某个外部事件则称它处于阻塞态。
rtos中的延时叫阻塞延时即任务需要延时的时候会放弃CPU的使用权,进入阻塞状态。在任务阻塞的这段时间CPU可以去执行其它的任务(如果其它的任务也在延时状态那么 CPU 就将运行空闲任务)当任务延时时间到重新获取 CPU 使用权任务继续运行。
空闲任务处理器空闲的时候运行的任务。当系统中没有其他就绪任务时空闲任务开始运行空闲任务的优先级是最低的。
空闲任务
定义空闲任务
#define portSTACK_TYPE uint32_t
typedef 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_t xStateListItem; /* 任务节点 */StackType_t *pxStack; /* 任务栈起始地址 */char pcTaskName[ configMAX_TASK_NAME_LEN ];/* 任务名称字符串形式 */TickType_t xTicksToDelay; /* 用于延时 */} tskTCB;
typedef tskTCB TCB_t;/*获取获取空闲任务的内存任务控制块、任务栈起始地址、任务栈大小*/
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize )
{*ppxIdleTaskTCBBufferIdleTaskTCB;//空闲任务的任务控制块*ppxIdleTaskStackBufferIdleTaskStack; //空闲任务的任务栈*pulIdleTaskStackSizeconfigMINIMAL_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 0 flag1 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时上一个任务运行的环境即xPSRPC任务入口地址R14R12R3R2R1R0任务的形参这些CPU寄存器的值会自动保存到任务的栈中剩下的r4~r11需要手动保存 *//* 获取任务栈指针到r0 */mrs r0, pspisbldr 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, r0dsbisbbl vTaskSwitchContext /* 调用函数vTaskSwitchContext寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */ mov r0, #0 /* 退出临界段 */msr basepri, r0ldmia sp!, {r3, r14} /* 恢复r3和r14 */ldr r1, [r3]ldr r0, [r1] /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/ldmia r0!, {r4-r11} /* 出栈 */msr psp, r0isbbx 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函数初始化SysTick
BaseType_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文件
#define XTAL (50000000UL) /* Oscillator frequency */
#define SYSTEM_CLOCK (XTAL / 2U)//FreeRTOSConfig.h文件
//系统时钟大小
#define configCPU_CLOCK_HZ ( ( unsigned long ) 25000000 )
//SysTick每秒中断多少次配置成10010ms中断一次
#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_HZconfigCPU_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;/* 更新系统时基计数器xTickCountxTickCount是一个在port.c中定义的全局变量 */const TickType_t xConstTickCount xTickCount 1;xTickCount xConstTickCount;//把xTickCount加1/* 扫描就绪列表中所有线程的xTicksToDelay如果不为0则减1 */for(i0; iconfigMAX_PRIORITIES; i){pxTCB ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( pxReadyTasksLists[i] ) );if(pxTCB-xTicksToDelay 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跑完了才进行任务切换如下图所示一个任务高低电平全搞完才切到下一个任务。