关键词:STM32 / FreeRTOS / RS485 / Modbus RTU / MQTT / ESP8266 / 4G 模块 / PyQt5 / paho-mqtt / SQLite


一、项目概述

1.1 背景与目标

智慧农业的核心,是把“地里的环境”变成“可量化的数据”,再把数据变成“可执行的控制”。在温室/大棚/育苗房等场景里,常见痛点是:

  • 环境参数(温湿度、土壤含水率、光照等)依赖人工巡检,数据不连续
  • 浇灌/通风/加热等执行器控制不及时,导致作物应激、病害风险上升
  • 多个监测点分散部署,布线距离长、干扰大,WiFi 覆盖不稳定

本文给出一套端到端可落地的方案:

  • 采集节点(STM32 + FreeRTOS):采集温湿度、土壤墒情(模拟量),支持继电器/电磁阀控制
  • 现场网络(RS485 + Modbus RTU):多节点总线组网,抗干扰强,距离可达上百米
  • 网关(WiFi/4G):轮询 Modbus,从站数据汇聚后通过 MQTT 上云(或直连上位机)
  • 上位机(Python + PyQt5):实时曲线、设备状态、阈值报警、历史数据入库

你可以把它理解为:“工业现场总线 + IoT 上云协议” 的组合拳:底层稳,上层轻。


二、技术选型(含 WiFi/4G 两种上行)

模块 选型 说明
采集 MCU STM32F103(示例) 成本低、生态成熟;你也可替换 F4/H7
RTOS FreeRTOS 任务划分清晰:采集/协议/控制互不阻塞
温湿度 SHT30(I2C)/ DHT22(二选一) SHT30 精度更稳,工业场景更推荐
土壤墒情 电容式土壤湿度传感器(模拟量) ADC 采样即可;注意供电与干扰
现场总线 RS485 + Modbus RTU 抗干扰强、距离远、支持多节点
网关(WiFi) ESP8266 / ESP32 Arduino 生态方便,做 Modbus 主站 + MQTT 客户端
网关(4G) EC20/SIM7600 等 方案一:网关上跑 MQTT;方案二:用 4G DTU 透传到云
协议(上云) MQTT(EMQX/阿里云/自建) 轻量、QoS 可选、适合 IoT
上位机 Python + PyQt5 + pyqtgraph 实时曲线很舒服,开发效率高
本地存储 SQLite 单机部署简单,后续可升级 MySQL/InfluxDB

三、系统架构设计

在这里插入图片描述

3.1 三层架构(感知层/传输层/应用层)

  • 感知层(多节点):STM32 采集传感器 + 控制执行器(阀门/风机/加热)
  • 传输层(网关):RS485 上的 Modbus 主站轮询;上行走 WiFi 或 4G;MQTT 发布数据
  • 应用层:MQTT Broker + 上位机订阅展示/入库/报警

3.2 通信链路

在这里插入图片描述

3.3 Topic 与数据格式(建议)

  • 数据上报 Topic:
    • agri/{gatewayId}/node/{nodeId}/telemetry
  • 控制下发 Topic(可选):
    • agri/{gatewayId}/node/{nodeId}/cmd

上报 payload(JSON):

{
  "ts": 1700000000,
  "nodeId": 3,
  "t": 26.4,
  "rh": 58.2,
  "soil": 41.5,
  "relay": 0,
  "rssi": -61,
  "vbat": 3.29
}

四、硬件连接与注意事项

4.1 采集节点(STM32)引脚示例

  • SHT30(I2C):PB6(SCL) / PB7(SDA)
  • 土壤湿度模拟量:PA0(ADC)
  • RS485(USART1):PA9(TX) / PA10(RX)
  • RS485 方向控制:PA8(DE/RE)
  • 继电器:PB0(或任意 GPIO)

4.2 RS485 总线经验(很关键)

  • 总线两端加 120Ω 终端电阻(只在两端,不要每个节点都加)
  • 节点多、线长时,建议加入偏置电阻(上拉/下拉)避免空闲态抖动
  • Modbus RTU 典型参数:9600/19200,8N1;线长上去以后宁可降波特率换稳定
  • DE/RE 一定要切换干净:发送前拉高,发送结束后延时 1~2 字节时间再拉低

五、STM32(FreeRTOS)固件实现

这里以“Modbus 从站”为核心:网关轮询寄存器,STM32 负责更新寄存器值并响应。

5.1 寄存器映射设计(建议)

用保持寄存器(Holding Registers, 4xxxx)承载数据:

