概述
很多人喜欢养花 ,但是总是养不好,经常能听到这样的对白:哎···最近又把花养死了,真是罪过,罪过······。每当听到这样的话你有没有想过 问题出在哪里呢?究其原因无非就是无法确切知道土壤湿度,给花草浇适量的水,要么浇太多了,要么浇太少了导致花草死掉。本文将介绍如何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
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
低三位与各分频数对应关系可以看如下图表设置:
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.
完整的代码点击这里下载
DownLoad Url osoyoo.com