基于ESP32-S2模块实现FM收音机和天气台
【基于ESP32-S2模块的物联网/音频信号处理平台】 实现FM收音机和天气台的功能
标签
Arduino
ESP32
收音机
网络
2022寒假在家练
136ytr
更新2022-03-02
汕头大学
1285

 

 

 

项目介绍

基于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.hRDA5807M.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();
    }
    

    在使用过程中发现该库存在一定的问题,所以对其进行了一些修改。

    1. 增加了可以直接获取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
      
    2. 通过查阅RDA5807M模块的寄存器数据手册,重新修改了搜台时信号强度的阈值。
      修改RDA5807M.cpp,将
      registers[RADIO_REG_VOL] = 0x9081;
      
      改为
      registers[RADIO_REG_VOL] = 0xA681;
      
  • 显示屏(以天气台为例)
    使用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开发板中遇到了网络连接不良的问题,解决方法是离线添加,具体过程我也写了一篇博客进行记录。

存在问题及未来计划
  1. 现在的程序中,如果时间获取失败,在每一次循环中都会再次获取,会导致按键响应和屏幕显示迟钝。
    暂时还没有想到比较好的解决方法。
  2. 目前的使用Arduino IDE环境进行开发,经过查找资料,无法顺利解析网络电台的音频数据。
    后期打算用IDF和ADF的开发环境重新编写这一个项目的程序。
  3. 目前使用的API是免费提供的,其地址,天气数据精度较低,时有偏差。
    后期有条件可以采用商用API以提高数据精度。
  4. 后期可以增加电池,提高模块的便携性。
参考资料 校时

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

JSON

https://blog.csdn.net/u014091490/article/details/104803307

FM

http://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

获取地址、天气api 有用

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

附件下载
esp32_fm.ino
Arduino程序文件
RDA5807M.cpp
库文件(已修改)
RDA5807M.h
库文件(已修改)
团队介绍
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号