概述

很多人喜欢养花 ,但是总是养不好,经常能听到这样的对白:哎···最近又把花养死了,真是罪过,罪过······。每当听到这样的话你有没有想过 问题出在哪里呢?究其原因无非就是无法确切知道土壤湿度,给花草浇适量的水,要么浇太多了,要么浇太少了导致花草死掉。本文将介绍如何DIY一套智能花园浇水系统,能实时检测土壤湿度,自动给花草浇水,帮助你养出更多别致的花草。

工作原理

给系统上电后,首先会进行系统初始化,初始化完成Arduino会实时读取按键标志位。如果按键标志位是0,读取当前土壤湿度值,当土壤湿度低于土壤湿度临界值,打开电池阀开始浇水,直到土壤湿度高于土壤湿度临界值再关闭电磁阀停止浇水;如果按键标志位是1,进入修改土壤临界值提示页面;如果按键标志位是2,读取编码器脉冲数;如果按键标志位是3,确认修改土壤临界值。

硬件

智能花园浇水系统主要包括五大部分,分别是控制部分、检测部分、显示部分、调节部分、浇水部分。下面将对每一部分详细介绍。

1 控制部分

智能花园浇水系统我们用Arduino UNO为控制核心,在整个系统中扮演着最重要的角色,它就像一个人的大脑,起着支配身体其他部分的作用。

2 检测部分

我们用YL-69土壤湿度传感器作为检测部分,用于检测土壤湿度,该模块规格如下

输入电压 3.3V-5V
输出电压 0-4.2V
输入电流 35mA
输出信号 模拟&数字

该模块有4个引脚,描述如下

VCC 输入电源
GND
DO 数字输出
AO 模拟输出

本文中我们只使用土壤湿度传感器模块的模拟输出,因为数字输出引脚只能输出高电平或低电平,而我们需要获得一个连续的输出。将土壤湿度传感器模块的模拟输出引脚接到Arduino的Analog引脚,Arduino将读取到在0-1023之间连续变化的值,我们叫这个0-1023的值换算成0-100%的百分数,表示土壤湿度。

3 显示部分

我们用I2C接口的1602液晶作为显示器,显示土壤实时湿度以及其他信息。I2C接口1602LCD只需要2个IO口 就能驱动,大大 节省了IO资源。

4 调节部分

花草品种不一样,可能对水分的需求是不一样的,因此需要有装置调节浇水装置浇水的临界值,在本项目中我们利用旋转编码器来实现这一功能。

旋转编码器上有一个旋转按钮,通过旋转可以计数正方向和反方向转动过程中输出脉冲的次数,在本项目中当电位器正方向旋转增加计数,反方向旋转减少计数,按下编码器上的按键确认当前的数值为土壤湿度临界值。

5 浇水部分

浇水部分由继电器、电磁阀和水管组成。

连接图

1 )ardunio 与1602

lcd-Connection

 

2)ardunio 与土壤湿度模块及继电器

3 )ardunio 与旋转编码器

 

软件

/*
  LiquidCrystal_I2Clibrary download address:https://osoyoo.com/driver/LiquidCrystal_I2C.zip
  注意:LiquidCrystal_I2C要使用上面下载的LiquidCrystal_I2C版本,其他版本的可能会出现如下bug:
  如果要在屏上打印String字符串,用print("String")方法只能显示String首字母S
*/
#include <LiquidCrystal_I2C.h>

首先需要添加程序需要用到的库文件,要驱动IIC接口的1602液晶,需要安装LiquidCrystal_I2C库,如果LiquidCrystal_I2C库版本比较老,可能在调用print方法时候只能打印单个字符无法打印字符串的bug,需要安装代码链接里面提供的库文件

#define YL69 A0          //define as YL69 the Pin A0 used to connect the Sensor
#define RELAY 6         //define as RELAY the Pin 4 used to connect the Sensor
#define CLK_PIN 2 // Encoder's clk pin connected to D2
#define DT_PIN 4   //Encoder's dt pin connected to D4
#define SW_PIN 3   //Encoder's sw pin connected to D3

