FreeRTOS 是一个轻量级、实时性强的操作系统内核,专门为嵌入式系统设计,它本身是用 C 语言编写的,并提供 C 语言 API 供开发者使用,掌握 FreeRTOS 是嵌入式开发工程师的一项重要技能。

FreeRTOS 的核心概念(为什么需要它?)
在没有操作系统的单片机中,程序通常是“前后台系统”:
- 后台: 一个
while(1)的主循环,顺序执行各种任务。 - 前台: 中断服务程序,可以打断主循环。
这种模式简单,但在处理多个需要“运行的复杂任务时,会变得非常困难,代码会变得臃肿、难以维护,且无法保证任务的实时性。
FreeRTOS 通过引入多任务和调度器来解决这个问题。
任务
任务可以看作是一个独立的、拥有自己栈空间的、无限循环的 C 函数,每个任务都有其优先级,看起来像是在“运行。

关键特性:
- 独立性: 每个任务有自己的栈空间,一个任务的崩溃通常不会直接导致整个系统崩溃。
- 优先级: FreeRTOS 是一个抢占式内核,高优先级的任务可以随时抢占低优先级任务的 CPU 使用权。
- 状态: 任务有几种状态:
Running(运行态)、Ready(就绪态)、Blocked(阻塞态)、Suspended(挂起态)。
调度器
调度器是 FreeRTOS 的“大脑”,它决定在任何一个时刻,哪个处于就绪态的任务最高优先级的任务应该获得 CPU 的控制权。
- 抢占式调度: 当一个高优先级的任务从阻塞态变为就绪态时,它会立即抢占当前正在运行的低优先级任务。
- 时间片轮转: 对于相同优先级的多个任务,调度器会以时间片(由
configTICK_RATE_HZ决定)为单位,轮流让它们运行。
队列
队列是任务间、任务与中断间进行通信和同步的主要方式,它像一个 FIFO(先进先出)的管道,可以发送和接收数据。
- 用途:
- 传递数据(如传感器读数、控制命令)。
- 信号量(一种特殊的队列,用于同步)。
- 通知(一种更轻量级的同步机制)。
信号量
信号量主要用于任务间的同步和资源管理,本质上是计数值大于等于 0 的队列。

