文章目录
前言
本文适合对FreeRTOS完全不了解的零基础小白入门。
一、FreeRTOS介绍
1.FreeRTOS概念
FreeRTOS 是一款开源、轻量级的实时操作系统(RTOS),专为资源受限的嵌入式设备设计。
抢占式任务调度:支持多任务并发执行,按优先级动态分配CPU时间。(并发执行就是很多事情同时做)
低内存占用:最小内存 footprint 可低至 几KB(适用于8位/16位MCU)。
跨平台支持:兼容ARM、AVR、PIC、RISC-V等多种架构。(同一段代码可以重复应用在不同芯片上,无需重复编写底层驱动)
2.FreeRTOS 核心功能
任务管理:(给控制器分配任务的,)
创建/删除任务(Task),设置优先级和堆栈大小。
支持任务阻塞(如 vTaskDelay())和唤醒(如事件触发)。
作用:通过创建/删除任务(xTaskCreate())、设置优先级(数值越小优先级越高)、动态调整堆栈大小,实现多任务并发执行。
意义:
抢占式调度确保高优先级任务(如实时控制)优先执行,满足实时性要求。
资源分配灵活:适应不同复杂度的任务(简单传感器采集 vs 复杂算法处理)。
同步机制:
任务间通信:队列(Queue)、信号量(Semaphore)、互斥锁(Mutex)。
事件通知(Event)和二值信号量(Binary Semaphore)。
作用:通过队列(Queue)、信号量(Semaphore)、互斥锁(Mutex)等工具,协调任务间通信与资源访问。
意义:
避免竞争条件:防止多个任务同时操作共享资源(如传感器数据或通信接口)。
事件驱动编程:通过信号量触发任务唤醒(如按键按下触发UI更新)。
时间管理:
软件定时器(Software Timer)和硬件定时器中断。
作用:提供软定时器(xTimerCreate())和硬件定时器中断,支持任务延时(vTaskDelay())和周期性事件。
意义:
精确控制时序:确保周期性任务(如PWM调速)按时执行。
超时检测:监测任务响应时间(如网络请求超时重试)。
内存管理:
动态内存分配(pvPortMalloc() / vPortFree())。
作用:动态分配内存(pvPortMalloc())并回收(vPortFree()),优化内存利用率。
意义:
减少内存碎片:在资源受限设备(如8位MCU)中避免内存耗尽。
适应动态需求:根据任务负载灵活调整内存分配(如临时缓存数据)。
二、核心功能示例代码
1.基础任务创建与调度
#include <FreeRTOS.h>
#include <task.h>
// 任务句柄声明
TaskHandle_t Task1_Handle = NULL;
TaskHandle_t Task2_Handle = NULL;
// 任务1函数
void Task1(void *pvParameters) {
while (1) {
printf("Task 1 running...\n");
vTaskDelay(pdMS_TO_TICKS(1000)); // 延迟1秒
}
}
// 任务2函数
void Task2(void *pvParameters) {
while (1) {
printf("Task 2 running...\n");
vTaskDelay(pdMS_TO_TICKS(500)); // 延迟0.5秒
}
}
int main() {
FreeRTOS.begin(); // 初始化FreeRTOS
// 创建任务
xTaskCreate(Task1, "Task1", 256, NULL, 1, &Task1_Handle); // 优先级1
xTaskCreate(Task2, "Task2", 256, NULL, 2, &Task2_Handle); // 优先级2
vTaskStartScheduler(); // 启动任务调度器
return 0;
}
展示多任务并发执行:任务1每秒打印一次,任务2每0.5秒打印一次。
优先级机制:优先级1的任务优先级高于优先级2,因此任务1的打印间隔更规律。
输出:
Task 1 running…
Task 2 running…
Task 1 running…
Task 2 running…
…
2.队列实现任务间通信
#include <FreeRTOS.h>
#include <task.h>
#include <queue.h>
QueueHandle_t xQueue = NULL;
void TaskProducer(void *pvParameters) {
int data = 0;
while (1) {
xQueueSend(xQueue, &data, portMAX_DELAY); // 发送数据到队列
data++;
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void TaskConsumer(void *pvParameters) {
int receivedData;
while (1) {
xQueueReceive(xQueue, portMAX_DELAY); // 从队列接收数据
printf("Received: %d\n", receivedData);
}
}
int main() {
FreeRTOS.begin();
xQueue = xQueueCreate(10, sizeof(int)); // 创建容量为10的队列
xTaskCreate(TaskProducer, "Producer", 256, NULL, 1, NULL);
xTaskCreate(TaskConsumer, "Consumer", 256, NULL, 2, NULL);
vTaskStartScheduler();
return 0;
}
(1)逐行解析
QueueHandle_t xQueue = NULL;
作用:声明一个队列句柄(指针),用于后续操作队列。
初始化:NULL表示队列尚未创建。
任务 TaskProducer(生产者)
功能:向队列发送递增的整数(0,1,2,…)。
任务 TaskConsumer(消费者)
功能:从队列中接收数据并打印。
xTaskCreate(TaskProducer, “Producer”, 256, NULL, 1, NULL);
xTaskCreate(TaskConsumer, “Consumer”, 256, NULL, 2, NULL);
生产者任务优先级为1,消费者为2(数字越小优先级越高)。
堆栈大小:256字节(足够简单任务使用)。
代码运行流程
初始化队列:队列最多存储10个整数。
创建任务:
生产者任务(优先级1):每0.5秒发送一个递增的整数到队列。
消费者任务(优先级2):每收到一个整数就打印它。
任务调度:
生产者任务先执行,发送数据到队列。
消费者任务随后执行,从队列中读取数据并打印。
队列满时:若队列已满(10个元素),生产者任务会被阻塞,直到队列有空闲空间。
队列空时:消费者任务会被阻塞,直到生产者发送新数据。
(2)总结
1.队列(Queue)的作用
任务间通信:允许不同任务共享数据,无需直接访问全局变量。
同步机制:生产者/消费者通过队列协调操作节奏(如“生产-消费”模式)。
FIFO特性:先发送的数据先被接收,保证数据顺序。
2.阻塞函数(portMAX_DELAY)
定义:任务在等待资源(如队列空间或数据)时进入阻塞状态,释放CPU使用权。
优势:
避免忙等待(Polling),节省能源。
提高系统响应性(其他任务可在此期间运行)。
3.任务优先级
生产者优先级1 < 消费者优先级2:
如果队列未满且消费者正在等待数据,消费者任务的优先级更高,会抢占生产者任务吗?
不会! 因为消费者任务只有在队列非空时才会执行 xQueueReceive,而生产者任务持续发送数据。
实际运行中:生产者任务每隔0.5秒发送一次数据,消费者几乎立即接收并打印,因此两个任务交替执行。
3.信号量实现资源保护
#include <FreeRTOS.h>
#include <task.h>
#include <semphr.h>
SemaphoreHandle_t xSemaphore = NULL;
void Task1(void *pvParameters) {
while (1) {
if (xSemaphoreTake(xSemaphore, portMAX_DELAY)) { // 获取信号量
printf("Task 1 acquired the semaphore!\n");
vTaskDelay(pdMS_TO_TICKS(1000)); // 延迟1秒
xSemaphoreGive(xSemaphore); // 释放信号量
}
}
}
void Task2(void *pvParameters) {
while (1) {
if (xSemaphoreTake(xSemaphore, portMAX_DELAY)) {
printf("Task 2 acquired the semaphore!\n");
vTaskDelay(pdMS_TO_TICKS(1000));
xSemaphoreGive(xSemaphore);
}
}
}
int main() {
FreeRTOS.begin();
xSemaphore = xSemaphoreCreateBinary(); // 创建二值信号量
xTaskCreate(Task1, "Task1", 256, NULL, 1, NULL);
xTaskCreate(Task2, "Task2", 256, NULL, 2, NULL);
vTaskStartScheduler();
return 0;
}
(1)逐行解析
全局变量 xSemaphore
作用:存储二值信号量的句柄,初始为 NULL(未创建)。
任务 Task1 和 Task2
核心逻辑:
任务循环中尝试获取信号量(xSemaphoreTake)。
若成功获取(!= pdFAIL),打印消息并延迟1秒后释放信号量(xSemaphoreGive)。
关键点:
互斥访问:同一时间只有一个任务能获取信号量。
阻塞等待:若信号量不可用,任务会被阻塞直到超时(portMAX_DELAY 表示无限等待)。
运行流程与输出
初始化:
创建信号量 xSemaphore(初始值为1)。
创建Task1(优先级1)和Task2(优先级2)。
任务执行:
第一次循环:
Task1成功获取信号量,打印消息并延迟1秒。
Task2尝试获取信号量,但信号量已被Task1占用,被阻塞。
第二次循环:
Task1释放信号量,Task2立即获取并打印消息。
Task1再次尝试获取信号量,成功后重复上述过程。
(2)总结
信号量的作用:
互斥控制:保护共享资源(如硬件引脚、通信接口),防止多个任务同时访问。
同步事件:任务A通过信号量通知任务B某个事件已发生。
二值信号量 vs 计数信号量:
二值信号量:只能取0或1,适用于互斥场景。
计数信号量:可取大于1的值,允许多次获取(需对应多次释放)。
阻塞与唤醒:
任务在信号量不可用时自动进入阻塞状态,释放CPU资源供其他任务使用。
信号量释放后,优先级最高的等待任务会被唤醒。
4.互斥锁保护共享变量
#include <FreeRTOS.h>
#include <task.h>
#include <semphr.h>
SemaphoreHandle_t xMutex = NULL;
int shared_counter = 0;
void Task1(void *pvParameters) {
while (1) {
xSemaphoreTake(xMutex, portMAX_DELAY); // 加锁
shared_counter++;
printf("Task 1: Counter = %d\n", shared_counter);
vTaskDelay(pdMS_TO_TICKS(500));
xSemaphoreGive(xMutex); // 解锁
}
}
void Task2(void *pvParameters) {
while (1) {
xSemaphoreTake(xMutex, portMAX_DELAY);
shared_counter--;
printf("Task 2: Counter = %d\n", shared_counter);
vTaskDelay(pdMS_TO_TICKS(500));
xSemaphoreGive(xMutex);
}
}
int main() {
FreeRTOS.begin();
xMutex = xMutexCreate(); // 创建互斥锁
xTaskCreate(Task1, "Task1", 256, NULL, 1, NULL);
xTaskCreate(Task2, "Task2", 256, NULL, 2, NULL);
vTaskStartScheduler();
return 0;
}
(1)逐行解析
全局变量
SemaphoreHandle_t xMutex = NULL;
int shared_counter = 0;
xMutex:互斥锁句柄。
shared_counter:需保护的共享变量(初始值为0)。
任务逻辑
void Task1(void *pvParameters) {
while (1) {
xSemaphoreTake(xMutex, portMAX_DELAY); // 加锁
shared_counter++;
printf("Task 1: Counter = %d\n", shared_counter);
vTaskDelay(pdMS_TO_TICKS(500));
xSemaphoreGive(xMutex); // 解锁
}
}
互斥锁操作:
xSemaphoreTake(xMutex):获取互斥锁(若被占用,任务阻塞)。
xSemaphoreGive(xMutex):释放互斥锁,允许其他任务获取。
共享变量操作:
递增 shared_counter 并打印结果。
互斥锁创建
xMutex = xMutexCreate();
xMutexCreate():创建一个互斥锁(初始为可用状态)。
运行流程与输出
初始化:
创建互斥锁 xMutex。
创建Task1(优先级1)和Task2(优先级2)。
任务执行:
Task1 先执行,获取互斥锁后递增 shared_counter 并打印。
Task2 尝试获取互斥锁,但被Task1占用,进入阻塞。
Task1 释放互斥锁后,Task2 获取锁并递减 shared_counter,打印结果。
循环执行,输出交替递增/递减的值。
(3)总结
互斥锁的作用:
原子操作:确保对共享变量的修改(如 shared_counter++)不可分割,避免竞态条件。
严格互斥:同一时间仅允许一个任务持有互斥锁。
与信号量的区别:
信号量:允许多次获取和释放(计数信号量),适用于资源池管理。
互斥锁:仅允许一次获取和释放,强制排他访问。
优先级反转:
若高优先级任务等待低优先级任务释放互斥锁,会发生优先级反转。
解决方案:使用 优先级继承(FreeRTOS默认开启)临时提升低优先级任务的优先级。
三.工程实际运用
1. 任务创建与删除 → 多传感器数据采集系统
场景:工业物联网设备(如环境监测仪)需同时采集温度、湿度、气压数据。
实现:
Task1:温度传感器采样(每秒一次)。
Task2:湿度传感器采样(每秒一次)。
Task3:气压传感器采样(每秒一次)。
动态删除:若某传感器故障(如温度超限),自动删除对应任务并报警。、
意义:
资源优化:故障时释放内存,避免无效任务占用CPU。
扩展性:新增传感器只需创建新任务,无需修改现有逻辑。
2.队列 → 工业PLC与执行器通信
场景:PLC(可编程逻辑控制器)通过队列向多个执行器(如电机、阀门)发送控制指令。
实现:
生产者任务(PLC):将指令(如“打开阀门1”)发送到队列。
消费者任务(执行器):从队列接收指令并执行。
// 执行器任务
void Task_Executor(void *pvParameters) {
Instruction_t instruction;
while (1) {
xQueueReceive(xQueue, &instruction, portMAX_DELAY);
actuator_execute(instruction.type, instruction.value);
}
}
意义:
解耦设计:PLC与执行器无需直接通信,通过队列隔离。
实时性保障:指令按顺序处理,避免执行器竞争。
3.信号量 → 电机控制中的方向保护
场景:步进电机需要通过两个任务(方向控制与速度调节)协同工作,但方向不能冲突。
实现:
信号量 xDirSemaphore:确保同一时间只允许一个任务修改方向。
Task1:根据用户输入设置方向(如“顺时针”)。
Task2:根据PID算法调整转速。
代码关联:
void Task_SetDirection(int direction) {
if (xSemaphoreTake(xDirSemaphore, portMAX_DELAY)) {
set_motor_direction(direction);
xSemaphoreGive(xDirSemaphore);
}
}
意义:
防冲突:避免方向指令与速度调节指令同时修改电机状态。
可靠性:适用于工业机械臂、无人机航向控制等场景。
4. 互斥锁 → 共享显示屏的数据更新
场景:物联网网关需要同时显示温度、湿度、网络状态,且数据更新频繁。
实现:
共享变量 display_data:存储当前显示内容。
Task1:每秒更新温度数据。
Task2:每5秒更新网络状态。
互斥锁保护:确保一次只更新一个任务的数据。
代码关联:
void Task_UpdateDisplay(int data_type, int value) {
xSemaphoreTake(xMutex, portMAX_DELAY);
display_data[data_type] = value;
update_LCD(display_data);
xSemaphoreGive(xMutex);
}
意义:
数据一致性:防止屏幕闪烁或显示错误(如温度和湿度数值交替跳动)。
扩展性:新增显示类型只需修改锁保护的变量,无需重构代码。