基于ESP32-S2-Mini-1模块制作的本地气象台/温度计
基于ESP32-S2-Mini-1模块制作的本地气象台/温度计,可以在OLED屏幕上显示当地的时间和气象信息。
标签
嵌入式系统
显示
网络与通信
2022寒假在家练
Irving
更新2022-03-03
浙江大学
1568

硬件介绍

本实验平台的核心模块是乐鑫公司的ESP32-S2-Mini-1模块,是一颗通用型Wi-Fi MCU模组,内置 ESP32­S2 系列芯片,Xtensa® 单核 32 位 LX7 微处理器,功能强大。具有37个GPIO口和丰富的外设接口,可用于可穿戴电子设备、智能家居等场景。

用于显示的是128*64的OLED显示屏,由SSD1306芯片驱动。SSD1306与核心模块通过SPI接口连接,用于显示信息。

除此之外,本实验平台还搭载有FM模块和音频输入输出模块等,但此项目中只用了ESP32-S2-Mini-1模块和OLED模块。

FsHqIxLYIB7XY5ieL6iUS5JPR-y_

Ft94o3HpYQNSH6CvTfRfxBvnMJzY

工程目标

制作一个本地气象台/温度计

  • 利用OLED显示
  • 显示当前本地的时间、温度和气象信息

环境搭建

开始设计之前,首先应该搭建程序编译构建的环境,可以是在Arduino平台开发和ESP-IDF环境的开发。搭建环境的方法在项目直播课中讲过了,可以观看回放。

ESP-IDF环境的搭建也可以按照官方的教程来,快速入门 - ESP32 - — ESP-IDF 编程指南 latest 文档 (espressif.com)

但我的安装过程其实一波三折,安装IDF的步骤非常复杂,安装的时候出现这样那样的问题,最后自己没有耐心,准备转而使用Arduino来开发。但也不知道是什么原因,我电脑里的Arduino中搜索不到ESP32S2的板子。后来在一位朋友的指导下,发现使用docker容器下载IDF编译环境特别实用。官网教程给出了在Linux系统下利用docker镜像搭建开发环境的方法,步骤如下:

  • 要有一个Linux系统,Microsoft有一个Windows下的Linux子系统WSL,利用它就可以在VS code里面方便地使用Linux命令。按照微软官方教程来安装即可。安装 WSL | Microsoft Docs
  • 在docker官网下载并安装docker软件,个人版是免费的。Docker Personal | Docker

在WSL和docker都安装好了,就可以下载容器了。注意下载之前需要打开docker软件,否则就不能使用docker命令。用VS code打开工程文件夹,点击VS code左下角【打开远程窗口】,然后选择【Reopen Folder in WSL】,这样就进入了Linux系统。

Fn9XPjXWONd0hK7L4AXbViHFOnKB

在终端输入以下命令开始下载容器:

docker run --name=ESP-IDF_container -it -v $PWD:/Project -w /Project espressif/idf

关于docker更多命令,可以参考Docker 命令大全 | 菜鸟教程 (runoob.com)

  • 下载容器后会自动进入容器中,此时在容器的终端输入以下命令,开始编译和构建
idf.py build
  • 构建完成后,就可以下载运行,在终端输入:
./esptool.exe --chip esp32s2 --port COMx --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dout --flash_freq 40m --flash_size 4MB 0x1000 build/bootloader/bootloader.bin 0x10000 build/main.bin 0x8000 build/partition_table/partition-table.bin
#指定芯片 ESP32S2, 串口 COMx, flash DIO 模式, 40MHz, flash 为 4MB, 0x1000 地址烧录 bootloader.bin, 0x10000 地址烧录 main.bin, 0x8000 地址烧录 partition-table.bin

#如果要用esptool.py直接把命令中的'./esptool.exe'换成‘./esptool.py’

 

其中“COMx”的x是连接板子的串口号数字1,2……,可以在设备管理器中查看,main.bin这个文件是构建的二进制文件,要改为自己工程中文件名。

