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+流量)低频上报
LoRa3-15km极低大面积传感器网络

LoRa 的核心优势

  1. 超远距离:郊区空旷环境可达 10-15km,城市/农田环境典型覆盖 3-5km
  2. 超低功耗:休眠电流 <5μA,发送一次数据仅需几十毫秒,AA 电池可工作数月
  3. 免授权频段:使用 433MHz(中国)、868MHz(欧洲)、915MHz(美国)ISM 频段,无需许可证
  4. 穿透能力强:低频段信号对植被、土壤、建筑结构的穿透优于 2.4GHz WiFi
  5. 自建网络:无需运营商基站,自己部署网关即可覆盖整个农场

适用场景

  • 🌾 大田农业:土壤墒情监测、气象站数据收集
  • 🍇 果园/葡萄园:分布式温湿度传感器网络
  • 🏡 温室大棚:多棚环境参数集中监控
  • 🐄 畜牧养殖:牲畜定位追踪、围栏监控
  • 💧 灌溉系统:阀门远程控制、用水量统计

LoRa 技术核心参数解析

扩频调制原理

LoRa 基于 CSS(Chirp Spread Spectrum,线性调频扩频) 技术,将数据编码为频率随时间变化的”chirp”信号。相比传统 FSK 调制,LoRa 在相同功率下可获得更高的接收灵敏度。

关键参数:扩频因子(Spreading Factor, SF)

SF 值每比特 chirp 数传输速率接收灵敏度抗干扰能力
SF7128-123 dBm
SF9512-130 dBm
SF124096-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


硬件选型与电路连接

核心组件清单

组件推荐型号单价(约)说明
主控 MCUESP32-WROOM-32¥15双核 240MHz,内置 WiFi/BLE
LoRa 模块SX1278 433MHz¥12Semtech 原厂芯片,+20dBm
土壤湿度传感器Capacitive v1.2¥8电容式,抗腐蚀
温湿度传感器SHT30¥15I2C 接口,精度 ±2%RH
光照传感器BH1750¥6I2C 接口,1-65535 lux
天线433MHz 弹簧天线¥3增益 2dBi
电源18650 电池 + TP4056¥203.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 库管理器中搜索并安装:

  • LoRa by Sandeep Mistry
  • Adafruit SHT31 Library
  • BH1750 by 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);  // 微秒
}

代码要点解析

  1. 二进制打包:相比 JSON 文本,二进制格式将数据包从 ~50 字节压缩到 11 字节,传输时间减少 70%
  2. CRC 校验:简单的异或校验可检测传输错误,网关收到后验证
  3. 深度休眠esp_deep_sleep() 关闭 CPU 和大部分外设,电流降至 ~10μA
  4. 固定间隔:每 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();
}

网关设计要点

  1. 持续监听:网关不进入休眠,持续调用 LoRa.parsePacket() 检查是否有数据
  2. RSSI 记录LoRa.packetRssi() 返回信号强度,可用于判断节点距离和天线方向
  3. WiFi 重连:如果 WiFi 断开,网关应自动重连,避免数据丢失
  4. 数据缓冲:如果服务器暂时不可用,可将数据暂存到 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:节点无法被网关收到

排查步骤

  1. 检查 LoRa 参数是否完全一致(频率、SF、BW、CR)
  2. 用串口监视器查看节点的 LoRa.endPacket() 返回值
  3. 测量天线馈线是否有断路或短路
  4. 尝试将节点靠近网关(<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 个月重新校准一次。

现场部署建议

  1. 天线高度:将网关天线架设在 2-3 米高度,节点天线至少 0.5 米,避免被作物遮挡
  2. 防水处理:所有节点外壳使用 IP65 以上防护等级,接线处用热缩管密封
  3. 防雷措施:雷雨多发地区,天线与设备之间加装气体放电管或 TVS 二极管
  4. 标签管理:每个节点外壳标注 Node ID 和安装位置,方便后期维护
  5. 测试先行:正式部署前,先在目标区域进行 链路预算测试,确认最远节点的 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 替换为 RAK4200ESP32 + RFM95 + LMIC 库,通过 OTAA 加入网络。优势是多网关漫游、云端管理;劣势是配置复杂、依赖公共网络。

3. 数据可视化

将网关上传的数据接入 Grafana + InfluxDB,实现:

  • 实时温湿度曲线
  • 土壤湿度热力图
  • 异常值告警(邮件/短信)
  • 历史数据导出

4. 边缘计算

在网关端增加简单逻辑:

  • 土壤湿度 <30% 且未来 2 小时无雨 → 自动开启灌溉
  • 温度 >35°C → 启动遮阳网
  • 连续 3 次通信失败 → 标记节点离线

使用 ESP32 的第二个核心运行控制逻辑,不影响 LoRa 接收。


总结

LoRa 技术为农业监控提供了一种低成本、低功耗、远距离的解决方案。通过 ESP32 + SX1278 的组合,你可以用不到 ¥100 的单节点成本,构建覆盖数平方公里的传感器网络。

关键要点回顾

  • 选择合适的 SF 和带宽,平衡距离与功耗
  • 使用二进制打包和深度休眠,最大化电池寿命
  • 星型拓扑简单可靠,适合低频上报场景
  • 现场部署注意天线高度和防水防雷

希望本文能帮助你快速搭建自己的 LoRa 农业监控系统。如果有问题,欢迎在评论区交流!


本文代码已上传至 GitHub:[链接待补充] 硬件采购清单:[淘宝/京东链接待补充]