2021暑假一起练-用基于ESP32-S2-Mini-1模块的音频处理平台设计了FM收音机&网络收音机
本项目使用基于ESP32-S2-Mini-1模块的音频处理平台和板载RDA5807M收音机模块,在ESP-IDF环境下设计了网络收音机和FM收音机。
标签
嵌入式系统
测试
网络与通信
esp-idf
ESP32-S2
esp32小白
更新2021-09-08
2304

开发板介绍

   本次使用的开发板使用的是乐鑫的ESP32-S2-Mini-1模块,功能强大,具有丰富的外设接口,应用广泛芯片搭载了Xtensa® 32 位LX7 单核处理器,工作频率高达 240 MHz,带有37个可编程GPIO口。

功能实现

我完成的是项目1   实现网络收音机/FM收音机的功能

  • 可以通过WiFi接收网络上的电台,也可以通过FM模块接收空中的电台,并可以通过按键进行切换、选台
  • 在OLED显示屏上显示网络电台的IP地址、节目名字等相关信息或FM信号的频段
  • 系统能够自动校时,开机后自动调节到准确的时间(年、月、日、时、分、秒)

按键操作

      在时间界面第四个键长按1秒进入FM收音机模式,按第一个键向上调频,按第二个键向下调频,长按第三个键静音,再次长按第三个键解除静音,按第四个键切换到网络收音机模式,长按第1,2,3个键(可能需要按5-6秒)换不同的台,长按第四个键(可能需要按5-6秒)切换到FM收音机。

代码实现框图

FmnsSQEDqiPcHMYqwwrD9BJQLSTz

Menuconfig设置

idf.py menuconfig进入

      Serial flasher config--->Flash size--->4 MB

      Audio HAL--->Audio board--->改为ESP32-S2-Kaluga-1 v1.2

      Compiler options--->Optimization Level--->打开 Optimize for size (-0s)

      Component config--->ESP32S2-specific--->CPU frequency--->240MHz

      Component config--->Wi-Fi--->关闭WiFi IRAM speed optimization 和 WiFi RX IRAM speed optimization

      Component config--->LVGL configuration--->Font usage--->Enable built-in fonts打开Enable Montserrat 14、Enable Montserrat 18、Enable Montserrat 22、Enable Montserrat 28、Enable Simsun 16 CJK、Enable Montserrat 20

      Component config--->LVGL configuration--->Memory manager settings--->内存大小32设置为6

      Partition Table--->Partition Table--->打开Custom partition table CSV

注:麻烦测试工程师把手机热点打开,WiFi账号改为 ssid,密码改为 password,即可连接WiFi

烧录

Fsd08NKlgLZHAQnU_38UuvB980TZ

代码分析

app_main()入口函数下,首先调用pin_init()函数,主要初始化SSD1306 SPI屏幕mosi,sclk,dc,reset引脚,我使用的是LVGL屏幕驱动,可以调用不同的字体,操作简单。

void pin_init()
{
    esp_err_t ret = ESP_OK;

    spi_bus_handle_t bus_handle = NULL;
    spi_config_t bus_conf = {
        .miso_io_num = 27,
        .mosi_io_num = 35,
        .sclk_io_num = 36,
    }; // spi_bus configurations

    bus_handle = spi_bus_create(SPI2_HOST, &bus_conf);

    scr_interface_spi_config_t spi_ssd1306_cfg = {
        .spi_bus = bus_handle,    /*!< Handle of spi bus */
        .pin_num_cs = 26,           /*!< SPI Chip Select Pin*/
        .pin_num_dc = 33,           /*!< Pin to select Data or Command for LCD */
        .clk_freq = 20*1000*1000,                /*!< SPI clock frequency */
        .swap_data = false,             /*!< Whether to swap data */
    };

    scr_interface_driver_t *iface_drv;
    scr_interface_create(SCREEN_IFACE_SPI, &spi_ssd1306_cfg, &iface_drv);

    /** Find screen driver for SSD1306 */
    ret = scr_find_driver(SCREEN_CONTROLLER_SSD1306, &g_lcd);
    if (ESP_OK != ret) {
        return;
        ESP_LOGE(TAG, "screen find failed");
    }

    /** Configure screen controller */
    scr_controller_config_t lcd_cfg = {
        .interface_drv = iface_drv,
        .pin_num_rst = 34,      // The reset pin is not connected
        .pin_num_bckl = -1,     // The backlight pin is not connected
        .rst_active_level = 0,
        .bckl_active_level = 1,
        .offset_hor = 0,
        .offset_ver = 0,
        .width = 128,
        .height = 64,
        .rotate = SCR_DIR_RLBT,
    };

    /** Initialize SSD1306 screen */
    g_lcd.init(&lcd_cfg);

    lvgl_init(&g_lcd, NULL);    /* Initialize LittlevGL */
}

