项目介绍
基于ESP32-S2模块的物联网/音频信号处理平台实现FM收音机和天气台的功能
硬件介绍- 基于ESP32-S2-Mini-1 WiFi核心模块
- 128*64 OLED显示,SPI接口,显示信息 4个按键,用于参数控制、菜单选择
- 2路音频输出,并有功率放大,可以驱动喇叭和耳机插座
- RDA5807M:FM接收模块,ESP32通过I2C接口对其进行参数设置,调节FM电台频率以及设置音量大小
- RS2101XC6:模拟开关,切换来自ESP32产生的音频还是FM输出的音频,模块开关的输出送到喇叭或耳机输出
使用四个按键中断和定时器中断来调整标志位,程序中通过标志位来决定显示的界面与执行的函数。
- 可以通过FM模块接收空中的电台,并可以通过按键进行切换、选台
- 在OLED显示屏上显示FM信号的频段
- 系统能够自动校时,开机后自动调节到准确的时间(年、月、日、时、分、秒)
- 可以通过FM接收电台信号,并播放出来
- 系统能够通过自身ip地址获取地址和天气
-
网络连接
网络连接使用官方提供的WiFiMulti.h
库,可以配置多个网络,主动选择最优网络进行连接。#include <WiFi.h> #include <WiFiMulti.h> WiFiMulti wifiMulti; int failtime = 0; //连接网络失败次数 #define SSID1 "*" #define PASSWORD1 "*" #define SSID2 "*" #define PASSWORD2 "*" #define SSID3 "" #define PASSWORD3 "" //网络连接 wifiMulti.addAP(SSID1, PASSWORD1); wifiMulti.addAP(SSID2, PASSWORD2); wifiMulti.addAP(SSID3, PASSWORD3); Serial.println("Connecting Wifi..."); //未连接网络时重连(最多5次) while(wifiMulti.run() != WL_CONNECTED && failtime < 5) { Serial.println("WiFi not connected!"); delay(1000); failtime++; } if(wifiMulti.run() == WL_CONNECTED) { Serial.println(""); Serial.println("WiFi connected"); Serial.println("IP address: "); Serial.println(WiFi.localIP()); }
-
校时,日期、时间、星期格式化
校时使用官方提供的time.h
库,只需要连接好网络,调用configTime
函数即可实现NTP时间的获取及时区、夏令时的设置。#include "time.h" //NTP服务器 #define NTP1 "pool.ntp.org" #define NTP2 "ntp.org.cn" #define NTP3 "time1.cloud.tencent.com/" //时区、夏令时 const long gmtOffset_sec = 3600 * +8; const int daylightOffset_sec = 0; //星期 const char weekday[7][15] = {"星期日","星期一","星期二","星期三","星期四","星期五","星期六"}; configTime(gmtOffset_sec, daylightOffset_sec, NTP1, NTP2, NTP3); //日期、时间、星期格式化,便于输出 struct tm timeinfo; char time_buffer[20]; char day_buffer[40]; if(!getLocalTime(&timeinfo)) { Serial.println("Failed to obtain time"); strcpy(time_buffer, "not time"); strcpy(day_buffer, " 获取日期时间失败 "); configTime(gmtOffset_sec, daylightOffset_sec, NTP1, NTP2, NTP3); } else { Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S"); strftime(time_buffer, 20, "%H:%M:%S", &timeinfo); strftime(day_buffer, 40, "%Y年%m月%d日", &timeinfo); strncat(day_buffer," ",1); strncat(day_buffer,weekday[timeinfo.tm_wday],9); }
-
按键中断(仅展示一个按键的中断函数)
使用GPIO外部中断,结合millis
函数,实现按键消抖及点击,长按的识别。//IO口初始化 pinMode(1,INPUT); pinMode(2,INPUT); pinMode(3,INPUT); pinMode(6,INPUT); //定义一个按钮的结构体 typedef struct { unsigned long counter = 0; // 计时器 bool prevState = 1; // 前一状态 bool currentState; // 当前状态 } Button; Button button1, button2, button3, button4; //按键1中断函数 void button_type() { button1.currentState = digitalRead(1); if (button1.currentState != button1.prevState) { if (button1.currentState == 0) { button1.counter = millis(); } else { unsigned long diff = millis() - button1.counter; if (diff >= 50 && diff < 1000) { Serial.println("1 pressed"); // 单击 type++; if (type == 1) { radio_init_flag = 1; } else if (type == 2) { type = 0; last_type = 0; radio_term_flag = 1; } } else if (diff >= 1000) { Serial.println("1 long pressed"); // 长按 if (type == 0) { type = last_type; } else { last_type = type; type = 0; } } } button1.prevState = button1.currentState; } } //启用中断 attachInterrupt(1,button_type,CHANGE); attachInterrupt(2,button_down,CHANGE); attachInterrupt(3,button_up,CHANGE); attachInterrupt(6,button_mute,CHANGE);
-
定时器中断
设置了一个定时器,每10分钟触发,用于更新天气。hw_timer_t * timer = NULL; //声明一个定时器 //定时器中断函数 void IRAM_ATTR onTimer() { get_info_flag = 1; Serial.println("info"); } //启用定时器中断(每10分钟触发,更新天气) timer = timerBegin(0, 80, true); timerAttachInterrupt(timer, &onTimer, true); timerAlarmWrite(timer, 10*60*1000000, true); timerAlarmEnable(timer);
-
获取地址,天气
通过HTTPClient.h
库建立http连接,从API中获取地址,天气数据,再使用ArduinoJson.h
库对数据进行解析,补充到储存信息的结构体中。//获取、解析地点天气相关的库 #include <HTTPClient.h> #include <ArduinoJson.h> //定义一个信息的结构体 typedef struct { char city[15] = ""; //城市 char weather[25] = "获取天气失败"; //天气 char temp[10] = ""; //气温 } Info; Info info; //获取地点、天气函数(每10分钟执行一次) void get_info() { if(!get_info_flag) { return; } HTTPClient http; http.begin("https://ip.help.bj.cn/"); //Specify the URL int httpCode = http.GET(); //Make the request if (httpCode > 0) { //Check for the returning code String payload = http.getString(); Serial.println(httpCode); Serial.println(payload); //解析JSON、写入info结构体 DynamicJsonBuffer jsonBuffer; JsonObject& root = jsonBuffer.parseObject(payload); if (!root.success()) { Serial.println("JSON parsing failed!"); http.end(); strcpy(info.city, ""); strcpy(info.weather, "获取天气失败"); strcpy(info.temp, ""); return; } else { if (strlen(root["data"][0]["city"]) > sizeof(info.city) || strlen(root["data"][0]["weather"]["weather"]) > sizeof(info.weather) || strlen(root["data"][0]["weather"]["temp"]) > sizeof(info.temp)) { http.end(); return; } strcpy(info.city, root["data"][0]["city"]); strcpy(info.weather, root["data"][0]["weather"]["weather"]); strcpy(info.temp, root["data"][0]["weather"]["temp"]); get_info_flag = 0; } } else { Serial.println("Error on HTTP request"); strcpy(info.city, ""); strcpy(info.weather, "获取天气失败"); strcpy(info.temp, ""); } http.end(); //Free the resources Serial.printf("城市:"); Serial.println(info.city); Serial.printf("天气:"); Serial.println(info.weather); Serial.printf("气温:"); Serial.println(info.temp); }
-
FM收音机调频,音量控制
使用radio.h
和RDA5807M.h
库,通过I2C方式对RDA5807M模块实现FM功能。//驱动FM模块相关的库 #include <Wire.h> #include <radio.h> #include <RDA5807M.h> RDA5807M radio; // Create an instance of Class for RDA5807M Chip bool radio_init_flag = 0; //FM启动标志 bool radio_term_flag = 0; //FM终止标志 bool seekdown_flag = 0; //FM向下搜台标志 bool seekup_flag = 0; //FM向上搜台标志 bool frequency_change_flag = 0; //FM频率调整标志 int station = 10200; //FM频率 int volume = 3; //FM音量 //FM初始化 Wire.begin(5, 4); pinMode(42,OUTPUT); digitalWrite(42,LOW); if (radio_init_flag){ radio.init(); radio.setBandFrequency(RADIO_BAND_FM, station); radio.setVolume(volume); radio.setMono(false); radio.setMute(false); radio.setSoftMute(true); radio_init_flag = 0; } if (seekdown_flag) { radio.seekDown(true); seekdown_flag = 0; } else if (seekup_flag) { radio.seekUp(true); seekup_flag = 0; } else if (frequency_change_flag) { radio.setFrequency(station); frequency_change_flag = 0; } delay(50); station = radio.getFrequency(); if (radio.getVolume() != volume) { radio.setVolume(volume); delay(50); volume = radio.getVolume(); }
在使用过程中发现该库存在一定的问题,所以对其进行了一些修改。
- 增加了可以直接获取RSSI信号强度的函数。
修改RDA5807M.cpp
,增加如下代码:
修改uint8_t RDA5807M::getRSSI() { _readRegisters(); uint8_t rssi = registers[RADIO_REG_RB] >> 9; return(rssi); } // getRSSI
RDA5807M.h
,增加如下代码:uint8_t getRSSI(); // getRSSI
- 通过查阅RDA5807M模块的寄存器数据手册,重新修改了搜台时信号强度的阈值。
修改RDA5807M.cpp
,将
改为registers[RADIO_REG_VOL] = 0x9081;
registers[RADIO_REG_VOL] = 0xA681;
- 增加了可以直接获取RSSI信号强度的函数。
-
显示屏(以天气台为例)
使用U8g2lib.h
库,以SPI的方式对OLED显示屏进行控制。//驱动显示屏相关的库 #include <Arduino.h> #include <U8g2lib.h> #include <SPI.h> //图标数据 //非静音标志 const unsigned char mute_logo[] U8X8_PROGMEM = { 0x00,0x00,0x60,0x00,0x70,0x1C,0x78,0x3C,0x7F,0x70,0x7F,0xE6,0x7F,0xCE,0x7F,0xCC, 0x7F,0xCC,0x7F,0xCE,0x7F,0xE6,0x7F,0x70,0x78,0x3C,0x70,0x1C,0x60,0x00,0x00,0x00, }; //静音标志 const unsigned char muted_logo[] U8X8_PROGMEM = { 0x00,0x00,0x60,0x00,0x70,0x00,0x78,0x00,0x7F,0xC6,0x7F,0xEE,0x7F,0x7C,0x7F,0x38, 0x7F,0x38,0x7F,0x7C,0x7F,0xEE,0x7F,0xC6,0x78,0x00,0x70,0x00,0x60,0x00,0x00,0x00, }; //未连接网络标志 const unsigned char no_wifi_logo[] U8X8_PROGMEM = { 0x00,0xC3,0x00,0xE7,0x00,0x7E,0x00,0x3C,0x03,0x3C,0x1F,0x7E,0x3C,0xE7,0x70,0xC3, 0xE0,0x00,0xC3,0x01,0x8F,0x03,0x1C,0x07,0x38,0x06,0x30,0x06,0x63,0x0C,0x63,0x0C, }; //已连接网络标志 const unsigned char wifi_logo[] U8X8_PROGMEM = { 0x0F,0x00,0x3F,0x00,0xF8,0x00,0xC0,0x03,0x03,0x07,0x1F,0x0F,0x3C,0x1E,0x70,0x18, 0xE0,0x38,0xC3,0x31,0x8F,0x73,0x1C,0x67,0x38,0xE6,0x30,0xC6,0x63,0xCC,0x63,0xCC, }; U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/ 36, /* data=*/ 35, /* cs=*/ -1, /* dc=*/ 33, /* reset=*/ 34); u8g2.clearBuffer(); // 清除内部缓冲区 u8g2.setFont(u8g2_font_9x18_tr); u8g2.drawStr(27,16,time_buffer); if(wifiMulti.run() == WL_CONNECTED) { u8g2.drawXBM(2,2,16,16,wifi_logo); } else { u8g2.drawXBM(2,2,16,16,no_wifi_logo); } if(mute_flag) { u8g2.drawXBM(109,2,16,16,muted_logo); digitalWrite(41,LOW); } else { u8g2.drawXBM(109,2,16,16,mute_logo); digitalWrite(41,HIGH); } u8g2.setFont(u8g2_font_wqy12_t_gb2312a); u8g2.drawUTF8(0,40,day_buffer); u8g2.drawUTF8(0,60,info.city); u8g2.drawUTF8(strlen(info.city)/3*12 + (128-(strlen(info.city)/3*12+strlen(info.weather)/3*12+(strlen(info.temp)-1)*12/2))/2,60,info.weather); u8g2.drawUTF8(127-(strlen(info.temp)-1)*12/2,60,info.temp); u8g2.sendBuffer(); // transfer internal memory to the display
目前比较主流的编程方法有三种,一种是IDF(应该是最正统的),一种是Arduino,还有一种是MicroPython。一开始是打算尝试一下使用IDF因为自己之前有使用过Arduino和MicroPython,想要换一种新的方式尝试一下,结果发现难度比较大,所以还是打算使用回Arduino。
但是在Arduino IDE中添加ESP32开发板中遇到了网络连接不良的问题,解决方法是离线添加,具体过程我也写了一篇博客进行记录。
- 现在的程序中,如果时间获取失败,在每一次循环中都会再次获取,会导致按键响应和屏幕显示迟钝。
暂时还没有想到比较好的解决方法。 - 目前的使用Arduino IDE环境进行开发,经过查找资料,无法顺利解析网络电台的音频数据。
后期打算用IDF和ADF的开发环境重新编写这一个项目的程序。 - 目前使用的API是免费提供的,其地址,天气数据精度较低,时有偏差。
后期有条件可以采用商用API以提高数据精度。 - 后期可以增加电池,提高模块的便携性。
https://dronebotworkshop.com/esp32-intro/#Repeat_Timer
https://blog.csdn.net/weixin_42880082/article/details/120947163
https://blog.csdn.net/dpjcn1990/article/details/92831760
https://blog.csdn.net/qq_44343584/article/details/105667492
https://blog.csdn.net/shanglala/article/details/108559314
https://blog.csdn.net/yulusilian1/article/details/117470880
https://blog.csdn.net/qq_34804120/article/details/80516245
https://www.dotcpp.com/course/590
https://www.cnblogs.com/wucongzhou/p/12691682.html
https://www.bookstack.cn/read/arduino-esp32/date-2018.06.07.11.25.17
https://www.jianshu.com/p/e3b7e5a37599
https://blog.csdn.net/huang4998802/article/details/110471160
https://blog.csdn.net/u014091490/article/details/104803307
FMhttp://www.mathertel.de/Arduino/RadioLibrary.aspx
http://mathertel.github.io/Radio/html/class_r_d_a5807_m.html
http://ndfweb.cn/news-773.html
https://blog.csdn.net/baidu_25117757/article/details/108940528
https://www.cnblogs.com/iron2222/p/15849123.html#%E7%BD%91%E7%BB%9C%E6%94%B6%E9%9F%B3
https://blog.csdn.net/tianyer/article/details/111930874
https://ip.help.bj.cn/
http://api.help.bj.cn/api/
http://ip.ws.126.net/ipquery
http://ip-api.com/json/?lang=zh-CN
http://whois.pconline.com.cn/jsFunction.jsp