IoT设备的完整开发周期,用于在Arduino环境中对ESP8266进行泳池加热控制

在本出版物中,我将分享从头开始创建IoT设备的经验:从出现想法及其在硬件中的实现,到创建控制器的固件以及通过Internet管理创建的设备的Web界面。


在创建此设备之前,我:


  • 几乎不了解电路。 仅在工作原则一级
    电阻/晶体管...我没有创建任何复杂电路的经验。
  • 从未设计过电路板。
  • 切勿焊接SMD组件。 烙铁的水平等于焊锡丝和某种继电器的水平。
  • 我从未为微控制器编写过如此复杂的程序。 整个体验是在“使Arduino上的LED点亮”级别上,我首先遇到了ESP8266控制器。
  • 我为“老大哥”写了很多C ++,但是那是十几年前的事了,很久以前,一切都被遗忘了。

当然,作为程序员(主要是Microsoft .NET)的工作经验和系统的思想帮助我理解了这一主题。 我认为读者可以。 互联网上的有用链接和文章。 我认为这篇文章最有趣,并且有助于理解该主题。


问题陈述


我住在明斯克附近的一所私人住宅中,尽管是最简单的框架,但我自己的泳池却是许多居住在乡间别墅中的人们所获得的“好处”的有机组成部分。 在不稳定的气候中,事实证明,如果在室外游泳,在游泳池里游泳会很不舒服:夜晚的水凉了,白天的大风天气却使游泳变得不舒服。 去年,我用自己的双手在游泳池上方建造了一个更饱满的测地线圆顶,放了一座小山并悬挂了蹦极-孩子们很高兴。



Flickr上圆顶建筑的照片报告


今年,我走得更远,决定组织一个燃气锅炉的泳池加热器,
在冬天为房屋供暖,在夏天为热水供暖。


在夏季,锅炉的“加热”回路借助阀门转换为加热
池。 泳池水在钛制热交换器的帮助下被加热,钛热交换器的主回路使加热回路中的冷却剂(不含杂质的热水)通过,而副水(来自泳池的水)则由过滤系统的循环泵泵送。 由于我将游泳池与加氯机一起使用( ForumHouse讨论了很多有趣的话题),因此水中含有少量盐,因此需要使用钛制热交换器。 您不能只取水直接让水通过锅炉,否则会用盐腐蚀所有管道。



穿过热交换器,由锅炉加热的热载体以约70-90°C的温度向池中的水放热,将其加热几度。 冷却剂本身冷却几十度,然后返回锅炉以便再次冷却
热身。 从锅炉到池水加热的水冷却比取决于许多因素:热交换器的容量以及一次回路和二次回路中水循环的速度。


从水池连接到热交换器的管道是普通的聚乙烯管道,
目前用于向私人住宅供应冷水。 廉价,承受体面压力的能力,无腐蚀-这些是此类管道的主要优点。 毫无疑问,聚乙烯管道的所有工作温度都限制在40摄氏度以内。 原则上,这对于池来说绰绰有余。


但是,万一泵出现紧急情况的可能性很高
池水的水再循环将由于某种原因而停止,并且锅炉将继续加热热交换器:在这种情况下,热交换器次级回路中的水将迅速上升至初级回路的温度,这意味着与热交换器相邻的聚乙烯管部分将融化,池中的水将泛滥周围的所有空间。


必须有可能保护热交换器的过热。


快速修复


为了解决该问题,在池水再循环回路的回路中包括基于霍尔效应原理的流量传感器。 此外,位于次级回路上的温度传感器
热交换器提供第二级防御,跟踪可能的过热。


不可能仅通过温度传感器来控制过热:系统具有很大的惯性:在水池回路中突然停水后,
关闭锅炉后,温度仍然持续升高一段时间,因为 锅炉仍然通过惯性驱动热水沿着回路运动,从而防止“我,我的爱人”过热。


因此,重要的是要尽快做出反应:即,停止回路中的水流
池。


使用这样的流量传感器。 塑料外壳和传感器与水缺乏接触,使其可以在盐水中使用。


温度传感器,决定使用Dallas DS18B20,它们很容易在一根1-Wire总线上一次连接几块。



