FreeRTOS是开源的实时操作系统(Real Time Operation System, RTOS)

什么是RTOS

很多人看到操作系统这个字眼就下意识觉得这东西和Windows、Linux等系统一样, 是庞大复杂的东西, 从而望而生畏。但在嵌入式领域, RTOS是广泛应用的, 且上手门槛并不高。

可以简单理解为把裸机编程的单线程的中断/循环轮询驱动, 变为RTOS中的非阻塞与多线程工作, 这些活都交给RTOS内核干, 而我们要做的只是把几个任务创建好。在多外设等复杂系统中, RTOS可以说是必不可少。

在嵌入式网络编程中, 启用RTOS是 LwIP中支持socket/netconn API的必需条件

为什么使用FreeRTOS?

开始

以下所有内容是面向新手、便于理解的版本。更细节的编程手册详见:

这里不讨论关于RTOS移植到新平台的问题, 只基于现有平台(STM32CubeMX、ESP-IDF)。

Core/Src/main.c
int main(void)  
{  
  
  /* USER CODE BEGIN 1 */  
  
  /* USER CODE END 1 */  
  /* MCU Configuration--------------------------------------------------------*/  
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */  
  HAL_Init();  
  
  /* USER CODE BEGIN Init */  
  
  /* USER CODE END Init */  
  /* Configure the system clock */  
  SystemClock_Config();  
  
  /* USER CODE BEGIN SysInit */  
  
  /* USER CODE END SysInit */  
  /* Initialize all configured peripherals */  
  MX_GPIO_Init();  
  MX_DMA_Init();  
  MX_ADC1_Init();  
  MX_I2C1_Init();  
  MX_HRTIM1_Init();  
  MX_SPI1_Init();  
  MX_ADC2_Init();  
  MX_ADC3_Init();  
  MX_ADC4_Init();  
  MX_USART3_UART_Init();  
  MX_TIM2_Init();  
  /* USER CODE BEGIN 2 */  
  
  /* USER CODE END 2 */  
  /* Init scheduler */  
  osKernelInitialize(); 
  /* Call init function for freertos objects (in cmsis_os2.c) */  
  MX_FREERTOS_Init();  
  /* Start scheduler */  
  osKernelStart();  
  
  /* We should never get here as control is now taken by the scheduler */  
  
  /* Infinite loop */  /* USER CODE BEGIN WHILE */  
  while(1)  
  {  
    /* USER CODE END WHILE */  
  
    /* USER CODE BEGIN 3 */  
  }  
  /* USER CODE END 3 */  
}

以上是STM32启用了FreeRTOS的初始化代码。我们只看最后三行关于RTOS初始化的代码。

第一行为初始化整个RTOS。第二行为创建一个任务(主任务, 也可以创建其他的任务), 但此时还没有开始运行。第三行为启动调度器, 整个系统开始运转。这个函数不会返回, 作为调度器的主进程一直运行, 所以后面注释写不会运行到while(1)处。

再看创建线程:

Core/Src/app_freertos.c
osThreadId_t defaultTaskHandle;  
 
const osThreadAttr_t defaultTask_attributes = {  
  .name = "defaultTask",  
  .priority = (osPriority_t) osPriorityNormal,  
  .stack_size = 1024 * 4  
};
 
void StartDefaultTask(void *argument)  
{  
  /* USER CODE BEGIN StartDefaultTask */  
  /* Infinite loop */  
  app_main();  
  // while(1)  
  // {  
  //   vTaskDelay(100);  
  // }  
  vTaskDelete(NULL);  
  /* USER CODE END StartDefaultTask */  
}
 
void MX_FREERTOS_Init(void)
{  
    defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);  
}

函数内只有这一行, 即创建一个任务, 任务函数为StartDefaultTask

Note

cmsis对FreeRTOS的API做了一层封装, 即osThreadNew等这些函数, 用于为包括FreeRTOS在内的多种RTOS(如RT-Threadembeddos等)搭建一个统一的抽象层, 但这只适用于STM32平台, 如果你还没习惯使用这些API, 那么建议直接用FreeRTOS的原生api来操作。

这里在主任务中调用app_main(), 在其中写我们的业务代码。这也是ESP-IDF的风格, 它的整个程序入口就是app_main(), HAL层由系统配置, 只给出主任务的接口, 原生即FreeRTOS。

User/main/main.c
void app_main(void)  
{  
    xTaskCreate(LED_task0, "LED", 128, NULL, 10, NULL);  
    //在这里进行业务代码。
}

以上就是STM32平台下FreeRTOS的初始化流程。对的非常简便, 我们不需要与操作系统交互, 只写业务代码即可

RTOS 基本用法

以下组件标题可点击查看官网的文档

  1. 任务相关
  • 创建任务
