关于串口屏的使用我不打算介绍,只需要参考你的屏的开发文档就可以了。这属于一个“工作性学习”,和使用Arduino或者ESP是有区别的,完全可以用完忘了,下次用再看。并且我的时间很不充裕,把其他部分都精简掉,只讲一下Arduino部分,因为Arduino是这个综合性较强的闭环控制系统的控制器,所以它读取传感器信息、控制控制器动作,代码相对比较多,看起来更像一个工程了:

一、整体的代码:

/*Name:      ArduinoProject.inoCreated:  2020/4/8 14:01:14Author:    zcsor中控,负责:控制HMI、ESP、执行器,读取传感器,进行运算。 为了更好的适应实际情况,修改了库部分库的内容。由于代码比较多,所以定义了一些模块和类以实现功能:
一、信息交换子系统:1、和HMI串口通讯,实现可视化的本地信息交互。2、和ESP传偶通讯,通过APP实现可视化的远程信息交互。3、接收Serial的set命令,从而完成对各项数值的设置。使用轮询——事件模式进行设计二、控制系统1、定时器:控制系统实现了精确的1秒定时器,用于将信息交换子系统和控制系统操作进行隔离:提高信息交换子系统的实时性,减少控制系统本身对资源的浪费。2、传感器:DS18B20传感器测定水温HCSR04传感器测定水位TS300B传感器测定浊度*DIY TDS传感器测定电导率3、控制器:处理信息交换子系统的信息交互,并对操作作出响应读取传感器的值,根据传感器的值控制执行器的动作给执行器发送指令,更改执行器的状态达到控制目的4、执行器:根据控制器的命令,进行状态转移从而达到控制目的传感器使用轮询模式进行设计控制器和执行器使用有限状态机进行设计
*//*
热敏电阻
温度 电压读数
70 --200
23 --530
*/#include <Fsm.h>
#include "ActuatorAirPump.h"
#include "ActuatorFeed.h"
#include "ActuatorTemp.h"
#include "ActuatorWaterCycle.h"
#include "ActuatorLED.h"
#include "ActuatorWaterCycle.h"
#include "SensorTurb.h"
#include "SensorDist.h"
#include "SensorTemp.h"
#include "SensorColor.h"
#include "SensorTDS.h"
#include "ParameterConfig.h"    //负责HMI、ESP的各项参数的读写。数据保存在EEPROM中。
#include "ESPMessage.h"         //负责与ESP通讯。将数据帧分类派发到回调,向ESP发送指令。
#include "HMIMessage.h"         //负责与HMI通讯。将数据帧分类派发到回调,向HMI发送指令。
#include "SerialFrame.h"        //负责与指定串口通讯。从串口数据中整理出数据帧。
#include <MsTimer2.h>           //负责主循环中按秒计时的计时器。除了信息交换子系统的轮询外,其它的轮询都是每秒一次。
//#include <avr/interrupt.h>//====================信息交换子系统====================
HardwareSerial* Serial_HMI = &Serial1;
HardwareSerial* Serial_ESP = &Serial2;
uint8_t ESPLost = 0;
uint8_t ESPLostMax = 3;
uint8_t ESPCH_PDPin = 22;       //硬件重启ESP8266(未接)
//====================本地控制子系统====================
//------------------------计时器------------------------
volatile uint8_t Time_Hour = 0;              //当前时
volatile uint8_t Time_Minute = 0;            //当前分
volatile uint8_t Time_Second = 0;            //当前1秒
volatile bool Time_IsChecked = false;        //时间是否校准
uint8_t Time_LastSecond = 0;        //上次的1秒
uint8_t Time_Step_Count = 0;        //经历的1秒的次数
uint8_t Time_Run_Cycle = 0;         //周期切换//------------------------传感器------------------------
//1.0、水位传感器
uint8_t Sensor_Pin_Dist_TRIG = 40;
uint8_t Sensor_Pin_Dist_ECHO = 41;
//2.0、浊度传感器(以电压表示)
uint8_t Sensor_Pin_Turb = A13;
//3.0、电导率传感器
uint8_t Sensor_Pin_Conductivity_Read = A14;
//4.0、温度传感器
uint8_t Sensor_Pin_Temp = A15;
//5.0、制冷器热端温度传感器
uint8_t Sensor_Pin_CollTemp = A12;
//------------------------执行器------------------------
//在Setup中设置了底层Timer4分频,从使得6、7、8引脚的PWM频率达到31372.55 Hz
//1.1、循环水泵(电压输入端加续流二极管)
uint8_t Actuator_WaterPump_Pin = 8;
//1.2、补水水泵(电压输入端加续流二极管)
uint8_t Actuator_WaterIn_Pin = 5;
//1.3、排水三通电磁阀(电压输入端加续流二极管)
uint8_t Actuator_WaterOut_Pin = 4;
//4.1、降温风扇(电压输入端加续流二极管)。
//uint8_t Actuator_FanLeft_Pin = 7;
//uint8_t Actuator_FanRight_Pin = 7;
//4.2、PTC加热。PID控制(最大功率60w)
uint8_t Actuator_Heat_Pin = 2;
//4.3、半导体制冷片、风扇、水冷泵
uint8_t Actuator_ChillPlate_Pin = 11;       //制冷片
//5.1、光源LED。PID控制(最低亮度占空比32,频率≈490/255*32≈61Hz。最高亮度96)
uint8_t Actuator_LED_Pin = 3;
//6.1、自动喂食
uint8_t Actuator_Feed_Pin = 37;
//7.1、气泵(时间控制,电压输入端加续流二极管)
uint8_t Actuator_AirPump_Pin = 6;
//------------------------控制器//------------------------
//读写EEPROM保存的配置信息
ParameterConfigClass ParamConfig = ParameterConfigClass();//初始化函数
void setup() {//设置Timer4的分频,将6、7、8引脚的PWM频率修改为31372.55Hz,使气泵、降温风扇、水泵从而实现更好的近似DC,降低它们的噪音。TCCR4B = TCCR4B & B11111000 | B00000001;//设置Timer3的分频,将2、3、5上的PWM频率修改为3921.16Hz,从而使LED的电流更平稳TCCR3B = TCCR3B & B11111000 | B00000010;//打开串口Serial.begin(115200);while (!Serial){delay(5);}//读配置ParamConfig.init();//先设置HMI,初始化时会初始化按钮状态HMIMessage::Init(HMI_NumberChange, HMI_StringChange, HMI_ClickButton);HMIMessage::Begin(Serial_HMI, 115200);HMIMessage::SetDefUI(ParamConfig.HMI_ParamConfig,sizeof(ParamConfig.HMI_ParamConfig) / sizeof(ParamConfig.HMI_ParamConfig[0]),ParamConfig.ESP_Config.WifiSSID,ParamConfig.ESP_Config.MQTTPassword);//设置ESPESPMessage::Init(ESPCH_PDPin, ESP_ServerCommand, ESP_NTPCheck, ESP_NetState);ESPMessage::Begin(Serial_ESP, 115200);ESPMessage::SetDefConfig(ParamConfig.ESP_Config);ESPMessage::SendReconnect();//初始化传感器SensorTemp.init(Sensor_Pin_Temp);SensorDist.init(Sensor_Pin_Dist_TRIG, Sensor_Pin_Dist_ECHO, 20, 1000);SensorTurb.init(Sensor_Pin_Turb);SensorTDS.init(Sensor_Pin_Conductivity_Read);//初始化传感器值,避免执行器错误动作SensorTemp.Read();SensorDist.Read(SensorTemp.Value);SensorTurb.Read(SensorTemp.Value);SensorTDS.Read(SensorTemp.Value);//初始化执行器ActuatorWaterCycle::init(Actuator_WaterPump_Pin, Actuator_WaterIn_Pin, Actuator_WaterOut_Pin, &SensorDist, &ParamConfig);ActuatorLED::init(Actuator_LED_Pin, &ParamConfig);ActuatorTemp::init(Actuator_Heat_Pin, Actuator_ChillPlate_Pin, Sensor_Pin_CollTemp, &ParamConfig, &SensorTemp);ActuatorFeed::init(Actuator_Feed_Pin, &ParamConfig);ActuatorAirPump::init(Actuator_AirPump_Pin, &ParamConfig);//开启计时器,975,快1.5625MsTimer2::set(1000, MathBaseTime);MsTimer2::start();
}//主循环
void loop() {//处理Serial命令SerialCommand();//处理HMI的串口信息HMIMessage::Loop();//处理ESP8266的串口信息ESPMessage::Loop();//执行器ActuatorWaterCycle::Run();ActuatorLED::Run(Time_Second);ActuatorFeed::Run(Time_Second);//每过至少1秒if (Time_Second != Time_LastSecond) {Time_LastSecond = Time_Second;//若未校准时间,则校准时间if (!Time_IsChecked) ESPMessage::SendNTPCheck();if (Time_Second == 0) {             //每分钟运行一次//更新分钟ParamConfig.SetHMIConfigByIndex(HMI_ID_SettingCurTimeMinute, Time_Minute);HMIMessage::SetFormatNumber(HMI_ID_SettingType, HMI_ID_SettingCurTimeMinute, Time_Minute);ESPMessage::SendHMIConfig(HMI_ID_SettingCurTimeMinute, Time_Minute);}if (Time_Minute == 0) {             //每小时运行一次//更新小时ParamConfig.SetHMIConfigByIndex(HMI_ID_SettingCurTimeHour, Time_Hour);HMIMessage::SetFormatNumber(HMI_ID_SettingType, HMI_ID_SettingCurTimeHour, Time_Hour);ESPMessage::SendHMIConfig(HMI_ID_SettingCurTimeHour, Time_Hour);}if (Time_Step_Count == 0) {//温度转换,DS18B20转换和读取都需要较长时间,最好分开运行。SensorTemp.Conversion();//更新温度SensorTemp.Read();//发送给HMI显示HMIMessage::SetVal(HMI_ID_SensorType, HMI_ID_SensorTemp, SensorTemp.Value * 10);//更新距离SensorDist.Read(SensorTemp.Value);//发送给HMI显示HMIMessage::SetVal(HMI_ID_SensorType, HMI_ID_SensorDist, SensorDist.Value);       //这里不用*10,单位转化为cm}else if (Time_Step_Count == 2) {//更新浊度SensorTurb.Read(SensorTemp.Value);//发送给HMI显示HMIMessage::SetVal(HMI_ID_SensorType, HMI_ID_SensorTurb, SensorTurb.Value * 10);//当浊度高于设定值时进行提醒HMIMessage::SetPco(HMI_ID_SensorType, HMI_ID_SensorTurb, SensorTurb.Value > ParamConfig.GetHMIConfigByIndex(HMI_ID_SettingTurbUpper) ? 0 : 2);//更新电导率SensorTDS.Read(SensorTemp.Value);//发送给HMI显示HMIMessage::SetVal(HMI_ID_SensorType, HMI_ID_SensorTDS, SensorTDS.Value * 10);//当电导率高于设定值时进行提醒HMIMessage::SetPco(HMI_ID_SensorType, HMI_ID_SensorTDS, SensorTDS.Value > ParamConfig.GetHMIConfigByIndex(HMI_ID_SettingTDSUpper) ? 0 : 2);}else if (Time_Step_Count == 4) {        //这两个是全自动控制,不用特别快的响应速度ActuatorTemp::Run();ActuatorAirPump::Run();}if (Time_Step_Count == 1 && Time_Run_Cycle == 1) {ESPMessage::SendSensorValue(HMI_ID_SensorTemp, SensorTemp.Value * 10);ESPMessage::SendSensorValue(HMI_ID_SensorDist, SensorDist.Value);}else if (Time_Step_Count == 3 && Time_Run_Cycle == 1) {ESPMessage::SendSensorValue(HMI_ID_SensorTurb, SensorTurb.Value * 10);ESPMessage::SendSensorValue(HMI_ID_SensorTDS, SensorTDS.Value * 10);}else if (Time_Step_Count == 4 && Time_Run_Cycle == 1) {//查询服务器状态,若未连接服务器,则会连接服务器ESPMessage::SendGetNetworkState();//上传服务器数据ESPMessage::SendPublishSensors();//检查ESP工作状态ESPLost++;          //增加无通讯状态记录if (ESPLost >= ESPLostMax) {//硬件重启ESP8266//等待//重新初始化ESP8266ESPMessage::Init(ESPCH_PDPin, ESP_ServerCommand, ESP_NTPCheck, ESP_NetState);ESPMessage::Begin(Serial_ESP, 115200);ESPMessage::SetDefConfig(ParamConfig.ESP_Config);ESPMessage::SendReconnect();//归零无通讯状态记录ESPLost = 0;}}if (Time_Step_Count == 5) {Time_Step_Count = 0;Time_Run_Cycle = 1 - Time_Run_Cycle;}else {Time_Step_Count ++;}}
}//计时器函数。全部控制都是以1秒为时间单位进行的。
void MathBaseTime() {//每次少1.5625个计数volatile static int cnt = 0;volatile static int ofm = 0;cnt += 1;if (cnt >=125) {              //每个计数1.024毫秒,125个计数为128毫秒cnt -= 16;               ofm += 128;                 //累计快的毫秒数}if (ofm > 1000) {           //将快的超出1秒的部分找回ofm -= 1000;}else {Time_Second++; if (Time_Second >= 60) {Time_Second = 0;Time_Minute++;}if (Time_Minute >= 60) {Time_Minute = 0;Time_Hour++;}if (Time_Hour >= 24) {Time_Hour = 0;//每天校准一次Time_IsChecked = false;}}
}//设置网络参数
void SetNtConfig(uint32_t id, String val) {if (id == 300) {ParamConfig.SetWifiSSID(val);HMIMessage::SetText("n", 300, val);}else if (id == 301) {ParamConfig.SetWifiPassword(val);HMIMessage::SetText("n", 301, val);}else if (id == 302) {ParamConfig.SetMQTTServerIP(val);}else if (id == 303) {ParamConfig.SetMQTTUserName(val);}else if (id == 304) {ParamConfig.SetMQTTPassword(val);}else if (id == 305) {ParamConfig.SetMQTTClientName(val);}else if (id == 306) {ParamConfig.SetMQTTServerPort(val);}ESPMessage::SendNetworkConfig(id, val);
}//串口命令——字符串变化(WIFI SSID,WIFI PASSWORD)
void HMI_StringChange(uint32_t id, String str, uint32_t strlen) {//发送消息给HMI,这样用户可以看到设置值是否被正确执行。(HMI不接收包头,发送包尾时使用write保证传输个数和类型正确)HMIMessage::SetText("n",id, str);//发消息给ESP8266(ESP识别包头,除字符串以外都用write传输个数和类型正确)ESPMessage::SendNetworkConfig(id, str);//重新连接ESPMessage::SendReconnect();//保存数据设置到EEPROMif (id == ESP_ID_SettingWifiSSID) {ParamConfig.SetWifiSSID(str);}if (id == ESP_ID_SettingWifiPassword) {ParamConfig.SetWifiPassword(str);}ParamConfig.WriteESPConfigToEEPROM();
}//串口命令——数字变化(时间、上下限等)
void HMI_NumberChange(uint32_t id,  uint32_t num) {//发送数据到HMIHMIMessage::SetFormatNumber("n", id, num);//发送数据到ESP8266(ESP识别包头,除字符串以外都用write传输个数和类型正确)ESPMessage::SendHMIConfig(id, num);//保存数据设置到EEPROMParamConfig.SetHMIConfigByIndex(id, num);ParamConfig.WriteHMIConfigToEEPROM();
}void HMI_ClickButton(uint32_t id, uint32_t num) {//保存按钮值(有限状态机用)HMIMessage::SaveControlButtonValue(id, num);//发送数据到ESP8266——除了下载按钮(直接发送下载地址)if (id == 400) {//发送数据到HMIHMIMessage::SetText("qr", 0, ParamConfig.GetHMIDownloadUrl());}else if (id == 401) {HMIMessage::SetText("qr", 0, "qywl" + (String)ParamConfig.ESP_Config.MQTTUserName + "|" + (String)ParamConfig.ESP_Config.MQTTPassword + "|" + (String)ParamConfig.ESP_Config.MQTTClientName);}
}//接收到服务器控制命令回调
void ESP_ServerCommand(uint32_t id, uint32_t num) {HMIMessage::SaveControlButtonValue(id, num);
}//同步时间回调
void ESP_NTPCheck(uint32_t id, uint32_t num) {//记录时间、更新上次执行时的秒数Time_Hour = id & 0xff;Time_Minute = id >> 8 & 0xff;Time_Second = (id >> 16 & 0xff);//更新分钟HMIMessage::SetFormatNumber(HMI_ID_SettingType, HMI_ID_SettingCurTimeMinute, Time_Minute);//更新小时HMIMessage::SetFormatNumber(HMI_ID_SettingType, HMI_ID_SettingCurTimeHour, Time_Hour);//更新记录ParamConfig.SetHMIConfigByIndex(HMI_ID_SettingCurTimeMinute, Time_Minute);ParamConfig.SetHMIConfigByIndex(HMI_ID_SettingCurTimeHour, Time_Hour);
}//更新服务器连接状态,若未连接则重新发送数据并连接
void ESP_NetState(uint32_t id, uint32_t num) {static bool oldWifiIsConnected = false;static bool oldMqttIsConnected = false;//归零无通讯状态记录ESPLost = 0;//Wifi连接状态HMIMessage::SetPco("n", 302, id);//MQTT服务器状态HMIMessage::SetPco("n", 303, num);if (oldWifiIsConnected==false && id == 1) {         //连接上Wifi,同步时间ESPMessage::SendNTPCheck();    }if (oldMqttIsConnected == false && num == 1) {      //连接上MQTT,发送按钮值、设置值for (int i = 402; i <= 409; i++) {delay(50);ESPMessage::SendClickButton(i, HMIMessage::LoadControlButtonValue(i));}for (int i = 202; i <= 212; i++) {delay(50);ESPMessage::SendHMIConfig(i, ParamConfig.GetHMIConfigByIndex(i));}for (int i = 220; i <= 225; i++) {delay(50);ESPMessage::SendHMIConfig(i, ParamConfig.GetHMIConfigByIndex(i));}}oldWifiIsConnected = id == 1;oldMqttIsConnected = num == 1;//如果没有连接服务器,则重新发送连接参数并连接if (!oldMqttIsConnected) {ESPMessage::SetDefConfig(ParamConfig.ESP_Config);ESPMessage::SendReconnect();}
}//串口命令,主要用于手动配置各个端口和HMI、ESP的参数
void SerialCommand() {String SerialCommandStr = "set304=……………………………………=\r\n";int SerialCommandID = 0;int SerialCommandVal = 0;int SerialCommandSplit = 0;if (Serial.available()>3 && Serial.find('set')) {SerialCommandStr = Serial.readStringUntil('\r');if (SerialCommandStr !="") {SerialCommandSplit = SerialCommandStr.indexOf('=');SerialCommandID = atoi(SerialCommandStr.substring(0, SerialCommandSplit).c_str());SerialCommandStr = SerialCommandStr.substring(SerialCommandSplit + 1);if (SerialCommandID < 100) {      //设置端口pinMode(SerialCommandID, OUTPUT);SerialCommandVal = atoi(SerialCommandStr.c_str());Serial.println((String)"Set pin " + SerialCommandID + "=" + SerialCommandVal);if (SerialCommandVal <= 1) {digitalWrite(SerialCommandID, SerialCommandVal);}else {analogWrite(SerialCommandID, SerialCommandVal);}}else if (SerialCommandID == 100) {   //设置下载地址Serial.println("Set DwonloadUrl " + SerialCommandStr);ParamConfig.WriteHMIDownloadUrlToEEPROM(SerialCommandStr);Serial.println("Set DwonloadUrl " + ParamConfig.GetHMIDownloadUrl());}else if (SerialCommandID >= 300) {              //设置ESPSerial.println((String)"Set esp " + SerialCommandID + "=" + SerialCommandStr);//300=234                                    WifiSSID//301=234                               WifiPassword//302=234                             MQTTServerIP//303=234                                    MQTTUserName//304=234              MQTTPassword//305=234                                 MQTTClientName//306=234                                      MQTTServerPort//310=234                                         重连MQTT,保存设置SetNtConfig(SerialCommandID, SerialCommandStr);if (SerialCommandID == 310) {ParamConfig.WriteESPConfigToEEPROM();ESPMessage::SendReconnect();}}}}
}

首先,setup函数中使用一些小技巧来更改了一些计时器的分频,这样做的好处显而易见——除了LED其他的我并没有使用滤波电路,并且它们经受住了时间的考验。而使用PWM控制LED亮度时的滤波电路也仅仅是一个简单的RC滤波,虽然它很热,但依旧经受住了时间的考验。接下来就是各种初始化,在loop函数中,按时间间隔处理各种事物,而不是一直在处理,这是一个非常好玩的事情——使用delay不利于更新时间,因为很多自动控制是被设计为到达指定时间开始和结束的,并且我不希望它在一分钟之内反复横跳:

void HMI_ClickButton(uint32_t id, uint32_t num) {//保存按钮值(有限状态机用)HMIMessage::SaveControlButtonValue(id, num);//发送数据到ESP8266——除了下载按钮(直接发送下载地址)if (id == 400) {//发送数据到HMIHMIMessage::SetText("qr", 0, ParamConfig.GetHMIDownloadUrl());}else if (id == 401) {HMIMessage::SetText("qr", 0, "qywl" + (String)ParamConfig.ESP_Config.MQTTUserName + "|" + (String)ParamConfig.ESP_Config.MQTTPassword + "|" + (String)ParamConfig.ESP_Config.MQTTClientName);}
}

这个函数动态的设置了二维码的内容,所以,这个二维码就像上面显示的一样,当点击下载时将会显示下载APP的URL,而点击登录时会显示当前设备的信息,使用APP扫码即可登录。关于最后一个函数:


//串口命令,主要用于手动配置各个端口和HMI、ESP的参数
void SerialCommand() {String SerialCommandStr = "set304=………………………………………………g=\r\n";int SerialCommandID = 0;int SerialCommandVal = 0;int SerialCommandSplit = 0;if (Serial.available()>3 && Serial.find('set')) {SerialCommandStr = Serial.readStringUntil('\r');if (SerialCommandStr !="") {SerialCommandSplit = SerialCommandStr.indexOf('=');SerialCommandID = atoi(SerialCommandStr.substring(0, SerialCommandSplit).c_str());SerialCommandStr = SerialCommandStr.substring(SerialCommandSplit + 1);if (SerialCommandID < 100) {      //设置端口pinMode(SerialCommandID, OUTPUT);SerialCommandVal = atoi(SerialCommandStr.c_str());Serial.println((String)"Set pin " + SerialCommandID + "=" + SerialCommandVal);if (SerialCommandVal <= 1) {digitalWrite(SerialCommandID, SerialCommandVal);}else {analogWrite(SerialCommandID, SerialCommandVal);}}else if (SerialCommandID == 100) {   //设置下载地址Serial.println("Set DwonloadUrl " + SerialCommandStr);ParamConfig.WriteHMIDownloadUrlToEEPROM(SerialCommandStr);Serial.println("Set DwonloadUrl " + ParamConfig.GetHMIDownloadUrl());}else if (SerialCommandID >= 300) {              //设置ESPSerial.println((String)"Set esp " + SerialCommandID + "=" + SerialCommandStr);//300=123                                    WifiSSID//301=123                               WifiPassword//302=123                             MQTTServerIP//303=123                                   MQTTUserName//304=123              MQTTPassword//305=123                                 MQTTClientName//306=123                                      MQTTServerPort//310=1                                         重连MQTT,保存设置SetNtConfig(SerialCommandID, SerialCommandStr);if (SerialCommandID == 310) {ParamConfig.WriteESPConfigToEEPROM();ESPMessage::SendReconnect();}}}}
}

这个函数中有一段不太好理解的东西:>=300时的处理,这是在串口调试器中手工调试与ESP通讯时使用的测试代码。

二、几个核心类的实现:

1、串口通讯

#include "SerialFrame.h"bool SerialFrame::Read(HardwareSerial* hwSerial,bool debugMessage)
{PackHead = false;                               //是否找到帧头   PackTail = false;                               //是否找到帧尾Serial_FrameBuff_Ptr = 0;                       //缓存指针//memset(Serial_FrameBuff, 0, sizeof(Serial_FrameBuff));  //数据清零(不清零也不影响正确工作)//循环读取串口数据,得到一个完整帧(舍弃了包头,但是包尾还在)while (hwSerial->available() > 0){Serial_FrameBuff[Serial_FrameBuff_Ptr++] = (uint8_t)hwSerial->read();if (!PackHead) {                                    //没找到包头时if (debugMessage) {Serial.print((char)Serial_FrameBuff[Serial_FrameBuff_Ptr - 1]);}//Serial.print((char)Serial_FrameBuff[Serial_FrameBuff_Ptr-1]);     //输出其它信息if (Serial_FrameBuff_Ptr >= 1) {                //确定缓存中后3字节是不是包头PackHead = Serial_FrameBuff[Serial_FrameBuff_Ptr - 1] == Serial_Frame_Start;if (PackHead) {Serial_FrameBuff_Ptr = 0;               //找到包头时从缓存头部写入数据}}}else {                                              //找到包头时if (Serial_FrameBuff_Ptr >= 11) {               //确定缓存中后3字节是不是包尾PackTail = Serial_FrameBuff[Serial_FrameBuff_Ptr - 1] == Serial_Frame_End[2] &&Serial_FrameBuff[Serial_FrameBuff_Ptr - 2] == Serial_Frame_End[1] &&Serial_FrameBuff[Serial_FrameBuff_Ptr - 3] == Serial_Frame_End[0];}}if (PackHead && PackTail) {                         //找到完整包的时候,把两个32位数字提取出来memmove(&Serial_FrameBuff_Index, Serial_FrameBuff + 1, sizeof(Serial_FrameBuff_Index));memmove(&Serial_FrameBuff_Number, Serial_FrameBuff + 5, sizeof(Serial_FrameBuff_Number));//memset(Serial_FrameBuff + 9 + Serial_FrameBuff_Number, 0, 3);       //剔除包尾break;}delay(5);                                           //确保一帧数据完全达到串口}return PackHead && PackTail;
}void SerialFrame::Write(HardwareSerial* hwSerial, uint8_t cmd,uint32_t id,uint32_t val)
{hwSerial->write((uint8_t*)&Serial_Frame_Start, 1);hwSerial->write((uint8_t*)&cmd, 1);hwSerial->write((uint8_t*)&id, sizeof(id));hwSerial->write((uint8_t*)&val, sizeof(val));hwSerial->write((uint8_t*)Serial_Frame_End, sizeof(Serial_Frame_End));delay(10);
}void SerialFrame::Write(HardwareSerial* hwSerial, uint8_t cmd, uint32_t id, String str,uint32_t strlen)
{hwSerial->write((uint8_t*)&Serial_Frame_Start, 1);hwSerial->write((uint8_t*)&cmd, 1);hwSerial->write((uint8_t*)&id, sizeof(id));hwSerial->write((uint8_t*)&strlen, sizeof(strlen));hwSerial->print(str);hwSerial->write((uint8_t*)Serial_Frame_End, sizeof(Serial_Frame_End));delay(10);
}void SerialFrame::Write(HardwareSerial* hwSerial, String str)
{hwSerial->print(str);hwSerial->write((uint8_t*)Serial_Frame_End, sizeof(Serial_Frame_End));delay(10);
}uint8_t SerialFrame::GetFlag()
{return Serial_FrameBuff[0];
}uint32_t SerialFrame::GetIndex()
{return Serial_FrameBuff_Index;
}uint32_t SerialFrame::GetNumber()
{return Serial_FrameBuff_Number;
}String SerialFrame::GetString()
{String val = (char*)(Serial_FrameBuff + 9);val = val.substring(0, Serial_FrameBuff_Number);return val;
}void SerialFrame::PrintData()
{Serial.println("Frame Data:");for (int i = 0; i < Serial_FrameBuff_Ptr; i++) {Serial.print((char)Serial_FrameBuff[i]);}Serial.println();
}SerialFrame Frame;

这个类使用的代码和之前ESP的类似,所以你也需要进行类似的修改。在整个通讯过程中,每个包只有3、4个数据需要发送或接收,所以这里很容易编写这些读写函数需要注意的还是指针类型的转换。

2、TDS传感器

//
//
// #include "SensorTDS.h"void SensorTDSClass::init( uint8_t analogPin)
{Sensor_Turb_Read_Pin = analogPin;pinMode(Sensor_Turb_Read_Pin, INPUT);
}void SensorTDSClass::ReadTDS(float curTemp)
{//读取模块输出的模拟电压转换成参考电压5v,并从0~1024映射到0~5V://V=analogRead() * Sensor_Turb_Drive_Voltate/5.0f;//V=V/1024*5.0ffloat V = analogRead(Sensor_Turb_Read_Pin) * Sensor_Turb_Ref_Voltate / 1024.0f;//计算TDSfloat TDS = (133.42f * V * V * V - 255.86f * V * V + 857.39 * V) / 2;//计算温度矫正float TK = curTemp > 50.0f ? 1.0f : 1.0f + 0.02f * (curTemp - 25.0f);//应用矫正系数k和温度矫正Value = TDS * RatioK / TK;
}float SensorTDSClass::MathK(float sampleTemp)
{//计算参考TKfloat refTK = 1.0f + 0.02f * (refTemp - 25.0f);//把参考TDS换算成25℃下的值//TDSn/TKn=TDS25/TK25,TK25=1float refTDS = refTDSppm / refTK;//计算样本TKfloat samTK = 1.0f + 0.02f * (sampleTemp - 25.0f);//获取样本TDS(未校正)RatioK = 1.0f;ReadTDS(sampleTemp);//计算校正系数k//Value=TDS*RatioKRatioK = refTDS / Value;return RatioK;
}//纯净水=0;自来水=6~7;自然界淡水18;达到19说明盐分高于海水。
void SensorTDSClass::Read(float curTemp)
{//读取模块输出的模拟电压转换成参考电压5v,并从0~1024映射到0~5V://V=analogRead() * Sensor_Turb_Drive_Voltate/5.0f;//V=V/1024*5.0ffloat V = analogRead(Sensor_Turb_Read_Pin) * Sensor_Turb_Ref_Voltate / 1024.0f;//计算TDSfloat TDS = (133.42f * V * V * V - 255.86f * V * V + 857.39 * V) / 2;//计算温度矫正float TK = curTemp > 50.0f ? 1.0f : 1.0f + 0.02f * (curTemp - 25.0f);//应用矫正系数k和温度矫正Value = TDS * RatioK / TK;Value /= 12.0f;//规范化:if (Value > 100.0f) {Value = 100.0f;}if (Value < 0.0f) {Value = 0.0f;}
}SensorTDSClass SensorTDS;

这个类负责驱动TDS传感器,这个模块在我使用的时候发现很多问题,包括驱动的范例、供电使用脉冲或一直供电时读数相差甚远等等,所以没办法只好找来一些数据表然后参照原始的驱动代码进行了修改。

3、超声波传感器

//
//
// #include "SensorDist.h"void SensorDistClass::init(int pinTRIG, int pinECHO, int minDict, int maxDict)
{Sensor_Dict = new HCSR04(pinTRIG, pinECHO, minDict, maxDict);
}void SensorDistClass::Read(float temp)
{Value = Sensor_Dict->distanceInMillimeters() + Dist_Offset;delay(5);Value += Sensor_Dict->distanceInMillimeters() + Dist_Offset;delay(5);Value += Sensor_Dict->distanceInMillimeters() + Dist_Offset;Value /= 3.0f;if (Value < 0) {Value = 0;}
}SensorDistClass SensorDist;

这里非常简单粗暴的使用了平均值,直接读数是不太可靠的——我的超声波传感器太廉价了^ ^,更好的做法应该是用一个数组存储多次,然后进行软件滤波。这个传感器在近距离(5cm)时是无法正确读数的,所以安装时要距离水面一段距离,如果是底滤或者侧滤则应该安装在泵室上方,我做的上滤所以需要在底板开孔,为了防止鱼虾跳缸,用钢丝滤网遮蔽一下——这完全没有问题,不会影响传感器测定水面的高度。

4、颜色传感器

//
//
// #include "SensorColor.h"void SensorColorClass::begin(HardwareSerial* hwSerial)
{Serial_TCS34725 = hwSerial;Serial_TCS34725->begin(9600);while (!Serial_TCS34725){delay(1);}delay(100);Serial_TCS34725->write(TCS34725_Command_DefMode, 3);
}bool SensorColorClass::read()
{PackHead = false;                               //是否找到帧头   PackHead_Prt = 0;                               //包头校验指针Serial_FrameBuff_Ptr = 0;                       //缓存指针Serial_FrameBuff_Sum = result_Head_Sum;                       //校验和//循环读取串口数据,得到一个完整帧(缓存中只有r,g,b)while (Serial_TCS34725->available() > 0){//读1字节到缓存TCS34725_Result_Buff[Serial_FrameBuff_Ptr] = (uint8_t)Serial_TCS34725->read();if (!PackHead) {                                    //没找到包头时,一直写入缓存的0字节//Serial.print((char)Serial_FrameBuff[Serial_FrameBuff_Ptr-1]);     //输出其它信息if (TCS34725_Result_Head[PackHead_Prt] == TCS34725_Result_Buff[Serial_FrameBuff_Ptr]) {PackHead_Prt++;}else {PackHead_Prt == 0;}PackHead = PackHead_Prt == sizeof(TCS34725_Result_Head);        //头校验是否通过}else {          //找到包头的时候,从0字节往后写Serial_FrameBuff_Sum += TCS34725_Result_Buff[Serial_FrameBuff_Ptr++];if (Serial_FrameBuff_Ptr == 4) {Serial_FrameBuff_Sum -= TCS34725_Result_Buff[Serial_FrameBuff_Ptr - 1];Serial_FrameBuff_Sum &= 0xff;break;}}delay(5);                                           //确保一帧数据完全达到串口}//发送命令Serial_TCS34725->write(TCS34725_Command_ReadMcu, sizeof(TCS34725_Command_ReadMcu));//返回if (PackHead && (Serial_FrameBuff_Sum == TCS34725_Result_Buff[Serial_FrameBuff_Ptr - 1])) {r = TCS34725_Result_Buff[0];g = TCS34725_Result_Buff[1];b = TCS34725_Result_Buff[2];//灰度Value = (3.0 * r + 5.9 * g + 1.1 * b) / 100.0;return true;}return false;
}SensorColorClass SensorColor;

这个并没有实装,它的作用是代替浊度传感器,只需要安装一个LED和一片小镜片,就可以得到水的颜色偏差,但可以想象的是,这玩意比浊度传感器更不容易清理,也经常会呈现绿色、褐色…………

5、最后的部分:各种执行器的控制

如果是一个很简单的控制系统,那if就完了。但是作为一个较简单的控制系统,里面有各种传感器、执行器,if出来很low——不易于调试和修改。所以这里使用一种更好玩的技术——有限状态机。如果你不了解这个东西还想理解下面这个最简单的气泵控制过程是如何工作的,那只能先去看一下有限状态机的理论,然后看看Fsm这个库的范例了。

// ActuatorAirPump.h#ifndef _ACTUATORAIRPUMP_h
#define _ACTUATORAIRPUMP_h#if defined(ARDUINO) && ARDUINO >= 100#include "arduino.h"
#else#include "WProgram.h"
#endif#include <Fsm.h>
#include "ParameterConfig.h"namespace ActuatorAirPump {//气泵驱动Pin,配置文件void init(int8_t drivePin, ParameterConfigClass* config);//运行状态机void Run();//白天状态void On_AirPump_Sunlight_Enter();void On_AirPump_Sunlight_State();//晚上状态void On_AirPump_Moonlight_Enter();void On_AirPump_Moonlight_State();//引脚static uint8_t Actuator_Pin_AirPump ;//PWMstatic int Actuator_PWM_AirPump_Sunlight = 64;static int Actuator_PWM_AirPump_Moonlight = 72;//配置static ParameterConfigClass* Param_Config;//时段定义
#define SunRises 6.0
#define SunSets 18.0//白天状态static State State_AirPump_Sunlight(&On_AirPump_Sunlight_Enter, &On_AirPump_Sunlight_State, NULL);//夜晚状态static State State_AirPump_Moonlight(&On_AirPump_Moonlight_Enter, &On_AirPump_Moonlight_State, NULL);//状态机static Fsm Fsm_AirPump(&State_AirPump_Sunlight);}#endif
//
//
// #include "ActuatorAirPump.h"void ActuatorAirPump::init(int8_t drivePin, ParameterConfigClass* config)
{Actuator_Pin_AirPump = drivePin;Param_Config = config;pinMode(Actuator_Pin_AirPump, OUTPUT);Fsm_AirPump.add_transition(&State_AirPump_Sunlight, &State_AirPump_Moonlight, 1, NULL);Fsm_AirPump.add_transition(&State_AirPump_Moonlight, &State_AirPump_Sunlight, 0, NULL);
}void ActuatorAirPump::Run()
{Fsm_AirPump.run_machine();
}void ActuatorAirPump::On_AirPump_Sunlight_Enter()
{//执行analogWrite(Actuator_Pin_AirPump, Actuator_PWM_AirPump_Sunlight);
}void ActuatorAirPump::On_AirPump_Sunlight_State()
{//是否处于晚上时段(更高的PWM)if (Param_Config->GetHMIConfigByIndex(HMI_ID_SettingCurTimeHour) < SunRises || Param_Config->GetHMIConfigByIndex(HMI_ID_SettingCurTimeHour) > SunSets) {Fsm_AirPump.trigger(1);}else {Fsm_AirPump.trigger(0);}
}void ActuatorAirPump::On_AirPump_Moonlight_Enter()
{//执行analogWrite(Actuator_Pin_AirPump, Actuator_PWM_AirPump_Moonlight);
}void ActuatorAirPump::On_AirPump_Moonlight_State()
{//是否处于白天时段(较低的PWM)if (Param_Config->GetHMIConfigByIndex(HMI_ID_SettingCurTimeHour) >= SunRises && Param_Config->GetHMIConfigByIndex(HMI_ID_SettingCurTimeHour) <= SunSets) {Fsm_AirPump.trigger(0);}else {Fsm_AirPump.trigger(1);}
}

这个有限状态机的库需要定义一些状态,每个状态有进入、持续、结束几个函数被在对应时刻调用。所以在处理事物时,我们只需要定义状态切换条件和要做什么,而不必if它们——有限状态机是一个成熟的代码它知道自己该调用什么函数。当然,你可能觉得这么简单的东西,我if就完了,那你可能会在温度控制(加热、制冷)和上下水(排水、补水)的时候要多写一会。我的建议还是使用有限状态机这一技术来完成这样的工作。

【DIY】自动鱼缸控制系统——【二】相关推荐

  1. 利用 Amazon IoT Greengrass 在边缘 DIY 自动浇花系统

    曾经有这样一则新闻,一男子智能养鱼遇断网,4万余斤鱼或因缺氧死亡.这个塘主通过手机App监控鱼塘情况并利用智能插座控制增氧机进行增氧:但因遇到网络故障,无法及时为鱼塘启动增氧设备而造成重大经济损失.这 ...

  2. 第六十九篇:从ADAS到自动驾驶(二):ADAS的功能及发展

    作者:liaojiacai     邮箱: ljc_v2x_adas@foxmail.com 从ADAS到自动驾驶(二):ADAS的功能及发展          根据Wikipedia在线百科全书的定 ...

  3. 自己设计并制作了一个自动温度控制系统

    自己设计并制作了一个自动温度控制系统 一.课题任务 设计并制作一个水温自动控制系统,控制对象为1升净水,容器为搪瓷器皿.水温可以在一定范围内由人工设定,并能在环境温度降低时实现自动控制,以保持设定的温 ...

  4. EFQRCode:自动生成花式二维码

    原文链接:https://github.com/EyreFree/EFQRCode EFQRCode:自动生成花式二维码.# 为开源点赞# -- 由SwiftLanguage分享 EFQRCode i ...

  5. DL之RNN:基于TF利用RNN实现简单的序列数据类型(DIY序列数据集)的二分类(线性序列随机序列)

    DL之RNN:基于TF利用RNN实现简单的序列数据类型(DIY序列数据集)的二分类(线性序列&随机序列) 目录 序列数据类型&输出结果 设计思路 序列数据类型&输出结果 1.t ...

  6. JavaScript——易班优课YOOC课群在线测试自动答题解决方案(二十二)脚本更新3.1

    目录 Web安全--易班优课YOOC课群在线测试自动答题解决方案 更新日志 1.实现简单课程视频立刻完成: 2.集成禁止打开控制台解决方案: 3.修复因易班更新导致的一些问题: 4.添加自动刷题时答案 ...

  7. 基于51单片机的自动窗帘控制系统

    1. 简介 自动窗帘控制系统核心采用的是单片机STC89C52,其次利用了光照传感器.按键.显示屏以及信号调理电路等外围电路,使整个系统在各模块的配合下可实现半自动控制.自动控制.定时控制等功能.其主 ...

  8. 远程水箱自动检测控制系统

    通过远程水箱自动检测控制系统实现自来水水箱中的水位检测以及水泵的自动控制,而无需人员到现场检查.从而减少人工成本,提高工作效率.具体方案如下: 在水箱处,安装一个S130主机(A),并且通过水位探测器 ...

  9. 安捷伦or是德信号源+频谱仪操作:从程控到自动测试(二)增益计算的程控实现

    ** 安捷伦or是德信号源+频谱仪操作:从程控到自动测试(二)增益计算的程控实现 ** 一.概述 目前,对于射频模块的调试与测试,国内大多数的厂商(特别是中小型企业)均是通过最原始的手工调测方式,需要 ...

最新文章

  1. php基础知识手册,PHP基础知识(三)
  2. iOS 问题整理04----Runtime
  3. 数据库降级_阿里 双11 同款流控降级组件 Sentinel Go 正式GA,云原生服务稳稳稳...
  4. 李洋疯狂C语言之break和continue的区别
  5. c++ 一个函数包括多个返回值判断_Python函数的概念和使用
  6. ug10许可证错误一8_面对排污许可证后监督检查,企业应做好哪些准备?
  7. python 回归去掉共线性_线性回归中的多重共线性与岭回归
  8. 一次docker中的nginx进程响应慢问题定位记录
  9. 是谁让网管员的薪水如此低廉?
  10. Atitit html5 Canvas 如何自适应屏幕大小
  11. u8系统计算机上启动不了,用友erp u8装好后为何启动不了
  12. 程序员基础(自学)适合入门,大一
  13. Linux 服务器时区、时间校准,定时校准脚本
  14. 如何防止黑客攻击,保证服务器安全
  15. 【RocketMQ】发送事务消息
  16. 零基础玩转C语言系列第一章——初始C语言(上)
  17. mysql infile ignore_mysql导入数据load data infile用法
  18. Hierarchical Russian Roulette for Vertex Connections论文研读
  19. 神州电脑安装docker for Windows
  20. [阅读体会] 学习OpenCV 3 (Learning OpenCV 3)

热门文章

  1. 数据中台-实施篇:数据接入相关规范
  2. Windows软件打包工具
  3. 计算机里的公共汽车(总线)
  4. k型热电偶材料_k型热电偶补偿导线材质
  5. VNCTF2023-misc方向wp
  6. 最简单安装使用peda
  7. QBASIC在win8-64位系统中的编译及运行
  8. 使用百度AI实现视频的人流量统计(静态+动态)代码及效果演示
  9. Spring Boot 企业级开发课后题答案黑马程序员
  10. 计算机扫描的文件保存在哪,电脑教程:文件扫描后自动保存哪里去了