树莓派Python自动浇水系统:土壤湿度传感器+继电器闭环控制
1. 项目缘起与整体设计思路1.1 为什么我会盯上“自动浇水”这件事养植物这件事说难不难说简单也不简单。出差三天回来阳台那盆养了两年的琴叶榕叶子全耷拉下来加班一周忘了管窗台上的多肉直接化水。我试过定时器插座加小水泵的方案结果夏天蒸发快、冬天蒸发慢固定时间浇水要么涝要么旱。后来我干脆用树莓派加Python搭了一套“看土浇水”的系统核心逻辑很简单土壤湿度低于阈值就浇水高于阈值就停再配合温湿度传感器做辅助判断。这套方案适合谁如果你手头有一块吃灰的树莓派3B、4B、Zero W都行家里养了几盆需要规律浇水的植物又不想买那种几十块钱但只能定时、不能看土壤状态的成品浇水器那这套方案就是为你准备的。整套硬件成本大概一百出头软件全用Python写代码量不大但涉及传感器读取、继电器控制、定时任务、数据记录这几个嵌入式开发的典型环节拿来练手非常合适。1.2 方案选型为什么是“土壤湿度触发”而不是“定时浇水”先说结论定时浇水是开环控制土壤湿度触发是闭环控制。开环的问题在于它假设环境不变但实际环境一直在变。我做过一个粗略统计同一盆绿萝在梅雨季和干燥的暖气房里土壤水分蒸发速度能差三倍以上。定时器每天浇200毫升梅雨季必然烂根暖气房必然不够。闭环控制的核心是反馈。土壤湿度传感器实时告诉系统“现在土里还有多少水”系统据此决定要不要开泵。这就像你渴不渴自己知道而不是别人规定你每小时必须喝一杯水。那为什么不用更高级的方案比如称重法通过花盆重量判断含水量或者图像识别看叶子是否萎蔫称重法精度高但需要高精度称重传感器成本上去了而且换盆、植物长大都会导致基准重量变化标定麻烦。图像识别更复杂需要摄像头、训练数据而且叶子萎蔫时往往已经缺水一段时间了属于“事后补救”。土壤湿度传感器直接测根区水分响应快、成本低、标定简单是性价比最高的选择。1.3 系统架构从传感器到水泵的完整链路整个系统的数据流是这样的土壤湿度传感器 → 模数转换ADS1115或MCP3008→ 树莓派Python程序 → 判断逻辑 → GPIO输出 → 继电器模块 → 水泵 → 出水这里有个关键点树莓派的GPIO是数字接口只能读高电平或低电平而土壤湿度传感器输出的是模拟电压信号。所以必须加一块模数转换芯片。我选的是ADS111516位精度I2C接口接线简单比MCP3008的SPI接口少几根线。如果你用的是树莓派Pico它自带ADC可以省掉这块芯片但Pico没有完整的Linux系统跑定时任务和日志记录不如树莓派方便。继电器模块的作用是隔离。树莓派GPIO输出3.3V、电流只有十几毫安带不动水泵。继电器用3.3V信号控制触点端接水泵的独立电源通常是12V或220V这样强弱电分离保护树莓派不被水泵启动时的反向电动势打坏。我一开始图省事直接用GPIO驱动小水泵结果水泵启动瞬间树莓派直接重启后来加了继电器和续流二极管才稳定。2. 硬件清单与接线实操2.1 物料准备别买错型号下面是我实际用到的物料清单附上选型理由和避坑提示物料推荐型号数量备注树莓派3B/4B/Zero W1任意带40针GPIO的型号均可土壤湿度传感器电容式如SEN0193类1-4电阻式易腐蚀电容式寿命长模数转换模块ADS111514通道可接4个传感器继电器模块5V低电平触发1注意触发电平高/低电平要匹配微型水泵5V或12V隔膜泵112V扬程更高适合多盆电源12V 2A适配器1给水泵供电树莓派单独供电硅胶管内径6mm若干配合水泵出水口杜邦线母对母、公对母若干建议买彩色方便区分注意土壤湿度传感器一定要选电容式。电阻式传感器靠裸露电极测导电率通电后电极会电解腐蚀用不了几个月就报废而且腐蚀产物会污染土壤。电容式传感器表面有阻焊层不与土壤直接发生电化学反应寿命长得多。我第一套用的电阻式三个月后读数飘得没法看拆开一看电极已经绿了。2.2 接线图与关键引脚说明接线分三部分传感器到ADS1115、ADS1115到树莓派、继电器和水泵。传感器到ADS1115传感器VCC → ADS1115 VDD3.3V传感器GND → ADS1115 GND传感器AO → ADS1115 A0第一路ADS1115到树莓派VDD → 树莓派3.3V物理引脚1GND → 树莓派GND物理引脚6SCL → 树莓派GPIO3物理引脚5SDA → 树莓派GPIO2物理引脚3继电器到树莓派继电器VCC → 树莓派5V物理引脚2继电器GND → 树莓派GND物理引脚9继电器IN → 树莓派GPIO17物理引脚11水泵到继电器水泵正极 → 12V电源正极水泵负极 → 继电器常开端NO继电器公共端COM→ 12V电源负极提示继电器模块的触发电平一定要确认。市面上有高电平触发和低电平触发两种。我买的第一块是高电平触发代码里写GPIO.output(17, True)才开泵后来换了一块低电平触发的逻辑正好反过来排查了半天才发现是模块类型不同。买之前看商品页面的参数或者用万用表测一下IN脚悬空时继电器是否吸合。2.3 供电与防水两个容易翻车的点供电方面树莓派和水泵必须分开供电。树莓派用5V 2.5A以上的适配器水泵用12V 2A适配器。共地是必须的也就是两个电源的负极要连在一起否则继电器控制信号没有参考电平会乱动作。防水方面传感器探头可以插进土里但传感器电路板部分不能沾水。我一般用热缩管把电路板包起来只留探头部分。水泵放在水桶里出水口接硅胶管引到花盆。如果多盆植物共用一个水泵需要在出水端加分流阀但更简单的做法是每盆配一个小水泵用继电器分别控制。我目前是四盆植物配四个小水泵每个水泵单独一个继电器通道代码里循环检测四路传感器哪盆缺水就开对应的泵。3. Python程序核心逻辑与代码实现3.1 环境搭建I2C和依赖库树莓派系统默认关闭I2C需要手动开启。在终端输入sudo raspi-config进入Interface Options → I2C → Yes然后重启。重启后用以下命令检查I2C设备是否被识别sudo apt-get install -y i2c-tools i2cdetect -y 1如果看到地址0x48ADS1115默认地址说明接线正确。如果没看到检查SDA和SCL是否接反、电源是否接好。安装Python库pip3 install adafruit-circuitpython-ads1x15 RPi.GPIO如果你用的是较新的树莓派系统BookwormGPIO库可能需要换成rpi-lgpio或gpiozero。我实测下来RPi.GPIO在Bookworm上会有兼容性警告但功能正常。如果追求稳定可以用gpiozero重写继电器控制部分。3.2 读取土壤湿度的完整代码下面是我实际在用的读取脚本加了多次采样取中值滤波避免单次读数跳变import time import board import busio import adafruit_ads1x15.ads1115 as ADS from adafruit_ads1x15.analog_in import AnalogIn # 初始化I2C和ADS1115 i2c busio.I2C(board.SCL, board.SDA) ads ADS.ADS1115(i2c) ads.gain 1 # 增益设为1量程±4.096V # 四个通道对应四盆植物 channels [ AnalogIn(ads, ADS.P0), AnalogIn(ads, ADS.P1), AnalogIn(ads, ADS.P2), AnalogIn(ads, ADS.P3), ] def read_moisture(channel, samples11): 读取土壤湿度返回电压值伏特 values [] for _ in range(samples): values.append(channel.voltage) time.sleep(0.05) values.sort() return values[len(values) // 2] # 取中值 # 测试读取 for i, ch in enumerate(channels): v read_moisture(ch) print(f通道{i} 电压: {v:.3f} V)这里解释几个关键点。增益设为1是因为ADS1115在增益1时量程±4.096V而传感器输出范围通常在0-3.3V正好覆盖。如果增益设大了比如±0.512V传感器输出超过量程会饱和读数一直是最大值。采样11次取中值是为了滤掉偶然的尖峰干扰比如水泵启动瞬间的电磁干扰。中值比平均值更抗脉冲噪声计算量也小。3.3 标定把电压转换成“干”和“湿”传感器输出的电压本身没有意义需要标定。标定方法很简单把传感器插进完全干燥的土里记录电压记为V_dry。把传感器插进刚浇透的土里等几分钟稳定后记录电压记为V_wet。之后读到的电压V湿度百分比可以近似为moisture_percent (V_dry - V) / (V_dry - V_wet) * 100注意电容式传感器的特性越湿电压越低。我实测干燥土电压约2.6V浇透后约1.2V。所以公式里是V_dry - V别搞反了。注意标定不是一劳永逸的。不同土壤类型泥炭土、椰糠、园土的介电常数不同换土后要重新标定。另外温度也会影响读数冬天和夏天同一盆土的电压可能差0.1-0.2V。如果追求精度可以加一个DS18B20温度传感器做补偿但一般家庭养花没必要设阈值时留点余量就行。3.4 浇水控制逻辑与防抖动核心控制逻辑不复杂但有几个细节决定系统稳不稳定import RPi.GPIO as GPIO import time RELAY_PIN 17 GPIO.setmode(GPIO.BCM) GPIO.setup(RELAY_PIN, GPIO.OUT) GPIO.output(RELAY_PIN, GPIO.HIGH) # 初始关闭假设低电平触发 # 阈值设定 THRESHOLD_ON 30 # 湿度低于30%开始浇水 THRESHOLD_OFF 50 # 湿度高于50%停止浇水 MAX_WATER_TIME 30 # 单次最长浇水30秒防止传感器故障导致淹水 def water_plant(channel_index): 给指定植物浇水 start_time time.time() GPIO.output(RELAY_PIN, GPIO.LOW) # 开泵 while True: v read_moisture(channels[channel_index]) moisture (V_dry[channel_index] - v) / (V_dry[channel_index] - V_wet[channel_index]) * 100 if moisture THRESHOLD_OFF: break if time.time() - start_time MAX_WATER_TIME: print(f通道{channel_index} 超时保护触发) break time.sleep(1) GPIO.output(RELAY_PIN, GPIO.HIGH) # 关泵 time.sleep(2) # 等水渗下去再测这里有两个保护机制。超时保护是防止传感器脱落或损坏导致读数一直很低水泵不停抽水把花淹了。浇水后等待2秒是让水在土壤里扩散一下否则刚浇的水还没渗到传感器位置读数没变化会误判为没浇够继续浇。另外阈值不能设得太窄。比如低于30%开、高于35%关这样浇水几秒就停了但水还没渗开过一会儿湿度又掉下去系统频繁启停继电器咔咔响。我建议开和关之间至少差15-20个百分点给水留出扩散时间。4. 定时任务、数据记录与远程查看4.1 用cron做定时检测树莓派上最简单的定时方案是cron。我设置每30分钟检测一次crontab -e添加一行*/30 * * * * /usr/bin/python3 /home/pi/plant_water.py /home/pi/plant_log.txt 21这样每半小时跑一次脚本检测四盆植物的湿度需要浇水的就浇。日志追加到plant_log.txt方便回溯。提示cron的环境变量和登录shell不同脚本里用到的Python库路径可能找不到。保险做法是在脚本开头加上#!/usr/bin/env python3并且用绝对路径调用Python。如果还是报模块找不到在crontab里加一行PATH/usr/local/bin:/usr/bin:/bin。4.2 数据记录与趋势观察光浇水不够我还想知道土壤湿度随时间的变化趋势用来调整阈值。脚本每次运行会把读数追加到CSV文件import csv from datetime import datetime def log_data(moistures): with open(/home/pi/moisture_log.csv, a, newline) as f: writer csv.writer(f) row [datetime.now().strftime(%Y-%m-%d %H:%M:%S)] [f{m:.1f} for m in moistures] writer.writerow(row)跑了一周后把CSV拉到电脑上用Excel画个折线图就能看出每盆植物的“干湿循环”周期。比如我发现朝南那盆绿萝大概5天从50%降到30%而朝北那盆要8天。根据这个周期我把朝南的阈值调高了一点让它更早浇水因为南向蒸发快等降到30%可能已经有点晚了。4.3 远程查看用Flask做个极简页面有时候出差在外想看看植物状态我用Flask写了个单页应用树莓派上跑一个轻量HTTP服务手机浏览器打开就能看from flask import Flask, jsonify app Flask(__name__) app.route(/status) def status(): data {} for i, ch in enumerate(channels): v read_moisture(ch) m (V_dry[i] - v) / (V_dry[i] - V_wet[i]) * 100 data[fplant_{i}] round(m, 1) return jsonify(data) if __name__ __main__: app.run(host0.0.0.0, port5000)这个页面只返回JSON数据不涉及任何外部服务纯局域网访问。如果你想让外网也能看那是另一个话题了涉及网络配置这里不展开。局域网内手机连同一个WiFi就能访问http://树莓派IP:5000/status够用了。5. 常见问题与排查技巧实录5.1 读数飘忽不定怎么办这是最常见的问题。排查顺序如下现象可能原因解决方法读数跳变超过0.5V电源干扰传感器和树莓派用同一电源或加滤波电容读数缓慢漂移土壤温度变化正常现象阈值留余量读数一直最大值传感器未插好检查杜邦线是否松动读数一直最小值传感器短路检查探头是否进水短路某个通道异常ADS1115通道坏换一个通道测试我遇到过一次读数疯狂跳变换了传感器、换了ADS1115都没用最后发现是水泵电源线和传感器线捆在一起水泵启动时的电磁干扰串进来了。把线分开走问题消失。强电和弱电信号线一定要分开走线这是硬件调试的基本功。5.2 水泵不启动或一直转水泵不启动先看继电器指示灯。如果继电器灯亮但泵不转说明继电器触点没闭合可能是触发电平搞反了。如果继电器灯不亮用万用表测GPIO输出看代码里控制的引脚号对不对。水泵一直转最常见的原因是传感器读数一直低于阈值。拔出来放在空气中看读数是否上升。如果空气中读数还是低传感器坏了。另一个原因是程序卡在while循环里超时保护没生效。检查MAX_WATER_TIME变量是否被正确比较time.time()的单位是秒别和毫秒搞混。5.3 土壤发霉与水泵空转土壤长期过湿会发霉这是浇水过量导致的。除了调低阈值还可以在代码里加一个最小浇水间隔比如两次浇水至少间隔6小时防止传感器误触发导致频繁浇水。水泵空转是指水桶没水了泵还在转。这个靠软件很难检测最简单的办法是在水桶里加一个浮球开关水位低于阈值时断开继电器电源。浮球开关就是一个干簧管水位下降时磁铁靠近触点闭合或断开接在继电器电源线上即可。我加了一个之后再也没烧过泵。5.4 系统稳定性经验汇总最后分享几条我踩坑后总结的经验树莓派要加散热片。长时间跑Flask和cronCPU温度能到70度以上夏天会降频。加个铝散热片温度能压到55度左右。SD卡容易坏。日志频繁写入会加速SD卡磨损。我把日志写到外接U盘或者用log2ram把日志先写到内存定期同步到SD卡。代码要加异常捕获。传感器偶尔读失败会抛异常整个脚本崩掉。用try...except包住读取逻辑失败时跳过这次检测记录错误日志下次再试。继电器选固态继电器更好。机械继电器有触点寿命频繁开关会坏。固态继电器无触点寿命长但价格贵一点。如果浇水频率不高机械继电器也够用。这套系统我跑了大半年四盆植物都活得挺好。最冷的那几天暖气房湿度掉得快系统自动多浇了几次梅雨季几乎没怎么浇过。代码不长硬件也不复杂但把传感器、执行器、控制逻辑、数据记录串起来是一个很完整的嵌入式小项目。如果你手头有树莓派不妨试试比买成品浇水器有意思得多。