目录
一、任务与协程的区别:
(1)任务的特点:
(2)协程的特点:
(3)总结:
二、任务概述 :
(1)任务状态:
(2)任务优先级:
(3)任务调度:
三、任务控制函数:
(1)任务结构体:
(2)任务创建宏:
(3)任务创建:
(4)Delay()函数:
四、任务创建及使用:
(1)动态创建任务:
编辑(2)静态创建任务:
五、FreeRTOS教程示例代码下载:
一、任务与协程的区别:
单纯使用任务或协程,或者结合使用任务与协程均可设计应用程序——不过,任务与协程使用不同的 API 函数,因此不可通过队列(或信号量)在任务和协程之间传递数据。协程实际上仅用于具有严格 RAM 限制的非常小型的处理器。
(1)任务的特点:
- 独立性:每个任务在自己的上下文中执行,不依赖于其他任务或RTOS调度器。
- 调度:RTOS调度器负责决定哪个任务应该执行,可能会频繁地启动和停止任务。
- 上下文保存:任务切换时,当前任务的执行上下文(如寄存器值、堆栈内容)被保存到其堆栈中,以便恢复。
- 堆栈:每个任务都有自己独立的堆栈,这有助于提高RAM的使用率。
- 抢占式:支持完全抢占式机制,任务的执行可以被更高优先级的任务打断。
- 优先级:任务完全按优先级顺序排列。
- 重入问题:如果使用抢占式机制,需要考虑函数的重入性问题。
(2)协程的特点:
- 共享堆栈:所有协程共享一个堆栈,这大大减少了RAM的使用。
- 调度和优先级:协程间使用优先级协同调度,可以包含在使用抢占式任务的应用程序中。
- 宏实现:协程是通过一组宏实现的,这意味着它们的实现更轻量级。
- 使用限制:由于共享堆栈,协程的构造受到一些限制,以减少RAM使用。
- 协作操作:协程间的调度是协作式的,减少了重入问题。
- 移植性:可以在不同架构间移植。
- 优先级:相对于其他协程完全优先,但如果混用协程和任务,协程总是会被任务抢占。
- API限制:对API调用位置有限制,只能在协程间进行协作操作。
(3)总结:
- 任务提供了更灵活的并发控制,适合需要抢占式调度的场景,但可能会增加RAM的使用。
- 协程则是一种更轻量级的并发机制,适合资源受限的环境,但它们的调度是协作式的,需要开发者更仔细地管理协程的执行。
二、任务概述 :
(1)任务状态:
任务可以存在于以下状态中:
运行:
- 当任务实际执行时,它被称为处于运行状态。任务当前正在使用处理器。 如果运行 RTOS 的处理器只有一个内核, 那么在任何给定时间内都只能有一个任务处于运行状态。
准备就绪:
- 准备就绪任务指那些能够执行(它们不处于阻塞或挂起状态), 但目前没有执行的任务, 因为同等或更高优先级的不同任务已经处于运行状态。
阻塞:
- 如果任务当前正在等待时间或外部事件,则该任务被认为处于阻塞状态。 例如,如果一个任务调用vTaskDelay(),它将被阻塞(被置于阻塞状态), 直到延迟结束——一个时间事件。 任务也可以通过阻塞来等待队列、信号量、事件组、通知或信号量 事件。处于阻塞状态的任务通常有一个"超时"期, 超时后任务将被超时,并被解除阻塞, 即使该任务所等待的事件没有发生。“阻塞”状态下的任务不使用任何处理时间,不能 被选择进入运行状态。
挂起:
- 与“阻塞”状态下的任务一样, “挂起”状态下的任务不能 被选择进入运行状态,但处于挂起状态的任务 没有超时。相反,任务只有在分别通过 vTaskSuspend() 和 xTaskResume() API 调用明确命令时 才会进入或退出挂起状态。
(2)任务优先级:
每个任务均被分配了从 0 到 ( configMAX_PRIORITIES - 1 ) 的优先级,其中 configMAX_PRIORITIES 定义为 FreeRTOSConfig.h。
如果正在使用的移植实现了使用“前导零计数”类指令的移植优化任务选择机制 (针对单一指令中的任务选择)而且 configUSE_PORT_OPTIMISED_TASK_SELECTION 在 FreeRTOSConfig.h 中设置为 1,则 configMAX_PRIORITIES 无法高于 32。在其他所有情况下, configMAX_PRIORITIES 可以取任何合理数值——但为了保证 RAM 的使用效率,应取实际需要的最小值。
优先级数字小表示任务优先级低。空闲任务的优先级为零 (tskIDLE_PRIORITY)。
FreeRTOS 调度器可确保在就绪或运行状态下的任务始终比同样处于就绪状态下的更低优先级任务先获得处理器 (CPU) 时间。 换句话来说,处于运行状态的任务始终是能够运行的最高优先级任务。
处于相同优先级的任务数量不限。如果 configUSE_TIME_SLICING 未经定义,或者如果 configUSE_TIME_SLICING 设置为 1,则具有相同优先级的若干就绪状态任务将通过时间切片轮询调度方案共享可用的处理时间。
(3)任务调度:
FreeRTOS 调度(单核、AMP 和 SMP):
本文简要概述的 FreeRTOS 调度算法适用于单核、非对称多核 (AMP) 和对称多核 (SMP) RTOS 配置。调度算法是决定哪个 RTOS 任务应处于的运行状态软件程序。在任何给定时间, 每个处理器核心只能有一个任务处于运行状态。在 AMP 中, 每个处理器核心运行自身的FreeRTOS实例。在 SMP 中,存在一个实例 FreeRTOS,可以跨多核调度 RTOS 任务。
默认 RTOS 调度策略(单核):
FreeRTOS 默认使用固定优先级的抢占式调度策略,对同等优先级的任务执行时间切片轮询调度:
- “固定优先级”是指调度器不会永久更改任务的优先级, 但可能会因优先级继承而暂时提高任务的优先级。
- “抢占式”是指调度器始终运行优先级最高且可运行的 RTOS 任务, 无论任务何时能够运行。例如, 如果中断服务程序 (ISR) 更改了优先级最高且可运行的任务, 调度器会停止当前正在运行的低优先级任务 并启动高优先级任务——即使这发生在同一个时间片内 。这种情况下可以说高优先级任务 “抢占”了低优先级任务。
- “轮询调度”是指具有相同优先级的任务轮流进入运行状态。
- “时间切片”是指调度器会在每个 tick 中断上在同等优先级任务之间进行切换, tick 中断之间的时间构成一个时间切片。(tick 中断是 RTOS 用来衡量时间的周期性中断。)
使用优先排序的抢占式调度器,避免任务饥饿:
始终运行优先级最高且可运行的任务的一个后果是从未进入阻塞或挂起状态的高优先级任务将永久性剥夺所有更低优先级任务的任何执行时间。这就是通常最好创建事件驱动型任务的原因之一 。例如,如果一个高优先级任务正在等待一个事件,那么它就不应处于该事件的循环(轮询)中,因为如果处于轮询中,它会一直运行,永远不进入“阻塞”或 “挂起”状态。相反,该任务应进入“阻塞”状态等待这一事件。该事件可以通过某个发送FreeRTOS任务间通信和同步原语至任务。收到事件后,优先级更高的任务会自动解除“阻塞”状态。这样低优先级任务会运行,而高优先级任务会处于“阻塞”状态。
配置 RTOS 调度策略:
以下 FreeRTOSConfig.h 设置更改了默认调度行为:
configUSE_PREEMPTION
如果configUSE_PREEMPTION
为 0,则表示抢占已关闭, 而且只有当运行状态的任务进入“阻塞”或“挂起”状态, 或运行状态任务调用 taskYIELD()
,或中断服务程序 (ISR) 手动请求上下文切换时,才会发生上下文切换。
configUSE_TIME_SLICING
如果configUSE_TIME_SLICING
为 0,则表示时间切片已关闭, 因此调度器不会在每个 tick 中断上在同等优先级的任务之间切换。
FreeRTOS AMP 调度策略:
使用 FreeRTOS 的非对称多处理 (AMP) 是指多核设备的每个核心都单独运行自己的 FreeRTOS 实例。这些核心并不都需要具有相同架构, 但如果 FreeRTOS 实例之间需要进行通信,则需要共享一些内存。
每个核心都会运行自己的 FreeRTOS 实例, 因此任何给定核心上的调度算法与上文的单核系统调度算法完全相同 。您可以使用流缓冲区或消息缓冲区作为核间通信原语, 这样一来,一个核心上的任务可以进入“阻塞”状态, 以等待另一个核心发来的数据或事件。
FreeRTOS SMP 调度策略:
使用 FreeRTOS 的对称多处理 (SMP) 指的是一个 FreeRTOS 实例可以跨多个处理器核心调度 RTOS 任务。 由于只有一个 FreeRTOS 实例在运行,一次只能使用 FreeRTOS 的一个移植, 因此每个核心必须具有相同的处理器架构并共用相同的内存空间。
FreeRTOS SMP 调度策略使用与单核调度策略相同的算法, 但与单核和 AMP 场景不同的是, SMP 在任何给定时间都会导致多个任务处于运行状态 (每个核心上都有一个运行状态的任务)。这意味着, 只有缺乏可运行的高优先级任务时,才会运行低优先级任务的假设不再成立 。要想了解其中的原因,请考虑一下, 若起初只有一个高优先级任务和两个中等优先级任务处于 “就绪”状态,SMP 调度器会如何选择在双核微控制器上运行的任务。调度器需要选择两个任务,每个核心对应一个任务。 首先,高优先级任务是指可运行的最高优先级任务, 因此会选择将它用于第一个核心。这样就剩下了两个中等优先级的任务作为可运行的最高优先级任务,因此会将它们用于第二个核心 。结果是高优先级和中等优先级的任务同时运行。
配置 SMP RTOS 调度策略:
将为单核或 AMP RTOS 配置编写的代码移动到 SMP RTOS 配置,而且该代码依据的假设是:如果存在能够运行的较高优先级任务, 则较低优先级任务将不会运行时,可以使用以下配置选项。
configRUN_MULTIPLE_PRIORITIES
如果 configRUN_MULTIPLE_PRIORITIES
在 FreeRTOSConfig.h
中设置为 0,则只有在多个任务具有相同优先级的情况下, 调度器才会同时运行多个任务。这可以修复基于下列假设编写的代码: 一次将只运行一个任务,但同时必须牺牲 SMP 配置带来的一些好处。
configUSE_CORE_AFFINITY
如果 configUSE_CORE_AFFINITY
在 FreeRTOSConfig.h
中设置为 1,则 vTaskCoreAffinitySet()
API 函数可用于定义某个任务可以在哪些核心上运行以及不可以在哪些核心上运行。使用该方法,应用程序编写者可以防止同时执行假设了自身执行顺序的两个任务。
三、任务控制函数:
(1)任务结构体:
任务应具有以下结构体。
void vATaskFunction( void *pvParameters )
{
for( ;; ) // 无限循环,任务的主循环
{
// 这里放置任务的应用程序代码
}
/* 如果尝试从任务的实现函数返回或以其他方式退出,
在更新的FreeRTOS端口中,如果定义了configASSERT(),则会调用它。
如果任务需要退出,则调用vTaskDelete(NULL)来确保其退出是干净的
*/
vTaskDelete( NULL ); // 删除任务自身,确保任务能够干净地退出
}
TaskFunction_t 类型是指返回 void 并将 void 指针作为其唯一参数的函数 。所有实现任务的函数都应为此类型。该参数可用于 将任何类型的信息传递到任务中, 如一些标准演示应用程序任务标准演示应用程序任务标准演示应用程序任务所示。
任务函数不应返回,因此通常实现为连续循环。但是,通常最好创建事件驱动型任务,避免优先级较低的任务因缺少处理时间而饥饿, 从而形成以下结构体:
void vATaskFunction( void *pvParameters )
{
for( ;; ) // 无限循环,任务的主循环
{
// 伪代码,展示任务等待事件的发生,带有阻塞时间。
// 如果事件发生,则处理它。
// 如果在事件发生之前超时到期,则系统可能处于错误状态,因此需要处理错误。
// 这里的伪代码 "WaitForEvent()" 可以替换为 xQueueReceive()、ulTaskNotifyTake()、
// xEventGroupWaitBits() 或任何其他 FreeRTOS 通信和同步原语。
if( WaitForEvent( EventObject, TimeOut ) == pdPASS )
{
// 在这里处理事件
}
else
{
// 在这里清除错误或采取行动
}
}
// 根据上面的代码列表。
vTaskDelete( NULL ); // 删除任务自身,确保任务能够干净地退出
}
如需创建任务,请调用xTaskCreate()或xTaskCreateStatic();如需删除任务,调用vTaskDelete()。
(2)任务创建宏:
可以_选择_使用 portTASK_FUNCTION 或 portTASK_FUNCTION_PROTO 宏定义任务函数 。提供这些宏是为了允许将编译器特定的语法分别添加到函数定义 和原型中。所用端口(目前仅限 PIC18 fedC 端口)相关文档中未作具体说明, 则无需使用这些宏。
上述函数的原型可以写为:
void vATaskFunction( void *pvParameters );
或者:
portTASK_FUNCTION_PROTO( vATaskFunction, pvParameters );
同样,上述函数同样可以写为:
portTASK_FUNCTION( vATaskFunction, pvParameters )
{
for( ;; )
{
//任务应用程序代码在这里。
}
}
(3)任务创建:
模块有:
-
xTaskCreate:这是一个动态创建任务的函数。它在运行时分配内存,用于任务的堆栈和控制块。如果有足够的可用内存,这个函数可以创建任意数量的任务。
-
xTaskCreateStatic:这个函数用于静态创建任务。与
xTaskCreate
不同,它要求在编译时就提供任务的堆栈和控制块的内存。这种方式节省了动态内存分配的开销,但限制了任务的数量。 -
xTaskCreateRestrictedStatic:这个函数用于创建具有特定属性(如优先级、堆栈大小等)的静态任务。它同样要求在编译时提供内存,但提供了更多的控制和灵活性。
-
vTaskDelete:这个函数用于删除任务。当任务完成其工作或不再需要时,可以调用这个函数来清理任务使用的资源。
TaskHandle_t:
TaskHandle_t 是FreeRTOS中用于引用任务的类型。它是一个句柄,可以用来操作特定的任务。以下是TaskHandle_t
相关的详细描述:
-
任务引用的类型:
TaskHandle_t
是任务引用的类型。例如,调用xTaskCreate
(通过指针参数)返回TaskHandle_t
变量,然后可以将该变量用作vTaskDelete
的参数来删除任务。
总的来说,TaskHandle_t
是一个任务句柄,用于标识和操作FreeRTOS中的任务。通过这个句柄,可以对任务进行各种操作,如删除任务、查询任务状态等。
xTaskCreate函数:
创建一项新任务并将其添加到准备运行的任务列表中。configSUPPORT_DYNAMIC_ALLOCATION 必须在 FreeRTOSConfig.h 中设置为 1,或处于未定义状态(默认为 1),才可使用此 RTOS API 函数。
每项任务都需要 RAM 来保存任务状态,并由任务用作其堆栈。如果 使用 xTaskCreate() 创建任务,则所需的 RAM 会自动从 FreeRTOS堆分配。如果使用 xTaskCreateStatic()创建任务, 则 RAM 由应用程序编写者提供,因此可以在编译时静态分配。 有关详细信息,请参阅静态分配与动态分配页面。
如果使用的是FreeRTOS-MPU,建议 使用 xTaskCreateRestricted(),而不是 xTaskCreate()。
函数原型:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 指向任务函数的指针
const char * const pcName, // 任务的名称,用于调试和跟踪
const configSTACK_DEPTH_TYPE uxStackDepth, // 任务堆栈的深度
void *pvParameters, // 传递给任务函数的参数
UBaseType_t uxPriority, // 任务的优先级
TaskHandle_t *pxCreatedTask // 用于存储新创建的任务的句柄
);
参数名 | 类型 | 描述 |
---|---|---|
pvTaskCode | TaskFunction_t | 指向任务入口函数的指针(即实现任务的函数名称)。 任务通常以无限循环的形式实现;实现任务的函数绝不能尝试返回或退出。但是,任务可以自行删除。 |
pcName | const char * const | 任务的描述性名称。此参数主要用于方便调试,但也可用于 获取任务句柄。任务名称的最大长度 由FreeRTOSConfig.h 中的 configMAX_TASK_NAME_LEN 定义。 |
uxStackDepth | configSTACK_DEPTH_TYPE | 要分配用作任务堆栈的字数(不是字节数!)。例如,如果堆栈宽度为16位,uxStackDepth 为 100,则将分配 200 字节用作任务堆栈。再举一例,如果堆栈宽度为 32 位,uxStackDepth 为 400, 则将分配1600 字节用作任务堆栈。堆栈深度与堆栈宽度的乘积不得超过 size_t 类型变量所能包含的最大值。 |
pvParameters | void * | 作为参数传递给所创建任务的值。如果 pvParameters 设置为某变量的地址, 则在创建的任务执行时,该变量必须仍然存在,因此,不能传递堆栈变量的地址。 |
uxPriority | UBaseType_t | 创建的任务将以该指定优先级执行。支持 MPU 的系统 可以通过在 uxPriority 中设置 portPRIVILEGE_BIT 位来选择以特权(系统)模式创建任务。 例如,要创建优先级为 2 的特权任务,请将 uxPriority 设置为 ( 2 | portPRIVILEGE_BIT )。应断言优先级低于 configMAX_PRIORITIES。如果 configASSERT 未定义,则优先级默认上限为 (configMAX_PRIORITIES - 1)。 |
pxCreatedTask | TaskHandle_t * | 用于将句柄传递至由 xTaskCreate() 函数创建的任务。pxCreatedTask 是可选参数, 可设置为 NULL。 |
返回值 | 类型 | 描述 |
---|---|---|
pdPASS | BaseType_t | 如果任务创建成功,则返回pdPASS。 |
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY | BaseType_t | 如果任务创建失败,通常是因为内存不足,则返回此错误。 |
使用示例:
//待创建的任务
void vTaskCode(void *pvParameters)
{
/*参数值预期为1,因为1是在下面的xTaskCreate()
调用中的pvParameters值。*/
configASSERT( ( ( uint32_t ) pvParameters ) == 1 );
for( ;; )
{
//任务代码在这里
}
}
//创建任务的函数
void vOtherFunction( void )
{
BaseType_t xReturned; //用于存储任务创建函数的返回值
TaskHandle_t xHandle = NULL; // 用于存储新创建的任务的句柄
//创建任务,存储句柄
xReturned = xTaskCreate(
vTaskCode, //实现任务的函数
"NAME", //任务的文本名称
STACK_SIZE, //以字为单位的堆栈大小,而不是字节
( void * ) 1, //参数传递给任务
tskIDLE_PRIORITY,//创建任务的优先级
&xHandle ); //用于传递创建的任务句柄
if( xReturned == pdPASS )
{
//完成任务的创建,使用任务句柄删除任务。
vTaskDelete( xHandle );
}
}
xTaskCreateStatic:
创建一项新任务并将其添加到准备运行的任务列表中。configSUPPORT_STATIC_ALLOCATION 必须在 FreeRTOSConfig.h
中设置为 1,才可使用此 RTOS API 函数。
每项任务都需要 RAM 来保存任务状态,并由任务用作其堆栈。如果使用 xTaskCreate() 创建任务, 则所需的 RAM 将从FreeRTOS 堆自动分配。如果使用 xTaskCreateStatic()
创建任务,则 RAM 由应用程序编写者提供,这会产生更多的参数, 但这样能够在编译时静态分配 RAM。
如果使用的是FreeRTOS-MPU,建议使用 xTaskCreateRestricted(),而不是 xTaskCreateStatic()
。
TaskHandle_t xTaskCreateStatic(
TaskFunction_t pxTaskCode, // 指向任务函数的指针
const char * const pcName, // 任务的名称
const uint32_t ulStackDepth, // 任务堆栈的深度,以字节为单位
void * const pvParameters, // 传递给任务函数的参数
UBaseType_t uxPriority, // 任务的优先级
StackType_t * const puxStackBuffer, // 指向为任务堆栈分配的内存
StaticTask_t * const pxTaskBuffer // 指向为任务控制块分配的内存
);
参数名 | 类型 | 描述 |
---|---|---|
pxTaskCode | TaskFunction_t | 指向任务入口函数的指针(即实现任务的函数名称)。任务通常以无限循环的形式实现;实现任务的函数绝不能尝试返回或退出。但是,任务可以自行删除。 |
pcName | const char * const | 任务的描述性名称。此参数主要用于方便调试,但也可用于获取任务句柄。任务名称的最大长度由FreeRTOSConfig.h中的configMAX_TASK_NAME_LEN定义。 |
ulStackDepth | const uint32_t | puxStackBuffer参数用于将stackType_t变量的数组传递至xTaskCreateStatic()。ulStackDepth必须设置为数组中的索引数。 |
pvParameters | void * const | 作为参数传递给所创建任务的值。如果pvParameters设置为某变量的地址,则在创建的任务执行时,该变量必须仍然存在,因此,不能传递堆栈变量的地址。 |
uxPriority | UBaseType_t | 创建的任务将以该指定优先级执行。支持MPU的系统可以通过在uxPriority中设置portPRIVILEGE_BIT位来选择以特权(系统)模式创建任务。例如,要创建优先级为2的特权任务,请将uxPriority设置为(2portPRIVILEGE_BIT)。应断言优先级低于configMAX_PRIORITIES。如果configASSERT未定义,则优先级默认上限为(configMAX_PRIORITIES - 1)。 |
puxStackBuffer | StackType_t* const | 必须指向至少包含ulStackDepth个索引的StackType_t数组(见上述ulStackDepth参数),该数组将用作任务堆栈,因此必须持久存在(不能在函数的堆栈上声明)。 |
pxTaskBuffer | StaticTask_t* const | 必须指向StaticTask_t类型的变量。该变量将用于保存新任务的数据结构体(TCB),因此必须持久存在(不能在函数的堆栈上声明)。 |
返回值 | 类型 | 描述 |
---|---|---|
TaskHandle_t | 如果 puxStackBuffer和 pxTaskBuffer均不为 NULL,则创建任务, 并返回任务的句柄。如果 puxStackBuffer或pxTaskBuffer为 NULL,则不会创建任务, 并返回 NULL。 |
使用示例:
/* 任务将用作堆栈的缓冲区的尺寸。
注意:这是堆栈将容纳的单词数量,而不是字节数。例如,如果每个堆栈项是32位,
并且这个设置为100,则将分配400字节(100 * 32位)。 */
#define STACK_SIZE 200
/* 将保存被创建任务的TCB的结构体。 */
StaticTask_t xTaskBuffer;
/* 被创建任务将用作其堆栈的缓冲区。注意这是一个StackType_t变量数组。
StackType_t的大小取决于RTOS端口。 */
StackType_t xStack[STACK_SIZE];
/* 实现被创建任务的函数。 */
void vTaskCode(void *pvParameters)
{
/* 预期参数值为1,因为在调用xTaskCreateStatic()时pvParameters值传递为1。 */
configASSERT((uint32_t)pvParameters == 1UL);
for (;;)
{
/* 任务代码写在这里。 */
}
}
/* 创建任务的函数。 */
void vOtherFunction(void)
{
TaskHandle_t xHandle = NULL;
/* 不使用任何动态内存分配来创建任务。 */
xHandle = xTaskCreateStatic(
vTaskCode, /* 实现任务的函数。 */
"NAME", /* 任务的文本名称。 */
STACK_SIZE, /* xStack数组中的索引数。 */
(void *)1, /* 传递给任务的参数。 */
tskIDLE_PRIORITY,/* 创建任务的优先级。 */
xStack, /* 用作任务堆栈的数组。 */
&xTaskBuffer); /* 保存任务数据结构的变量。 */
/* puxStackBuffer和pxTaskBuffer都不是NULL,所以任务将被创建,
xHandle将是任务的句柄。使用句柄来挂起任务。 */
vTaskSuspend(xHandle); // 挂起任务
}
vTaskDelete:
void vTaskDelete( TaskHandle_t xTask );
INCLUDE_vTaskDelete必须定义为 1,才可使用此函数。从 RTOS 内核管理中移除任务。要删除的任务将从所有就绪、 阻塞、挂起和事件列表中移除。注意:空闲任务负责释放由 RTOS 内核分配给已删除任务的内存。因此,如果应用程序调用了vTaskDelete(),请务必确保空闲任务获得足够的微控制器处理时间。任务代码分配的内存不会自动释放, 应在任务删除之前手动释放。
参数(xTask): 要删除的任务的句柄。如果传递 NULL,会删除调用任务。
使用示例:
void vOtherFunction( void )
{
TaskHandle_t xHandle = NULL;
// 创建任务,存储句柄。
xTaskCreate( vTaskCode, "NAME", STACK_SIZE, NULL, tskIDLE_PRIORITY, &xHandle );
// 使用句柄删除任务。
if( xHandle != NULL )
{
vTaskDelete( xHandle );
}
}
(4)Delay()函数:
vTaskDelay():
void vTaskDelay( const TickType_t xTicksToDelay );
INCLUDE_vTaskDelay必须定义为 1,才可使用此函数。按给定的滴答数延迟任务。任务保持阻塞的实际时间取决于滴答频率 。常量 portTICK_PERIOD_MS(默认为1000/1000=1)可用于根据滴答频率计算实际时间,其中使用一个滴答周期的分辨率。vTaskDelay()指定任务想要取消阻塞的时间,该时间相对于调用 vTaskDelay()的时间 。例如,指定 100 个滴答的阻塞周期将导致任务 在 vTaskDelay()被调用之后取消阻塞 100 个滴答。因此,vTaskDelay()不能很好地控制 周期性任务的频率,因为途经代码的路径以及其他任务和中断 将影响 vTaskDelay()被调用的频率,从而影响任务下一次执行的时间 。请参阅 vTaskDelayUntil(),了解设计用于方便固定频率执行的替代 API 函数。此函数指定调用任务应取消阻塞的绝对时间(而非相对时间)来实现这一点 。
参数 | 作用 |
xTicksToDelay | 调用任务应阻塞的 tick 周期数 |
用法示例:
void vTaskFunction( void * pvParameters )
{
// 阻塞500ms
const TickType_t xDelay = 500 / portTICK_PERIOD_MS;
for( ;; )
{
// 只需每500ms切换LED,每次切换之间阻塞。
vToggleLED();
vTaskDelay( xDelay );
}
}
vTaskDelayUntil():
函数原型1:
void vTaskDelayUntil( TickType_t *pxPreviousWakeTime,
const TickType_t xTimeIncrement );
INCLUDE_vTaskDelayUntil必须定义为 1,才可使用此函数。将任务延迟到指定时间。此函数可以由周期性任务使用,来确保恒定的执行频率。
此函数与 vTaskDelay()在一个重要方面有所不同:vTaskDelay()会指定任务想要取消阻塞的时间,该时间是相对于vTaskDelay()被调用的时间, 而vTaskDelayUntil()会指定任务希望取消阻塞的绝对时间。
vTaskDelay()将导致一个任务从调用vTaskDelay()时起阻塞指定的滴答数 。因此,很难单独使用vTaskDelay()来生成固定的执行频率,因为任务在调用vTaskDelay()后取消阻塞与该任务 再次调用 vTaskDelay()之间的时间可能不是固定的(该任务可能在两次调用之间采用不同的代码路径,或者可能在每次执行时被打断或被抢占的次数不同)。
vTaskDelay()指定了相对于函数被调用时的唤醒时间, 而 vTaskDelayUntil()则指定了它希望解除阻塞的绝对(精确)时间 。
应注意,如果 vTaskDelayUntil()被用于指定已过去的唤醒时间, 该函数将立即返回(不阻塞)。因此,使用vTaskDelayUntil()定期执行的任务,在周期性执行因任何原因停止 (例如,任务被挂起),而导致任务错过一个或多个周期性执行时, 必须重新计算其所需的唤醒 时间。这可以通过检查由引用传递的变量来发现,该变量是针对当前滴答计数的pxPreviousWakeTime参数。但是,这在大多数使用场景下并非必要。
常量 portTICK_PERIOD_MS配合滴答周期分辨率 可用于从滴答频率计算实际时间。
当调用了vTaskSuspendAll()挂起 RTOS 调度器时,不得调用此函数。
参数 | 作用 |
pxPreviousWakeTime | 指向一个变量的指针,该变量用于保存任务最后一次解除阻塞的时间。该变量在首次使用前必须用当前时间初始化(见下面的示例)。在这之后,该变量会在 vTaskDelayUntil() 内自动更新。 |
xTimeIncrement | 周期时间段。该任务将在 (*pxPreviousWakeTime + xTimeIncrement)时间解除阻塞。 使用相同的xTimeIncrement参数值调用 vTaskDelayUntil将导致任务 以固定的间隔期执行。 |
用法示例:
// 定义一个任务函数,该函数将在每10个系统时钟节拍(ticks)执行一次动作。
void vTaskFunction( void * pvParameters )
{
TickType_t xLastWakeTime; // 定义一个变量,用于记录任务上次唤醒的时间
const TickType_t xFrequency = 10; // 定义任务执行的频率,这里是每10个ticks执行一次
// 初始化xLastWakeTime变量,记录当前的系统时钟节拍数。
xLastWakeTime = xTaskGetTickCount(); // 获取当前的系统时钟节拍数,并赋值给xLastWakeTime
for( ;; ) // 无限循环,任务将在这个循环中持续运行
{
// 调用vTaskDelayUntil函数,使任务延迟到下一个预定的执行时间点。
// 这个函数将使任务休眠,直到xLastWakeTime加上xFrequency的时间点。
// xTaskDelayUntil会更新xLastWakeTime为下一次唤醒的时间。
vTaskDelayUntil( &xLastWakeTime, xFrequency );
// 在这里执行需要周期性执行的动作。
// 例如,可以是读取传感器数据、更新显示、发送数据等。
// 注意:这里没有具体的代码实现,需要根据实际需求添加相应的操作。
}
}
函数原型2:
BaseType_t xTaskDelayUntil( TickType_t *pxPreviousWakeTime,
const TickType_t xTimeIncrement );
INCLUDE_xTaskDelayUntil 必须定义为 1 ,此函数才可用。 将任务延迟到指定时间。此函数可以由周期性任务使用, 来确保恒定的执行频率。
此函数与 vTaskDelay() 在一个重要的方面有所不同:
vTaskDelay() 会导致一个任务从调用 vTaskDelay() 起阻塞指定的滴答数, 而 xTaskDelayUntil() 将导致一个任务从 pxPreviousWakeTime 参数中指定的时间起阻塞指定的滴答数。使用vTaskDelay() 本身很难产生一个固定的执行频率, 因为从一个任务开始执行到该任务调用 vTaskDelay() 之间的时间可能并不固定 [该任务在调用之间可能采取不同的代码路径, 或者每次执行时可能被中断或被抢占的次数不同]。 xTaskDelayUntil() 可以用来生成一个恒定的执行频率。
vTaskDelay() 指定了相对于函数被调用时的唤醒时间, 而 xTaskDelayUntil() 则指定了它希望解除阻塞的绝对(精确)时间 。宏 pdMS_TO_TICKS() 可以用来计算以毫秒为单位的时间的 tick 数, 分辨率为一个 tick 周期。
参数 | 作用 |
pxPreviousWakeTime | 指向一个变量的指针,该变量用于保存任务最后一次解除阻塞的时间。该变量在首次使用前 必须用当前时间初始化(见下面的示例)。在这之后,该变量 会在 xTaskDelayUntil() 中自动更新。 |
xTimeIncrement | 周期时间段。任务将在 (*pxPreviousWakeTime + xTimeIncrement) 时间取消阻塞。使用 相同的 xTimeIncrement 参数值调用 xTaskDelayUntil 将导致任务以固定的间隔周期执行 。 |
返回 | 可用于检查任务是否实际延迟的值:任务延迟时返回 pdTRUE, 否则返回 pdFALSE。如果下一个预计唤醒时间已过,则任务将不会延迟。 |
用法示例:
// 定义一个任务函数,该函数将在每10个系统时钟节拍(ticks)执行一次动作。
void vTaskFunction( void * pvParameters )
{
TickType_t xLastWakeTime; // 定义一个变量,用于记录任务上次唤醒的时间
const TickType_t xFrequency = 10; // 定义任务执行的频率,这里是每10个ticks执行一次
BaseType_t xWasDelayed; // 定义一个变量,用于记录任务是否因为延迟而错过了执行周期
// 初始化xLastWakeTime变量,记录当前的系统时钟节拍数。
xLastWakeTime = xTaskGetTickCount(); // 获取当前的系统时钟节拍数,并赋值给xLastWakeTime
for( ;; ) // 无限循环,任务将在这个循环中持续运行
{
// 调用xTaskDelayUntil函数,使任务延迟到下一个预定的执行时间点。
// 这个函数将使任务休眠,直到xLastWakeTime加上xFrequency的时间点。
// xTaskDelayUntil会更新xLastWakeTime为下一次唤醒的时间。
xWasDelayed = xTaskDelayUntil( &xLastWakeTime, xFrequency );
// 在这里执行需要周期性执行的动作。
// 例如,可以是读取传感器数据、更新显示、发送数据等。
// xWasDelayed值可以用来判断任务是否因为执行时间过长而错过了执行周期。
// 如果任务执行的动作需要的时间超过了预定的周期(这里是10个ticks),
// 那么xWasDelayed将返回pdTRUE,表示任务错过了一个或多个执行周期。
}
}
在FreeRTOS中,系统时钟节拍(tick)的频率是由配置常量 configTICK_RATE_HZ
定义的,这个值表示每秒有多少个时钟节拍。默认情况下,许多FreeRTOS配置会使用1000Hz的时钟节拍频率,即每秒1000个节拍(ticks)。这意味着每个节拍的时间间隔是1毫秒(1/1000hz=0.001ms)。
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
特性 | 返回 void 的 vTaskDelayUntil | 返回 BaseType_t 的 xTaskDelayUntil |
---|---|---|
返回类型 | void | BaseType_t (通常是 pdPASS 或 pdFALSE ) |
功能描述 | 使任务延迟直到指定的时间点。 | 使任务延迟直到指定的时间点,并提供是否错过周期的信息。 |
错过周期检测 | 不提供。 | 提供。如果自上次唤醒以来错过了一个或多个周期,返回 pdTRUE 。 |
参数列表 | TickType_t *pxPreviousWakeTime, TickType_t xTimeIncrement | TickType_t *pxPreviousWakeTime, TickType_t xTimeIncrement |
更新唤醒时间 | 是,更新 pxPreviousWakeTime 。 | 是,更新 pxPreviousWakeTime 。 |
使用场景 | 不需要检测错过周期的情况。 | 需要检测是否错过了周期,以便采取相应措施。 |
效率 | 可能稍高,因为没有返回值处理。 | 可能稍低,因为需要处理返回值。 |
四、任务创建及使用:
(1)动态创建任务:
使用xTaskCreate()动态创建任务函数。在运行时分配内存,用于任务的堆栈和控制块。如果有足够的可用内存,这个函数可以创建任意数量的任务。
#include "stm32f10x.h" // 包含STM32F10x系列微控制器的头文件
#include "FreeRTOS.h"
#include "task.h" // 包含任务相关函数的头文件,用于任务创建和管理。
#include "stdio.h"
#include "uart.h"
/***********************************
* @method xTaskCreate函数动态分配内存,
* 直接在main函数里中创建
* @author The_xzs
* @date 2025.1.22
************************************/
// 定义任务函数,用于打印The_xzs帅吗?。
void vTaskCode1(void *pvParameters)
{
/*参数值预期为1,因为1是在下面的xTaskCreate()
调用中的pvParameters值。*/
configASSERT( ( ( uint32_t ) pvParameters ) == 1 );
for( ;; )
{
//任务代码
printf("Is The_xzs handsome?");
vTaskDelay(1000); // 延迟1000个时钟节拍。
}
}
// 定义任务函数,用于打印是的!。
void vTaskCode2(void *pvParameters)
{
/*参数值预期为1,因为1是在下面的xTaskCreate()
调用中的pvParameters值。*/
configASSERT( ( ( uint32_t ) pvParameters ) == 1 );
for( ;; )
{
//任务代码
printf("yes!");
vTaskDelay(1000); // 延迟1000个时钟节拍。
}
}
// 主函数
int main(void)
{
TaskHandle_t TaskHandler; // 定义一个任务句柄变量,用于跟踪任务。
Uart_Init(115200);
DMA1_Init();
// 创建名为"Handsome"的任务,栈大小为64字节,优先级为2,任务函数为vTaskCode1,传递参数1给任务。
// 创建名为"Yes"的任务,栈大小为64字节,优先级为2,任务函数为vTaskCode2,传递参数1给任务。
xTaskCreate(vTaskCode1, "Handsome", 64,(void*)1, 2, &TaskHandler);
xTaskCreate(vTaskCode2, "Yes", 64, (void*)1, 2, &TaskHandler);
// 启动任务调度器,这是FreeRTOS开始执行任务的地方。
vTaskStartScheduler();
return 0;
}
创建两个任务,串口分别打印Is The_xzs handsome? 和yes!两句话,再延时1000ms观察串口打印的数据,数据如下:
yes!比Is The_xzs handsome?更早打印出来。这是因为xTaskCreate()函数创建任务会使得更高优先级的、或者后面创建的任务先运行。
调整优先级:
优先级数字小表示任务优先级低,空闲任务的优先级为零 (tskIDLE_PRIORITY)。
xTaskCreate(vTaskCode1, "Handsome", 64,(void*)1, 3, &TaskHandler);
xTaskCreate(vTaskCode2, "Yes", 64, (void*)1, 2, &TaskHandler);
(2)静态创建任务:
使用xTaskCreateStatic()静态创建任务,与xTaskCreate
不同,它要求在编译时就提供任务的堆栈和控制块的内存。这种方式节省了动态内存分配的开销,但限制了任务的数量。
使用该函数时,configSUPPORT_STATIC_ALLOCATION 必须在 FreeRTOSConfig.h
中设置为 1。在置1后常见问题:
显示未定义的符号vApplicationGetIdleTaskMemory(从tasks.o引用)。这说明vApplicationGetIdleTaskMemory在tasks.c文件中未定义,但在tasks.h中有函数原型。通过FreeRTOS官网可以知道,如果 configSUPPORT_STATIC_ALLOCATION设置为 1,则应用程序编写者还必须提供两个回调函数:vApplicationGetIdleTaskMemory(),为 RTOS 空闲任务提供内存;(如果 configUSE_TIMERS设置为 1)vApplicationGetTimerTaskMemory(),为 RTOS 守护进程/定时器服务任务提供内存。
我们在tasks.c文件中找到configSUPPORT_STATIC_ALLOCATION=1判断处可以看到,
在判断中并没有定义vApplicationGetIdleTaskMemory()函数,只定义了xTaskCreateStatic()函数,我们可以将vApplicationGetIdleTaskMemory()函数添加到该判断后面,
再次编译,即可解决报错情况。
编写程序代码如下:
#include "stm32f10x.h" // 包含STM32F10x系列微控制器的头文件
#include "FreeRTOS.h"
#include "task.h" // 包含任务相关函数的头文件,用于任务创建和管理。
#include "stdio.h"
#include "uart.h"
/***********************************
* @method xTaskCreateStatic函数静态分配内存,
* 在任务创建函数task_Init里创建任务
* @author The_xzs
* @date 2025.1.22
************************************/
#define STACK_SIZE 200
/*
正在创建的任务将用作栈的缓冲区。
注意这是StackType_t变量的数组。
StackType_t的大小取决于RTOS端口。
*/
StackType_t xStack1[STACK_SIZE];
StackType_t xStack2[STACK_SIZE];
/* 将保存被创建任务的TCB的结构体。 */
StaticTask_t xTaskBuffer1;
StaticTask_t xTaskBuffer2;
// 定义任务函数,用于打印The_xzs帅吗?
void vTaskCode1(void *pvParameters)
{
/*参数值预期为1,因为1是在下面的xTaskCreateStatic()
调用中的pvParameters值。*/
configASSERT((uint32_t)pvParameters == 1UL);
for( ;; )
{
//任务代码
printf("Is The_xzs handsome?");
vTaskDelay(1000); // 延迟1000个时钟节拍。
}
}
// 定义任务函数,用于打印是的!。
void vTaskCode2(void *pvParameters)
{
/*参数值预期为1,因为1是在下面的xTaskCreateStatic()
调用中的pvParameters值。*/
configASSERT((uint32_t)pvParameters == 1UL);
for( ;; )
{
//任务代码
printf("yes!");
vTaskDelay(1000); // 延迟1000个时钟节拍。
}
}
/* 创建任务的函数。 */
void task_Init(void)
{
/*不使用任何动态内存分配来创建任务。
xHandle = xTaskCreateStatic(
vTaskCode, // 实现任务的函数
"NAME", // 任务的文本名称。
STACK_SIZE, // xStack数组中的索引数
( void * ) 1, // 参数传递给任务
tskIDLE_PRIORITY,// 创建任务的优先级
xStack, // 数组用作任务的堆栈
&xTaskBuffer ); // 变量来保存任务的数据结构。
*/
xTaskCreateStatic(vTaskCode1, "Handsome", STACK_SIZE, (void *)1, 3, xStack1, &xTaskBuffer1);
xTaskCreateStatic(vTaskCode2, "YES", STACK_SIZE, (void *)1, 2, xStack2, &xTaskBuffer2);
// 启动任务调度器
vTaskStartScheduler();
}
// 主函数
int main(void)
{
Uart_Init(115200);
DMA1_Init();
task_Init();
return 0;
}
效果如下:
注意:
-
不要将任务的堆栈和任务控制块定义为局部变量,因为它们的生命周期会随着函数返回而结束,导致任务无法正常运行。
-
将这些变量定义为全局变量或静态变量,确保它们在整个程序运行期间都有效。
例如:如果将 StackType_t xStack1[STACK_SIZE];
和 StaticTask_t xTaskBuffer1;
等变量定义在 task_Init()
函数内部,程序只会执行一次打印 "yes!" 的原因有:
1.变量的作用域和生命周期:
当你将这些变量定义在 task_Init()
函数内部时,它们成为局部变量。局部变量的生命周期仅限于函数的执行期间。一旦 task_Init()
函数执行完成并返回,这些局部变量的存储空间会被释放,它们的值也会失效。
2. FreeRTOS 的任务调度器依赖静态分配的内存:
xTaskCreateStatic()
函数需要一个静态分配的堆栈(xStack
)和任务控制块(xTaskBuffer
)来存储任务的上下文信息。如果这些变量是局部变量,当 task_Init()
函数返回后,这些变量的存储空间会被释放,任务的堆栈和任务控制块会丢失,导致任务无法正常运行。
3. 为什么只打印一次 "yes!":
在 task_Init()
函数中,任务被创建并启动,任务调度器也开始运行。
任务调度器会尝试切换到创建的任务,但由于任务的堆栈和任务控制块在 task_Init()
函数返回后被释放,任务的上下文信息丢失。
因此,任务只执行了一次(打印了 "yes!"),然后就无法继续运行,因为它的堆栈和任务控制块已经失效。
五、FreeRTOS教程示例代码下载:
FreeRTOS教程示例代码将会持续更新...
通过网盘分享的文件:FreeRTOS教程示例代码
链接: https://pan.baidu.com/s/1363h7hHmf8u2pjauwKyhtw?pwd=mi98 提取码: mi98