决定在次级和初级的输入和输出上悬挂一对传感器
电路:总共4个传感器。 这种方法的另一个优点是
监视系统参数的能力:您可以监视一次回路中冷却剂的冷却量以及二次回路中池中的水被加热的量。 因此-监视加热的最佳状态并预测加热时间。


热交换器和进水管上的传感器位置

设备参数


该设备的第一个原型是在Arduino Uno的基础上构建并成功启动的。



但是后来很明显,我想要更多。 加热了16立方米的水,甚至只是
几度不快。 而且我想直接监控工作中的加热参数,将其打开/关闭。 但与此同时,拍摄例如每天的供暖时间表将很有趣。


好吧,既然我们已经有了物联网设备,那为什么不同时控制泳池加氯机和泵的远程激活呢?


职权范围


因此,决定开发一种设备-多功能池控制器。 他必须能够:


  • 要通过热交换器控制泳池的供暖,请打开/关闭用于加热水的燃气锅炉。
  • 通过监视次级回路中池水的存在和次级回路的温度过高来防止热交换器过热。
  • 实时显示加热统计数据(两个回路的入口和出口的温度)。
  • 在闪存中记录(记录)温度值。 显示数据
    以图表形式的特定时期。
  • 使用继电器,可以打开/关闭泳池泵和加氯机。
  • 通过内置的微型Web服务器远程管理所有设备参数。

还有一种诱使Blink,MQTT失控的诱惑。 但是从第一阶段的这些“钟声”开始
决定拒绝。 更重要的是,我不想在外部进行控制。 就我而言,内置的Web服务器已经足够。 您只能通过VPN从外部世界进入家庭网络,从而确保了安全性。


硬体


作为控制器,决定使用便宜且流行的ESP8266。 这对我来说是完美的,除了一件事:将5伏传感器的信号电平与3.3伏控制器逻辑相匹配。 原则上,达拉斯传感器似乎可以在3伏特下工作,但是从控制器到传感器的连线很长,大约7米。 因此,最好增加电压。


确定需要硬件:


  • ESP8266控制器或其较老版本的ESP32(作为DevKit模块)。
  • 传感器信号电平的对齐。
  • 功率调节器是电路的5伏部分。
  • 继电器控制模块。
  • RTC时钟+闪存用于记录。
  • 最简单的2行LCD显示屏可显示传感器的当前值以及设备和继电器的状态。
  • 几个物理按钮,无需通过Web即可控制设备状态。

列表中的许多组件都作为Arduino的模块出售,许多模块与3.3v逻辑兼容。 但是,我不想用线束将所有这些东西“塞”在面包板上,因为我想拥有一个整洁漂亮的“设备”。 是的,对于中文提供给模块的钱,您可以完全绘制并订购您的单个印刷电路板,并且它的到来的期望将得到相对快速而可靠的安装的补偿。


再一次,我注意到这是我在电路和设计此类硬件方面的第一次经验。 我必须学习很多东西。 确实,就我的专业而言,我对微控制器有点过分。 但是,“尽我所能”做的每一件事都不允许我心中存在完美主义的精神。


电路图


市场上有许多程序可让您绘制电路和印刷电路板。 没有这方面的经验,我立即喜欢EasyEDA ,它是一个免费的在线编辑器,可让您精美绘制电路图,检查是否没有遗忘,所有组件都有连接,绘制印刷电路板,然后立即订购其生产。


我遇到的第一个困难是:DevKit ESP8266或ESP32控制器有很多选择,其中一些在引脚的位置和用途上有所不同,甚至在宽度上也有所不同。 决定画图电路,以便可以将任意宽度和端子的任何位置的DevKit放置在其侧面-两排成对的跳线孔,然后相对于专门购买的控制器进行接线以连接必要的端子。


将控制器和两对成对的跳线放置在图中:JH1和JH2在图中:



对于不同的DevKit,内置稳定器的电源输入5v和输出3.3v的引脚以及GND的位置在我看来都是相同的,但我仍然决定安全使用并使其跳线:图中的JP1,JP2,JP3。


我决定通过将跳线连接到电路上可能具有的功能的元件上来对其进行签名。


这就是我最终购买并安装的DevKit ESP8266的外观

D1(GPIO5)和D2(GPIO4)负责I2C总线,1-Wire的D5(GPIO14),D6(GPIO12)负责接收流量传感器的脉冲。


