【嵌入式开发】FreeRTOS实战入门:从项目代码学起
·
作为一名嵌入式开发者,我在项目中大量使用 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!如果有问题,欢迎在评论区讨论。
📝 :以上代码均来自实际项目,已脱敏处理。如果你正在学习嵌入式开发,建议从简单的任务创建开始,逐步引入队列、信号量等组件。实践是最好的老师!
更多推荐


所有评论(0)