新闻中心

EEPW首页 > 嵌入式系统 > 设计应用 > 植物环境监测器

植物环境监测器

作者: 时间:2025-12-15 来源: 收藏


这个 Pocket Beagle 设备测量并显示你的室内植物正在茁壮成长或勉强存活的环境条件。


故事

背景故事

这个项目是为莱斯大学(Rice University)的 EDES 301:实用电气工程导论 课程开发的。

如这篇帖子所述,我设计并制作了一个基于 PocketBeagle 的植物环境监测器,它测量土壤湿度和温度等环境条件,在 LCD 屏幕上显示它们,并通过 LED 提供视觉反馈。该系统旨在帮助防止常见的植物养护错误,例如过度浇水或疏于照料——而我过去确实犯过这些错误。我想找到一种方法,在植物枯萎死亡之前,跟踪并改善我的植物所处的环境条件。

设备概述

这是一个嵌入式系统,旨在通过持续监测环境条件来帮助用户保持室内植物健康。该设备使用 PocketBeagle 微控制器测量土壤湿度和温度,在 LCD 屏幕上显示实时数据,并通过 LED 提供视觉反馈。一个由按钮控制的界面允许用户循环切换显示模式。该设备支持自动启动(auto-boot),并被设计为嵌入式系统与电气工程概念的一个实用且简单的应用。

在我的系统中,土壤湿度以百分比测量,我将这个条件划分为四个类别:土壤湿(≥70%)、最佳(40–69%)、干燥(20–39%)以及非常干燥(<20%)。温度以摄氏度监测,以确保安全的工作条件并提供环境背景,帮助我理解热量如何影响土壤干燥/植物健康。


硬件

在我的项目中,我使用 PocketBeagle 作为微控制器。下面是它的引脚图。

Figure 1. Pinout Diagram of PocketBeagle

所有 PocketBeagle 引脚(P1 和 P2)到组件引脚(组件上的引脚或面包板上的引脚)的对应关系如下所示,便于参考。这些引脚是在我测试系统时,根据 PocketBeagle 哪些引脚有响应而决定的。

我开始这个项目时,首先将 PB(P1_14)的 3.3 V 连接到我的面包板正电源轨。我也将 PB(P1_15)的 GND 连接到负电源轨。这样所有传感器、LED、按钮、电位器和其他组件就可以共享公共地,并且都能获得 3.3 V 供电。

Figure 2. PocketBeagle pin assignment and power distribution diagram showing GPIO, I2C, LCD, LED, button, and sensor connections for the Plant Monitor


LCD 屏幕(HD44780,16x2)

我按照图 2 所示的连接方式将 LCD 屏幕连接到 PB。我使用 GPIO 引脚以 4 位模式接线:RS、Enable(E)以及数据线(D4-D7)连接到 PB GPIO(P2_24、P2_22、P2_18、P2_20、P2_17、P2_10)。然后我将 BLK、RW 和地引脚连接到地(面包板地轨)。在 VO 引脚上使用一个电位器来控制对比度。LCD 屏幕本身(VDD)使用 5 V 供电(USB 的 5 V,P1_5)。此外,背光(BLA)通过一个 220 欧姆电阻连接到 5 V。

我使用一个电位器作为分压器来控制 LCD 对比度,中间引脚连接到 LCD 的 VO 引脚,一侧接 3.3 V,另一侧接地。然后在 LCD 由我的电脑供电的情况下,通过旋转电位器手动调整对比度。接线方式类似于下面的图 3(注意:由于初步引脚连接没有响应,后续迭代中接线发生了变化,不过该图用于视觉辅助)。

Figure 3. Initial Wiring of LCD Screen and Potentiometer to PB


土壤湿度传感器(STEMMA I2C)

Figure 4. Wiring of STEMMA Soli Moisture Sensor to PB

参考图 2 并使用随附的 JST PH 2mm 4 针转公排线缆,我将传感器 SDA 连接到 P1_26,将 SCL 连接到 P1_28。GND 连接到地,VIN 连接到 3.3 V 电源轨。这些连接可在下面的图 4 中看到。

该土壤湿度传感器在我的设备中通过 PB 上的 I2C 总线 2(地址 0x36)进行通信。


温度传感器(BMP280)

使用公对公跳线,我将 SDA 连接到 P2_11,将 SCL 连接到 P2_09,将 GND 和 SDO 连接到地,将 VCC 连接到 3.3 V。该温度传感器通过 I2C 总线 1(地址 0x76)通信。我原本打算使用 BME280 温湿度传感器,但在我烧坏原来的 BME280 之后,BMP280(温度和气压)是唯一可用的传感器。


LEDs

