首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >基于FreeRTOS的多传感器数据采集系统设计与实现 — 从任务调度到MQTT上云

基于FreeRTOS的多传感器数据采集系统设计与实现 — 从任务调度到MQTT上云

原创
作者头像
97java-xyz
发布2026-08-12 13:44:10
发布2026-08-12 13:44:10
1120
举报

基于FreeRTOS的多传感器数据采集系统设计与实现 — 从任务调度到MQTT上云

一份真正“能打”的嵌入式项目,不是跑通例程,而是把 RTOS 的每一个机制用到刀刃上。本文用一个完整工业级项目,拆解 FreeRTOS 在复杂多任务环境下的实战策略。


1. 为什么需要 RTOS?—— 裸机之痛与项目背景

在物联网与工业4.0的浪潮下,终端节点往往需要同时处理:

  • 多种传感器(温度、湿度、压力、IMU)以不同频率采样
  • 实时响应外部中断(按键、报警)
  • 与云端保持心跳并支持 OTA
  • 低功耗与故障自恢复

裸机轮询或前后台系统在这种场景下会迅速陷入“中断堆叠 + 主循环阻塞”的泥潭。而 FreeRTOS 提供了一套轻量级、确定性强的任务调度、同步与通信原语,使得系统复杂度从 O(N²) 降为 O(N),且每个功能模块可独立测试、演化和热插拔。

本文项目命名为 “FreeRTOS-DAQ”,是一款面向智能制造的数据采集网关,硬件基于 STM32F407 + ESP8266 (AT固件),软件核心为 FreeRTOS v10.4.6,搭载 MQTT over TCP 上云(腾讯云 IoT Hub)。


2. 系统总体架构

代码语言:javascript
复制
┌─────────────────────────────────────────────────────────┐
│                    Application Layer                     │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐  │
│  │ CLI 调试  │ │ MQTT 发布 │ │  OTA     │ │ 告警策略 │  │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘  │
├─────────────────────────────────────────────────────────┤
│                  FreeRTOS Kernel                        │
│  ┌─────┐ ┌──────┐ ┌───────┐ ┌──────┐ ┌───────────┐  │
│  │任务  │ │队列  │ │信号量 │ │事件组│ │软件定时器 │  │
│  │调度器│ │(Queue)│ │(Sem)  │ │(EventGrp)│ │(Timer)   │  │
│  └─────┘ └──────┘ └───────┘ └──────┘ └───────────┘  │
├─────────────────────────────────────────────────────────┤
│              BSP & Driver Layer (HAL)                   │
│  I2C  │  SPI  │  UART  │  GPIO  │  DMA  │  RTC  │  WD │
└─────────────────────────────────────────────────────────┘
│  Hardware: STM32F407 │ SHT30 │ BMP280 │ MPU6050 │ ESP8266 │
└─────────────────────────────────────────────────────────┘

任务划分(共 6 个独立任务 + 2 个中断服务例程)

任务名

优先级

栈大小

周期/触发

功能

vTaskSensorReader

3 (高)

512

每 200ms

读取 I2C 传感器,原始数据放入队列

vTaskDataProcessor

2 (中)

512

队列有数据即唤醒

滤波、单位转换、异常检测

vTaskMQTTPublisher

2 (中)

768

每 5s 或队列满

打包 JSON 并通过 ESP8266 发布

vTaskHeartbeat

1 (低)

256

每 1s

LED 闪烁、喂看门狗

vTaskCLI

1 (低)

512

阻塞等待 UART 输入

交互式调试指令

vTaskOTA

4 (最高)

640

事件组触发

从云端拉取固件并写入 Flash


3. 核心通信机制设计与代码实现

3.1 数据结构与队列定义

