项目Github地址: https://github.com/Daweiandhislittlefriend/esp32s2-radio
克隆项目
git clone https://github.com/Daweiandhislittlefriend/esp32s2-radio.git
实现功能:
-
开机自动校时,若20秒内校时失败,则重启重新校时,直到校时成功
-
开机自动连接预设WiFi收听HLS协议的网络电台节目
-
收听空中FM电台,频率为87Mhz到108MHz
-
两种模式下都可实现静音与取消静音操作
-
通过按键切换网络收音机页面、FM收音机页面、时间显示页面
- 通过按键切换网络收音机模式下电台与FM收音机模式下频率
项目环境:
-
ESP-IDF v4.3.1:乐鑫ESP系列的基本开发环境,借鉴了hello world例程。官方文档ESP-IDF 编程指南。
-
ESP-ADF v2.3:乐鑫的音频开发框架,主要借鉴了pipeline_living_stream、pipeline_play_mp3_with_dac_or_pwm和play_mp3_control三个例程。官方文档ADF音频应用开发框架。
-
ESP-IOT-SOLUTION:Iot设备驱动和解决方案。(使用了其中的ssd1306驱动、lvgl8.1、按键单击,按键长按。将里面的lvgl7.5直接替换为lvgl8.1,ssd1306进行了一些修改,下文讲到)官方文档ESP-IoT-Solution 编程指南。
-
LVGL v8.1:嵌入式GUI库。百问网lvgl中文手册,lvgl官网。
-
ESP-IDF-LIB v0.8.2:基于IDF开发的一些常用芯片的库(项目中使用RDA5807M、I2CDEV的库)。
- Visual Studio Code中插件Espressif IDF v1.3.0。
硬件介绍:
- 关于使用到的元器件,PCB,原理图文件请参考项目网址。
-
收音机部分原理图简介
usb转串口电路,自动下载电路。
ESP32-S2模组周围电路
按键电路,按下后为低电平
电源电路,5V->3.3V
FM、模拟开关,音频功放电路。
设计思路:
1.项目前瞻
刚看到这个项目的时候,由于之前没有接触过ESP32的esp-idf框架,ESP32-S2也是第一次听说,自己确实没有一些解决办法,尤其时网络收音机的音频部分。据我研究发现大多数小伙伴使用的是arduino开发环境,这个环境我也接触过,界面简洁,编写代码也比较简单,但是我不太喜欢这个环境主要是因为它不能查看源代码(或者查看比较麻烦),所以我还是决定使用官方的esp-idf框架。由于暑假已经有很多同学做出来收音机这个项目,所以首要的参考文件就是他们暑假编写的代码,在我研究了几位同学的之后,我找到了一位实现全部功能并且实际演示效果较佳的同学的代码,DamingBear同学,我决心研究学习他的代码,研究了两周后,在这位热心师兄的帮助下,我大概弄懂了ESP32-S2的开发流程以及整个收音机搭建的来龙去脉,之后我开始自己移植那些功能,修改一些存在的已知的问题。终于利用寒假一个月时间实现了项目要求的所有功能。
2.设计框图
3.程序思路
如上框图,上电后进行一系列组件的初始化,最后进行SNTP校时,如果SNTP校时20秒内未成功,则设备会重启,直到校时成功。
ssd1306初始化参考ESP-IoT-Solution 编程指南中的例程,初始化spi频率为80MHz,之后调用lvgl_init(&g_lcd, NULL)函数初始化lvgl的配置,因为本项目没有使用到触摸屏,所以第二个参数为null并且未对触摸进行适配。
加载动画显示线程,只是创建了一个优先级最低的线程,在里面创建了一个屏幕,以50ms为周期对屏幕中的内容进行一些改变,之后在时间获取成功回调函数中删除该线程。
nvs_flash初始化和wifi连接参考play_living_stream_example例程,值得注意的是需要把该例程的Kconfig.projbuild文件拷贝到自己工程的main文件夹下,这是进行编译前预配置wifi的名字和密码,注意wifi只支持2.4GHz频段的。
创建网络电台页面和FM电台页面主要是进行屏幕和label的创建,同时设置各label使用的字体(本设计只用到了3种字体,lv_font_montserrat_20,Chinese_characters(自定义字体),UNSCII 8(默认))显示位置、显示内容、显示屏幕等。2个屏幕一个负责网络电台的信息显示、另一个负责FM电台的信息显示。网络电台显示的有:IP地址、节目名称、音频信息、当前时间。FM电台显示的有:频段、RSSI、当前时间。加上时间屏幕,三个屏幕同时存在,通过按键控制进行切换,切换时有个简单的滑动动画,程序可以设置动画时间,切换到另一个屏幕的时候,并没有删除另一个屏幕,仍然占据内存(也就几K),相当于作为后台运行。
独立按键也参考了ESP-IoT-Solution 编程指南中的例程,可以检测单击,双击,多击次数,长按这些状态,提供两种方式进行检测,轮询和回调。项目使用到了按键的单击和长按,使用的是独立按键的回调方式,关于按键时间的配置可以在menuconfig中进行。值得说明的是按键回调函数中正如官方文档中所说,最好不要使用延时函数,我经过实测这会影响系统的运行。在长按中我并不需要很快的调节FM的接收频率,所以使用了更新时间线程中的一个变量,让这个变量100ms增加一次,这样长按的时候只要这个变量发生变化,才去调节FM的接收频率,我觉得这种方法比较笨,但暂时没有深入研究其他办法,可能freertos中的功能可以排得上用场,读者可以尝试其他方法。
网络电台初始化的配置参考pipeline_living_stream、pipeline_play_mp3_with_dac_or_pwm和play_mp3_control。在运行过程中,首先会自动准备好播放第一个网络电台,先获取m3u8文件后,对文件中的aac音频地址进行下载、解码、播放,获取解码播放这些功能在ADF中被称作元素,这些元素都被登记到一个管道中。DamingBear同学已经将它们移植好了,我直接使用的,我们自己移植的方法就是认真研究这三个例程每行代码的意义,自己将需要的功能移植到自己的工程中。使用官方的音频开发框架可以省去开发人员大量的精力和时间,我们没必要重复造轮子。
FM5807初始化参考esp-idf-lib\examples\rda5807m,使用官方的库我们没必要关心具体是如何驱动的,但是遇到问题后我们不得不研究底层驱动是否有问题。
时间校准是直接套用了IDF中SNTP的例程,并且注册校准成功后的回调函数,在回调函数中创建了时间的屏幕和更新时间的线程,我们只要调用time(&now)函数更新时间即可,非常简单,之后只是为了显示进行的准备工作。我为了方便分割时间,将时间处理为这样的格式,例2022-01-28#20:59:59#Friday,之后调用C语言标准库中函数char *strtok(char *str, const char *delim)将时间按照“#”分割成三段显示。
关于CMAKE的构建的一些文件,比如CMakeLists.txt、sdkconfig、Kconfig.projbuild、component.mk、component.cmake的语法和使用方法本人也是略知一二,只是达到了能简单使用的水平,大家自行找相关资料研究学习。这是我CMakeLists.txt的内容。
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
include($ENV{ADF_PATH}/CMakeLists.txt)
include(${CMAKE_CURRENT_LIST_DIR}/components/esp-iot-solution/component.cmake)
add_definitions(-DCONFIG_LVGL_TFT_DISPLAY_MONOCHROME)
project(esp32s2-radio)
遇到的主要难题及解决方法:
俗话说前人栽树,后人乘凉。DamingBear同学已经帮我们踩了很多坑,特别感谢这位师兄。
1、ssd1306问题
我们需要按照下面图片更改。
2、解决oled显示亮度低问题,将屏幕调到最亮
3、rda5807不能正常使用问题,具体问题请参考那位师兄,我的解决办法如下
4、ssd1306移植问题,参考lvgl中的lv_port_esp32例程和乐鑫的esp-iot-solution\components\gui\lvgl_gui\lvgl_adapter.c文件
我们需要拷贝ex_disp_rounder和ex_disp_set_px函数。具体移植后代码如下
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "freertos/semphr.h"
#include "lvgl_adapter.h"
#include "sdkconfig.h"
static const char *TAG = "lvgl adapter";
static scr_driver_t lcd_obj;
static touch_panel_driver_t touch_obj;
static uint16_t g_screen_width = 128;
static uint16_t g_screen_height = 64;
static lv_disp_t * disp;
#define BIT_SET(a,b) ((a) |= (1U<<(b)))
#define BIT_CLEAR(a,b) ((a) &= ~(1U<<(b)))
static void ex_disp_rounder(lv_disp_drv_t * disp_drv, lv_area_t *area)
{
uint8_t hor_max = disp_drv->hor_res;
uint8_t ver_max = disp_drv->ver_res;
area->x1 = 0;
area->y1 = 0;
area->x2 = hor_max - 1;
area->y2 = ver_max - 1;
}
static void ex_disp_set_px(lv_disp_drv_t * disp_drv, uint8_t * buf, lv_coord_t buf_w, lv_coord_t x, lv_coord_t y,
lv_color_t color, lv_opa_t opa)
{
uint16_t byte_index = x + (( y>>3 ) * buf_w);
uint8_t bit_index = y & 0x7;
if ((color.full == 0) && (LV_OPA_TRANSP != opa)) {
BIT_SET(buf[byte_index], bit_index);
} else {
BIT_CLEAR(buf[byte_index], bit_index);
}
}
/*Write the internal buffer (VDB) to the display. 'lv_flush_ready()' has to be called when finished*/
static void ex_disp_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map)
{
lcd_obj.draw_bitmap(area->x1, area->y1, (uint16_t)(area->x2 - area->x1 + 1), (uint16_t)(area->y2 - area->y1 + 1), (uint16_t *)color_map);
/* IMPORTANT!!!
* Inform the graphics library that you are ready with the flushing*/
lv_disp_flush_ready(drv);
}
#define DISP_BUF_SIZE (g_screen_width * (g_screen_height / 8))
#define SIZE_TO_PIXEL(v) ((v) / sizeof(lv_color_t))
#define PIXEL_TO_SIZE(v) ((v) * sizeof(lv_color_t))
#define BUFFER_NUMBER (2)
esp_err_t lvgl_display_init(scr_driver_t *driver)
{
if (NULL == driver) {
ESP_LOGE(TAG, "Pointer of lcd driver is invalid");
return ESP_ERR_INVALID_ARG;
}
lcd_obj = *driver;
scr_info_t info;
lcd_obj.get_info(&info);
g_screen_width = info.width;
g_screen_height = info.height;
static lv_disp_drv_t disp_drv; /*Descriptor of a display driver 8.0后的版本需要设置为static! */
lv_disp_drv_init(&disp_drv); /*Basic initialization*/
disp_drv.hor_res = g_screen_width;
disp_drv.ver_res = g_screen_height;
disp_drv.flush_cb = ex_disp_flush; /*Used in buffered mode (LV_VDB_SIZE != 0 in lv_conf.h)*/
disp_drv.rounder_cb = ex_disp_rounder;
disp_drv.set_px_cb = ex_disp_set_px;
size_t free_size = heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
const size_t remain_size = 60 * 1024; /**< Remain for other functions */
size_t alloc_pixel = DISP_BUF_SIZE;
if (((BUFFER_NUMBER * PIXEL_TO_SIZE(alloc_pixel)) + remain_size) > free_size) {
size_t allow_size = (free_size - remain_size) & 0xfffffffc;
alloc_pixel = SIZE_TO_PIXEL(allow_size / BUFFER_NUMBER);
ESP_LOGW(TAG, "Exceeded max free size, force shrink to %u Byte", allow_size);
ESP_LOGW(TAG, "Free Size : %u Byte", free_size);
ESP_LOGW(TAG, "Used Size : %u Byte", (BUFFER_NUMBER * PIXEL_TO_SIZE(alloc_pixel)));
}
lv_color_t *buf1 = heap_caps_malloc(PIXEL_TO_SIZE(alloc_pixel), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
if (NULL == buf1) {
ESP_LOGE(TAG, "Display buffer memory not enough");
return ESP_ERR_NO_MEM;
}
#if (BUFFER_NUMBER == 2)
lv_color_t *buf2 = heap_caps_malloc(PIXEL_TO_SIZE(alloc_pixel), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
if (NULL == buf2) {
heap_caps_free(buf1);
ESP_LOGE(TAG, "Display buffer memory not enough");
return ESP_ERR_NO_MEM;
}
#endif
ESP_LOGI(TAG, "Alloc memory total size: %u Byte", BUFFER_NUMBER * PIXEL_TO_SIZE(alloc_pixel));
static lv_disp_draw_buf_t disp_buf;
/* Actual size in pixels */
alloc_pixel *= 8;
#if (BUFFER_NUMBER == 2)
lv_disp_draw_buf_init(&disp_buf, buf1, buf2, alloc_pixel);
#else
lv_disp_draw_buf_init(&disp_buf, buf1, NULL, alloc_pixel);
#endif
disp_drv.draw_buf = &disp_buf;
/* Finally register the driver */
disp = lv_disp_drv_register(&disp_drv);
return ESP_OK;
}
/*Function pointer to read data. Return 'true' if there is still data to be read (buffered)*/
static void ex_tp_read(struct _lv_indev_drv_t *indev_drv, lv_indev_data_t *data)
{
data->state = LV_INDEV_STATE_REL;
touch_panel_points_t points;
touch_obj.read_point_data(&points);
// please be sure that your touch driver every time return old (last clcked) value.
if (TOUCH_EVT_PRESS == points.event) {
int32_t x = points.curx[0];
int32_t y = points.cury[0];
data->point.x = x;
data->point.y = y;
data->state = LV_INDEV_STATE_PR;
}
return ;
}
/* Input device interface,Initialize your touchpad */
esp_err_t lvgl_indev_init(touch_panel_driver_t *driver)
{
if (NULL == driver) {
ESP_LOGE(TAG, "Pointer of touch driver is invalid");
return ESP_ERR_INVALID_ARG;
}
touch_obj = *driver;
lv_indev_drv_t indev_drv; /*Descriptor of an input device driver*/
lv_indev_drv_init(&indev_drv); /*Basic initialization*/
indev_drv.type = LV_INDEV_TYPE_POINTER; /*The touchpad is pointer type device*/
indev_drv.read_cb = ex_tp_read; /*Library ready your touchpad via this function*/
lv_indev_drv_register(&indev_drv); /*Finally register the driver*/
return ESP_OK;
}
主要代码片段:
1、按键回调函数
static void button_num_0_cb(void *arg)
{
if (iot_button_get_event((button_handle_t)arg) == BUTTON_SINGLE_CLICK)
{
if (lv_scr_act() == display_time_scr)
{
lv_scr_load_anim(internet_scr, LV_SCR_LOAD_ANIM_MOVE_LEFT, 400, 0, false);
play_living_stream_restart();
ESP_LOGI(TAG, "changed to internet_scr\n");
}
else if (lv_scr_act() == internet_scr&&cnt%2==0)
{
/* 停止living_stream */
play_living_stream_end();
lv_scr_load_anim(FM_scr, LV_SCR_LOAD_ANIM_MOVE_LEFT, 400, 0, false);
xTaskCreate(updata_rda5807m_info_task, "updata_rda5807m_task", 2048, NULL, 0, &updata_rda5807m_info_task_handle);
// 模拟开关切换至FM输出
gpio_set_level(GPIO_NUM_42, 0);
ESP_LOGI(TAG, "changed to FM_scr\n");
}
else if (lv_scr_act() == FM_scr)
{
// 模拟开关切换至网络电台输出
gpio_set_level(GPIO_NUM_42, 1);
lv_scr_load_anim(display_time_scr, LV_SCR_LOAD_ANIM_MOVE_LEFT, 400, 0, false);
vTaskDelete(updata_rda5807m_info_task_handle); // 删除FM屏幕上RDA5807M信息更新任务
ESP_LOGI(TAG, "changed to display_time_scr\n");
}
}
}
uint32_t num_button=0;
static void button_num_1_cb(void *arg)
{
if (iot_button_get_event((button_handle_t)arg) == BUTTON_SINGLE_CLICK)
{
if (internet_scr == lv_scr_act()&&cnt%2==0)
{
// 当按下按键,且当前屏幕为internet_scr 时,才能切换电台
// 切换下一个电台URL 参考mp3_control
HLS_list_index++;
if (HLS_list_index == MAX_HLS_URL_NUM)
HLS_list_index = 0;
audio_pipeline_stop(pipeline);
audio_pipeline_wait_for_stop(pipeline);
audio_pipeline_reset_ringbuffer(pipeline);
audio_pipeline_reset_elements(pipeline);
audio_element_set_uri(http_stream_reader, HLS_list[HLS_list_index].hls_url);
audio_pipeline_run(pipeline);
lv_label_set_text_fmt(radio_label, "%s", HLS_list[HLS_list_index].program_name);
}
else if (FM_scr == lv_scr_act())
{
rda5807m_app_add_frequency(100);
lv_label_set_text_fmt(fre_label, "%.1fMHz", ((float)rda5807m_current_fre/1000));
}
}
else if (iot_button_get_event((button_handle_t)arg) == BUTTON_LONG_PRESS_HOLD)
{
if (FM_scr == lv_scr_act())
{
if(num_button!=num_time)
{
num_button=num_time;
rda5807m_app_add_frequency(100);
lv_label_set_text_fmt(fre_label, "%.1fMHz", ((float)rda5807m_current_fre/1000));
}
}
}
}
static void button_num_2_cb(void *arg)
{
if (iot_button_get_event((button_handle_t)arg) == BUTTON_SINGLE_CLICK)
{
if ( internet_scr == lv_scr_act()&&cnt%2==0)
{
// 切换上一个电台URL 参考mp3_control
HLS_list_index--;
if (HLS_list_index == 255)
HLS_list_index = MAX_HLS_URL_NUM - 1;
audio_pipeline_stop(pipeline);
audio_pipeline_wait_for_stop(pipeline);
audio_pipeline_reset_ringbuffer(pipeline);
audio_pipeline_reset_elements(pipeline);
audio_element_set_uri(http_stream_reader, HLS_list[HLS_list_index].hls_url);
audio_pipeline_run(pipeline);
lv_label_set_text_fmt(radio_label, "%s", HLS_list[HLS_list_index].program_name);
}
else if(FM_scr==lv_scr_act())
{
rda5807m_app_reduce_frequency(100);
lv_label_set_text_fmt(fre_label, "%.1fMHz", ((float)rda5807m_current_fre/1000));
}
}
else if (iot_button_get_event((button_handle_t)arg) == BUTTON_LONG_PRESS_HOLD)
{
if(FM_scr==lv_scr_act())//400ms减少一次
{
if(num_button!=num_time)
{
num_button=num_time;
rda5807m_app_reduce_frequency(100);
lv_label_set_text_fmt(fre_label, "%.1fMHz", ((float)rda5807m_current_fre/1000));
}
}
}
}
static void button_num_3_cb(void *arg)
{
static bool mute=0;
if (iot_button_get_event((button_handle_t)arg) == BUTTON_SINGLE_CLICK)
{
if ( internet_scr == lv_scr_act())
{
if(cnt%2==0)
{
audio_pipeline_stop(pipeline);
audio_pipeline_wait_for_stop(pipeline);
lv_label_set_text(int_sound_icon,LV_SYMBOL_MUTE);
}
else
{
audio_pipeline_reset_ringbuffer(pipeline);
audio_pipeline_reset_elements(pipeline);
audio_pipeline_run(pipeline);
lv_label_set_text(int_sound_icon,LV_SYMBOL_VOLUME_MAX);
}
cnt++;
}
else if(FM_scr==lv_scr_act())
{
rda5807m_get_mute(&rda5807m_dev, &mute);
if(mute==false)
{
rda5807m_set_mute(&rda5807m_dev, true);
lv_label_set_text(fm_sound_icon,LV_SYMBOL_MUTE);
}
else
{
rda5807m_set_mute(&rda5807m_dev, false);
lv_label_set_text(fm_sound_icon,LV_SYMBOL_VOLUME_MAX);
}
}
}
}
2、获取时间线程
void time_task(void *pvParameters)
{
time_t now = 0;
struct tm timeinfo = { 0 };
char strftime_buf[64];
gpio_reset_pin(LED_GPIO);
/* Set the GPIO as a push/pull output */
gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);
setenv("TZ", "CST-8", 1);
tzset();
vTaskDelete( anim_xHandle ); //删除开机动画任务
lv_scr_load(display_time_scr);
while (1)
{
if(num_time%10==0)
{
time(&now); // update 'now' variable with current time
localtime_r(&now, &timeinfo);
strftime(strftime_buf, sizeof(strftime_buf), "%Y-%m-%d#%H:%M:%S#%A", &timeinfo); //例2022-01-28#20:59:59#Friday
//ESP_LOGI(TAG, "The current date/time in Shanghai is: %s", strftime_buf);
lv_label_set_text(time_label1, strtok(strftime_buf, "#"));//分割字符串
strcpy(data_time_label,strtok(NULL, "#"));
lv_label_set_text(time_label2,data_time_label);
lv_label_set_text(data_time_label1, data_time_label);
lv_label_set_text(data_time_label2, data_time_label);
lv_label_set_text(time_label3, strtok(NULL, "#"));
}
num_time++;
gpio_set_level(LED_GPIO,(num_time/10)%2);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
3、网络电台初始化函数
void play_living_stream_start(void)
{
esp_log_level_set("*", ESP_LOG_INFO);
esp_log_level_set(TAG, ESP_LOG_DEBUG);
ESP_LOGI(TAG, "[1.0] Create audio pipeline for playback");
audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
pipeline = audio_pipeline_init(&pipeline_cfg);
ESP_LOGI(TAG, "[1.1] Create http stream to read data");
http_stream_cfg_t http_cfg = HTTP_STREAM_CFG_DEFAULT();
http_cfg.event_handle = _http_stream_event_handle;
http_cfg.type = AUDIO_STREAM_READER;
http_cfg.enable_playlist_parser = true;
http_stream_reader = http_stream_init(&http_cfg);
ESP_LOGI(TAG, "[2.2] Create PWM stream to write data to codec chip");
pwm_stream_cfg_t pwm_cfg = PWM_STREAM_CFG_DEFAULT();
pwm_cfg.pwm_config.gpio_num_left = 17;
pwm_cfg.pwm_config.gpio_num_right = 18;
output_stream_writer = pwm_stream_init(&pwm_cfg);
ESP_LOGI(TAG, "[2.3] Create aac decoder to decode aac file");
aac_decoder_cfg_t aac_cfg = DEFAULT_AAC_DECODER_CONFIG();
aac_decoder = aac_decoder_init(&aac_cfg);
ESP_LOGI(TAG, "[2.4] Register all elements to audio pipeline");
audio_pipeline_register(pipeline, http_stream_reader, "http");
audio_pipeline_register(pipeline, aac_decoder, "aac");
audio_pipeline_register(pipeline, output_stream_writer, "output");
ESP_LOGI(TAG, "[2.5] Link it together http_stream-->aac_decoder-->pwm_stream-->[codec_chip]");
const char *link_tag[3] = {"http", "aac", "output"};
audio_pipeline_link(pipeline, &link_tag[0], 3);
ESP_LOGI(TAG, "[2.6] Set up uri (http as http_stream, aac as aac decoder, and output is PWM)");
audio_element_set_uri(http_stream_reader, HLS_list[0].hls_url);
ESP_LOGI(TAG, "[ 3 ] Set up event listener");
audio_event_iface_cfg_t evt_cfg = AUDIO_EVENT_IFACE_DEFAULT_CFG();
evt = audio_event_iface_init(&evt_cfg);
ESP_LOGI(TAG, "[3.1] Listening event from all elements of pipeline");
audio_pipeline_set_listener(pipeline, evt);
ESP_LOGI(TAG, "[3.2] Listening event from peripherals");
audio_event_iface_set_listener(esp_periph_set_get_event_iface(set), evt);
ESP_LOGI(TAG, "[ 4 ] Start audio_pipeline");
// audio_pipeline_run(pipeline);
xTaskCreate(living_stream_task, "living_stream_task", 4096, NULL, 25, NULL);
//play_living_stream_end();
}
4、关于url结构体的声明和定义
#define MAX_HLS_URL_NUM (20)
typedef struct
{
const char * hls_url;
const char * program_name;
} HLS_INFO_t;
uint8_t HLS_list_index = 0;
HLS_INFO_t HLS_list[MAX_HLS_URL_NUM] = {
{.hls_url = "http://open.ls.qingting.fm/live/386/64k.m3u8?format=aac", .program_name = "CNR中国之声"},
{.hls_url = "http://open.ls.qingting.fm/live/4985/64k.m3u8?format=aac", .program_name = "CNR中国交通广播"},
{.hls_url = "http://open.ls.qingting.fm/live/274/64k.m3u8?format=aac", .program_name = "上海动感101"},
{.hls_url = "http://open.ls.qingting.fm/live/4804/64k.m3u8?format=aac", .program_name = "怀集音乐之声"},
{.hls_url = "http://open.ls.qingting.fm/live/1005/64k.m3u8?format=aac", .program_name = "CRI环球资讯"},
{.hls_url = "http://open.ls.qingting.fm/live/387/64k.m3u8?format=aac", .program_name = "CNR经济之声"},
{.hls_url = "http://open.ls.qingting.fm/live/398/64k.m3u8?format=aac", .program_name = "阅读之声"},
{.hls_url = "http://open.ls.qingting.fm/live/1006/64k.m3u8?format=aac", .program_name = "轻松调频EZFM"},
{.hls_url = "http://open.ls.qingting.fm/live/5022405/64k.m3u8?format=aac", .program_name = "亚洲音乐台"},
{.hls_url = "http://open.ls.qingting.fm/live/20091/64k.m3u8?format=aac", .program_name = "中国校园之声"},
{.hls_url = "http://open.ls.qingting.fm/live/4900/64k.m3u8?format=aac", .program_name = "太原交通广播"},
{.hls_url = "http://open.ls.qingting.fm/live/20006/64k.m3u8?format=aac", .program_name = "太原综合广播"},
{.hls_url = "http://open.ls.qingting.fm/live/1654/64k.m3u8?format=aac", .program_name = "石家庄音乐广播"},
{.hls_url = "http://open.ls.qingting.fm/live/20485/64k.m3u8?format=aac", .program_name = "山西文艺广播"},
{.hls_url = "http://open.ls.qingting.fm/live/4900/64k.m3u8?format=aac", .program_name = "太原交通广播"},
{.hls_url = "http://open.ls.qingting.fm/live/5022396/64k.m3u8?format=aac", .program_name = "大同交通广播"},
{.hls_url = "http://open.ls.qingting.fm/live/15318393/64k.m3u8?format=aac", .program_name = "河南电台网络戏曲广播"},
{.hls_url = "http://open.ls.qingting.fm/live/1649/64k.m3u8?format=aac", .program_name = "河北音乐广播"},
{.hls_url = "http://open.ls.qingting.fm/live/4932/64k.m3u8?format=aac", .program_name = "山西音乐广播"},
{.hls_url = "http://open.ls.qingting.fm/live/1007/64k.m3u8?format=aac", .program_name = "CRI HIT FM"},
};
代码使用:
请参考我视频中的配置或者DamingBear同学的配置,基本一致。
相关展示:
1、编译后总代码大小:
Total sizes:
DRAM .data size: 15864 bytes
DRAM .bss size: 20568 bytes
Used static DRAM: 0 bytes ( 0 available, nan% used)
Used static IRAM: 0 bytes ( 0 available, nan% used)
Used stat D/IRAM: 101759 bytes ( 275073 available, 27.0% used)
Flash code: 779963 bytes
Flash rodata: 202052 bytes
Total image size:~1063206 bytes (.bin may be padded larger)
2、系统启动后运行网络电台1分钟时,各线程占用资源情况,可以看到lvgl占用的CPU时间最长,可能时屏幕刷新66帧和label循环显示需要有较大的计算量。
3、作品图片展示
lvgl帧率显示
时间显示页面
网络收音机
FM收音机
日志打印
I (21) boot: ESP-IDF v4.3.1 2nd stage bootloader
I (21) boot: compile time 23:15:58
I (21) boot: chip revision: 0
I (24) qio_mode: Enabling default flash chip QIO
I (29) boot.esp32s2: SPI Speed : 80MHz
I (34) boot.esp32s2: SPI Mode : QIO
I (39) boot.esp32s2: SPI Flash Size : 4MB
I (43) boot: Enabling RNG early entropy source...
I (49) boot: Partition Table:
I (52) boot: ## Label Usage Type ST Offset Length
I (60) boot: 0 nvs WiFi data 01 02 00009000 00006000
I (67) boot: 1 phy_init RF data 01 01 0000f000 00001000
I (75) boot: 2 factory factory app 00 00 00010000 00300000
I (82) boot: End of partition table
I (86) esp_image: segment 0: paddr=00010020 vaddr=3f000020 size=3163ch (202300) map
I (129) esp_image: segment 1: paddr=00041664 vaddr=3ffc1f30 size=03df8h ( 15864) load
I (133) esp_image: segment 2: paddr=00045464 vaddr=40022000 size=0abb4h ( 43956) load
I (145) esp_image: segment 3: paddr=00050020 vaddr=40080020 size=be3dch (779228) map
I (279) esp_image: segment 4: paddr=0010e404 vaddr=4002cbb4 size=0537ch ( 21372) load
I (284) esp_image: segment 5: paddr=00113788 vaddr=50000000 size=00010h ( 16) load
I (293) boot: Loaded app from partition at offset 0x10000
I (293) boot: Disabling RNG early entropy source...
I (307) c���f�~���~ruction cache : size 8KB, 4Ways, cache line size 32Byte
I (308) cpu_start: Pro cpu up.
I (321) cpu_start: Pro cpu start user code
I (321) cpu_start: cpu freq: 240000000
I (321) cpu_start: Application information:
I (326) cpu_start: Project name: esp32s2-radio
I (332) cpu_start: App version: 1
I (336) cpu_start: Compile time: Feb 5 2022 23:18:19
I (342) cpu_start: ELF file SHA256: 38e0084c606d8ae1...
I (348) cpu_start: ESP-IDF: v4.3.1
I (353) heap_init: Initializing. RAM available for dynamic allocation:
I (360) heap_init: At 3FF9E000 len 00002000 (8 KiB): RTCRAM
I (366) heap_init: At 3FFCAD80 len 00031280 (196 KiB): DRAM
I (373) heap_init: At 3FFFC000 len 00003A10 (14 KiB): DRAM
I (379) spi_flash: detected chip: generic
I (384) spi_flash: flash io: qio
I (391) cpu_start: Starting scheduler on PRO CPU.
I (393) gpio: GPIO[41]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (393) gpio: GPIO[42]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (403) spi_bus: SPI2 bus created
I (413) spi_bus: SPI2 bus device added, CS=38 Mode=0 Speed=80000000
I (613) lvgl_gui: GUI Run at esp32s2 Pinned to Core0
I (613) lvgl adapter: Alloc memory total size: 2048 Byte
I (613) lvgl_gui: Start to run LVGL
I (623) PLAY_LIVING_STREAM: Start and wait for Wi-Fi network
I (633) wifi:wifi driver task: 3ffd7030, prio:23, stack:6656, core=0
I (633) system_api: Base MAC address is not set
I (633) system_api: read default base MAC address from EFUSE
I (643) wifi:wifi firmware version: 88c8747
I (643) wifi:wifi certification version: v7.0
I (643) wifi:config NVS flash: enabled
I (653) wifi:config nano formating: disabled
I (653) wifi:Init data frame dynamic rx buffer num: 32
I (653) wifi:Init management frame dynamic rx buffer num: 32
I (663) wifi:Init management short buffer num: 32
I (663) wifi:Init dynamic tx buffer num: 32
I (673) wifi:Init static rx buffer size: 1600
I (673) wifi:Init static rx buffer num: 10
I (683) wifi:Init dynamic rx buffer num: 32
I (693) wifi_init: rx ba win: 6
I (693) wifi_init: tcpip mbox: 32
I (693) wifi_init: udp mbox: 6
I (693) wifi_init: tcp mbox: 6
I (703) wifi_init: tcp tx win: 5744
I (703) wifi_init: tcp rx win: 5744
I (703) wifi_init: tcp mss: 1440
I (713) phy_init: phy_version 1800,e7ef680,Apr 13 2021,11:45:08
I (793) wifi:mode : sta (7c:df:a1:97:3f:f2)
I (793) wifi:enable tsf
I (923) wifi:new:<2,0>, old:<1,0>, ap:<255,255>, sta:<2,0>, prof:1
I (1923) wifi:state: init -> auth (b0)
I (1933) wifi:state: auth -> assoc (0)
I (1943) wifi:state: assoc -> run (10)
I (1963) wifi:connected with radio, aid = 2, channel 2, BW20, bssid = fe:f1:3b:0f:20:d1
I (1963) wifi:security: WPA2-PSK, phy: bgn, rssi: -25
I (1963) wifi:pm start, type: 1
W (1963) PERIPH_WIFI: WiFi Event cb, Unhandle event_base:WIFI_EVENT, event_id:4
I (2003) wifi:AP's beacon interval = 102400 us, DTIM period = 2
I (2933) esp_netif_handlers: sta ip: 192.168.234.196, mask: 255.255.255.0, gw: 192.168.234.238
I (2933) PERIPH_WIFI: Got ip:192.168.234.196
I (2943) gpio: GPIO[1]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (2943) board_button: Button[0] created
I (2953) gpio: GPIO[2]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (2963) board_button: Button[1] created
I (2963) gpio: GPIO[3]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (2973) board_button: Button[2] created
I (2973) gpio: GPIO[6]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (2983) board_button: Button[3] created
I (2993) PLAY_LIVING_STREAM: [1.0] Create audio pipeline for playback
I (2993) PLAY_LIVING_STREAM: [1.1] Create http stream to read data
I (3003) PLAY_LIVING_STREAM: [2.2] Create PWM stream to write data to codec chip
I (3013) PLAY_LIVING_STREAM: [2.3] Create aac decoder to decode aac file
I (3023) PLAY_LIVING_STREAM: [2.4] Register all elements to audio pipeline
I (3023) PLAY_LIVING_STREAM: [2.5] Link it together http_stream-->aac_decoder-->pwm_stream-->[codec_chip]
I (3033) AUDIO_PIPELINE: link el->rb, el:0x3ffdd718, tag:http, rb:0x3ffe0310
I (3043) AUDIO_PIPELINE: link el->rb, el:0x3ffdff80, tag:aac, rb:0x3ffe5458
I (3053) PLAY_LIVING_STREAM: [2.6] Set up uri (http as http_stream, aac as aac decoder, and output is PWM)
I (3063) PLAY_LIVING_STREAM: [ 3 ] Set up event listener
I (3073) PLAY_LIVING_STREAM: [3.1] Listening event from all elements of pipeline
I (3073) PLAY_LIVING_STREAM: [3.2] Listening event from peripherals
I (3083) PLAY_LIVING_STREAM: [ 4 ] Start audio_pipeline
I (3093) rda5807m: Device initialized
I (3093) rda5807m: Frequency: 103300 kHz
I (3103) rda5807m: Volume set to 15
I (3103) GET_TIME: Initializing SNTP
I (3113) GET_TIME: Waiting for system time to be set... (1/10)
I (5113) GET_TIME: Waiting for system time to be set... (2/10)
W (6633) wifi:<ba-add>idx:0 (ifx:0, fe:f1:3b:0f:20:d1), tid:6, ssn:4, winSize:64
I (7113) GET_TIME: Waiting for system time to be set... (3/10)
W (7133) wifi:<ba-add>idx:1 (ifx:0, fe:f1:3b:0f:20:d1), tid:0, ssn:4, winSize:64
I (7143) GET_TIME: Notification of a time synchronization event,Start the time synchronization thread
I (7143) gpio: GPIO[16]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (26563) AUDIO_ELEMENT: [http-0x3ffdd718] Element task created
I (26563) AUDIO_ELEMENT: [aac-0x3ffdff80] Element task created
I (26563) AUDIO_ELEMENT: [output-0x3ffdfbbc] Element task created
I (26573) AUDIO_PIPELINE: Func:audio_pipeline_run, Line:359, MEM Total:81712 Bytes
I (26583) AUDIO_ELEMENT: [http] AEL_MSG_CMD_RESUME,state:1
I (26593) AUDIO_ELEMENT: [aac] AEL_MSG_CMD_RESUME,state:1
I (26593) AUDIO_ELEMENT: [output] AEL_MSG_CMD_RESUME,state:1
I (26593) AUDIO_PIPELINE: Pipeline started
I (26603) board_button: changed to internet_scr
I (26793) HTTP_STREAM: total_bytes=781
I (26813) HTTP_STREAM: Live stream URI. Need to be fetched again!
I (26973) HTTP_STREAM: total_bytes=57856
I (26973) CODEC_ELEMENT_HELPER: The element is 0x3ffdff80. The reserve data 2 is 0x0.
I (26973) AAC_DECODER: a new song playing
I (26973) AAC_DECODER: this audio is RAW AAC
I (26993) PLAY_LIVING_STREAM: Receive music info from aac decoder, sample_rates=24000, bits=16, ch=2
W (30993) HTTP_STREAM: No more data,errno:0, total_bytes:57856, rlen = 0
I (30993) AUDIO_ELEMENT: IN-[http] AEL_IO_DONE,0
I (31043) HTTP_STREAM: total_bytes=57787
W (32233) AUDIO_ELEMENT: OUT-[aac] AEL_IO_ABORT
W (32233) AAC_DECODER: output aborted -3 4096
I (32233) AAC_DECODER: Closed by [3]
W (32253) AUDIO_ELEMENT: OUT-[http] AEL_IO_ABORT
I (32253) AUDIO_PIPELINE: Func:audio_pipeline_run, Line:359, MEM Total:79480 Bytes
WiFi输出日志中会出现警告:W (1591) PERIPH_WIFI: WiFi Event cb, Unhandle event_base:WIFI_EVENT, event_id:4
长时间收听网络电台偶尔会发生重启