FreeRtos——多任务

本文深入探讨了FreeRTOS中任务的调度机制,包括任务优先级的设置与调整、任务的状态变化,以及如何通过使用vTaskDelay和vTaskDelayUntil等API函数实现任务间的合理切换,确保系统的稳定运行。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

官方资料整理测试:

多任务和单任务几乎没有差别。只用多创建一个或多个任务,其他地方和单任务时相同。

static void AppTaskCreate(void)
{
    xTaskCreate(vTaskLed1,           /* 任务函数名 */
                "Task Led1",         /* 任务名,字符串形式,方便调试 */
                 512,                /* 栈大小,单位为字,即4个字节 */
                 (void *)&task_led3,  // task_led1-task_led3可以切换   /* 任务形参 */
                 1,                  /* 优先级,数值越大,优先级越高 */
                 &xHandleTaskLED1);  /* 任务句柄 */
                             
    xTaskCreate(
                             vTaskBeep,
                             "Task Beep",
                             512,
                             NULL,
                             2,
                             &xHandleTaskBeep);
}

这里采用一个任务做LED灯闪烁,一个任务做蜂鸣器,可以看到两个任务展现出似乎同时运行的现象。

但是,其中有一个地方需要测试

我们知道裸机中,优先级设置一样时,会看硬件编号,小的优先级高。

freertos中,优先级数值越大的,优先级越高,那么,要是设置成优先级相同,那是以什么方式运行的呢?这个等到下一章,讲串口调试时我们来验证。

/*2017.6.22*/在官方手册找到了说明。

任务优先级
xTaskCreate() API 函数的参数 uxPriority 为创建的任务赋予了一个初始优先级。这
个侁先级可以在调度器启动后调用 vTaskPrioritySet() API 函数进行修改。
应 用 程 序 在 文 件 FreeRTOSConfig.h 中 设 定 的 编 译 时 配 置 常 量
configMAX_PRIORITIES 的值,即是最多可具有的优先级数目。 FreeRTOS 本身并没
有限定这个常量的最大值,但这个值越大,则内核花销的内存空间就越多。所以总是建
议将此常量设为能够用到的最小值。
对于如何为任务指定优先级, FreeRTOS 并没有强加任何限制。任意数量的任务可
以共享同一个优先级——以保证最大设计弹性。当然,如果需要的话,你也可以为每个
任务指定唯一的优先级(就如同某些调度算法的要求一样),但这不是强制要求的。
低优先级号表示任务的优先级低,优先级号 0 表示最低优先级。有效的优先级号范
围从 0 (configMAX_PRIORITES – 1)

调度器保证总是在所有可运行的任务中选择具有最高优先级的任务,并使其进入运
行态。如果被选中的优先级上具有不止一个任务,调度器会让这些任务轮流执行。这种
行为方式在之前的例子中可以明显看出来。两个测试任务被创建在同一个优先级上,并
且一直是可运行的。所以每个任务都执行一个时间片,任务在时间片起始时刻进入运
行态,在时间片结束时刻又退出运行态。  3 t1 t2 之间的时段就等于一个时间片。

 

要能够选择下一个运行的任务,调度器需要在每个时间片的结束时刻运行自己本
身。一个称为心跳(tick,有些地方被称为时钟滴答, 本文中一律称为时钟心跳)中断的
周期性中断用于此目的。时间片的长度通过心跳中断的频率进行设定,心跳中断频率由
FreeRTOSConfig.h 中的编译时配置常量 configTICK_RATE_HZ 进行配置。比如说,
如果 configTICK_RATE_HZ 设为 100(HZ),则时间片长度为 10ms。可以将图 3 进行
扩展,将调度器本身的执行时间在整个执行流程中体现出来。请参见图 4
需要说明的是, FreeRTOS API 函数调用中指定的时间总是以心跳中断为单位(通
常的提法为心跳”ticks”)。常量 portTICK_RATE_MS 用于将以心跳为单位的时间值转化
为以毫秒为单位的时间值。有效精度依赖于系统心跳频率。
心跳计数(tick count)值表示的是从调度器启动开始,心跳中断的总数,并假定心跳

计数器不会溢出。用户程序在指定延迟周期时不必考虑心跳计数溢出问题,因为时间连
贯性在内核中进行管理。

