FreeRTOS学习笔记之任务调度
一、简介
FreeRTOS 的任务(Task)是其核心概念之一,本质上是一个独立的线程或执行单元。每个任务都有自己的执行上下文(堆栈、寄存器值等),并能在 FreeRTOS 内核的调度下,以协作式或抢占式方式运行
对于单核的 CPU 而言,CPU 在同一时刻只能够处理一个任务,但是多任务系统的任务调度器会根据相关的任务调度算法,将 CPU 的使用权分配给任务,在任务获取 CPU 使用权之后的极短时间(宏观角度)后,任务调度器又会将 CPU 的使用权分配给其他任务,如此往复,在宏观的角度看来,就像是多个任务同时运行了一样
也就是说同一时间仅一个任务在运行
1.1 调度
目前,做常用的调度算法为优先级抢占调度与时间片轮转调度这两种,遇到阻塞时均会切换任务;
任务优先级数值越大,优先级越高;这与中断优先级刚好相反
对于优先级抢占调度:主要针对优先级不同的任务
例如:目前有三个处于就绪态的任务Task1,Task2,Task3,这三个任务的优先级分别为1,2,3,task3在运行2ms时有个延迟函数延迟5ms;Task2在执行1ms时有个延迟函数延迟10ms;Task1运行1ms时有个延迟函数延迟5ms
注:三个中的延迟只执行一次
0ms - 2ms: Task3运行
初始就绪任务:Task3(优先级3) > Task2(2) > Task1(1)
CPU执行最高优先级任务 Task3(运行2ms)2ms: Task3阻塞
Task3调用vTaskDelay(5)
进入阻塞态(解除阻塞时间=2ms+5ms=7ms)
就绪任务更新:Task2(2), Task1(1)
调度器选择最高优先级就绪任务 Task22ms - 3ms: Task2运行
Task2执行1ms后调用vTaskDelay(10)
(解除阻塞时间=3ms+10ms=13ms)
Task2进入阻塞态
就绪任务仅剩 Task13ms - 4ms: Task1运行
Task1执行1ms后调用vTaskDelay(5)
(解除阻塞时间=4ms+5ms=9ms)
Task1进入阻塞态
所有用户任务均阻塞4ms - 7ms: 空闲任务运行
无用户任务就绪,CPU执行优先级0的空闲任务(Idle Task)7ms: Task3解除阻塞
Task3延迟结束(7ms时刻),重新进入就绪态
因Task3优先级最高,立即抢占CPU
执行 Task3(从上次阻塞点继续运行)7ms以后: Task3独占CPU
9ms: Task1延迟结束进入就绪态,但优先级(1) < Task3(3),不抢占
13ms: Task2延迟结束进入就绪态,优先级(2) < Task3(3),不抢占
只要Task3不主动阻塞,将一直占用CPU(即使Task1/Task2已就绪)
对于时间片轮转调度:主要针对优先级相同的任务,任务调度器会在每个时间片结束时切换任务;
常用1ms进行一次中断,中断时进行任务调度,任务调度时用到PendSV中断进行任务切换
注意:若任务途中被打断或阻塞,没有用完的时间片不会再使用,下次该任务得到执行还是按照一个时间片的时钟节拍运行
举例1:Task1 在 1 ms 时调用 delay(3 ms) 阻塞,此时它的时间片只用了 1 ms,剩余 4 ms 作废。4 ms 时 Task1 就绪,再次得到调度,这时它拿到新的 5 ms 时间片,并从 delay 返回后的那一条指令继续执行,而不是重新从 Task1 的第一条语句开始。举例2:目前有三个处于就绪态的任务Task1,Task2,Task3,设定时间片为1ms,这三个任务的优先级相同,task3在运行3ms处有个延迟函数延迟1ms;Task2在执行2ms时有个延迟函数延迟1ms;Task1运行3ms时有个延迟函数延迟1ms,请问CPU如何执行这三个任务?(注:三个任务都是循环的,所有延迟函数均循环执行)
时间 运行任务 事件与状态变化 0 Task1 Task1运行1ms(累计1/3)→ 加入队列末尾
就绪队列:Task2 → Task3 → Task11 Task2 Task2运行1ms(累计1/2)→ 加入队列末尾
就绪队列:Task3 → Task1 → Task22 Task3 Task3运行1ms(累计1/3)→ 加入队列末尾
就绪队列:Task1 → Task2 → Task33 Task1 Task1运行1ms(累计2/3)→ 加入队列末尾
就绪队列:Task2 → Task3 → Task14 Task2 Task2运行1ms(累计2/2)→ 阻塞1ms(至t=5)
就绪队列:Task3 → Task15 Task3 Task2唤醒(t=5开始)→ 加入队列末尾
Task3运行1ms(累计2/3)→ 加入队列末尾
就绪队列:Task1 → Task2 → Task36 Task1 Task1运行1ms(累计3/3)→ 阻塞1ms(至t=7)
就绪队列:Task2 → Task37 Task2 Task1唤醒(t=7开始)→ 加入队列末尾
Task2运行1ms(重置后累计1/2)→ 加入队列末尾
就绪队列:Task3 → Task1 → Task28 Task3 Task3运行1ms(累计3/3)→ 阻塞1ms(至t=9)
就绪队列:Task1 → Task29 Task1 Task3唤醒(t=9开始)→ 加入队列末尾
Task1运行1ms(重置后累计1/3)→ 加入队列末尾
就绪队列:Task2 → Task3 → Task110 Task2 Task2运行1ms(累计2/2)→ 阻塞1ms(至t=11)
就绪队列:Task3 → Task111 Task3 Task2唤醒(t=11开始)→ 加入队列末尾
Task3运行1ms(重置后累计1/3)→ 加入队列末尾
就绪队列:Task1 → Task2 → Task312 Task1 Task1运行1ms(累计2/3)→ 加入队列末尾
就绪队列:Task2 → Task3 → Task113 Task2 Task2运行1ms(重置后累计1/2)→ 加入队列末尾
就绪队列:Task3 → Task1 → Task214 Task3 Task3运行1ms(累计2/3)→ 加入队列末尾
就绪队列:Task1 → Task2 → Task315 Task1 Task1运行1ms(累计3/3)→ 阻塞1ms(至t=16)
就绪队列:Task2 → Task316 Task2 Task1唤醒(t=16开始)→ 加入队列末尾
Task2运行1ms(累计2/2)→ 阻塞1ms(至t=17)
就绪队列:Task3 → Task117 Task3 Task2唤醒(t=17开始)→ 加入队列末尾
Task3运行1ms(累计3/3)→ 阻塞1ms(至t=18)
就绪队列:Task1 → Task218 Task1 Task3唤醒(t=18开始)→ 加入队列末尾
Task1运行1ms(重置后累计1/3)→ 加入队列末尾
就绪队列:Task2 → Task3 → Task119 Task2 Task2运行1ms(重置后累计1/2)→ 加入队列末尾
就绪队列:Task3 → Task1 → Task2
任务的优先级作用于调度器调度顺序
中断优先级高于所有任务
FreeRTOS推荐将中断优先级的4bit全部配置为抢占优先级
2.1 任务状态
FreeRTOS 中任务存在四种任务状态,分别为运行态、就绪态、阻塞态和挂起态。FreeRTOS运行时,任务的状态一定是这四种状态中的一种。
仅就绪态可以转变为运行态
其他状态任务的任务想运行,必须先转变为就绪态
1.3 任务优先级
任务优先级是决定任务调度器如何分配 CPU 使用权的因素之一。每一个任务都被分配一个0~ (configMAX_PRIORITIES-1)的任务优先级,宏 configMAX_PRIORITIES 在 FreeRTOSConfig.h文件中定义
对于STM32来说,由于硬件平台的限制,任务优先级最大支持32个优先级;如果不考虑硬件限制,软件优可支持无数个优先级
二、任务API
2.1 任务创建
@函数原型:
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE uxStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask//将任务当前相关信息保存在此句柄中传出
);
@函数功能:动态任务创建,在创建该任务时需要为其分配空间
@函数参数:
【1】pvTaskCode,指向任务入口函数的指针(只是实现任务的函数的名称),任务通常实现为无限循环;实现任务绝不能尝试返回或退出。函数返回值必须为void,函数参数必须为void*typedef void (* TaskFunction_t)( void * ); -> TaskFunction_t==void (*)(void *)
【2】pcName,字符串,任务的描述性名称。这主要是为了方便调试,也可以用以获取任务句柄
【3】uxStackDepth,以分配用作任务的堆栈(以字为单位)。当任务切换时,将任务当前的状态保存到自己的栈空间
【4】pvParameters,传递给任务函数的参数
【5】uxPriority,任务优先级
【6】pxCreatedTask,pxCreatedTask 用于传出任务的句柄。这个句柄将在 API 调用中对该创建出来的任务进行引用,比如改变任务优先级,或者删除任务。如果应用程序中不会用到这个任务的句柄,则pxCreatedTask 可以被设为 NULL
@函数返回:如果任务创建成功,则返回 pdPASS。
否则返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY,没有足够空间为任务栈分配。
@任务函数
void xxx_Task(void*pvParameters){
while(1)
{
……//任务
}
}
就序列表中的任务优先级相等时,最后一个被创建的任务先执行,其他任务按照创建顺序执行
2.2 任务删除
@函数原型:void vTaskDelete( TaskHandle_t xTask );
@函数功能:从 RTOS 内核管理中删除任务。删除的任务将从就绪、阻塞、挂起列表移除。
@函数参数:【1】xTask,待删除任务的句柄
要注意的是,空闲任务会负责释放被删除任务中由系统分配的内存,但是由用户在任务删除前申请的内存,则需要由用户在任务被删除前提前释放,否则将导致内存泄露。任务句柄:
typedef struct tskTaskControlBlock * TaskHandle_t;
在使用xTaskCreate创建一个任务时,会创建一个结构体struct tskTaskControlBlock,该结构体叫做任务控制块等价于Linux中的线程控制块(TCB),保存当前创建的任务信息的。
任务句柄就是任务TCB的地址
2.3 任务阻塞
@函数原型:void vTaskDelay( const TickType_t xTicksToDelay );
@函数功能:将任务延迟给定数量的时钟周期
@函数参数:【1】xTicksToDelay,任务阻塞的节拍数
2.4 任务挂起
@函数原型:void vTaskSuspend(TaskHandle_t xTaskToSuspend);@函数功能:将xTaskToSuspend指定任务进行挂起函数参数:
【1】xTaskToSuspend,待挂起的任务句柄
2.5 解除挂起
@函数原型:void vTaskResume( TaskHandle_t xTaskToResume);@函数功能:解除任务挂起函数参数:
【1】xTaskToResume,待解除挂起的任务句柄;函数返回:void
2.6 启动调度
@函数原型:void vTaskStartScheduler(void);
@函数功能:启动 RTOS 调度器。调用后,RTOS 内核可以控制执行哪些任务以及何时执行。空闲任务和计时器守护程序任务(可选)是在 RTOS 调度器启动时自动创建的。
三、应用
//创建LED3 任务,50ms翻转//创建LED4 任务,150ms翻转//创建LED5 任务,500ms翻转//创建KEY_UP 任务,LED4点亮
void LED3_Task(void*p1) ;
void LED4_Task(void*p1) ;
void LED5_Task(void*p1) ;
void KEY_UPTask(void*p1);TaskHandle_t LED3_Handler ; //创建任务句柄TaskHandle_t LED4_Handler ; //创建任务句柄TaskHandle_t LED5_Handler ; //创建任务句柄TaskHandle_t KEY_UP_Handler ; //创建任务句柄
void Start_Task(void)
{xTaskCreate( LED3_Task,"LED3_Task",200,NULL,1, &LED3_Handler ); //创建任务xTaskCreate( LED4_Task,"LED3_Task",200,NULL,1, &LED4_Handler ); //创建任务xTaskCreate( LED5_Task,"LED3_Task",200,NULL,1, &LED5_Handler ); //创建任务xTaskCreate( KEY_UPTask,"LED3_Task",200,NULL,1,&KEY_UP_Handler); //创建任务
}void LED3_Task(void*p1)
{while(1){LED3_Tog;vTaskDelay(50);}
}
void LED4_Task(void*p1)
{while(1){LED4_Tog;vTaskDelay(150);}
}
void LED5_Task(void*p1)
{while(1){LED5_Tog;vTaskDelay(500);}
}
void KEY_UPTask(void*p1)
{while(1){if(KEY_UP){vTaskDelay(30);while(KEY_UP){LED6_Tog;}}}
}int main(void)
{//1.优先级分组设置为4,全部为抢占【0~15】NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//2.系统节拍定时器配置SysTick_Config(configCPU_CLOCK_HZ/configTICK_RATE_HZ);LED_Init();Key_Init();Start_Task();vTaskStartScheduler();//开启调度
}
3.1 裁剪
对于裁剪,有两种方式,一种是有关组件的源文件,不需要的可以直接丢弃,另一种是对于功能组件,有些宏或者函数用不到
3.2 堆空间
在FreeRTOSConfig.h文件中,有关当前FreeRTOS使用的堆空间,为17*1024=0x4400,这个空间在哪里分配呢?看一下map文件
RTOS的堆空间是属于bss区(属于全局区);相当于RTOS定义了一个大小为0x4400字节的全的数组,RTOS后期的所有开销全在这个数组里;heap_4.o是该堆区空间的内存分配方案
若在STM32中使用堆区,尽量不要使用malloc/free这类函数,因为这类函数本身占用的代码量比较大,多次使用malloc/free这类函数会造成STM32堆区内存碎片化且无法得知内存是否溢出;推荐用户自己编写内存分配方案
任务被创建:
<1>在FreeRTOS管理的heap[0x4400]开启任务栈空间[栈深度*4]
<2>在FreeRTOS管理的heap中开启TCB_T大小的空间,保存创建任务的信息
<3>将任务栈空间地址赋值给pxNewTCB->pxStack=pxStack;<4>初始化任务pxNewTCB:任务优先级、任务名、任务状态列表、任务事件列表……
<5>将新任务添加到就绪列表中
3.3(TCB)结构体
TCB是FreeRTOS中用于管理任务的核心数据结构,每个任务都有一个独立的TCB,用于保存任务的状态、堆栈指针、优先级等信息
typedef struct tskTaskControlBlock
{volatile StackType_t *pxTopOfStack; /* 指向任务堆栈的栈顶 */ListItem_t xStateListItem; /* 用于将任务链接到状态列表(就绪、阻塞、挂起等) */ListItem_t xEventListItem; /* 用于将任务链接到事件列表(如事件组、队列等) */UBaseType_t uxPriority; /* 任务优先级 */StackType_t *pxStack; /* 指向任务堆栈的起始位置 */char pcTaskName[ configMAX_TASK_NAME_LEN ]; /* 任务名称,以字符串形式保存 */#if ( portSTACK_GROWTH > 0 )StackType_t *pxEndOfStack; /* 堆栈结束位置,用于堆栈溢出检测 */#endif#if ( portCRITICAL_NESTING_IN_TCB == 1 )UBaseType_t uxCriticalNesting; /* 临界区嵌套深度 */#endif#if ( configUSE_TRACE_FACILITY == 1 )UBaseType_t uxTCBNumber; /* 任务唯一标识,用于调试跟踪 */UBaseType_t uxTaskNumber; /* 任务编号,可以由用户设置 */#endif#if ( configUSE_MUTEXES == 1 )UBaseType_t uxBasePriority; /* 任务的基础优先级(用于优先级继承) */UBaseType_t uxMutexesHeld; /* 任务持有的互斥量数量 */#endif#if ( configUSE_APPLICATION_TASK_TAG == 1 )TaskHookFunction_t pxTaskTag; /* 任务标签,用于调试或用户自定义 */#endif#if ( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )void *pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ]; /* 线程本地存储指针 */#endif#if ( configGENERATE_RUN_TIME_STATS == 1 )uint32_t ulRunTimeCounter; /* 任务的运行时间统计 */#endif#if ( configUSE_NEWLIB_REENTRANT == 1 )struct _reent xNewLib_reent; /* 为每个任务分配一个newlib的reent结构体 */#endif#if ( configUSE_TASK_NOTIFICATIONS == 1 )volatile uint32_t ulNotifiedValue; /* 任务通知值 */volatile uint8_t ucNotifyState; /* 任务通知状态 */#endif#if ( configUSE_POSIX_ERRNO == 1 )int iTaskErrno; /* 任务特定的errno */#endif/* 其他与移植相关的成员 */#if ( portUSING_MPU_WRAPPERS == 1 )xMPU_SETTINGS xMPUSettings; /* MPU设置 */#endif
} tskTCB;
1.pxTopOfStack:指向任务当前堆栈的栈顶。在任务切换时,硬件上下文(寄存器)被保存到这个位置以下(堆栈向下增长)或以上(堆栈向上增长)。这是任务切换的关键字段。
2.xStateListItem: 一个链表项,用于将任务链接到某个状态列表中(如就绪列表、阻塞列表、挂起列表等)。通过这个链表项,调度器可以快速找到所有处于某个状态的任务。
3. xEventListItem: 另一个链表项,用于将任务链接到事件相关的链表(如事件组、队列等)。例如,当任务等待一个信号量时,它会被挂接到信号量的等待列表上,使用的就是这个链表项。
4. uxPriority: 任务的优先级。数值越大优先级越高(默认情况下,但可以通过配置改变)。
5. pxStack: 指向任务堆栈的起始位置(堆栈分配的内存起始地址)。用于堆栈溢出检测和删除任务时释放堆栈内存。
6. pcTaskName: 任务的可读名称,便于调试。
7. uxBasePriority(当使用互斥量时): 任务的基础优先级。当任务持有互斥量时,可能会临时提升优先级(优先级继承),当释放互斥量后,优先级会恢复到基础优先级。
8. ulNotifiedValue 和 ucNotifyState(当使用任务通知时): 任务通知功能相关字段。任务通知是轻量级的信号量、事件标志等替代机制。
9. pvThreadLocalStoragePointers: 线程本地存储指针数组,用于为任务提供私有数据存储。
10. 与移植相关的成员: 如`xMPUSettings`用于支持MPU(内存保护单元)的处理器,为任务配置内存保护区域。
FreeRTOS 中堆、栈和 TCB 的关系:
堆是资源池:提供 TCB 和栈所需的内存
TCB 是管理者:记录栈位置和任务状态
栈是运行空间:保存任务上下文和局部变量
3.4 临界段
被保护的代码段,不希望在执行这个代码段时被其他任务/中断干扰
vPortEnterCritical() | 进入临界区 | 1. 关闭可屏蔽中断 2. 增加嵌套计数 | 必须与vPortExitCritical()配对使用 |
vPortExitCritical() | 退出临界区 | 1. 减少嵌套计数 2. 当计数为0时恢复中断 | 调用次数必须与Enter匹配 |
在FreeRTOSConfig.h 配置中可以配置:
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 5
该宏决定了FreeRTOS可管理的最高中断优先级为5(可由用户自己配置),也就是说vPortEnterCritical()可以屏蔽中断优先级比≥5的优先级
注意:PendSV/SVCSysTick中断优先级为15(最低),在进入临界区后PendSV/SVCSysTick中断将会被屏蔽,任务将无法执行