嵌入式开发—天气时钟

从大学期间就一直想做一个天气时钟,大四时看到学长的一个用ESP8266开发的天气时钟,更是让我想做一个属于自己的天气时钟。当时买了ESP32开发板,但是由于需要用到Arduion,个人实在是不了解,当时在装好开发环境烧写了学长的程序发现跑不通之后就放弃了。当时甚至买了一个棱镜,准备做一个放棱镜的。

现在摇身一变成了一名社畜,工作之余也尝试学习了微信小程序,最近的两个项目中又开始用起了STM32系列的芯片,所以干脆自己又买了一个开发板和屏幕,再次开始自己天气时钟的尝试。

1 概述

1.1 系统组成

系统主要三大部分,ESP8266 WIFI模块、RTC和GUI页面。比较花时间的是第一部分和第三部分。第一部分整整花了两天半的时间,整个小项目花了五天。当然这是因为博主是之前没有接触过ESP8266模块的菜鸟,接触过的应该不会花那么久。

ESP8266 WIFI模块的主要任务是去网上获取需要的天气现象、温度、湿度以及实时时间。获取天气现象和气温访问的是心知天气API。湿度信息访问的是高德API。实时时间访问的是苏宁实时时间API。温度信息和湿度信息分开访问主要是因为心知天气API如果需要获取湿度信息的话需要花钱,但是刚好本人之前开发微信小程序用的高德API可以返回湿度信息,所以干脆也用到了这里。时间的话个人觉得苏宁实时时间API还是非常好用的,可以直接返回北京时间和年月日。

RTC是为了在开机获取到实时时间之后能够保证后续的时间准确,个人测试了一下,运行了一晚上,基本是没有时间差距的,不过测试的时间还是短了,后续可以再慢慢发现问题。

GUI还是选择了经典的旋转太空人,此外有对应的天气图标、WIFI图标、温湿度图标、时间字体颜色等。做这一部分的时候虽然不难,但是还是比较花时间和精力的,图标为了好看,本人去阿里巴巴矢量图库找了一些图标,自己设计了颜色,整体流程后面会详细介绍。

1.2 硬件

用到的硬件也比较简单,淘宝店里直接买的普中STM32F103准端-Z100的板子和配套的电容屏。ESP8266买的正点原子的。这里链接就不单独贴出来了,有需要的友友可以私信。

1.3 实现效果

这里只展示一下简陋的开机页面和开机后的页面。
开机页面

开机页面会显示当前正在执行哪一步,如果成功会显示当前步骤OK。

开机后页面

2 ESP8266 WIFI模块开发

2.1 常用AT指令

这里只介绍一下用到的几个常用的AT指令,当然由于博主买来的正点原子的ESP8266已经有了AT指令的固件库,没有的小伙伴需要自己去烧固件,这里就不再单独介绍。

这里给出的格式是单片机串口发送给ESP8266的格式

  • AT\r\n
    检查ESP8266模块连接是否正常

  • AT+CWMODE=1\r\n
    配置模块为Sta模式

  • AT+CWJAP=“WIFI名称”,“WIFI密码”\r\n
    连接指定WIFI

  • AT+CIPMUX=0\r\n
    设置成单连接

  • AT+CIPMODE=1\r\n
    开启透传模式

  • AT+CIPSTART=“TCP”,“203.119.175.194”,80\r\n
    创建TCP连接
    如何获取IP地址,后面会有介绍

  • AT+CIPMODE=1\r\n
    进入透传模式
    进入到透传模式后AT指令就会失效,需要退出后才能生效

  • AT+CIPSEND\r\n
    准备向服务器发送请求,前面都成功的前提下发送完这个指令后会出现一个>,此时输入GET信息即可

2.2 访问API流程

首先按照2.1介绍的步骤连接上WIFI,然后开始下面的操作。

2.2.1 获取IP地址

以访问心知天气API为例,介绍一下获取IP的方法。
电脑win+R,输入ping api.seniverse.com,点击确定就可以获取到IP
获取IP
如果是高德API的话可以Ping restapi.amap.com。苏宁实时时间API可以Ping quan.suning.com。

获取完IP后建立TCP连接,可以看2.1的叙述。

2.2.2 GET 信息

建立完TCP连接后按照2.1的叙述配置到发送GET信息的步骤,还是以获取心知天气API信息为例,在出现>后发送以下信息

GET https://api.seniverse.com/v3/weather/now.json?key=your_key&location=qingdao&language=zh-Hans&unit=c\r\n

其中your_key是你申请的密钥。关于密钥怎么获取,可以去看看其他博主的关于心知天气API使用的博客,这里也不再做介绍。