电路图:



(可点击图片)


尽管ESP8266上有内置3.3v电源稳压器,但我们仍然需要5伏电压为传感器和LCD供电,以及12伏电压为继电器供电。 决定使电路板电源为12伏,然后将稳压器AMS1117-5.0置于输入,在输出处提供所需的5伏。


为了匹配1-Wire总线上的信号电平,我使用了BSS138 c场效应晶体管,其两侧均带有电压“上拉”。



5V和3.3V器件的逻辑电平匹配一文中,有关电平匹配的知识非常出色。


为了匹配流量传感器的信号电平,我只在电阻器之间使用了一个分压器。 流量传感器只是一个集电极开路装置。 一些传感器可能已经内置了上拉电阻,应考虑以下因素:



图中的蓝色是流量传感器组件的示意图。 我在连接器的右侧选择了分压器,以使输出的最大电平为3.3伏。


在I2C总线上,我悬挂了DS3231SN实时时钟和AT24C256C闪存,用于存储日志。 ESP8266内置的闪存不适合使用,因为它具有很少的重写周期(根据数据手册,AT24Cxxx的重写周期为1万对100万)。


继电器控制在许多PCF8574AT和ULN2803A芯片上进行组织。



第一块芯片是I2C微控制器端口扩展器。 通过选择I2C总线上的地址来选择有效输出或输入PCF8574AT的状态。
该芯片具有一些有趣的功能,在I2C端口扩展器PCF8574一文中有很好的描述。


芯片无法直接控制负载(继电器)。 为此,使用晶体管矩阵ULN2803A。 它的一个特点是:矩阵可以很容易地将负载输出拉至地面,这意味着,如果向继电器的第二极施加电源电压,电流将流经继电器绕组,继电器触点将闭合。 不幸的是,由于包含了这一点,我们产生了一个副作用:来自控制器的信号值被反转,并且当电路接通时,所有继电器都“喀哒”一声。 我还没有弄清楚如何删除此功能。


有关此芯片的更多信息,请参见此处


PCF8574AT端口扩展器也可以用作输入:硬件按钮可以挂在它的某些输入上,在I2C总线上读取它们的值。 在该图中,引脚4-7可用于读取按钮的状态。 最重要的是不要忘记以编程方式启用对营养的相应腿部的内置收紧。


同时,如果您突然想要连接其他继电器,我会将布线留到晶体管矩阵。 为了进行可能的连接,我将所有引线都带到了连接器上(更确切地说,带到了它们下面的可焊接电线或可焊接标准2.54 mm DIP连接器的孔中)。


INT端口扩展器的引脚可用于快速响应按钮的按下。 它可以连接到控制器的空闲端口,并设置中断触发器以更改该引脚的状态。


两行LCD显示屏也通过PCF8574AT扩展器控制。 要点:显示器由5伏特供电,而显示器本身由3伏特逻辑控制。 顺便说一下,用于I2C的标准Arduino适配器不是为双电压设计的。 我在Internet上的某个地方找到了建立这种连接的想法,但是不幸的是,我失去了链接,所以我没有引用源。


电路板


在设计电路板时,事实证明,带脚的普通零件占用太多空间,而且DIP设计中的许多芯片也不容易找到。 在Internet上阅读了SMD的安装并没有那么复杂之后,并且凭借适当的技能,它甚至花费了更少的时间,我决定为SMD零件设计电路板。 而且我没有记错。 事实证明,这是一款紧凑,美观的主板,可以轻松放置所需的所有东西。 事实证明,具有良好烙铁,助焊剂和焊料的SMD零件确实非常易于安装。


在板上,如果我突然想焊接其他东西,我在原型上增加了一些方形的孔边距。


我制作了一个尺寸为97x97毫米的印刷电路板。 它很容易装入标准的切割电箱中。 另外,尺寸小于100x100的电路板制造便宜。 根据开发的布局,最少要生产5块板,成本为5美元,而运往白俄罗斯的成本为9美元。



评估板的设计位于EasyEDA网站上,每个人都可以使用。


我注意到,在下面的控制器照片中出现了电路板的第一个样本,在该样本上我“扭曲”了许多不必要的东西(以期在其他项目中使用最少的5个电路板)。 在这里和EasyEDA上,我发布了所有这些不必要的东西的“清理过的”版本。