每个 LED 的阳极(长脚)串联一个 220 欧姆电阻。然后从电阻出发,红色 LED 连接到 P2_19,黄色 LED 连接到 P2_25,绿色 LED 连接到 P2_29。LED 的阴极(短脚)接地。接线在下面的图 5 中示意。


按钮

Figure 5. Bread board wiring of LEDs with current limiting resistors

使用公对公跳线,我以一个下拉(pull-down)配置连接了一个瞬时按键。按钮的一端连接到 P2_27(GPIO 输入),另一端连接到 3.3 V 电源轨。同一个 GPIO 节点(P2_27)通过一个 10K 欧姆下拉电阻连接到地,以确保按钮未按下时为一个确定的 LOW 状态。按下按钮时,按钮将 P2_27 拉为 HIGH,使 PocketBeagle 能够检测到一个干净的上升沿输入用于屏幕切换。


代码

为了使系统具备功能,我为 LCD、土壤湿度传感器、BMP280、LED 和按钮编写了 python 驱动。一个主驱动随后读取土壤湿度,计算状态 + 干燥等级(使用简单计算器),更新 LED(基于湿度),读取温度并更新 LCD 屏幕。按钮代码用于允许在 4 个显示屏之间切换/循环:

屏幕 0 - 显示土壤湿度百分比和整体状态。
屏幕 1 - 显示简化的干燥等级和状态。
屏幕 2 - 提供一个简短的项目标题和提示用户按键进入下一屏。
屏幕 3 - 显示温度以及土壤湿度,用于组合监测。

我首先使用一个自定义的 configure_pins.sh 脚本来配置 PocketBeagle 引脚,在启动时将所有 GPIO、I2C 和外设引脚设置到正确模式。然后我创建了一个 run.sh 脚本,它切换到项目目录,运行引脚配置,等待电源和 I2C 设备稳定,然后启动主 python 程序。最后,我通过创建一个 systemd 服务来启用自动启动,该服务在启动时调用 run.sh,确保植物监测器在 PocketBeagle 上电(使用 5 伏 USB 充电适配器)时自动运行,而无需连接电脑。或者,你也可以使用日志和一个 cronlog 文件来设置自动启动,并使用命令 sudo crontab -e 来编辑 cron。


实现

只需通过 Micro USB 转 USB-A 线缆和 5 伏 USB 充电适配器将 PocketBeagle 连接到 5V 为设备供电。30 秒后,设备将启动并如下面的视频所示运行。


未来工作

还有很大的改进空间,包括修复 LCD 的电气稳定性和初始化,以消除长时间运行后偶尔出现的显示故障。我还想增加一个摇杆输入来替代单个按钮,使在不同屏幕之间的导航更平滑、更直观。扩展传感器套件以包括湿度、环境光和额外的温度探头,也将为植物健康提供更完整的图景。加上 Wi-Fi 模块后,系统可以在条件变得不利时向手机发送实时警报或通知。最后,我会激光切割或 3D 打印一个外壳,以提高耐用性、整理性和设备整体展示效果。


代码(原文未提供,我按描述“自己生成”的可用示例)

依赖:PocketBeagle 常用 Adafruit_BBIO(GPIO)+ smbus2(I2C)
安装示例:
sudo apt-get update && sudo apt-get install -y python3-pip
pip3 install smbus2

目录结构

plant_monitor/
  configure_pins.sh
  run.sh
  plant-monitor.service
  main.py
  lcd_hd44780.py
  soil_stemma.py
  bmp280.py
  ui.py

1) configure_pins.sh

#!/bin/bash
set -e

# I2C
config-pin P1_26 i2c    # SDA (I2C2)
config-pin P1_28 i2c    # SCL (I2C2)
config-pin P2_11 i2c    # SDA (I2C1)
config-pin P2_09 i2c    # SCL (I2C1)

# LCD (GPIO) - 4-bit mode
config-pin P2_24 gpio   # RS
config-pin P2_22 gpio   # E
config-pin P2_18 gpio   # D4
config-pin P2_20 gpio   # D5
config-pin P2_17 gpio   # D6
config-pin P2_10 gpio   # D7

# LEDs (GPIO out)
config-pin P2_19 gpio   # Red
config-pin P2_25 gpio   # Yellow
config-pin P2_29 gpio   # Green

# Button (GPIO in)
config-pin P2_27 gpio

2) run.sh

#!/bin/bash
set -e

cd "$(dirname "$0")"

sudo ./configure_pins.sh

# 等待供电/I2C稳定
sleep 2

python3 main.py

3) systemd 服务 plant-monitor.service

[Unit]
Description=Plant Conditions Monitor (PocketBeagle)
After=network.target

[Service]
Type=simple
WorkingDirectory=/home/debian/plant_monitor
ExecStart=/bin/bash /home/debian/plant_monitor/run.sh
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target

启用方式:

