ESP8266驱动RDA5807实现的FM收音机,可连接WiFi显示B站粉丝数

记得我在读书的时候,也做过一个收音机,当时使用红米1S的手机包装盒做的成品如下图:
小米盒子收音机
收音机模块采用的是RDA5807,主控芯片采用的是51单片机,配合OLED12864显示屏和红外接收头,实现了遥控搜台和频率显示。配合牛皮纸材质的手机盒,整体彰显出一种小清新的感觉。

毕业之后,我也一直把这个小收音机带在身边。一个人背井离乡,在陌生的城市搬了几次家后,一些重要的东西就逐渐丢失了。于是我准备再复刻一版这个收音机,算是对大学时光的怀念吧。

小米手机早已不再使用牛皮纸包装盒,想要100%复刻似乎不大可能。为了能使制作收音机的材料更容易获取,我准备采用PCB板做外壳,薅一把板厂的羊毛。其他小伙伴若有兴趣,也更容易仿制。

制作好的成品如下图:

新版收音机成品图

硬件原理图

硬件原理图

新设计采用了ESP8266作为主控芯片,显示屏使用的是240*240的LCD,WS2812用作信号指示灯。另配备了CD42充放电模块,断开充电线也能使用。详细的网络连接如下图:

8266引脚所连接的模块
0编码器按键
2编码器旋钮 A相
4RDA5807 -->SCL
5RDA5807 -->SDA
12WS2812_IN
13LCD_SPI_MOSI
14LCD_SPI_SCL
15LCD_DC
16编码器旋钮 B相

硬件渲染图

渲染图正面
渲染图背面

焊接好的实物图

软件代码

为了方便开发和修改,软件是采用MicroPython实现的,核心代码如下:

main.py

import time
from machine import Timer,Pin

from ST7789V import LCD_240_240
from biliflow import BiliFlow
from RDA58XX import RDA5807
from neopixel import NeoPixel


tim = Timer(-1)

bf = BiliFlow() #b站粉丝数量显示
lcd = LCD_240_240()#显示屏驱动
rda = RDA5807() #收音机启动
np = NeoPixel(Pin(12, Pin.OUT) , 1)   #WS2812驱动 

lcd.Disp_Pic(0x320000) #显示背景图片

def bilibili(t): #定时刷新B站的粉丝数
  r, g, b = np[0]
  np[0] = (50, g, b)
  np.write()
  lcd.Disp_Str(90,140,str(bf.get_follower()).encode('utf-8'))
  np[0] = (0, g, b)
  np.write()

tim.init(period=10000, mode=Timer.PERIODIC, callback=bilibili)

key0 = Pin(0, Pin.IN, Pin.PULL_UP)
key0_statue = key0.value()

key1 = Pin(2, Pin.IN, Pin.PULL_UP)
key1_statue = key1.value()

need_re = 0
while True:
  if not key0_statue == key0.value(): #按下编码器 静音
    key0_statue = key0.value()
    time.sleep_ms(10)
    if key0.value() == 0:
      print("mute")
      rda.mute() 
      
  if not key1_statue == key1.value(): #右旋编码器,向下搜台
    key1_statue = key1.value()
    
    if key1.value() == 0:
      print("next")
      rda.next_sta()
      time.sleep_ms(3000)
      need_re = 1
      
  if need_re == 1: #刷新当前电台的频率
      rda.refresh_info()
      need_re = 0
      lcd.Disp_Str(0,0,b'              ')
      lcd.Disp_Str(0,0,("FM:" + str(rda.tune)).encode('utf-8'))
      print("FM:", rda.tune ,"RSSI:", rda.rssi)

另外还需要有收音机芯片驱动,LCD显示屏驱动及粉丝数显示相关的代码

RDA58XX.py 收音机驱动代码

from machine import Pin, SoftI2C
import time
import machine