连接WiFi

   引用pipeline_living_stream例程来解码网络流媒体,去掉开头的音频解码器初始化,音频流解码完成后,连接到WiFi,接入网络。

ESP_LOGI(TAG, "[ 3 ] Start and wait for Wi-Fi network");
esp_periph_config_t periph_cfg = DEFAULT_ESP_PERIPH_SET_CONFIG();
esp_periph_set_handle_t set = esp_periph_set_init(&periph_cfg);
periph_wifi_cfg_t wifi_cfg = {
    .ssid = EXAMPLE_WIFI_SSID,
    .password = EXAMPLE_WIFI_PASS,
};
esp_periph_handle_t wifi_handle = periph_wifi_init(&wifi_cfg);
esp_periph_start(set, wifi_handle);
periph_wifi_wait_for_connected(wifi_handle, portMAX_DELAY);

校时

   接入网络后,从互联网获取时间,在屏幕上显示。如果获取到的年份小于(2016-1900),则说明时间出错,串口打印“Time is not set yet. Connecting to WiFi and getting time over NTP.”,重新获取时间;否则说明时间正确,设置时区为“CST-8”,在屏幕上显示年月日时分秒具体时间,并把整个获取时间并显示的代码放入while循环,用来监测按键状态,按键未按下则每隔1秒刷新一次时间,按键按下则结束时间刷新并清空屏幕。

time_t now = 0;
struct tm timeinfo = { 0 };
char strftime_buf[64] = {0 };
char detail_time[21] = { 0};//9
char rough_time[16] = { 0 };
int once = 1;
obtain_time();
do
{        
    time(&now);
    localtime_r(&now, &timeinfo);
    // Is time set? If not, tm_year will be (1970 - 1900).
    if (timeinfo.tm_year < (2016 - 1900)) {
        ESP_LOGI(TAG, "Time is not set yet. Connecting to WiFi and getting time over NTP.");
        obtain_time();
        // update 'now' variable with current time
        time(&now);
    }
    setenv("TZ", "CST-8", 1);
    tzset();
    localtime_r(&now, &timeinfo);
    strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
    
    if(timeinfo.tm_year > (2020-1900)){
        for (int i=0; i<8; i++){
            detail_time[i] = strftime_buf[i+11];
        }
        for (int j=0; j<4; j++){
            rough_time[j] = strftime_buf[j+20];
        }
        for (int k = 0; k < 6; k++)
        {
            rough_time[4+k] = strftime_buf[4+k];
        }
        if(once){
            lv_label_set_text(label0, "");
            lv_label_set_text(label3, "");
            once = 0;
        }
        lv_label_set_text(label7, detail_time);
        lv_obj_align(label7, NULL, LV_ALIGN_CENTER, 0, -10);
        lv_label_set_text(label8, rough_time);
        lv_obj_align(label8, NULL, LV_ALIGN_CENTER, 0, 15);
    }
    vTaskDelay(1000 / portTICK_PERIOD_MS);
}while (gpio_get_level(GPIO_NUM_6) == 1);
lv_label_set_text(label7, "");
lv_label_set_text(label8, "");
vTaskDelay(500 / portTICK_PERIOD_MS);