我们使用两个队列解耦生产者和消费者:

  • xSensorDataQueue:存放原始传感器数据(结构体 SensorRaw_t
  • xPublishQueue:存放待发布的 JSON 字符串指针(动态分配)
代码语言:javascript
复制
// 传感器原始数据(含时间戳)
typedef struct {
    TickType_t timestamp;
    float temperature;   // SHT30
    float humidity;
    float pressure;      // BMP280
    float accel_x, accel_y, accel_z; // MPU6050
    uint8_t sensor_id;
} SensorRaw_t;

// 队列句柄
QueueHandle_t xSensorDataQueue;
QueueHandle_t xPublishQueue;

// 在 main() 中创建
xSensorDataQueue = xQueueCreate(10, sizeof(SensorRaw_t));
xPublishQueue   = xQueueCreate(5,  sizeof(char *));

3.2 传感器读取任务 — 使用 vTaskDelayUntil 实现固定采样

为了避免任务周期的累积误差,使用绝对延迟函数:

代码语言:javascript
复制
void vTaskSensorReader(void *pvParameters) {
    TickType_t xLastWakeTime = xTaskGetTickCount();
    const TickType_t xPeriod = pdMS_TO_TICKS(200);
    SensorRaw_t raw;

    while (1) {
        vTaskDelayUntil(&xLastWakeTime, xPeriod);

        // 读取 SHT30(I2C)
        if (HAL_I2C_Master_Transmit(&hi2c1, SHT30_ADDR, cmd, 2, 100) == HAL_OK) {
            HAL_I2C_Master_Receive(&hi2c1, SHT30_ADDR, rx_buf, 6, 100);
            raw.temperature = sht30_calc_temp(rx_buf);
            raw.humidity    = sht30_calc_humi(rx_buf);
        }

        // 读取 BMP280(SPI)
        bmp280_read(&raw.pressure, &raw.temperature_secondary);

        // 读取 MPU6050(通过 DMA 加速,此处略)
        mpu6050_read_gyro_accel(&raw.accel_x, ...);

        raw.timestamp = xTaskGetTickCount();

        // 发送到处理队列(若队列满则丢弃,采用非阻塞)
        if (xQueueSend(xSensorDataQueue, &raw, 0) != pdPASS) {
            // 可增加丢弃计数统计
        }
    }
}

3.3 数据处理任务 — 滑动平均滤波与异常标记

该任务阻塞在队列接收上,使用 xQueueReceive 配合超时。为提升效率,采用批处理:一次 pdMS_TO_TICKS(100) 超时内尽可能多取。

代码语言:javascript
复制
void vTaskDataProcessor(void *pvParameters) {
    SensorRaw_t raw, filtered;
    static float temp_buf[5] = {0};
    static uint8_t idx = 0;

    while (1) {
        if (xQueueReceive(xSensorDataQueue, &raw, portMAX_DELAY) == pdPASS) {
            // 滑动平均滤波(温度)
            temp_buf[idx++ % 5] = raw.temperature;
            float sum = 0;
            for (int i = 0; i < 5; i++) sum += temp_buf[i];
            filtered.temperature = sum / 5.0f;

            // 异常检测:若温度突变 > 5°C,置标志
            static float last_temp = 0;
            if (fabs(filtered.temperature - last_temp) > 5.0f) {
                filtered.temperature = last_temp; // 剔除野值
                // 可发送事件到告警任务
            }
            last_temp = filtered.temperature;

            // 打包为 JSON 字符串(使用 cJSON 库)
            cJSON *root = cJSON_CreateObject();
            cJSON_AddNumberToObject(root, "temp", filtered.temperature);
            cJSON_AddNumberToObject(root, "humi", raw.humidity);
            cJSON_AddNumberToObject(root, "press", raw.pressure);
            cJSON_AddNumberToObject(root, "ts", raw.timestamp);
            char *json_str = cJSON_PrintUnformatted(root);
            cJSON_Delete(root);

            // 发送到发布队列(若队列满则等待 100ms)
            if (xQueueSend(xPublishQueue, &json_str, pdMS_TO_TICKS(100)) != pdPASS) {
                free(json_str); // 防止内存泄漏
            }
        }
    }
}

3.4 MQTT 发布任务 — 与 ESP8266 的 UART 交互

这是项目中最易阻塞的部分。ESP8266 通过 AT 指令进行 TCP 连接和 MQTT 发布,每条指令响应时间可达数百毫秒。我们不能在任务中忙等,因此设计状态机 + 非阻塞 UART 接收中断

核心思路:任务只负责从队列取出 JSON 串,通过 DMA 发送至 ESP8266,然后等待事件组中的 ESP_RESPONSE_BIT 信号(由 UART 中断置位),并设置超时。

代码语言:javascript
复制
#define ESP_CMD_TIMEOUT  pdMS_TO_TICKS(3000)

void vTaskMQTTPublisher(void *pvParameters) {
    char *json_str;
    while (1) {
        if (xQueueReceive(xPublishQueue, &json_str, portMAX_DELAY) == pdPASS) {
            // 构建 MQTT PUBLISH 报文(此处简化,实际需生成固定头部)
            char mqtt_pub[512];
            snprintf(mqtt_pub, sizeof(mqtt_pub),
                "AT+MQTTPUB=0,\"data/%.3f\",\"%s\",0,0\r\n",
                (double)xTaskGetTickCount() / 1000.0, json_str);

            // 通过 DMA 发送至 ESP8266
            HAL_UART_Transmit_DMA(&huart2, (uint8_t*)mqtt_pub, strlen(mqtt_pub));

            // 等待响应(事件组)
            EventBits_t bits = xEventGroupWaitBits(
                xESP8266EventGroup,
                ESP_RESPONSE_BIT | ESP_ERROR_BIT,
                pdTRUE,   // 清除位
                pdFALSE,  // 任一满足
                ESP_CMD_TIMEOUT
            );

            if (bits & ESP_ERROR_BIT) {
                // 重试或进入错误处理
                xEventGroupSetBits(xSystemEventGroup, SYS_ERR_MQTT_FAIL);
            } else if (!(bits & ESP_RESPONSE_BIT)) {
                // 超时,复位 ESP8266
                HAL_GPIO_WritePin(ESP_RST_PIN, GPIO_PIN_RESET);
                vTaskDelay(pdMS_TO_TICKS(100));
                HAL_GPIO_WritePin(ESP_RST_PIN, GPIO_PIN_SET);
            }

            free(json_str); // 释放动态内存
        }
    }
}

3.5 软件定时器 — 看门狗喂食与系统健康检查

使用 FreeRTOS 软件定时器替代硬件定时器,灵活性更高。创建两个定时器:

  • xWDTimer:每 500ms 回调,喂外部硬件看门狗(IWDG)
  • xHealthTimer:每 30s 检查各任务栈剩余量,若低于阈值则记录日志
代码语言:javascript
复制
TimerHandle_t xWDTimer;

void vWDTCallback(TimerHandle_t xTimer) {
    HAL_IWDG_Refresh(&hiwdg);
    // 同时检查系统事件组是否有致命错误
    if (xEventGroupGetBits(xSystemEventGroup) & SYS_FATAL_MASK) {
        // 触发软复位
        NVIC_SystemReset();
    }
}

// 在 main 中创建
xWDTimer = xTimerCreate("WDT", pdMS_TO_TICKS(500), pdTRUE, NULL, vWDTCallback);
xTimerStart(xWDTimer, 0);

4. 中断与任务同步 — 基于二值信号量的按键响应

物理按键用于触发“立即上报”或“进入配网模式”。中断服务程序只负责给出信号量,具体业务在任务中处理。

代码语言:javascript
复制
// 按键中断(EXTI)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == KEY_PIN) {
        BaseType_t xHigherPriorityTaskWoken = pdFALSE;
        xSemaphoreGiveFromISR(xKeySemaphore, &xHigherPriorityTaskWoken);
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}

