当前位置: 首页 > news >正文

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 
注:三个中的延迟只执行一次

  1. 0ms - 2ms: Task3运行
    初始就绪任务:Task3(优先级3) > Task2(2) > Task1(1)
    CPU执行最高优先级任务 Task3(运行2ms)

  2. 2ms: Task3阻塞
    Task3调用vTaskDelay(5)进入阻塞态(解除阻塞时间=2ms+5ms=7ms)
    就绪任务更新:Task2(2), Task1(1)
    调度器选择最高优先级就绪任务 Task2

  3. 2ms - 3ms: Task2运行
    Task2执行1ms后调用vTaskDelay(10)(解除阻塞时间=3ms+10ms=13ms)
    Task2进入阻塞态
    就绪任务仅剩 Task1

  4. 3ms - 4ms: Task1运行
    Task1执行1ms后调用vTaskDelay(5)(解除阻塞时间=4ms+5ms=9ms)
    Task1进入阻塞态
    所有用户任务均阻塞

  5. 4ms - 7ms: 空闲任务运行
    无用户任务就绪,CPU执行优先级0的空闲任务(Idle Task)

  6. 7ms: Task3解除阻塞
    Task3延迟结束(7ms时刻),重新进入就绪态
    因Task3优先级最高,立即抢占CPU
    执行 Task3(从上次阻塞点继续运行

  7. 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如何执行这三个任务?(注:三个任务都是循环的,所有延迟函数均循环执行)

时间运行任务事件与状态变化
0Task1Task1运行1ms(累计1/3)→ 加入队列末尾
就绪队列:Task2 → Task3 → Task1
1Task2Task2运行1ms(累计1/2)→ 加入队列末尾
就绪队列:Task3 → Task1 → Task2
2Task3Task3运行1ms(累计1/3)→ 加入队列末尾
就绪队列:Task1 → Task2 → Task3
3Task1Task1运行1ms(累计2/3)→ 加入队列末尾
就绪队列:Task2 → Task3 → Task1
4Task2Task2运行1ms(累计2/2)→ 阻塞1ms(至t=5)
就绪队列:Task3 → Task1
5Task3Task2唤醒(t=5开始)→ 加入队列末尾
Task3运行1ms(累计2/3)→ 加入队列末尾
就绪队列:Task1 → Task2 → Task3
6Task1Task1运行1ms(累计3/3)→ 阻塞1ms(至t=7)
就绪队列:Task2 → Task3
7Task2Task1唤醒(t=7开始)→ 加入队列末尾
Task2运行1ms(重置后累计1/2)→ 加入队列末尾
就绪队列:Task3 → Task1 → Task2
8Task3Task3运行1ms(累计3/3)→ 阻塞1ms(至t=9)
就绪队列:Task1 → Task2
9Task1Task3唤醒(t=9开始)→ 加入队列末尾
Task1运行1ms(重置后累计1/3)→ 加入队列末尾
就绪队列:Task2 → Task3 → Task1
10Task2Task2运行1ms(累计2/2)→ 阻塞1ms(至t=11)
就绪队列:Task3 → Task1
11Task3Task2唤醒(t=11开始)→ 加入队列末尾
Task3运行1ms(重置后累计1/3)→ 加入队列末尾
就绪队列:Task1 → Task2 → Task3
12Task1Task1运行1ms(累计2/3)→ 加入队列末尾
就绪队列:Task2 → Task3 → Task1
13Task2Task2运行1ms(重置后累计1/2)→ 加入队列末尾
就绪队列:Task3 → Task1 → Task2
14Task3Task3运行1ms(累计2/3)→ 加入队列末尾
就绪队列:Task1 → Task2 → Task3
15Task1Task1运行1ms(累计3/3)→ 阻塞1ms(至t=16)
就绪队列:Task2 → Task3
16Task2Task1唤醒(t=16开始)→ 加入队列末尾
Task2运行1ms(累计2/2)→ 阻塞1ms(至t=17)
就绪队列:Task3 → Task1
17Task3Task2唤醒(t=17开始)→ 加入队列末尾
Task3运行1ms(累计3/3)→ 阻塞1ms(至t=18)
就绪队列:Task1 → Task2
18Task1Task3唤醒(t=18开始)→ 加入队列末尾
Task1运行1ms(重置后累计1/3)→ 加入队列末尾
就绪队列:Task2 → Task3 → Task1
19Task2Task2运行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中断将会被屏蔽,任务将无法执行

http://www.lryc.cn/news/589995.html

相关文章:

  • 《机器学习数学基础》补充资料:标准差与标准化
  • 《Qt信号与槽机制》详解:从基础到实践
  • Qt中实现文件(文本文件)内容对比
  • 若依框架下前后端分离项目交互流程详解
  • ScratchCard刮刮卡交互元素的实现
  • MR 处于 WIP 状态的WIP是什么
  • Django+Celery 进阶:Celery可视化监控与排错
  • 手撕Spring底层系列之:IOC、AOP
  • hadoop 集群问题处理
  • gem install报错解析
  • mac电脑无法阅读runc源码
  • UE5多人MOBA+GAS 24、创建属性UI(一)
  • 从 “洗澡难” 到 “洗得爽”:便携智能洗浴机如何重塑生活?
  • RK3566-EVB开发板如何新建一个产品分支
  • Jetpack Compose 中 Kotlin 协程的使用
  • 基于Hadoop与LightFM的美妆推荐系统设计与实现
  • Chrome紧急更新,谷歌修复正遭活跃利用的关键零日漏洞
  • iPhone 数据擦除软件评测(最新且全面)
  • 力扣面试150题--建立四叉树
  • 分布式光伏气象站:光伏产业的智慧守护者
  • 秘塔AI搜索的深度研究推出:它的“免费午餐”还能走多远?
  • 分布式弹性故障处理框架——Polly(1)
  • PyCharm(入门篇)
  • Python设计模式深度解析:建造者模式(Builder Pattern)完全指南
  • vivo S30评测:用设计诠释科技,以性能书写情怀
  • Git版本控制完全指南:从入门到精通
  • RoMa: Robust Dense Feature Matching论文精读(逐段解析)
  • 【Call For Paper| EI会议】第五届计算机图形学、人工智能与数据处理国际学术会议 (ICCAID 2025)
  • Weblogic历史漏洞利用
  • 5.Java类与对象