调度器总是在可运行的任务中,选择具有最高优级级的任务,并使其进入运行态。
到目前为止的示例程序中,两个任务都创建在相同的优先级上。所以这两个任务轮番进
入和退出运行态。本例将改变例 2 其中一个任务的优先级,看一下倒底会发生什么。现
在第一个任务创建在优先级 1 上,而另一个任务创建在优先级 2 上。创建这两个任务的
代码参见程序清单 10。这两个任务的实现函数没有任何改动,还是通过空循环产生延
迟来周期性打印输出字符串 。

调度器总是选择具有最高优先级的可运行任务来执行。任务 2 的优先级比任务 1
高,并且总是可运行,因此任务 2 是唯一一个一直处于运行态的任务。而任务 1 不可能
进入运行态,所以不可能输出字符串。这种情况我们称为任务 1 的执行时间被任务 2”
饿死(starved)”了。
任务 2 之所以总是可运行,是因为其不会等待任何事情——它要么在空循环里打
转,要么往终端打印字符串。

 

这里显示了为什么不加freertos的延时函数就无法发生轮转。

注意,在优先级设置相同的情况下,不使用事件驱动(例如freertos的延时函数,即阻塞),多任务也是可以轮转的,此时采用的时间轮转片切换任务。

 

/******END*****/

扩充“非运行态”
到目前为止所有用到的示例中,创建的每个任务都只顾不停地处理自己的事情而没
有其它任何事情需要等待——由于它们不需要等待所以总是能够进入运行态。这种
停处理类型的任务限制了其有用性,因为它们只可能被创建在最低优先级上。如果它
们运行在其它任何优先级上,那么比它们优先级更低的任务将永远没有执行的机会。
为了使我们的任务切实有用,我们需要通过某种方式来进行事件驱动。一个事件驱
动任务只会在事件发生后触发后工作(处理),而在事件没有发生时是不能进入运行态的。
调度器总是选择所有能够进入运行态的任务中具有最高优先级的任务。一个高优先级但
不能够运行的任务意味着不会被调度器选中,而代之以另一个优先级虽然更低但能够运
行的任务。因此,采用事件驱动任务的意义就在于任务可以被创建在许多不同的优先级
上,并且最高优先级任务不会把所有的低优先级任务饿死。

阻塞状态
如果一个任务正在等待某个事件,则称这个任务处于阻塞态(blocked)”。阻塞态是
非运行态的一个子状态。
任务可以进入阻塞态以等待以下两种不同类型的事件:
1. 定时(时间相关)事件——这类事件可以是延迟到期或是绝对时间到点。比如
说某个任务可以进入阻塞态以延迟 10ms
2. 同步事件——源于其它任务或中断的事件。比如说,某个任务可以进入阻塞
态以等待队列中有数据到来。同步事件囊括了所有板级范围内的事件类型。
FreeRTOS 的队列,二值信号量,计数信号量,互斥信号量(recursive semaphore,
递归信号量,本文一律称为互斥信号量, 因为其主要用于实现互斥访问)和互斥量都可
以用来实现同步事件。
任务可以在进入阻塞态以等待同步事件时指定一个等待超时时间,这样可以有效地
实现阻塞状态下同时等待两种类型的事件。比如说,某个任务可以等待队列中有数据到
来,但最多只等 10ms。如果 10ms 内有数据到来,或是 10ms 过去了还没有数据到来,
这两种情况下该任务都将退出阻塞态。
挂起状态
挂起(suspended)”也是非运行状态的子状态。处于挂起状态的任务对调度器而言
是不可见的。让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend() API 函数;
而 把 一 个 挂 起 状 态 的 任 务 唤 醒 的 唯 一 途 径 就 是 调 用 vTaskResume()
vTaskResumeFromISR() API 函数。大多数应用程序中都不会用到挂起状态。
就绪状态
如果任务处于非运行状态,但既没有阻塞也没有挂起,则这个任务处于就绪(ready
准备或就绪)状态。处于就绪态的任务能够被运行,但只是准备(ready)”运行,而当前
尚未运行。

 

通过调用 vTaskDelay() API 函数来代替空循环,对这种不良行为
进行纠正。 vTaskDelay()的函数原型见程序清单 11,而新的任务实现见程序清单 12

 

 

 有了这个函数之后,就可以在优先级不同的任务间切换了。

