本文详尽地描述了一个基于阿里云的智能温度计项目,功能是上报温度数据到阿里云的物联网平台,所有的代码都是“物联网零妖”这位大佬的,我改了一部分,此文旨在加以解释。如果觉得我说的不好,可以看他的原视频:https://edu.aliyun.com/course/1492?spm=5176.10731491.0.0.ZXRrm4

摘要

  本系统利用51系列单片机为中心,通过ESP8266模块连接能上网的路由器,采集温湿度数据并上传阿里云,使用者可以使用手机APP或者在阿里云后台实时监控开发板所在环境温湿度。后续可以在云端对数据作分析并下发属性报文来控制其他设备对环境温度作调整,由于各种限制,本系统并未展开,目前仅可以上报温湿度数据,程序里写了相关的订阅报文,但是并没有实际设备执行。

第一章 绪论

  本系统主要功能是监测温湿度数据并上传阿里云。

第二章 系统需求分析

  本系统主要功能是监测温湿度数据并上传阿里云。细分一下,主要有以下需求:

  1. 获取环境温湿度
  2. 连接能上网的路由器
  3. 连接阿里云的MQTT服务器并上传属性报文。

第三章 系统软硬件设计与实现

  根据需求分析,这里也同样分三块作软硬件设计实现的说明:

(一)获取环境温度

1.硬件选择及线路连接:

  温湿度传感器采用DHT11:
  单片机采用STC15W4K56S4,封装为LQFP48;
  DHT11共四脚,VCC和GND不必多说,串行数据双向口SDA和单片机的IO口直连(26号管脚即P3.5/T1/T0CLKO/CCP0_2);SCL口(串行时钟)悬空。

2.软件部分程序编写

  相关代码为DHT11.c:
  首先定义DHT11串行数据管脚SDA:

#define	DHT11_IO	P35

  下面是两个延时函数,不作展开

static void Delay1us(unsigned int a)		//@22.1184MHz
static void Delay1ms(unsigned int a)		//@22.1184MHz

  通过查询DHT11文档可知,DHT12 支持 I2C 方式进行通讯,完全按照 I2C 标准协议编制。
dht12文档内容
  据此写以下函数

static unsigned char recive_byte(void)//一个字节接收函数
{
	unsigned char i,dat;
	dat = 0;
	for(i=0;i<8;i++)
	{
	while(DHT11_IO);			//等待DHT11拉低总线
	while(DHT11_IO==0); 		//等待DHT11拉高总线
	Delay1us(40);
	dat=dat<<1;
	if(DHT11_IO) 			//27us还是1则表示数据1
		{
			dat=dat+1;
		}
	}
	return dat;				//返回接收到的数据
}
void Read_DHT11(void)
{
	unsigned char Check_Res=0;	
	//发送起始信号,通知DHT11准备数据
	DHT11_IO = 0;
	Delay1ms(25);
	DHT11_IO = 1;
	while(DHT11_IO);			//等待DHT11拉低总线
	while(DHT11_IO==0);		//等待DHT11拉高总线
	//开始接收数据
	Humi_H = recive_byte();
	Humi_L = recive_byte();
	Temp_H = recive_byte();
	Temp_L = recive_byte();
	CheckSum = recive_byte();
	//接收DHT11输出的结束信号
	Delay1us(60);				//延迟后,将由DHT11拉高总线
	Check_Res = Humi_H+Humi_L+Temp_H+Temp_L;
	if(Check_Res != CheckSum)	//作校验,数据有误直接返回
	{
		return;
	}
}
void Read_DHT11_Str(unsigned char *Temp,unsigned char *Humi)
{
	//湿度两位数,温度一位小数
	Read_DHT11();			//读取DHT11数据到全局变量
	Humi[0] = Humi_H/10 + 0x30;
	Humi[1] = Humi_H%10 + 0x30;
	Humi[2] = 0;
	
	if((Temp_L&0X80) == 0x80)	 //温度低于0°
	{
		Temp_L &= 0X7F;
		Temp_L *= 10;
		Temp[0] = 0x2D;
		Temp[1] = Temp_H / 10 + 0x30;
		Temp[2] = Temp_H % 10 + 0x30;
		Temp[3] = 0x2E;
		Temp[4] = Temp_L / 10 + 0x30;
		Temp[5] = 0;
	}
	else						//温度高于0°
	{
		Temp_L *= 10;
		Temp[0] = 0x2B;
		Temp[1] = Temp_H / 10 + 0x30;
		Temp[2] = Temp_H % 10 + 0x30;
		Temp[3] = 0x2E;
		Temp[4] = Temp_L / 10 + 0x30;
		Temp[5] = 0;
	}
}

