【项目实战】基于 STM32 的智慧农业环境监测与远程控制系统:温湿度+土壤墒情采集、RS485 Modbus 组网、WiFi/4G MQTT 上云、Python(PyQt5)可视化
·
关键词: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()
八、调试与踩坑总结
- 先单点跑通传感器:SHT30/DHT22/ADC 的数据是否稳定?供电是否干净?
- 再跑通 RS485 物理层:示波器看 A/B 线波形、是否有反射、终端电阻是否正确
- 再验证 Modbus:用 PC + USB-RS485 + Modbus Poll 读寄存器,确认 CRC/字节序
- 最后再接 MQTT:先在网关端
publish,用 MQTTX 订阅确认 payload
常见问题:
- 读取寄存器偶发失败:多半是 DE/RE 切换时序、或总线终端/偏置不正确
- 土壤湿度波动大:模拟量线太长、没有 RC 滤波、传感器供电纹波大
- 上位机卡顿:UI 线程里做了网络/数据库 I/O;改为队列 + 定时刷新
九、总结
这套方案的关键点不是“堆模块”,而是链路设计:
- 现场侧用 RS485/Modbus 保证稳定(抗干扰、远距离、多节点)
- 上行用 MQTT 保证轻量与可扩展(上云/上位机都容易接)
- 上位机用 PyQt5 提升交付效率(可视化、报警、入库一把梭)
更多推荐


所有评论(0)