函数原型:
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
                   const char * const pcName,
                   const configSTACK_DEPTH_TYPE usStackDepth,
                   void * const pvParameters,
                   UBaseType_t uxPriority,
                   TaskHandle_t * const pxCreatedTask)
 
    • pxTaskCode: 任务函数, 签名必须为void (*)(void *) 即不返回值, 传入参数为void*的函数。这个void*用于接收创建任务时的初始参数(因为通用任务可能有多种用途, 所以可用传入参数决定)。
    • pcName: 任务名称, 用于标识
    • pvParameters: 传入参数的指针, 如果无传入参数填NULL
    • usStackDepth: 栈大小, 原生FreeRTOS中这个参数的单位是字(word), 即4字节, 因平台而异。
    • uxPriority: 优先级, 优先级数字越小, 优先度越低。
    • pxCreatedTask: 任务句柄的指针, 用于控制任务(删除、暂停等)

用法:

void taskToCreate(void* pvParameter)
{
    int var1;
    while(1)
    {
        //do something;
        vTaskDelay(10);
    }
}
 
void app_main()
{
    TaskHanle_t taskHandle;
    xTaskCreate(taskToCreate, "Name", 256, NULL, 10, &taskHandle);
}
  • 删除任务

Tip

FreeRTOS中任务函数不可以返回, 必须由vTaskDelete删除

void app_main()
{
    TaskHanle_t taskHandle;
    xTaskCreate(taskToCreate, "Name", 256, NULL, 10, &taskHandle);
    vTaskDelete(taskHandle); // 在其他线程中调用句柄删除
}
 
或者:
 
void taskToCreate(void* pvParameter)
{
    int var1;
    while(1)
    {
        //do something;
        vTaskDelay(10);
        break;
    }
    
    vTaskDelete(NULL); //在任务中调用, 传入空指针删除自身任务
}
  • 暂停/恢复
vTaskSuspend(taskHandle);
//传入需要暂停的任务的句柄
vTaskSuspend(NULL);
//暂停自身任务
 
vTaskResume(taskHandle);
//恢复任务运行 只能由其他任务调用(因为暂停了不能恢复自身)

RTOS组件

多线程引入了问题: 线程间通信和同步、竞态等

线程间通信

事件组(EventGroup)

  • 即一个32位整数, 每一位都代表一个标志位(Flag). 适合传递多个状态的组合。
//Example by Gemini
#include "FreeRTOS.h"
#include "event_groups.h"
 
// 1. 定义事件位 (Bitmask)
#define BIT_WIFI_CONNECTED    (1 << 0) // 第0位
#define BIT_SERVER_CONNECTED  (1 << 1) // 第1位
#define BIT_ALL_READY         (BIT_WIFI_CONNECTED | BIT_SERVER_CONNECTED)
 
// 定义事件组句柄
EventGroupHandle_t xSystemEvents;
 
// --- 任务 A:负责设置状态 (比如网络任务) ---
void TaskNetwork(void *pvParameters)
{
    // 模拟连接 WiFi
    vTaskDelay(pdMS_TO_TICKS(2000));
    printf("WiFi Connected!\r\n");
    xEventGroupSetBits(xSystemEvents, BIT_WIFI_CONNECTED);
 
    // 模拟连接服务器
    vTaskDelay(pdMS_TO_TICKS(1000));
    printf("Server Connected!\r\n");
    xEventGroupSetBits(xSystemEvents, BIT_SERVER_CONNECTED);
    
    vTaskDelete(NULL); // 任务完成使命,自杀
}
 
// --- 任务 B:等待所有条件满足 (主逻辑) ---
void TaskMainWork(void *pvParameters)
{
    EventBits_t uxBits;
    printf("Waiting for network...\r\n");
 
    // 等待事件
    // 参数1: 句柄
    // 参数2: 等待的位 (0x03)
    // 参数3: pdTRUE = 退出时清除这些位 (复位)
    // 参数4: pdTRUE = 等待“所有”位都变1 (AND逻辑); pdFALSE = 任意一位变1就醒 (OR逻辑)
    // 参数5: 超时时间
    uxBits = xEventGroupWaitBits(xSystemEvents, 
                                 BIT_ALL_READY, 
                                 pdTRUE, 
                                 pdTRUE, 
                                 portMAX_DELAY);
 
    if ((uxBits & BIT_ALL_READY) == BIT_ALL_READY)
    {
        printf("All systems GO! Starting main logic...\r\n");
        // 开始核心业务...
    }
}
 
// --- 初始化 ---
void Init_App(void)
{
    xSystemEvents = xEventGroupCreate();
    // 创建任务...
}

消息队列(Queue)

  • 可存放指定个数的指定大小的数据。

  • 队列会将接收的数据复制一份保存, 所以如果数据量很大, 可以使用静态缓冲区, 传递缓冲区的指针。

  • 发送方在队列满、接收方在队列空时均可可阻塞等待。

//Example by Gemini
#include "FreeRTOS.h" 
#include "task.h" 
#include "queue.h" 
 
