【嵌入式开发】FreeRTOS实战入门:从项目代码学起
作为一名嵌入式开发者我在项目中大量使用FreeRTOS来管理多任务。很多刚入门的朋友问我“FreeRTOS 到底怎么用那些队列、信号量都是什么东西”今天我就以我们项目中的实际代码为例用最通俗易懂的方式带你入门 FreeRTOS。一、为什么要用 FreeRTOS先看一个场景// 裸机代码的困境voidmain(){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;// 声明队列句柄voidSelfDefineProtocolInit(){// 创建队列256个位置每个位置存1字节uartRxQueuexQueueCreate(256,sizeof(uint8_t));}② 中断中发送数据// 串口接收中断回调函数voidHAL_UART_RxCpltCallback(UART_HandleTypeDef*uart){BaseType_t xHigherPriorityTaskWoken;if(uarthuart4){// 把接收到的1字节发送到队列xQueueSendFromISR(uartRxQueue,uartRx,xHigherPriorityTaskWoken);// 重新开启中断等待下一个字节HAL_UART_Receive_IT(huart4,uartRx,1);}}关键点中断里必须用xQueueSendFromISR不能用普通的xQueueSend③ 任务中接收数据voidSelfDefineProtocolRecvProcess(void){uint8_tRxData;// 从队列读取数据最多等待100msif(xQueueReceive(uartRxQueue,RxData,100/portTICK_RATE_MS)pdPASS){// 解析协议ReceiveDataProcess(huart4,sSelfDefinePack,RxData);}}队列的工作原理┌─────────────────────────────────────────────────────────────┐ │ 中断与任务的桥梁 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 串口中断(ISR) uartRxQueue 协议解析任务 │ │ │ │ │ │ │ │ 发送字节 │ │ │ │ └─────────────────────│ │ │ │ │ FIFO先进先出 │ │ │ │ │ │ │ │ 接收字节 │ │ │ └─────────────────────│ │ │ │ │ ISR快速退出 队列暂存数据 从容解析 │ └─────────────────────────────────────────────────────────────┘为什么要用队列❌ 直接在中断里解析协议中断执行时间太长会丢数据✅ 用队列中断只负责快递打包任务负责拆包处理2. 事件组 (Event Group) - 状态信号灯核心作用任务间的状态同步比如协议解析完成或通信出错。① 创建事件组EventGroupHandle_t uartEventGroup;voidSelfDefineProtocolInit(){uartEventGroupxEventGroupCreate();}② 定义事件标志// 用位表示不同事件#definePROTOCOL_EVENT_ERR(10)// 第0位错误事件#definePROTOCOL_EVENT_OK(11)// 第1位成功事件③ 设置事件voidReceiveDataProcess(...){if(pack-cErr1){// 设置错误事件xEventGroupSetBits(uartEventGroup,PROTOCOL_EVENT_ERR);}elseif(pack-cFinish1){// 设置成功事件xEventGroupSetBits(uartEventGroup,PROTOCOL_EVENT_OK);}}④ 等待事件HAL_StatusTypeDefCommunicate2FPGA(...){// 等待成功或错误事件超时100msEventBits_t eventValuexEventGroupWaitBits(uartEventGroup,PROTOCOL_EVENT_ERR|PROTOCOL_EVENT_OK,// 关注这两个事件pdTRUE,// 等待后自动清除标志pdFALSE,// 任一事件发生即可返回100/portTICK_RATE_MS// 超时时间);// 处理结果if(eventValue0){returnHAL_TIMEOUT;// 超时}elseif(eventValuePROTOCOL_EVENT_ERR){returnHAL_ERROR;// 错误}returnHAL_OK;// 成功}事件组的工作原理┌─────────────────────────────────────────────────────────────┐ │ 任务间的红绿灯 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 协议解析任务 uartEventGroup FPGA任务│ │ │ │ │ │ │ │ 解析成功 │ │ │ │ └───────────────────────│ [0][1] [0][1] │ │ │ │ ERR OK │ │ │ │ │ │ │ │ 等待事件 │ │ │ │─────────────────────┤ │ │ │ │ │ │ │ OK事件发生 │ │ │ └─────────────────────│ │ │ │ └─────────────────────────────────────────────────────────────┘3. 互斥锁 (Mutex) - 共享资源保护核心作用防止多个任务同时访问同一资源如串口、硬件寄存器。① 创建互斥锁xSemaphoreHandle mutexSemaphore;voidSelfDefineProtocolInit(){mutexSemaphorexSemaphoreCreateMutex();}② 使用互斥锁HAL_StatusTypeDefCommunicate2FPGA(...){// 获取锁阻塞等待直到获取成功xSemaphoreTake(mutexSemaphore,portMAX_DELAY);// 临界区开始 // 这里的代码同一时间只有一个任务能执行TxSelfDefineProtocol(huart4,pack);// 等待响应...eventValuexEventGroupWaitBits(...);// 临界区结束 // 释放锁xSemaphoreGive(mutexSemaphore);returnret;}互斥锁的工作原理┌─────────────────────────────────────────────────────────────┐ │ 共享资源的厕所门 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 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;voidSelfDefineProtocolInit(){// 创建一个5秒后执行的定时器DelayTimerxTimerCreate(DelayTimer,// 定时器名称configTICK_RATE_HZ*5,// 定时时间(5秒)pdFALSE,// pdFALSE单次触发, pdTRUE周期触发(void*)1,// 定时器IDDelayTimerCallback// 回调函数);// 启动定时器xTimerStart(DelayTimer,0);}② 定时器回调函数voidDelayTimerCallback(TimerHandle_t xTimer){// 5秒后执行这些初始化ModbusAddrSet();// 设置Modbus地址ltc2662_init();// 初始化DAC芯片dac81408_init();// 初始化另一个DAC芯片}为什么要延迟初始化FPGA 需要时间完成配置硬件上电后需要稳定时间某些设备需要按特定顺序初始化四、完整数据流示例把这些组件串起来完整的通信流程是这样的┌─────────────────────────────────────────────────────────────────────────────┐ │ 完整通信流程图 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ [用户任务] [FPGA通信任务] [协议解析任务] [串口中断] │ │ │ │ │ │ │ │ │ 请求读FPGA │ │ │ │ │ ├─────────────│ │ │ │ │ │ │ 获取互斥锁 │ │ │ │ │ │ │ │ │ │ │ │ 发送协议包 │ │ │ │ │ │────────────────│ │ │ │ │ │ │ │ │ │ │ │ 等待事件(100ms)│ │ │ │ │ │◄────────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ [FPGA回复] │ │ │ │ │ │ │ 串口中断触发 │ │ │ │ │ │ 发送数据到队列 │ │ │ │ │ 从队列取数据 │ │ │ │ │ │────────────────┘ │ │ │ │ │ │ │ │ │ │ │ 解析协议完成 │ │ │ │ │ │ 设置事件OK │ │ │ │ │ 事件触发 │ │ │ │ │ │────────────────┘ │ │ │ │ │ │ │ │ │ │ │ 读取FPGA数据 │ │ │ │ │ │ 释放互斥锁 │ │ │ │ │ │ │ │ │ │ │ 返回数据 │ │ │ │ │ │─────────────┤ │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘五、新手常见坑1. 在中断中使用错误的函数// ❌ 错误中断中不能用普通版本voidHAL_UART_RxCpltCallback(...){xQueueSend(uartRxQueue,uartRx,0);// 错}// ✅ 正确要用 xxxFromISR 版本voidHAL_UART_RxCpltCallback(...){xQueueSendFromISR(uartRxQueue,uartRx,xHigherPriorityTaskWoken);// 对}2. 忘记释放互斥锁// ❌ 错误获取锁后没释放xSemaphoreTake(mutexSemaphore,portMAX_DELAY);if(error_occurred){returnHAL_ERROR;// 锁没释放其他任务永远拿不到锁}xSemaphoreGive(mutexSemaphore);// ✅ 正确确保任何路径都释放锁xSemaphoreTake(mutexSemaphore,portMAX_DELAY);if(error_occurred){xSemaphoreGive(mutexSemaphore);// 先释放returnHAL_ERROR;}xSemaphoreGive(mutexSemaphore);3. 队列大小设置不合理// ❌ 队列太小数据会丢失uartRxQueuexQueueCreate(8,sizeof(uint8_t));// 只能存8字节// ✅ 根据实际需求设置uartRxQueuexQueueCreate(256,sizeof(uint8_t));// 可以存256字节六、总结组件函数用途队列xQueueCreatexQueueSendFromISRxQueueReceive任务/中断间传递数据事件组xEventGroupCreatexEventGroupSetBitsxEventGroupWaitBits任务间同步状态互斥锁xSemaphoreCreateMutexxSemaphoreTakexSemaphoreGive保护共享资源定时器xTimerCreatexTimerStart定时执行任务FreeRTOS 并不难关键是理解每个组件的设计意图队列解决数据怎么传的问题事件组解决状态怎么同步的问题互斥锁解决资源怎么共享的问题定时器解决任务什么时候执行的问题希望这篇文章能帮助你理解 FreeRTOS如果有问题欢迎在评论区讨论。 以上代码均来自实际项目已脱敏处理。如果你正在学习嵌入式开发建议从简单的任务创建开始逐步引入队列、信号量等组件。实践是最好的老师