董事会两面的照片

正面:



背面:



软件部分


为了对微控制器进行编程,考虑到在Arduino Uno上以原型形式存在积压,决定使用安装了ESP8266 Arduino Core的Arduino环境。 是的,您可以在ESP8266上使用Lua ,但他们表示存在挂起。 考虑到已执行的关键功能,我一点也不想这样做。
Arduino环境本身对我来说似乎有些过时,但是幸运的是,Visual Micro有一个Visual Studio 扩展 。 该环境使您可以使用IntelliSence代码提示,快速跳转到函数声明,重构代码:通常,“成人”计算机环境允许的所有内容。 Visual Micro的付费版本还允许您方便地调试代码,但是我对free选项感到满意。

项目结构


该项目包含以下文件:
Visual Studio中的项目结构

档案文件预约时间
WaterpoolManager.ino
基本变量和常量的声明。 初始化。 主循环。
HeaterMainLogic.ino
控制锅炉继电器(根据温度)和辅助继电器的基本逻辑。
Sensors.ino
读取传感器数据
Settings.ino
设备设置,将其保存到控制器闪存
液晶显示器
LCD上的信息输出
ClockTimer.ino
RTC时钟读取或时钟模拟
Relays.ino
继电器开/关控制
ButtonLogic.ino
对硬件按钮状态做出反应的逻辑
ReadButtonStates.ino
读取硬件按钮状态
EEPROM_Logging.ino
传感器数据记录在EEPROM中
WebServer.ino
内置Web服务器用于设备管理和状态显示
网页
Web服务器页面存储在此文件夹中。
索引
显示设备状态的主页。 使用ajax调用读取当前状态。 每5秒钟刷新一次。
日志图
在图形中显示传感器数据和继电器状态的日志。 使用了jqPlot库-所有构建都在客户端进行。 对控制器的请求仅发送至二进制文件-来自EEPROM的数据副本。
日志表
也是,但以表格的形式
settings.h
管理设备设置:设置温度,水流量,数据记录频率的限制
时间
当前时间设定

图书馆
EepromLogger.cpp
闪存日志库
EepromLogger.h
crc8.cpp
8- CRC
crc8.h
TimeSpan.cpp

TimeSpan.h


OneWire tempSensAddr. . ( 4 ):


while (ds.search(tempSensAddr[lastSensorIndex]) && lastSensorIndex < 4) { Serial.print("ROM ="); for (byte i = 0; i < 8; i++) { Serial.print(' '); Serial.print(tempSensAddr[lastSensorIndex][i], HEX); } if (OneWire::crc8(tempSensAddr[lastSensorIndex], 7) != tempSensAddr[lastSensorIndex][7]) { Serial.print(" CRC is not valid!"); } else lastSensorIndex++; Serial.println(); } ds.reset_search(); lastSensorIndex--; Serial.print("\r\nTemperature sensor count: "); Serial.print(lastSensorIndex + 1, DEC);  ,       ().       Serial   LCD  : // Read sensor values and print temperatures ds.reset(); ds.write(0xCC, TEMP_SENSOR_POWER_MODE); // Request all sensors at the one time ds.write(0x44, TEMP_SENSOR_POWER_MODE); // Acquire temperatures delay(1000); // Delay is required by temp. sensors char tempString[10]; for (byte addr = 0; addr <= lastSensorIndex; addr++) { ds.reset(); ds.select(tempSensAddr[addr]); ds.write(0xBE, TEMP_SENSOR_POWER_MODE); // Read Scratchpad tempData[addr] = ds.read() | (ds.read() << 8); // Read first 2 bytes which carry temperature data int tempInCelsius = (tempData[addr] + 8) >> 4; // In celsius, with math rounding Serial.print(tempInCelsius, DEC); // Print temperature Serial.println(" C"); } 

根据数据表,传感器在请求温度值和接收来自传感器的响应之间至少需要750毫秒的延迟。 因此,该代码引入了一个延迟很小的延迟。