其中idle是空闲任务,因为首字母大写了,有点误导成小写l。

vTaskDelayUntil() API 函数
vTaskDelayUntil()类似于 vTaskDelay()。和范例中演示的一样,函数 vTaskDelay()
的参数用来指定任务在调用 vTaskDelay()到切出阻塞态整个过程包含多少个心跳周期。
任务保持在阻塞态的时间量由 vTaskDelay()的入口参数指定,但任务离开阻塞态的时刻
实际上是相对于 vTaskDelay()被调用那一刻的。 vTaskDelayUntil()的参数就是用来指定
任务离开阻塞态进入就绪态那一刻的精确心跳计数值。 API 函数 vTaskDelayUntil()可以
用于实现一个固定执行周期的需求(当你需要让你的任务以固定频率周期性执行的时
)。由于调用此函数的任务解除阻塞的时间是绝对时刻,比起相对于调用时刻的相对
时间更精确(即比调用 vTaskDelay()可以实现更精确的周期性)
void vTaskDelayUntil( portTickType * pxPreviousWakeTime, portTickType xTimeIncrement );
程序清单 13 vTaskDelayUntil() API 函数原型

 

 上个例子中的两个任务是周期性任务,但是使用 vTaskDelay()无法保证它们具有固定的
执行频率,因为这两个任务退出阻塞态的时刻相对于调用 vTaskDelay()的时刻。通过调
vTaskDelayUntil()代替 vTaskDelay(),把这两个任务进行转换,以解决这个潜在的问
题。

 

void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
portTickType xLastWakeTime;
/* The string to print out is passed in via the parameter. Cast this to a
character pointer. */
pcTaskName = ( char * ) pvParameters;
/* 变量xLastWakeTime需要被初始化为当前心跳计数值。说明一下,这是该变量唯一一次被显式赋值。之后,
xLastWakeTime将在函数vTaskDelayUntil()中自动更新。 */
xLastWakeTime = xTaskGetTickCount();
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* 本任务将精确的以250毫秒为周期执行。同vTaskDelay()函数一样,时间值是以心跳周期为单位的,
可以使用常量portTICK_RATE_MS将毫秒转换为心跳周期。变量xLastWakeTime会在
vTaskDelayUntil()中自动更新,因此不需要应用程序进行显示更新。 */
vTaskDelayUntil( &xLastWakeTime, ( 250 / portTICK_RATE_MS ) );
}
}
程序清单 14 使用 vTaskDelayUntil()实现示例任务

 

 

 