地址(寄存器索引) 含义 单位/倍率 说明
0 温度 T ×10 26.4℃ → 264
1 湿度 RH ×10 58.2% → 582
2 土壤湿度 Soil ×10 41.5% → 415
3 继电器状态 0/1 读状态
4 设备电压 ×100 3.29V → 329

控制使用线圈(Coils, 0xxxx)或写单寄存器:

  • 线圈 0:继电器开关(0/1)

5.2 任务划分

  • Task_Sensor:周期采集 SHT30 + ADC,做滤波/标定,写入寄存器缓存
  • Task_Modbus:处理 UART 接收帧、CRC 校验、组包响应
  • Task_Control:根据线圈/寄存器的控制位驱动继电器,并做互锁保护

5.3 关键代码(示例,可直接改到工程里)

(1) 全局寄存器缓存
#include "cmsis_os.h"
#include <stdint.h>

#define HOLD_REG_NUM  16
volatile uint16_t g_holdReg[HOLD_REG_NUM];
volatile uint8_t  g_coilRelay;   // 线圈0:继电器

// 写入时注意临界区(不同任务会读写)
static inline void HoldReg_Write(uint16_t idx, uint16_t val)
{
    taskENTER_CRITICAL();
    g_holdReg[idx] = val;
    taskEXIT_CRITICAL();
}
(2) 传感器采集任务(含简单滤波)
void Task_Sensor(void *arg)
{
    float t=0, rh=0;
    uint16_t adc=0;

    for(;;)
    {
        // 1) SHT30 读取(这里用伪函数,按你驱动替换)
        // SHT30_Read(&t, &rh);

        // 2) ADC 采样(土壤湿度模拟量)
        // adc = ADC_ReadOnce();

        // 3) 简单映射:0~4095 -> 0~1000(=100.0%)
        float soil = (float)adc * 100.0f / 4095.0f;

        // 4) 写寄存器(倍率:×10)
        HoldReg_Write(0, (int16_t)(t * 10));
        HoldReg_Write(1, (uint16_t)(rh * 10));
        HoldReg_Write(2, (uint16_t)(soil * 10));
        HoldReg_Write(3, g_coilRelay ? 1 : 0);

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}
(3) Modbus RTU CRC16
static uint16_t Modbus_CRC16(const uint8_t *buf, uint16_t len)
{
    uint16_t crc = 0xFFFF;
    for (uint16_t i=0; i<len; i++) {
        crc ^= buf[i];
        for (uint8_t j=0; j<8; j++) {
            if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001;
            else crc >>= 1;
        }
    }
    return crc;
}
(4) 读保持寄存器(0x03)响应(核心)
// req: [addr][03][startHi][startLo][qtyHi][qtyLo][crcLo][crcHi]
static uint16_t Build_Resp_03(uint8_t addr, uint16_t start, uint16_t qty, uint8_t *out)
{
    out[0] = addr;
    out[1] = 0x03;
    out[2] = (uint8_t)(qty * 2);

    for(uint16_t i=0;i<qty;i++){
        uint16_t v = g_holdReg[start + i];
        out[3 + 2*i] = (uint8_t)(v >> 8);
        out[4 + 2*i] = (uint8_t)(v & 0xFF);
    }

    uint16_t len = 3 + qty*2;
    uint16_t crc = Modbus_CRC16(out, len);
    out[len + 0] = (uint8_t)(crc & 0xFF);
    out[len + 1] = (uint8_t)(crc >> 8);
    return len + 2;
}
(5) 写单线圈(0x05)用于继电器控制
// req: [addr][05][coilHi][coilLo][FF00/0000][crc]
static uint16_t Handle_Req_05(const uint8_t *req, uint8_t *resp)
{
    uint16_t coil = (req[2]<<8) | req[3];
    uint16_t val  = (req[4]<<8) | req[5];

    if(coil == 0){
        g_coilRelay = (val == 0xFF00) ? 1 : 0;
    }

    // 按 Modbus 规范:响应原样回显
    for(int i=0;i<8;i++) resp[i] = req[i];
    return 8;
}

工程建议:UART 接收用 DMA + 空闲中断(IDLE)取帧,稳定性比轮询好。这里为了文章可读性用“功能片段”方式讲清楚核心逻辑。


六、网关实现(两条路:WiFi 网关 or 4G 网关)

6.1 WiFi 网关(ESP8266/ESP32)思路

  • ESP 作为 Modbus 主站:轮询各节点寄存器(0x03)
  • 拼成 JSON,发布到 MQTT

ESP8266(Arduino)示例伪代码:

// 伪代码:Modbus 主站轮询 + MQTT 发布
for (int nodeId=1; nodeId<=N; nodeId++) {
  auto regs = modbusReadHolding(nodeId, 0, 5);
  float t = regs[0] / 10.0;
  float rh = regs[1] / 10.0;
  float soil = regs[2] / 10.0;
  int relay = regs[3];

  String payload = String("{\"nodeId\":") + nodeId +
    ",\"t\":" + t + ",\"rh\":" + rh + ",\"soil\":" + soil + ",\"relay\":" + relay + "}";

  mqtt.publish(topic, payload.c_str(), true);
}

6.2 4G 上行(EC20/SIM7600)两种实践

方案 A:4G 网关跑 MQTT(推荐工程化)

  • 4G 模块拨号上网(或自带 MQTT/HTTP AT 指令)
  • 网关同样做 Modbus 主站,MQTT 发布

方案 B:4G DTU 透传(最快落地)

  • RS485 数据透传到云端
  • 云端解析 Modbus/自定义帧

建议:小规模快速演示用透传,大规模稳定运行用 MQTT 网关


七、上位机(Python + PyQt5)实时可视化与入库

7.1 功能点

  • 订阅 agri/+/node/+/telemetry
  • 实时曲线(温度/湿度/土壤)
  • 设备在线/离线(基于最后上报时间)
  • 阈值报警(弹窗/声音/日志)
  • SQLite 历史数据存储

7.2 MQTT 接收(paho-mqtt)核心代码

import json, time
import paho.mqtt.client as mqtt

LATEST = {}  # nodeId -> last payload

def on_message(client, userdata, msg):
    try:
        data = json.loads(msg.payload.decode('utf-8'))
        node = int(data.get('nodeId', 0))
        data['__rx_ts'] = time.time()
        LATEST[node] = data
    except Exception as e:
        print('bad message:', e)

cli = mqtt.Client()
cli.on_message = on_message
cli.connect('127.0.0.1', 1883, 60)
cli.subscribe('agri/+/node/+/telemetry')
cli.loop_start()

UI 线程与 MQTT 线程不要互相阻塞:MQTT 用 loop_start(),UI 用 QTimer 每 200ms 刷一次 LATEST

7.3 PyQt5 + pyqtgraph 实时曲线(刷新逻辑)

# 伪代码:QTimer 每 200ms 取 LATEST 更新曲线
from PyQt5.QtCore import QTimer

def refresh_ui():
    node = current_node_id
    if node in LATEST:
        d = LATEST[node]
        t = d['t']; rh = d['rh']; soil = d['soil']
        append_point('t', t)
        append_point('rh', rh)
        append_point('soil', soil)

timer = QTimer()
timer.timeout.connect(refresh_ui)
timer.start(200)

7.4 SQLite 入库(建议结构)

表结构:

CREATE TABLE IF NOT EXISTS telemetry (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  ts INTEGER,
  nodeId INTEGER,
  t REAL,
  rh REAL,
  soil REAL,
  relay INTEGER
);

写入(在 on_message 或单独线程/队列里做):

import sqlite3
conn = sqlite3.connect('agri.db', check_same_thread=False)
cur = conn.cursor()

def insert_row(d):
    cur.execute(
        'INSERT INTO telemetry(ts,nodeId,t,rh,soil,relay) VALUES(?,?,?,?,?,?)',
        (int(d.get('ts', time.time())), d['nodeId'], d['t'], d['rh'], d['soil'], d.get('relay',0))
    )
    conn.commit()

八、调试与踩坑总结

  1. 先单点跑通传感器:SHT30/DHT22/ADC 的数据是否稳定?供电是否干净?
  2. 再跑通 RS485 物理层:示波器看 A/B 线波形、是否有反射、终端电阻是否正确
  3. 再验证 Modbus:用 PC + USB-RS485 + Modbus Poll 读寄存器,确认 CRC/字节序
  4. 最后再接 MQTT:先在网关端 publish,用 MQTTX 订阅确认 payload

常见问题:

  • 读取寄存器偶发失败:多半是 DE/RE 切换时序、或总线终端/偏置不正确
  • 土壤湿度波动大:模拟量线太长、没有 RC 滤波、传感器供电纹波大
  • 上位机卡顿:UI 线程里做了网络/数据库 I/O;改为队列 + 定时刷新

九、总结

这套方案的关键点不是“堆模块”,而是链路设计:

  • 现场侧用 RS485/Modbus 保证稳定(抗干扰、远距离、多节点)
  • 上行用 MQTT 保证轻量与可扩展(上云/上位机都容易接)
  • 上位机用 PyQt5 提升交付效率(可视化、报警、入库一把梭)
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