所用环境:stm32CubeMX + cmake + vscode

前言:

在嵌入式开发中,随着功能复杂度提升,传统裸机开发的 “顺序执行 + 延时阻塞” 模式逐渐暴露瓶颈:实时性差、资源利用率低、功能扩展困难。FreeRTOS 作为一款轻量级实时操作系统,通过多任务调度机制完美解决这些问题,成为嵌入式开发的主流选择。

一、FreeRTOS 核心概念:从 “裸机思维” 到 “多任务思维”

1. 什么是 FreeRTOS?

FreeRTOS 是一款开源的实时操作系统内核(RTOS),核心功能是任务调度—— 通过优先级机制让多个任务 “并发” 运行(逻辑上的并行,物理上由 CPU 分时切换实现)。其优势在于:

  • 实时性:高优先级任务可抢占低优先级任务,确保紧急事件优先处理;
  • 资源高效:任务阻塞时(如延时、等待资源),CPU 自动切换到其他任务,避免资源浪费;
  • 模块化:功能按任务拆分,代码结构清晰,便于维护和扩展。

2. 核心术语解析

  • 任务(Task):最小的执行单元,每个任务是一个独立的while(1)循环,包含专属的栈空间和运行状态(就绪、运行、阻塞、挂起)。
  • 调度器(Scheduler):FreeRTOS 的核心,负责根据任务优先级和状态切换任务,无需开发者手动干预。
  • 优先级(Priority):任务的重要程度(0~configMAX_PRIORITIES-1,数值越大优先级越高),高优先级任务可抢占低优先级任务。
  • 阻塞(Blocking):任务因等待资源(如延时、信号量)暂时停止运行,释放 CPU 给其他任务。
  • 任务间通信:多任务协作的基础,包括全局变量、消息队列、信号量等机制。

3. 从裸机到多任务的转变

最初我未能跳出裸机开发的固有思维,仍执着于在主函数中手动调度各个功能函数;但当vTaskStartScheduler()被调用后,任务的调度权便完全交由 FreeRTOS 内核接管 —— 此时我们的核心工作,是在拆分好的各个任务函数中,妥善处理任务间的通信协作与各自的业务逻辑实现。

场景 裸机开发 FreeRTOS 多任务
周期性任务 顺序执行 +delay,周期相互干扰 每个任务独立vTaskDelay,周期精准
资源利用 delay期间 CPU 空闲,浪费资源 阻塞时自动切换任务,CPU 利用率 100%
实时响应 低优先级任务可能阻塞高优先级事件 高优先级任务可抢占,响应及时
代码结构 单函数内堆砌逻辑,耦合度高 按功能拆分任务,模块化清晰

二、FreeRTOS 基础使用:任务的创建与运行

1. 任务的基本结构

每个任务是一个void (*)(void*)类型的函数,包含初始化逻辑和无限循环,示例:

// 温度采集任务
void temp_task(void *pvParameters) {
    // 1. 初始化(仅执行一次)
    temp_sensor_init();
    
    // 2. 任务主循环(无限执行)
    while(1) {
        // 业务逻辑:采集温度
        float temp = temp_sensor_read();
        // 延时3秒(阻塞期间CPU切换到其他任务)
        vTaskDelay(pdMS_TO_TICKS(3000));
    }
}

2. 任务的创建与启动

通过xTaskCreate创建任务,最后调用vTaskStartScheduler启动调度器:

int main(void) {
    // 1. 硬件初始化(时钟、外设等)
    hardware_init();
    
    // 2. 创建任务
    xTaskCreate(
        temp_task,       // 任务函数
        "TempTask",      // 任务名称(调试用)
        128,             // 栈大小(单位:字,如STM32中1字=4字节)
        NULL,            // 传递给任务的参数
        1,               // 优先级(1级,较低)
        NULL             // 任务句柄(无需记录时设为NULL)
    );
    
    xTaskCreate(
        humi_task,       // 湿度采集任务
        "HumiTask", 
        128, 
        NULL, 
        1,               // 与温度任务同优先级
        NULL
    );
    
    // 3. 启动调度器,任务开始运行
    vTaskStartScheduler();
    
    // 调度器启动后不会执行到此处(除非内存不足)
    while(1) {}
}

三、任务间通信:多任务协作的核心

多任务并行时,不可避免需要交互(如按键任务通知 OLED 任务更新显示)。FreeRTOS 提供多种通信机制,核心原则是避免直接调用任务函数,通过间接方式实现协作。

1. 全局变量(状态机)

适用于传递简单状态(如 “显示模式”“运行状态”),需配合互斥锁(Mutex)避免数据竞争。

示例:按键触发 OLED 显示切换

// 1. 定义共享状态和互斥锁
typedef enum {
    SHOW_TEMP,    // 显示温度
    SHOW_HUMI,    // 显示湿度
    SHOW_KEY      // 显示按键信息
} OledMode;

OledMode g_oled_mode = SHOW_TEMP;  // 全局状态变量
osMutexId_t g_mode_mutex;          // 保护状态的互斥锁

// 2. 按键任务:修改状态
void key_task(void *arg) {
    while(1) {
        if (key_pressed()) {  // 检测按键
            // 申请互斥锁,安全修改状态
            osMutexWait(g_mode_mutex, osWaitForever);
            g_oled_mode = SHOW_KEY;  // 切换到按键显示模式
            osMutexRelease(g_mode_mutex);
        }
        vTaskDelay(pdMS_TO_TICKS(50));  // 50ms检测一次
    }
}

