开发板介绍
本次使用的开发板使用的是乐鑫的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收音机。
代码实现框图
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
烧录
代码分析
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