电子工程师技术服务社区
公告
登录
|
注册
首页
技术问答
厂商活动
正点原子
板卡试用
资源库
下载
文章
社区首页
文章
用Arduino改造你的鱼缸
分 享
扫描二维码分享
用Arduino改造你的鱼缸
arduino
鱼缸
xukejing
关注
发布时间: 2020-03-06
丨
阅读: 4290
## 1、项目背景 今天给大家带来的这个作品是个传说中的“懒人鱼缸”。因为使用了生化过滤(水产专用EM菌,起作用的主要是芽孢杆菌和硝化细菌),可以很久不换水。只是偶尔换掉一部分水,以防止硝酸盐积累过多。 下图就是鱼缸底部的过滤桶。它的主要任务不是过滤“便便”,但对维持水质起到了很重要的作用。 ![](https://cf05.ickimg.com/bbsimages/202003/b422c5f195228c04fcc11241990e0245.jpg) 我把滤筒在鱼缸里的入水口设置得相对较高,这样它就不会直接吸到“便便”了,一年清理一次便可。 滤筒里面放了陶瓷环和生化棉,用来培养细菌,可以起到一定的物理过滤作用,吸附并分解水中的细小微粒,让水看上去清澈;更重要的作用把水里的毒性较高的氨转化为毒性较低的硝酸盐。 “便便”主要靠潜水泵和上滤槽来过滤。下图展示了上滤槽的结构,上滤槽进水口套了一个袜子,拦截吸上来的“便便”。“便便”会存在袜子里,然后被腐生细菌吃掉,产生氨再溶解到水里。 ![](https://cf05.ickimg.com/bbsimages/202003/3f98cf4a11078a0d8b0a4e3d1166f3c2.jpg) 上面那个图里的水有些混浊。这个混浊是因为刚给鱼缸做了大扫除后新倒入了细菌培养液,不小心倒多了,但并无大碍,让滤筒工作一天后水就转清了 ![](https://cf05.ickimg.com/bbsimages/202003/213f0b9b76a59c7cb33776ed1fb562ad.jpg) 滤筒是要24小时保持运行的,保证桶里的好氧细菌存活。考虑到滤筒水流几乎没有声音,24小时运行也无伤大雅。 然而,上滤槽潜水泵的水流较大,会砸出较大水花,晚上睡觉时可能会觉得有些吵。况且上滤槽只是用来把鱼缸里的“便便”吸起来存进袜子,并不需要24小时不间断工作。只要让上滤槽潜水泵从早上8点工作到20点,其他时间关闭,平时鱼缸里就看不到“便便”了,这样还不会影响睡眠。这里有了第一个需求,水泵定时启动。 鱼缸为了保持温暖,会用到加热棒,但是普通加热棒使用机械温差开关控温,精度低且温度回差大。于是,这里有了第二个需求,更准确的温度控制,并减小温度回差。 于是,就有了下面这个作品。用Arduino做的鱼缸自动控制盒。 ![](https://cf05.ickimg.com/bbsimages/202003/dbb4fdd24362b933251e90867113a846.jpg) ## 2、功能简介 1、上滤槽潜水泵定时控制。每天晚上睡觉时间保持上滤槽潜水泵关闭,减少噪音。 2、温度控制。温度低于目标温度0.2度,加热棒启动。温度超过目标温度0.1度,加热棒关闭。目标温度是个变化的量。我设计了一个中午热晚上冷的变化曲线,模拟东南亚热带季风气候。 3、锦上添花的臭氧消毒功能。每天只工作数秒,减少水里的腥味,抑制厌氧细菌生长。臭氧不能工作太长时间,水中的臭氧浓度太高会把过滤桶里的有益细菌也杀死太多,这样反而影响生化过滤。 ## 3、制作方法 制作鱼缸自动控制盒需要模块包括: 1、[Arduino Nano](https://buy.icxbk.com/index.php?ctl=Product&met=detail&item_id=4565 "Arduino Nano"),主控板 2、DS3231,RTC时钟模块 3、[DS18B20](https://buy.icxbk.com/index.php?ctl=Product&met=detail&item_id=3077 "DS18B20"),温度传感器 4、[多路继电器模块](https://buy.icxbk.com/index.php?ctl=Product&met=lists&key_type=1&keywords=%E7%BB%A7%E7%94%B5%E5%99%A8%E6%A8%A1%E5%9D%97 "多路继电器模块") 5、[蓝牙串口模块](https://buy.icxbk.com/index.php?ctl=Product&met=lists&key_type=1&keywords=%E8%93%9D%E7%89%99%E6%A8%A1%E5%9D%97 "蓝牙串口模块") 6、[5V AC-DC电源](https://buy.icxbk.com/index.php?ctl=Product&met=detail&item_id=4679 "5V AC-DC电源") 这些模块与Arduino的连接线路如下图所示。其中各模块的电源VCC和GND都接到5V AC-DC电源的输出。 ![](https://cf05.ickimg.com/bbsimages/202003/cd326fac5ccbe2618f979e0183b419cf.jpg) 程序代码已经开源到本人的github,地址: [https://github.com/xukejing/ArduAquarium/](https://github.com/xukejing/ArduAquarium/ "https://github.com/xukejing/ArduAquarium/") 代码下载后用Arduino IDE打开并烧写到板子上便可。 控制盒的程序经过多次迭代。项目主目录里的是2017年的代码版本,也是最后一次修改后的稳定版本,后来发现代码已经足够完美了,后面几年就没再修改。较老的2015和2016年设计的代码被放入独立的文件夹,它们使用上也同样稳定,只是代码风格没最新版那么好看。另外,pic目录里放入了一些项目照片,透露了一些硬件设备的细节,比如过滤桶用的型号是创新CF800。 ![](https://cf05.ickimg.com/bbsimages/202003/79366a1b1ad8cd9f31ec4a6d78cf1ad7.jpg) ## 4、程序代码 这是个开源项目,程序代码全部给大家,欢迎大家在此基础上修改并增加自己需要的功能。 为了方便大家对代码做修改,我将对代码做一下介绍。 ### 4.1、继电器引脚配置 继电器引脚配置见 init.ino文件。引脚5、6、7分别为控制加热棒、上滤槽潜水泵、臭氧发生器的继电器的控制引脚。继电器是低电平导通。 ![](https://cf05.ickimg.com/bbsimages/202003/1f3aa10dccba239266fbe536e022a06c.jpg) ### 4.2、继电器控制函数 继电器控制方法见control.ino文件。其中的init_control()是初始化函数,该函数里有eepromgetT()和eepromgetB()两个函数,这两个函数是从eeprom读出温度和水泵开关时间的设置值。 ```c void init_control() { pinMode(pin_j1, OUTPUT); pinMode(pin_j2, OUTPUT); pinMode(pin_j3, OUTPUT); digitalWrite(pin_j1, HIGH); digitalWrite(pin_j2, HIGH); digitalWrite(pin_j3, HIGH); eepromgetT(); eepromgetB(); } ``` 一天里面的温度是有个变化过程的,从最低温度升到最高温度,然后再降回去。目标温度通过realtemp()函数控制。温度回差控制的控制逻辑见tempur_control()函数 ```c void realtemp() { set_Temp = -cos(((myclock.hour * 60 + myclock.minute) - 120) / 229.2994)*(setTempMax - setTempMin) / 2 + (setTempMax + setTempMin) / 2; } void tempur_control() { realtemp(); if (temp < set_Temp - 0.2) digitalWrite(pin_j1, LOW); else if (temp > set_Temp + 0.1) digitalWrite(pin_j1, HIGH); } ``` 水泵可以手动开关或自动定时控制(变量autoBUMP控制手动还是自动)。自动时,一天最多256次开关控制。只控制上滤槽水泵。底部过滤桶的水泵不关。bump_control()函数如下: ```c void bump_control() { byte ii = 0; if (autoBUMP) { for (bump_i = 0; bump_i < bump_counts; bump_i++) { if (time <= bump_T_max[bump_i] && time >= bump_T_min[bump_i]) ii=ii+1; } if(ii>0) digitalWrite(pin_j2, LOW); else digitalWrite(pin_j2, HIGH); } else { if(BUMP) digitalWrite(pin_j2, LOW); else digitalWrite(pin_j2, HIGH); } } ``` 臭氧控制是自动定时的,见o3_control()函数里的else部分,在代码里固定了启停时间。虽然不能通过蓝牙实时修改启停时间,但留了个o3_on变量接口,比通过蓝牙串口的函数接口给o3_on改个值10,就可以临时把臭氧打开一小会儿。 ```c void o3_control() { if(o3_on>0) { o3_on=o3_on-1; digitalWrite(pin_j3, LOW); Serial.print("O3--delay-- "); Serial.print(o3_on); Serial.println(" s"); } else { if (time >= 8.0&&time <= 8.01) digitalWrite(pin_j3, LOW); else if (time >= 17.0&&time <= 17.01) digitalWrite(pin_j3, LOW); else digitalWrite(pin_j3, HIGH); } } ``` ### 4.3、蓝牙串口协议解析函数 serial.ino文件是串口数据解析。当程序运行时,可以通过蓝牙串口的数据解析,实时地改变程序里面几个控制变量的值。 发给板子的串口数据,如果第一个字符是t,就告诉程序这是一个测试命令,进入comReadControltest()函数。如果第一个字符是小写的s,就告诉程序这是一个控制命令,进入comReadControl()函数。如果你要调时间,那么第一个字符就要是大写的S,告诉程序这是一个改DS3231实时时钟的命令,让程序进入setTime()函数。 ```c void comRead(void) { ssRead(); if (mark == true) { if (comdata[0] == 't') { ssAnalysis(2, 4); comReadControltest(); } else if (comdata[0] == 's') comReadControl(); else if (comdata[0] == 'S') setTime(); mark = false; comdata = ""; } } ``` 当第一个字符为大写S时,进入setTime()函数。如果第二个字符时T,则修改时间。如果第二个字符是D,则修改日期。举个例子,修改时间为8点30分00秒的命令为“ST8,30,0,”,修改日期为20年2月1日的命令为“SD20,2,1,”, ```c void setTime() { if (comdata[1] == 'T') { ssAnalysis(2, 3); ds3231put2(); } else if (comdata[1] == 'D') { ssAnalysis(2, 4); ds3231put1(); } } ``` 当第一个字符为小写s时,进入comReadControl()函数,然后根据后面的字符判断具体操作(comReadControl函数见下面)。举例,设置一天里面控制目标的最高温度30度,最低温度20度,命令“st30,20,”。设置水泵进入自动模式,命令“sB1,”。设置水泵进入手动模式并关闭,命令“sB0,0,”。设置水泵进入手动模式并保持一直打开,命令“sB0,1,”。 自动模式下,如果要设置水泵一天里自动开关两次,设置命令为“sBn2”。可以对任意一次动作时间设置,命令为“sb第几次控制,开启时间的时,开启时间的分,关闭时间的时,关闭时间的分”。通过串口改完参数后,可以把水泵设置参数写入eeprom,这样掉电后数据不丢失,命令"sbb"。查看数据命令“sbp”。 临时开启臭氧10秒的命令示例“so10,” ```c void comReadControl() { int ii; if (comdata[1] == 't') { ssAnalysis(2, 2); setTempMax = dataByte[0]; setTempMin = dataByte[1]; if (setTempMax > 32) setTempMax = 32; else if (setTempMax < 12) setTempMax = 12; else if (setTempMin > 32) setTempMax = 32; else if (setTempMin < 12) setTempMax = 12; eepromputT(); } else if (comdata[1] == 'B') { if(comdata[2] == 'n') { ssAnalysis(3, 1); bump_counts = dataByte[0]; Serial.print("bump_c="); Serial.println(bump_counts); } else { ssAnalysis(2, 2); if (dataByte[0] == 0) { autoBUMP = false; Serial.println("bump_auto off"); if (dataByte[1] == 0) { BUMP = false; Serial.println("bump off");//sB0,0, eepromputB(); } else if (dataByte[1] == 1) { BUMP = true; Serial.println("bump on");//sB0,1, eepromputB(); } } else if (dataByte[0] == 1) { autoBUMP = true; Serial.println("bump_auto on");//sB1, eepromputB(); } } } else if (comdata[1] == 'b') { if (comdata[2] == 'b') { eepromputB(); } else if (comdata[2] == 'p') { Serial.print("There are "); Serial.print(bump_counts); Serial.println("bump control"); for (ii = 0; ii < bump_counts; ii++) { Serial.print(ii); Serial.print(" open at T="); Serial.print( bump_T_min[ii]); Serial.print(" and close at T="); Serial.println(bump_T_max[ii]); } } else { ssAnalysis(2, 5); bump_T_min[dataByte[0]] = dataByte[1] + dataByte[2] / 100.0; bump_T_max[dataByte[0]] = dataByte[3] + dataByte[4] / 100.0; Serial.print(dataByte[0]); Serial.print(" open at T="); Serial.print(bump_T_min[dataByte[0]]); Serial.print(" and close at T="); Serial.println(bump_T_max[dataByte[0]]); } } else if (comdata[1] == 'o') { ssAnalysis(2, 2); o3_on=dataByte[0]; } } ``` ### 4.4 RTC时钟接口函数 时钟控制的函数接口见ds3231.ino文件。其中ds3231put1()函数设置年月日,ds3231put2()函数设置时分秒。这两个函数会在4.3小节的comReadControl()函数里被调用。 ds3231get()函数读取RTC时钟模块的实时时间,赋值给Clock结构体变量和time变量。其中time变量把时间转化为浮点,比如时间8点半就是time=8.5。 ```c void ds3231get() { myclock.second = Clock.getSecond(); myclock.minute = Clock.getMinute(); myclock.hour = Clock.getHour(h12, PM); myclock.date = Clock.getDate(); myclock.month = Clock.getMonth(Century); myclock.year = Clock.getYear(); myclock.DoW = Clock.getDoW(); myclock.temperature = Clock.getTemperature(); time = myclock.hour + myclock.minute / 100.0; } void ds3231put1() { Clock.setDoW(dataByte[3]); //Set the day of the week Clock.setDate(dataByte[2]); //Set the date of the month Clock.setMonth(dataByte[1]); //Set the month of the year Clock.setYear(dataByte[0]); //Set the year (Last two digits of the year) Serial.println(dataByte[0]); } void ds3231put2() { Clock.setHour(dataByte[0]); //Set the day of the H Clock.setMinute(dataByte[1]); //Set the date of the M Clock.setSecond(dataByte[2]); //Set the month of the S } ``` ### 4.5、eeprom接口函数 为了掉电不丢数据。要把设置参数存进eeprom里,然后每次开机时读取eeprom。eeprom的接口函数见epprom_put_get.ino文件。 参数写入eeprom的接口函数eepromputT()和eepromputB()会在4.3小节的comReadControl()函数里被调用。 参数从eeprom读取的接口函数eepromgetT()和eepromgetB()会在4.2的init_control()函数里被调用。 其中函数名中的T表示温度参数,T是tempurature的缩写。B表示水泵时间参数,B是bump的缩写。 ```c #include <EEPROM.h> void eepromputT() { //Serial.print("epprom put "); int eeAddress = 0; EEPROM.put(eeAddress, setTempMax); eeAddress += 4; EEPROM.put(eeAddress, setTempMin); //Serial.println("done"); } void eepromgetT() { //Serial.print("epprom get "); int eeAddress = 0; EEPROM.get(eeAddress, setTempMax); eeAddress += 4; EEPROM.get(eeAddress, setTempMin); //Serial.println("done"); } void eepromputB() { int ii; //Serial.print("epprom put "); int eeAddress = 8; EEPROM.put(eeAddress, autoBUMP); eeAddress += 1; EEPROM.put(eeAddress, bump_counts); eeAddress += 1; for (ii = 0; ii < bump_counts; ii++) { EEPROM.put(eeAddress, bump_T_max[ii]); eeAddress += 4; EEPROM.put(eeAddress, bump_T_min[ii]); eeAddress += 4; } //Serial.println("done"); } void eepromgetB() { int ii; //Serial.print("epprom put "); int eeAddress = 8; EEPROM.get(eeAddress, autoBUMP); eeAddress += 1; EEPROM.get(eeAddress, bump_counts); eeAddress += 1; for (ii = 0; ii < bump_counts; ii++) { EEPROM.get(eeAddress, bump_T_max[ii]); eeAddress += 4; EEPROM.get(eeAddress, bump_T_min[ii]); eeAddress += 4; } //Serial.println("done"); } ``` ### 4.6、主函数 见fish_tank_controller_vs_V0.ino文件。setup()函数对程序作了初始化,设置串口波特率9600。void loop()函数每次延时1秒循环往复,即每次控制动作的间隔约大于1秒。比如,当通过串口发送打开水泵的命令后,要过1秒,从下一个循环周期开始,水泵才打开。 ```c #include <OneWire.h> #include <DallasTemperature.h> #include <Wire.h> #include <DS3231.h> #define ONE_WIRE_BUS 2//8 OneWire oneWire(ONE_WIRE_BUS); DallasTemperature sensors(&oneWire); DS3231 Clock; struct MyStruct { byte year, month, date, DoW, hour, minute, second, temperature; }; MyStruct myclock; bool Century = false; bool h12; bool PM; bool autoBUMP = true; bool BUMP = false; String comdata = ""; bool mark = false; unsigned int dataByte[10] = { 0 }; byte i,j; float temp = 0; float temp10 = 0; float set_Temp=0; float set_Temp10 = 0; float setTempMax=0; float setTempMin=0; float time = 0; byte bump_i = 0; byte bump_counts = 1; float bump_T_max[20] = { 0 }; float bump_T_min[20] = { 0 }; int o3_on=0; void setup() { Wire.begin(); sensors.begin(); Serial.begin(9600); init_control(); //delay(5000); Serial.println("hello xkj"); } void loop() { comRead(); ds3231get(); sensors.requestTemperatures(); temp = sensors.getTempCByIndex(0); comWriteTime(); comWriteTempur(); tempur_control(); bump_control(); o3_control(); delay(1000); } ``` # 5、展望 目前看,这个项目已经足够稳定了。板子24小时不断电,一跑就是1年,而且也已经好几年没出错了。一是因为我的代码写得比较严谨,没有BUG。二是因为Arduino开发板确实很稳定,在室内环境下可以稳定长时间工作而不会死机。 之所以把代码完全开源,是为了让大家可以根据自己鱼缸的实际情况对代码做必要的修改,提高项目的适用性。 举个例子,默认臭氧发生器是8点和17点各工作0.01小时,即36秒。当鱼缸比较小的时候,需要限制臭氧发生器的自动工作时间,防止臭氧浓度过高。可以打开control.ino文件。 找到: ```c if (time >= 8.0&&time <= 8.01) digitalWrite(pin_j3, LOW); else if (time >= 17.0&&time <= 17.01) digitalWrite(pin_j3, LOW); else digitalWrite(pin_j3, HIGH); ``` 改为: ```c if (time >= 8.0&&time <= 8.005) digitalWrite(pin_j3, LOW); else if (time >= 17.0&&time <= 17.005) digitalWrite(pin_j3, LOW); else digitalWrite(pin_j3, HIGH); ``` 这样,臭氧发生器是8点和17点的自动工作时间就变成了0.005小时,即18秒。
原创作品,未经权利人授权禁止转载。详情见
转载须知
。
举报文章
点赞
(
1
)
xukejing
擅长:其他应用
关注
评论
(1)
登录后可评论,请
登录
或
注册
yoyojacky
51
天前...
哈哈,老徐你真是爱好广泛啊。。。时间充裕
0
回复
发布
相关文章推荐
MK-米客方德推出工业级存储卡
Beetle ESP32 C3 蓝牙数据收发
Beetle ESP32 C3 wifi联网获取实时天气信息
开箱测评Beetle ESP32-C3 (RISC-V芯片)模块
正点原子数控电源DP100测评
DP100试用评测-----开箱+初体验
Beetle ESP32 C3环境搭建
【花雕体验】16 使用Beetle ESP32 C3控制8X32位WS2812硬屏之二
X
你的打赏是对原创作者最大的认可
请选择打赏IC币的数量,一经提交无法退回 !
100IC币
500IC币
1000IC币
自定义
IC币
确定
X
提交成功 ! 谢谢您的支持
返回
我要举报该内容理由
×
广告及垃圾信息
抄袭或未经授权
其它举报理由
请输入您举报的理由(50字以内)
取消
提交