// 3. OLED任务:根据状态更新显示
void oled_task(void *arg) {
    while(1) {
        // 读取当前状态(加锁保护)
        osMutexWait(g_mode_mutex, osWaitForever);
        OledMode current_mode = g_oled_mode;
        osMutexRelease(g_mode_mutex);
        
        // 根据状态执行对应逻辑
        switch(current_mode) {
            case SHOW_TEMP: oled_show_temp(); break;
            case SHOW_HUMI: oled_show_humi(); break;
            case SHOW_KEY:  oled_show_key(); break;
        }
        vTaskDelay(pdMS_TO_TICKS(100));  // 100ms刷新一次
    }
}

2. 消息队列(Queue)

适用于需要传递具体信息(如按键值、传感器数据)的场景,任务间通过 “发送 / 接收消息” 异步通信,无需共享变量。

示例:按键任务发送指令给 OLED 任务

// 1. 定义消息结构体和队列
typedef struct {
    uint8_t type;   // 消息类型:0=温度,1=按键
    char data[20];  // 消息内容
} OledMsg;

osMessageQueueId_t g_oled_queue;  // 消息队列句柄

// 2. 初始化队列(main中)
g_oled_queue = osMessageQueueNew(5, sizeof(OledMsg), NULL);  // 容量5条消息

// 3. 按键任务:发送消息
void key_task(void *arg) {
    OledMsg msg;
    while(1) {
        if (key_pressed()) {
            msg.type = 1;
            sprintf(msg.data, "Key: %d pressed", get_key_value());
            // 发送消息(无限期等待队列空闲)
            osMessageQueuePut(g_oled_queue, &msg, 0, osWaitForever);
        }
        vTaskDelay(50);
    }
}

// 4. OLED任务:接收消息并处理
void oled_task(void *arg) {
    OledMsg msg;
    while(1) {
        // 接收消息(超时100ms,超时则显示默认内容)
        if (osMessageQueueGet(g_oled_queue, &msg, NULL, 100) == osOK) {
            if (msg.type == 1) {
                oled_show_string(msg.data);  // 显示按键信息
            }
        } else {
            oled_show_temp();  // 超时显示温度
        }
    }
}

3. 信号量(Semaphore)

适用于单纯的 “事件触发”(如 “按键按下” 需要通知 OLED 刷新),不传递具体数据,仅标记 “事件发生”。

示例:按键触发 OLED 刷新

// 1. 定义信号量
osSemaphoreId_t g_refresh_sem;

// 2. 初始化信号量(main中)
g_refresh_sem = osSemaphoreNew(1, 0, NULL);  // 初始值0(无信号)

// 3. 按键任务:释放信号量
void key_task(void *arg) {
    while(1) {
        if (key_pressed()) {
            osSemaphoreRelease(g_refresh_sem);  // 发送“刷新”信号
        }
        vTaskDelay(50);
    }
}

// 4. OLED任务:等待信号量
void oled_task(void *arg) {
    while(1) {
        // 等待信号量(无限期等待)
        osSemaphoreAcquire(g_refresh_sem, osWaitForever);
        oled_show_key_info();  // 收到信号后显示按键信息
        vTaskDelay(2000);      // 显示2秒后恢复
        oled_show_temp();
    }
}

4. 事件标志组(Event Group)

适用于需要 “多个事件中任意一个或全部发生” 才触发动作的场景(如 “按键按下” 或 “传感器更新” 都需刷新 OLED)。

示例:多事件触发 OLED 刷新

// 1. 定义事件标志组
osEventFlagsId_t g_oled_events;

// 2. 初始化事件组(main中)
g_oled_events = osEventFlagsNew(NULL);

// 3. 按键任务:设置“按键事件”位
void key_task(void *arg) {
    while(1) {
        if (key_pressed()) {
            osEventFlagsSet(g_oled_events, 1 << 0);  // 设置bit0
        }
        vTaskDelay(50);
    }
}

// 4. 传感器任务:设置“数据更新事件”位
void sensor_task(void *arg) {
    while(1) {
        sensor_update();
        osEventFlagsSet(g_oled_events, 1 << 1);  // 设置bit1
        vTaskDelay(1000);
    }
}

// 5. OLED任务:等待任意事件
void oled_task(void *arg) {
    while(1) {
        // 等待bit0或bit1被设置
        uint32_t flags = osEventFlagsWait(
            g_oled_events, 
            (1<<0)|(1<<1),  // 关注的事件位
            osFlagsWaitAny, // 任意事件触发
            osWaitForever   // 无限期等待
        );
        if (flags & (1<<0)) oled_show_key();      // 处理按键事件
        if (flags & (1<<1)) oled_show_sensor();   // 处理传感器事件
    }
}

四、总结:FreeRTOS 开发的核心原则

  1. 任务拆分:按功能模块拆分任务(如采集、显示、交互),每个任务专注单一职责;
  2. 优先级设计:紧急任务(如按键响应)设高优先级,周期性任务(如采集)设低优先级;
  3. 通信选择:简单状态用全局变量 + 互斥锁,复杂数据用消息队列,事件通知用信号量 / 事件组;
  4. 避免直接调用:任务间通过通信机制交互,禁止直接调用其他任务的函数;
  5. 合理延时:通过vTaskDelay释放 CPU,避免任务长期占用资源。
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