一、FreeRTOS 概述
FreeRTOS 是一款轻量级、开源的 实时操作系统(RTOS),专为嵌入式系统设计,广泛应用于物联网、工业控制、消费电子等领域。其核心特点包括:
轻量高效:内核最小可裁剪至 4KB 代码 + 几百字节 RAM,适合资源受限的微控制器(如 STM32、ESP32 等)。
可扩展性:支持任务管理、消息队列、信号量、定时器等功能,且可通过插件扩展(如文件系统、网络协议栈)。
开源免费:遵循 MIT 许可协议,允许商业使用,社区活跃且文档丰富。
跨平台兼容:支持 ARM Cortex-M、RISC-V、x86 等多种架构,提供 Keil、IAR、GCC 等开发环境的适配。
二、核心组件与机制
1. 任务管理(Task Management)
任务定义:每个任务是一个无限循环的函数,拥有独立的栈空间和优先级。
void vTaskFunction(void *pvParameters) {
for (;;) {
// 任务逻辑
vTaskDelay(pdMS_TO_TICKS(100)); // 延时调度
}
}
任务状态:
运行(Running):当前正在执行的任务。
就绪(Ready):具备运行条件,等待调度器分配 CPU 资源。
阻塞(Blocked):因等待事件(如延时、消息队列)暂停执行。
挂起(Suspended):被显式挂起,不参与调度。
创建与删除任务:
TaskHandle_t xTaskHandle;
// 创建任务(动态分配内存)
xTaskCreate(
vTaskFunction, // 任务函数
"TaskName", // 任务名称(调试用)
128, // 栈大小(字节)
NULL, // 参数
1, // 优先级(数值越大优先级越高)
&xTaskHandle // 任务句柄
);
// 删除任务(通常在任务内部自我删除)
vTaskDelete(xTaskHandle); // 或 vTaskDelete(NULL); // 删除自身
2. 优先级抢占机制(Preemption)
调度策略:
抢占式调度(默认):高优先级任务可中断低优先级任务的执行,确保实时性。
协作式调度:任务主动释放 CPU(如调用 vTaskDelay),适合对实时性要求不高的场景。
优先级数量:可配置为 1~32 级(通过 configMAX_PRIORITIES 定义),数值越大优先级越高。
临界区保护:通过 taskENTER_CRITICAL() 和 taskEXIT_CRITICAL() 关闭 / 开启中断,防止任务调度被打断(如操作共享资源时)。
3. 任务间通信(Inter-Task Communication, IPC)
消息队列(Queues):用于任务间传递数据(如字节数组、结构体),支持先进先出(FIFO)或优先级队列。
QueueHandle_t xQueue;
xQueue = xQueueCreate(5, sizeof(int)); // 创建队列(5个元素,每个元素4字节)
// 发送数据(阻塞模式,超时时间100ms)
int data = 100;
xQueueSend(xQueue, &data, pdMS_TO_TICKS(100));
// 接收数据(阻塞模式,永久等待)
int receive_data;
xQueueReceive(xQueue, &receive_data, portMAX_DELAY);
信号量(Semaphores):
二进制信号量:用于任务同步或互斥访问资源(如互斥锁)。
计数信号量:用于限制访问共享资源的任务数量(如限制最多 3 个任务同时使用打印机)。
事件组(Event Groups):用于任务等待多个事件的组合(如 “事件 1 发生且事件 3 发生”)。
4. 定时器(Software Timers)
周期性定时器:创建后按固定周期触发回调函数,用于定时任务(如传感器数据采集)。
TimerHandle_t xTimer;
// 创建定时器(周期1s,自动重装,回调函数)
xTimer = xTimerCreate(
"Timer", // 名称
pdMS_TO_TICKS(1000), // 周期
pdTRUE, // pdTRUE=自动重装,pdFALSE=单次
NULL, // 用户参数
vTimerCallback // 回调函数
);
xTimerStart(xTimer, 0); // 启动定时器
单次定时器:触发一次后停止,用于延迟任务执行。
三、内存管理与裁剪配置
1. 内存管理策略
FreeRTOS 提供 5 种内存分配方案(位于 heap_1.c~heap_5.c):
heap_1:简单的静态分配,适合只创建不删除任务的场景(无内存碎片)。
heap_2:基于块链表的动态分配,使用 pvPortMalloc/vPortFree,适合频繁创建 / 删除任务(存在轻微碎片)。
heap_3:封装标准库的 malloc/free,通过互斥锁实现线程安全。
heap_4:高效的动态分配(类似 glibc 的 ptmalloc),适合大多数场景(需设置堆起始地址和大小)。
heap_5:支持跨多个非连续内存块分配,适合内存不连续的系统(如包含外部 RAM)。
2. 配置文件(FreeRTOSConfig.h)
通过修改该文件裁剪功能,典型配置项:
#define configUSE_PREEMPTION 1 // 使能抢占式调度
#define configUSE_TICKLESS_IDLE 1 // 使能低功耗tickless模式
#define configMAX_PRIORITIES 5 // 最大优先级数
#define configTICK_RATE_HZ 1000 // 系统滴答频率(1ms中断一次)
#define configMINIMAL_STACK_SIZE 128 // 任务最小栈大小(字节)
#define INCLUDE_vTaskDelete 1 // 使能vTaskDelete函数
四、实时性与性能优化
减少中断延迟:
缩短中断服务程序(ISR)执行时间,关键操作可通过消息队列传递给任务处理。
使用 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 设置可安全调用 RTOS API 的最高中断优先级。
任务优先级分配:
高实时性任务(如电机控制)设为高优先级,低优先级任务处理非关键业务(如日志打印)。
避免优先级反转(高优先级任务等待低优先级任务释放资源),可通过优先级继承机制(需开启 configUSE_MUTEXES)缓解。
栈溢出检测:
开启 configCHECK_FOR_STACK_OVERFLOW,通过回调函数检测任务栈溢出(调试阶段常用)。
五、典型应用场景
多任务并发控制:
案例:同时运行 LED 控制任务、按键检测任务、串口通信任务,通过消息队列传递按键状态给 LED 任务。
实时传感器处理:
高优先级任务周期性采集传感器数据(如 ADC 采样),低优先级任务处理数据并通过 WiFi 发送。
低功耗系统:
利用 tickless idle 模式,在无任务运行时关闭系统滴答定时器,降低 MCU 功耗。
一、任务创建与删除(FreeRTOS API 详解)
不管是静态创建还是动态创建任务,都是来自SRAM区。静态创建任务来自SRAM区的堆栈,动态创建任务来自SRAM区的总堆。
1. 任务创建(xTaskCreate)
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode, // 任务函数指针(入口函数)
const char * const pcName, // 任务名称(用于调试,最大 configMAX_TASK_NAME_LEN 字符)
configSTACK_DEPTH_TYPE usStackDepth, // 任务栈大小(单位:字,非字节)
void *pvParameters, // 传递给任务函数的参数(void* 类型)
UBaseType_t uxPriority, // 任务优先级(0 为最低优先级)
TaskHandle_t *pxCreatedTask // 任务句柄(用于后续操作,如删除任务)
);
void vTaskFunction(void *pvParameters) {
// 任务代码
for (;;) {
// 任务主体逻辑
vTaskDelay(pdMS_TO_TICKS(100)); // 延时100ms
}
}
// 创建任务
TaskHandle_t xTaskHandle = NULL;
xTaskCreate(vTaskFunction, "TaskName", 256, NULL, 1, &xTaskHandle);
2. 任务删除(vTaskDelete)
void vTaskDelete(TaskHandle_t xTaskToDelete);
- 参数:
xTaskToDelete
为任务句柄,传入NULL
时删除当前正在执行的任务。
- 注意:被删除任务的栈空间和 TCB(任务控制块)会被 FreeRTOS 自动回收,但需手动释放任务中动态分配的资源(如 malloc 的内存)。
-
// 在其他任务中删除指定任务 vTaskDelete(xTaskHandle); // 在任务内部自我删除 vTaskDelete(NULL);
二、优先级抢占机制(核心调度规则)
1. 优先级定义与范围
- 数值范围:
- 优先级从
0
(最低)到configMAX_PRIORITIES-1
(最高),通过uxPriority
参数设置。 - 建议使用
tskIDLE_PRIORITY
(空闲任务优先级,值为 0)作为基准,例如:#define TASK_PRIORITY (tskIDLE_PRIORITY + 1) // 比空闲任务高一级
- 优先级从
- 抢占规则:
- 高优先级任务 可立即抢占 低优先级任务 的 CPU 使用权(若内核配置为可抢占模式,
configUSE_PREEMPTION
为 1)。 - 相同优先级任务 按时间片轮转(时间片长度由
configTICK_RATE_HZ
决定,默认 10ms)。
- 高优先级任务 可立即抢占 低优先级任务 的 CPU 使用权(若内核配置为可抢占模式,
2. 调度器行为示例
假设有三个任务:
- TaskA(优先级 2,周期 100ms)
- TaskB(优先级 1,周期 200ms)
- TaskC(优先级 3,周期 500ms)
调度时序:
- TaskC(最高优先级)运行,直到主动阻塞(如调用
vTaskDelay()
)。 - TaskA(次高优先级)抢占 TaskB,运行 100ms 后阻塞。
- TaskB(最低优先级)获得 CPU,运行 200ms 后阻塞。
- 若所有任务均阻塞,空闲任务(优先级 0)运行,直到有任务就绪。
三、关键配置参数(FreeRTOSConfig.h)
1. 抢占式调度 vs 合作式调度
#define configUSE_PREEMPTION 1 // 1=抢占式调度(默认),0=合作式调度
- 抢占式:高优先级任务可立即中断低优先级任务。
- 合作式:任务仅在主动放弃 CPU(如调用
taskYIELD()
)时切换。
2. 优先级数量限制
#define configMAX_PRIORITIES 5 // 允许的最大优先级数量
3. 时间片调度
#define configUSE_TIME_SLICING 1 // 1=启用时间片(默认),0=禁用
- 仅对 相同优先级任务 有效,每个任务运行一个时间片(由
configTICK_RATE_HZ
决定)后切换。
四、实战技巧与注意事项
1. 任务优先级设计原则
- 关键任务(如传感器数据采集):设为高优先级(如 3)。
- 用户交互任务(如按键处理):设为中等优先级(如 2)。
- 后台任务(如数据处理):设为低优先级(如 1)。
- 空闲任务(系统自动创建):优先级最低(0)。
2. 避免优先级反转问题
- 场景:低优先级任务持有互斥锁时,被中优先级任务抢占,导致高优先级任务无法获取锁而阻塞。
- 解决方案:
- 使用 互斥量(Mutex)代替二值信号量,支持 优先级继承(
configUSE_MUTEXES
需为 1)。
- 使用 互斥量(Mutex)代替二值信号量,支持 优先级继承(
SemaphoreHandle_t xMutex;
xMutex = xSemaphoreCreateMutex(); // 创建互斥量
// 获取锁(在任务中)
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// 临界区代码
xSemaphoreGive(xMutex); // 释放锁
}
3. 任务创建与删除的线程安全
- 创建任务:可在调度器启动前或运行中调用,但需注意:
- 调度器启动前创建的任务会在
vTaskStartScheduler()
后立即执行。 - 调度器运行中创建任务需确保堆内存充足(FreeRTOS 使用动态内存分配 TCB 和栈)。
- 调度器启动前创建的任务会在
- 删除任务:避免在中断服务函数(ISR)中直接调用
vTaskDelete()
,应使用xTimerPendFunctionCallFromISR()
替代。
五、调试与验证
1. 查看任务状态
通过 uxTaskGetSystemState()
和 vTaskList()
函数获取任务运行时信息:
char pcTaskList[512];
vTaskList(pcTaskList); // 获取任务列表(包含优先级、状态、栈使用等)
printf("Task List:\n%s\n", pcTaskList);
2. 优先级验证
- 在低优先级任务中添加高优先级任务创建代码,观察调度器行为:
void vLowPriorityTask(void *pvParameters) { printf("Low priority task running...\n"); // 创建高优先级任务 xTaskCreate(vHighPriorityTask, "HighTask", 256, NULL, 3, NULL); printf("Low priority task continues...\n"); // 此行不会立即执行(被高优先级任务抢占) }
创建完任务后,需要开启调度器-vTaskStartScheduler()函数。vTaskStartScheduler()函数实现了空闲任务和定时器任务。
FreeRTOS 一旦启动,就必须要保证系统中每时每刻都有一个任务处于运行态(Runing),并且空闲任务不可以被挂起与删除, 空闲任务的优先级是最低的,以便系统中其他任务能随时抢占空闲任务的 CPU 使用权。
项目 5:多任务 LED 控制
- Task1:呼吸灯(PWM)
- Task2:按键状态检测
// FreeRTOS任务配置
#define TASK_BLINK_PRIORITY (tskIDLE_PRIORITY + 2) // 呼吸灯任务优先级
#define TASK_BLINK_STACK_SIZE 256 // 任务栈大小(字)
#define TASK_KEY_PRIORITY (tskIDLE_PRIORITY + 1) // 按键检测任务优先级
#define TASK_KEY_STACK_SIZE 128 // 任务栈大小(字)
// PWM配置
#define PWM_FREQ_HZ 1000 // PWM频率(Hz)
#define PWM_PERIOD (1000000 / PWM_FREQ_HZ) // PWM周期(微秒)
#define PWM_MAX_DUTY 100 // 最大占空比(%)
// 按键配置
#define KEY_DEBOUNCE_TIME 20 // 消抖时间(ms)
// 初始化PWM输出(假设使用TIM3_CH1,对应GPIOA_PIN6)
void PWM_Init(void) {
TIM_HandleTypeDef htim3;
TIM_OC_InitTypeDef sConfigOC;
// 使能定时器和GPIO时钟
__HAL_RCC_TIM3_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置GPIO为复用推挽输出
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置定时器
htim3.Instance = TIM3;
htim3.Init.Prescaler = 72 - 1; // 72MHz / 72 = 1MHz
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = PWM_PERIOD - 1; // 1MHz / 1000 = 1kHz
htim3.Init.ClockDivision = 0;
HAL_TIM_PWM_Init(&htim3);
// 配置PWM输出
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 0; // 初始占空比为0%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);
// 启动PWM输出
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}
// 设置PWM占空比
void PWM_SetDutyCycle(uint8_t duty) {
// 将0-100%的占空比转换为定时器计数值
uint32_t pulse = (duty * PWM_PERIOD) / 100;
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulse);
}
// 初始化按键输入(假设使用GPIOA_PIN0)
void KEY_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIO时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置GPIO为输入模式,上拉
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
// 读取按键状态(已消抖)
uint8_t KEY_Read(void) {
static uint8_t key_state = 0;
static uint32_t debounce_time = 0;
uint8_t current_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
if (current_state != key_state) {
// 状态变化,开始消抖计时
debounce_time = HAL_GetTick();
key_state = current_state;
}
// 消抖时间后返回稳定状态
if (HAL_GetTick() - debounce_time > KEY_DEBOUNCE_TIME) {
return key_state;
}
return key_state;
}
// 呼吸灯任务
void vTaskBlink(void *pvParameters) {
uint8_t duty = 0; // 初始占空比
uint8_t direction = 1; // 1=增加,0=减少
for (;;) {
// 更新PWM占空比,实现呼吸灯效果
PWM_SetDutyCycle(duty);
// 调整占空比变化方向
if (duty >= PWM_MAX_DUTY) {
direction = 0;
} else if (duty <= 0) {
direction = 1;
}
// 根据方向调整占空比
duty += (direction ? 1 : -1);
// 延时,控制呼吸灯变化速度
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// 按键检测任务
void vTaskKey(void *pvParameters) {
uint8_t key_last_state = 1; // 初始状态:按键释放
for (;;) {
uint8_t key_current_state = KEY_Read();
// 检测按键下降沿(按下)
if (key_last_state == 1 && key_current_state == 0) {
printf("按键按下!\n");
// 此处可添加按键按下的处理逻辑
}
key_last_state = key_current_state;
// 延时,控制按键扫描频率
vTaskDelay(pdMS_TO_TICKS(20));
}
}
int main(void) {
// 硬件初始化
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
PWM_Init();
KEY_Init();
// 创建任务
xTaskCreate(vTaskBlink, "Blink", TASK_BLINK_STACK_SIZE, NULL, TASK_BLINK_PRIORITY, NULL);
xTaskCreate(vTaskKey, "Key", TASK_KEY_STACK_SIZE, NULL, TASK_KEY_PRIORITY, NULL);
// 启动调度器
vTaskStartScheduler();
// 如果程序执行到这里,说明发生了错误
for (;;);
}
若需实现按键控制呼吸灯开关,可添加任务间通信机制
// 创建二值信号量(用于控制呼吸灯开关)
SemaphoreHandle_t xLedControl;
// 在main()中初始化信号量
xLedControl = xSemaphoreCreateBinary();
xSemaphoreGive(xLedControl); // 默认允许呼吸灯运行
// 修改呼吸灯任务
void vTaskBlink(void *pvParameters) {
uint8_t duty = 0;
uint8_t direction = 1;
for (;;) {
// 获取信号量,控制呼吸灯是否运行
if (xSemaphoreTake(xLedControl, portMAX_DELAY) == pdTRUE) {
// 更新PWM占空比
PWM_SetDutyCycle(duty);
// 调整占空比
if (duty >= PWM_MAX_DUTY) direction = 0;
if (duty <= 0) direction = 1;
duty += (direction ? 1 : -1);
// 释放信号量
xSemaphoreGive(xLedControl);
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// 修改按键任务(检测到按键按下时切换呼吸灯状态)
void vTaskKey(void *pvParameters) {
uint8_t key_last_state = 1;
static uint8_t led_enabled = 1; // 呼吸灯默认开启
for (;;) {
uint8_t key_current_state = KEY_Read();
if (key_last_state == 1 && key_current_state == 0) {
// 按键按下,切换呼吸灯状态
led_enabled = !led_enabled;
if (led_enabled) {
printf("呼吸灯开启\n");
xSemaphoreGive(xLedControl); // 允许呼吸灯运行
} else {
printf("呼吸灯关闭\n");
xSemaphoreTake(xLedControl, 0); // 禁止呼吸灯运行
PWM_SetDutyCycle(0); // 立即熄灭LED
}
}
key_last_state = key_current_state;
vTaskDelay(pdMS_TO_TICKS(20));
}
}