目录
1. 空闲任务详解
1.1 空闲任务简介
1.2 空闲任务的创建
1.3 空闲任务函数
2. 空闲任务钩子函数详解
2.1 钩子函数
2.2 空闲任务钩子函数
3. 空闲任务钩子函数实验
3.1 main.c
空闲任务是 FreeRTOS 必不可少的一个任务,其他 RTOS 类系统也有空闲任务,比如 UCOS。顾名思义,空闲任务就是处理器空闲的时候去运行的一个任务,当系统中没有其他就绪任务的时候空闲任务就会开始运行,空闲任务最重要的作用就是让处理器在无事可做的时候找点事做,防止处理器无聊,因此,空闲任务的优先级必然是最低的。当然是实际中,必然不会如此浪费宝贵的处理器资源,FreeRTOS 空闲任务中也会执行一些其他的处理。
1. 空闲任务详解
1.1 空闲任务简介
当 FreeRTOS 的调度器启动以后就会自动的创建一个空闲任务,这样就可以确保至少有一个任务可以运行。但是这个空闲任务使用最低优先级,如果应用中有其他高优先级任务处于就绪态的话这个空闲任务就不会跟高优先级的任务抢占 CPU 资源。空闲任务还有另外一个重要的职责,如果某个任务要调用函数 vTaskDelete() 删除自身,那么这个任务的任务控制块 TCB 和任务堆栈等这些由 FreeRTOS 系统自动分配的内存需要在空闲任务中释放掉,如果删除的是别的任务那么相应的内存就会被直接释放掉,不需要在空闲任务中释放。因此,一定要给空闲任务执行的机会!除此以外空闲任务就没有什么特别重要的功能了,所以可以根据实际情况减少空闲任务使用 CPU 的时间(比如,当 CPU 运行空闲任务的时候使处理器进入低功耗模式)。
用户可以创建与空闲任务优先级相同的应用任务,当宏 configIDLE_SHOULD_YIELD 为 1 的话应用任务就可以使用空闲任务的时间片,也就是说空闲任务会让出时间片给同优先级的应用程序。
1.2 空闲任务的创建
当调用函数 vTaskStartScheduler() 启动任务调度器的时候此函数就会自动创建空闲任务,代码如下:
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
//创建空闲任务,使用最低优先级
#if( configSUPPORT_STATIC_ALLOCATION == 1 ) (1) 使用静态方法创建空闲任务
{
StaticTask_t *pxIdleTaskTCBBuffer = NULL;
StackType_t *pxIdleTaskStackBuffer = NULL;
uint32_t ulIdleTaskStackSize;
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer,
&ulIdleTaskStackSize );
xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
"IDLE",
ulIdleTaskStackSize,
( void * ) NULL,
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
pxIdleTaskStackBuffer,
pxIdleTaskTCBBuffer );
if( xIdleTaskHandle != NULL )
{
xReturn = pdPASS;
}
else
{
xReturn = pdFAIL;
}
}
#else (2) 使用动态方法创建空闲任务,空闲任务的任务函数为 prvIdleTask(),任务堆栈大小为 configMINIMAL_STACK_SIZE,任务堆栈大小可以在 FreeRTOSConfig.h 中修改。任务优先级为 tskIDLE_PRIORITY,宏 tskIDLE_PRIORITY 为 0,说明空闲任务优先级最低,用户不能随意修改空闲任务的优先级!
{
xReturn = xTaskCreate( prvIdleTask,
"IDLE",
configMINIMAL_STACK_SIZE,
( void * ) NULL,
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
&xIdleTaskHandle );
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
/*********************************************************************/
/**************************省略其他代码*******************************/
/*********************************************************************/
}
1.3 空闲任务函数
空闲任务的任务函数为 prvIdleTask(),但是实际上是找不到这个函数的,因为它是通过宏定义来实现的,在文件 portmacro.h 中有如下宏定义:
#define portTASK_FUNCTION(vFunction,pvParameters) void vFunction(void *pvParameters)
其中,portTASK_FUNCTION() 在文件 tasks.c 中有定义,它就是空闲任务的任务函数,源码如下:
static portTASK_FUNCTION( prvIdleTask, pvParameters ) (1) 将此行展开就是 static void prvIdleTask(void *pvParameters),创建空闲任务的时候任务函数名就是 prvIdleTask()
{
( void ) pvParameters; //防止报错
//本函数为 FreeRTOS 的空闲任务任务函数,当任务调度器启动以后空闲任务会自动创建
for( ;; )
{
//检查是否有任务要删除自己,如果有的话就释放这些任务的任务控制块 TCB 和
//任务堆栈的内存
prvCheckTasksWaitingTermination(); (2) 调用函数 prvCheckTasksWaitingTermination 检查是否有需要释放内存的被删除任务,当有任务调用函数 vTaskDelete() 删除自身的话,此任务就会添加到列表 xTasksWaitingTermination 是否为空,如果不为空的话就依次将列表中所有任务对应的内存释放掉(任务控制块 TCB 和任务堆栈的内存)
#if ( configUSE_PREEMPTION == 0 )
{
//如果没有使用抢占式内核的话就强制进行一次任务切换查看是否有其他
//任务有效,如果有使用抢占式内核的话就不需要这一步,因为只要有任
//何任务有效(就绪)之后都会自动的抢夺 CPU 使用权
taskYIELD();
}
#endif /* configUSE_PREEMPTION */
#if ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) ) (3) 使用抢占式内核并且 configIDLE_SHOULD_YIELD 为 1,说明空闲任务需要让出时间片给同优先级的其他就绪任务
{
//如果使用抢占式内核并且使能时间片调度的话,当有任务和空闲任务共享
//一个优先级的时候,并且此任务处于就绪态的话空闲任务就应该放弃本时
//间片,将本时间片剩余的时间让给这个就绪任务。如果在空闲任务优先级
//下的就绪列表中有多个用户任务的话就执行这些任务。
if( listCURRENT_LIST_LENGTH( (4) 检查优先级为 tskIDLE_PRIORITY(空闲任务优先级)的就绪任务列表是否为空,如果不为空的话就调用函数 taskYIELD() 进行一次任务切换。
&( pxReadyTasksLists[ tskIDLE_PRIORITY ] ) )> ( UBaseType_t ) 1 )
{
taskYIELD();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
#if ( configUSE_IDLE_HOOK == 1)
{
extern void vApplicationIdleHook( void );
//执行用户定义的空闲任务钩子函数,注意!钩子函数里面不能使用任何
//可以引起阻塞空闲任务的 API 函数。
vApplicationIdleHook(); (5) 如果使能了空闲任务钩子函数的话就执行这个钩子函数,空闲任务钩子函数的函数名为 vApplicationIdleHook(),这个函数需要用户自行编写!在编写这个钩子函数的时候一定不能调用任何可以阻塞空闲任务的 API 函数。
}
#endif /* configUSE_IDLE_HOOK */
//如果使能了 Tickless 模式的话就执行相关的处理代码
#if ( configUSE_TICKLESS_IDLE != 0 ) (6) configUSE_TICKLESS_IDLE 不为 0,说明使能了 FreeRTOS 的低功耗 Tickless 模式。
{
TickType_t xExpectedIdleTime;
xExpectedIdleTime = prvGetExpectedIdleTime(); (7) 调用函数 prvGetExpectedIdleTime() 获取处理器进入低功耗模式的时长,此值保存在变量 xExpectedIdleTime 中,单位为时钟节拍数
if( xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP ) (8) xExpectedIdleTime 的值要大于 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 才有效
{
vTaskSuspendAll(); (9) 处理 Tickless 模式,挂起任务调度器,其实就是起到临界段代码保护功能
{
//调度器已经被挂起,重新采集一次时间值,这次的时间值可以
//使用
configASSERT( xNextTaskUnblockTime >= xTickCount );
xExpectedIdleTime = prvGetExpectedIdleTime(); (10) 重新获取一次时间值,这次的时间值是直接用于 portSUPPRESS_TICKS_AND_SLEEP() 的
if( xExpectedIdleTime >=
configEXPECTED_IDLE_TIME_BEFORE_SLEEP )
{
traceLOW_POWER_IDLE_BEGIN();
portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime ); (11) 调用 portSUPPRESS_TICKS_AND_SLEEP() 进入低功耗 Tickless 模式
traceLOW_POWER_IDLE_END();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
( void ) xTaskResumeAll(); (12) 恢复任务调度器
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TICKLESS_IDLE */
}
}
2. 空闲任务钩子函数详解
2.1 钩子函数
FreeRTOS 中有多个钩子函数,钩子函数类似于回调函数,当某个功能(函数)执行的时候就会调用钩子函数,至于钩子函数的具体内容就由用户来编写。如果不需要使用钩子函数的话就什么也不用管,钩子函数是一个可选功能,可以通过宏定义来选择使用哪个钩子函数。
宏定义: 描述:
configUSE_IDLE_HOOK 空闲任务钩子函数,空闲任务会调用此钩子函数
configUSE_TICK_HOOK 时间片钩子函数,xTaskIncrementTick()会调用此钩子函数。此钩子函数最终会被节拍中断服务函数用,对于 STM32 来说就是
滴答定时器中断服务函数
configUSE_MALLOC_FAILED_HOOK 内存申请失败钩子函数,当使用函数 pvPortMalloc() 申请内存失败的时候就会调用此钩子函数
configUSE_DAEMON_TASK_STARTUP_HOOK 守护(Daemon)任务启动钩子函数,守护任务也就是定时器服务任务
钩子函数的使用方法基本相同,用户使能相应的钩子函数,然后自行根据实际需求编写钩子函数的内容。
2.2 空闲任务钩子函数
在每个空闲任务运行周期都会调用空闲任务钩子函数,如果想在空闲任务优先级下处理某个任务有两种选择:
1. 在空闲任务钩子函数中处理任务
不管什么时候都要保证系统中至少有一个任务可以运行,因此绝对不能在空闲任务钩子函数中调用任何可以阻塞空闲任务的 API 函数,比如 vTaskDelay(),或者其他带有阻塞时间的信号量或队列操作函数。
2. 创建一个与空闲任务优先级相同的任务
创建一个任务是最好的解决方法,但是这种方法也消耗更多的 RAM。
要使用空闲任务钩子函数首先要在 FreeRTOSConfig.h 中将宏 configUSE_IDLE_HOOK 改为 1,然后编写空闲任务钩子函数 vApplicationIdleHook()。通常在空闲任务钩子函数中将处理器设置为低功耗模式来节省电能,为了与 FreeRTOS 自带的 Tickless 模式做区别,这里将这种低功耗的实现方法称之为通用低功耗模式。(因为几乎所有的 RTOS 系统都可以使用这种方法实现低功耗)。
上图中总共有三个任务,它们分别是一个空闲任务(Idle),两个用户任务(Task1 和 Task2),其中空闲任务一共运行了三次,分别为(1)、(2)和(3),其中 T1 到 T12 是 12 个时刻,下面我们分别从这两种低功耗的实现方法去分析一下整个过程。
1. 通用低功耗模式
如果使用通用低功耗模式的话每个滴答定时器中断都会将处理器从低功耗模式中唤醒,以(1)为例,在 T2 时刻处理器从低功耗模式中唤醒,但是接下来由于没有就绪的其他任务所以处理器又再一次进入低功耗模式。T2、T3 和 T4 这三个时刻都一样,反复的进入低功耗、退出低功耗,最理想的情况应该是从 T1 时刻就进入低功耗,然后在 T5 时刻退出。
在(2)中空闲任务只工作了两个时钟节拍,但是也执行了低功耗模式的进入和退出,显然这个意义不大,因为进出低功耗也是需要时间的。
(3)中空闲任务在 T12 时刻被某个外部中断唤醒,中断的具体处理过程在任务 2 (使用信号量实现中断与任务之间的同步)。
2. 低功耗 Tickless 模式
在(1)中的 T1 时刻处理器进入低功耗模式,在 T5 时刻退出低功耗模式。相比通用低功耗模式少了 3 次进出低功耗模式的操作。
在(2)中由于空闲任务只运行了两个时钟节拍,所以就没必要进入低功耗模式。说明在 Tickless 模式中只有空闲任务要运行时间超过某个最小阈值的时候才会进入低功耗模式,此阈值通过 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 来设置。
(3)中的情况和通用低功耗模式一样。
可以看出相对于通用低功耗模式,FreeRTOS 自带的 Tickless 模式更加合理有效,所以如果有低功耗设计需求的话大家尽量使用 FreeRTOS 自带的 Tickless 模式。
3. 空闲任务钩子函数实验
本实验学习如何在 FreeRTOS 空闲任务钩子函数中实现低功耗。在空闲任务钩子函数中使用 WFI 指令使处理器进入睡眠模式。
3.1 main.c
#include "stm32f4xx.h"
#include "FreeRTOS.h" //这里注意必须先引用FreeRTOS的头文件,然后再引用task.h
#include "task.h" //存在一个先后的关系
#include "LED.h"
#include "LCD.h"
#include "Key.h"
#include "usart.h"
#include "delay.h"
#include "string.h"
#include "beep.h"
#include "malloc.h"
#include "timer.h"
#include "queue.h"
#include "semphr.h"
//任务优先级
#define START_TASK_PRIO 1 //用于创建其他两个任务
//任务堆栈大小
#define START_STK_SIZE 256
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);
//任务优先级
#define TASK1_TASK_PRIO 2 //控制 LED0 闪烁,提示系统正在运行
//任务堆栈大小
#define TASK1_STK_SIZE 256
//任务句柄
TaskHandle_t Task1Task_Handler;
//任务函数
void task1_task(void *pvParameters);
//任务优先级
#define DATAPROCESS_TASK_PRIO 3 //指令处理函数
//任务堆栈大小
#define DATAPROCESS_STK_SIZE 256
//任务句柄
TaskHandle_t DataProcess_Handler;
//任务函数
void DataProcess_task(void *pvParameters);
//二值信号量句柄
SemaphoreHandle_t BinarySemaphore; //二值信号量句柄
//用于命令解析用的命令值
#define LED1ON 1
#define LED1OFF 2
#define BEEPON 3
#define BEEPOFF 4
#define COMMANDERR 0xFF
//进入低功耗模式前需要处理的事件
//ulExpectedIdleTime:低功耗模式运行时间
void PreSleepProcessing(void) //因为二值信号量实验用到了串口,所以对GPIOB~H时钟不使能!
{
//关闭某些低功耗模式下不使用的外设时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,DISABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC,DISABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD,DISABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE,DISABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF,DISABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG,DISABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOH,DISABLE);
}
//退出低功耗模式以后需要处理的事情
//ulExpectedIdleTime:低功耗模式运行时间
void PostSleepProcessing(void)
{
//退出低功耗模式以后打开那些被关闭的外设时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC,ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD,ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE,ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF,ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG,ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOH,ENABLE);
}
//空闲任务钩子函数
void vApplicationIdleHook(void)
{
__disable_irq();
__dsb(portSY_FULL_READ_WRITE );
__isb(portSY_FULL_READ_WRITE );
PreSleepProcessing(); //进入睡眠模式之前需要处理的事情
__wfi(); //进入睡眠模式
PostSleepProcessing(); //退出睡眠模式之后需要处理的事情
__dsb(portSY_FULL_READ_WRITE );
__isb(portSY_FULL_READ_WRITE );
__enable_irq();
}
//函数 LowerToCap 用于将串口发送过来的命令中的小写字母统一转换成大写字母,
//这样就可以在发送命令的时候不用区分大小写,因为开发板会统一转换成大写。
//将字符串中的小写字母转换为大写
//str:要转换的字符串
//len:字符串长度
void LowerToCap(u8 *str,u8 len)
{
u8 i;
for(i=0;i<len;i++)
{
//判断字符串的ASCII码是否位于96到123之间
if((96<str[i])&&(str[i]<123)) //小写字母
{
//ASCII码是一种用于表示字符的编码系统。在ASCII码中,每个字符都被赋予一个唯一的整数值。
//大写字母的ASCII码值是65到90
//小写字母的ASCII码值是97到122 所以一旦确定ASCII码值位于小写字母的范畴内,只需要将ASCII码值减去32即可转换为大写
str[i] = str[i] - 32; //转换为大写
}
}
}
//函数 CommandProcess 用于将接收到的命令字符串转换成命令值,比如说命令“LED1ON”转换成命令值就是 0(宏LED1ON为 0)
//命令处理函数,将字符串命令转换成命令值
//str:命令
//返回值:0xFF,命令错误;其他值,命令值
u8 CommandProcess(u8 *str)
{
u8 CommandValue = COMMANDERR;
if(strcmp((char*)str,"LED1ON")==0) //strcmp 字符串比较函数
//这个函数会比较两个参数;比较时,会以字符的ASCII值进行比较
//如果str1的ASCII码值小于str2,返回一个负数;反之,返回一个正数;
//如果str1的ASCII码值等于str2,返回 0,此时,if判断语句成立
CommandValue = LED1ON; //设置的LED1ON的宏为1,也就是在串口输入1,if判断语句成立
else if(strcmp((char*)str,"LED1OFF")==0)
CommandValue = LED1OFF; //在串口输入2,if判断语句成立
else if(strcmp((char*)str,"BEEPON")==0)
CommandValue = BEEPON; //在串口输入3,if判断语句成立
else if(strcmp((char*)str,"BEEPOFF")==0)
CommandValue = BEEPOFF; //在串口输入4,if判断语句成立
return CommandValue;
}
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //设置系统中断优先级
delay_init(168);
uart_init(115200);
LED_Init();
KEY_Init();
BEEP_Init();
LCD_Init();
my_mem_init(SRAMIN); //初始化内部内存池
POINT_COLOR=RED;
LCD_ShowString(10,10,200,16,16,"ATK STM32F407");
LCD_ShowString(10,30,200,16,16,"FreeRTOS Example");
LCD_ShowString(10,50,200,16,16,"Binary Semaphore");
LCD_ShowString(10,70,200,16,16,"Command Data:");
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
}
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
//创建二值信号量,也就是创建一个长度为1的队列
BinarySemaphore = xSemaphoreCreateBinary(); //xSemaphoreCreateBinary函数为动态创建二值信号量函数
//返回 NULL,二值信号量创建失败;返回其他值,表示创建成功的二值信号量的句柄;
//所以BinarySemaphore表示创建成功的二值信号量的句柄;
//创建Task1任务
xTaskCreate((TaskFunction_t )task1_task, //任务函数
(const char* )"task1_task", //任务名称
(uint16_t )TASK1_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )TASK1_TASK_PRIO, //任务优先级
(TaskHandle_t* )&Task1Task_Handler); //任务句柄
//创建Task2任务
xTaskCreate((TaskFunction_t )DataProcess_task, //任务函数
(const char* )"DataProcess_task", //任务名称
(uint16_t )DATAPROCESS_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )DATAPROCESS_TASK_PRIO, //任务优先级
(TaskHandle_t* )&DataProcess_Handler); //任务句柄
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
//Task1任务
//控制 LED0 闪烁,提示系统正在运行
void task1_task(void *pvParameters)
{
while(1)
{
LED0=!LED0;
vTaskDelay(500); //延时500ms,也就是500个时钟节拍
}
}
//DataProcess_task函数
//指令处理任务,根据接收到的指令来控制不同的外设
void DataProcess_task(void *pvParameters)
{
u8 len=0;
u8 CommandValue=COMMANDERR;
BaseType_t err=pdFALSE;
u8 *CommandStr;
while(1)
{
err=xSemaphoreTake(BinarySemaphore,portMAX_DELAY); //获取信号量函数;返回值pdTURE,获取信号量成功;pdFALSE,获取信号量失败;
//第一个参数,要获取的信号量句柄
//第二个参数,阻塞时间,这里设置为portMAX_DEALY,译为无限等待,直至获得信号量
if(err==pdTRUE) //获取信号量成功
{
len=USART_RX_STA&0x3fff; //得到此次接收到的数据长度
//接收状态
//bit15, 接收完成标志
//bit14, 接收到0x0d
//bit13~0, 接收到的有效字节数目
CommandStr=mymalloc(SRAMIN,len+1); //申请内存 指针指向申请内存的首地址
sprintf((char*)CommandStr,"%s",USART_RX_BUF); //打印接收缓存区,把接收缓存区的数据保存到CommandStr中
CommandStr[len]='\0'; //加上字符串结尾符号
//CommandStr 是个指针,长度为len,数组是从下角标 0 开始的,所以len就表示数组的最后一个
LowerToCap(CommandStr,len); //将字符串转换成大写
CommandValue=CommandProcess(CommandStr); //命令解析,也就是获取上面定义的宏 1 2 3 4
if(CommandValue!=COMMANDERR)//if判断语句成立,表示CommandValue不等于0xFF,那也就是 LED1ON、LED1OFF、BEEPON、BEEPOFF 其中一个指令
{
printf("命令为:%s\r\n",CommandStr);
switch(CommandValue)
{
case LED1ON:
LED1=0;
break;
case LED1OFF:
LED1=1;
break;
case BEEPON:
BEEP=1;
break;
case BEEPOFF:
BEEP=0;
break;
}
}
else
{//当命令错误的时候开发板会向串口调试助手发送命令错误的提示信息
//比如我们发送 LED1_off 这个命令,串口助手会显示:无效的命令,请重新输入!!
printf("无效的命令,请重新输入!!\r\n");
}
USART_RX_STA = 0;
memset(USART_RX_BUF,0,USART_REC_LEN); //串口接收缓冲区清零
myfree(SRAMIN,CommandStr); //释放内存
}
}
}