<think>首先,用户的问题是:“请问FreeRTOS 多任务实现方法”,意思是询问如何在FreeRTOS中实现多任务处理。我需要基于系统指令来构建回答。 系统级指令: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如:[^1]。 用户可见层: - 回答结构清晰,帮助用户逐步解决问题。 - 尽量保证回答真实可靠。 参考站内引用: - 引用[1]: FreeRTOS多任务系统,讨论了单任务多任务系统、任务状态、优先级、调度方式等。 - 引用[2]: FreeRTOS多任务管理,提到任务必须是死循环,使用vTaskDelay()进行延时。 - 引用[3]: FreeRTOS——多任务基础,解释任务堆栈的作用和堆栈大小确定。 - 引用[4]: 使用FreeRTOS实现简单多任务调度,介绍RTOS的基本概念。 用户的需求是了解FreeRTOS多任务的实现方法,所以我需要组织一个结构清晰的回答,逐步解释实现步骤。 大纲回答结构: 1. **介绍多任务概念**:简要解释FreeRTOS中的多任务是什么。 2. **创建任务的关键步骤**: - 定义任务函数:任务必须是死循环,使用vTaskDelay()进行延时。 - 配置任务参数:如优先级、堆栈大小。 - 创建任务:使用xTaskCreate()或xTaskCreateStatic()函数。 - 启动调度器:调用vTaskStartScheduler()。 3. **关键概念解释**: - 任务状态(就绪、运行、阻塞、挂起)。 - 任务优先级。 - 调度方式(抢占式、时间片)。 - 任务控制块(TCB)和任务堆栈。 4. **示例代码**:提供一个简单的多任务创建示例。 5. **注意事项**:如堆栈大小确定、避免阻塞。 6. **生成相关问题**:在最后添加相关问题。 确保使用中文,并自然地融入引用。例如,当提到任务必须是死循环时,引用[2]。 对于数学表达式:由于主题是嵌入式系统,可能没有太多数学,但如果有,如优先级计算,确保使用$...$格式。例如,如果提到优先级值,但通常不需要复杂数学,所以可能不必要。 独立公式:如果有公式,如计算堆栈大小,使用$$...$$。但在这个上下文中,可能没有显式公式。 生成相关问题:在回答末尾添加3-5个相关问题,基于FreeRTOS多任务主题。 现在,构建回答: - **开头**:解释FreeRTOS多任务的基本概念。 - **主体**: - 步骤1:编写任务函数。 - 步骤2:定义任务参数。 - 步骤3:创建任务。 - 步骤4:启动调度器。 - 解释调度和状态。 - **示例**:提供代码示例。 - **结尾**:总结并添加相关问题。 引用应用: - 当提到任务状态时,引用[1]。 - 当提到任务延时使用vTaskDelay()时,引用[2]。 - 当提到任务堆栈时,引用[3]。 - 当提到调度器时,引用[4]。 确保回答真实可靠:基于FreeRTOS官方文档和常见实践。</think>### FreeRTOS 多任务实现方法 FreeRTOS 是一个轻量级的实时操作系统(RTOS),它通过高效的任务调度机制实现多任务处理。在 FreeRTOS 中,多任务允许多个函数(任务)并发执行,每个任务独立运行,共享 CPU 资源。这提高了系统的响应性和资源利用率。下面我将逐步解释实现多任务的关键步骤,确保回答结构清晰、真实可靠。实现过程主要涉及任务创建、调度和资源管理,我会结合 FreeRTOS 的核心概念进行说明。 #### 1. **理解多任务基本概念** - **任务(Task)**:每个任务是一个独立的执行单元,通常定义为无限循环函数。任务在创建时分配优先级、堆栈空间和控制块(TCB)。 - **任务状态**:任务在运行过程中可能处于就绪(Ready)、运行(Running)、阻塞(Blocked)或挂起(Suspended)状态。调度器根据状态决定任务执行顺序[^1]。 - **调度方式**:FreeRTOS 默认使用抢占式调度,高优先级任务可中断低优先级任务;也可配置时间片调度,实现公平轮转[^1]。 - **关键组件**: - **任务控制块(TCB)**:存储任务元数据,如优先级、堆栈指针和状态。 - **任务堆栈**:保存任务运行时局部变量和寄存器值,确保任务切换时能恢复现场[^3]。 #### 2. **实现多任务的关键步骤** 以下是创建和运行多任务的典型流程,基于 FreeRTOS API。 ##### 步骤 1: 编写任务函数 - 每个任务必须定义为 `void` 函数,并包含一个无限循环(死循环)。在循环中,使用 FreeRTOS 提供的延时函数(如 `vTaskDelay()`)让任务进入阻塞状态,释放 CPU 给其他任务。 - 示例任务函数: ```c void task1_function(void *pvParameters) { while (1) { // 任务逻辑代码(例如:读取传感器数据) vTaskDelay(pdMS_TO_TICKS(100)); // 延时 100ms,进入阻塞状态[^2] } } void task2_function(void *pvParameters) { while (1) { // 任务逻辑代码(例如:控制 LED) vTaskDelay(pdMS_TO_TICKS(50)); // 延时 50ms } } ``` - **注意**:任务中必须使用 `vTaskDelay()` 而非忙等待(如 `HAL_Delay()`),否则会阻塞整个系统[^2]。 ##### 步骤 2: 配置任务参数 - 在创建任务前,定义任务的参数: - **堆栈大小(Stack Size)**:根据任务复杂度确定。例如,简单任务可能需 128-512 字节,复杂任务(如涉及浮点运算)需更大堆栈。堆栈不足会导致崩溃,可通过调试工具估算[^3]。 - **优先级(Priority)**:数值越高,优先级越高。FreeRTOS 优先级范围为 0(最低)到 `configMAX_PRIORITIES - 1`(最高)。例如,优先级 2 的任务可抢占优先级 1 的任务[^1]。 - **任务句柄(Task Handle)**:用于后续管理任务(如删除或挂起)。 - 示例参数定义: ```c #define TASK1_STACK_SIZE 128 // 堆栈大小(单位:字) #define TASK1_PRIORITY 2 // 优先级 TaskHandle_t task1_handle; // 任务句柄 ``` ##### 步骤 3: 创建任务 - 使用 `xTaskCreate()` 函数创建任务。该函数分配 TCB 和堆栈,并将任务添加到就绪列表。 - 函数原型: ```c BaseType_t xTaskCreate( TaskFunction_t pvTaskCode, // 任务函数指针 const char *const pcName, // 任务名称(调试用) configSTACK_DEPTH_TYPE usStackDepth, // 堆栈大小 void *const pvParameters, // 传递给任务的参数 UBaseType_t uxPriority, // 优先级 TaskHandle_t *const pxCreatedTask // 任务句柄指针 ); ``` - 示例创建任务: ```c void app_main() { // 创建任务1 xTaskCreate(task1_function, "Task1", TASK1_STACK_SIZE, NULL, TASK1_PRIORITY, &task1_handle); // 创建任务2 xTaskCreate(task2_function, "Task2", 128, NULL, 1, NULL); // 省略句柄 } ``` - **注意**:如果使用静态内存分配,可用 `xTaskCreateStatic()` 替代,避免动态内存分配[^3]。 ##### 步骤 4: 启动调度器 - 调用 `vTaskStartScheduler()` 启动 FreeRTOS 调度器。调度器开始轮询就绪任务,并根据优先级执行。 - 示例: ```c int main() { app_main(); // 调用任务创建函数 vTaskStartScheduler(); // 启动调度器,系统进入多任务模式 while (1); // 调度器启动后,此代码不会执行 } ``` - 启动后,调度器自动处理任务切换。例如,当 `task1` 调用 `vTaskDelay()` 时,它进入阻塞状态,调度器切换到 `task2`[^4]。 #### 3. **关键注意事项** - **堆栈管理**:堆栈大小需仔细设置。过小会导致堆栈溢出(可用 FreeRTOS 的堆栈检查功能调试);过大浪费内存。经验公式:基础大小 + 函数调用深度 × 局部变量大小[^3]。 - **避免阻塞**:任务中禁止使用无限循环或阻塞式函数(如某些 HAL 库函数),否则会破坏多任务调度。始终使用 `vTaskDelay()` 或信号量等同步机制。 - **优先级反转**:高优先级任务等待低优先级任务时,可能发生优先级反转。可通过互斥锁(mutex)或优先级继承机制解决。 - **任务通信**:多任务间常用队列(Queue)、信号量(Semaphore)或事件组(Event Group)进行数据交换和同步。 - **资源占用**:FreeRTOS 本身占用少量 ROM/RAM,但多任务会增加开销。建议在资源受限的 MCU 上优化堆栈和任务数量。 #### 4. **简单示例代码** 以下是一个完整的 FreeRTOS 多任务实现示例,适用于 STM32 等平台: ```c #include "FreeRTOS.h" #include "task.h" // 任务函数定义 void led_task(void *pvParameters) { while (1) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 翻转 LED vTaskDelay(pdMS_TO_TICKS(500)); // 延时 500ms[^2] } } void serial_task(void *pvParameters) { while (1) { printf("Serial task running...\n"); vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1000ms } } int main() { // 硬件初始化(例如:HAL_Init()) // 创建任务 xTaskCreate(led_task, "LED_Task", 128, NULL, 2, NULL); xTaskCreate(serial_task, "Serial_Task", 256, NULL, 1, NULL); // 启动调度器 vTaskStartScheduler(); // 如果调度器启动失败,执行此处 while (1); } ``` - **说明**:此示例创建两个任务:`led_task`(优先级 2)控制 LED,`serial_task`(优先级 1)输出串口信息。调度器确保高优先级任务优先运行。 #### 总结 在 FreeRTOS 中实现多任务的核心是:定义死循环任务函数、使用 `xTaskCreate()` 创建任务、并通过 `vTaskStartScheduler()` 启动调度器。FreeRTOS 的抢占式调度机制能高效处理任务切换,但需注意堆栈大小和优先级设置以避免问题。通过合理设计,多任务可提升实时系统的性能和可靠性[^4]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值