嵌入式开发—天气时钟

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 本文详细介绍了利用ESP8266 WIFI模块制作天气时钟的过程,从ESP8266联网,访问API获取信息,到GUI设计,非常详细。想要尝试设计一个自己的天气时钟的小伙伴可以看一看,期待能够互相交流。


🎀 文章作者:二土电子
🐸 期待大家一起学习交流!


从大学期间就一直想做一个天气时钟,大四时看到学长的一个用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 实现效果

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

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

94938fa8fd83aafc688abba37ed0ef82_4be49d6d2e4c4bc7a343b127d3d5fa3f.jpeg

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
6a9a88da938778559e151ff94e2be135_0440bc2fa531490ab19f76fc0128ccfd.png

如果是高德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底色是黑色,而实际下载来的图标都是白底图标黑色内容。针对白底黑字的图标比较好办,在使用取模软件取模是可以直接勾选颜色反转,就可以直接得到黑底白字的图标。

9ebb85ac263c506abc1191a1f8d4e7be_da0fe83c9fca4f998e32a75b8e9849a3.png

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

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

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分帧之后挨个取模,固定时间间隔不断循环显示即可达到动态旋转的效果。

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月前
|
传感器 数据采集 IDE
LabVIEW编程开发天气监测系统
LabVIEW编程开发天气监测系统
61 0
|
7月前
|
传感器 存储 数据处理
汇编语言与接口技术实验报告——单总线温度采集
汇编语言与接口技术实验报告——单总线温度采集
80 0
|
传感器
振弦传感器钢筋计埋设与安装方法及注意事项
振弦传感器是一种常用的钢筋计测量设备,它通过测量钢筋振动的频率和振幅来判断钢筋的应力状态和疲劳程度,从而实现对钢筋的检测和监测。振弦传感器的钢筋计埋设和安装是使用该设备的关键步骤,下面将详细介绍其方法。
振弦传感器钢筋计埋设与安装方法及注意事项
|
传感器 数据采集 数据处理
振弦传感器钢筋计埋设与安装方法
振弦传感器钢筋计是一种常用于钢筋混凝土结构应变监测的传感器,其可以在钢筋受力时产生微小的振动信号,进而通过数据采集系统进行数据处理,得出钢筋受力状态的参数。在钢筋计的应用过程中,钢筋计的埋设和安装是至关重要的环节,下面我们来详细介绍一下振弦传感器钢筋计的埋设和安装方法。
振弦传感器钢筋计埋设与安装方法
|
7月前
|
物联网
STC51单片机-多功能信号发生器设计-物联网应用系统设计项目开发
STC51单片机-多功能信号发生器设计-物联网应用系统设计项目开发
97 0
|
JSON JavaScript API
MicroPython 玩转硬件系列6:获取天气情况
MicroPython 玩转硬件系列6:获取天气情况
|
传感器 芯片
聊聊身边的嵌入式,方便好用的人体感应灯
聊聊身边的嵌入式,方便好用的人体感应灯
|
传感器 开发者
花最少的时间驱动湿温度传感器之RT-Thread sht3x之(DIY一个小小天气站+万年历)
花最少的时间驱动湿温度传感器之RT-Thread sht3x之(DIY一个小小天气站+万年历)
93 0
STM32智能小车 0基础教学(驱动小车电机)
STM32智能小车 0基础教学(驱动小车电机)
599 0