但是,当整个设备仅在等待答案时,这种延迟在开始时是可以接受的,但是每次都定期轮询传感器绝对是不合适的。 因此,编写了以下棘手的代码,定时器每50毫秒调用一次:


 #define TEMP_MEASURE_PERIOD 20 // Time of measuring, * TEMP_TIMER_PERIODICITY ms #define TEMP_TIMER_PERIODICITY 50 // Periodicity of timer calling, ms timer.attach_ms(TEMP_TIMER_PERIODICITY, tempReadTimer); int tempMeasureCycleCount = 0; void tempReadTimer() // Called many times in second, perform only one small operation per call { tempMeasureCycleCount++; if (tempMeasureCycleCount >= TEMP_MEASURE_PERIOD) { tempMeasureCycleCount = 0; // Start cycle again } if (tempMeasureCycleCount == 0) { ds.reset(); ds.write(0xCC, TEMP_SENSOR_POWER_MODE); // Request all sensors at the one time ds.write(0x44, TEMP_SENSOR_POWER_MODE); // Acquire temperatures } // Between phases above and below should be > 750 ms int addr = TEMP_MEASURE_PERIOD - tempMeasureCycleCount - 1; if (addr >= 0 && addr <= lastSensorIndex) { ds.reset(); ds.select(tempSensAddr[addr]); ds.write(0xBE, TEMP_SENSOR_POWER_MODE); // Read Scratchpad tempData[addr] = ds.read() | (ds.read() << 8); // Read first 2 bytes which carry temperature data } } 

在每个tempMeasureCycleCount周期开始时,要求传感器读取其值。 经过约50个这样的循环后(总计为50 * 20 = 1000 ms = 1 sec),每个传感器的值被读取,一次读取一个。 所有工作都被分解为多个部分,因此在计时器中断中运行的代码不会占用控制器大量的时间。


流量传感器的值计算如下。 通过中断传感器悬挂的销,我们增加了来自流量传感器的滴答计数器的值:


 pinMode(FLOW_SENSOR_PIN, INPUT); attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_PIN), flow, RISING); // Setup Interrupt volatile int flow_frequency; // Flow sensor pulses int flowMeasureCycleCount = 0; void flow() // Flow sensor interrupt function { flow_frequency++; } 

