ESP32实战指南:基于FreeRTOS的任务间通信与参数传递

张开发
2026/4/18 15:08:28 15 分钟阅读

分享文章

ESP32实战指南:基于FreeRTOS的任务间通信与参数传递
1. ESP32与FreeRTOS的完美搭档第一次接触ESP32开发板时我就被它强大的多任务处理能力惊艳到了。这块小小的开发板内置了双核处理器配合FreeRTOS实时操作系统可以轻松实现多个任务并行运行。想象一下你的智能家居设备需要同时处理传感器数据、响应按键操作、还要保持网络连接这些任务如果串行执行肯定会卡顿而多任务处理就能完美解决这个问题。在实际项目中我经常遇到这样的场景一个任务负责采集温湿度传感器数据另一个任务需要将这些数据通过Wi-Fi上传到服务器。这两个任务必须协同工作但又不能互相干扰。这时候FreeRTOS的任务间通信机制就派上用场了。通过队列、信号量、事件组等方式可以让任务之间安全地交换数据就像办公室里的同事通过邮件和会议沟通一样自然。提示ESP32的FreeRTOS默认使用双核调度任务可以分配到不同的CPU核心上运行这比传统的单核单片机强大得多。2. 任务创建与参数传递实战2.1 基础任务创建让我们从一个最简单的多任务例子开始。下面这段代码创建了两个任务分别打印不同的信息TaskHandle_t task1Handle NULL; TaskHandle_t task2Handle NULL; void setup() { Serial.begin(115200); xTaskCreate( task1Function, // 任务函数 Task1, // 任务名称 10000, // 栈大小(字节) NULL, // 任务参数 1, // 优先级 task1Handle // 任务句柄 ); xTaskCreate( task2Function, Task2, 10000, NULL, 1, task2Handle ); } void task1Function(void *pvParameters) { while(1) { Serial.println(我是任务1正在运行...); vTaskDelay(1000 / portTICK_PERIOD_MS); } } void task2Function(void *pvParameters) { while(1) { Serial.println(任务2报告一切正常); vTaskDelay(2000 / portTICK_PERIOD_MS); } } void loop() { // 主循环可以空着因为任务已经交给FreeRTOS调度了 }这个例子中两个任务独立运行互不干扰。但实际项目中任务之间往往需要协作这就涉及到参数传递和通信了。2.2 安全的参数传递方式直接传递变量指针给任务是很危险的因为局部变量可能在任务开始执行前就已经失效了。我踩过这个坑后来找到了几种安全的参数传递方法动态内存分配使用pvPortMalloc分配内存任务结束后记得释放全局变量简单但不够优雅容易造成命名冲突静态变量生命周期与程序相同安全可靠这里演示一个使用静态变量的例子typedef struct { int sensorType; float calibrationFactor; } TaskParams; void sensorTask(void *pvParameters) { TaskParams *params (TaskParams *)pvParameters; Serial.print(传感器类型: ); Serial.println(params-sensorType); Serial.print(校准系数: ); Serial.println(params-calibrationFactor, 2); vTaskDelete(NULL); } void setup() { Serial.begin(115200); static TaskParams myParams { .sensorType 22, .calibrationFactor 1.23 }; xTaskCreate( sensorTask, SensorTask, 10000, myParams, 1, NULL ); }3. 任务间通信的三大法宝3.1 队列(Queue) - 数据高速公路队列是FreeRTOS中最常用的通信机制。它就像一个管道一个任务往里面写数据另一个任务从里面读数据。我在一个气象站项目中就用队列来传递传感器数据QueueHandle_t sensorQueue; typedef struct { float temperature; float humidity; uint32_t timestamp; } SensorData; void sensorReadingTask(void *pvParameters) { SensorData data; while(1) { // 模拟读取传感器数据 data.temperature random(200, 300) / 10.0; data.humidity random(400, 800) / 10.0; data.timestamp millis(); // 发送数据到队列 xQueueSend(sensorQueue, data, portMAX_DELAY); vTaskDelay(5000 / portTICK_PERIOD_MS); } } void dataProcessingTask(void *pvParameters) { SensorData receivedData; while(1) { if(xQueueReceive(sensorQueue, receivedData, portMAX_DELAY) pdTRUE) { Serial.print(时间戳: ); Serial.print(receivedData.timestamp); Serial.print(, 温度: ); Serial.print(receivedData.temperature); Serial.print(℃, 湿度: ); Serial.print(receivedData.humidity); Serial.println(%); } } } void setup() { Serial.begin(115200); // 创建能存储5个SensorData的队列 sensorQueue xQueueCreate(5, sizeof(SensorData)); xTaskCreate(sensorReadingTask, SensorTask, 10000, NULL, 2, NULL); xTaskCreate(dataProcessingTask, ProcessTask, 10000, NULL, 1, NULL); }3.2 信号量(Semaphore) - 资源守卫者信号量用来控制对共享资源的访问防止多个任务同时操作造成冲突。最常见的是二进制信号量相当于一个钥匙谁拿到钥匙谁就能使用资源。我曾经遇到过一个bug两个任务同时往SD卡写数据导致文件损坏。后来用信号量完美解决了SemaphoreHandle_t sdCardSemaphore; void logTask1(void *pvParameters) { while(1) { if(xSemaphoreTake(sdCardSemaphore, portMAX_DELAY) pdTRUE) { Serial.println(任务1获得SD卡访问权); // 模拟写入SD卡操作 vTaskDelay(100 / portTICK_PERIOD_MS); Serial.println(任务1释放SD卡); xSemaphoreGive(sdCardSemaphore); } vTaskDelay(1000 / portTICK_PERIOD_MS); } } void logTask2(void *pvParameters) { while(1) { if(xSemaphoreTake(sdCardSemaphore, portMAX_DELAY) pdTRUE) { Serial.println(任务2获得SD卡访问权); // 模拟写入SD卡操作 vTaskDelay(150 / portTICK_PERIOD_MS); Serial.println(任务2释放SD卡); xSemaphoreGive(sdCardSemaphore); } vTaskDelay(1200 / portTICK_PERIOD_MS); } } void setup() { Serial.begin(115200); // 创建二进制信号量 sdCardSemaphore xSemaphoreCreateBinary(); // 初始时信号量可用 xSemaphoreGive(sdCardSemaphore); xTaskCreate(logTask1, LogTask1, 10000, NULL, 2, NULL); xTaskCreate(logTask2, LogTask2, 10000, NULL, 2, NULL); }3.3 事件组(Event Group) - 多任务协调员事件组允许任务等待多个事件中的任意一个或全部发生。这在需要同步多个任务的场景中特别有用。比如一个任务需要等待数据就绪和网络连接成功两个条件都满足后才能执行EventGroupHandle_t dataEventGroup; #define DATA_READY_BIT (1 0) #define NETWORK_READY_BIT (1 1) void dataCollectionTask(void *pvParameters) { while(1) { // 模拟数据采集 vTaskDelay(3000 / portTICK_PERIOD_MS); Serial.println(数据采集完成设置DATA_READY标志); xEventGroupSetBits(dataEventGroup, DATA_READY_BIT); } } void networkTask(void *pvParameters) { while(1) { // 模拟网络连接 vTaskDelay(5000 / portTICK_PERIOD_MS); Serial.println(网络连接成功设置NETWORK_READY标志); xEventGroupSetBits(dataEventGroup, NETWORK_READY_BIT); vTaskDelay(15000 / portTICK_PERIOD_MS); // 模拟网络断开 xEventGroupClearBits(dataEventGroup, NETWORK_READY_BIT); Serial.println(网络断开清除NETWORK_READY标志); } } void dataUploadTask(void *pvParameters) { const EventBits_t requiredBits (DATA_READY_BIT | NETWORK_READY_BIT); while(1) { Serial.println(等待数据和网络都就绪...); xEventGroupWaitBits( dataEventGroup, requiredBits, pdTRUE, // 等待所有位都置位 pdTRUE, // 等待成功后清除这些位 portMAX_DELAY ); Serial.println(条件满足开始上传数据); // 模拟数据上传 vTaskDelay(1000 / portTICK_PERIOD_MS); Serial.println(数据上传完成); } } void setup() { Serial.begin(115200); dataEventGroup xEventGroupCreate(); xTaskCreate(dataCollectionTask, DataCollect, 10000, NULL, 1, NULL); xTaskCreate(networkTask, Network, 10000, NULL, 1, NULL); xTaskCreate(dataUploadTask, DataUpload, 10000, NULL, 2, NULL); }4. 物联网传感器节点的完整实现现在让我们把这些知识综合起来实现一个完整的物联网传感器节点。这个节点需要定期采集环境数据缓存多组数据在有网络连接时批量上传通过LED指示灯显示状态4.1 系统架构设计我们使用四个任务来构建这个系统任务名称优先级功能描述SensorTask3采集传感器数据并存入队列NetworkTask2管理Wi-Fi连接设置网络状态标志StorageTask2从队列读取数据并缓存到内存UploadTask1当网络可用时上传缓存的数据4.2 关键代码实现首先是共享资源和通信机制的初始化QueueHandle_t sensorQueue; EventGroupHandle_t systemEvents; SemaphoreHandle_t dataBufferMutex; #define WIFI_CONNECTED_BIT (1 0) #define BUFFER_FULL_BIT (1 1) // 传感器数据结构 typedef struct { float temperature; float humidity; uint32_t timestamp; } SensorData; // 数据缓冲区 #define BUFFER_SIZE 10 SensorData dataBuffer[BUFFER_SIZE]; uint8_t bufferIndex 0; void setup() { Serial.begin(115200); // 初始化通信机制 sensorQueue xQueueCreate(20, sizeof(SensorData)); systemEvents xEventGroupCreate(); dataBufferMutex xSemaphoreCreateMutex(); // 创建任务 xTaskCreate(sensorTask, Sensor, 10000, NULL, 3, NULL); xTaskCreate(networkTask, Network, 10000, NULL, 2, NULL); xTaskCreate(storageTask, Storage, 10000, NULL, 2, NULL); xTaskCreate(uploadTask, Upload, 10000, NULL, 1, NULL); }传感器任务实现void sensorTask(void *pvParameters) { SensorData data; while(1) { // 模拟传感器读数 data.temperature random(200, 300) / 10.0; data.humidity random(400, 800) / 10.0; data.timestamp millis(); // 发送到队列 if(xQueueSend(sensorQueue, data, 100 / portTICK_PERIOD_MS) ! pdTRUE) { Serial.println(警告传感器队列已满丢弃数据); } vTaskDelay(2000 / portTICK_PERIOD_MS); } }网络任务实现void networkTask(void *pvParameters) { while(1) { // 模拟Wi-Fi连接过程 vTaskDelay(5000 / portTICK_PERIOD_MS); // 随机决定是否连接成功 if(random(0, 10) 2) { // 70%概率成功 xEventGroupSetBits(systemEvents, WIFI_CONNECTED_BIT); Serial.println(网络已连接); // 保持连接一段时间 vTaskDelay(15000 / portTICK_PERIOD_MS); xEventGroupClearBits(systemEvents, WIFI_CONNECTED_BIT); Serial.println(网络断开); } else { Serial.println(网络连接失败稍后重试); } } }存储任务实现void storageTask(void *pvParameters) { SensorData data; while(1) { // 从队列获取传感器数据 if(xQueueReceive(sensorQueue, data, portMAX_DELAY) pdTRUE) { // 获取互斥锁保护共享缓冲区 if(xSemaphoreTake(dataBufferMutex, 100 / portTICK_PERIOD_MS) pdTRUE) { if(bufferIndex BUFFER_SIZE) { dataBuffer[bufferIndex] data; Serial.print(存储数据: ); Serial.print(data.temperature); Serial.print(℃, ); Serial.print(data.humidity); Serial.println(%); // 检查缓冲区是否已满 if(bufferIndex BUFFER_SIZE) { xEventGroupSetBits(systemEvents, BUFFER_FULL_BIT); } } xSemaphoreGive(dataBufferMutex); } } } }上传任务实现void uploadTask(void *pvParameters) { while(1) { // 等待网络连接且缓冲区有数据 EventBits_t bits xEventGroupWaitBits( systemEvents, WIFI_CONNECTED_BIT | BUFFER_FULL_BIT, pdTRUE, // 等待所有位 pdTRUE, // 清除这些位 portMAX_DELAY ); // 获取互斥锁访问缓冲区 if(xSemaphoreTake(dataBufferMutex, 200 / portTICK_PERIOD_MS) pdTRUE) { Serial.println(开始上传缓冲区数据...); // 模拟上传过程 for(int i 0; i bufferIndex; i) { Serial.print(上传: ); Serial.print(dataBuffer[i].temperature); Serial.print(℃, ); Serial.print(dataBuffer[i].humidity); Serial.print(%, 时间: ); Serial.println(dataBuffer[i].timestamp); vTaskDelay(200 / portTICK_PERIOD_MS); } bufferIndex 0; // 重置缓冲区 xSemaphoreGive(dataBufferMutex); Serial.println(上传完成); } } }4.3 系统优化与调试在实际部署这个系统时我发现几个需要注意的地方队列大小设置太小会导致数据丢失太大会浪费内存。根据数据产生速度和消费速度合理设置。任务优先级传感器任务优先级最高确保数据不会因为系统繁忙而丢失上传任务优先级最低避免影响实时数据采集。互斥锁超时所有获取互斥锁的操作都应该设置超时避免死锁。我在早期版本中因为没有设置超时导致系统偶尔会完全卡死。内存监控使用FreeRTOS自带的内存监控函数定期检查内存使用情况void checkMemory() { Serial.print(剩余堆内存: ); Serial.print(xPortGetFreeHeapSize()); Serial.println(字节); Serial.print(最小剩余堆内存: ); Serial.print(xPortGetMinimumEverFreeHeapSize()); Serial.println(字节); }在调试过程中我习惯使用FreeRTOS的任务状态查询功能来监控系统运行情况void printTaskStats() { char buffer[256]; vTaskList(buffer); // 获取任务状态列表 Serial.println(任务状态:); Serial.println(名称 状态 优先级 剩余栈 任务编号); Serial.println(buffer); vTaskGetRunTimeStats(buffer); Serial.println(任务运行时间统计:); Serial.println(buffer); }把这些调试函数放在一个单独的任务中定期调用可以很好地掌握系统运行状态。

更多文章