- 二值信号量: 计数只能是 0 或 1,常用于任务同步,例如一个任务等待某个事件发生(中断触发)。
- 计数信号量: 计数可以大于 1,常用于管理多个相同的资源,例如有 3 个空闲的 UART 缓冲区。
- 互斥量: 一种特殊的二值信号量,带有优先级继承机制,用于保护共享资源(如全局变量、硬件外设),防止优先级反转。
互斥量
当多个任务访问同一个共享资源时,可能会发生“优先级反转”(低优先级任务持有资源,高优先级任务被阻塞,导致中优先级任务先执行),互斥量通过“优先级继承”机制来解决此问题:当一个高优先级任务被一个低优先级任务阻塞时,低优先级任务的临时优先级会提升到与高优先级任务相同,直到它释放互斥量。
FreeRTOS 的关键 API 函数(C 语言接口)
以下是使用最频繁的一些 API 函数。
任务管理
| 函数 | 描述 |
|---|---|
xTaskCreate() |
创建一个新任务,这是最常用的创建任务函数。 |
vTaskDelete() |
删除一个任务,释放其占用的资源。 |
vTaskDelay() |
让当前任务进入阻塞态,指定的时间后自动进入就绪态。 |
vTaskSuspend() / vTaskResume() |
挂起和恢复任务,挂起的任务不会被调度器执行。 |
队列管理
| 函数 | 描述 |
|---|---|
xQueueCreate() |
创建一个队列。 |
xQueueSend() / xQueueReceive() |
向队列发送数据和从队列接收数据(阻塞式)。 |
xQueueSendFromISR() / xQueueReceiveFromISR() |
在中断服务程序中向队列发送/接收数据。 |
信号量管理
| 函数 | 描述 |
|---|---|
xSemaphoreCreateBinary() |
创建一个二值信号量。 |
xSemaphoreCreateMutex() |
创建一个互斥量。 |
xSemaphoreGive() / xSemaphoreTake() |
释放和获取信号量/互斥量(阻塞式)。 |
xSemaphoreGiveFromISR() / xSemaphoreTakeFromISR() |
在中断服务程序中释放/获取信号量。 |
一个完整的 FreeRTOS 项目示例(C 语言)
下面是一个基于 STM32 HAL 库和 FreeRTOS 的简单示例,创建两个任务:Task1 每 1 秒打印一次 "Hello from Task 1",Task2 每 2 秒打印一次 "Hello from Task 2"。
包含必要的头文件
/* 包含 FreeRTOS 的头文件 */ #include "FreeRTOS.h" #include "task.h" #include "queue.h" /* 包含标准库和芯片特定的头文件 */ #include <stdio.h> #include "main.h" // STM32 HAL 生成的头文件 /* 定义任务句柄,用于后续任务管理 */ TaskHandle_t Task1Handle; TaskHandle_t Task2Handle;
定义任务函数
每个任务函数都必须是一个无限循环,并且通常返回 void。
/**
* @brief 任务1:每1秒打印一次信息
* @param pvParameters 任务创建时传入的参数(本例中未使用)
*/
void Task1Function(void *pvParameters)
{
for (;;)
{
printf("Hello from Task 1\n");
vTaskDelay(pdMS_TO_TICKS(1000)); // 阻塞1秒 (1000ms)
}
}
/**
* @brief 任务2:每2秒打印一次信息
* @param pvParameters 任务创建时传入的参数(本例中未使用)
*/
void Task2Function(void *pvParameters)
{
for (;;)
{
printf("Hello from Task 2\n");
vTaskDelay(pdMS_TO_TICKS(2000)); // 阻塞2秒 (2000ms)
}
}
在 main 函数中初始化和启动任务
main 函数是整个程序的入口,你需要初始化硬件,然后创建任务,最后启动调度器。
int main(void)
{
/* 1. 初始化硬件(如HAL库、时钟、串口等) */
HAL_Init();
SystemClock_Config();
MX_USART1_UART_Init(); // 假设你使用串口1打印
/* 2. 创建任务 */
// 参数1: 任务函数名
// 参数2: 任务名称(用于调试)
// 参数3: 任务栈大小(字节数)
// 参数4: 任务参数(本例为NULL)
// 参数5: 任务优先级(数字越大,优先级越高)
// 参数6: 任务句柄(用于后续管理)
// 参数7: 任务创建在哪个核心(对于多核MCU,如ESP32)
xTaskCreate(
Task1Function, /* 任务函数 */
"Task 1", /* 任务名称 */
128, /* 栈大小 */
NULL, /* 任务参数 */
1, /* 任务优先级 */
&Task1Handle /* 任务句柄 */
);
xTaskCreate(
Task2Function,
"Task 2",
128,
NULL,
1, // 注意:两个任务优先级相同,调度器会进行时间片轮转
&Task2Handle
);
/* 3. 启动调度器 */
// 这行代码之后,FreeRTOS 将接管CPU控制权,开始调度任务。
// vTaskStartScheduler() 不会返回。
vTaskStartScheduler();
// 正常情况下,代码不会执行到这里。
// 如果调度器启动失败(比如内存不足),会进入一个错误处理函数。
for (;;);
}
FreeRTOSConfig.h 配置文件
这是 FreeRTOS 的“心脏”,它定义了内核的各种行为,你需要根据你的硬件和应用需求来修改它。
#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H
#include "stm32f4xx.h" // 根据你的MCU型号修改
/* 基础配置 */
#define configUSE_PREEMPTION 1 // 使用抢占式调度
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1 // 使用优化的任务选择
#define configUSE_TICK_HOOK 0 // 不使用Tick Hook函数
#define configCPU_CLOCK_HZ (SystemCoreClock) // CPU主频
#define configTICK_RATE_HZ 1000 // 1KHz的节拍率,即1ms一个tick
/* 内存分配 */
#define configSUPPORT_STATIC_ALLOCATION 1 // 支持静态内存分配
#define configSUPPORT_DYNAMIC_ALLOCATION 1 // 支持动态内存分配
#define configTOTAL_HEAP_SIZE (15 * 1024) // 总堆大小15KB
#define configAPPLICATION_ALLOCATED_HEAP 0 // 不由应用程序分配堆
/* 任务相关 */
#define configMAX_PRIORITIES 5 // 最大任务优先级数(0-4)
#define configMINIMAL_STACK_SIZE 128 // 最小任务栈大小(字节数)
/* IDLE任务和钩子函数 */
#define configIDLE_SHOULD_YIELD 1 // IDLE任务可以主动让出CPU
#define configUSE_IDLE_HOOK 0 // 不使用IDLE Hook
#define configUSE_TICK_HOOK 0 // 不使用Tick Hook
/* 栈溢出检测 */
#define configCHECK_FOR_STACK_OVERFLOW 2 // 栈溢出检测级别
/* 断言定义 */
#define configASSERT( x ) if ((x) == 0) { taskDISABLE_INTERRUPTS(); for( ;; ); }
/* 中断配置 */
#define configKERNEL_INTERRUPT_PRIORITY 255 // 最低优先级
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 // 最高可屏蔽中断优先级
#define configLIBRARY_KERNEL_INTERRUPT_PRIORITY 15 // FreeRTOS内核中断优先级
#endif /* FREERTOS_CONFIG_H */
最佳实践和注意事项
- 栈大小设置: 栈大小是任务能使用的内存上限,设置太小会导致栈溢出,程序崩溃;设置太大会浪费内存,可以通过
uxTaskGetStackHighWaterMark()函数来监控任务的最大使用量,从而调整栈大小。 - 中断处理:
- 中断要尽可能短! 在中断中只做最紧急的事,如读取硬件状态、清除中断标志。
- 不要在中断中调用会阻塞的 API,如
vTaskDelay()或xQueueSend()(除非是FromISR版本)。 - 使用
FromISR版本的 API 与任务通信,并注意处理“发送完成”的返回值,确保任务能被正确唤醒。
- 优先级反转: 在访问共享资源时,始终使用互斥量来保护,而不是简单地用
taskENTER_CRITICAL()/taskEXIT_CRITICAL()(关中断)。 - 资源竞争: 避免在多个任务中直接修改全局变量,通过队列或信号量来传递数据,确保数据的一致性。
- 调试: 使用 RTOS 提供的调试工具,如任务状态查看器、栈溢出检测等,IAR Keil、Segger Ozone 等调试器都支持 FreeRTOS 的可视化调试。
在 C 语言中使用 FreeRTOS,核心就是围绕 “任务” 和 “同步/通信机制”(队列、信号量)来构建你的应用。
- 任务是你的“工人”,负责完成具体的工作。
- 调度器是“项目经理”,决定哪个工人该在什么时候工作。
- 队列/信号量是“工人们之间的沟通工具”或“工作许可证”。
通过合理地组合使用这些组件,你可以将复杂的嵌入式应用分解为多个清晰、独立、易于维护的任务,从而构建出稳定、高效、可靠的嵌入式系统。