在询问温度传​​感器的同一计时器中,我们每秒获取一次滴答值,然后使用FLOW_SENSOR_CONST常数将其转换为升,该常数的值可以在传感器的特性中找到:


 flowMeasureCycleCount++; if (flowMeasureCycleCount >= 1000 / TEMP_TIMER_PERIODICITY) { flowMeasureCycleCount = 0; litersInMinute = (flow_frequency / FLOW_SENSOR_CONST); // Pulse frequency (Hz) = FLOW_SENSOR_CONST*Q, Q is flow rate in L/min. flow_frequency = 0; // Reset Counter } 

从传感器和设备状态记录数据


在开发日志记录机制时,可以突然关闭设备,即关闭设备。 几乎在任何时候。 当您停止录制时,我们必须能够将录制的所有内容还原到最后一刻。 同时,我们不能不断地重写闪存的相同区域(例如,某个位置的某个标题,记住记录的最后保存地址),以避免在该位置加速擦拭闪存。


经过一些“累积”之后,发明并实施了以下记录模型:



每个记录都是一条记录,其中包含有关水流的当前值,传感器温度以及字节中编码的设备状态的信息(各个位指示继电器是否打开,是否启用加热):


 struct LogEvent { unsigned char litersInMinute = 0; unsigned char tempCelsius[4]{ 0, 0, 0, 0 }; unsigned char deviceStatus = 0; } 

每条记录之后,都有一个CRC校验和字节,指示该记录是否正确写入,以及通常是否在此存储位置中至少写入了一些内容。


由于就容量而言,在每个记录的当前时间( 时间戳 )上记录数据太昂贵了,因此数据以大块组织,每个块中有N条记录。 每个块的时间戳记仅记录一次,其余部分-基于有关记录频率的信息进行计算。


 unsigned int logRecordsInBlock = 60 * 60 / loggingPeriodSeconds; // 1 block for hour unsigned int block_size = sizeof(Block_Header) + logRecordsInBlock * (record_size + crcSize); unsigned int block_count = total_storage_size / block_size; 

例如,以每30秒一次的记录频率,我们在一个块中将有120个条目,并且块大小约为840个字节。 总共,我们可以在32 KB的闪存驱动器的内存中容纳39个块。 通过这样的组织,事实证明每个块都从内存中严格定义的地址开始,并且“遍历”所有块都不是问题。


因此,在最后一次关闭设备的过程中,如果记录突然中断,我们将有一个未完成的块(也就是说,其中的某些记录丢失了)。 打开设备电源后,算法将搜索最后一个有效的块标题(时间戳+ crc)。 并从下一个块开始继续记录。 记录是循环执行的:最新块覆盖最旧块的数据。


读取时,将顺序读取所有块。 无效块(那些未通过CRC传递时间戳的块)将被完全忽略。 读取每个块中的记录,直到遇到第一个无效记录为止(即,如果未完全记录该块,则最后一次切断该记录的记录)。 其余的将被忽略。
对于每个记录,根据块的时间戳和该块中记录的序列号来计算当前时间。


液晶屏


该设备使用显示器QC1602A,能够显示2行16个字符。 第一行显示有关传感器当前值的当前信息:流量和温度。 如果超过了指定的限制,则该值附近会出现一个感叹号。 第二行显示加热继电器和泵的状态,以及自打开或关闭加热以来经过的时间。 每隔5秒,第二行的显示会短暂显示当前限制。 出版物末尾显示了各种模式下的显示照片。


图表


通过内置的Web服务器请求时,将使用JavaScript以二进制形式读取日志记录数据:


 var xhttp = new XMLHttpRequest(); xhttp.open("GET", "logs.bin", true); xhttp.responseType = "arraybuffer"; xhttp.onprogress = updateProgress; xhttp.onload = function (oEvent) { var arrayBuffer = xhttp.response; if (arrayBuffer) { var byteArray = new Uint8Array(arrayBuffer); … }}; xhttp.send(null); 

以某些流行的非二进制格式(例如ajax)读取它们对于控制器来说是不可接受的,这主要是因为内置的HTTP服务器必须返回大量数据。


出于同样的原因, jqPlot JavaScript库用于构建图,并且JS库文件本身是从流行的CDN加载的。


设备时间表的示例:



可以清楚地看到,在大约9:35打开设备进行加热时,锅炉开始逐渐加热加热回路(传感器T3,T4),然后池电路的温度开始升高(传感器T1,T2)。 大约10:20左右的某个时间,锅炉切换为加热房屋中的热水,加热回路的温度下降。 然后又过了10分钟,锅炉又回到加热池水的状态。 在10:50发生事故:游泳池中循环水的泵突然关闭。 水流量急剧下降至零,加热继电器关闭(第二张图表上的红色虚线),以防止过热。 但是设备仍处于加热状态(第二张图上的红线)。 即 如果再次打开泵且温度正常,则设备将恢复加热。 我注意到,在紧急关闭泵后,由于热交换器的过热,池水回路(T1,T2)中的温度开始急剧上升。 如果不迅速关闭锅炉,那将会有麻烦。


嵌入式Web服务器


要与外界通信,请使用标准类ESP8266WebServer 。 设备启动时,将使用#define AP_PASS中指定的默认密码将其初始化为访问点。 网页将自动打开以选择可用的wi-fi网络并输入密码。 输入密码后,设备将重新启动并连接到指定的访问点。


成品设备


将完成的设备放置在标准切割盒中进行布线。 LCD上切了一个孔,连接器上也打了孔。



设备在不同模式下的外观照片

显示开机后经过的时间:



显示限制:



结论


总而言之,我想说的是,开发这样的设备时,我在电路,PCB设计,SMD组件的安装技巧,微控制器的体系结构和编程方面拥有丰富的经验,我记得几乎忘记了C ++以及对内存和其他有限控制器资源的谨慎处理。 HTML5,JavaScript的知识以及浏览器中脚本的调试技巧在某种程度上也很有用。


这些技能和在设备开发过程中获得的乐趣是获得的主要好处。 以及设备的源代码,电路图,印刷电路板-请使用,修改。 所有项目源代码都在GitHab上。 EasyEDA上公共项目中的硬件。 我在网络驱动器上收集了项目中使用的芯片上的数据。

Source: https://habr.com/ru/post/zh-CN413955/


All Articles