作为一名嵌入式开发者,我在项目中大量使用 FreeRTOS 来管理多任务。很多刚入门的朋友问我:“FreeRTOS 到底怎么用?那些队列、信号量都是什么东西?”

今天我就以我们项目中的实际代码为例,用最通俗易懂的方式,带你入门 FreeRTOS。


一、为什么要用 FreeRTOS?

先看一个场景:

// 裸机代码的困境
void main() {
    while(1) {
        // 1. 检查串口接收
        if(uart_has_data()) {
            process_uart_data();  // 可能耗时很长
        }
        
        // 2. 处理按键
        if(key_pressed()) {
            handle_key();
        }
        
        // 3. 更新显示屏
        update_display();
    }
}

问题来了:如果 process_uart_data() 正在处理大数据,按键和显示更新就会被阻塞!

FreeRTOS 的解决方案:把这些工作拆成独立的任务,由操作系统调度执行。


二、项目中常用的 FreeRTOS 组件

我们项目中主要用到这四个核心组件:

组件作用类比
队列 (Queue)任务间传递数据快递中转站
事件组 (Event Group)任务间状态同步红绿灯信号
互斥锁 (Mutex)保护共享资源厕所门锁
定时器 (Timer)定时执行任务闹钟

三、实战案例:串口通信协议解析

场景说明

我们项目需要通过串口与 FPGA 通信,协议格式如下:

┌──────┬──────┬──────────┬─────────┬─────────┬─────┐
│Header│Length│Function │ Channel │  Data   │ CRC │
│ 0xAA │ 1-32 │ 功能码  │ 通道号  │ 数据域  │ 校验│
└──────┴──────┴──────────┴─────────┴─────────┴─────┘

1. 队列 (Queue) - 数据中转站

核心作用:解决中断和任务之间的数据传递问题。

① 创建队列
// 在初始化函数中创建一个256字节的队列
xQueueHandle uartRxQueue;  // 声明队列句柄

void SelfDefineProtocolInit() {
    // 创建队列:256个位置,每个位置存1字节
    uartRxQueue = xQueueCreate(256, sizeof(uint8_t));
}
② 中断中发送数据
// 串口接收中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *uart) {
    BaseType_t xHigherPriorityTaskWoken;
    
    if (uart == &huart4) {
        // 把接收到的1字节发送到队列
        xQueueSendFromISR(uartRxQueue, &uartRx, &xHigherPriorityTaskWoken);
        
        // 重新开启中断,等待下一个字节
        HAL_UART_Receive_IT(&huart4, &uartRx, 1);
    }
}

关键点:中断里必须用 xQueueSendFromISR,不能用普通的 xQueueSend!

③ 任务中接收数据
void SelfDefineProtocolRecvProcess(void) {
    uint8_t RxData;
    
    // 从队列读取数据,最多等待100ms
    if (xQueueReceive(uartRxQueue, &RxData, 100 / portTICK_RATE_MS) == pdPASS) {
        // 解析协议
        ReceiveDataProcess(&huart4, &sSelfDefinePack, RxData);
    }
}

队列的工作原理:

┌─────────────────────────────────────────────────────────────┐
│                    中断与任务的桥梁                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   串口中断(ISR)           uartRxQueue           协议解析任务 │
│        │                      │                      │       │
│        │ 发送字节             │                      │       │
│        └─────────────────────>│                      │       │
│                               │  FIFO先进先出        │       │
│                               │                      │       │
│                               │ 接收字节             │       │
│                               └─────────────────────>│       │
│                                                             │
│   ISR快速退出                 队列暂存数据         从容解析   │
└─────────────────────────────────────────────────────────────┘

为什么要用队列?

  • ❌ 直接在中断里解析协议:中断执行时间太长,会丢数据
  • ✅ 用队列:中断只负责"快递打包",任务负责"拆包处理"

2. 事件组 (Event Group) - 状态信号灯

核心作用:任务间的状态同步,比如"协议解析完成"或"通信出错"。

① 创建事件组
EventGroupHandle_t uartEventGroup;