(二)连接能上网的路由器

1.硬件选择及线路连接

  此部分与原理图中硬件选择有所不同,由于各种原因导致短时间内无法使用原定的EWM3080模块,转而使用ESP8266模块。二者在AT指令部分略有不同。对方案的实现没有大的差别。主要使用的管脚也都是:VCC,GND,RXD,TXD这四个。原来的所有关于EWM3080的部分也暂作保留,使用了新的串口及初始化函数来和ESP8266连接。

  如图,由于原理图中一开始引出了串口3,4以及VCC和GND作调试用,因此这里选择串口3与ESP8266连接。RXD3和ESO8266的TX连接,TXD3和ESP8266的RX连接。
dht12文档内容
  串口1和USB转串口模块连接,用来下载程序以及载入部分数据到flash

2.软件部分程序编写

  软件程序部分主要涉及到串口3的初始化,中断处理,数据收发等函数;串口1的初始化等;CLI逻辑(下文具体解释);AT指令的发送

void Init_Uart2(void)		//串口3初始化
{
	S3CON = 0x10;		//8位数据,可变波特率
	S3CON &= 0xBF;		//串口3选择定时器2为波特率发生器
	AUXR |= 0x04;		//定时器2时钟为Fosc,即1T
	T2L = 0xD0;		//设定定时初值
	T2H = 0xFF;		//设定定时初值
	AUXR |= 0x10;		//启动定时器2
	IE2 = 0x08;    //中断使能
	Queue_Init(&WIFI_Read_Buf);
}

  串口初始化主要由STC官方给的工具输入系统频率和波特率计算而来;
dht12文档内容
  串口中断使能通过查阅单片机相关数据手册得知IE2寄存器的第四位是中断使能标志位,因此置IE2为0x08表示中断使能。
数据收发等基本操作不作赘述。
dht12文档内容
  下面主要说明连接路由器部分,之前提到了CLI逻辑,这是用上位机将一些会使用到的数据生成并载入到单片机的FLASH中,其中原理不再展开,与在程序中直接写数据效果一致。
  之前我们已经将串口3和ESP8266的串口相连了,接下来就是通过串口发送AT指令给ESP8266让其连接到路由器,并且与阿里云服务器建立TCP连接,进入透传模式发送报文。
  查询ESP8266指令集可知,大部分指令,如果ESP8266正确接收,就会返回一个“OK”,据此,我们写了如下代码来表示基础的发送AT指令,如果一定时间内没有收到返回的“OK”就重发。