// 按键处理任务(高优先级)
void vTaskKeyHandler(void *pvParameters) {
    while (1) {
        if (xSemaphoreTake(xKeySemaphore, portMAX_DELAY) == pdPASS) {
            // 防抖:延迟 50ms 后再读
            vTaskDelay(pdMS_TO_TICKS(50));
            if (HAL_GPIO_ReadPin(KEY_PIN) == GPIO_PIN_RESET) {
                // 触发一次即时发布(向发布队列插入当前最新数据)
                // 这里可借助队列集或直接读取全局共享数据(需用互斥量保护)
            }
        }
    }
}

5. 内存优化与栈溢出保护

嵌入式项目中,堆栈溢出是头号杀手。我们采取三重防护:

  1. 静态分析:使用 uxTaskGetStackHighWaterMark() 在每个任务循环中定期打印剩余栈,调整配置。
  2. 堆栈溢出钩子:在 FreeRTOSConfig.h 中启用 configCHECK_FOR_STACK_OVERFLOW == 2,并实现 vApplicationStackOverflowHook,记录崩溃任务名并复位。
  3. 动态内存分配:所有动态内存(JSON串、队列消息)统一使用 pvPortMalloc,并开启 configUSE_MALLOC_FAILED_HOOK,当分配失败时触发告警。
代码语言:javascript
复制
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
    // 写入内部 Flash 日志区,然后复位
    write_log_to_flash("Stack overflow in %s", pcTaskName);
    NVIC_SystemReset();
}

6. 低功耗设计 — Tickless 模式

对于电池供电场景,我们启用 FreeRTOS 的 configUSE_TICKLESS_IDLE。当所有任务阻塞时,系统进入 STOP 模式,通过 RTC 闹钟或外部中断唤醒。

实现要点:

  • vPortSetupTimerInterrupt() 中配置可编程的休眠唤醒时间
  • 重写 vApplicationSleep(),调用 HAL_PWR_EnterSTOPMode()
  • 注意在唤醒后重新配置系统时钟(HSE 稳定时间)

7. 集成腾讯云 IoT Hub — MQTT 连接与认证