void SelfDefineProtocolInit() {
    uartEventGroup = xEventGroupCreate();
}
② 定义事件标志
// 用位表示不同事件
#define PROTOCOL_EVENT_ERR (1 << 0)  // 第0位:错误事件
#define PROTOCOL_EVENT_OK  (1 << 1)  // 第1位:成功事件
③ 设置事件
void ReceiveDataProcess(...) {
    if (pack->cErr == 1) {
        // 设置错误事件
        xEventGroupSetBits(uartEventGroup, PROTOCOL_EVENT_ERR);
    } else if (pack->cFinish == 1) {
        // 设置成功事件
        xEventGroupSetBits(uartEventGroup, PROTOCOL_EVENT_OK);
    }
}
④ 等待事件
HAL_StatusTypeDef Communicate2FPGA(...) {
    // 等待成功或错误事件,超时100ms
    EventBits_t eventValue = xEventGroupWaitBits(
        uartEventGroup,
        PROTOCOL_EVENT_ERR | PROTOCOL_EVENT_OK,  // 关注这两个事件
        pdTRUE,   // 等待后自动清除标志
        pdFALSE,  // 任一事件发生即可返回
        100 / portTICK_RATE_MS  // 超时时间
    );
    
    // 处理结果
    if (eventValue == 0) {
        return HAL_TIMEOUT;  // 超时
    } else if (eventValue & PROTOCOL_EVENT_ERR) {
        return HAL_ERROR;    // 错误
    }
    return HAL_OK;           // 成功
}

事件组的工作原理:

┌─────────────────────────────────────────────────────────────┐
│                    任务间的红绿灯                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   协议解析任务              uartEventGroup           FPGA任务│
│        │                        │                      │     │
│        │ 解析成功               │                      │     │
│        └───────────────────────>│ [0][1] = [0][1]      │     │
│                                 │   ERR   OK           │     │
│                                 │                      │     │
│                                 │ 等待事件              │     │
│                                 │<─────────────────────┤     │
│                                 │                      │     │
│                                 │ OK事件发生!         │     │
│                                 └─────────────────────>│     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3. 互斥锁 (Mutex) - 共享资源保护

核心作用:防止多个任务同时访问同一资源(如串口、硬件寄存器)。

① 创建互斥锁
xSemaphoreHandle mutexSemaphore;

void SelfDefineProtocolInit() {
    mutexSemaphore = xSemaphoreCreateMutex();
}
② 使用互斥锁
HAL_StatusTypeDef Communicate2FPGA(...) {
    // 获取锁,阻塞等待直到获取成功
    xSemaphoreTake(mutexSemaphore, portMAX_DELAY);
    
    // ========== 临界区开始 ==========
    // 这里的代码同一时间只有一个任务能执行
    TxSelfDefineProtocol(&huart4, &pack);
    
    // 等待响应...
    eventValue = xEventGroupWaitBits(...);
    // ========== 临界区结束 ==========
    
    // 释放锁
    xSemaphoreGive(mutexSemaphore);
    
    return ret;
}

互斥锁的工作原理:

┌─────────────────────────────────────────────────────────────┐
│                    共享资源的厕所门                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Task A                   mutexSemaphore              Task B│
│      │                          │                        │   │
│      │ 想上厕所                  │                        │   │
│      ├─────────────────────────>│                        │   │
│      │              [Locked]    │                        │   │
│      │                          │                        │   │
│      │   使用串口通信...         │                        │   │
│      │                          │                        │   │
│      │ 用完了                   │                        │   │
│      ├─────────────────────────>│                        │   │
│      │              [Unlocked]  │   想上厕所              │   │
│      │                          │<───────────────────────┤   │
│      │                          │        [Locked]        │   │
│      │                          │   使用串口通信...       │   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

为什么需要互斥锁?

想象一下:如果两个任务同时给串口发数据,就会变成这样:

任务A: 发送 [0xAA][0x05][0x01]...
任务B:        [0x03][0xAA][0x02]...
串口:    [0xAA][0x03][0xAA][0x05][0x01][0x02]...  ❌ 数据混乱!

4. 定时器 (Timer) - 定时执行任务

核心作用:在指定时间执行某个函数,比如延迟初始化。

① 创建定时器
TimerHandle_t DelayTimer;

void SelfDefineProtocolInit() {
    // 创建一个5秒后执行的定时器
    DelayTimer = xTimerCreate(
        "DelayTimer",                    // 定时器名称
        configTICK_RATE_HZ * 5,          // 定时时间(5秒)
        pdFALSE,                         // pdFALSE=单次触发, pdTRUE=周期触发
        (void *)1,                       // 定时器ID
        DelayTimerCallback               // 回调函数
    );
    
    // 启动定时器
    xTimerStart(DelayTimer, 0);
}
② 定时器回调函数
void DelayTimerCallback(TimerHandle_t xTimer) {
    // 5秒后执行这些初始化
    ModbusAddrSet();   // 设置Modbus地址
    ltc2662_init();    // 初始化DAC芯片
    dac81408_init();   // 初始化另一个DAC芯片
}

