项目功能要求:
1.可以通过WiFi加载本地网络能连接到的地址里的音频文件,也可以通过FM模块接收空中的电台,并可以通过按键进行切换、选台
2.在OLED显示屏上显示网络电台的IP地址、FM信号的频段
3.系统能够自动校时,开机后自动调节到准确的时间(年、月、日、时、分、秒)
组件列表:
硬件组件列表:
ESP32-S2-MINI-1:搭载PCB板载天线,模组配置了4MB SPI flash,采用的是 ESP32-S2FN4 芯片。该芯片搭载了Xtensa 32 位LX7 单核处理器,工作频率高达 240 MHz。用户可以关闭 CPU 的电源,利用低功耗协处理器监测外设的状态变化或某些模拟量是否超出阈值。ESP32-S2-FH4 还集成了丰富的外设接口。
CH340C:自带始终的USB串口芯片,带有type-c驱动以及串口通讯功能
RDA5807:FM收音机模块
NCP2890:音频功放
SPI_OLED:SPI驱动显示屏
杜邦线:FM模式下更好地接受电台信号
编译软件、驱动以及环境:
Arduino IDE
ESP-IDF v4.2:乐鑫ESP系列的基本开发环境
CH340串口驱动 使bin文件烧录至板中
代码分析:
初始化:
//Time
const char *ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 8 * 3600;
const int daylightOffset_sec = 0;
//Wifi
const char* ssid = "XYY_Wifi";
const char* password = "1120018biteducn";
//OLED
U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/ 36, /* data=*/ 35, /* cs=*/ 46, /* dc=*/ 33, /* reset=*/ 34);
Wifi-FM模式切换:
//从网络上获取音频
if (current_mode == 2 && whether_connected == true) {
if (iswaitecho == false && (writep - readp) < 2) {
client.write('n');
iswaitecho = true;
}
if (writep % 120 == 0) dispOled();
if (client.available()) {
num = client.read(buffer_net[writep % 3], 1024);
if (writep == 0 && readp == 0) {
flipper.attach(0, onTimer); //定时中断20KHz
}
writep++;
iswaitecho = false;
}
} else {
dispOled(); //一旦网络连接开始播放音乐,
}
void loop() {
uint16_t num = 0;
if (digitalRead(switch1) == LOW) // 切换Wifi/FM
{
delay(80); //消抖
if (digitalRead(switch1) == LOW) {
if (current_mode == 2) {
client.write('q');
client.stop();
flipper.detach();
whether_connected = false;
}
current_mode = (current_mode + 1) % 3;
switch_mode(current_mode);
if (current_mode == 2) { //网络模式
server_connect();//试图连接服务器
}
oldsec = -1;
dispSerCustStat();
}
}
FM模式下的频道切换:
void switch_channel() {
while (1) {
if (current_channel == capacity_of_channel - 1) {
current_channel = 0;
rx.setFrequency(fm_Channel[current_channel]);
//Serial.println(fm_Channel[current_channel]);
return;
}
if (fm_Channel[current_channel + 1] != 0) {
rx.setFrequency(fm_Channel[current_channel + 1]);
current_channel++;
Serial.println(fm_Channel[current_channel]);
return;
}
current_channel++;
}
}
频道计数:
uint8_t countChannel() {
uint8_t i;
for (i = 0; i < capacity_of_channel; i++) {
if (fm_Channel[i] == 0) return i;
}
return i;
}
从网络获取音频:
uint8_t buffer_net[3][1024]; //网络数据缓冲区
uint16_t writep = 0;
uint16_t readp = 0;
WiFiClient client; //此cilent与服务器链接
bool whether_connected = false;
bool iswaitecho = false;
Ticker flipper; //定时器中断;
uint16_t m_offset = 0;
#define IP_server "192.168.0.125"
#define port_server 1473
void onTimer(void) {
if (readp <= writep) dacWrite(17, buffer_net[readp % 3][m_offset++]);
if (m_offset >= 1024) {
m_offset = 0;
readp++; //读取完成一个缓冲区
}
}
//连接服务器
bool server_connect() {
uint8_t i = 0;
while (i < 5) { //尝试次数最大=5
if (client.connect(IP_server, port_server)) {
whether_connected = true;
return true;
} else {
Serial.println("Connection failed");
client.stop();
}
i++;
delay(100);
}
return false;
}
技术关键点解析
如何从服务器获取音频:首先,获取音频流。程序使用的 http 协议从一个服务器上面获取的音频数据,并将整个数据存放到一个 buffer 中。
对音频流进行解码。当 buffer 中有一定的数据后(可以通过宏进行调整),开启解码线程。解码线程会从这个 buffer 中取出数据,然后调用解码库,将音频流解码为可直接输出的数字信号。
将解码后的数据通过 DAC 输出。解码线程每解完一帧数据后,将它通过 I2S 驱动程序直接送给 DAC。
这里面存在一个同步的问题,即从服务器上面获取音频流与对音频流进行解码的同步。(本程序中采用Ticker中断)如果获取的音频流过快,超过了解码的速度,则buffer有溢出风险,因此会丢失部分数据;如果解码的速度过快,超过了获取音频的速度,则它可能将 buffer 消耗的干干净净,从而影响音质。
这里服务器的音频为WAV模式,通过python脚本从公网上的URL地址中的音频爬取到自己的服务器IP上。
参考:[1]https://how2electronics.com/simple-esp32-internet-web-radio-with-oled-display/