注意,这一下载命令在容器中不能运行,原因是本地计算机的端口号没有映射到容器中,容器识别不到COMx,因此可以在VS code新建一个终端,在WSL环境中下载,当然,要把esptool.exe发在当前文件夹下。

  • 下载完成后,就可以删掉容器,下次要用的时候重新下载就OK了。

设计思路

       本项目的设计思路非常简单,主要分为四部分,联网、获取时间和气象信息、OLED显示,时间和气象信息更新。流程如下:

Fq8nWjUfHHzq-D97IEdWhUzwy1Wb

       联网部分,我知道的联网方式有两种,一种是直接联网,即将wifi的SSID和password直接保存在代码中,通过下载后wifi模块直接联网;另一种是通过ESP-TOUCH软件来实现间接联网。我使用的是后一种方式,仿照官方的smart_config例程(位于...\esp-idf-v4.3.2\examples\wifi\smart_config当中)。ESP-TOUCH软件可以在乐鑫官网下载App | 乐鑫科技 (espressif.com)

       联网成功后获取气象信息。首先设置好提供天气API的网址、端口。这里用的是心知天气。

#define WEB_SERVER "api.seniverse.com" 
#define WEB_PORT "80"
#define WEB_PATH "https://api.seniverse.com/v3/weather/now.json?key=SqCrtAOQ3W2oEHqZO&location=hangzhou&language=en&unit=c"

       访问网址并获取信息,用函数getaddrinfo()。然后对访问是否成功做出判断。

int err = getaddrinfo(WEB_SERVER, WEB_PORT, &hints, &res);
 if(err != 0 || res == NULL) {
            ESP_LOGE(TAG, "DNS lookup failed err=%d res=%p", err, res);
            ssd1306_display_text(&dev, 7, "........dns fail", 16, false);
            vTaskDelay(1000 / portTICK_PERIOD_MS);
            continue;
        }

        /* Code to print the resolved IP.

           Note: inet_ntoa is non-reentrant, look at ipaddr_ntoa_r for "real" code */
        addr = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
        ESP_LOGI(TAG, "DNS lookup succeeded. IP=%s", inet_ntoa(*addr));

        s = socket(res->ai_family, res->ai_socktype, 0);
        if(s < 0) {
            ESP_LOGE(TAG, "... Failed to allocate socket.");
            //ssd1306_display_text(&dev, 7, "...allocate fail", 16, false);
            freeaddrinfo(res);
            vTaskDelay(1000 / portTICK_PERIOD_MS);
            continue;
        }
        ESP_LOGI(TAG, "... allocated socket");
//…

       连接成功后,将得到的json格式的信息解析出来,并保存需要的气象信息。