为什么要延迟初始化?

  • FPGA 需要时间完成配置
  • 硬件上电后需要稳定时间
  • 某些设备需要按特定顺序初始化

四、完整数据流示例

把这些组件串起来,完整的通信流程是这样的:

┌─────────────────────────────────────────────────────────────────────────────┐
│                        完整通信流程图                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  [用户任务]    [FPGA通信任务]    [协议解析任务]    [串口中断]                 │
│       │              │                 │                 │                  │
│       │  请求读FPGA   │                 │                 │                  │
│       ├─────────────>│                 │                 │                  │
│       │              │  获取互斥锁     │                 │                  │
│       │              │                 │                 │                  │
│       │              │  发送协议包     │                 │                  │
│       │              │────────────────>│                 │                  │
│       │              │                 │                 │                  │
│       │              │  等待事件(100ms)│                 │                  │
│       │              │◄────────────────┘                 │                  │
│       │              │                 │                 │                  │
│       │              │                 │    [FPGA回复]    │                  │
│       │              │                 │                 │  串口中断触发     │
│       │              │                 │                 │  发送数据到队列  │
│       │              │                 │  从队列取数据   │                 │
│       │              │                 │<────────────────┘                  │
│       │              │                 │                  │                  │
│       │              │                 │  解析协议完成   │                  │
│       │              │                 │  设置事件OK     │                  │
│       │              │  事件触发!     │                 │                  │
│       │              │<────────────────┘                 │                  │
│       │              │                 │                 │                  │
│       │              │  读取FPGA数据   │                 │                  │
│       │              │  释放互斥锁     │                 │                  │
│       │              │                 │                 │                  │
│       │  返回数据     │                 │                 │                  │
│       │<─────────────┤                 │                 │                  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

五、新手常见坑

1. 在中断中使用错误的函数

// ❌ 错误:中断中不能用普通版本
void HAL_UART_RxCpltCallback(...) {
    xQueueSend(uartRxQueue, &uartRx, 0);  // 错!
}

// ✅ 正确:要用 xxxFromISR 版本
void HAL_UART_RxCpltCallback(...) {
    xQueueSendFromISR(uartRxQueue, &uartRx, &xHigherPriorityTaskWoken);  // 对!
}

2. 忘记释放互斥锁

// ❌ 错误:获取锁后没释放
xSemaphoreTake(mutexSemaphore, portMAX_DELAY);
if (error_occurred) {
    return HAL_ERROR;  // 锁没释放!其他任务永远拿不到锁!
}
xSemaphoreGive(mutexSemaphore);

// ✅ 正确:确保任何路径都释放锁
xSemaphoreTake(mutexSemaphore, portMAX_DELAY);
if (error_occurred) {
    xSemaphoreGive(mutexSemaphore);  // 先释放
    return HAL_ERROR;
}
xSemaphoreGive(mutexSemaphore);

3. 队列大小设置不合理

// ❌ 队列太小,数据会丢失
uartRxQueue = xQueueCreate(8, sizeof(uint8_t));  // 只能存8字节

// ✅ 根据实际需求设置
uartRxQueue = xQueueCreate(256, sizeof(uint8_t));  // 可以存256字节

六、总结

组件函数用途
队列xQueueCreate xQueueSendFromISR xQueueReceive任务/中断间传递数据
事件组xEventGroupCreate xEventGroupSetBits xEventGroupWaitBits任务间同步状态
互斥锁xSemaphoreCreateMutex xSemaphoreTake xSemaphoreGive保护共享资源
定时器xTimerCreate xTimerStart定时执行任务

FreeRTOS 并不难,关键是理解每个组件的"设计意图":

  • 队列:解决"数据怎么传"的问题
  • 事件组:解决"状态怎么同步"的问题
  • 互斥锁:解决"资源怎么共享"的问题
  • 定时器:解决"任务什么时候执行"的问题

希望这篇文章能帮助你理解 FreeRTOS!如果有问题,欢迎在评论区讨论。

📝 :以上代码均来自实际项目,已脱敏处理。如果你正在学习嵌入式开发,建议从简单的任务创建开始,逐步引入队列、信号量等组件。实践是最好的老师!

更多推荐