FM收音机部分

   本次FM收音机使用的是RDA5807M模块,此模块支持76-108MHz全球FM频段兼容,(包括日本76-91MHz和欧美87.5-108.5MHz),应用I2C串行数据总线接口通讯,支持外部基准时钟输入方式,内置噪声消除,软静音低音增强电路设计。代码中首先初始化LVGL屏幕显示的字体,在左上角显示“FM Radio”标识进入FM收音机模式,屏幕中间显示当前频率, 屏幕下方显示静音状态“Mute”和“unMute”;接着调用rda5807m.h库中函数进行初始化,包括指定I2C管脚,芯片晶振频率,音量,频段,和初始频率。通过while循环检测按键来向上搜台、向下搜台、静音、取消静音和退出FM收音机。

static void test(void *pvParameters)
{
/////////// 字体设置 初始化 ///////////////////////////////////////////
    lv_obj_t * scr = lv_disp_get_scr_act(NULL);
    lv_obj_t * label4 = lv_label_create(scr, NULL);// 显示 FM Radio
    lv_obj_t * label5 = lv_label_create(scr, NULL);// 显示 频率变化
    lv_obj_t * label6 = lv_label_create(scr, NULL);// 显示 静音状态
    static lv_style_t font_style1;// FM Radio 和 mute 共用 style1
    static lv_style_t font_style2;// 频率用 style2
    lv_style_init(&font_style1);
    lv_style_init(&font_style2);
    lv_style_set_text_font(&font_style1, LV_STATE_DEFAULT, &lv_font_montserrat_14);
    lv_style_set_text_font(&font_style2, LV_STATE_DEFAULT, &lv_font_montserrat_20);
    lv_obj_add_style(label4, 0, &font_style1);
    lv_obj_add_style(label5, 0, &font_style2);
    lv_obj_add_style(label6, 0, &font_style1);
    lv_label_set_text(label5, "");
    lv_label_set_text(label6, "");
////////////////////////////////////////////////////////////////
    lv_label_set_text_static(label4, "FM Radio");
    lv_obj_align(label4, NULL, LV_ALIGN_IN_TOP_LEFT, 0, 0);

    char freq[14];
    char ifmute[7];

	gpio_set_level(GPIO_NUM_41, 1);

    dev.i2c_dev.cfg.scl_pullup_en = true;
    dev.i2c_dev.cfg.sda_pullup_en = true;

    ESP_ERROR_CHECK(rda5807m_init_desc(&dev, I2C_PORT, SDA_GPIO, SCL_GPIO));
    ESP_ERROR_CHECK(rda5807m_init(&dev, RDA5807M_CLK_32768HZ));
    ESP_ERROR_CHECK(rda5807m_set_volume(&dev, 10));
    ESP_ERROR_CHECK(rda5807m_set_band(&dev, RDA5807M_BAND_76_108));
    ESP_ERROR_CHECK(rda5807m_set_frequency_khz(&dev, DEF_FREQ));

    rda5807m_state_t state;

    sprintf(freq, "%3d.%d MHz", DEF_FREQ / 1000, (DEF_FREQ % 1000) / 100);

    lv_label_set_text(label5, freq);
    lv_obj_align(label5, NULL, LV_ALIGN_CENTER, 0, 0);

    lv_label_set_text(label6, "unMute");
    lv_obj_align(label6, NULL, LV_ALIGN_IN_BOTTOM_MID, 0, 0);

    while (1)
    {
        if(gpio_get_level(GPIO_NUM_1) == 0)
        {
            vTaskDelay(pdMS_TO_TICKS(60));
            if(gpio_get_level(GPIO_NUM_1) == 0)
            {
                ESP_ERROR_CHECK(rda5807m_seek_start(&dev, true, true, RDA5807M_SEEK_TH_DEF));//向上调台
                memset(&state, 0, sizeof(state));
                ESP_ERROR_CHECK(rda5807m_get_state(&dev, &state));

                ESP_ERROR_CHECK(rda5807m_seek_stop(&dev));

                sprintf(freq, "%3d.%d MHz", state.frequency / 1000, (state.frequency % 1000) / 100);

                lv_label_set_text(label5, freq);
                lv_obj_align(label5, NULL, LV_ALIGN_CENTER, 0, 0);
            }
        }
        if(gpio_get_level(GPIO_NUM_2) == 0)
        {
            vTaskDelay(pdMS_TO_TICKS(60));
            if(gpio_get_level(GPIO_NUM_2) == 0)
            {
                ESP_ERROR_CHECK(rda5807m_seek_start(&dev, false, true, RDA5807M_SEEK_TH_DEF));//向下调台
                memset(&state, 0, sizeof(state));
                ESP_ERROR_CHECK(rda5807m_get_state(&dev, &state));

                ESP_ERROR_CHECK(rda5807m_seek_stop(&dev));

                sprintf(freq, "%3d.%d MHz", state.frequency / 1000, (state.frequency % 1000) / 100);

                lv_label_set_text(label5, freq);
                lv_obj_align(label5, NULL, LV_ALIGN_CENTER, 0, 0);

                ESP_ERROR_CHECK(rda5807m_seek_stop(&dev));
            }
        }
        if(gpio_get_level(GPIO_NUM_3) == 0)
        {
            vTaskDelay(pdMS_TO_TICKS(1000));
            if(gpio_get_level(GPIO_NUM_3) == 0)
            {
                ESP_ERROR_CHECK(rda5807m_set_mute(&dev, true));
            }
            lv_label_set_text(label6, "Mute");
            lv_obj_align(label6, NULL, LV_ALIGN_IN_BOTTOM_MID, 0, 0);

            vTaskDelay(pdMS_TO_TICKS(500));
        }
        if(gpio_get_level(GPIO_NUM_3) == 0)
        {
            vTaskDelay(pdMS_TO_TICKS(60));
            if(gpio_get_level(GPIO_NUM_3) == 0)
            {
                ESP_ERROR_CHECK(rda5807m_set_mute(&dev, false));
            }
            lv_label_set_text(label6, "unMute");
            lv_obj_align(label6, NULL, LV_ALIGN_IN_BOTTOM_MID, 0, 0);

            vTaskDelay(pdMS_TO_TICKS(500));
        }
        if(gpio_get_level(GPIO_NUM_6) == 0)
        {
            vTaskDelay(pdMS_TO_TICKS(60));
            if(gpio_get_level(GPIO_NUM_6) == 0)
            {
                gpio_set_level(GPIO_NUM_42, 1);
                vTaskDelay(pdMS_TO_TICKS(1000));
                lv_label_set_text(label4, "");
                lv_label_set_text(label5, "");
                lv_label_set_text(label6, "");
                return;
            }
        }
    }
}

