4、系统任务划分:传感器采集任务、数据处理任务、通信上报任务、看门狗监控任务

好,咱们进入正题。任务划分这件事,说白了就是决定「谁干什么活」。

我见过不少新手,上来就把所有代码塞进一个大循环里。结果呢?传感器还没读完,通信就超时了;通信刚发到一半,看门狗又复位了。整个系统乱成一锅粥。

RTOS 的好处,就是把这些「乱成一锅粥」的事情,拆成几个独立的小任务。每个任务只管自己那一亩三分地。系统自然就稳了。

4.1 传感器采集任务:数据的源头

这个任务负责从传感器里把原始数据读出来。比如 pH 值、溶解氧、浊度、温度等等。

我个人习惯,把这个任务的优先级设得中等偏上。为什么?因为数据是新鲜的才好用。你想想看,如果采集任务被别的任务堵住了,读到的数据都是几分钟前的,那后面的数据处理和上报还有什么意义?

核心要点:

  • 优先级:中高优先级。保证数据新鲜度。
  • 周期:根据传感器类型定。温度传感器可以慢一点(比如 5 秒一次),浊度传感器可能需要快一点(比如 1 秒一次)。
  • 注意:传感器通信(I2C、SPI、UART)往往有等待时间。别让任务在这干等,可以用信号量或者消息队列来异步处理。

避坑指南:我曾经在一个项目中,把传感器采集任务的周期设成了 100ms。结果发现传感器芯片根本反应不过来,每次读取都超时。后来查手册才知道,那个传感器的转换时间需要 200ms。嗯,这里要注意,任务的周期不能比传感器的转换时间还短,否则你读到的全是无效数据。

4.2 数据处理任务:把生米煮成熟饭

传感器采集到的原始数据,通常不能直接用。比如 pH 传感器输出的是电压值,你得把它换算成 pH 值。再比如,你可能需要做滑动平均滤波,把偶尔的跳变毛刺去掉。

这个任务,我建议优先级设成中等。它不需要像采集任务那么实时,但也不能太慢,否则数据积压会越来越多。

我的经验:数据处理任务最好用消息队列来接收采集任务发来的数据。这样两个任务就解耦了。采集任务只管往队列里扔数据,处理任务只管从队列里取数据。哪怕处理任务偶尔卡了一下,采集任务也不会被阻塞。

举个例子,一个简单的滑动平均滤波代码片段:

// 数据处理任务
void DataProcessTask(void *param) {
    float raw_data;
    float filtered_data;
    float buffer[10] = {0};
    int index = 0;
    float sum = 0;

    while(1) {
        // 从消息队列接收原始数据
        if(xQueueReceive(dataQueue, &raw_data, portMAX_DELAY) == pdPASS) {
            // 更新滑动窗口
            sum -= buffer[index];
            buffer[index] = raw_data;
            sum += buffer[index];
            index = (index + 1) % 10;

            // 计算滤波后的值
            filtered_data = sum / 10.0f;

            // 把处理后的数据发给通信任务
            xQueueSend(commQueue, &filtered_data, 0);
        }
    }
}

你看,这个任务的核心就是「收数据 -> 处理 -> 发数据」。逻辑清晰,不容易出错。

4.3 通信上报任务:把话说出去

数据处理完了,得想办法传出去。可能是通过 4G 模块、LoRa、NB-IoT,或者就是简单的串口打印。

这个任务的优先级,我建议设成中低。为什么?因为通信往往是最慢的环节。你想想看,发一条数据到云平台,可能要几百毫秒甚至几秒。如果优先级设得太高,它会霸占 CPU,导致传感器采集和数据处理都停下来。

警告:千万别在通信任务里做阻塞式的延时等待!比如用 HAL_Delay(1000) 等模块响应。这会直接卡死整个系统。正确的做法是用状态机 + 超时机制,或者用 RTOS 的延时函数 vTaskDelay() 让出 CPU。

我记得有一次,一个同事把通信任务的优先级设成了最高。结果传感器采集任务一直抢不到 CPU,数据全是旧的。上报上去的数据自然也是错的。排查了半天才发现是优先级搞反了。

4.4 看门狗监控任务:最后的防线

看门狗(Watchdog)是干啥的?说白了,就是系统万一死机了,它能帮你自动重启。

但这个任务比较特殊。它不是用来「喂狗」的。真正的喂狗操作,应该放在一个定时器中断里,或者放在空闲任务里。这个监控任务的作用,是检查其他任务是否还活着

实现思路:

  • 每个任务(采集、处理、通信)都有一个「心跳计数器」。
  • 每个任务在正常运行循环中,把自己的心跳计数器加 1。
  • 看门狗监控任务定期检查这些心跳值。如果某个任务的心跳长时间没变化,说明它可能卡死了。
  • 一旦发现任务卡死,监控任务可以尝试重启该任务,或者直接触发系统复位。

代码示意一下:

// 看门狗监控任务
void WatchdogMonitorTask(void *param) {
    uint32_t last_heartbeat[3] = {0}; // 0:采集, 1:处理, 2:通信

    while(1) {
        // 检查采集任务
        if(task_heartbeat[0] == last_heartbeat[0]) {
            // 心跳没变,任务可能卡死了
            printf("采集任务异常!准备复位...\n");
            // 这里可以尝试重启任务,或者直接复位
            NVIC_SystemReset();
        }
        last_heartbeat[0] = task_heartbeat[0];

        // 检查处理任务
        if(task_heartbeat[1] == last_heartbeat[1]) {
            printf("数据处理任务异常!准备复位...\n");
            NVIC_SystemReset();
        }
        last_heartbeat[1] = task_heartbeat[1];

        // 检查通信任务
        if(task_heartbeat[2] == last_heartbeat[2]) {
            printf("通信任务异常!准备复位...\n");
            NVIC_SystemReset();
        }
        last_heartbeat[2] = task_heartbeat[2];

        // 喂硬件看门狗
        HAL_IWDG_Refresh(&hiwdg);

        // 每 5 秒检查一次
        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}

这个任务的优先级,我建议设成最低。因为它不着急,5 秒检查一次就够了。优先级低,也不会影响其他关键任务。

4.5 任务划分总结

好了,四个任务都讲完了。咱们用一张表总结一下:

任务名称 优先级 周期/触发方式 核心职责
传感器采集任务 中高 定时触发(如 1s) 读取原始传感器数据
数据处理任务 中等 消息队列触发 滤波、换算、数据融合
通信上报任务 中低 消息队列触发 打包数据并发送到上位机/云平台
看门狗监控任务 最低 定时触发(如 5s) 监控其他任务心跳,异常时复位系统

你可能会问:「为什么不用一个任务搞定所有事?」

嗯,这个问题我当年也问过。答案是:耦合度太高。一个任务里如果既有采集、又有处理、又有通信,一旦通信卡住,整个系统就停了。拆成四个独立任务,每个任务只做一件事,出了问题也容易定位。这就是 RTOS 的魅力所在。

最后一个小建议:任务划分不是一成不变的。如果你的水质监测系统只需要采集一个传感器,而且数据直接通过串口打印,那可能两个任务就够了(采集+通信)。但如果你要做多参数监测、数据存储、远程控制,那四个任务可能还不够。灵活一点,根据实际需求来调整。