项目需求分析:
1.可以通过WiFi接收网络上的电台,也可以通过FM模块接收空中的电台,并可以通过按键进行切换、选台
需求:连接wifi、fm频率搜索、fm频率优化、按键定义、网络服务器推流
2.在OLED显示屏上显示网络电台的IP地址、节目名字等相关信息或FM信号的频段
需求:Oled显示、网络服务器ip地址解析(FM频段之前已经实现)
3.系统能够自动校时,开机后自动调节到准确的时间(年、月、日、时、分、秒)
需求:从时间服务器获取时间并显示
硬件实现:
ESP32-S2-MINI-1:PCB板载天线,模组配置了4MB SPI flash,采用的是 ESP32-S2FN4 芯片。该芯片搭载了Xtensa® 32 位LX7 单核处理器,工作频率高达 240 MHz。用户可以关闭 CPU 的电源,利用低功耗协处理器监测外设的状态变化或某些模拟量是否超出阈值。ESP32-S2-FH4 还集成了丰富的外设接口。
可实现:wifi连接、获取网络服务器推流、程序主控、获取网络时间等
CH340C:USB总线的转接芯片,实现USB转串口或者USB转打印口,内置时钟,无需外部晶振
可实现:type-c口驱动、程序烧录、ticker等
RDA5807:FM收音机模块
可实现:FM频率搜索、输出等
NCP2890:1W功率的音频功率放大器
可实现:网络电台音频解码、播放
SPI_OLED:SPI驱动显示屏
可实现:GUI输出等
软件实现:
Arduino IDE:开源IDE,使用户能够创建交互式电子对象
可实现:程序编写、调试、烧录等工作
自制网络推流服务器:通过TCP协议进行音频文件的解码以及推流,最高可实现DSD256解码。基于Python实现(暂不开源)(现已更改公网ip以及关闭端口)
可实现:网络电台
程序架构分析:
初始化部分:
串口激活后进行显示器缓冲区置零,然后进行FM搜台并优化,再将结果置于存储器中等待调用,而后41、41管脚置于高阻,4按钮管脚置于高阻,之后wifi连接并返回本机IP,同时获取网络时间并校准晶振。至此,该程序初始化完毕,进入功能选择状态。
功能选择部分:
模式参数置于1,即时间界面,而后通过第一个按钮的阻抗变化(按下),来实现计数器的模3累加操作。2为FM模式,即关闭WI-FI模块,同时将存储器中的电台数据进行调用,以电台编号为变量来选择频率。选择后将目标频率发送至FM模块,以此来实现电台的调整。3模式为网络电台模式,即将预设好的服务器地址、端口号、密码等数据以客户端形式发送至服务端,待服务端正确返回后显示连接成功。同时由DAC模块驱动扬声器。由于本人使用的网络服务器可以对音频来进行处理,因此客户端不需要任何音频处理模块。
程序实现:
所调用头函数:
#include <ETH.h>
#include <WiFi.h>
#include <WiFiAP.h>
#include <WiFiClient.h>
#include <WiFiGeneric.h>
#include <WiFiMulti.h>
#include <WiFiScan.h>
#include <WiFiServer.h>
#include <WiFiSTA.h>
#include <WiFiType.h>
#include <WiFiUdp.h>
#include <SPI.h>
#include <U8g2lib.h>
#include <RDA5807.h>
#include <Ticker.h>
wifi模块:
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
delay(500);
u8g2.print(" CONNECTED");
u8g2.sendBuffer();
ipaddress = WiFi.localIP().toString();
初始化部分:
Serial.begin(115200);
u8g2.begin();
u8g2.firstPage();
u8g2.clearBuffer();
u8g2.setFontDirection(0);
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.setCursor(10, 40);
u8g2.print("Searching FM");
pinMode(41, OUTPUT);
pinMode(42, OUTPUT);
Wire.begin(ESP32_I2C_SDA, ESP32_I2C_SCL);
rx.setup();
rx.setVol(vol);
searchFM_chunnel();
rx.setFrequency(station[stations]);
selectDevice(curr_mod);
u8g2.firstPage();
u8g2.clearBuffer();
u8g2.setFontDirection(0);
u8g2.setCursor(10, 40);
u8g2.print("Connecting ");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
delay(500);
u8g2.print(" CONNECTED");
u8g2.sendBuffer();
ipaddress = WiFi.localIP().toString();
Serial.println(ipaddress.c_str());
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
pinMode(button1Pin, INPUT_PULLUP);
pinMode(button2Pin, INPUT_PULLUP);
pinMode(button3Pin, INPUT_PULLUP);
pinMode(button4Pin, INPUT_PULLUP);
程序实现部分:
uint16_t num = 0;
if (digitalRead(button1Pin) == LOW)
{
delay(200);
if (digitalRead(button1Pin) == LOW)
{
if (curr_mod == 2)
{
client.stop();
flipper.detach();
connstat = false;
}
curr_mod = (curr_mod + 1) % 3;
selectDevice(curr_mod);
if (curr_mod == 2)
connSever();
sec = -1;
dispStat();
}
}
if (curr_mod == 2 && connstat == true)
{
if (iswait == false && (write - read) < 2)
{
client.write('n');
iswait = true;
}
if (writep % 120 == 0)
display_Oled();
if (client.available())
{
num = client.read(netbuf[write % 3], 1024);
if (write == 0 && read == 0)
{
flipper.attach(0, onTimer);
}
write++;
iswait = false;
}
}
else
display_Oled();
if (curr_mod == 1)
{
if (digitalRead(button2Pin) == LOW)
{
delay(200);
if (digitalRead(button2Pin) == LOW)
{
changeStation();
dispStat();
sec = -1;
}
}
if (digitalRead(button3Pin) == LOW)
{
delay(200);
if (digitalRead(button3Pin) == LOW)
{
if (vol > 0) vol--;
rx.setVol(vol);
dispStat();
sec = -1;
}
}
if (digitalRead(button4Pin) == LOW)
{
delay(200);
if (digitalRead(button4Pin) == LOW)
{
vol++;
if (vol > 15) vol = 15;
rx.setVol(vol);
dispStat();
sec = -1;
}
}
}
三种模式下显示区域设置:
struct tm timeinfo;
char str[64] = {0};
if (!getLocalTime(&timeinfo))
{
Serial.println("Failed to obtain time");
return;
}
if (sec == timeinfo.tm_sec)
return;
sec = timeinfo.tm_sec;
u8g2.firstPage();
u8g2.clearBuffer();
u8g2.setFontDirection(0);
if (curr_mod == 0)
{
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.setCursor(25, 20);
u8g2.print(&timeinfo, "%H:%M:%S");
u8g2.setFont(u8g2_font_ncenB08_tf);
u8g2.setCursor(30, 36);
u8g2.print(&timeinfo, "%Y/%m/%d");
u8g2.setCursor(33, 54);
u8g2.print(ipaddress.c_str());
}
else if (curr_mod == 1)
{
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.setCursor(15, 20);
u8g2.print("FM Radio");
u8g2.setFont(u8g2_font_ncenB08_tf);
u8g2.setCursor(20, 32);
sprintf(str, "CH:%d/%d", stations + 1,availableStation());
u8g2.setCursor(15, 46);
sprintf(str, "Volume: %02d/%d", vol, 15);
u8g2.print(str);
u8g2.setCursor(18, 60);
sprintf(str, "Channel: %.2f MHz", station[stations]/100);
u8g2.print(str);
}
else if (curr_mod == 2)
{
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.setCursor(15, 20);
u8g2.print("Net Radio");
u8g2.setFont(u8g2_font_ncenB08_tf);
if (connstat == false)
{
u8g2.setCursor(25, 46);
u8g2.print("Unconnected");
u8g2.setCursor(20, 60);
u8g2.print("CONNECTION FAILED");
}
else
{
u8g2.setCursor(27, 46);
u8g2.print("connected");
u8g2.setCursor(20, 60);
sprintf(str, "%s:%d", WEBSERVERIP, WEBSERVERPORT);
u8g2.print(str);
}
}
u8g2.sendBuffer();
}
程序烧录:
添加ESP32-S2开发版支持:
由于我观察到这台机器出厂自带了一个程序,因此判断不用烧录bootloader等支持
添加USB转串口芯片驱动
选择模式
烧录
一些未来可以改进的点:
1、Wi-Fi连接信息只能烧录在程序中,无法进行适配,未来可以接入更大的显示屏以及输入模块进行扩展
2、音频输出质量极差,可通过换运放、扬声器等解决
3、功能单一,可通过添加外部传感器(pcb已留好接口)、麦克风(已经在pcb上)、天线(空焊位)等解决,可作为网络时钟,远程服务器无人值守监视器等
运行截图:
网络连接(启动界面):
主界面(时间、本机ip显示)
网络播放界面(服务器代码、端口显示)