class RDA5807(object):
  def __init__(self):
    self.i2c = SoftI2C(scl=Pin(4), sda=Pin(5), freq=10000)
    self.i2c_buf = bytearray(2)
    self.i2c_buf4 = bytearray(4)
    self.is_mute = 0
    self.rssi = 0
    self.tune = 0
    
  def next_sta(self): #向下搜台
    self.i2c_buf[0] = 0xd3
    self.i2c_buf[1] = 0x81
    self.i2c.writeto(0x10, self.i2c_buf)
    
  def pro_sta(self): #向上搜台
    self.i2c_buf[0] = 0xd1
    self.i2c_buf[1] = 0x81
    self.i2c.writeto(0x10, self.i2c_buf)
    
  def set_ch(self, ch): #设置频率eg. 965  unit 100KHz
    self.i2c_buf4[0] = 0xd0
    self.i2c_buf4[1] = 0x01
    ch -= 870
    self.i2c_buf4[2] = ((ch >> 2) & 0xff)
    self.i2c_buf4[3] = ((ch << 5) & 0xff)
    self.i2c.writeto(0x10, self.i2c_buf4)
    
  def refresh_info(self):  #获取当前频率,信号强度等
    self.i2c.readfrom_into(0x11, self.i2c_buf4)
    self.tune = 870 + self.i2c_buf4[1]
    self.rssi = self.i2c_buf4[2] >> 1
  
  def mute(self): #静音/取消静音
    if self.is_mute == 1:
      self.i2c_buf[0] = 0xd0
      self.is_mute = 0
    else:
      self.i2c_buf[0] = 0x90
      self.is_mute = 1
      
    self.i2c_buf[1] = 0x01
    self.i2c.writeto(0x10, self.i2c_buf)

ST7789V.py 显示屏驱动

from machine import Pin, SPI
import time
import machine
import esp

class LCD_240_240(object):
  def __init__(self):    
    self.hspi = SPI(1, 40000000)
    self.lcd_dc = Pin(15, Pin.OUT)

    self.lcd_dc.off()
    self.hspi.write(b'\x11')
    
    time.sleep(0.05)
    
    self.LCD_Cmd_Data(b'\x36',b'\x00')
    self.LCD_Cmd_Data(b'\x3A',b'\x05')
    self.LCD_Cmd_Data(b'\xB2',b'\x0C\x0C\x00\x33\x33')
    self.LCD_Cmd_Data(b'\xB7',b'\x35')
    self.LCD_Cmd_Data(b'\xBB',b'\x19')
    self.LCD_Cmd_Data(b'\xC0',b'\x2C')
    self.LCD_Cmd_Data(b'\xC2',b'\x01')
    self.LCD_Cmd_Data(b'\xC3',b'\x12')
    self.LCD_Cmd_Data(b'\xC4',b'\x20')
    self.LCD_Cmd_Data(b'\xC6',b'\x0F')
    self.LCD_Cmd_Data(b'\xD0',b'\xA4\xA1')
    self.LCD_Cmd_Data(b'\xE0',b'\xD0\x04\x0D\x11\x13\x2B\x3F\x54\x4C\x18\x0D\x0B\x1F\x23')
    self.LCD_Cmd_Data(b'\xE1',b'\xD0\x04\x0C\x11\x13\x2C\x3F\x44\x51\x2F\x1F\x1F\x20\x23')

    self.lcd_dc.off()
    self.hspi.write(b'\x21\x29')

  def LCD_Cmd_Data(self,cmd, data):
    self.lcd_dc.off()
    self.hspi.write(cmd)
    self.lcd_dc.on()
    self.hspi.write(data)

  def SetDisArea(self,xStart,yStart,xEnd,yEnd):    
    buf = bytearray(4)
    buf[0] = 0
    buf[1] = xStart
    buf[2] = 0
    buf[3] = xEnd
    self.LCD_Cmd_Data(b'\x2A',buf)

    buf[1] = yStart
    buf[3] = yEnd
    self.LCD_Cmd_Data(b'\x2B',buf)
    
    self.lcd_dc.off()
    self.hspi.write(b'\x2C')
  
  def Disp_Color(self,Color): #整屏填充某个颜色
    self.SetDisArea(0,0,239,239)
    self.lcd_dc.on()
    
    buf = bytearray(480)
    for j in range(240):
      buf[j*2] = Color >> 8
      buf[j*2+1] = Color & 0xFF
   
    for i in range(240):
      self.hspi.write(buf)
    
  def DrawPoint(self,x,y,color):#在指定位置打点
    buf = bytearray(2)
    buf[0] = 0
    buf[1] = x
    self.LCD_Cmd_Data(b'\x2A',buf)
    
    buf[1] = y
    self.LCD_Cmd_Data(b'\x2B',buf)

    buf[0] = color >> 8
    buf[1] = color &0xFF  
    self.LCD_Cmd_Data(b'\x2C',buf)
  
  def Disp_Pic(self, star_addr): #显示图像,从Flash固定位置读取图像并显示在屏幕上
    self.SetDisArea(0,0,239,239)
    self.lcd_dc.on()
    buf = bytearray(480)
    byte_offset = star_addr

    for i in range(240):
      esp.flash_read(byte_offset, buf)
      self.hspi.write(buf)
      byte_offset += 480

  def Disp_Str(self,x,y,str_datda):#显示字符串,需提前将字库烧录到0x300000位置
    font_buf = bytearray(64)
    
    for c in str_datda:
      start_addr = 0x300000 + 65*(c - 0x20)
      esp.flash_read(start_addr, font_buf)
      self.SetDisArea(x,y,x+15,y+31)
      x+=16
      self.lcd_dc.on()
      
      for j in range(64):
        for k in range(8):
          if (font_buf[j] & (0x01<<k)) ==  (0x01 << k):
            self.hspi.write(b'\xf8\x00')
          else:
            self.hspi.write(b'\xff\xff')