//发送AT指令,等待回复的”OK”,否则会重发
void Send_AT(unsigned char *Str)
{
	unsigned char Dat=0;
	unsigned char Count=0;
	unsigned char Loop_Count=0;
	unsigned char ReSend_Count=0;
	WIFI_Send_Str(Str);		//通过串口发送指令,WIFI的串口和串口3直连
	while(1)					//等待回复的OK
	{
		Delay1ms(50);
		Loop_Count++;
		if(Loop_Count >= 100)
		{
			ReSend_Count++;
			if(ReSend_Count < 3)
			{
				Loop_Count = 0;
				Send_Str1("\r\n重发指令:");
				Send_Str1(Str);
				Send_Str1("\r\n");
				WIFI_Send_Str(Str);//重发一遍
			}
			else			//重发次数超过3,直接退出
			{
					Send_Str1("\r\n发送失败:");
					Send_Str1(Str);
					Send_Str1("\r\n");
				return;		//发送失败,退出
			}
		}
		if(Get_Byte_WIFI(&Dat))	//监测接收缓冲区是否是OK
		{
			if (Dat == 'O')
			{
				Send_Str1(Str);
				Send_Str1("\r\n");
				Delay1ms(20);
				Get_Byte_WIFI(&Dat);
				if(Dat == 'K')
				{
					Send_Str1("\r\n成功执行一条指令");
					Send_Str1(Str);
					Send_Str1("\r\n");
					return;
				}
			}
		}
	}	
}

  下面两个函数不是本系统重点,不作展开

//拼接两条字符串,把2添加到1的后面
void Sum_Str_Tail(unsigned char *str1,unsigned char *str2)
//拼接两条字符串,把2添加到1的钱前面
void Sum_Str_Header(unsigned char *str1,unsigned char *str2)

  下面的函数是建立TCP连接并进入透传模式,接下来所有发给串口3的数据将不会当作指令而是直接转给服务器。即下一步的发送报文步骤。也是参考AT指令集来写的:
dht12文档内容

//建立TCP连接,并进入透传模式 
void WiFi_Connect_Server(void){
	unsigned char DataBuf[256];
	unsigned int DataLen=0;
	Read_Flash_Message(MQTT_URL_Addr,DataBuf,&DataLen);
	Sum_Str_Header(DataBuf,"\"");
	Sum_Str_Tail(DataBuf,"\"");
	Sum_Str_Tail(DataBuf,",1883,360\r\n");//在后面加上
	Sum_Str_Header(DataBuf,"AT+CIPSTART=\"TCP\",");//在前面加上
	Send_AT(DataBuf);				//AT指令发出去
	Send_Str1("\r\n  prepareto get in transparent transmission \r\n");
	Send_AT("AT+CIPSEND\r\n");	//AT指令发出去
}
```   
&ensp;&ensp;下面是WIFI的初始化,一直到连接上路由器。由于程序最后会让模块进入透传模式,因此一开始要输入“+++”退出透传模式。
下面要连接路由器,需要WIFI的SSID和密码,这些应该是要提前在上位机中写入FLASH,如果没有写(即内容是00或者FF)则执行CLI逻辑,写入相关数据。  
``` c
//初始化WIFI包括设置SSID等参数
void Init_WIFI(void)
{
	unsigned char DataBuf[256];
	unsigned char DataBuf2[30];//给WIFI密码用
	unsigned int DataLen=0;
	unsigned int i=0;
	
	//退出透传模式
	Send_AT("+++");
	Delay1ms(2000);
	Send_AT("AT\r\n");
	Send_AT("AT\r\n");
	//set station mode
	Send_AT("AT+CWMODE=1\r\n");
			
	//查看FLASH内容,WIFI的SSID等内容,如果是00或者FF
//表示是第一次开机,要执行CLI逻辑写入数据
	DataLen = Read_One_Byte(SSID_Addr);
	DataLen <<= 8;
	DataLen += Read_One_Byte(SSID_Addr+1);
	if((DataLen == 0XFFFF)|(DataLen == 0))
	{
		Send_Str1("\r\n  设备第一次开机,请进行设置 \r\n");
		while(1)
		{
			CLI();//执行CLI逻辑
		}
	}
	/*
//调试用,重写WIFI的SSID,MQTT域名即报文等数据
	//please reload dat
		Send_Str1("\r\n reload data \r\n");
		while(1)
		{
			CLI();//Ö´ÐÐCLIÂß¼­
		}
	*/
	//连接WIFI到路由器,station模式
	Read_Flash_Message(SSID_Addr,DataBuf,&DataLen);
	Read_Flash_Message(WIFI_Pass_Addr,DataBuf2,&DataLen);
	Sum_Str_Header(DataBuf,"\"");
	Sum_Str_Tail(DataBuf,"\"");
	Sum_Str_Header(DataBuf,"AT+CWJAP=");
	Sum_Str_Tail(DataBuf,",");
	Sum_Str_Header(DataBuf2,"\"");
	Sum_Str_Tail(DataBuf2,"\"");
	Sum_Str_Tail(DataBuf,DataBuf2);
	Sum_Str_Tail(DataBuf,"\r\n");
	Send_AT(DataBuf);
	
	Send_Str1("\r\n  prepare to get IP address \r\n");
	Send_AT("AT+CIFSR\r\n");
	
	Send_Str1("\r\n  prepare to close transparent transsion \r\n");
	Send_AT("AT+CIPMODE=1\r\n");
	
	Send_Str1("\r\n  prepare to close multiple link \r\n");
	Send_AT("AT+CIPMUX=0\r\n");
	
	Send_Str1("\r\n  prepare to Open feedback \r\n");
	Send_AT("ATE1\r\n");

	//Clear_WIFI();
	
	//建立TCP连接,并进入透传模式
	WiFi_Connect_Server();
}

