拓冰建站拓冰建站
首页 / 资讯中心 / 正文

六. 实现 空闲任务和阻塞延时

为什么需要空闲任务和阻塞延时之前我们在任务中延时使用的是软件延时其实就是让CPU空等这样也浪费了CPU我们使用CPU也是为了最大的使用CPU那么为了最大的利用CPU的性能FreeRTOS中的延时叫阻塞延时。我们要实现一个任务延时的时候该任务要放弃CPU的使用权转而让CPU去做别的事情(也就是其他的任务)。当该任务延时的时间到的时候这个任务会重新被CPU执行如果没有其他的任务执行我们会自定义一个空闲任务这个空闲任务让CPU去执行这样可以保证CPU每时每刻都在执行任务实际上在应用的时候我们一般会在空闲任务中实现单片机的休眠或者低功耗等事情。目前我们这里为了学习需要实现的空闲任务只是对一个全局变量进行计数。这里要说明的是该空闲任务是在哪里创建的呢它其实是 系统在启动调度器的时候创建的一个优先级最低的任务做一些系统内存的清理相关工作也可以是用户的钩子函数目前是死循环因为空闲任务是不允许阻塞的它属于系统任务保证系统每时每刻都有一个空闲任务。一、 怎么实现空闲任务1.先定义一个空闲任务的栈main.c中实现/* 在main.c中 */ /* 定义一个空闲任务栈 */ StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE]; /* 在FreeRTOSConfig.h中 */ #define configMINIMAL_STACK_SIZE ((unsigned short) 128) //单位为字2.定义空闲任务的任务控制块/* 在main.c中 */ /* 定义一个任务控制块 */ TCB_t IdleTaskTCB;3.定义空闲任务的主体函数/* 在task.c中 */ #define portTASK_FUNCTION(vFunction, pvParameters) void vFunction(void * pvParameters) static portTASK_FUNCTION(prvIdleTask, pvParameters) //等效于 void prvIdleTask(void * pvParameters) { /* 防止编译器的警告 */ (void) pvParameters; for(;;) { /* 空闲任务暂时什么都不做 */ } }4.创建空闲任务我们知道空闲任务是在启动调度器的时候实现的。/* main.c中添加 */ void vApplicationGetIdleTaskMemory(TCB_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize) { *ppxIdleTaskTCBBuffer IdleTaskTCB; *ppxIdleTaskStackBuffer IdleTaskStack; *pulIdleTaskStackSize configMINIMAL_STACK_SIZE; } /* task.c中添加 */ static TaskHandle_t xIdleTaskHandle NULL; extern TCB_t IdleTaskTCB; extern 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); /* 任务控制块 */ /* 将任务添加到就绪列表 */ vListInserEnd((pxReadyTasksLists[0]), (((TCB_t *)pxIdleTaskTCBBuffer)-xStateListItem)); /* 创建空闲任务END */ /* 手动指定第一个运行的任务 */ pxCurrentTCB Task1TCB; /* 指定调度器 */ if(xPortStartScheduler() ! pdFALSE) { /* 调度器启动成功则不会返回即不会来到这里 */ } }总结来说空闲任务的以下几个特点1CPU空闲的时候才执行2优先级最低3它做的是一些内存清理后面我们可以研究FreeRTOS的源码就知道了总之它就是做一些系统辅助性的工作用户也可以做一些钩子函数比如打印一些信息或者获取一下系统电量二、 怎么实现阻塞延时在FreeRTOS中使用vTaskDelay()函数来实现翻译过来也叫任务延时。阻塞延时的阻塞是指 任务调用该延时函数后任务会被剥离CPU的使用权然后进入阻塞状态直到延时结束任务重新获取CPU使用权才可以继续执行。假如系统有任务1 任务2 还有一个空闲任务。任务1调用了vTaskDelay()函数则任务1就会放弃CPU的使用权此时任务1处于阻塞状态CPU就会去运行任务2的内容若此时任务2也处于阻塞状态那么CPU就会去运行空闲任务。空闲任务是无阻塞的即不允许被阻塞。第一步就是需要在任务控制块中添加一个成员变量xTicksToDelay:/* 在FreeRTOS.h中实现 */ 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;它的单位是SysTick的一个周期那么SysTick是怎么来的接下来我们要修改vTaskSwitchContext()函数原先我们是人为的轮流切换不能这样因为当前系统多了一个空闲任务假如任务1和任务2都被阻塞这是要运行空闲任务而不是在这两个任务之间切换。以下代码我们要好好理解其思路/* task.c中添加 */ extern TCB_t IdleTaskTCB; #if 0 void vTaskSwitchContext(void) { /* 两个任务轮流切换 */ if(pxCurrentTCB Task1TCB) { pxCurrentTCB Task2TCB; } else { pxCurrentTCB Task1TCB; } } #else 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; /* 返回不进行切换因为两个线程都处于延时中 */ } } } } #endif接下来我们实现一下任务的延时也就是vTaskDelay()函数其实也很简单就几行代码/* 在task.c中添加 */ void vTaskDelay(const TickType_t xTicksToDelay) { TCB_t *pxTCB NULL; /* 获取当前任务的TCB */ pxTCB pxCurrentTCB; /* 设置延时时间 */ pxTCB-xTicksToDelay xTicksToDelay; /* 任务切换 */ taskYIELD(); } /* 这里我们要把该函数提前在task.h中声明一下方便后面调用它*/ void vTaskDelay(const TickType_t xTicksToDelay)如果一个任务要延时一开始xTickToDelay肯定不为0xTickToDelay自减1当xTickToDelay变为0的时候表示延时结束。那么xTickToDelay是以什么周期在递减在哪里递减在FreeRTOS中这个周期由SysTick中断提供操作系统里面的最小的时间单位就是SysTick的中断周期我们称之为一个TickSysTick中断服务函数在port.c中实现即函数xPortSysTickHandler()./* 在port.c中添加 */ /* ************************************************************************* * SysTick中断服务函数 ************************************************************************* */ void xPortSysTickHandler( void ) { /* 关中断 */ vPortRaiseBASEPRI(); /* 更新系统时基 */ xTaskIncrementTick(); /* 开中断 */ vPortClearBASEPRIFromISR(); }/* 在portmacro.h中添加 */ /*-----------------------------------------------------------*/ static portFORCE_INLINE void vPortClearBASEPRIFromISR( void ) { __asm { /* Set BASEPRI to 0 so no interrupts are masked. This function is only used to lower the mask in an interrupt, so memory barriers are not used. */ msr basepri, #0 } } /*-----------------------------------------------------------*//* 在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; /* 扫描就绪列表中所有线程的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(); } /* 在task.h中声明 */ void xTaskIncrementTick( void );我们已经实现了SysTick的中断那么我们在哪里去启动SysTick呢也就是初始化SysTick我们知道SysTick是一个内核外设所以他也是与内核相关的我们也放到port.c文件里。在FreeRTOS中对SysTick初始化是直接操作寄存器的比如说直接设置SysTick的重装载寄存器的值。/* 在port.c中添加放在SysTick中断服务函数之前*/ /* ************************************************************************* * 初始化SysTick ************************************************************************* */ void vPortSetupTimerInterrupt( void ) //该函数将会在xPortStartScheduler()中被调用 { /* 设置重装载寄存器的值 */ 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 ); } /* 里面有宏定义我们还要提前声明一下 在port.c中 */ /* SysTick 配置寄存器 */ #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 #define portNVIC_SYSTICK_INT_BIT ( 1UL 1UL ) #define portNVIC_SYSTICK_ENABLE_BIT ( 1UL 0UL ) /* 在FreeRTOSConfig.h中声明 */ #define configCPU_CLOCK_HZ ( ( unsigned long ) 25000000 ) //也可以使用system.ARMCM4.c(Startup) #define configTICK_RATE_HZ ( ( TickType_t ) 100 ) //表示SysTick每秒中断多少次关于寄存器的地址和每个位的功能我也摘出来了供大家参考学习寄存器部分不做重点怎么将vPortSetupTimerInterrupt()插入到 启动调度器函数 xPortStartScheduler()函数中呢/* 在port.c中修改xPortStartScheduler()函数 */ /* 调度器启动函数 */ BaseType_t xPortStartScheduler(void) { /* 配置PendSV和SysTick的中断优先级为最低 */ portNVIC_SYSPRI2_REG | portNVIC_PENDSV_PRI; portNVIC_SYSPRI2_REG | portNVIC_SYSTICK_PRI; /* 初始化SysTick */ vPortSetupTimerInterrupt(); /* 启动第一个任务不再返回 */ prvStartFirstTask(); /* 不应该运行到这里 */ return 0; } /* 因为vPortSetupTimerInterrupt()函数在后面定义的所以起初要先声明一下 */ void vPortSetupTimerInterrupt( void );后面我们就开始进行实验的部分在main.c中我们定义好的任务函数中使用的是软件延时要切换到阻塞延时。我们这里设置的延时就简单点设置20ms。此时需要修改任务函数代码如下/* 在main.c中修改 */ //定义一个任务函数 void Task1_Entry(void *p_arg) { for(;;) { flag1 1; vTaskDelay(2); flag1 0; vTaskDelay(2); //taskYIELD(); 此时也不需要任务切换函数了 } } //定义一个任务函数 void Task2_Entry(void *p_arg) { for(;;) { flag2 1; vTaskDelay(2); flag2 0; vTaskDelay(2); //taskYIELD(); 此时也不需要任务切换函数了 } }最后我们仿真下这两个任务CPU几乎是同时运行的。使CPU的运行效率得到很大的提高至此实现完成成功你对FreeRTOS的理解又进了一大步
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门