基于SETP PICO的音乐播放器
这是一个关于基于SETP PICO的音乐播放器的项目说明文档。其中详细介绍了开发环境、硬件平台、实现的功能以及音乐播放的具体实现方法。文档中提到,通过PWM生成不同频率,持续时间不同的音调,驱动蜂鸣器播放音乐,程序中制作一个标准的音符库,播放音乐的时候通过查找表的方式把该音符对应的频率的信号播放出来。(这一段是Notion AI写的哈哈)
项目要求具体要求:利用板上的蜂鸣器,播放一首音乐
实现方式:利用PWM生成不同频率,持续时间不同的音调,驱动蜂鸣器播放音乐,程序中制作一个标准的音符库,播放音乐的时候通过查找表的方式把该音符对应的频率的信号播放出来
1. 开发环境RP2040作为树莓派开源基金会推出的微处理器对开发语言的兼容性极佳,它既可以用C/C++这种面向过程的语言进行开发,也可以用MicroPython这种面向对象的语言进行开发。
众所周知,Python语言简单易用,极易上手。代码风格也都非常简洁,只需使用缩进就可以完成程序作用域的划分。
而MicroPython是Python3编程语言的精简、高效实现 。它包括Python标准库的一小部分,并经过优化以在微控制器和受限环境中运行,也是新手快速上手Raspberry Pi PICO的不二之选。
1.2 集成开发环境(IDE)我使用的集成开发环境是视频教程里推荐的Thonny,它具有简单易用、兼容性好、集成便利、调试工具丰富、代码提示等优点。 不过VSCode和PyCharm也可以配置MicroPython环境,不过对于我这种小白反而太过复杂了。
1.3 硬件平台硬件平台采用的是硬禾学堂提供的STEP Pico核心板和STEP PICO嵌入式系统学习平台。
SETP PICO相较于树莓派PICO在板子上增加了4个WS2812灯珠,一个复位按键,而且将板载Micro USB接口修改为更为常见的Type-C接口,更利于开发。
而STEP PICO嵌入式系统学习平台则有丰富的板载硬件。其具有
- 2个按键输入
- 4个单色LED
- 12个WS2812B RGB三色灯
- 1个姿态传感器
- 1个128*64 OLED显示屏
- 1个蜂鸣器
- 1个可调电位计(用于电压表)
- 1路音频信号输入(用于示波器)
- 8位R-2R电阻网络构成的DAC(用于DDS信号发生器)
- 音乐“沙滩”的播放;
- WS2812B的呼吸灯效果;
- OLED屏幕上对正播放的音符和音长以及歌曲信息的显示;
- 通过按键调整音量大小
音乐播放是本次项目的核心功能,如何实现有源蜂鸣器播放不同的音调也是实现这个功能的重中之重。
3.1.1 无源蜂鸣器及其驱动蜂鸣器根据驱动方式一般分为有源蜂鸣器和无源蜂鸣器。
有源蜂鸣器是一种内置振荡器的蜂鸣器,只需要提供直流电压就可以使它发出声音,优点是驱动简单,缺点是只能发出固定频率的声音。
无源蜂鸣器则没有内置振荡器,所以直流电无法让其发声,需要外部输入方波信号才能发出声音。而根据输入方波频率的不同,里面的振膜也会以对应的频率震动,从而发出不同音调的声音。故若要实现音乐播放,只能选用无源蜂鸣器。
在STEP PICO训练板的原理图上,我们可以看到无源蜂鸣器是通过三极管进行驱动,与RP2040的GPIO18相连。
故只需要使其对应的IO口输出不同频率的PWM方波,便可以完成不同音调的播放。
3.1.2 音调与频率的关系音乐是一个非常复杂的学科,但对于本次的项目,我们只需知道:不同频率对应着不同音调即可。
在了解这一知识点后,音乐播放的问题便迎刃而解。只需在程序中建立一个“标准音符库”,在播放音符的时候只需要输入音符对应频率在表中的索引即可。
在这里,我只采用了从中央C(约261Hz)开始后的几个音,这些音符组合起来播放一首简单的音乐绰绰有余了。
music_freq = [0,261,293,329,349,392,440,493,523,587,659,698,783,878,987,1045] #频率对照表
在代码中我建立了一个列表作为标准音符库。你可能发现这和上图的频率有一点小小的出入,这是因为在实际应用中我发现直接把这个频率输入进去后音并不是非常准,MicroPython的参考文档也对于这一问题作出了解释。
大致意思就是由于硬件本身计算的离散性输出的频率是有一定的误差的。所以我作出了一点“微调”。
3.1.3 实现音乐播放在解决完所有理论问题后我们就可以着手解决软件问题。
MicroPython在machine
库中非常贴心地提供了PWM类,用于驱动RP2040的硬件PWM。
通过PWM.freq([
value
])
和PWM.duty_u16([
value
])
方法就可以非常方便地调整PWM输出的频率和占空比。我在程序中也将其封装成了一个函数,便于直接修改其数据。
def PWM_Init(freq,duty_ns):
pwm.freq(freq)
pwm.duty_ns(duty_ns)
现在,只需要控制每个音符的音长和音调就可以实现音乐的播放了。
我选择的音乐是陶喆的沙滩,我在网上直接把它的简谱扒了下来放进了程序。
#歌曲音符(简谱)
music = [8,7,5,3,3,2,1,2,3,0,\
8,7,5,3,3,2,1,2,3,0,\
6,7,8,9,8,6,8,9,8,8,9,7,6,5,6,0,\
7,8,12,7,8,12,7,8,0,\
8,7,8,9,8,6,5,3,5,\
7,8,9,8,6,9,10,12,10,\
9,10,11,10,8,0,11,10,8,10,13,12]
但是仅仅有音调是不够的,还需要记录每个音的音长,于是我又建立了一个列表用于记录音长。
#歌曲音长
time = [1.5,1,1,2,0.5,0.5,1,0.75,4.5,1,\
1.5,1,1,2,0.5,0.5,1,0.75,4.5,1,\
0.5,0.5,0.5,1.5,1.5,0.5,0.5,1.5,1.5,0.5,0.5,1.5,1,1,1.5,1.5,\
1,0.5,1.5,1,1,2,1,1,1,\
0.5,0.5,0.5,1.5,1.5,1.5,0.5,0.5,1.5,\
0.5,0.5,1.5,1.5,1,0.5,0.5,0.5,1.5,\
0.5,0.5,1.5,1.5,2,0.5,0.5,1,1,3,1,4]
在有了以上数据,构造一个方法来进行音乐播放,最后放入主程序即可。
大致思路是:用一个变量来保存当前正播放的音符的索引,将其从0初始化,然后从music
列表中查找到对应的音调,再从music_freq
列表,即标准音符库中查找到音调对应的频率,最后用PWM.freq
方法进行输出频率的设置,用PWM.duty_u16
方法进行延时即可。在每播放完一个音后索引加一。
def play_music(i):
global duty,freq
freq = music_freq[music[i]]
if freq != 0:
pwm.freq(freq)
pwm.duty_ns(duty)
else:
pwm.duty_ns(0)
sleep(time[i] / 2)
至此,主要功能音乐播放就完成了。
3.2 OLED的显示仅仅实现了音乐播放我觉得还不够,它只是一个基本功能,于是在此基础上我又增加了OLED的显示功能。
板载OLED屏幕是用SSD1306进行驱动的,SSD1306由SPI总线与RP2040进行相连,硬禾学堂官方给出了SSD1306的驱动,所以我们不必留意其具体驱动方法,直接调用即可。
OLED使用了SPI总线,所以需要先初始化SPI总线,再对OLED的参数如反色,方向等进行初始化便完成了初始化操作。
from machine import Pin, SPI
from ssd1306 import SSD1306_SPI
import framebuf
spi = SPI(1, 100000, mosi=Pin(pin_cfg.spi1_mosi), sck=Pin(pin_cfg.spi1_sck))
oled = SSD1306_SPI(128, 64, spi, Pin(pin_cfg.spi1_dc),Pin(pin_cfg.spi1_rstn), Pin(pin_cfg.spi1_cs))
oled.rotate(1)
在完成初始化操作后便可以用现成的方法进行OLED屏幕的显示。
def OLED_Show_Music(num,time):
oled.fill(0)
oled.text("NOTE DURATION",10,0)
oled.text("%3u %3.1f" % (music[i],time),10,15)
oled.text("NOWPLAYING",20,30)
oled.text("SANDBEACH",25,40)
oled.text("BY David Tao",13,50)
oled.show()
OLED的底层驱动调用了MicroPython的framebuf
库,里面有一个方法是FrameBuffer.text(
s
,
x
,
y
[,
c
])
,用来显示文本。我也是用这个方法来在OLED上显示的音符、音长、乐曲名和歌手。但是framebuf里的字体是固定的,大小也无法调整,这一点是比较讨厌的,不像Arduino中的u8g2库,可以任意选择字体。
呼吸灯效果通过改变全局变量brightness
的值来控制灯光的亮度,并通过改变direction
的值来控制亮度的递增或递减方向。通过递加或递减亮度来实现呼吸灯的效果。
def ws2812b_breathe_effect(t):
global brightness,direction,duty
ws2812b.on_all("#00ffff", brightness)
key_fuc()
brightness += 0.05 * direction
if brightness >= 1.0:
brightness = 1.0
direction = -1
elif brightness <= 0.0:
brightness = 0.0
direction = 1
由于在主函数中为了实现音长使用了阻塞方法sleep
,如果直接把呼吸灯函数在主函数中直接调用是会出问题的,因为进程在sleep
处会被堵塞,无法顺利执行下面的函数。所以我就调用了一个定时器,在定时器中断中执行呼吸灯效果和按键的扫描,以达到并行的效果。
tim = Timer() #New Timer
tim.init(mode=Timer.PERIODIC, period=75, callback=ws2812b_breathe_effect)
我将定时器初始化为3.4 按键及音量大小调整的实现PERIODIC
模式,也就是周期性中断,周期为75ms。中断的回调函数是ws2812b_breathe_effect()
,这样就完成了定时器中断的设置。
按键方面,我采用了标志位的写法来实现非阻塞式的按键扫描。大致思路为:当按键按下,将对应标志位设置为True,代表有按键曾按下过。在按键抬起且标志位为True时,代表一次按键按下,此时将标志位设置为False并执行按键需要执行的操作即可。
代码如下:
def key_fuc():
global k1_v,k2_v,duty
if k1.value() == 0:
if k1_v == False:
k1_v = True
duty += 5000
else:
k1_v = False
if k2.value() == 0:
if k2_v == False:
k2_v = True
duty -= 5000
if duty < 0:
duty = 0
else:
k2_v = False
我们知道,PWM输出的主要参数有频率和占空比。在本次项目中,频率控制了音调,而占空比则控制了音量的大小。占空比即脉冲序列信号在一个周期内,正半周持续时间与周期的比值。占空比越大,蜂鸣器震动所占时间也就越多,蜂鸣器声音也就越大;占空比越小蜂鸣器震动所占时间也就越少,蜂鸣器声音也就越小。故通过调整占空比便可以调整蜂鸣器的声音大小
至此,程序实现部分全部结束。
4 遇到的问题在本次项目中,遇到问题的主要原因就是对于MicroPython语言的不熟悉。
如MicroPython中全局变量的使用,变量的“声明”,局部变量的作用域以及MicroPython中不存在局部静态变量等等问题。不过还好在查阅资料后都一一解决
此前我一直在使用Arduino,C51等C或者类似于C的语言进行开发,所以在开发过程中踩了不少坑。不过实际上万变不离其宗,无论用什么语言开发,其核心思路都是相同的。MicroPython确实也很方便,对于C来说减小了不少工作量,不必纠结于其底层的实现。
5 展望未来本次项目使我学习到不少内容,不过我认为我所编写的代码还有很大的提升空间,也有很多可以优化的地方,仍需继续努力。
MicroPython与Python的语法规则基本一致,但是还有许多基本的语法问题玩没有弄明白,如元组与列表等。对于面向对象语言和面向过程的语言相比较还是有很大的区别的,在编程思路和方法上也有很大的不同。
在未来,我仍需继续学习Python语言的语法,为编程打好基础。同时,也应更加熟悉RP2040的各种特性,如各种外设、多核编程的方法等。在未来,我也会尝试使用C/C++语言对2040进行开发。
最后感谢硬禾学堂提供了这次学习的机会,文章中可能也存在不少问题,欢迎各位大佬指正。
MicroPython Documentation. (2021). Retrieved July 23, 2021, from https://docs.micropython.org/en/latest/index.html
源代码
from board import pin_cfg
from machine import Pin,PWM,Timer,ADC
from time import sleep
import ws2812b
############for OLED############
from machine import Pin, SPI
from ssd1306 import SSD1306_SPI
import framebuf
spi = SPI(1, 100000, mosi=Pin(pin_cfg.spi1_mosi), sck=Pin(pin_cfg.spi1_sck))
oled = SSD1306_SPI(128, 64, spi, Pin(pin_cfg.spi1_dc),Pin(pin_cfg.spi1_rstn), Pin(pin_cfg.spi1_cs))
oled.rotate(1)
##############ADC###############
adc = ADC(Pin(pin_cfg.adc1)) # create ADC object on ADC pin
tim = Timer() #New Timer
##############PIN INIT###############
buz = Pin(pin_cfg.buzzer)
pwm = PWM(buz)
k1 = Pin(12,Pin.IN, Pin.PULL_UP)
k2 = Pin(13,Pin.IN, Pin.PULL_UP)
########### MUSIC ###########
music_freq = [0,261,293,329,349,392,440,493,523,587,659,698,783,878,987,1045] #频率对照表
#歌曲音符(简谱)
music = [8,7,5,3,3,2,1,2,3,0,\
8,7,5,3,3,2,1,2,3,0,\
6,7,8,9,8,6,8,9,8,8,9,7,6,5,6,0,\
7,8,12,7,8,12,7,8,0,\
8,7,8,9,8,6,5,3,5,\
7,8,9,8,6,9,10,12,10,\
9,10,11,10,8,0,11,10,8,10,13,12]
#歌曲音长
time = [1.5,1,1,2,0.5,0.5,1,0.75,4.5,1,\
1.5,1,1,2,0.5,0.5,1,0.75,4.5,1,\
0.5,0.5,0.5,1.5,1.5,0.5,0.5,1.5,1.5,0.5,0.5,1.5,1,1,1.5,1.5,\
1,0.5,1.5,1,1,2,1,1,1,\
0.5,0.5,0.5,1.5,1.5,1.5,0.5,0.5,1.5,\
0.5,0.5,1.5,1.5,1,0.5,0.5,0.5,1.5,\
0.5,0.5,1.5,1.5,2,0.5,0.5,1,1,3,1,4]
freq = 0
duty = 8000 #PWM占空比,可通过此调整声音大小
i = 0
########### MUSIC ###########
def PWM_Init(freq,duty_ns):
pwm.freq(freq)
pwm.duty_ns(duty_ns)
def OLED_Show_Music(num,time):
oled.fill(0)
oled.text("NOTE DURATION",10,0)
oled.text("%3u %3.1f" % (music[i],time),10,15)
oled.text("NOWPLAYING",20,30)
oled.text("SANDBEACH",25,40)
oled.text("BY David Tao",13,50)
oled.show()
brightness = 1
direction = 0.1
k1_v = False
k2_v = False
def key_fuc():
global k1_v,k2_v,duty
if k1.value() == 0:
if k1_v == False:
k1_v = True
duty += 5000
else:
k1_v = False
if k2.value() == 0:
if k2_v == False:
k2_v = True
duty -= 5000
if duty < 0:
duty = 0
else:
k2_v = False
def ws2812b_breathe_effect(t):
global brightness,direction,duty
ws2812b.on_all("#00ffff", brightness)
key_fuc()
brightness += 0.05 * direction
if brightness >= 1.0:
brightness = 1.0
direction = -1
elif brightness <= 0.0:
brightness = 0.0
direction = 1
def play_music(i):
global duty,freq
freq = music_freq[music[i]]
if freq != 0:
pwm.freq(freq)
pwm.duty_ns(duty)
else:
pwm.duty_ns(0)
sleep(time[i] / 2)
oled.fill(0)
oled.show()
tim.init(mode=Timer.PERIODIC, period=75, callback=ws2812b_breathe_effect)
while True:
OLED_Show_Music(music[i],time[i])
print(freq,' ',music[i],' ',time[i],' ',i,"DUTY:",duty)
i+=1
if i <= 74:
play_music(i - 1)
else :
i = 0