2.3 返回信息解析

使用ESP8266时单片机不仅需要发送信息给ESP8266,还需要解析ESP8266返回的信息,用来判断需要配置的步骤是否配置成功。这里博主的解析方法可以说是非常的原始。总体思想就是寻找返回信息中的关键字符,比如某条AT指令里面OK是代表指令配置成功,那么就在接收到回复消息后看回复消息的特定位置是否有OK字符。能使用这种方法的前提是每次ESP8266返回的正确信息是定长的,OK是在固定位置出现。对于不定长的信息,博主利用for循环去查找关键字符,然后提取所需要的信息。

2.4 指令重发

当单片机检测到回复信息不是正常信息时,认为指令并没有配置成功,会间隔一段时间再次发送指令,直到收到正确返回信息。

2.5 ESP8266程序设计

1. ESP8266初始化

首先是ESP8266初始化,实际初始化是访问API获取相应信息。访问的顺序是高德API获取湿度信息,苏宁实时时间API获取实时时间,心知天气API获取天气现象代码和实时气温。

u8 gAtComCount = 0;   // AT指令发送次数
u32 gWaitTime = 0;   // AT指令发送等待时间计数变量
u8 gGetFlag = 0;   // 初始化时控制请求的API

void Esp8266_AT_Init(void)   // ESP8266初始化
{
	// AT指令检查ESP8266连接情况
		while (!Uart1_Rece_Parse())
	{
		gAtComCount = 1;
		delay_us(100);
		gWaitTime = gWaitTime + 1;
		
		if (gWaitTime >= 10000)
		{
			printf ("AT\r\n");   // 发送AT查询连接状况
			gWaitTime = 0;
		}
	}
	
    gWaitTime = 0;
	// 配置模块为sta模式
	while (!Uart1_Rece_Parse())
	{
		gAtComCount = 2;
		delay_us(100);
		gWaitTime = gWaitTime + 1;
		
		if (gWaitTime >= 10000)
		{
			printf ("AT+CWMODE=1\r\n");   // 配置模块为sta模式
			gWaitTime = 0;
		}
	}
	
    gWaitTime = 0;
	// 连接到指定的路由器
	while (!Uart1_Rece_Parse())
	{
		gAtComCount = 3;
		delay_us(100);
		gWaitTime = gWaitTime + 1;
		
		if (gWaitTime >= 10000)
		{
			printf ("AT+CWJAP=\"ertu\",\"ertu201801101102\"\r\n");   // 连接到指定的路由器
			gWaitTime = 0;
		}
	}
	
	gWaitTime = 0;
	// 设置单连接
	while (!Uart1_Rece_Parse())
	{
		gAtComCount = 4;
		delay_us(100);
		gWaitTime = gWaitTime + 1;
		
		if (gWaitTime >= 10000)
		{
			printf ("AT+CIPMUX=0\r\n");   // 设置单连接
			gWaitTime = 0;
		}
	}
	
	gWaitTime = 0;
	// 开启透传模式
	while (!Uart1_Rece_Parse())
	{
		gAtComCount = 5;
		delay_us(100);
		gWaitTime = gWaitTime + 1;
		
		if (gWaitTime >= 10000)
		{
			printf ("AT+CIPMODE=1\r\n");   // 开启透传模式
			gWaitTime = 0;
		}
	}
	
	gWaitTime = 0;
	if (gGetFlag == 0)   // 访问高德API
	{
		// 创建TCP连接
		while (!Uart1_Rece_Parse())
		{
			gAtComCount = 6;
			delay_us(100);
			gWaitTime = 0;
			}
		}
		
		gWaitTime = 0;
		// 进入透传模式
		while (!Uart1_Rece_Parse())
		{
			gAtComCount = 7;
			delay_us(100);
			gWaitTime = gWaitTime + 1;
			
			if (gWaitTime >= 10000)
			{
				printf ("AT+CIPMODE=1\r\n");   // 进入透传模式
				gWaitTime = 0;
			}
		}
		
		gWaitTime = 0;
		// 准备向服务器发送请求
		while (!Uart1_Rece_Parse())
		{
			gAtComCount = 8;
			delay_us(100);
			gWaitTime = gWaitTime + 1;
						
			if (gWaitTime >= 10000)
			{
				printf ("AT+CIPSEND\r\n");   // 准备向服务器发送请求
				gWaitTime = 0;
			}
		}
		
		gWaitTime = 0;
		// 向服务器请求湿度信息
		while (!Uart1_Rece_Parse())
		{
			gAtComCount = 9;
			delay_us(100);
			gWaitTime = gWaitTime + 1;
		
			if (gWaitTime >= 10000)
			{
				printf ("GET https://restapi.amap.com/v3/weather/weatherInfo?city=370200&key=your_key\r\n");	

		gWaitTime = 0;
		// 退出透传模式
		while (!Uart1_Rece_Parse())
		{
			gAtComCount = 10;
			delay_us(100);
			gWaitTime = gWaitTime + 1;
		
			if (gWaitTime >= 10000)
			{
				printf ("+++");   // 退出透传模式
				gWaitTime = 0;
			}
		}
		
		gWaitTime = 0;
		// ESP8266复位
		while (!Uart1_Rece_Parse())
		{
			gAtComCount = 11;
			delay_us(100);
			gWaitTime = gWaitTime + 1;
		
			if (gWaitTime >= 10000)
			{
				printf ("AT+RST\r\n");   // ESP8266复位
				gWaitTime = 0;
			}
		}
		
		delay_ms(1000);   // 必要,猜测是等待RST信息发送完毕,防止其覆盖创建TCP请求时的接收信息
		
		gGetFlag = 1;   // 访问苏宁实时时间API

	stepSixOkFlag = 0;   // 串口解析时用到的标志位
	gWaitTime = 0;
	if (gGetFlag == 1)   // 访问苏宁实时时间API
	{
		// 创建TCP连接
		
		while (!Uart1_Rece_Parse())
		{
			gAtComCount = 6;
			delay_us(100);
			gWaitTime = gWaitTime + 1;
			
			if (gWaitTime >= 10000)
			{
				printf ("AT+CIPSTART=\"TCP\",\"120.220.191.55\",80\r\n");   // 创建TCP连接
				gWaitTime = 0;
			}
		}
		
		gWaitTime = 0;
		// 进入透传模式
		while (!Uart1_Rece_Parse())
		{
			gAtComCount = 7;
			delay_us(100);
			gWaitTime = gWaitTime + 1;
			
			if (gWaitTime >= 10000)
			{
				printf ("AT+CIPMODE=1\r\n");   // 进入透传模式
				gWaitTime = 0;
			}
		}
		
		gWaitTime = 0;
		// 准备向服务器发送请求
		while (!Uart1_Rece_Parse())
		{
			gAtComCount = 8;
			delay_us(100);
			gWaitTime = gWaitTime + 1;
						
			if (gWaitTime >= 10000)
			{
				printf ("AT+CIPSEND\r\n");   // 准备向服务器发送请求
				gWaitTime = 0;
			}
		}
			
		gWaitTime = 0;
		// 向服务器请求实时时间
		while (!Uart1_Rece_Parse())
		{
			gAtComCount = 9;
			delay_us(100);
			gWaitTime = gWaitTime + 1;
		
			if (gWaitTime >= 10000)
			{
				printf ("GET http://quan.suning.com/getSysTime.do HTTP/1.1\r\nHost: quan.suning.com\r\n\r\n");   // 向服务器请求实时时间
				
		gWaitTime = 0;
		// ESP8266复位
		while (!Uart1_Rece_Parse())
		{
			gAtComCount = 11;
			delay_us(100);
			gWaitTime = gWaitTime + 1;
		
			if (gWaitTime >= 10000)
			{
				printf ("AT+RST\r\n");   // ESP8266复位
				gWaitTime = 0;
			}
		}
		
		delay_ms(1000);   // 必要,猜测是等待RST信息发送完毕,防止其覆盖创建TCP请求时的接收信息
		
		gGetFlag = 2;   // 访问心知天气API
		
	stepSixOkFlag = 0;
	gWaitTime = 0;
	if (gGetFlag == 2)   // 访问心知天气API
	{
		// 创建TCP连接
		while (!Uart1_Rece_Parse())
		{
			gAtComCount = 6;
			delay_us(100);
			gWaitTime = gWaitTime + 1;
			
			if (gWaitTime >= 10000)
			{
			    printf ("AT+CIPSTART=\"TCP\",\"116.62.81.138\",80\r\n");   // 创建TCP连接
				gWaitTime = 0;
			}
		}
				
		gWaitTime = 0;
		// 进入透传模式
		while (!Uart1_Rece_Parse())
		{
			gAtComCount = 7;
			delay_us(100);
			gWaitTime = gWaitTime + 1;
			
			if (gWaitTime >= 10000)
			{
				printf ("AT+CIPMODE=1\r\n");   // 进入透传模式
				gWaitTime = 0;
			}
		}
		
		gWaitTime = 0;
		// 准备向服务器发送请求
		while (!Uart1_Rece_Parse())
		{
			gAtComCount = 8;
			delay_us(100);
			gWaitTime = gWaitTime + 1;
			
			if (gWaitTime >= 10000)
			{
				printf ("AT+CIPSEND\r\n");   // 准备向服务器发送请求
				gWaitTime = 0;
			}
		}
		gWaitTime = 0;
		// 向服务器请求实时天气信息
		while (!Uart1_Rece_Parse())
		{
			gAtComCount = 9;
			delay_us(100);
			gWaitTime = gWaitTime + 1;
		
		    if (gWaitTime >= 10000)
			{
				printf ("GET https://api.seniverse.com/v3/weather/now.json?key=your_key&location=qingdao&language=zh-Hans&unit=c\r\n");   // 向服务器请求实时天气信息
				gWaitTime = 0;
			}
		}
		
	  gWaitTime = 0;
	  gOpenFlag = 1;
	}
}

2. ESP8266返回信息解析

返回信息解析放在了串口的.c文件中,如上面所说,信息解析主要是两种方法,一种是在固定位置寻找“OK”字符,另一种是for循环在整个接收信息中查找固定字符。

这里只分开介绍一下这两种方法的程序设计。

首先是在固定位置查找“OK”字符。值得注意的是,在发送完AT指令后ESP8266并不是直接回复OK或者ERROR,而是会先重复一遍单片机发送的AT指令,再加一个回车换行,然后才是OK或者ERROR。以发送AT指令为例,如果要查询“OK”,需要在第6位和第7位查询。

    // 如果指令回复为OK
    // 第一条
	if (receFifo[6] == 79 && receFifo[7] == 75 && gAtComCount == 1)
	{
		sucFlag = 1;
	}

对于API返回的天气信息,比较长,或者对于一些不定长的返回信息,这些不太方便用在固定位置查找固定字符的方法来判断是否回复信息正确,所以博主采用了for循环的方法,利用for循环在接收字符串中寻找需要的信息,找到后直接break,节省时间。这里以解析心知天气API返回的天气信息为例介绍一下程序设计。

	// 解析心知天气API信息
	if (gGetFlag == 2)
	{
		for (getWeatherCodeIndex = 0;getWeatherCodeIndex < 1000;getWeatherCodeIndex ++)
		{
			// 查询天气现象编码
			if (receFifo[getWeatherCodeIndex] == 99 && receFifo[getWeatherCodeIndex + 1] == 111
				&& receFifo[getWeatherCodeIndex + 2] == 100 && receFifo[getWeatherCodeIndex + 3] == 101)
			{
				getWeatherCodeIndex = getWeatherCodeIndex + 7;
				getTemperIndex = getWeatherCodeIndex + + 18;
						
				// 天气现象编码只有一位数
				gWeatherCode = receFifo[getWeatherCodeIndex] - 48;   // 获取天气现象代码
										// 天气现象编码有两位数
				if (48 <= receFifo[getWeatherCodeIndex + 1] && receFifo[getWeatherCodeIndex + 1] <= 57)
				{
					gWeatherCode = gWeatherCode * 10;
					gWeatherCode = gWeatherCode + receFifo[getWeatherCodeIndex + 1] - 48;
							
					// 对于一些特殊的天气现象,直接按照晴显示
					if (gWeatherCode > 25)
					{
						gWeatherCode = 1;
					}
					getTemperIndex = getTemperIndex + 1;   // 气温索引往后移一位
				}
						
				// 气温只有一位数
				gTemper = receFifo[getTemperIndex] - 48;   // 获取实时气温
	
				// 气温有两位数
				if (48 <= receFifo[getTemperIndex + 1] && receFifo[getTemperIndex + 1] <= 57)
				{
					gTemper = gTemper * 10;
					
					gTemper = gTemper + receFifo[getTemperIndex + 1] - 48;
				}
						
				sucFlag = 1;	
				
				break;
			}
		}
	}						

这里的气象代码是心知天气给出的,不同的数字代表不同的天气现象,需要的友友可以移步心知天气官网自行查看。

3. 指令重发

对于没有收到正确返回信息,需要重发的情况也比较简单。比如创建TCP连接时可能一次不成功,就在固定时间间隔不断请求,直到收到正确的回复信息即可。

    while (!stepSixOkFlag)
    {
    	// 高德API
		if (gGetFlag == 0)
		{
			gTimeCount = gTimeCount + 1;
					
			if (gTimeCount > 200)
			{
				printf ("AT+CIPSTART=\"TCP\",\"203.119.175.194\",80\r\n");   // 创建TCP连接
				gTimeCount = 0;
			}
					
			for (getTcpOkIndex = 0;getTcpOkIndex < 1000;getTcpOkIndex ++)
			{
				if (receFifo[getTcpOkIndex] == 79 && receFifo[getTcpOkIndex + 1] == 75)
				{
					stepSixOkFlag = 1;
				}
			}	
	    }		
    }

4. 串口1接收中断服务函数

相信对于使用过STM32的友友们对于串口接收中断服务函数应该并不陌生,好多例程程序里都有。但是这个环节博主却是花了很多时间。主要问题是博主想要做到接收不定长的字符串,而且可以对接收到的字符串进行解析。正点或者普中给出的例程里时使用一个变量r接收的信息,并不能对接收到的内容进行解析。但是当博主像下面这样写的时候,接收到的内容会丢失。

void USART1_IRQHandler(void)  
{
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)   //接收到一个字节  
	{
		receFifo[receCount++] = USART_ReceiveData(USART1);
	}
	USART_ClearITPendingBit(USART1,USART_IT_RXNE);  //清除RXNE标志位
	
    receEndFlag = 1;   // 接收完成标志置1 
}

直到后续看到一个博主的文章才解决这个问题,这里贴出大佬文章连接,十分感谢!STM32串口接收一帧数据

这是博主最后的实现程序,不会出现接收数据丢失的情况。

void USART1_IRQHandler(void)  
{
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)   //接收到一个字节  
	{
		receFifo[receCount++] = USART_ReceiveData(USART1);
	}
	else if(USART_GetITStatus(USART1,USART_IT_IDLE) != RESET)   //接收到一帧数据
    {
		USART1->SR;//先读SR
		USART1->DR;//再读DR
		
		receEndFlag = 1;   // 接收完成标志置1 
    }  
}

3 RTC实时时钟

在网络查询到时间后,剩下的时间依靠RTC来计算显示。但是由于从请求到网络实时时间到实际RTC开始工作之间是存在时间差的,因此在给RTC写入初始时间日期时需要进行时间校正。

RTC实时时钟的程序这里就不单独描述了,可以直接拿正点原子的例程,这里着重讲一下时间校准。

时间校准并不是简单的差几秒就加几秒。例如获取到网络时间到给RTC写入实时时间之间的间隔为14秒,此时肯定是需要把这14秒加上的,但是加秒数时需要考虑加完14秒后是否满一分钟,如果满了一分钟还需要给分钟数加1。当给分钟数加1时又需要考虑加完这一分钟是不是满一小时,如果满1小时还需要给小时数加1。当给小时数加1时又需要考虑加上这一小时是不是满一天,如果满一天的话还需要给天数加1。给天数加1时需要考虑是不是2月份,是不是闰年,本月是30天还是31天。好在最后年份不需要判别,直接加1即可。

3.1 时间校正函数

对于RTC实时时钟,值得介绍的是时间校正函数的设计,这里直接贴出代码,有兴趣的友友可以对照上面的介绍看一下思路。

博主对于时间校正的程序设计十分简单粗暴,在真正的产品程序设计中十分不建议使用许多条件比较严格的if判断,一旦有一种情况没有考虑到,很容易导致产品出现问题,而且对于后期维护十分不利,这里贴出来只是觉得思路比较有意思

void RTC_Time_Correct(void)   // 开机时间校正
{
	// 加14秒不满1分钟
	if (gTimeSec < 46)
	{
		gTimeSec = gTimeSec + 14;
	}
	// 加14秒满1分钟
	else if (gTimeSec >= 46)
	{
		gTimeSec = gTimeSec + 14 - 60;
		
		// 分钟数需要加1
		// 加1分钟不满1小时
		if (gTimeMin < 59)
		{
			gTimeMin = gTimeMin + 1;
		}
		// 分钟数加1满1小时
		else if (gTimeMin == 59)
		{
			gTimeMin = 0;
			
			// 小时数需要加1
			// 加1小时不满1天
			if (gTimeHour < 23)
			{
				gTimeHour = gTimeHour + 1;
			}
			// 加1小时满1天
			else if (gTimeHour == 23)
			{
				gTimeHour = 0;
				// 天数需要加1
				// 天数小于28直接加1
				if (gTimeDay < 28)
				{
					gTimeDay = gTimeDay + 1;
				}
				// 天数等于28
				else if (gTimeDay == 28)
				{
					// 当前为二月
					if (gTimeMon == 2)
					{
						// 闰年
						if (Is_Leap_Year(gTimeYear))
						{
							gTimeDay = gTimeDay + 1;
						}
						// 当前为2月且不是闰年
						else
						{
							gTimeDay = 0;   // 天数置零
							gTimeMon = gTimeMon + 1;   // 月份加1
						}
					}
				}
				// 天数等于30
				else if (gTimeDay == 30)
				{
					// 当前月份只有30天
					if (gTimeMon == 2 || gTimeMon == 4 || gTimeMon == 6 || gTimeMon == 9
						  || gTimeMon == 11)
					{
						gTimeDay = 0;
						gTimeMon = gTimeMon + 1;
					}
					// 当前月份有31天
					else
					{
						gTimeDay = gTimeDay + 1;
					}
				}
				// 天数等于31
				else if (gTimeDay == 31)
				{
					gTimeDay = 0;
					
					// 加1月不满1年
					if (gTimeMon != 12)
					{
						gTimeMon = gTimeMon + 1;
					}
					else
					{
						gTimeMon = 1;
						gTimeYear = gTimeYear + 1;
					}
				}
			}
		}
	}
}

4 GUI页面

GUI页面设计主要有旋转太空人、温湿度图标、天气图标和时间数字。前三个还是比较省时间的,第四个时间图标,博主为了追求好看,每一个数字都找了单独的图片,而且取模的大小还不一样,分钟数字的颜色也不一样,所以整体这部分来说不算难点,但是还是挺花时间的。

4.1 图片取模显示

这部分其实比较简单,只是比较花时间。但是在做这一部分的时候也遇到了一些比较有意思的事情。这里用到的所有图标都是从阿里巴巴矢量图库找的。比较有意思的事情是博主选择的LCD底色是黑色,而实际下载来的图标都是白底图标黑色内容。针对白底黑字的图标比较好办,在使用取模软件取模是可以直接勾选颜色反转,就可以直接得到黑底白字的图标。

取模软件使用

但是对于下载来的彩色图标就不能简单地直接颜色反转取模。于是博主直接利用一个颜色反转的网站,输入自己想要的颜色,网站就可以直接告诉你该颜色反转后的颜色。更加幸运的是阿里巴巴矢量图库支持对下载的图片自定义颜色。因此只需要在下载图标时将图标修改成想要颜色的反转色,再利用取模软件取模时勾选颜色反转,再将颜色反转回来,这样就得到了想要颜色的黑底的图标。颜色反转地网站以及如何使用阿里巴巴矢量图库设计自己想要颜色的图标,如果有需要的友友可以私信讨论。

这里贴一下显示图片的函数。

void Gui_Drawbmp16_Icon(u16 x,u16 y,const unsigned char *p) //显示40*40 QQ图片
{
  	int i; 
	unsigned char picH,picL; 
	LCD_SetWindows(x,y,x+40-1,y+40-1);//窗口设置
    for(i=0;i<40*40;i++)
	{	
	 	picL=*(p+i*2);	//数据低位在前
		picH=*(p+i*2+1);				
		LCD_WR_DATA(picH<<8|picL);  						
	}	
	LCD_SetWindows(0,0,lcddev.width-1,lcddev.height-1);//恢复显示窗口为全屏	
}

void LCD_SetWindows(u16 xStar, u16 yStar,u16 xEnd,u16 yEnd)
{	
	LCD_WR_REG(lcddev.setxcmd);	
	LCD_WR_DATA(xStar>>8);
	LCD_WR_DATA(0x00FF&xStar);		
	LCD_WR_DATA(xEnd>>8);
	LCD_WR_DATA(0x00FF&xEnd);

	LCD_WR_REG(lcddev.setycmd);	
	LCD_WR_DATA(yStar>>8);
	LCD_WR_DATA(0x00FF&yStar);		
	LCD_WR_DATA(yEnd>>8);
	LCD_WR_DATA(0x00FF&yEnd);	

	LCD_WriteRAM_Prepare();	//开始写入GRAM				
}  

4.2 旋转太空人

对于旋转太空人的设计非常简单,只需要将太空人的gift分帧之后挨个取模,固定时间间隔不断循环显示即可达到动态旋转的效果,这里贴一下旋转太空人的gift,大家可以自行分帧取模。

旋转太空人动图

4.3 天气图标、时间数字显示

本设计中天气图标是能够根据获取到的天气现象代码实时变化的,时间的每一个数字也都是显示的图片。以小时为例,这里就简单贴一下博主拙劣的显示时间的函数。

void Lcd_Show_Time_HourNum_Ctrl(u16 hour)   // 小时显示数字控制
{
	// 小时第一位显示
	if (hour < 10)
	{
		Gui_Drawbmp16_40x40(112,140,gImage_whitenum0);   // 小时第一位
	}
	else if (10 <= hour && hour < 20)
	{
		Gui_Drawbmp16_40x40(112,140,gImage_whitenum1);   // 小时第一位
		hour = hour % 10;
	}
	else
	{
		Gui_Drawbmp16_40x40(112,140,gImage_whitenum2);   // 小时第一位
		hour = hour % 10;
	}

	// 小时第二位显示
	switch (hour)
	{
		case 0:
			Gui_Drawbmp16_40x40(144,140,gImage_whitenum0);   // 小时第二位
		break;

		case 1:
			Gui_Drawbmp16_40x40(144,140,gImage_whitenum1);   // 小时第二位
		break;

		case 2:
			Gui_Drawbmp16_40x40(144,140,gImage_whitenum2);   // 小时第二位
		break;

		case 3:
			Gui_Drawbmp16_40x40(144,140,gImage_whitenum3);   // 小时第二位
		break;

		case 4:
			Gui_Drawbmp16_40x40(144,140,gImage_whitenum4);   // 小时第二位
		break;

		case 5:
			Gui_Drawbmp16_40x40(144,140,gImage_whitenum5);   // 小时第二位
		break;

		case 6:
			Gui_Drawbmp16_40x40(144,140,gImage_whitenum6);   // 小时第二位
		break;

		case 7:
			Gui_Drawbmp16_40x40(144,140,gImage_whitenum7);   // 小时第二位
		break;

		case 8:
			Gui_Drawbmp16_40x40(144,140,gImage_whitenum8);   // 小时第二位
		break;

		case 9:
			Gui_Drawbmp16_40x40(144,140,gImage_whitenum9);   // 小时第二位
		break;
	}
}

5 天气信息实时更新

对于天气现象和气温是会更新的,但是对于湿度信息目前并没有实时更新,主要原因有两个,首先是由于获取湿度信和获取天气、温度信息是两个不同的API。如果想要做到二者都能够实时更新的话就需要频繁访问,也就意味着需要不断地断开TCP连接再建立TCP连接,考虑到时间问题,放弃了这种操作。另一个原因是,心知天气实际可以返回湿度信息,但是需要付费,考虑到湿度不是特别重要的信息,就没有在湿度信息方面付出太多。

实时获取气象和温度信息实际就是每间隔一段时间发送一次GET信息,前提是在进入系统后依然保持着与心知天气API的连接并且在透传模式里。这也是后续ESP8266初始化时最后访问心知天气API获取天气信息的原因。

5.1 天气信息实时更新程序设计

时间间隔的控制是依托于RTC实现的,每当59秒时就会向心知天气API请求一次实时天气信息。西面是请求天气信息并解析的函数。

void Esp8266_Weather_Heart(void)   // 间歇查询天气
{
	gWaitTime = 0;
	// 向服务器请求实时天气信息
	printf ("GET https://api.seniverse.com/v3/weather/now.json?key=your_key&location=qingdao&language=zh-Hans&unit=c\r\n");   // 向服务器请求实时天气信息
	
	for (getWeatherCodeIndex = 0;getWeatherCodeIndex < 1000;getWeatherCodeIndex ++)
	{
		// 查询天气现象编码
		if (receFifo[getWeatherCodeIndex] == 99 && receFifo[getWeatherCodeIndex + 1] == 111
			&& receFifo[getWeatherCodeIndex + 2] == 100 && receFifo[getWeatherCodeIndex + 3] == 101)
		{
			getWeatherCodeIndex = getWeatherCodeIndex + 7;
			getTemperIndex = getWeatherCodeIndex + 18;

			// 天气现象编码只有一位数
			gWeatherCode = receFifo[getWeatherCodeIndex] - 48;   // 获取天气现象代码
			// 天气现象编码有两位数
			if (48 <= receFifo[getWeatherCodeIndex + 1] && receFifo[getWeatherCodeIndex + 1] <= 57)
			{
				gWeatherCode = gWeatherCode * 10;
				gWeatherCode = gWeatherCode + receFifo[getWeatherCodeIndex + 1] - 48;

				// 对于一些特殊的天气现象,直接按照晴显示
				if (gWeatherCode > 25)
				{
					gWeatherCode = 1;
				}
				getTemperIndex = getTemperIndex + 1;   // 气温索引往后移一位
			}

			// 气温只有一位数
			gTemper = receFifo[getTemperIndex] - 48;   // 获取实时气温

			// 气温有两位数
			if (48 <= receFifo[getTemperIndex + 1] && receFifo[getTemperIndex + 1] <= 57)
			{
				gTemper = gTemper * 10;
				gTemper = gTemper + receFifo[getTemperIndex + 1] - 48;
			}
			
			// 请求成功次数加1
			gWeatherSucCunt = gWeatherSucCunt + 1;
			break;
		}
	}

	// 清空接收数组
	for (clearCount = 0;clearCount < receCount;clearCount ++)
	{
		receFifo[clearCount] = ' ';
	}
	
	receCount = 0;   // 清零接收计数变量
	receEndFlag = 0;   // 清零接收完成标志位
}

其中gWeatherSucCunt 时WIFI断开监听时用到的变量,在第6节会有介绍。

6 WIFI断开监听

需要判断WIFI连接是否正常,从而让使用者直到自己获取到的天气信息是不是实时信息。但是由于ESP8266在请求完天气信息后就一直保持在透传模式中,AT指令并不生效,不能查询WIFI信号强度。这里使用了另一种方法。

考虑到每间隔一段时间就会请求一次天气信息,如果WIFI断开可定是无法获取到正确的回复信息。因此就把能否获取到正确的回复信息作为WIFI连接是否正常的判据。程序中记录请求天气信息的次数和请求成功的次数。如果二者变化统一,也就是说加1就同时加1,就认为WIFI连接正常,否则认为WIFI异常。直到请求成功次数再次变化,认为WIFI连接恢复。

但是这个方案存在问题,就是会存在延迟,比如现在WIFI突然断掉,会在间隔一段时间后才会显示WIFI断开连接图标。同理WIFI恢复正常时也需要等一段时间才能显示正常。

这里贴一下WIFI断开监听的程序。

void Lcd_Show_WifiIcon(void)   // WIFI图标显示
{
	// WIFI图标显示控制
	// 请求次数与成功次数差1,或者开机时,认为WIFI正常
	if ((gWeatherReqCunt - gWeatherSucCunt) == 1 || gWeatherReqCunt == gWeatherSucCunt)
	{
		Gui_Drawbmp16_20x20(280,204,gImage_wifi);
		gTempSucCunt = gWeatherSucCunt;   // 记录当前请求成功次数
	}
	// 差值不等于1,说明WIFI出现问题
	if ((gWeatherReqCunt - gWeatherSucCunt) != 1 && gWeatherReqCunt != gWeatherSucCunt)
	{
		Gui_Drawbmp16_20x20(280,204,gImage_wifioff);
		
		// 直到再次检测到成功次数变化,认为WIFI恢复正常
		if (gTempSucCunt != gWeatherSucCunt)
		{
			Gui_Drawbmp16_20x20(280,204,gImage_wifi);
			// 将二者差值设置为1
			gWeatherReqCunt = 1;
			gWeatherSucCunt = 0;
		}
	}
}

7 总结

这个小项目总共花费了将近一个星期的时间,因为本人下周工作就开始忙起来,所以时间相对比较紧张,对于一些程序没有进一步思考有没有更好的方法,很多都是简单粗暴的实现方案,很期待能和大家在评论区讨论程序设计上的问题。

最后还是少不了常见的展望。其实做天气时钟还有一个原因,想做出来送人。但是现在只是用STM32实现,实际更简洁而且更节省成本的是直接使用ESP8266开发板实现。这样做出来的天气时钟体格很小,而且成本低,这也算是博主后续的努力方向。

  • 7
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
嵌入式RTC(实时时钟)是一种集成于嵌入式系统中的时钟芯片,用于提供系统的实时计时功能。它是基于硬件的计时器,独立于操作系统和CPU的运行,并且具有较低的功耗。 嵌入式RTC主要包括以下几个方面的功能: 1. 实时计时功能:嵌入式RTC能够提供精确的实时计时功能,包括年、月、日、星期、时、分、秒的计时。它能够准确地跟踪时间,不受系统中断或其他操作的影响。 2. 闹钟功能:嵌入式RTC可以设置多个闹钟,用于提醒用户在特定时间执行某项任务。 3. 定时器功能:嵌入式RTC可以作为定时器使用,在特定的时间间隔内触发中断或执行特定的操作。 4. 电源管理功能:嵌入式RTC能够与系统的电源管理模块结合使用,实现低功耗的待机和唤醒功能。它可以在系统关闭或进入睡眠状态时保持时间的计时,并在系统唤醒时恢复计时。 5. 时钟校准功能:嵌入式RTC可以通过外部设备(例如晶振)进行时钟校准,以提高计时的准确性和稳定性。 嵌入式RTC在各种应用场景中都得到广泛应用,包括工业控制、智能家居、智能穿戴设备等。它能够提供可靠的时间戳,用于记录事件发生的时刻;同时也能够在无需操作系统和CPU干预的情况下进行计时和提醒功能。由于其低功耗和可靠性,嵌入式RTC在很多需要实时计时功能的应用中扮演着重要的角色。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二土电子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值