定义各器件用到的IO口,这里将YL69(土壤湿度传感器)与Arduino 模拟口A0接一起;一路继电器接到数字口D6上;旋转编码器有总共有5个引脚,除去VCC和GND外,剩下CLK、DT和SW,分别将它们接到Arduino UNO的D2、D4和D3口上。其中D2和D3分别是Arduino的外部中断引脚,在程序中我们需要使用Arduino的D2和D3检测编码器旋转中断和编码器按键中断,关于Arduino外部中断请点击这里了解更多关于外部中断的信息。

volatile long count = 0;//编码器旋转时作为计数器
unsigned long t = 0;
volatile int inter_num=0;//按按键次数
int soilhum=60;//土壤湿度临界值
/*The (int soil=0) is the soil variable which turns the reading that comes 
 in on the sensor into a %, this will be an important variable in this project*/
int soil=0;//实时土壤湿度

count是旋转编码器旋转计数器,正转count递增,反转count递减;t记录程序运行时间;inter_num是按键标志位,记录按键被按了几次;soilhum是土壤湿度临界值,缺省值是60.

/*run ic2_scanner sketch and get the IC2 address, which is 0x27 in my case,it could be 0x27 in many cases
 run ic2_scanner sketch:https://osoyoo.com/wp-content/uploads/samplecode/ic2_scanner.txt 
 */
LiquidCrystal_I2C lcd(0x27, 16, 2); 

IIC接口1602屏在使用之前要先获取IIC地址,烧录上面链接中的代码到Arduino中,打开Serial Monitor就能获取到IIC地址了(可能是0x3f),将获取到的地址替换掉这里的0x27

//定时器初始化函数
void time1_init(void){
  cli();//关全局中断
  TCCR1A=0; //寄存器A是配置PWM的,这里我们只是使用定时功能,故把TCCR1A寄存器置零
  TCCR1B=(1<<CS12)| (0<<CS11)|(1<<CS10);//寄存器B是配置定时功能的,现在配置的是1024分频
  TCNT1=0xC2F6; //计数器初值,1s定时
  sei(); //开全局中断 
}

这是定时器初始化函数,咋一看这个函数好陌生,一点都不像Arduino的编程风格,函数里面的TCCR1A、TCCR1B好像从来没见过。下面详细讲讲这段代码。

当土壤实时湿度低于临界值时候开始浇水,为了防止浇水时间过长,需要开启定时器给浇水时间定时,时间到了就停止浇水。Arduino  UNO一共有3个定时器,分别是timer0、timer1、timer2,其中timer1是16位定时器,timer0和timer2是8位定时器,我们使用timer1,关于Arduino定时器更多信息请点击这里或者查看ATMega328p数据手册 。要看懂这段程序需要结合ATMega328P数据手册,代码中的调用cli()函数关闭全局中断;TCCR1A是timer1的16位控制寄存器的高8位,这个寄存器是控制PWM的,在数据手册的170页到172页有对这个寄存器的详细介绍,这里只介绍主要部分。TCCR1A是一个8位的寄存器,每一位可以置0或置1,其中灰色的2 3位是保留位(不可编程)

关于各位的详细描述如下

因为我们并不需要使用PWM功能,所以TCCR1A=0

接下来TCCR1B也是一个8位寄存器,是16位控制寄存器TCCR1的低8位,这个寄存器用于配置timer1的分频数,我们比较关心的是低3位,低三位正好是设置定时器输入时钟的分频数的。

低三位与各分频数对应关系可以看如下图表设置:

有上表知1024分频时候TCCR1B寄存器低三位分别是二进制101,所以上面代码中TCCR1B=(1<<CS12)| (0<<CS11)|(1<<CS10)将timer1设置成了1024分频,也就是说定时器的输入时钟频率是系统时钟的1/1024。因为Arduino UNO的系统时钟是16MHz,所以定时器的输入时钟频率=16MHz/1024 。在数据手册的173-174页有对TCCR1B寄存器详细介绍。
接下来介绍TCNT1。TCNT1是timer1的16位计数器寄存器,TCNT1被分为2个8位寄存器, TCNT1LTCNT1H,其中 TCNT1L代表低8位, TCNT1H代表高8位。TCNT1是设置定时器的初始值,当定时器启动后就会TCNT1里的初值开始计数,当记到0xFFFF(也就是TCNT1寄存器16位全为1的时候),TCNT1清零,从0开始计数。
TCNT1的值,根据文档是这样计算的: TCNT1=0xFFFF – 定时时间/(分频数* (1/晶振频率))。分频数是调整计数的,越大计数越慢,一般有18642561024分频;晶振频率16MHz,假设定时时间是1s,根据公式TCNT1=0xFFFF – 1/ (1024*(1/16)) = 0xC2F6
void setup() {
  time1_init();
  // 下降沿代表旋转编码器被转动
  attachInterrupt(digitalPinToInterrupt(CLK_PIN), rotaryEncoderChanged, FALLING);
  //按键按下表示进入设置模式
  attachInterrupt(digitalPinToInterrupt(SW_PIN), setmode, FALLING);
  pinMode(CLK_PIN, INPUT_PULLUP);// initialize the digital pin as Pull-up input. 
  pinMode(DT_PIN, INPUT_PULLUP); // initialize the digital pin as Pull-up input. 
  pinMode(SW_PIN, INPUT_PULLUP); // initialize the digital pin as Pull-up input. 

  pinMode(RELAY,OUTPUT);// initialize the digital pin as an output. 
  digitalWrite(RELAY,LOW);//set digital pin output low
  Serial.begin(9600);// initialize serial communication at 9600 bits per second
  lcd.begin();// initialize the LCD
  lcd.backlight();//enable LCD backlight
  
}

attachInterrupt(digitalPinToInterrupt(CLK_PIN), rotaryEncoderChanged, FALLING)函数将编码器CLK引脚设定外部中断0,在下降沿触发中断,当中断触发后转到中断服务函数rotaryEncoderChanged中执行;attachInterrupt(digitalPinToInterrupt(SW_PIN), setmode, FALLING)函数将编码器SW引脚设定外部中断1,在下降沿触发中断,当中断触发后转到中断服务函数setmode中执行。下面的将各个引脚设定成了相应的工作模式,设定串口波特率,初始化液晶。

void relay_on()//turn the Relay on 
{
    digitalWrite(RELAY,HIGH);//HIGH is the voltage level
}
void relay_off()//turn the Relay off 
{
    digitalWrite(RELAY,LOW);//LOW is the voltage level
}

这两个函数分别是打开继电器和关闭继电器,继电器是高电平闭合,低电平断开。

int get_YL69()//Get YL-69 sensor data
{
  int YL_Value = analogRead(YL69);// read the input on analog pin A0:
  YL_Value=constrain(YL_Value, 485, 1023);//Constrains YL_Value variable to be within a range. 
  soil = map(YL_Value, 485, 1023, 100, 0);//map the value to a percentage
  Serial.print("Soil Humidity:");
  // print out the soil water percentage you calculated:
  Serial.print(soil);// print out the value you read:
  Serial.println("%");
  return soil;// return the soil water percentage you calculated:
}