bilibiliflow.py 获取B站粉丝数

import network
import json  # 导入json功能模块
import urequests #导入urequests功能模块

class BiliFlow(object):
  def __init__(self):
    wifi = network.WLAN(network.STA_IF) # 将模块设为STA模式
    wifi.active(True) # 开启WIFI

    if not wifi.isconnected(): # 如果wifi模块未连接到热点
      print('WiFi 连接中...')
      wifi.connect('your_wifi' , 'xx123456') #连接自己的手机热点
      
      while not wifi.isconnected():  #等待wifi连接
        pass
        
    print('WiFi 连接成功' ,  wifi.ifconfig()) #连接成功
    
  def get_follower(self):#获取粉丝数,将下列vmid修改成自己的即可
    r = urequests.get('http://api.bilibili.com/x/relation/stat?vmid=604244921')
    p = json.loads(r.text)    
    return p['data']['follower']

结语

硬件设计资料全部在立创广场开源:https://oshwhub.com/Dr.Zhang/511radio

如果您喜欢的话,也可以尝试仿制一个哦,制作过程中遇到任何问题都可以在评论区留言。

B站搜索 我是鹏老师,发现更多惊喜!

esp8266 语音播放 //Priorities of the reader and the decoder thread. Higher = higher prio. #define PRIO_READER 11 #define PRIO_MAD 1 //The mp3 read buffer size. 2106 bytes should be enough for up to 48KHz mp3s according to the sox sources. Used by libmad. #define READBUFSZ (2106) static char readBuf[READBUFSZ]; static long bufUnderrunCt; //Reformat the 16-bit mono sample to a format we can send to I2S. static int sampToI2s(short s) { //We can send a 32-bit sample to the I2S subsystem and the DAC will neatly split it up in 2 //16-bit analog values, one for left and one for right. //Duplicate 16-bit sample to both the L and R channel int samp=s; samp=(samp)&0xffff; samp=(samp<65535) samp=65535; if (samp>11]; err=(samp&0x7ff); //Save rounding error. return samp; } //2nd order delta-sigma DAC //See http://www.beis.de/Elektronik/DeltaSigma/DeltaSigma.html for a nice explanation static int sampToI2sDeltaSigma(short s) { int x; int val=0; int w; static int i1v=0, i2v=0; static int outReg=0; for (x=0; x<32; x++) { val<0) w-=32767; else w+=32767; //Difference 1 w+=i1v; i1v=w; //Integrator 1 if (outReg>0) w-=32767; else w+=32767; //Difference 2 w+=i2v; i2v=w; //Integrator 2 outReg=w; //register if (w>0) val|=1; //comparator } return val; } //Calculate the number of samples that we add or delete. Added samples means a slightly lower //playback rate, deleted samples means we increase playout speed a bit. This returns an //8.24 fixed-point number int recalcAddDelSamp(int oldVal) { int ret; long prevUdr=0; static int cnt; int i; static int minFifoFill=0; i=spiRamFifoFill(); if (i<minFifoFill) minFifoFill=i; //Do the rest of the calculations plusminus every 100mS (assuming a sample rate of 44KHz) cnt++; if (cnt<1500) return oldVal; cnt=0; if (spiRamFifoLen()<10*1024) { //The FIFO is very small. We can't do calculations on how much it's filled on average, so another //algorithm is called for. int tgt=1600; //we want an average of this amount of bytes as the average minimum buffer fill //Calculate underruns this cycle int udr=spiRamGetUnderrunCt()-prevUdr; //If we have underruns, the minimum buffer fill has been lower than 0. if (udr!=0) minFifoFill=-1; //If we're below our target decrease playback speed, and vice-versa. ret=oldVal+((minFifoFill-tgt)*ADD_DEL_BUFFPERSAMP_NOSPIRAM); prevUdr+=udr; minFifoFill=9999; } else { //We have a larger FIFO; we can adjust according to the FIFO fill rate. int tgt=spiRamFifoLen()/2; ret=(spiRamFifoFill()-tgt)*ADD_DEL_BUFFPERSAMP; } return ret; } //This routine is called by the NXP modifications of libmad. It passes us (for the mono synth) //32 16-bit samples. void render_sample_block(short *short_sample_buff, int no_samples) { //Signed 16.16 fixed point number: the amount of samples we need to add or delete //in every 32-sample static int sampAddDel=0; //Remainder of sampAddDel cumulatives static int sampErr=0; int i; int samp; #ifdef ADD_DEL_SAMPLES sampAddDel=recalcAddDelSamp(sampAddDel); #endif sampErr+=sampAddDel; for (i=0; i(1<<24)) { sampErr-=(1<<24); //...and don't output an i2s sample } else if (sampErr<-(1<<24)) { sampErr+=(1<bufend-stream->next_frame; memmove(readBuf, stream->next_frame, rem); while (rem<sizeof(readBuf)) { n=(sizeof(readBuf)-rem); //Calculate amount of bytes we need to fill buffer. i=spiRamFifoFill(); if (i<n) n=i; //If the fifo can give us less, only take that amount if (n==0) { //Can't take anything? //Wait until there is enough data in the buffer. This only happens when the data feed //rate is too low, and shouldn't normally be needed! // printf("Buf uflow, need %d bytes.\n", sizeof(readBuf)-rem); bufUnderrunCt++; //We both silence the output as well as wait a while by pushing silent samples into the i2s system. //This waits for about 200mS for (n=0; nerror, mad_stream_errorstr(stream)); return MAD_FLOW_CONTINUE; } //This is the main mp3 decoding task. It will grab data from the input buffer FIFO in the SPI ram and //output it to the I2S port. void ICACHE_FLASH_ATTR tskmad(void *pvParameters) { int r; struct mad_stream *stream; struct mad_frame *frame; struct mad_synth *synth; //Allocate structs needed for mp3 decoding stream=malloc(sizeof(struct mad_stream)); frame=malloc(sizeof(struct mad_frame)); synth=malloc(sizeof(struct mad_synth)); if (stream==NULL) { printf("MAD: malloc(stream) failed\n"); return; } if (synth==NULL) { printf("MAD: malloc(synth) failed\n"); return; } if (frame==NULL) { printf("MAD: malloc(frame) failed\n"); return; } //Initialize I2S i2sInit(); bufUnderrunCt=0; printf("MAD: Decoder start.\n"); //Initialize mp3 parts mad_stream_init(stream); mad_frame_init(frame); mad_synth_init(synth); while(1) { input(stream); //calls mad_stream_buffer internally while(1) { r=mad_frame_decode(frame, stream); if (r==-1) { if (!MAD_RECOVERABLE(stream->error)) { //We're most likely out of buffer and need to call input() again break; } error(NULL, stream, frame); continue; } mad_synth_frame(synth, frame); } } } int getIpForHost(const char *host, struct sockaddr_in *ip) { struct hostent *he; struct in_addr **addr_list; he=gethostbyname(host); if (he==NULL) return 0; addr_list=(struct in_addr **)he->h_addr_list; if (addr_list[0]==NULL) return 0; ip->sin_family=AF_INET; memcpy(&ip->sin_addr, addr_list[0], sizeof(ip->sin_addr)); return 1; } //Open a connection to a webserver and request an URL. Yes, this possibly is one of the worst ways to do this, //but RAM is at a premium here, and this works for most of the cases. int ICACHE_FLASH_ATTR openConn(const char *streamHost, const char *streamPath) { int n, i; while(1) { struct sockaddr_in remote_ip; bzero(&remote_ip, sizeof(struct sockaddr_in)); if (!getIpForHost(streamHost, &remote_ip)) { vTaskDelay(1000/portTICK_RATE_MS); continue; } int sock=socket(PF_INET, SOCK_STREAM, 0); if (sock==-1) { continue; } remote_ip.sin_port = htons(streamPort); printf("Connecting to server %s...\n", ipaddr_ntoa((const ip_addr_t*)&remote_ip.sin_addr.s_addr)); if (connect(sock, (struct sockaddr *)(&remote_ip), sizeof(struct sockaddr))!=00) { close(sock); printf("Conn err.\n"); vTaskDelay(1000/portTICK_RATE_MS); continue; } //Cobble together HTTP request write(sock, "GET ", 4); write(sock, streamPath, strlen(streamPath)); write(sock, " HTTP/1.0\r\nHost: ", 17); write(sock, streamHost, strlen(streamHost)); write(sock, "\r\n\r\n", 4); //We ignore the headers that the server sends back... it's pretty dirty in general to do that, //but it works here because the MP3 decoder skips it because it isn't valid MP3 data. return sock; } } //Reader task. This will try to read data from a TCP socket into the SPI fifo buffer. void ICACHE_FLASH_ATTR tskreader(void *pvParameters) { int madRunning=0; char wbuf[64]; int n, l, inBuf; int t; int fd; int c=0; while(1) { fd=openConn(streamHost, streamPath); printf("Reading into SPI RAM FIFO...\n"); do { n=read(fd, wbuf, sizeof(wbuf)); if (n>0) spiRamFifoWrite(wbuf, n); c+=n; if ((!madRunning) && (spiRamFifoFree()0); close(fd); printf("Connection closed.\n"); } } //Simple task to connect to an access point, initialize i2s and fire up the reader task. void ICACHE_FLASH_ATTR tskconnect(void *pvParameters) { //Wait a few secs for the stack to settle down vTaskDelay(3000/portTICK_RATE_MS); //Go to station mode wifi_station_disconnect(); if (wifi_get_opmode() != STATION_MODE) { wifi_set_opmode(STATION_MODE); } //Connect to the defined access point. struct station_config *config=malloc(sizeof(struct station_config)); memset(config, 0x00, sizeof(struct station_config)); sprintf(config->ssid, AP_NAME); sprintf(config->password, AP_PASS); wifi_station_set_config(config); wifi_station_connect(); free(config); //Fire up the reader task. The reader task will fire up the MP3 decoder as soon //as it has read enough MP3 data. if (xTaskCreate(tskreader, "tskreader", 230, NULL, PRIO_READER, NULL)!=pdPASS) printf("Error creating reader task!\n"); //We're done. Delete this task. vTaskDelete(NULL); } //We need this to tell the OS we're running at a higher clock frequency. extern void os_update_cpu_frequency(int mhz); void ICACHE_FLASH_ATTR user_init(void) { //Tell hardware to run at 160MHz instead of 80MHz //This actually is not needed in normal situations... the hardware is quick enough to do //MP3 decoding at 80MHz. It, however, seems to help with receiving data over long and/or unstable //links, so you may want to turn it on. Also, the delta-sigma code seems to need a bit more speed //than the other solutions to keep up with the output samples, so it's also enabled there. #if defined(DELTA_SIGMA_HACK) SET_PERI_REG_MASK(0x3ff00014, BIT(0)); os_update_cpu_frequency(160); #endif //Set the UART to 115200 baud UART_SetBaudrate(0, 115200); //Initialize the SPI RAM chip communications and see if it actually retains some bytes. If it //doesn't, warn user. if (!spiRamFifoInit()) { printf("\n\nSPI RAM chip fail!\n"); while(1); } printf("\n\nHardware initialized. Waiting for network.\n"); xTaskCreate(tskconnect, "tskconnect", 200, NULL, 3, NULL); }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值