采用腾讯云提供的 MQTT 三元组(ProductID, DeviceName, DeviceSecret)生成 ClientID 和密码(通过 HMAC-SHA256)。代码中预置证书,但生产环境建议烧录至安全存储区。

代码语言:javascript
复制
// 连接参数示例
char client_id[128];
char username[128];
char password[128];

sprintf(client_id, "%s%s", PRODUCT_ID, DEVICE_NAME);
// 使用腾讯云官方 HMAC 算法生成 password(此处省略,可参考 qcloud-iot-sdk)

// 通过 AT 指令连接
AT+MQTTCONN=<client_id>,<username>,<password>,<keepalive>

发布 Topic 格式:$thing/up/property/{ProductID}/{DeviceName},Payload 为腾讯云标准物模型 JSON。


8. 系统稳定性压测与数据

在连续 72 小时运行测试中,系统表现:

  • 最大堆栈使用率:任务 vTaskMQTTPublisher 为 78%(512 字节)
  • 队列丢包率:当传感器以 200ms 周期产生数据,队列深度 10 时,丢包率为 0(经统计)
  • MQTT 重连恢复时间:断网后重新连接平均耗时 3.2s(含 DNS 解析)
  • 看门狗复位次数:0(异常注入除外)

关键调优点:

  • 将 MQTT 发布任务栈从 512 增至 768,因为 snprintfcJSON 占栈较大
  • configTICK_RATE_HZ 设为 1000,提高时间精度
  • 使用 xQueuePeek 在发布前检查是否有更新数据,避免频繁发送相同值(合并发布)

9. 项目可扩展性与就业级考核点

一个“就业级”项目不止于跑通,而要体现工程思维:

考察维度

本方案体现

任务划分合理性

按功能垂直拆分,避免单一任务过重

优先级反转处理

使用互斥量保护共享资源,必要时采用优先级继承(configUSE_MUTEXES)

内存管理

静态+动态混合,使用 heap_4.c 防碎片

异常恢复

看门狗 + 状态机 + 重试机制

调试手段

集成 CLI 任务,支持 ps、queueinfo、mem 等命令

编码规范

遵循 MISRA-C 子集,使用断言(configASSERT)


10. 总结与下一步演进

FreeRTOS 不是一个“黑盒调度器”,而是一套可控的并发工具箱。本文通过一个真实的多传感器上云项目,展示了如何将队列、信号量、事件组、软件定时器、Tickless 模式等特性有机组合,构建出既健壮又高效的嵌入式系统。

后续可演进方向:

  • 引入 FreeRTOS-Plus-TCP 替代 AT 指令,直接在 STM32 上运行 MQTT over lwIP
  • 增加 OTA 差分升级(使用 RTT 的 FAL 组件)
  • 加入 Profiling 工具(如 Tracealyzer)用于可视化调度分析

代码仓库(示例结构): /src ├── main.c ├── FreeRTOSConfig.h ├── tasks/ │ ├── sensor_reader.c │ ├── data_processor.c │ ├── mqtt_publisher.c │ └── cli_cmd.c ├── drivers/ │ ├── i2c_sht30.c │ ├── spi_bmp280.c │ └── uart_esp8266.c └── utils/ ├── ringbuffer.c └── crc16.c

写在最后:真正的嵌入式开发,不是在 IDE 里点“编译通过”就结束,而是要在资源受限、时序苛刻、故障频发的物理世界中,用软件逻辑去对抗不确定性。FreeRTOS 给予我们对抗的武器,而合理的系统设计才是决定胜负的战术。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 基于FreeRTOS的多传感器数据采集系统设计与实现 — 从任务调度到MQTT上云
    • 1. 为什么需要 RTOS?—— 裸机之痛与项目背景
    • 2. 系统总体架构
    • 3. 核心通信机制设计与代码实现
      • 3.1 数据结构与队列定义
      • 3.2 传感器读取任务 — 使用 vTaskDelayUntil 实现固定采样
      • 3.3 数据处理任务 — 滑动平均滤波与异常标记
      • 3.4 MQTT 发布任务 — 与 ESP8266 的 UART 交互
      • 3.5 软件定时器 — 看门狗喂食与系统健康检查
    • 4. 中断与任务同步 — 基于二值信号量的按键响应
    • 5. 内存优化与栈溢出保护
    • 6. 低功耗设计 — Tickless 模式
    • 7. 集成腾讯云 IoT Hub — MQTT 连接与认证
    • 8. 系统稳定性压测与数据
    • 9. 项目可扩展性与就业级考核点
    • 10. 总结与下一步演进
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档