【QuecOpen 实战-006】FreeRTOS 多任务编程实战
前言在前面的系列文章中我们已经介绍了移远 QuecOpen 开发环境搭建、基础 API 使用以及 GPIO、UART 等外设驱动开发。今天我们将深入 QuecOpen 开发的核心 ——FreeRTOS 多任务编程。移远 QuecOpen 平台基于 FreeRTOS 实时操作系统构建所有的应用程序都运行在 FreeRTOS 之上。掌握多任务编程是开发稳定、高效的 QuecOpen 应用的必备技能。本文将从基础概念入手结合大量实战代码详细讲解 QuecOpen 中 FreeRTOS 多任务的创建、管理、通信与同步以及实际开发中常见的坑点与解决方案。本文基于移远 EC200U 模块QuecOpen SDK V3.3.0 版本编写其他移远模块如 EC600U、EC800M、BG95 等的 FreeRTOS 接口基本一致可直接参考。一、QuecOpen 与 FreeRTOS 的关系1.1 QuecOpen 平台架构移远 QuecOpen 是基于高通 / 展锐芯片平台的开放式开发环境它允许开发者直接在模块上编写和运行 C 语言应用程序无需额外的 MCU。其架构如下┌─────────────────────────────────┐ │ 用户应用程序 (C语言) │ ├─────────────────────────────────┤ │ QuecOpen API 接口层 │ ├─────────────────────────────────┤ │ FreeRTOS 实时操作系统 │ ├─────────────────────────────────┤ │ 芯片硬件抽象层 (HAL) │ ├─────────────────────────────────┤ │ 硬件平台 │ └─────────────────────────────────┘1.2 FreeRTOS 在 QuecOpen 中的特点抢占式调度高优先级任务可以抢占低优先级任务的 CPU 使用权任务优先级QuecOpen 中 FreeRTOS 的优先级范围是0~310 是最低优先级31 是最高优先级系统任务QuecOpen 已经创建了多个系统任务如 AT 命令处理、网络协议栈、硬件驱动等用户任务优先级建议设置在 5~20 之间避免影响系统任务运行内存管理QuecOpen 使用 FreeRTOS 的 heap_4 内存管理算法支持动态内存分配与释放系统节拍默认系统节拍频率为1000Hz即每个 tick 为 1ms二、FreeRTOS 多任务基础2.1 任务的基本概念任务是 FreeRTOS 中最小的执行单元每个任务都有自己的栈空间、程序计数器和上下文。任务的状态主要有以下几种运行态任务正在占用 CPU 执行就绪态任务已经准备好可以被调度器调度运行阻塞态任务正在等待某个事件如延时、信号量、队列消息等挂起态任务被挂起不会被调度器调度直到被恢复2.2 任务函数的格式QuecOpen 中 FreeRTOS 任务函数的标准格式如下/** * brief FreeRTOS任务函数模板 * param pvParameters 任务创建时传入的参数 */ void vTaskFunction(void *pvParameters) { // 任务初始化代码只执行一次 for(;;) // 任务主体循环必须是死循环 { // 任务执行的代码 // 必须有阻塞调用否则会占用100%CPU vTaskDelay(pdMS_TO_TICKS(100)); } // 任务函数不能返回如果需要结束任务调用vTaskDelete(NULL) vTaskDelete(NULL); }重要注意事项任务函数必须是无返回值的函数任务函数必须包含死循环循环内部必须有阻塞调用如 vTaskDelay、xQueueReceive 等否则会导致 CPU 使用率 100%系统卡死任务函数的参数是void *类型可以传递任意类型的数据三、任务的创建与管理实战3.1 动态创建任务QuecOpen 中推荐使用xTaskCreate函数动态创建任务函数原型如下BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 任务函数指针 const char * const pcName, // 任务名称用于调试 const uint16_t usStackDepth, // 任务栈大小单位字即4字节 void * const pvParameters, // 传递给任务函数的参数 UBaseType_t uxPriority, // 任务优先级 TaskHandle_t * const pxCreatedTask // 任务句柄用于后续管理任务 );返回值pdPASS任务创建成功errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY内存不足任务创建失败实战代码创建两个简单任务#include ql_application.h #include ql_freertos.h #include ql_uart.h // 任务句柄 static TaskHandle_t s_task1_handle NULL; static TaskHandle_t s_task2_handle NULL; /** * brief 任务1每秒打印一次信息 */ void vTask1(void *pvParameters) { uint32_t count 0; QL_LOG_INFO(Task1 started, parameter: %d, (int)pvParameters); for(;;) { QL_LOG_INFO(Task1 running, count: %d, count); vTaskDelay(pdMS_TO_TICKS(1000)); // 延时1秒 } } /** * brief 任务2每500ms打印一次信息 */ void vTask2(void *pvParameters) { uint32_t count 0; QL_LOG_INFO(Task2 started, parameter: %s, (char *)pvParameters); for(;;) { QL_LOG_INFO(Task2 running, count: %d, count); vTaskDelay(pdMS_TO_TICKS(500)); // 延时500ms } } /** * brief 应用入口函数 */ void ql_main(void *param) { BaseType_t ret; QL_LOG_INFO(QuecOpen FreeRTOS Demo started); // 创建任务1优先级5栈大小2048字8KB传递参数123 ret xTaskCreate(vTask1, Task1, 2048, (void *)123, 5, s_task1_handle); if(ret ! pdPASS) { QL_LOG_ERROR(Create Task1 failed, ret: %d, ret); } // 创建任务2优先级6栈大小2048字传递字符串参数 ret xTaskCreate(vTask2, Task2, 2048, Hello QuecOpen, 6, s_task2_handle); if(ret ! pdPASS) { QL_LOG_ERROR(Create Task2 failed, ret: %d, ret); } // 主任务可以删除自己 vTaskDelete(NULL); }运行结果[INFO] QuecOpen FreeRTOS Demo started [INFO] Task1 started, parameter: 123 [INFO] Task2 started, parameter: Hello QuecOpen [INFO] Task2 running, count: 0 [INFO] Task1 running, count: 0 [INFO] Task2 running, count: 1 [INFO] Task2 running, count: 2 [INFO] Task1 running, count: 1 [INFO] Task2 running, count: 3 [INFO] Task2 running, count: 4 [INFO] Task1 running, count: 2 ...可以看到优先级更高的 Task2 会更频繁地执行。3.2 任务栈大小的设置任务栈大小是 QuecOpen 开发中非常重要的参数设置过小会导致栈溢出系统崩溃设置过大会浪费内存。QuecOpen 中任务栈大小的建议值简单任务只做打印、延时等1024~2048 字4~8KB中等复杂度任务包含串口通信、数据处理2048~4096 字8~16KB复杂任务包含网络通信、文件操作4096~8192 字16~32KB栈溢出检测QuecOpen 提供了栈溢出检测功能可以通过以下函数获取任务的剩余栈空间UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask);实战代码检测任务栈使用情况void vTaskStackCheck(void *pvParameters) { UBaseType_t stack_remaining; for(;;) { // 获取当前任务的剩余栈空间 stack_remaining uxTaskGetStackHighWaterMark(NULL); QL_LOG_INFO(Task stack remaining: %d words, stack_remaining); vTaskDelay(pdMS_TO_TICKS(5000)); } }3.3 任务的挂起与恢复// 挂起任务 void vTaskSuspend(TaskHandle_t xTaskToSuspend); // 恢复任务 void vTaskResume(TaskHandle_t xTaskToResume); // 在中断服务函数中恢复任务 BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume);实战代码任务挂起与恢复演示void vTaskControl(void *pvParameters) { uint32_t count 0; for(;;) { count; if(count 5) { QL_LOG_INFO(Suspend Task1); vTaskSuspend(s_task1_handle); // 挂起任务1 } else if(count 10) { QL_LOG_INFO(Resume Task1); vTaskResume(s_task1_handle); // 恢复任务1 count 0; } vTaskDelay(pdMS_TO_TICKS(1000)); } }3.4 删除任务// 删除任务 void vTaskDelete(TaskHandle_t xTaskToDelete);注意如果参数为NULL则删除当前任务删除任务时系统会自动回收任务的栈空间和 TCB任务控制块内存不要在中断服务函数中调用vTaskDelete四、任务间通信与同步实战在实际开发中多个任务之间经常需要交换数据和同步执行。FreeRTOS 提供了多种任务间通信与同步机制包括队列、信号量、互斥量和事件组。4.1 队列Queue队列是 FreeRTOS 中最常用的任务间通信机制它可以在任务之间传递任意类型的数据。队列基本操作函数// 创建队列 QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize); // 向队列发送数据 BaseType_t xQueueSend(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait); // 从队列接收数据 BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait); // 删除队列 void vQueueDelete(QueueHandle_t xQueue);实战代码使用队列传递数据// 队列句柄 static QueueHandle_t s_data_queue NULL; // 数据结构体 typedef struct { uint32_t id; char message[32]; } DataPacket_t; /** * brief 发送任务向队列发送数据 */ void vSenderTask(void *pvParameters) { DataPacket_t packet; BaseType_t ret; uint32_t count 0; for(;;) { // 构造数据包 packet.id count; snprintf(packet.message, sizeof(packet.message), Hello from sender, count: %d, count); // 向队列发送数据等待最多100ms ret xQueueSend(s_data_queue, packet, pdMS_TO_TICKS(100)); if(ret pdPASS) { QL_LOG_INFO(Sent packet: id%d, message%s, packet.id, packet.message); } else { QL_LOG_WARN(Send packet failed, queue is full); } vTaskDelay(pdMS_TO_TICKS(1000)); } } /** * brief 接收任务从队列接收数据 */ void vReceiverTask(void *pvParameters) { DataPacket_t packet; BaseType_t ret; for(;;) { // 从队列接收数据无限等待 ret xQueueReceive(s_data_queue, packet, portMAX_DELAY); if(ret pdPASS) { QL_LOG_INFO(Received packet: id%d, message%s, packet.id, packet.message); } } } // 在ql_main中创建队列和任务 void ql_main(void *param) { // 创建队列最多存储5个数据包每个数据包大小为DataPacket_t s_data_queue xQueueCreate(5, sizeof(DataPacket_t)); if(s_data_queue NULL) { QL_LOG_ERROR(Create data queue failed); return; } // 创建发送任务和接收任务 xTaskCreate(vSenderTask, Sender, 2048, NULL, 5, NULL); xTaskCreate(vReceiverTask, Receiver, 2048, NULL, 6, NULL); vTaskDelete(NULL); }4.2 二进制信号量Binary Semaphore二进制信号量主要用于任务间的同步和中断与任务间的同步。二进制信号量基本操作函数// 创建二进制信号量 SemaphoreHandle_t xSemaphoreCreateBinary(void); // 获取信号量 BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait); // 释放信号量 BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore); // 在中断服务函数中释放信号量 BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken); // 删除信号量 void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);实战代码使用二进制信号量同步任务// 二进制信号量句柄 static SemaphoreHandle_t s_sync_sem NULL; /** * brief 等待任务等待信号量 */ void vWaitingTask(void *pvParameters) { BaseType_t ret; for(;;) { QL_LOG_INFO(Waiting for semaphore...); // 获取信号量无限等待 ret xSemaphoreTake(s_sync_sem, portMAX_DELAY); if(ret pdPASS) { QL_LOG_INFO(Got semaphore, doing work...); // 模拟处理工作 vTaskDelay(pdMS_TO_TICKS(500)); QL_LOG_INFO(Work done); } } } /** * brief 触发任务释放信号量 */ void vTriggerTask(void *pvParameters) { uint32_t count 0; for(;;) { vTaskDelay(pdMS_TO_TICKS(3000)); QL_LOG_INFO(Triggering semaphore, count: %d, count); xSemaphoreGive(s_sync_sem); // 释放信号量 } } // 在ql_main中创建信号量和任务 void ql_main(void *param) { // 创建二进制信号量初始状态为未获取 s_sync_sem xSemaphoreCreateBinary(); if(s_sync_sem NULL) { QL_LOG_ERROR(Create semaphore failed); return; } // 创建等待任务和触发任务 xTaskCreate(vWaitingTask, Waiting, 2048, NULL, 5, NULL); xTaskCreate(vTriggerTask, Trigger, 2048, NULL, 6, NULL); vTaskDelete(NULL); }4.3 互斥量Mutex互斥量用于保护共享资源防止多个任务同时访问同一个资源导致数据不一致。互斥量基本操作函数// 创建互斥量 SemaphoreHandle_t xSemaphoreCreateMutex(void); // 获取互斥量 BaseType_t xSemaphoreTake(SemaphoreHandle_t xMutex, TickType_t xTicksToWait); // 释放互斥量 BaseType_t xSemaphoreGive(SemaphoreHandle_t xMutex); // 删除互斥量 void vSemaphoreDelete(SemaphoreHandle_t xMutex);实战代码使用互斥量保护共享资源// 互斥量句柄 static SemaphoreHandle_t s_uart_mutex NULL; // 共享资源UART打印函数 void uart_print_safe(const char *str) { // 获取互斥量 if(xSemaphoreTake(s_uart_mutex, pdMS_TO_TICKS(100)) pdPASS) { // 访问共享资源 QL_LOG_INFO(%s, str); // 释放互斥量 xSemaphoreGive(s_uart_mutex); } else { QL_LOG_WARN(Get uart mutex timeout); } } /** * brief 任务A打印信息 */ void vTaskA(void *pvParameters) { char buf[64]; uint32_t count 0; for(;;) { snprintf(buf, sizeof(buf), TaskA: count%d, count); uart_print_safe(buf); vTaskDelay(pdMS_TO_TICKS(500)); } } /** * brief 任务B打印信息 */ void vTaskB(void *pvParameters) { char buf[64]; uint32_t count 0; for(;;) { snprintf(buf, sizeof(buf), TaskB: count%d, count); uart_print_safe(buf); vTaskDelay(pdMS_TO_TICKS(700)); } } // 在ql_main中创建互斥量和任务 void ql_main(void *param) { // 创建互斥量 s_uart_mutex xSemaphoreCreateMutex(); if(s_uart_mutex NULL) { QL_LOG_ERROR(Create mutex failed); return; } // 创建任务A和任务B xTaskCreate(vTaskA, TaskA, 2048, NULL, 5, NULL); xTaskCreate(vTaskB, TaskB, 2048, NULL, 5, NULL); vTaskDelete(NULL); }五、QuecOpen FreeRTOS 开发常见坑点与解决方案5.1 任务栈溢出现象系统突然重启或者出现奇怪的崩溃现象原因任务栈大小设置过小解决方案使用uxTaskGetStackHighWaterMark函数检测任务栈使用情况适当增大任务栈大小避免在任务函数中定义过大的局部变量建议使用动态内存分配5.2 CPU 使用率 100%现象系统运行缓慢响应迟钝原因某个任务的循环中没有阻塞调用一直占用 CPU解决方案检查所有任务函数确保循环内部有阻塞调用如 vTaskDelay、xQueueReceive 等使用ulTaskGetRunTimeStats函数查看各个任务的 CPU 使用率5.3 优先级反转现象高优先级任务被低优先级任务阻塞导致系统实时性下降原因低优先级任务持有互斥量高优先级任务等待互斥量而中间优先级的任务抢占了低优先级任务的 CPU解决方案使用互斥量而不是二进制信号量来保护共享资源互斥量具有优先级继承机制合理设置任务优先级5.4 内存泄漏现象系统运行一段时间后内存不足无法创建新的任务或队列原因动态分配的内存没有被释放解决方案确保每个malloc都有对应的free使用xPortGetFreeHeapSize函数查看剩余堆内存避免频繁地创建和删除任务、队列等对象5.5 在中断服务函数中调用不允许的函数现象系统崩溃原因在中断服务函数中调用了非中断安全的 FreeRTOS 函数解决方案中断服务函数中只能调用以FromISR结尾的 FreeRTOS 函数复杂的处理逻辑应该交给任务来完成中断服务函数只负责触发任务六、实战项目多任务数据采集与处理系统为了让大家更好地理解 FreeRTOS 多任务编程我们来实现一个简单的多任务数据采集与处理系统。该系统包含以下三个任务数据采集任务模拟采集传感器数据数据处理任务对采集到的数据进行处理数据显示任务显示处理后的数据任务之间通过队列进行通信。完整代码#include ql_application.h #include ql_freertos.h #include ql_uart.h #include stdlib.h #include string.h // 队列句柄 static QueueHandle_t s_raw_data_queue NULL; static QueueHandle_t s_processed_data_queue NULL; // 原始数据结构体 typedef struct { uint32_t timestamp; float temperature; float humidity; } RawData_t; // 处理后的数据结构体 typedef struct { uint32_t timestamp; float avg_temperature; float avg_humidity; uint32_t sample_count; } ProcessedData_t; /** * brief 数据采集任务模拟采集传感器数据 */ void vDataAcquisitionTask(void *pvParameters) { RawData_t raw_data; BaseType_t ret; QL_LOG_INFO(Data Acquisition Task started); for(;;) { // 模拟采集数据 raw_data.timestamp xTaskGetTickCount(); raw_data.temperature 25.0f (rand() % 100) / 10.0f; // 25.0~35.0℃ raw_data.humidity 50.0f (rand() % 500) / 10.0f; // 50.0~100.0%RH // 将原始数据发送到队列 ret xQueueSend(s_raw_data_queue, raw_data, pdMS_TO_TICKS(100)); if(ret ! pdPASS) { QL_LOG_WARN(Raw data queue is full, dropping data); } // 每100ms采集一次数据 vTaskDelay(pdMS_TO_TICKS(100)); } } /** * brief 数据处理任务对原始数据进行平均处理 */ void vDataProcessingTask(void *pvParameters) { RawData_t raw_data; ProcessedData_t processed_data; BaseType_t ret; float temp_sum 0.0f; float humi_sum 0.0f; uint32_t count 0; QL_LOG_INFO(Data Processing Task started); for(;;) { // 从队列接收原始数据 ret xQueueReceive(s_raw_data_queue, raw_data, portMAX_DELAY); if(ret ! pdPASS) { continue; } // 累加数据 temp_sum raw_data.temperature; humi_sum raw_data.humidity; count; // 每10个数据计算一次平均值 if(count 10) { processed_data.timestamp raw_data.timestamp; processed_data.avg_temperature temp_sum / count; processed_data.avg_humidity humi_sum / count; processed_data.sample_count count; // 将处理后的数据发送到队列 ret xQueueSend(s_processed_data_queue, processed_data, pdMS_TO_TICKS(100)); if(ret ! pdPASS) { QL_LOG_WARN(Processed data queue is full, dropping data); } // 重置累加器 temp_sum 0.0f; humi_sum 0.0f; count 0; } } } /** * brief 数据显示任务显示处理后的数据 */ void vDataDisplayTask(void *pvParameters) { ProcessedData_t processed_data; BaseType_t ret; QL_LOG_INFO(Data Display Task started); for(;;) { // 从队列接收处理后的数据 ret xQueueReceive(s_processed_data_queue, processed_data, portMAX_DELAY); if(ret ! pdPASS) { continue; } // 显示数据 QL_LOG_INFO(); QL_LOG_INFO(Timestamp: %lu ms, processed_data.timestamp); QL_LOG_INFO(Average Temperature: %.2f ℃, processed_data.avg_temperature); QL_LOG_INFO(Average Humidity: %.2f %%RH, processed_data.avg_humidity); QL_LOG_INFO(Sample Count: %lu, processed_data.sample_count); QL_LOG_INFO(); } } /** * brief 应用入口函数 */ void ql_main(void *param) { QL_LOG_INFO(Multi-task Data Acquisition System started); // 创建原始数据队列最多存储20个数据 s_raw_data_queue xQueueCreate(20, sizeof(RawData_t)); if(s_raw_data_queue NULL) { QL_LOG_ERROR(Create raw data queue failed); return; } // 创建处理后的数据队列最多存储5个数据 s_processed_data_queue xQueueCreate(5, sizeof(ProcessedData_t)); if(s_processed_data_queue NULL) { QL_LOG_ERROR(Create processed data queue failed); vQueueDelete(s_raw_data_queue); return; } // 创建任务 xTaskCreate(vDataAcquisitionTask, DataAcq, 2048, NULL, 7, NULL); xTaskCreate(vDataProcessingTask, DataProc, 2048, NULL, 6, NULL); xTaskCreate(vDataDisplayTask, DataDisp, 2048, NULL, 5, NULL); // 主任务删除自己 vTaskDelete(NULL); }运行结果[INFO] Multi-task Data Acquisition System started [INFO] Data Acquisition Task started [INFO] Data Processing Task started [INFO] Data Display Task started [INFO] [INFO] Timestamp: 1000 ms [INFO] Average Temperature: 29.85 ℃ [INFO] Average Humidity: 74.20 %RH [INFO] Sample Count: 10 [INFO] [INFO] [INFO] Timestamp: 2000 ms [INFO] Average Temperature: 28.62 ℃ [INFO] Average Humidity: 71.35 %RH [INFO] Sample Count: 10 [INFO] ...七、总结本文详细讲解了移远 QuecOpen 平台上 FreeRTOS 多任务编程的基础知识和实战技巧包括任务的创建与管理、任务间通信与同步以及实际开发中常见的坑点与解决方案。最后通过一个完整的多任务数据采集与处理系统展示了 FreeRTOS 在实际项目中的应用。掌握 FreeRTOS 多任务编程是开发高质量 QuecOpen 应用的关键。在实际开发中我们应该合理设计任务划分每个任务只负责一个功能正确设置任务优先级和栈大小使用合适的任务间通信与同步机制注意避免常见的坑点如栈溢出、CPU 使用率 100%、优先级反转等后续预告下一篇文章【QuecOpen 实战-007】移远 4G QuecOpen C SDK 工业级开发实战彻底告别 AT 指令TCP/UDP 全栈实现原创不易如果本文对你有帮助欢迎点赞、收藏、关注三连有任何问题都可以在评论区留言我会及时回复。