get_YL69中通过analogRead读取A0口电压值,通过constrain将读取到的电压值限定在一定的范围内,最后用map函数将电压值换算成0-100%的百分数,
同时返回这个版分数。
void loop() {
  detachInterrupt(digitalPinToInterrupt(CLK_PIN));//disable interrupt 0
  if (inter_num==0){
    homepage();
     if (soil < soilhum) {
         TIMSK1=(1<<TOIE1); //溢出中断使能
      }
     else relay_off();
  }    
   //进入设置模式
  else if(inter_num==1){
    page_1();
    relay_off();
    TIMSK1=(0<<TOIE1);
  }    
   else if(inter_num==2){
       attachInterrupt(digitalPinToInterrupt(CLK_PIN), rotaryEncoderChanged, FALLING);//enable interrupt 0
       page_2();
       relay_off();
       TIMSK1=(0<<TOIE1);
   }
   else if(inter_num==3){
       detachInterrupt(digitalPinToInterrupt(CLK_PIN));//disable interrupt 0
       soilhum=count;
       Serial.print("soilhum=");
       Serial.println(soilhum); 
       relay_off();
       TIMSK1=(0<<TOIE1);
       delay(1000);
   }
   if(inter_num>3) inter_num=0;
}

在主循环中首先是关闭了外部中断0,是编码器旋转不计数。判断按键标志位,根据按键标志位执行不同的子函数。当按键标志位0,进入homepage()中,
同时判断实际土壤湿度和临界值大小关系,若小于临界值开启定时器,进入定时器中断服务函数,在定时器中断服务函数中打开继电器;否则关闭继电器停止浇水。如果按键标志位是1,进入page_1()中,这是提示页面,只显示提示信息,同时关闭继电器、关闭定时器中断。如果按键标志位是2,打开外部中断0,对编码器旋转计数,在page_2()先液晶上打印编码器计数值,同时关闭定时器和继电器。如果按键标志位为3,关闭外部中断0,将土壤湿度临界值修改为当前计数值。如按键标志位大于3,则将按键标志位清0.

void homepage(void){
  int soil=get_YL69();
   lcd.clear();
   lcd.setCursor(0, 0);//set the cursor to column 0, line 0
   lcd.print("soil:");
  /*显示单位*/
   lcd.setCursor(8, 0);//set the cursor to column 8, line 0 
   lcd.print("%");
  /*显示数据*/
   lcd.setCursor(5, 0);//set the cursor to column 5, line 0 
   lcd.print(soil);
   delay(1000);
}

当按键标志位为0的时候会执行这个函数。在这个函数中调用get_YL69()获取到土壤湿度,并将湿度值打印在液晶上。

void page_1(void){
    lcd.clear();
    lcd.setCursor(0, 0);//set the cursor to column 0, line 0 
    lcd.print("You will change ");
    lcd.setCursor(0, 1);//set the cursor to column 0, line 1 
    lcd.print("soil humidity");
    delay(1000);
}

void page_2(void){
  lcd.clear();
  lcd.setCursor(0, 0);//set the cursor to column 0, line 0 
  lcd.print("soilhum:");
  lcd.setCursor(0, 1);//set the cursor to column 0, line 1 
  lcd.print(count);
  lcd.setCursor(3, 1);//set the cursor to column 3, line 1 
  lcd.print("%");
  //soilhum=count;
  Serial.print("soilhum=");
  Serial.println(count);
  delay(1000);
}

这两个函数都是先液晶和串口打印信息。

void rotaryEncoderChanged(){ // when CLK_PIN is FALLING
  unsigned long temp = millis();
  if(temp - t < 200) // 去抖
    return;
  t = temp;
  // DT_PIN的状态代表正转或反转
  count += digitalRead(DT_PIN) == HIGH ? 1 : -1;
}

上面是外部中断0服务函数,也就是编码器旋转时候触发的中断。如果编码器正转,count加1,反转就减1

void setmode(){
   inter_num++;
}

setmode是外部中断1中断服务函数,当编码器按键按下inter_num会加1

ISR(TIMER1_OVF_vect){
  TCNT1=0xC2F6;//重装初值
  relay_on();//打开继电器
  TIMSK1=(0<<TOIE1); //溢出中断失能
}

这个是定时器1中断服务函数。首先对TCNT1寄存器重装值,然后打开继电器,关闭定时器1.

完整的代码点击这里下载