(三)连接阿里云的MQTT服务器并上传属性报文

1.MQTT域名生成即报文说明:

  这部分没有硬件的操作,和之前一样是WIFI模块的工作。需要提前做好的工作是把MQTT域名,ClientID,用户名,密码,属性上报以及下发属性的报文等写入FALSH。
dht12文档内容

  首先应该现在阿里云平台创建产品,未产品添加设备,获取设备的三元组信息,即图中:
ProductKey;DeviceName;DeviceSecret
dht12文档内容

  这是阿里云平台的MQTT协议产品文档,可知采用的是一机一密、一型一密预注册认证方式:使用设备证书(ProductKey、DeviceName和DeviceSecret)连接。根据如上规则生成预定的connect报文等,此时服务器就会知道是我的设备而不是其他设备,程序里已经提前把connect报文写在FLASH中,在程序中读取并发给WIFI的串口,因为之前已经进入与服务器建立了TCP连接并进入了透传模式,现在发送connect报文就会顺利与阿里云服务器建立mqtt连接,此时,如果连接成功就会返回20 02的报文表示已经建立连接,程序的编写逻辑也是如此。此外,mqtt也有心跳机制,如果长时间不发送心跳包(c0 00的报文),服务器就会与客户端断开连接。因此在程序里面也需要写一个函数上传心跳包以保活。
  最后是属性上报的报文,这一部分报文只有数据部分不同:比如下图是阿里云后台日志:
dht12文档内容

  属性上报报文上传的时候也就是上传这一部分,我们将其翻译成Hex如下:
dht12文档内容

  黄色的是数据,我们在程序中就是要把这一部分替换成实际的数据,而其他报文部分保留。

2.软件部分程序编写:

//发送connect报文,并等待服务器返回代码
void MQTT_Connect(void)
{
	unsigned char DataBuf[256];
	unsigned int DataLen=0;
	unsigned char Dat=0;
	unsigned int i=0;
	Delay1ms(200);
	Send_Str1("\r\n \r\n");
	//先发送断开连接,防止上次连接还在线
	/*
	for(i=0;i<2;i++)
	{
		WIFI_Send_Byte(MQTT_DisConnect[i]);
		//Delay1ms(1000);
		Send_Str1("\r\n now is sending disconnect command\r\n");
	}
	*/
	Delay1ms(500);
	Clear_WIFI();
//清空WIFI接受区,因为接下来要去接受区监测是否收到服务器的连接成功报文
	
	//链接MQTT服务器
	Read_Flash_Message(MQTT_Connect_Addr,DataBuf,&DataLen);//读取链接报文
	for(i=0;i<DataLen;i++)
	{
		WIFI_Send_Byte(DataBuf[i]);
	}
	Delay1ms(200);
	//等待服务器返回
	Delay1ms(1000);
	Dat = 0;
	while(Get_Byte_WIFI(&DataBuf[Dat++]));//获取缓冲区数据
	if((DataBuf[0]==0X20)&(DataBuf[1]==0X02))
	{
		Send_Str1("\r\n  链接MQTT服务器成功 \r\n");
		LED3 = 1;
//发送一个心跳保活(非必要,只是保险)
		WIFI_Send_Byte(0x0c);
		WIFI_Send_Byte(0x00);
	}
	else
	{
		Send_Str1("\r\n  链接MQTT服务器失败\r\n");
		Read_Flash_Message(MQTT_Connect_Addr,DataBuf,&DataLen);
//读取MQTT报文
		for(i=0;i<DataLen;i++)
		{
			WIFI_Send_Byte(DataBuf[i]);
		}
		Delay1ms(2);
	}
	
	//订阅属性 服务器设置设备属性 用来接收服务器下发的消息
	Read_Flash_Message(MQTT_Sub_Addr,DataBuf,&DataLen);//读取MQTT报文
	for(i=0;i<DataLen;i++)
	{
		WIFI_Send_Byte(DataBuf[i]);
	}
	
	//等待服务器返回
	Delay1ms(2000);
	Dat=0;
	while(Get_Byte_WIFI(&DataBuf[Dat++]));//获取接收缓冲区数据
	if((DataBuf[0]==0X90)&(DataBuf[1]==0X03))
	{
		Send_Str1("\r\n  订阅属性成功\r\n");
	}
	else
	{
		Send_Str1("\r\n  订阅属性失败\r\n");
		for(i=0;i<DataLen;i++)
		{
			WIFI_Send_Byte(DataBuf[i]);
		}
		Delay1ms(2000);
	}
}
//·发送心跳包 放到主循环中,定时器标志位到了就发送心跳
void Send_Heart(void)
{
	unsigned char MQTT_Heart[]={0xc0,0x00}; 
	if(MQTT_Heart_Count > 5000)//10ms定时器里面会自动+1
	{
		MQTT_Heart_Count = 0;
		WIFI_Send_Byte(MQTT_Heart[0]);
		WIFI_Send_Byte(MQTT_Heart[1]);
		Send_Str1("\r\n  ·发送一个心跳包到阿里云平台\r\n");
	}
}
//定时上报温度数据
void Pub_Temperature(void)
{
	unsigned char DataBuf[256];
	unsigned int DataLen=0,i=0;
	unsigned char Temperature_DHT11[7];//存放温度信息
	unsigned char Humi_DHT11[4];//存放湿度信息
	
	if(MQTT_PUB_Count > 700)
	{
		MQTT_PUB_Count = 0;
		
		Get_Temp(Temperature);
		Read_DHT11_Str(Temperature_DHT11,Humi_DHT11);
		
		Send_Str1("\r\n ADC_Temp=£º");
		Send_Str1(Temperature);
		Send_Str1(" DHT11_Temp=£º");
		Send_Str1(Temperature_DHT11);
		Send_Str1(" DHT11_Humi=£º");
		Send_Str1(Humi_DHT11);
		Send_Str1(" \r\n");
		
		LED4 = ~LED4;
		
		Read_Flash_Message(MQTT_Post_Addr,DataBuf,&DataLen);//属性上报报文
		Send_Str1(" \r\n be ready to pay attention \r\n");
		Delay1ms(2);
		for(i=0;i<132;i++)
//·发送前132个报文,下面4个字节是实际的数据,要去替换
{
			WIFI_Send_Byte(DataBuf[i]);
		}
		for(i=1;i<5;i++)//发送4个字节世界温度
		{
			WIFI_Send_Byte(Temperature_DHT11[i]);
		}
		//·发送12个自己固定部分报文
		for(i=0;i<12;i++)//再发送12个字节
		{
			WIFI_Send_Byte(DataBuf[136+i]);
		}
		for(i=0;i<2;i++)//·再发送2个字节的实际湿度报文
		{
			WIFI_Send_Byte(Humi_DHT11[i]);
		}
		
		//·发送剩下的报文
		for(i=150;i<DataLen;i++)
		{
			WIFI_Send_Byte(DataBuf[i]);
		}
	}
}

