1. 硬件平台介绍
本次活动提供了基于树莓派Pico的嵌入式系统学习平台,可以通过C/C++以及MicroPython编程来学习嵌入式系统的工作原理和应用。具体的硬件清单如下:
- 树莓派Pico扩展板 x1
- 硬禾版本树莓派Pico核心模块 - STEP Pico x1
- Type-C 数据线 x1
其中扩展板上提供了较为丰富的硬件资源,包括:
- 2个按键输入
- 4个单色LED
- 12个WS2812B RGB三色灯
- 1个姿态传感器
- 1个128*64 OLED显示屏
- 1个蜂鸣器
- 1个可调电位计(用于电压表)
- 1路音频信号输入(用于示波器)
- 8位R-2R电阻网络构成的DAC(用于DDS信号发生器)
更详细的硬件平台介绍可以参考活动首页。
2. 项目简介
本次活动要求使用Pico实现如下几个小项目:
- 项目1 - 制作一个反应测试器
- 制作一个交通灯控制器
- 制作一个音乐播放器
- 制作一个电压表
- 制作一个水平仪
- 制作一个节日彩灯
- 制作一个定时报警的时钟
以上各个项目的实现原理实际上在活动配套的课程中都有介绍了,我这里选择的是制作一个音乐播放器。我的音乐播放器实现了下面几个功能:
- 音乐列表
- 播放暂停
- 动作感应切换音乐
算是具备了一个音乐播放器的基本功能了。
3. 开发环境
首先是开发语言。我这里选择的是MicroPython。
其次是操作系统。使用的是ArchLinux。
最后是IDE工具。使用的是推荐的Thonny工具。
4. 设计思路
扩展板中的蜂鸣器可以播放音乐,OLED显示屏可以用来显示音乐列表,可以用一个按键实现播放和暂停的功能,姿态传感器可以检测抖动,从而实现音乐切换功能,所以硬件平台提供的资源足够实现一个简单的音乐播放器了,剩下的工作就是写代码让各硬件模块工作起来, 下面采用自底向上的方式,说明这个音乐播放器是如何实现的。
4.1 硬件相关问题
- 问题1:如何用蜂鸣器播放音乐?
使用一路PWM可以控制蜂鸣器发出我们想要的音符。具体来说,是通过控制PWM的频率,占空比和持续时间,让蜂鸣器播放出我们想要的音符。其中PWM的频率对应音符的频率,PWM的占空比可以控制音量的大小,PWM的持续时间可以控制音符节拍的长度。下面的表格展示了音符和频率的对应关系,表中的频率单位为Hz:
解决了播放单一音符的问题,因为一首音乐中包含若干个音符,我们只要按照乐谱的顺序播放规定好的音符即可得到一首完整的音乐了。
- 问题2:如何表示乐谱?
在了解了PWM控制蜂鸣器的原理之后,我们可以很方便的写代码让蜂鸣器输出想要的音符,但一首歌的乐谱中包含了很多的音符,我们需要一个表示乐谱的方法。这里我用json格式的文件存储乐谱数据,程序中只要解析json文件,之后按照顺序输出其中的每一个音符,就可以播放出一首完整的音乐了。下面的json文件是一个乐谱的示例:
{
"sheets": [
{
"name": "xiao xing xing",
"beat_time": 0.3,
"idle_time": 0.01,
"tones": [
[
1,
1,
1
],
...
[
1,
5,
2
]
]
},
...
]
其中beat_time表示一拍的时间长度,idle_time表示相邻两个音符之间不发声的时间长度,tones代表了乐谱中的各个音符,每个音符用三个数字描述,第一个数字表示高音,中音或者低音,用这个整数可以索引到python代码中不同的数组,第二个数字表示音符的编号,配合第一个数字就能够得到该音符对应的频率,第三个数字表示音符的长短,即音符发声的时间包含几拍,和beat相乘就可以得到该音符应该发声的时间了。用这种表示方法可以很方便的表达乐谱中的信息,Python提供了解析json的标准库,所以代码实现上也比较简单。
- 问题3:如何实现动作感应切换音乐?
板子上包含一个姿态传感器MMA7660,通过查找芯片手册可以知道,这款产品能够测量不同方向上的加速度,同时还能够检测抖动,只需要从TILT寄存器中读取数据进行判断就能够得到是否发生了抖动。利用这个功能,我们就可以在检测到抖动的时候切换播放的音乐了。
4.2 软件代码设计思路
软件上的实现较为简单,通过一个事件循环处理不同的任务,在循环中会检测按键以及抖动事件,并采取对应的动作。整体代码组织如下:
.
├── LICENSE
├── main.py
├── picomp
│ ├── conponent
│ │ ├── app.py
│ │ ├── constant.py
│ │ ├── event.py
│ │ ├── __init__.py
│ │ └── sound.py
│ ├── driver
│ │ ├── board.py
│ │ ├── buzzer.py
│ │ ├── __init__.py
│ │ ├── mma7660.py
│ │ ├── ssd1306.py
│ │ └── ws2812b.py
│ └── __init__.py
└── resources
└── sheets.json
最外层是main.py,是整个音乐播放器程序的入口,picomp包中又分为driver和conponent两个子包,driver包中是不同硬件的驱动代码,conponent包中实现了事件循环的处理逻辑。下面截取部分代码对软件实现的思路做具体说明:
- main.py
main.py中引用picomp包中封装好的App类,实例化一个app,调用初始化函数,最后调用run函数开启事件循环,等待响应用户的输入。
from picomp.conponent.app import App
app = App()
app.init()
app.run()
- app.py
App类定义在app.py文件中,在App中封装了事件循环,在init函数中完成了对不同事件的handler的注册,并把事件添加到事件循环之中,在run函数中会启动事件循环,针对不同事件调用注册好的handler。
from .event import *
from .sound import music_player
from ..driver.ws2812b import led_ring
from ..driver.ssd1306 import oled
import time
class App:
def __init__(self):
# splash screen
self.__splash = Splash()
# events and event loop
self.__event_loop = eventloop
self.__player = music_player
self.__player_paused = True
def init(self):
self.__player.load_all_sheets('/resources/sheets.json')
self.__sheet_names = self.__player.get_sheet_names()
key1_event.set_event_handler(self.key1_event_handler, None)
key2_event.set_event_handler(self.key2_event_handler, None)
shaking_event.set_event_handler(self.shaking_event_handler, None)
idle_event.set_event_handler(self.idle_event_handler, None)
self.__event_loop.loop_append_event(key1_event)
self.__event_loop.loop_append_event(key2_event)
self.__event_loop.loop_append_event(shaking_event)
self.__event_loop.loop_append_event(idle_event)
def run(self):
self.__splash.show()
self.__event_loop.loop()
self.__splash.hide()
......
- sound.py
该文件中实现了一个简单的音乐播放器。MusicPlayer提供了如下接口:
- load_all_sheets: 加载乐谱文件,解析音乐列表
- play: 获取当前音乐的下一个音符,并调用buzzer进行播放
- pause: 暂停
- next_song: 切歌
import json
from ..driver.buzzer import buzzer
class MusicPlayer:
def __init__(self):
self.__sheets = None
self.__buzzer = buzzer
self.__pause = False
def load_all_sheets(self, fname):
fd = open(fname, "r")
self. __sheets = json.load(fd)['sheets']
self.__num_sheets = len(self.__sheets)
fd.close()
self.__sheet_index = 0
self.__sheet_iter = iter(self.__sheets[self.__sheet_index]['tones'])
def play(self, volume=30):
if self.__pause:
return
try:
tone = next(self.__sheet_iter)
beat_time = self.__sheets[self.__sheet_index]['beat_time']
idle_time = self.__sheets[self.__sheet_index]['idle_time']
self.__buzzer.play(tone, beat_time, idle_time, volume)
except StopIteration:
total_sheets = len(self.__sheets)
self.__sheet_index = (self.__sheet_index + 1) % total_sheets
self.__sheet_iter = iter(
self.__sheets[self.__sheet_index]['tones'])
def pause(self, pause=True):
self.__pause = pause
def next_song(self):
self.__sheet_index += 1
self.__sheet_index %= self.__num_sheets
self.__sheet_iter = iter(self.__sheets[self.__sheet_index]['tones'])
def get_sheet_names(self):
names = []
for sheet in self.__sheets:
names.append(sheet['name'])
return names
def get_curren_sheet_index(self):
return self.__sheet_index
music_player = MusicPlayer()
- buzzer.py
Buzzer是蜂鸣器的驱动,使用pwm控制播放的音符,其中音符的频率通过all_tones可以查表得到,音符的持续时间通过一个节拍的持续时间beat_time以及节拍数量beats相乘计算得到。
from machine import Pin, PWM
import time
from .board import board
all_tones = ((100, 262, 294, 330, 349, 392, 440, 494),
(100, 523, 587, 659, 698, 784, 880, 988),
(100, 1041, 1175, 1319, 1397, 1568, 1760, 1976))
class Buzzer:
def __init__(self):
self.__pwm = PWM(Pin(board['buzzer']))
def play(self, tone, beat_time, idle_time, volume=10):
duty = int(volume * 65535 / 1000) if tone[1] != 0 else 0
freq = all_tones[tone[0]][tone[1]]
beats = tone[2]
self.__pwm.duty_u16(duty)
self.__pwm.freq(freq)
time.sleep(beat_time * beats)
self.__pwm.duty_u16(0)
time.sleep(idle_time)
def mute(self):
self.__pwm.duty_u16(0)
self.__pwm.freq(1000)
buzzer = Buzzer()
其他模块的代码可以参考附件中的代码实现。
5. 总结
本人从事的工作是软件开发,但一直希能够学习一些硬件方面的知识,用自己的代码驱动一块板子按照预期的方式工作,是一件很有意思的事情。在学校期间有电子电路相关课程,但因为各种原因当时并没有认真学习,导致自己现在在硬件方面的知识和技能都比较小白,很高兴能够参加电子森林组织的活动,希望自己能够多参加类似活动,把自己在硬件方面的小遗憾弥补回来。