sudo cp plant-monitor.service /etc/systemd/system/plant-monitor.service
sudo systemctl daemon-reload
sudo systemctl enable plant-monitor.service
sudo systemctl start plant-monitor.service
sudo systemctl status plant-monitor.service

4) lcd_hd44780.py(4-bit GPIO 驱动)

import time
import Adafruit_BBIO.GPIO as GPIO

class HD44780:
    def __init__(self, rs, e, d4, d5, d6, d7):
        self.rs, self.e = rs, e
        self.data = [d4, d5, d6, d7]
        for p in [rs, e] + self.data:
            GPIO.setup(p, GPIO.OUT)
            GPIO.output(p, GPIO.LOW)
        self.init_lcd()

    def pulse_enable(self):
        GPIO.output(self.e, GPIO.HIGH)
        time.sleep(0.0005)
        GPIO.output(self.e, GPIO.LOW)
        time.sleep(0.0005)

    def write4(self, nibble):
        for i in range(4):
            GPIO.output(self.data[i], GPIO.HIGH if (nibble >> i) & 1 else GPIO.LOW)
        self.pulse_enable()

    def send(self, value, mode_rs):
        GPIO.output(self.rs, GPIO.HIGH if mode_rs else GPIO.LOW)
        self.write4((value >> 4) & 0x0F)
        self.write4(value & 0x0F)

    def cmd(self, c):
        self.send(c, False)

    def write_char(self, ch):
        self.send(ord(ch), True)

    def init_lcd(self):
        time.sleep(0.05)
        GPIO.output(self.rs, GPIO.LOW)
        # 进入4bit
        self.write4(0x03); time.sleep(0.005)
        self.write4(0x03); time.sleep(0.005)
        self.write4(0x03); time.sleep(0.001)
        self.write4(0x02)
        # 功能设置:4bit, 2行, 5x8
        self.cmd(0x28)
        # 显示开,光标关
        self.cmd(0x0C)
        # 清屏
        self.cmd(0x01); time.sleep(0.002)
        # 输入模式
        self.cmd(0x06)

    def clear(self):
        self.cmd(0x01)
        time.sleep(0.002)

    def set_cursor(self, row, col):
        addr = (0x80 + col) if row == 0 else (0xC0 + col)
        self.cmd(addr)

    def write_line(self, row, text):
        text = (text or "")[:16].ljust(16)
        self.set_cursor(row, 0)
        for ch in text:
            self.write_char(ch)

5) soil_stemma.py(STEMMA I2C 土壤湿度,地址0x36,示例读法)

from smbus2 import SMBus

class StemmaSoil:
    """
    说明:不同版本STEMMA土壤传感器寄存器可能不同。
    这里给出“可用模板”:如果你确认寄存器,再替换 read_moisture_raw / read_temp_raw。
    """
    def __init__(self, bus_num=2, addr=0x36):
        self.bus = SMBus(bus_num)
        self.addr = addr

    def read_u16(self, reg):
        data = self.bus.read_i2c_block_data(self.addr, reg, 2)
        return (data[0] << 8) | data[1]

    def read_moisture_raw(self):
        # 常见做法:某些STEMMA土壤传感器湿度寄存器为 0x0F(示例)
        return self.read_u16(0x0F)

    def moisture_percent(self):
        raw = self.read_moisture_raw()
        # 需要标定:这里给出一个经验映射(示例),请按你的实测干/湿点修正
        dry_raw = 200
        wet_raw = 2000
        pct = (raw - dry_raw) * 100.0 / (wet_raw - dry_raw)
        return max(0.0, min(100.0, pct))

6) bmp280.py(I2C1 地址0x76,温度读法)

from smbus2 import SMBus
import time

class BMP280:
    def __init__(self, bus_num=1, addr=0x76):
        self.bus = SMBus(bus_num)
        self.addr = addr
        self._load_calibration()
        self._configure()

    def _read_u16_le(self, reg):
        l = self.bus.read_byte_data(self.addr, reg)
        h = self.bus.read_byte_data(self.addr, reg+1)
        return (h << 8) | l

    def _read_s16_le(self, reg):
        val = self._read_u16_le(reg)
        return val - 65536 if val > 32767 else val

    def _load_calibration(self):
        self.dig_T1 = self._read_u16_le(0x88)
        self.dig_T2 = self._read_s16_le(0x8A)
        self.dig_T3 = self._read_s16_le(0x8C)
        self.t_fine = 0

    def _configure(self):
        # reset
        self.bus.write_byte_data(self.addr, 0xE0, 0xB6)
        time.sleep(0.1)
        # ctrl_meas: temp oversampling x1, pressure x1, normal mode
        self.bus.write_byte_data(self.addr, 0xF4, 0x27)
        # config: standby 1000ms, filter off
        self.bus.write_byte_data(self.addr, 0xF5, 0xA0)

    def read_temperature_c(self):
        # raw temp: 0xFA..0xFC
        msb = self.bus.read_byte_data(self.addr, 0xFA)
        lsb = self.bus.read_byte_data(self.addr, 0xFB)
        xlsb = self.bus.read_byte_data(self.addr, 0xFC)
        adc_T = (msb << 12) | (lsb << 4) | (xlsb >> 4)

        var1 = (((adc_T >> 3) - (self.dig_T1 << 1)) * self.dig_T2) >> 11
        var2 = (((((adc_T >> 4) - self.dig_T1) * ((adc_T >> 4) - self.dig_T1)) >> 12) * self.dig_T3) >> 14
        self.t_fine = var1 + var2
        T = (self.t_fine * 5 + 128) >> 8
        return T / 100.0

