1. 项目缘起与整体设计思路WETRON 这个项目名字拆开看就是 Water Environment Tron本质上是一台自主环境监测终端。我最初做它的动机很朴素家里有个小院子种了些菜和花夏天浇水全靠感觉要么旱死要么涝死。市面上能买到的土壤湿度计要么是那种插土里指针式的精度感人要么是带 WiFi 的智能花盆价格不便宜而且数据全在厂商云上断网就废。于是我想干脆自己搭一个能长期扔在户外、自己供电、自己采集数据、自己把数据传回来不用我操心。这个项目的核心关键词是ESP32-S3、LoRa、Arduino IDE / PlatformIO IDE。ESP32-S3 是主控负责采集传感器数据、做本地逻辑判断、驱动 LoRa 模块发送LoRa 负责远距离低功耗的数据回传开发环境我两个都用过后面会详细对比为什么最后主力用 PlatformIO。整套系统要解决的核心问题是在无稳定网络覆盖的户外场景下实现低功耗、长续航、远距离的环境数据自主采集与回传。适合谁来参考这篇内容如果你玩过 Arduino、点过 LED、用过 DHT11 这类传感器那这个项目对你来说难度适中能学到从选型、供电设计、LoRa 组网到低功耗调优的完整链路。如果你是完全零基础也没关系我会把每个关键决策背后的“为什么”讲清楚你照着抄作业也能跑通。整套方案我实测下来节点在户外连续工作了两周多数据回传稳定中间踩过的坑我都会写出来。先说清楚这个项目不做什么它不做复杂的云端可视化大屏不做机器学习预测也不追求工业级防护。它就是一个务实的、能落地的、成本可控的户外环境监测方案。核心监测指标我选了四类空气温湿度、土壤湿度、光照强度、电池电压。这四类数据基本能覆盖一个小型种植场景的决策需求而且对应的传感器都便宜、成熟、好买。提示环境监测项目的成败一半在传感器选型一半在供电设计。主控和通信反而是最不容易出问题的部分。很多人一上来就纠结用什么芯片其实方向反了。2. 核心硬件选型与背后的取舍逻辑2.1 为什么是 ESP32-S3 而不是其他型号ESP32 家族型号很多ESP32、ESP32-S2、ESP32-S3、ESP32-C3 各有侧重。我选 S3 的理由有三条都是实际用下来才体会到的。第一S3 的 GPIO 数量和外设更充裕。这个项目要接 I2C 的温湿度传感器、ADC 采集土壤湿度和电池电压、还要驱动 LoRa 模块的 SPI 接口再加上可能的调试串口。普通 ESP32 勉强够用但一旦想加个 OLED 本地显示或者 SD 卡本地存储引脚就紧张了。S3 的引脚布局更宽松留有余量。第二S3 支持原生 USB调试和烧录不用额外买 USB-TTL 转换器一根 Type-C 线搞定。这在户外调试时特别省事少带一个模块就少一个故障点。第三S3 的 ADC 和低功耗管理比老款 ESP32 更成熟。虽然 ESP32 系列的 ADC 精度一直被吐槽但 S3 在深睡眠电流和唤醒逻辑上做了优化配合合理的电源设计续航能明显拉长。至于为什么不选 STM32 配 LoRa热词里也有“stm32 lora 温控电路”这种组合。STM32 的低功耗确实做得好但它的开发生态对个人玩家不够友好尤其是网络协议栈和 OTA 升级这块ESP32 有现成的库和成熟方案省心太多。这个项目追求的是快速落地和可维护性不是极致低功耗所以 ESP32-S3 是更平衡的选择。2.2 LoRa 模块怎么挑参数怎么看LoRa 这块水比较深市面上模块从十几块到上百块都有。核心要看的参数是频段、发射功率、接口类型和是否带屏蔽。频段必须选符合当地法规的免许可频段这个不能乱来选错了要么违法要么通信距离惨不忍睹。发射功率常见有 20dBm100mW和 30dBm1W两档功率越大距离越远但功耗和发热也越大。我这个项目节点是电池供电选了 20dBm 的模块实测在开阔地带两三百米没问题隔着两堵墙也能穿透对院子场景完全够用。接口类型上我强烈建议选SPI 接口的模块比如基于 SX1276/SX1278 芯片的那些。为什么不用串口透传模块因为串口模块虽然上手快但你失去了对底层参数的完全控制比如扩频因子、带宽、编码率这些调优的时候会很被动。SPI 模块配合现成的库参数全在代码里想怎么调怎么调。注意LoRa 模块一定要买带金属屏蔽罩的版本。我一开始图便宜买了个裸板的结果旁边电机一启动数据就乱码。换了带屏蔽的之后同样的环境稳如老狗。这个钱不能省。2.3 传感器选型清单与避坑传感器这块我踩的坑最多直接上表格对比都是我实际买过用过的。监测项型号接口实测表现避坑点空气温湿度SHT30I2C精度高长期稳定别买 DHT11户外湿度一高就飘土壤湿度电容式土壤湿度传感器ADC不腐蚀寿命长千万别买电阻式插土里两周就锈断光照强度BH1750I2C数字输出线性好注意安装朝向避免被遮挡电池电压电阻分压 ADCADC简单可靠分压电阻要选高精度否则电压读数偏差大这里重点说土壤湿度传感器。电阻式的便宜几块钱一个但它的原理是让电流通过土壤电极会电解腐蚀插在土里没几天就废了。电容式的贵一点但它不直接接触导电寿命长得多。这个钱必须花否则你每隔两周就要换传感器维护成本反而更高。电池电压监测用电阻分压是最简单的方案。ESP32-S3 的 ADC 输入范围是 0 到 3.3V而锂电池满电是 4.2V所以要用两个电阻分压。我选的是 100k 和 100k 等值分压这样满电 4.2V 分压后是 2.1V在安全范围内。选 100k 这么大的阻值是为了降低静态功耗分压电路一直在耗电阻值太小会白白浪费电池。3. 开发环境搭建与工程结构3.1 Arduino IDE 和 PlatformIO 到底用哪个热词里这两个都出现了我两个都深度用过说下真实感受。Arduino IDE 的优势是上手快、库管理简单、社区示例多。你搜“esp32-s3 ov5640驱动”或者“lora通信代码”大部分教程都是基于 Arduino IDE 的复制粘贴就能跑。但它的短板在项目变大之后非常明显没有真正的工程管理多个文件组织混乱库版本冲突难排查编译速度慢。PlatformIO 是 VS Code 的插件本质是一个专业的嵌入式开发平台。它的优势是工程结构清晰、库依赖用配置文件管理、编译快、支持多环境。我这个项目最终用 PlatformIO原因是它能把platformio.ini作为整个项目的配置中心板子型号、库依赖、编译参数全在一个文件里换电脑或者分享给别人克隆下来直接编译不会出现“在我电脑上能跑”的问题。如果你只是想快速验证一个传感器能不能用Arduino IDE 更顺手。但如果你要做的是一个会持续迭代、要长期维护的项目PlatformIO 是更专业的选择。我的建议是先用 Arduino IDE 跑通单个模块确认硬件没问题再迁移到 PlatformIO 做正式工程。这样学习曲线最平缓。3.2 PlatformIO 工程配置文件详解下面是我这个项目实际用的platformio.ini每一行都解释清楚。[env:esp32-s3-devkitc-1] platform espressif32 board esp32-s3-devkitc-1 framework arduino monitor_speed 115200 upload_speed 921600 lib_deps adafruit/Adafruit SHT31 Library^2.2.2 claws/BH1750^1.3.0 sandeepmistry/LoRa^0.8.0 build_flags -DCORE_DEBUG_LEVEL1 -DBOARD_HAS_PSRAMplatform指定平台是 espressif32board指定具体开发板型号这个型号要和你的实际板子对上否则引脚定义会错。framework选 arduino因为我要用 Arduino 的库生态。monitor_speed是串口监视器波特率115200 是常规选择。upload_speed我拉到 921600烧录快很多前提是你的 USB 线质量过关。lib_deps是库依赖这里列了三个SHT31 库用来读温湿度BH1750 库读光照LoRa 库驱动 LoRa 模块。版本号用^表示兼容该主版本的最新版这样既能拿到 bug 修复又不会因为大版本升级导致 API 不兼容。build_flags里CORE_DEBUG_LEVEL1是降低调试输出等级减少串口打印对实时性的影响。BOARD_HAS_PSRAM是告诉编译器这块板子有 PSRAM虽然这个项目暂时用不上但留着以后扩展。提示lib_deps里的库名和版本号建议去 PlatformIO 的库注册表里搜一下确认。有些库名字很像但作者不同用错了会编译报错。3.3 引脚分配与接线规划引脚分配是硬件项目最容易出错的地方我建议在动手接线之前先在纸上或者表格里把每个引脚的功能定下来避免接一半发现冲突。ESP32-S3 引脚连接对象功能备注GPIO8SHT30 SDAI2C 数据需上拉GPIO9SHT30 SCLI2C 时钟需上拉GPIO4土壤湿度传感器ADC 输入ADC1 通道GPIO5电池分压ADC 输入ADC1 通道GPIO6BH1750 SDAI2C 数据与 SHT30 共用 I2C 总线GPIO7BH1750 SCLI2C 时钟与 SHT30 共用 I2C 总线GPIO10LoRa NSSSPI 片选GPIO11LoRa MOSISPI 数据输出GPIO12LoRa MISOSPI 数据输入GPIO13LoRa SCKSPI 时钟GPIO14LoRa RST复位GPIO15LoRa DIO0中断用于接收中断I2C 总线可以挂多个设备SHT30 和 BH1750 地址不同所以可以共用 GPIO8/9 或者 GPIO6/7。我这里为了布线方便让它们共用一组 I2C 引脚实际接线时把两个传感器的 SDA 和 SCL 分别并联即可。注意 I2C 总线需要上拉电阻很多模块自带上拉如果没有就要外接 4.7k 电阻到 3.3V。ADC 引脚要特别注意ESP32-S3 的 ADC1 和 ADC2 有区别。ADC2 在 WiFi 开启时不可用虽然这个项目不用 WiFi但为了养成好习惯我全部用 ADC1 的通道。GPIO4 和 GPIO5 都属于 ADC1可以放心用。4. 核心代码实现与关键逻辑拆解4.1 传感器数据采集的完整实现先看温湿度采集。SHT30 是 I2C 接口用 Adafruit 的库很省事。#include Wire.h #include Adafruit_SHT31.h Adafruit_SHT31 sht30 Adafruit_SHT31(); void initSHT30() { if (!sht30.begin(0x44)) { Serial.println(SHT30 init failed); while (1) delay(10); } } float readTemperature() { return sht30.readTemperature(); } float readHumidity() { return sht30.readHumidity(); }0x44是 SHT30 的默认 I2C 地址如果模块上有地址选择跳线可能变成0x45这个要看你买的模块。begin返回 false 说明通信失败这时候死循环等待是合理的因为传感器挂了后面数据都没意义。土壤湿度采集稍微复杂一点因为电容式传感器输出的是模拟电压需要 ADC 读取后做映射。#define SOIL_PIN 4 int readSoilRaw() { int sum 0; for (int i 0; i 10; i) { sum analogRead(SOIL_PIN); delay(10); } return sum / 10; } float readSoilPercent() { int raw readSoilRaw(); // 校准值需要根据实际传感器实测调整 const int AIR_VALUE 3200; const int WATER_VALUE 1200; float percent (float)(AIR_VALUE - raw) / (AIR_VALUE - WATER_VALUE) * 100.0; return constrain(percent, 0.0, 100.0); }这里的关键是校准。AIR_VALUE是传感器暴露在空气中时的 ADC 读数WATER_VALUE是传感器插在水里时的读数。这两个值每个传感器都不一样必须自己实测。我的做法是先把传感器擦干放空气中读一次再插进一杯水里读一次把两个值填进去。constrain是防止计算出的百分比超出 0 到 100 的范围。注意土壤湿度传感器的读数受土壤类型、密实度、盐分影响很大绝对百分比没有意义有意义的是相对变化趋势。你只需要关注“今天比昨天干了多少”而不是“湿度是 45% 还是 50%”。电池电压采集用分压电路代码很简单但要注意 ADC 衰减设置。#define BATTERY_PIN 5 float readBatteryVoltage() { int raw analogRead(BATTERY_PIN); float voltage raw / 4095.0 * 3.3 * 2.0; // 2.0 是分压比 return voltage; }analogRead返回 0 到 4095对应 0 到 3.3V。因为用了等值分压实际电压是读数电压的两倍所以乘以 2.0。这里有个坑ESP32-S3 的 ADC 默认衰减可能不适合 3.3V 满量程需要在代码里设置analogSetAttenuation(ADC_11db)否则读数会偏低。这个细节很多教程不讲但实际影响很大。4.2 LoRa 通信代码与参数配置LoRa 通信是整个项目最核心也最容易出问题的部分。先看初始化和发送代码。#include SPI.h #include LoRa.h #define LORA_NSS 10 #define LORA_RST 14 #define LORA_DIO0 15 void initLoRa() { LoRa.setPins(LORA_NSS, LORA_RST, LORA_DIO0); if (!LoRa.begin(433E6)) { // 频段根据当地法规选择 Serial.println(LoRa init failed); while (1) delay(10); } LoRa.setSpreadingFactor(9); LoRa.setSignalBandwidth(125E3); LoRa.setCodingRate4(5); LoRa.setTxPower(20); } void sendData(String payload) { LoRa.beginPacket(); LoRa.print(payload); LoRa.endPacket(); }433E6是频段单位是赫兹。这个必须根据你所在地区允许的免许可频段来选选错了要么通信效果差要么不合规。setSpreadingFactor是扩频因子范围 6 到 12值越大通信距离越远但速率越慢。我选 9 是距离和速率的平衡点。setSignalBandwidth是带宽125kHz 是常用值带宽越小灵敏度越高但速率越低。setCodingRate4(5)是编码率5 表示 4/5 的纠错编码抗干扰能力较强。setTxPower(20)是发射功率 20dBm。这些参数发送端和接收端必须完全一致否则收到的就是乱码。我一开始调试时忘了统一折腾了半天才发现问题。数据打包我用的是简单的字符串拼接格式是字段1:值1,字段2:值2,...。为什么不用 JSON因为 LoRa 的带宽很宝贵JSON 的括号和引号会浪费不少字节。字符串拼接虽然土但省流量。String buildPayload(float temp, float humi, float soil, float light, float batt) { String payload T: String(temp, 1) ,H: String(humi, 1) ,S: String(soil, 0) ,L: String(light, 0) ,B: String(batt, 2); return payload; }String(temp, 1)里的 1 表示保留一位小数这样能进一步压缩数据长度。温度保留一位小数足够了土壤湿度和光照取整就行。4.3 低功耗策略与睡眠调度户外节点靠电池供电低功耗是必须认真对待的。ESP32-S3 支持多种睡眠模式我用的是深度睡眠Deep Sleep。核心思路是醒来、采集、发送、睡回去。大部分时间都在睡觉电流只有几十微安。#define uS_TO_S_FACTOR 1000000ULL #define TIME_TO_SLEEP 300 // 单位秒 void setup() { Serial.begin(115200); initSHT30(); initLoRa(); float temp readTemperature(); float humi readHumidity(); float soil readSoilPercent(); float light readLight(); float batt readBatteryVoltage(); String payload buildPayload(temp, humi, soil, light, batt); sendData(payload); Serial.println(Going to sleep); esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR); esp_deep_sleep_start(); } void loop() { // 深度睡眠模式下 loop 不会执行 }TIME_TO_SLEEP是睡眠时长我设的 300 秒也就是 5 分钟采集一次。这个频率对种植场景足够了土壤湿度不会 5 分钟就突变。如果你想更省电可以拉到 600 秒甚至更长。深度睡眠时ESP32-S3 的大部分外设都会断电但 LoRa 模块如果直接接在 GPIO 上可能会有漏电流。严谨的做法是用一个 MOS 管控制 LoRa 模块的电源睡眠时彻底断电。我一开始没做这个实测睡眠电流比理论值高不少后来加了电源控制才降下来。提示深度睡眠唤醒后程序是从setup()重新开始执行的不是从睡的地方继续。所以所有初始化都要放在setup()里不能假设变量还保留着。5. 常见问题排查与实战避坑记录5.1 LoRa 通信失败排查速查表LoRa 调不通是最常见的问题我把遇到过的现象和原因整理成表方便你对照排查。现象可能原因排查方法完全收不到数据频段不一致确认收发两端LoRa.begin()参数相同收到乱码扩频因子/带宽/编码率不一致逐项核对参数配置距离很近才能通天线没接好或功率设置过低检查天线连接确认setTxPower值偶尔丢包电源不稳或干扰示波器看电源纹波远离干扰源初始化失败SPI 引脚接错或模块损坏用万用表测通断换模块测试我遇到最坑的一次是天线问题。LoRa 模块的天线接口有 IPEX 和 SMA 两种我买的是 IPEX 的但手头只有 SMA 天线硬插上去接触不良通信距离只有几米。后来买了个 IPEX 转 SMA 的转接线才解决。天线是 LoRa 的命根子接口一定要匹配天线本身也要选对应频段的。5.2 传感器读数异常的处理经验SHT30 读数偶尔会返回 NaN这是 I2C 通信偶发失败导致的。我的处理方式是在读取函数里加一个重试机制。float readTemperatureSafe() { for (int i 0; i 3; i) { float t sht30.readTemperature(); if (!isnan(t)) return t; delay(50); } return -999; // 标记无效值 }返回 -999 作为无效标记接收端看到这个值就知道这次采集失败了可以选择丢弃或者用上次的值填充。土壤湿度传感器有个特性刚上电时读数不稳定需要预热几秒。我在setup()里初始化后加了个delay(2000)让传感器稳定下来再读。这个细节在传感器手册里不一定写但实测确实有影响。光照传感器 BH1750 要注意安装位置。我一开始把它和主控板放在同一个盒子里结果盒子本身的透光率影响了读数测出来永远偏低。后来把传感器用延长线引到盒子外面数据才正常。传感器要测什么就要让它直接接触被测环境不能隔着一层壳。5.3 供电与续航的实战数据最后说供电这是户外项目的生命线。我用的是 18650 锂电池标称 3000mAh配合一个 TP4056 充电模块和 DW01 保护板。实测数据5 分钟采集一次、每次发送约 100 字节、深度睡眠电流约 50uA、发送时峰值电流约 120mA 持续约 200ms。粗算一下发送占空比是 200ms / 300s约 0.067%平均发送电流约 0.08mA。加上睡眠电流 0.05mA传感器和 LoRa 待机漏电算 0.1mA总平均电流约 0.23mA。3000mAh 除以 0.23mA理论续航约 13000 小时也就是 540 天。当然这是理想值实际因为电池自放电、温度影响、LoRa 漏电等打对折比较现实大概能撑 8 到 10 个月。这个续航对户外监测来说已经相当可观了。注意锂电池在低温下容量会大幅下降冬天户外可能只有标称容量的一半。如果你的场景冬天很冷要么加大电池容量要么给电池仓做保温。6. 接收端与数据落地6.1 接收端硬件与代码接收端我用的是另一块 ESP32-S3 加同样的 LoRa 模块放在室内靠近路由器的地方。它的职责是接收数据、解析、然后通过 WiFi 上传到本地服务器或者直接打印到串口。void loop() { int packetSize LoRa.parsePacket(); if (packetSize) { String received ; while (LoRa.available()) { received (char)LoRa.read(); } Serial.println(Received: received); parseAndStore(received); } }parsePacket返回收到的字节数非零表示有数据。LoRa.available()和LoRa.read()配合把数据读出来。接收端不需要低功耗所以可以一直循环监听。解析函数就是把字符串按逗号和冒号拆开提取各个字段的值。这部分代码很直白就不展开了。6.2 数据存储与查看方案数据落地我试过两种方案。第一种是接收端直接通过串口打印用电脑上的串口助手记录到文件。这个方案最简单适合调试阶段。第二种是接收端连 WiFi通过 HTTP POST 把数据发到本地的一个小服务器存进数据库。这个方案适合长期运行方便做趋势分析。如果你不想折腾服务器还有个折中方案接收端加一个 SD 卡模块数据直接写 CSV 文件。CSV 的好处是通用Excel 直接打开就能画图。我现在的做法是串口和 SD 卡双写串口用于实时观察SD 卡用于长期存档。数据格式我统一用 CSV字段顺序是时间戳、温度、湿度、土壤湿度、光照、电池电压。时间戳用接收端的时间因为发送端深度睡眠后时间会丢每次醒来重新计时不现实。接收端一直供电可以用 NTP 同步时间这样时间戳是准的。7. 项目扩展方向与个人体会这个项目跑通之后能扩展的方向其实不少。比如加一个继电器控制水泵根据土壤湿度自动浇水就变成了一个闭环的灌溉系统。再比如加一个 LoRa 网关同时接收多个节点的数据就能覆盖更大的区域。热词里提到的“基于 esp32 与 lora 的环境监测系统”其实就是这个思路的规模化版本。我个人在实际操作中的体会是环境监测项目最难的不是写代码而是让硬件在真实环境里稳定运行。实验室里跑通和户外跑一周完全是两码事。湿度、温度、灰尘、虫子、供电波动每一个都可能让你的系统挂掉。所以我的建议是第一版不要追求功能多先把最核心的采集和通信做扎实在户外连续跑一周不出问题再考虑加功能。最后分享一个小技巧给你的节点做一个简单的状态指示比如一个 LED每次采集时闪一下。这样你路过的时候一眼就能看出它是不是还活着不用掏手机看数据。这个小小的反馈在调试和长期维护时能省很多事。