事件监听

设置事件监听,监听网络收音机pipeline、elements和peripherals信息。

ESP_LOGI(TAG, "[ 4 ] Set up  event listener");
audio_event_iface_cfg_t evt_cfg = AUDIO_EVENT_IFACE_DEFAULT_CFG();
audio_event_iface_handle_t evt = audio_event_iface_init(&evt_cfg);
ESP_LOGI(TAG, "[4.1] Listening event from all elements of pipeline");
audio_pipeline_set_listener(pipeline, evt);
ESP_LOGI(TAG, "[4.2] Listening event from peripherals");
audio_event_iface_set_listener(esp_periph_set_get_event_iface(set), evt);
ESP_LOGI(TAG, "[ 5 ] Start audio_pipeline");
audio_pipeline_run(pipeline);

播放网络音乐

   while循环中,从音频解码器中获取数据到内存中播放,并打印采样率、位数和音频通道。如果读取出错,则重启播放。

audio_event_iface_msg_t msg;
esp_err_t ret = audio_event_iface_listen(evt, &msg, portMAX_DELAY);
        
if (ret != ESP_OK) {
    ESP_LOGE(TAG, "[ * ] Event interface error : %d", ret);
    continue;
}
if (msg.source_type == AUDIO_ELEMENT_TYPE_ELEMENT
    && msg.source == (void *) aac_decoder
    && msg.cmd == AEL_MSG_CMD_REPORT_MUSIC_INFO) {
    audio_element_info_t music_info = {0};
    audio_element_getinfo(aac_decoder, &music_info);
    ESP_LOGI(TAG, "[ * ] Receive music info from aac decoder, sample_rates=%d, bits=%d, ch=%d",
             music_info.sample_rates, music_info.bits, music_info.channels);
    
    audio_element_setinfo(output_stream_writer, &music_info);
    pwm_stream_set_clk(output_stream_writer, music_info.sample_rates, music_info.bits, music_info.channels);
    continue;
}
/* restart stream when the first pipeline element (http_stream_reader in this case) receives stop event (caused by reading errors) */
if (msg.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && msg.source == (void *) http_stream_reader
    && msg.cmd == AEL_MSG_CMD_REPORT_STATUS && (int) msg.data == AEL_STATUS_ERROR_OPEN) {
    ESP_LOGW(TAG, "[ * ] Restart stream");
    audio_pipeline_stop(pipeline);
    audio_pipeline_wait_for_stop(pipeline);
    audio_element_reset_state(aac_decoder);
    audio_element_reset_state(output_stream_writer);
    audio_pipeline_reset_ringbuffer(pipeline);
    audio_pipeline_reset_items_state(pipeline);
    audio_pipeline_run(pipeline);
    continue;
}