(四)程序整体逻辑

  下面过一遍main函数的逻辑:

void main(void)
{
	Init_IO();//初始化IO口,LED等
	Init_Uart1();//115200 初始化串口1
	Init_Uart2();//115200 初始化串口2(实际用的是3,函数里面也是按3写的)
	Init_Timer1();			//´打开定时器1,10ms定时器,发送心跳包等计时用
	EA = 1;				//´打开单片机全局中断
	Delay1ms(50);
	Send_Str1("\r\n  Hello This is ZengHui! \r\n");
	Send_Str1("\r\n  ALi-IOT LP Demo Based On 51-MCU. \r\n");
	Send_Str1("\r\n  CLI Starting... \r\n");
	Send_Str2("\r\n  ALi-IOT LP Demo Based On 51-MCU. \r\n");
	Init_WIFI();			//WIFI模块初始化,一直到建立TCP链接
	MQTT_Connect();		//链接MQTT服务器
	while(1)
	{
		Send_Heart();		//发送心跳包
		Pub_Temperature();//定时上报温度数据
	}
}

第四章 系统调试与测试

(一)MQTT报文的准确性

  首先应该测试MQTT报文的准确性,这里用MQTTfx这个软件模拟设备去链接阿里云的服务器,发送相关属性上报的报文,或者在阿里云调试端下发相关属性下发,都能正常收到;

(二)本地温湿度数据的正确接收

ADC_Temp=:-50.0 DHT11_Temp=??25.6 DHT11_Humi=:65 
be ready to pay attention 
ADC_Temp=:-50.0 DHT11_Temp=:+25.6 DHT11_Humi=:65 
be ready to pay attention 
ADC_Temp=:-50.0 DHT11_Temp=??25.6 DHT11_Humi=:65 
be ready to pay attention 
ADC_Temp=:-50.0 DHT11_Temp=:+25.6 DHT11_Humi=:65 
be ready to pay attention 
ADC_Temp=:-50.0 DHT11_Temp=:+25.7 DHT11_Humi=:65 
be ready to pay attention 
ADC_Temp=:-50.0 DHT11_Temp=:+25.7 DHT11_Humi=:64 
be ready to pay attention 

  以上是串口1的反馈,可以看到除了部分扰动的数据,其他数据(DHT11的)均正常

(三)路由器的正常连接

  路由器我是用手机热点来代替的,以下是串口反馈:

AT+CWJAP="rabbit","88488848"
成功执行一条指令: AT+CWJAP="rabbit","88488848"  

  此时我的手机上也会有连接设备8266。

(四)阿里云服务器连接。

  这个部分失败了很多次,表现为连上但是只有一瞬间,查看阿里云日志发现只有online和offline的日志,且下线是非正常下线,错误码1911,不是1910,1910是由于心跳保活机制,1911是TCP链接断开。一般情况下1910才是更容易出现的错误。然后我手动另外接了一个串口与WIFI的串口直连,手动发送数据,发现没有问题,可以正常连接阿里云的物联网服务器,发送心跳包C0 00也可以收到回复D0 00。因此判定程序逻辑没有问题,后经过排查,发现有两个问题,一是我一开始在MQTT链接函数里一开始会发送E0 00去断开之前可能存在的链接,但是似乎一般情况下不去断开也没关系,反而断开会导致与TCP链接的断开。另一个问题我是发目标服务器换成本机,在本机上用网络调试助手建立一个TCP的服务器,连接之后发现程序跑的时候实际发送的报文会和预想的不一样,因此又作调整。最终链接成功。如下图,在阿里云物联网平台可以看到设备在线,并且在物模型中可以看到实时的数据以及记录。

图片描述
图片描述