目录
2.2.3 中断恢复任务Type_t xTaskResumeFromISR(TaskHandle_t xTaskToResume)
2.4.2 绝对延时函数 vTaskDelayUntil()
1. 简介
1.1 任务的简介
在裸机系统中,系统的主体就是 main 函数里面顺序执行的无限循环,这个无限循环里面 CPU 按照顺序完成各种事情。
在FreeRTOS中,根据功能的不同,把整个系统分割成一个个独立且无限循环、无法返回的函数,这个函数就称为任务。
1.2 任务调度器
在FreeRTOS中任何时刻,只有一个任务得到运行,对于哪一个任务先执行是由FreeRTOS调度器决定的。调度器会不断的启动、停止每一个任务,宏观看上去所有的任务都在同时在执行。
FreeRTOS 中的任务是抢占式调度机制,高优先级的任务可打断低优先级任务,低优先级任务必须在高优先级任务阻塞或结束后才能得到调度。
1.3 任务状态
任务的状态通常分为以下四种状态:就绪(Ready)、运行(Running)、阻塞(Blocked)、挂起态(Suspended)。
就绪态 (ready):该任务在就绪列表中,就绪的任务已经具备执行的能力,只等待调度器进行调度,新创建的任务会初始化为就绪态。
运行态 (running):该状态表明任务正在执行,此时它占用处理器,FreeRTOS调度器选择运行的永远是处于最高优先级的就绪态任务,当任务被运行的一刻,它的任务状态就变成了运行态。
阻塞态 (blocked):正在运行的任务发生阻塞 (延时、读信号量等待、读写队列或者等待读写事件) 时,该任务会从就绪列表中删除,任务状态由运行态变成阻塞态。这种状态下,进程或线程不会主动进行CPU调度,但是它仍然处于调度器的管理下,并且可以在事件发生后立即被重新调度到运行态。
挂起态 (suspended):处于挂起态的任务对调度器而言是不可见的,让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend() 函数;而把一个挂起状态的任务恢复 的唯一途径就是调用 vTaskResume() 或 vTaskResumeFromISR() 函数。
挂起态与阻塞态的区别:
当任务有较长的时间不允许运行的时候,我们可以挂起任务,这样调度器就不会管这个任务的任何信息,直到我们调用恢复任务的 API 函数;
而任务处于阻塞态的时候,系统还需要判断阻塞态的任务是否超时,是否可以解除阻塞。简单来说,
挂起态:任务的挂起和恢复,需要主动调用相关API,一旦挂起程序就不在管这个任务,直到主动调取API恢复。
阻塞态:阻塞态是由于某些事件的等待而被动进入的,进入后程序还要时不时查看一下他(调度器可以追踪其状态),并在事件发生时尽快唤醒。
1.4 任务状态转换
① 创建任务→就绪态 (ready):任务创建完成后进入就绪态,表明任务已准备就绪,随时可以运行,只等待调度器进行调度。
② 就绪态→运行态 (running):发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运行态。
③ 运行态→就绪态:有更高优先级任务创建或者恢复后,会发生任务调度,此刻就绪列表中最高优先级任务变为运行态,那么原先运行的任务由运行态变为就绪态,依然在就绪列表中,等待最高优先级的任务运行完毕,继续运行原来的任务 (此处可以看做是CPU使用权被更高优先级的任务抢占了)。
④ 运行态→阻塞态 (blocked):正在运行的任务发生阻塞 (延时、读信号量等待) 时,该任务会从就绪列表中删除,任务状态由运行态变成阻塞态,然后发生任务切换,运行就绪列表中当前最高优先级任务。
⑤ 阻塞态→就绪态:阻塞的任务被恢复后 (任务恢复、延时时间超时、读信号量超时 或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态;如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。
⑥⑦⑧ 就绪态、阻塞态、运行态→挂起态 (suspended):任务可以通过调用 vTaskSuspend() API 函数都可以将处于任何状态的任务挂起,被挂起的任务得不到CPU的使用权,也不会参与调度,除非它从挂起态中解除。
⑨ 挂起态→就绪态:把一个挂起状态的任务恢复的唯一途径就是调用 vTaskResume() 或 vTaskResumeFromISR() API 函数,如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生 任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。
2. 常用任务API函数
2.1 任务的挂起函数
2.1.1 挂起指定任务vTaskSuspend()
void vTaskSuspend(TaskHandle_t xTaskToSuspend) | ||
参数: | xTaskToSuspend | 要挂起的任务的任务句柄,创建任务的时候会为每个任务分配一个任务句柄。如果使用函数xTaskCreate()创建任务的话那么函数的参数pxCreatedTask就是此任务的任务句柄,如果使用函数 xTaskCreateStatic()创建任务的话那么函数的返回值就是此任务的任务句柄。也可以通过函数xTaskGetHandle()来根据任务名字来获取某个任务的任务句柄。 注意!如果参数为NULL的话表示挂起任务自己。 |
返回值 | 无 |
任务可以通过调用 vTaskSuspend() 函数都可以将处于任何状态的任务挂起,被挂起的任务得不到 CPU 的使用权,也不会参与调度,它相对于调度器而言是不可见的,除非它从挂起态中解除。
//KEY任务函数
void key_task(void *pvParameters)
{
u8 key=0;
while(1)
{
key=KEY_Scan(0);
if(key==KEY_UP_PRESS)
{
printf("挂起LED任务!\n");
vTaskSuspend(LED2Task_Handler);/* 挂起LED任务 */
printf("挂起LED任务成功!\n");
}
else if(key==KEY1_PRESS)
{
printf("恢复LED任务!\n");
vTaskResume(LED2Task_Handler);/* 恢复LED任务!*/
printf("恢复LED任务成功!\n");
}
vTaskDelay(20);
}
}
记住是任何状态,其中也包括自己本身,但是在挂起自身的时候会进行一次任务上下文切换,需要挂起自身就将 xTaskToSuspend 设置为NULL传递进来即可。
static TaskHandle_t LED_Task_Handle = NULL; /*LED任务句柄*/
static void KEY_Task(void* parameter)
{
while (1) {
if ( Key_Scan(KEY1_GPIO_PORT, KEY1_GPIO_PIN) == KEY_ON ) {
/*K1被按下*/
printf("挂起 LED 任务!\n");
vTaskSuspend(LED_Task_Handle); /*挂起LED任务*/
}
vTaskDelay(20); /*延时20个tick*/
}
}
2.1.2 挂起全部任务vTaskSuspendALL()
挂起全部任务,实际上就是挂起任务调度器。需要搭配vTaskResumeALL()一起使用,vTaskSuspendALL()被调用多少次,vTaskResumeALL()就要调用多少次。
void vTaskSuspendALL(void)
{
++uxSchedulerSuspended;
}
2.2 任务的恢复函数
2.2.1 恢复指定任务vTaskResume()
void vTaskResume(TaskHandle_t xTaskToResume) | ||
参数: | xTaskToResume | 要恢复任务的任务句柄 |
返回值: | 无 |
任务恢复就是让挂起的任务重新进入就绪状态,恢复的任务会保留挂起前的状态信息,在恢复的时候根据挂起时的状态,继续运行。
如果被恢复任务在所有就绪态任务中,处于最高优先级列表的第一位,那么系统将进行任务上下文的切换。
//KEY任务函数
void key_task(void *pvParameters)
{
u8 key=0;
while(1)
{
key=KEY_Scan(0);
if(key==KEY_UP_PRESS)
{
printf("挂起LED任务!\n");
vTaskSuspend(LED2Task_Handler);/* 挂起LED任务 */
printf("挂起LED任务成功!\n");
}
else if(key==KEY1_PRESS)
{
printf("恢复LED任务!\n");
vTaskResume(LED2Task_Handler);/* 恢复LED任务!*/
printf("恢复LED任务成功!\n");
}
vTaskDelay(20);
}
}
2.2.2 恢复全部任务vTaskResumeALL()
需要搭配vTaskSuspendALL()使用,vTaskSuspendALL()调用多少次,vTaskResumeALL()就要调用多少次。
2.2.3 中断恢复任务Type_t xTaskResumeFromISR(TaskHandle_t xTaskToResume)
Type_t xTaskResumeFromISR(TaskHandle_t xTaskToResume) | ||
参数: | xTaskToResume | 要恢复任务的任务句柄 |
返回值: | pdTRUE:恢复运行的任务的任务优先级等于或者高于正在运行的任务(被中断打断的任务),这意味着在退出中断服务函数以后必须进行一次上下文切换。 pdFALSE:恢复运行的任务的任务优先级低于当前正在运行的任务(被中断打断的 任务),这意味着在退出中断服务函数的以后不需要进行上下文切换。 |
2.3 删除任务函数 vTaskDelete()
对于删除任务函数:
(1) 当一个任务删除另外一个任务时,形参为 要删除任务 创建时返回的任务句柄;
(2) 如果是删除自身, 则形参为 NULL。
/* 删除任务本身 */
vTaskDelete( NULL );
/* 在其他任务删除 DeleteTask 任务 */
vTaskDelete( DeleteHandle );
2.4 任务的延时函数
2.4.1 相对延时函数 vTaskDelay()
阻塞延时的阻塞,是指任务调用该延时函数后,任务会被剥离 CPU 使用权,然后进入阻塞状态,直到延时结束,任务重新获取 CPU 使用权才可以继续运行。在任务阻塞的这段时间,CPU可以去执行其它的任务,如果其它的任务也在延时状态,那么 CPU就将运行空闲任务。
void vTaskDelay(const TickType_t xTicksToDelay)
延时的时长由形参xTicksToDelay决定,单位为系统节拍周期, 比如系统的时钟节拍周期为 1ms,那么调用 vTaskDelay(1) 的延时时间则为 1ms。
相对延时的意思是:想要延时100ms,那么这100ms是调用vTaskDelay() 之后开始计时的,经过100个系统节拍周期完成计时。因此,vTaskDelay() 并不适用与周期性执行任务的场合。
除此之外,其它任务和中断活动,也会影响到 vTaskDelay() 的调用(比如调用前,高优先级任务抢占了当前任务),进而影响到任务的下一次执行的时间。
void vTaskA( void * pvParameters )
{
while (1) {
// ...
// 这里为任务主体代码
// ...
/* 调用相对延时函数, 阻塞 1000 个 个 tick */
vTaskDelay( 1000 );
}
}
2.4.2 绝对延时函数 vTaskDelayUntil()
绝对延时函数,常用于较精确的周期运行任务,比如我有一个任务,希望它以固定频率定期执行,而不受外部的影响,任务从上一次运行开始到下一次运行开始的时间间隔是绝对的,而不是相对的。
void vTaskDelayUntil(TickType_t* const pxPreviousWakeTime,const TickType_t xTimeIncrement);
void vTaskA( void * pvParameters )
{
/* 用于保存上次时间。调用后系统自动更新 */
static portTickType PreviousWakeTime;
/* 设置延时时间,将时间转为节拍数 */
const portTickType TimeIncrement = pdMS_TO_TICKS(1000);
/* 获取当前系统时间 */
PreviousWakeTime = xTaskGetTickCount();
while (1) {
/* 调用绝对延时函数, 任务时间间隔为 1000 个 个 tick */
vTaskDelayUntil( &PreviousWakeTime ,TimeIncrement );
// ...
// 这里为任务主体代码
// ...
}
}
3. 代码编写
我们不在一步一步移植了,前面有移植过程:
这里直接使用移植好的动态任务文件:
3.1 初始化按键
key.c文件:
#include "key.h"
#include "SysTick.h"
#include "FreeRTOS.h"
#include "task.h"
/*******************************************************************************
* 函 数 名 : KEY_Init
* 函数功能 : 按键初始化
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void KEY_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB1和PB11引脚初始化为上拉输入
}
/**
* 函 数:按键获取键码
* 参 数:无
* 返 回 值:按下按键的键码值,范围:0~2,返回0代表没有按键按下
* 注意事项:此函数是阻塞式操作,当按键按住不放时,函数会卡住,直到按键松手
*/
uint8_t Key_GetNum(void)
{
uint8_t KeyNum = 0; //定义变量,默认键码值为0
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) //读PB1输入寄存器的状态,如果为0,则代表按键1按下
{
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0); //等待按键松手
KeyNum = 1; //置键码为1
}
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0) //读PB11输入寄存器的状态,如果为0,则代表按键2按下
{
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0); //等待按键松手
KeyNum = 2; //置键码为2
}
return KeyNum; //返回键码值,如果没有按键按下,所有if都不成立,则键码为默认值0
}
key.h文件:
#ifndef _key_H
#define _key_H
#include "system.h"
void KEY_Init(void);
uint8_t Key_GetNum(void);
#endif
3.2 主函数任务
按键1按下实现任务2的挂起,按键2按下实现任务2的恢复:
//任务3函数
void key_task(void *pvParameters)
{
uint8_t KeyNum;
while(1)
{
KeyNum = Key_GetNum();
if(KeyNum==1)
{
printf("挂起LED任务!\n");
vTaskSuspend(LED2Task_Handler);/* 挂起LED任务 */
printf("挂起LED任务成功!\n");
}
else if(KeyNum==2)
{
printf("恢复LED任务!\n");
vTaskResume(LED2Task_Handler);/* 恢复LED任务!*/
printf("恢复LED任务成功!\n");
}
vTaskDelay(20);
}
}
宏定义声明:
//任务优先级
#define KEY_TASK_PRIO 4
//任务堆栈大小
#define KEY_STK_SIZE 50
//任务句柄
TaskHandle_t KEYTask_Handler;
//任务函数
void key_task(void *pvParameters);
再开始任务中创建key任务:
//创建key任务
xTaskCreate((TaskFunction_t )key_task,
(const char* )"key_task",
(uint16_t )KEY_STK_SIZE,
(void* )NULL,
(UBaseType_t )KEY_TASK_PRIO,
(TaskHandle_t* )&KEYTask_Handler);
运行结果:开始时led1和led2闪烁,按键1(PB1)按下,任务2挂起,led2保持当前状态(亮或者灭),按键2(PB11)按下,任务2恢复,led2恢复正常运行。