网络收音机换台

   在while循环里设置按键检测来切换电台和退出网络收音机进入FM收音机,而在FM收音机中退出可以继续网络收音机的播放,由此实现两种模式收音机的切换。屏幕上,左上角显示“Web Radio”,中间显示电台名称,下方显示当前电台的IP地址(本来调用蜻蜓官方API应该是更好的选择,此处例程中使用电台的网址,因此就近取材)。

while (1) {
    if(gpio_get_level(GPIO_NUM_6) == 0)
    {
        vTaskDelay(pdMS_TO_TICKS(60));
        if(gpio_get_level(GPIO_NUM_6) == 0)
        {
            gpio_set_level(GPIO_NUM_42, 0);
            audio_pipeline_stop(pipeline);
            audio_pipeline_wait_for_stop(pipeline);
            audio_element_reset_state(aac_decoder);
            audio_element_reset_state(output_stream_writer);
            ESP_LOGI(TAG, "URL: %s", AAC_STREAM_URI1);
            lv_label_set_text(label3, "222.186.184.5");
            lv_obj_align(label3, NULL, LV_ALIGN_IN_BOTTOM_MID, 0, 0);
            audio_element_set_uri(http_stream_reader, AAC_STREAM_URI1);
            audio_pipeline_reset_ringbuffer(pipeline);
            audio_pipeline_reset_elements(pipeline);
            audio_pipeline_reset_items_state(pipeline);
            audio_pipeline_run(pipeline);
            lv_label_set_text(label1, "");
            lv_label_set_text(label2, "");
            lv_label_set_text(label3, "");
            vTaskDelay(1000 / portTICK_PERIOD_MS);
            test(NULL);
            lv_label_set_text(label1, "Web Radio");
            lv_obj_align(label1, NULL, LV_ALIGN_IN_TOP_LEFT, 0, 0);
            lv_label_set_text(label2, "南京音乐广播");
            lv_obj_align(label2, NULL, LV_ALIGN_CENTER, 0, 0);
            lv_label_set_text(label3, "113.107.249.5");
            lv_obj_align(label3, NULL, LV_ALIGN_IN_BOTTOM_MID, 0, 0);
        }
    }
/////////////////////////////////////////////////////////////
    if(gpio_get_level(GPIO_NUM_1) == 0)
    {
        vTaskDelay(pdMS_TO_TICKS(60));
        if(gpio_get_level(GPIO_NUM_1) == 0)
        {
            audio_pipeline_stop(pipeline);
            audio_pipeline_wait_for_stop(pipeline);
            audio_element_reset_state(aac_decoder);
            audio_element_reset_state(output_stream_writer);
            ESP_LOGI(TAG, "URL: %s", AAC_STREAM_URI1);
            lv_label_set_text(label2, "中国之声");
            lv_obj_align(label2, NULL, LV_ALIGN_CENTER, 0, 0);
            lv_label_set_text(label3, "222.186.184.5");
            lv_obj_align(label3, NULL, LV_ALIGN_IN_BOTTOM_MID, 0, 0);
            audio_element_set_uri(http_stream_reader, AAC_STREAM_URI1);
            audio_pipeline_reset_ringbuffer(pipeline);
            audio_pipeline_reset_elements(pipeline);
            audio_pipeline_reset_items_state(pipeline);
            audio_pipeline_run(pipeline);
            vTaskDelay(1000 / portTICK_PERIOD_MS);
        }
    }
/////////////////////////////////////////////////////////////
    if(gpio_get_level(GPIO_NUM_2) == 0)
    {
        vTaskDelay(pdMS_TO_TICKS(60));
        if(gpio_get_level(GPIO_NUM_2) == 0)
        {
            audio_pipeline_stop(pipeline);
            audio_pipeline_wait_for_stop(pipeline);
            audio_element_reset_state(aac_decoder);
            audio_element_reset_state(output_stream_writer);
            ESP_LOGI(TAG, "URL: %s", AAC_STREAM_URI2);
            lv_label_set_text(label2, "江苏新闻广播");
            lv_obj_align(label2, NULL, LV_ALIGN_CENTER, 0, 0);
            lv_label_set_text(label3, "112.30.245.5");
            lv_obj_align(label3, NULL, LV_ALIGN_IN_BOTTOM_MID, 0, 0);
            audio_element_set_uri(http_stream_reader, AAC_STREAM_URI2);
            audio_pipeline_reset_ringbuffer(pipeline);
            audio_pipeline_reset_elements(pipeline);
            audio_pipeline_reset_items_state(pipeline);
            audio_pipeline_run(pipeline);
            vTaskDelay(1000 / portTICK_PERIOD_MS);
        }
    }
/////////////////////////////////////////////////////////////
    if(gpio_get_level(GPIO_NUM_3) == 0)
    {
        vTaskDelay(pdMS_TO_TICKS(60));
        if(gpio_get_level(GPIO_NUM_3) == 0)
        {
            audio_pipeline_stop(pipeline);
            audio_pipeline_wait_for_stop(pipeline);
            
            audio_element_reset_state(aac_decoder);
            audio_element_reset_state(output_stream_writer);
            ESP_LOGI(TAG, "URL: %s", AAC_STREAM_URI3);
            lv_label_set_text(label2, "苏州音乐广播");
            lv_obj_align(label2, NULL, LV_ALIGN_CENTER, 0, 0);
            lv_label_set_text(label3, "121.226.246.5");
            lv_obj_align(label3, NULL, LV_ALIGN_IN_BOTTOM_MID, 0, 0);
            audio_element_set_uri(http_stream_reader, AAC_STREAM_URI3);
            audio_pipeline_reset_ringbuffer(pipeline);
            audio_pipeline_reset_elements(pipeline);
            audio_pipeline_reset_items_state(pipeline);
            audio_pipeline_run(pipeline);
            vTaskDelay(1000 / portTICK_PERIOD_MS);
        }
    }
......

存在BUG

   1.校时时间有长有短,一次校时不超过可以复位重启,未校时会一直处于校时状态,无法进入其他功能;

   2.运行网络收音机过程中会出现如下报错语句,导致WiFi重启,播放出现卡顿,可能是网络方面的问题,连接手机热点出现过不卡顿的情况。

I (58147) wifi:bcn_timout,ap_probe_send_start
W (58157) PERIPH_WIFI: WiFi Event cb, Unhandle event_base:WIFI_EVENT, event_id:21
I (60657) wifi:ap_probe_send over, resett wifi status to disassoc

 

附件下载
Audio_Project.rar
全部代码,烧录时请将WiFi热点账号改为 ssid,密码改为 password,或在代码宏定义中把WiFi改成自己的
bin.rar
二进制代码(可直接烧录)bootloader.bin烧录至 0x1000 ;partition-table.bin烧录至0x8000 ;sntp.bin烧录至0x10000
团队介绍
一名金陵科技学院信息工程专业的大一学生~
团队成员
esp32小白
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号