硬件介绍
本实验平台的核心模块是乐鑫公司的ESP32-S2-Mini-1模块,是一颗通用型Wi-Fi MCU模组,内置 ESP32S2 系列芯片,Xtensa® 单核 32 位 LX7 微处理器,功能强大。具有37个GPIO口和丰富的外设接口,可用于可穿戴电子设备、智能家居等场景。
用于显示的是128*64的OLED显示屏,由SSD1306芯片驱动。SSD1306与核心模块通过SPI接口连接,用于显示信息。
除此之外,本实验平台还搭载有FM模块和音频输入输出模块等,但此项目中只用了ESP32-S2-Mini-1模块和OLED模块。
工程目标
制作一个本地气象台/温度计
- 利用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系统。
在终端输入以下命令开始下载容器:
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显示,时间和气象信息更新。流程如下:
联网部分,我知道的联网方式有两种,一种是直接联网,即将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初始化、显示文本、清除文本的函数在内,只需直接调用即可。
这三个c文件中,给了SPI和I2C两种接口的驱动方式,本平台使用的是SPI接口,因此要选用ssd1306_spi.c中的初始化函数。
在获取完气象信息和时间信息后,使用以下函数就可以将信息显示在显示屏上了。
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。
问题与改进
- 利用smart_config连接的WIFI似乎不是特别稳定,时常会有断连的现象。
尝试让板卡处于WIFI信号强的区域里,如果是手机热点,尽量让手机离板卡近一点。要注意,连接的WIFI是要2.4GHz频段的,因为ESP-TOUCH不能广播5GHz的WIFI。第一次连接时,倘若一次连接不上,多尝试几次ESP-TOUCH的广播。
2. 不能手动更新,因为时间的一秒是用延时函数来实现的,所以可能会有误差,虽然会定期联网更新,但不可避免误差会累积。
考虑到提供API的网址有时太过拥挤,也不选择频繁地访问网址,也许可以通过定时器中断的方式解决这个问题。或者再设置一个按键中断,按下按键调用访问程序,实现手动更新。