// 1. 定义要传输的数据结构 
typedef struct {
    uint8_t sensor_id;
    float temperature;
    float humidity;
} SensorData_t;
 
// 定义队列句柄
QueueHandle_t xSensorQueue;
 
// --- 任务 A:发送者 (比如传感器读取) ---
void TaskSender(void *pvParameters)
{
    SensorData_t myData;
    myData.sensor_id = 1;
    while(1)
    { 
        // 模拟采集数据 
        myData.temperature = 25.5f;
        myData.humidity = 60.0f; 
        // 发送数据到队列
        // 参数: 句柄, 数据指针, 等待时间(如果队列满了)
        if (xQueueSend(xSensorQueue, &myData, pdMS_TO_TICKS(10)) == pdPASS)
        {
            printf("Data sent successfully\r\n");
        }
        else
        {
            printf("Queue is full!\r\n");
        }
        vTaskDelay(pdMS_TO_TICKS(1000)); 
    }
} 
// --- 任务 B:接收者 (比如屏幕显示) --- 
void TaskReceiver(void *pvParameters)
{
    SensorData_t receivedData;
    while(1)
    { 
    // 从队列接收数据 
    // 参数: 句柄, 接收缓存指针, 等待时间(如果队列空了) 
    // portMAX_DELAY 表示死等,直到有数据来 
    if (xQueueReceive(xSensorQueue, &receivedData, portMAX_DELAY) == pdPASS)
    {
        printf("ID: %d, Temp: %.1f, Humi: %.1f\r\n", receivedData.sensor_id, receivedData.temperature, receivedData.humidity);
        }
    }
}
// --- 初始化代码 (在 main 或 defaultTask 中) ---
void Init_App(void)
{ 
    // 创建队列:深度为 10,每个单元大小为 sizeof(SensorData_t)
    xSensorQueue = xQueueCreate(10, sizeof(SensorData_t));
    if (xSensorQueue != NULL)
    {
        xTaskCreate(TaskSender, "Sender", 1024, NULL, 1, NULL);
        xTaskCreate(TaskReceiver, "Receiver", 1024, NULL, 2, NULL);
    } 
}

消息通知(TaskNotify)

  • 向任务自带的邮箱中发送一个32位的整数。它的内存占用小、速度也快。

  • 限制/特点: 一对一发送。适合传递整数命令且不需要缓存多条的情况。

线程间同步

信号量(Semaphore)

即字面意思, 信号的量, 分为二值信号量(Binary Semaphore)、计数信号量(Counting Semaphore)、互斥锁(Mutex)、递归互斥锁(Recursive Mutex)

二值信号量

  • 只能被设为1或0, 用于记录某事件是否发生、状态是否就绪等, 并且获取到信号量后自动清零。主要用于任务间同步(在中断中发送信号量, 阻塞等待的任务被唤醒进行处理)

  • 当信号量被置1时只有若有多个任务在同时等待信号量, 则只有最高优先级的会被唤醒。

  • 相较于事件组更轻量、省资源。可以理解为只有一位的、点对点的事件组。

计数信号量

  • 每有一次Give()计数加一, 获取时计数减一。可用于管理有限的资源池(例如TCP netconn连接数), 拿走资源时获取信号量, 归还资源时给出信号量。

互斥锁

  • 为多个线程可能会同时访问的资源(变量、函数等)加锁, 持有互斥锁代表 “我现在在用这个资源, 其他线程不要访问/修改, 以免出现冲突”。

  • 可为高频访问、修改的缓冲区加锁以保证访问的原子性。

  • 持锁、释放锁的任务必须是同一个, 任务A持锁不能由任务B释放锁。

  • 互斥锁的特性、核心机制: 优先级继承

优先级继承:

  • 场景:低优先级任务 A 占用了互斥锁,高优先级任务 B 也要用这个锁。
  • 机制:FreeRTOS 会临时将 A 的优先级提升到与 B 一样高,让 A 尽快运行完并释放锁,从而避免 B 等待过久(解决优先级翻转问题)。但其实无法完全解决, 所以在写程序的时候就要避免任务长时间占用CPU, 从根源解决

如果没有优先级继承会导致的问题: 优先级翻转

优先级翻转:

  • 场景: 高优先级任务A需要持锁, 但低优先级任务C已经提前持锁, 所以任务A等待C释放锁。
  • 问题: 但此时中优先级B一直占用CPU, 导致任务C得不到处理时间, 所以无法释放锁, 从而导致A一直阻塞, 所以看上去是任务B优先于任务A运行, 即优先级翻转。

所以互斥锁就是有优先级继承、初值为1的二值信号量。

递归互斥锁

和互斥锁对二值信号量的改装一样, 递归互斥锁也是对计数互斥锁的改造, 允许优先级继承, 允许一个线程多次持锁而不会发生死锁, 持锁、释放锁次数相同。