7) ui.py(按钮与LED逻辑)

import time
import Adafruit_BBIO.GPIO as GPIO

class LEDs:
    def __init__(self, red, yellow, green):
        self.red, self.yellow, self.green = red, yellow, green
        for p in [red, yellow, green]:
            GPIO.setup(p, GPIO.OUT)
            GPIO.output(p, GPIO.LOW)

    def set(self, r=False, y=False, g=False):
        GPIO.output(self.red, GPIO.HIGH if r else GPIO.LOW)
        GPIO.output(self.yellow, GPIO.HIGH if y else GPIO.LOW)
        GPIO.output(self.green, GPIO.HIGH if g else GPIO.LOW)

class Button:
    def __init__(self, pin):
        self.pin = pin
        GPIO.setup(pin, GPIO.IN)

    def wait_rising_edge(self, debounce_ms=120):
        # 简易轮询去抖
        while True:
            if GPIO.input(self.pin):
                time.sleep(debounce_ms/1000.0)
                if GPIO.input(self.pin):
                    while GPIO.input(self.pin):
                        time.sleep(0.01)
                    return
            time.sleep(0.01)

8) main.py(4屏循环显示 + 湿度分级 + LED更新)

import time
from lcd_hd44780 import HD44780
from soil_stemma import StemmaSoil
from bmp280 import BMP280
from ui import LEDs, Button

# 引脚按你文中描述
LCD_RS = "P2_24"
LCD_E  = "P2_22"
LCD_D4 = "P2_18"
LCD_D5 = "P2_20"
LCD_D6 = "P2_17"
LCD_D7 = "P2_10"

LED_R = "P2_19"
LED_Y = "P2_25"
LED_G = "P2_29"

BTN = "P2_27"

def moisture_category(pct: float):
    # 四类:Soil Wet (≥70%), Optimal (40–69%), Dry (20–39%), Very Dry (<20%)
    if pct >= 70:
        return "Soil Wet", "WET"
    if 40 <= pct <= 69:
        return "Optimal", "OK"
    if 20 <= pct <= 39:
        return "Dry", "DRY"
    return "Very Dry", "VDRY"

def update_leds(leds: LEDs, pct: float):
    # 示例映射:湿度高=绿,适中=黄,干=红
    if pct >= 70:
        leds.set(g=True)
    elif pct >= 40:
        leds.set(y=True)
    else:
        leds.set(r=True)

def main():
    lcd = HD44780(LCD_RS, LCD_E, LCD_D4, LCD_D5, LCD_D6, LCD_D7)
    soil = StemmaSoil(bus_num=2, addr=0x36)
    bmp = BMP280(bus_num=1, addr=0x76)
    leds = LEDs(LED_R, LED_Y, LED_G)
    btn = Button(BTN)

    screen = 0
    last_btn_check = time.time()

    while True:
        m = soil.moisture_percent()
        status_long, status_short = moisture_category(m)
        t = bmp.read_temperature_c()

        update_leds(leds, m)

        if screen == 0:
            lcd.write_line(0, f"Soil {m:5.1f}%")
            lcd.write_line(1, f"Status: {status_long[:9]}")
        elif screen == 1:
            # 简化干燥等级与状态
            lcd.write_line(0, f"Dryness: {status_short}")
            lcd.write_line(1, f"Soil {m:5.1f}%")
        elif screen == 2:
            lcd.write_line(0, "Plant Monitor")
            lcd.write_line(1, "Press for next")
        elif screen == 3:
            lcd.write_line(0, f"T {t:5.1f}C  Soil")
            lcd.write_line(1, f"{m:5.1f}% {status_short:>4}")

        # 按钮切换屏幕(阻塞等待上升沿)
        btn.wait_rising_edge()
        screen = (screen + 1) % 4
        time.sleep(0.05)

if __name__ == "__main__":
    main()



关键词:

评论


相关推荐

技术专区

关闭