LoRa 农业监控系统:ESP32 + SX1278 实现千米级传感器网络
在现代农业中,精准灌溉和环境监测已经成为提高产量、节约资源的关键手段。传统的 WiFi 方案功耗高、覆盖范围有限;蓝牙通信距离太短;而 4G/NB-IoT 虽然覆盖广,但需要 SIM 卡和持续的流量费用。
LoRa(Long Range) 技术恰好填补了这个空白:超低功耗、千米级通信距离、无需基站和流量费。对于农场、果园、温室大棚等场景,LoRa 是构建传感器网络的理想选择。
本文将带你从零开始,使用 ESP32 + SX1278 LoRa 模块 搭建一套完整的农业监控系统,包括:
- LoRa 技术原理与关键参数解析
- 硬件选型与电路连接
- 星型拓扑组网方案(网关 + 多个节点)
- 土壤湿度、温度、光照数据采集代码
- 低功耗优化技巧(电池供电可运行数月)
- 实际部署中的调试经验
为什么选择 LoRa 做农业监控?
传统方案的痛点
| 方案 | 覆盖范围 | 功耗 | 成本 | 适用场景 |
|---|---|---|---|---|
| WiFi | <100m | 高 | 低 | 室内、有电源 |
| 蓝牙 BLE | <50m | 中 | 低 | 近距离配对 |
| Zigbee | <200m | 中 | 中 | 智能家居 mesh |
| 4G Cat.1 | 全覆盖 | 高 | 高(SIM+流量) | 远程单点 |
| NB-IoT | 全覆盖 | 低 | 中(SIM+流量) | 低频上报 |
| LoRa | 3-15km | 极低 | 低 | 大面积传感器网络 |
LoRa 的核心优势
- 超远距离:郊区空旷环境可达 10-15km,城市/农田环境典型覆盖 3-5km
- 超低功耗:休眠电流 <5μA,发送一次数据仅需几十毫秒,AA 电池可工作数月
- 免授权频段:使用 433MHz(中国)、868MHz(欧洲)、915MHz(美国)ISM 频段,无需许可证
- 穿透能力强:低频段信号对植被、土壤、建筑结构的穿透优于 2.4GHz WiFi
- 自建网络:无需运营商基站,自己部署网关即可覆盖整个农场
适用场景
- 🌾 大田农业:土壤墒情监测、气象站数据收集
- 🍇 果园/葡萄园:分布式温湿度传感器网络
- 🏡 温室大棚:多棚环境参数集中监控
- 🐄 畜牧养殖:牲畜定位追踪、围栏监控
- 💧 灌溉系统:阀门远程控制、用水量统计
LoRa 技术核心参数解析
扩频调制原理
LoRa 基于 CSS(Chirp Spread Spectrum,线性调频扩频) 技术,将数据编码为频率随时间变化的”chirp”信号。相比传统 FSK 调制,LoRa 在相同功率下可获得更高的接收灵敏度。
关键参数:扩频因子(Spreading Factor, SF)
| SF 值 | 每比特 chirp 数 | 传输速率 | 接收灵敏度 | 抗干扰能力 |
|---|---|---|---|---|
| SF7 | 128 | 快 | -123 dBm | 中 |
| SF9 | 512 | 中 | -130 dBm | 高 |
| SF12 | 4096 | 慢 | -137 dBm | 极高 |
经验法则:SF 每增加 1,传输时间翻倍,但接收灵敏度提升约 2.5dB。农业监控通常选择 SF9-SF10,平衡距离和功耗。
带宽(Bandwidth)
SX1278 支持 7.8kHz - 500kHz 带宽。常用配置:
- 125kHz:标准配置,兼容大多数 LoRaWAN 网络
- 250kHz:速率翻倍,适合高频数据采集
- 500kHz:最高速率,但接收灵敏度下降 6dB
农业场景中,传感器数据变化缓慢(每小时采集一次即可),125kHz 是最佳选择。
编码率(Coding Rate)
LoRa 使用前向纠错(FEC),编码率 CR 表示冗余度:
- CR 4/5:20% 冗余,最快
- CR 4/8:50% 冗余,最可靠
默认使用 CR 4/5,在电磁环境复杂的农场可适当提高到 CR 4/6。
实际通信距离估算
理论链路预算 = 发射功率 - 接收灵敏度
以 SX1278 为例:
- 发射功率:+20 dBm(100mW,中国法规允许的最大值)
- SF10 接收灵敏度:-132 dBm
- 链路预算:20 - (-132) = 152 dB
根据自由空间路径损耗公式,152dB 链路预算在 433MHz 频段对应约 8-10km 的理论距离。实际环境中,受地形、植被、建筑影响,典型覆盖为 3-5km。
硬件选型与电路连接
核心组件清单
| 组件 | 推荐型号 | 单价(约) | 说明 |
|---|---|---|---|
| 主控 MCU | ESP32-WROOM-32 | ¥15 | 双核 240MHz,内置 WiFi/BLE |
| LoRa 模块 | SX1278 433MHz | ¥12 | Semtech 原厂芯片,+20dBm |
| 土壤湿度传感器 | Capacitive v1.2 | ¥8 | 电容式,抗腐蚀 |
| 温湿度传感器 | SHT30 | ¥15 | I2C 接口,精度 ±2%RH |
| 光照传感器 | BH1750 | ¥6 | I2C 接口,1-65535 lux |
| 天线 | 433MHz 弹簧天线 | ¥3 | 增益 2dBi |
| 电源 | 18650 电池 + TP4056 | ¥20 | 3.7V 锂电,带充电保护 |
| PCB/洞洞板 | - | ¥5 | 节点组装基板 |
总成本:单节点约 ¥80-90,网关(只需 ESP32 + SX1278 + 电源)约 ¥35。
SX1278 与 ESP32 接线
SX1278 模块通过 SPI 总线 与 ESP32 通信:
SX1278 ESP32
──────── ─────
VCC → 3.3V
GND → GND
SCK → GPIO 18 (VSPI SCK)
MISO → GPIO 19 (VSPI MISO)
MOSI → GPIO 23 (VSPI MOSI)
NSS/CS → GPIO 5 (VSPI SS)
RESET → GPIO 14
DIO0 → GPIO 2 (中断引脚,接收完成信号)
⚠️ 注意:SX1278 工作电压为 1.8-3.6V,必须接 3.3V,切勿接 5V!
传感器接线
SHT30 (I2C):
VCC → 3.3V
GND → GND
SDA → GPIO 21
SCL → GPIO 22
BH1750 (I2C):
VCC → 3.3V
GND → GND
SDA → GPIO 21 (与 SHT30 共用)
SCL → GPIO 22 (与 SHT30 共用)
电容式土壤湿度传感器:
VCC → 3.3V
GND → GND
AOUT → GPIO 34 (ADC1_CH6)
I2C 设备可共用总线,每个设备有独立地址(SHT30: 0x44, BH1750: 0x23)。
天线选择
- 弹簧天线:成本低,全向辐射,适合固定安装
- 胶棒天线:增益更高(5dBi),方向性更强
- PCB 天线:集成在模块上,体积小但距离短
农业场景推荐使用 外置弹簧天线或胶棒天线,并将天线尽量架高(1-2 米),避免被作物遮挡。
星型拓扑组网方案
农业监控网络采用 星型拓扑:一个网关居中,多个节点分布在周围。
┌──────────┐
│ Gateway │ ← ESP32 + SX1278 + WiFi/4G
│ (中心) │ 数据上传到云服务器
└─────┬─────┘
│ LoRa 433MHz
┌───────────────┼───────────────┐
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ Node 1 │ │ Node 2 │ │ Node 3 │
│土壤湿度 │ │温湿度 │ │光照 │
└─────────┘ └─────────┘ └─────────┘
为什么不用 Mesh?
LoRa Mesh(如 LoRaMesh、RadioHead Mesh)存在以下问题:
- 路由开销大,增加功耗
- 网络稳定性依赖中间节点
- 调试复杂,故障定位困难
对于农业监控这种低频上报、单向为主的场景,星型拓扑更简单可靠。如果某个节点距离网关过远,可增加中继节点(仅转发,不采集数据)。
节点地址分配
每个节点使用唯一的 Node ID(1-255),在代码中硬编码或通过 DIP 开关设置。网关收到数据后,根据 Node ID 区分来源。
// 节点代码中定义
#define NODE_ID 1 // 每个节点修改此值
节点代码示例
使用 Arduino IDE + LoRa library by Sandeep Mistry(GitHub: sandeepmistry/arduino-LoRa)进行开发。
安装依赖库
在 Arduino IDE 库管理器中搜索并安装:
LoRaby Sandeep MistryAdafruit SHT31 LibraryBH1750by Christopher Laws
节点完整代码
#include <SPI.h>
#include <LoRa.h>
#include <Wire.h>
#include <Adafruit_SHT31.h>
#include <BH1750.h>
// ========== 配置参数 ==========
#define NODE_ID 1 // 节点ID,每个节点不同
#define LORA_FREQ 433E6 // 频率 433MHz
#define LORA_SF 10 // 扩频因子 SF10
#define LORA_BW 125E3 // 带宽 125kHz
#define LORA_CR 5 // 编码率 4/5
#define TX_INTERVAL 300000 // 发送间隔 5分钟(ms)
// SPI 引脚定义 (ESP32 VSPI)
#define LORA_SS 5
#define LORA_RST 14
#define LORA_DIO0 2
// 传感器对象
Adafruit_SHT31 sht31;
BH1750 lightMeter;
// ADC 引脚
#define SOIL_PIN 34
void setup() {
Serial.begin(115200);
Serial.println("LoRa Agriculture Node Starting...");
// 初始化传感器
Wire.begin(21, 22); // SDA, SCL
if (!sht31.begin(0x44)) {
Serial.println("SHT31 not found!");
}
if (!lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE)) {
Serial.println("BH1750 not found!");
}
// 初始化 LoRa
LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);
if (!LoRa.begin(LORA_FREQ)) {
Serial.println("LoRa init failed!");
while (1);
}
LoRa.setSpreadingFactor(LORA_SF);
LoRa.setSignalBandwidth(LORA_BW);
LoRa.setCodingRate4(LORA_CR);
LoRa.setTxPower(20); // 最大功率 20dBm
Serial.println("LoRa initialized successfully");
}
void loop() {
// 读取传感器数据
float temperature = sht31.readTemperature();
float humidity = sht31.readHumidity();
uint16_t light = lightMeter.readLightLevel();
int soilMoisture = analogRead(SOIL_PIN); // 0-4095
// 构建数据包: [NODE_ID][temp][humidity][light][soil]
// 使用二进制格式减少传输时间
uint8_t packet[11];
packet[0] = NODE_ID;
// 温度: int16_t, 单位 0.01°C
int16_t tempInt = (int16_t)(temperature * 100);
memcpy(&packet[1], &tempInt, 2);
// 湿度: uint16_t, 单位 0.01%
uint16_t humInt = (uint16_t)(humidity * 100);
memcpy(&packet[3], &humInt, 2);
// 光照: uint16_t, 单位 lux
memcpy(&packet[5], &light, 2);
// 土壤湿度: uint16_t, 原始 ADC 值
uint16_t soilInt = (uint16_t)soilMoisture;
memcpy(&packet[7], &soilInt, 2);
// CRC 校验 (简单异或)
uint8_t crc = 0;
for (int i = 0; i < 9; i++) {
crc ^= packet[i];
}
packet[9] = crc;
packet[10] = 0xAA; // 结束标记
// 发送数据
LoRa.beginPacket();
LoRa.write(packet, 11);
int status = LoRa.endPacket();
if (status) {
Serial.printf("Node %d: T=%.1f H=%.1f L=%d S=%d [OK]\n",
NODE_ID, temperature, humidity, light, soilMoisture);
} else {
Serial.println("Transmission failed!");
}
// 进入深度休眠
Serial.println("Entering deep sleep...");
esp_deep_sleep(TX_INTERVAL * 1000); // 微秒
}
代码要点解析
- 二进制打包:相比 JSON 文本,二进制格式将数据包从 ~50 字节压缩到 11 字节,传输时间减少 70%
- CRC 校验:简单的异或校验可检测传输错误,网关收到后验证
- 深度休眠:
esp_deep_sleep()关闭 CPU 和大部分外设,电流降至 ~10μA - 固定间隔:每 5 分钟唤醒一次,采集并发送,然后继续休眠
网关代码示例
网关负责接收所有节点的数据,并通过 WiFi 上传到服务器。
#include <SPI.h>
#include <LoRa.h>
#include <WiFi.h>
#include <HTTPClient.h>
// ========== WiFi 配置 ==========
const char* ssid = "YourWiFiSSID";
const char* password = "YourWiFiPassword";
// ========== 服务器配置 ==========
const char* serverUrl = "http://your-server.com/api/lora-data";
// LoRa 引脚 (同节点)
#define LORA_SS 5
#define LORA_RST 14
#define LORA_DIO0 2
void setup() {
Serial.begin(115200);
// 连接 WiFi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected");
// 初始化 LoRa (参数必须与节点一致)
LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);
if (!LoRa.begin(433E6)) {
Serial.println("LoRa init failed!");
while (1);
}
LoRa.setSpreadingFactor(10);
LoRa.setSignalBandwidth(125E3);
LoRa.setCodingRate4(5);
Serial.println("Gateway ready, waiting for packets...");
}
void loop() {
// 尝试接收数据包
int packetSize = LoRa.parsePacket();
if (packetSize == 11) { // 预期长度
uint8_t packet[11];
LoRa.readBytes(packet, 11);
// 验证结束标记
if (packet[10] != 0xAA) {
Serial.println("Invalid packet end");
return;
}
// 验证 CRC
uint8_t crc = 0;
for (int i = 0; i < 9; i++) {
crc ^= packet[i];
}
if (crc != packet[9]) {
Serial.println("CRC check failed");
return;
}
// 解析数据
uint8_t nodeId = packet[0];
int16_t tempInt;
memcpy(&tempInt, &packet[1], 2);
float temperature = tempInt / 100.0;
uint16_t humInt;
memcpy(&humInt, &packet[3], 2);
float humidity = humInt / 100.0;
uint16_t light;
memcpy(&light, &packet[5], 2);
uint16_t soil;
memcpy(&soil, &packet[7], 2);
// 打印日志
Serial.printf("Received from Node %d: T=%.1f H=%.1f L=%d S=%d RSSI=%d\n",
nodeId, temperature, humidity, light, soil, LoRa.packetRssi());
// 上传到服务器
uploadToServer(nodeId, temperature, humidity, light, soil);
}
}
void uploadToServer(uint8_t nodeId, float temp, float hum,
uint16_t light, uint16_t soil) {
if (WiFi.status() != WL_CONNECTED) {
return;
}
HTTPClient http;
http.begin(serverUrl);
http.addHeader("Content-Type", "application/json");
// 构建 JSON payload
String json = "{";
json += "\"node_id\":" + String(nodeId) + ",";
json += "\"temperature\":" + String(temp, 2) + ",";
json += "\"humidity\":" + String(hum, 2) + ",";
json += "\"light\":" + String(light) + ",";
json += "\"soil_moisture\":" + String(soil);
json += "}";
int httpResponseCode = http.POST(json);
if (httpResponseCode > 0) {
Serial.printf("Upload OK: %d\n", httpResponseCode);
} else {
Serial.printf("Upload failed: %d\n", httpResponseCode);
}
http.end();
}
网关设计要点
- 持续监听:网关不进入休眠,持续调用
LoRa.parsePacket()检查是否有数据 - RSSI 记录:
LoRa.packetRssi()返回信号强度,可用于判断节点距离和天线方向 - WiFi 重连:如果 WiFi 断开,网关应自动重连,避免数据丢失
- 数据缓冲:如果服务器暂时不可用,可将数据暂存到 SPIFFS/SD 卡,后续补传
低功耗优化技巧
农业传感器节点通常部署在野外,依靠电池供电。以下是经过实战验证的低功耗优化方法:
1. 深度休眠策略
ESP32 的深度休眠电流约 10-15μA,是普通运行状态(~80mA)的 1/5000。
// 计算休眠时间
#define INTERVAL_HOURS 1 // 每小时采集一次
esp_deep_sleep(INTERVAL_HOURS * 3600 * 1000000); // 微秒
电池寿命估算(2000mAh 18650 电池):
- 活跃时间:每次唤醒后工作 ~2 秒(采集 + 发送)
- 休眠时间:3598 秒
- 平均电流 ≈ (80mA × 2s + 10μA × 3598s) / 3600s ≈ 0.05mA
- 理论续航:2000mAh / 0.05mA ≈ 40000 小时 ≈ 4.5 年
实际中由于自放电、温度影响,典型续航为 6-12 个月。
2. 关闭未使用的外设
// 进入休眠前关闭 WiFi/BLE(如果未使用)
WiFi.disconnect(true);
btStop();
// 关闭 I2C 外设电源(使用 MOSFET 控制)
digitalWrite(SENSOR_POWER_PIN, LOW); // 断电
delay(100);
3. 自适应发送间隔
根据数据变化率动态调整采集频率:
// 如果土壤湿度变化 <5%,延长到 2 小时间隔
if (abs(currentSoil - lastSoil) < 200) {
esp_deep_sleep(2 * 3600 * 1000000);
} else {
esp_deep_sleep(30 * 60 * 1000000); // 变化大时缩短到 30 分钟
}
4. 太阳能补充
对于长期部署的节点,可加装小型太阳能板(5V 1W)+ TP4056 充电模块,实现永久续航。注意选择带过充保护的充电板。
实际部署调试经验
常见问题与解决方案
问题 1:节点无法被网关收到
排查步骤:
- 检查 LoRa 参数是否完全一致(频率、SF、BW、CR)
- 用串口监视器查看节点的
LoRa.endPacket()返回值 - 测量天线馈线是否有断路或短路
- 尝试将节点靠近网关(<10m)测试基本通信
常见错误:
// ❌ 错误:网关和节点 SF 不一致
// 节点: LoRa.setSpreadingFactor(10);
// 网关: LoRa.setSpreadingFactor(7); // 不匹配!
// ✅ 正确:两者必须相同
问题 2:数据包 CRC 校验失败
可能原因:
- 电磁干扰(附近有电机、变频器)
- 天线接触不良
- 距离过远,信号接近接收灵敏度极限
解决方法:
- 提高编码率:
LoRa.setCodingRate4(6)或(7) - 增加 SF:从 SF10 提升到 SF11 或 SF12
- 检查天线连接,确保 SMA 接头拧紧
问题 3:电池消耗过快
排查清单:
- 确认进入了
esp_deep_sleep(),而非delay() - 检查是否有外设持续耗电(如 LED、传感器未断电)
- 测量休眠电流:断开电池,串联万用表,应 <20μA
- 检查 SX1278 是否在发送后进入休眠模式
测量方法:
电池正极 → 万用表(电流档) → ESP32 VCC
正常休眠电流: 10-15μA
异常偏高: >100μA,需排查漏电
问题 4:土壤湿度读数不稳定
电容式传感器易受以下因素影响:
- 土壤紧密程度(接触电阻变化)
- 肥料/盐分浓度(电导率变化)
- 传感器表面氧化
校准方法:
// 空气中读数(干燥)
int dryValue = analogRead(SOIL_PIN); // 约 3000-3500
// 水中读数(饱和)
int wetValue = analogRead(SOIL_PIN); // 约 1000-1500
// 线性映射到 0-100%
int moisturePercent = map(soilValue, wetValue, dryValue, 100, 0);
moisturePercent = constrain(moisturePercent, 0, 100);
建议每 3-6 个月重新校准一次。
现场部署建议
- 天线高度:将网关天线架设在 2-3 米高度,节点天线至少 0.5 米,避免被作物遮挡
- 防水处理:所有节点外壳使用 IP65 以上防护等级,接线处用热缩管密封
- 防雷措施:雷雨多发地区,天线与设备之间加装气体放电管或 TVS 二极管
- 标签管理:每个节点外壳标注 Node ID 和安装位置,方便后期维护
- 测试先行:正式部署前,先在目标区域进行 链路预算测试,确认最远节点的 RSSI > -120 dBm
后续扩展方向
完成基础监控后,可以进一步扩展系统功能:
1. 下行控制
当前系统是单向通信(节点→网关)。如需远程控制灌溉阀门,可增加下行链路:
// 网关发送控制命令
LoRa.beginPacket();
LoRa.write(0x01); // 目标 Node ID
LoRa.write(0xA5); // 命令:打开阀门
LoRa.endPacket();
// 节点监听下行数据
if (LoRa.parsePacket()) {
uint8_t cmd = LoRa.read();
if (cmd == 0xA5) {
digitalWrite(RELAY_PIN, HIGH); // 打开电磁阀
}
}
注意:节点需要定期唤醒监听下行数据,会增加功耗。可采用时分复用:节点在固定时间窗口(如整点)唤醒 5 秒监听。
2. 接入 LoRaWAN
如果希望兼容标准 LoRaWAN 网络(如 The Things Network),可将 SX1278 替换为 RAK4200 或 ESP32 + RFM95 + LMIC 库,通过 OTAA 加入网络。优势是多网关漫游、云端管理;劣势是配置复杂、依赖公共网络。
3. 数据可视化
将网关上传的数据接入 Grafana + InfluxDB,实现:
- 实时温湿度曲线
- 土壤湿度热力图
- 异常值告警(邮件/短信)
- 历史数据导出
4. 边缘计算
在网关端增加简单逻辑:
- 土壤湿度 <30% 且未来 2 小时无雨 → 自动开启灌溉
- 温度 >35°C → 启动遮阳网
- 连续 3 次通信失败 → 标记节点离线
使用 ESP32 的第二个核心运行控制逻辑,不影响 LoRa 接收。
总结
LoRa 技术为农业监控提供了一种低成本、低功耗、远距离的解决方案。通过 ESP32 + SX1278 的组合,你可以用不到 ¥100 的单节点成本,构建覆盖数平方公里的传感器网络。
关键要点回顾:
- 选择合适的 SF 和带宽,平衡距离与功耗
- 使用二进制打包和深度休眠,最大化电池寿命
- 星型拓扑简单可靠,适合低频上报场景
- 现场部署注意天线高度和防水防雷
希望本文能帮助你快速搭建自己的 LoRa 农业监控系统。如果有问题,欢迎在评论区交流!
本文代码已上传至 GitHub:[链接待补充] 硬件采购清单:[淘宝/京东链接待补充]