cJSON* root = NULL;
        root = cJSON_Parse(recv_results);
        if( root != NULL ) // 判断字段是否json格式
        {
            cJSON* cjson_item =cJSON_GetObjectItem(root,"results");
            cJSON* cjson_results =  cJSON_GetArrayItem(cjson_item,0);
            cJSON* cjson_now = cJSON_GetObjectItem(cjson_results,"now");
            cJSON* cjson_temperature = cJSON_GetObjectItem(cjson_now,"temperature");
            cJSON* cjson_weather = cJSON_GetObjectItem(cjson_now,"text");
            strcpy(LocalTemp,cjson_temperature->valuestring);
            strcpy(LocalWeather,cjson_weather->valuestring);
            cJSON_Delete(root);
            get_weather_success=1;
            ssd1306_display_text(&dev, 5, "Got wether!     ", 16, false);
            break;
        }

       获取时间信息。获取时间信息我参考的就是CSDN上的文章,(73条消息) 【ESP32-IDF】 05-3 WIFI-esp32获取网络时间_Ciaran-byte的博客-CSDN博客_esp32获取网络时间,但要注意的是本次项目的联网方式和这位网友不同,因此他包含的头文件中的"bsp_wifi_station.h"应该删除。具体的步骤和获取天气一样,也是先设置网址,访问,判断访问是否成功,解析json文件。只是访问的函数有些不同,这里用的是esp_http_client_open()这个函数。

   //向配置结构体内部写入url
   static const char *URL = "http://quan.suning.com/getSysTime.do";
    config.url = URL;

    //初始化结构体
    esp_http_client_handle_t client = esp_http_client_init(&config);    //初始化http连接

    //设置发送请求 
    esp_http_client_set_method(client, HTTP_METHOD_GET);

    //02-3 循环通讯

    for (i=0;i<MAXIMUM_RETRY;i++)
    {
    // 与目标主机创建连接,并且声明写入内容长度为0
    esp_err_t err = esp_http_client_open(client, 0);
    //如果连接失败
    if (err != ESP_OK) {
    //    ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
    ssd1306_display_text(&dev, 7, "Failed to open HTTP", 16, false);
    vTaskDelay(1000 / portTICK_PERIOD_MS);//延迟1s
    } 
//如果连接成功
//…

       OLED显示。这里要用到ssd1306的驱动库函数。在github上可以获取。这里面包含了ssd1306初始化、显示文本、清除文本的函数在内,只需直接调用即可。

FpsX_a_uL1VgiJwgEtKAL8_mVEsA

       这三个c文件中,给了SPI和I2C两种接口的驱动方式,本平台使用的是SPI接口,因此要选用ssd1306_spi.c中的初始化函数。

FkSeDI4_qhb0pqHsEpcQKSqbU3Tl

       在获取完气象信息和时间信息后,使用以下函数就可以将信息显示在显示屏上了。

void Display_weather(char* weather, char* temperature)
{
    char temp1[16]="               ";//16个空格,此temp1表示临时变量而非温度
    char temp2[16]=" "              ;//1个空格,此temp2表示临时变量而非温度
    ssd1306_clear_line(&dev, 5, false);
    ssd1306_clear_line(&dev, 7, false);
    ssd1306_display_text(&dev, 5, " Weather   Temp ", 16, false);
    strcpy(temp1,"               ");
    strcpy(temp1+13-strlen(temperature),temperature);
    ssd1306_display_text(&dev, 7, temp1, 16, false);
    strcat(temp2,weather);
    ssd1306_display_text(&dev, 7, temp2, strlen(temp2), false);

}

      以及

void Display_time(void)//显示时间
{
    if(Time_Got)
    {
        char temp1[17]="       -  -     ";
        strcpy(temp1+13-strlen(Time.day),Time.day);
        ssd1306_display_text(&dev, 2, temp1, 16, false);
        strcpy(temp1+10-strlen(Time.month),Time.month);
        ssd1306_display_text(&dev, 2,temp1, 10, false);
        strcpy(temp1+7-strlen(Time.year),Time.year);
        ssd1306_display_text(&dev, 2,temp1, 7, false);
        char temp2[17]="      :  :      ";
        strcpy(temp2+12-strlen(Time.second),Time.second);
        ssd1306_display_text(&dev, 3, temp2, 16, false);
        strcpy(temp2+9-strlen(Time.minute),Time.minute);
        ssd1306_display_text(&dev, 3,temp2, 9, false);
        strcpy(temp2+6-strlen(Time.hour ),Time.hour);
        ssd1306_display_text(&dev, 3,temp2, 6, false);
    }
}

时间和气象信息更新。这一步本可以用中断服务来实现,但鉴于整个项目没有别的task,所以这里只用循环和延时就好了。回顾我们前面获得的时间的json数据,有用的其实就是以"2022-02-25 20:03:30"这种格式的字符串。这个数据并不适合用来做每秒时间的更新。因此将它保存在由六个字符数组组成的结构当中去。

typedef struct{
    char year[5]; char month[3]; char day[3]; char hour[3]; char minute[3]; char second[3];
}TimeStruc;
void StoreTime()
{
   int i;
    for (i=0;i<4;i++)
    {
        Time.year[i]=CurrentTime[i];
    }
    Time.year[i]='\0';
    for (i=0;i<2;i++)
    {
        Time.month[i]=CurrentTime[i+5];
    }
    Time.month[i]='\0';
    for (i=0;i<2;i++)
    {
        Time.day[i]=CurrentTime[i+8];
    }
    Time.day[i]='\0';
    for (i=0;i<2;i++)
    {
        Time.hour[i]=CurrentTime[i+11];
    }
    Time.hour[i]='\0';
    for (i=0;i<2;i++)
    {
        Time.minute[i]=CurrentTime[i+14];
    }
    Time.minute[i]='\0';
    for (i=0;i<2;i++)
    {
        Time.second[i]=CurrentTime[i+17];
    }
    Time.second[i]='\0';
}

然后对保存的时间的结构变量进行操作,即加一秒。

void TimePlusOneSec()               //Time 结构变量保存的时间+1s
{
    if (strcmp(Time.day," "))
    {
        int flag[6]={0,0,0,0,0,0};  //判断是否有进位
        if (strcmp(Time.second,"59")==0)
        {
            flag[5]=1;             //满60s,进位
            strcpy(Time.second,"00");
        }
        else
        {
            if (Time.second[1]=='9')
            {
                Time.second[1]='0';
                Time.second[0]++;
            }
            else
            {
                Time.second[1]++;
            }
        }
        if (flag[5])                //如果秒数有进位
        {
            if (strcmp(Time.minute,"59")==0)
            {
                flag[4]=1;          //满60min,进位
                strcpy(Time.minute,"00");
            }
            else
            {
                if (Time.minute[1]=='9')
                {
                    Time.minute[1]='0';
                    Time.minute[0]++;
                }
                else
                {
                    Time.minute[1]++;
                }
            }
        }
        if (flag[4])               // 如果分钟有进位
        {
            if (strcmp(Time.hour,"23")==0)
            {
                strcpy(Time.hour,"00"); //满24小时
            }
            else
            {
                if (Time.hour[1]=='9')
                {
                    Time.hour[1]='0';
                    Time.hour[0]++;
                }
                else
                {
                    Time.hour[1]++;
                }
            }
        }
        // 由于月份和日期规律比较复杂,年月日的进位就不再这里计算了,这可以直接通过联网更新来解决。
    }
}

       这样的话,可以通过在while循环中每延时一秒调用一次加一秒的函数,并且更新显示即可。每10min从网络校对一次时间,30min校对一次气象。

void MyTask(void)
{
    int i=0;
    sdd1306_clear();
    while(1)
    {
        if(i%600==0)       //每10min
        {
            http_get_time();  //获取时间
        }
        if(i%1800==0)      //每30min
        {
            http_get_weather();//获取天气
            Display_weather(LocalWeather,LocalTemp);
        }
        Display_time();
        vTaskDelay(1000 / portTICK_PERIOD_MS);//延迟1s
        TimePlusOneSec();  //加一秒
        i++;
        if(i>1800)
            i=1;    
    }
}

下载验证

      将整个工程编译后,通过上述方法将程序下载到板卡上。手机或者iPad下载好ESP-TOUCH软件,连接WIFI,打开ESP-TOUCH软件,选择EspTouch,输入WIFI密码,选择广播,点击确认,这时板卡收到ESP-TOUCH的信号后会自动连接WIFI。注意,如果板卡长时间没有连接上,请多重复几次,重新给板卡上电,并广播WIFI信息。我在实验中发现这两个步骤不能长时间间隔,否则会连接不上WIFI。

Fs1YUXQzDu7bDMLRBiEj794QPOMC

Fp-n4sn505ok1DVR-TQs1eN9ZjIr

问题与改进

  1. 利用smart_config连接的WIFI似乎不是特别稳定,时常会有断连的现象。

      尝试让板卡处于WIFI信号强的区域里,如果是手机热点,尽量让手机离板卡近一点。要注意,连接的WIFI是要2.4GHz频段的,因为ESP-TOUCH不能广播5GHz的WIFI。第一次连接时,倘若一次连接不上,尝试几次ESP-TOUCH的广播。

    2. 不能手动更新,因为时间的一秒是用延时函数来实现的,所以可能会有误差,虽然会定期联网更新,但不可避免误差会累积。

      考虑到提供API的网址有时太过拥挤,也不选择频繁地访问网址,也许可以通过定时器中断的方式解决这个问题。或者再设置一个按键中断,按下按键调用访问程序,实现手动更新。

 

 

 

 

 

 

附件下载
MyProject.zip
包含了源文件和最终的二进制文件
团队介绍
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号