当前位置: 首页 > news >正文

Arduino实战:用蜂鸣器与OLED实现PUBG主题音乐动画播放器

1. 项目概述与核心价值

如果你手头有一块Arduino开发板,想做一个既炫酷又能学到东西的项目,那么这个结合了PUBG主题音乐和OLED动画显示的小玩意儿,绝对值得一试。它不是什么高深莫测的科研,而是一个典型的、能串联起多个嵌入式开发核心技能的“练手”项目。核心就两件事:让蜂鸣器精准地“唱”出那首熟悉的PUBG主题曲,同时让一块小小的OLED屏像幻灯片一样,配合音乐节奏切换游戏相关的图标和动画。

为什么说它有价值?首先,它覆盖了硬件交互的经典组合:通过I2C总线驱动OLED显示屏,以及通过数字引脚驱动无源蜂鸣器生成PWM方波来发声。这两项是玩转Arduino乃至其他微控制器的基本功。其次,它逼着你处理“时序”和“协同”问题——音乐不能断,画面切换要卡点,这涉及到对程序非阻塞设计(比如用millis()替代delay)的初步理解。最后,从一张普通的JPG图片到单片机能够识别的位图数组,这个转换过程本身就是一个完整的“数据预处理”小课题,你会接触到图像的二值化、尺寸缩放和数据结构,这些概念在更复杂的图形显示项目中是相通的。

无论你是刚学完Arduino基础语法的新手,想找个综合项目巩固知识;还是有一定经验的爱好者,希望探索如何让硬件项目更有表现力和趣味性,这个项目都能提供一个清晰的路径和可复现的成果。接下来,我会拆解从硬件连接到代码编写的每一个环节,并分享我在实现过程中踩过的坑和总结的技巧。

2. 硬件选型、连接与电路解析

2.1 核心元件选型背后的考量

这个项目的硬件清单非常精简,但每一件的选择都有其道理。

主控:Arduino Nano我选择了Arduino Nano,主要是看中它的小巧和性价比。在面包板上搭建原型时,Nano的引脚排布与标准间距兼容,直接插上就行,省去了杜邦线,让电路更整洁。当然,正如原文所说,Uno、Mega甚至ESP8266/ESP32这类板子都能用,只要它们有足够的数字I/O引脚和I2C接口。选择Nano意味着我们默认使用5V逻辑电平,这点在与外围模块通信时要留意。

显示模块:0.96英寸 SSD1306 OLED (I2C接口)为什么是OLED而不是LCD?核心原因是“视觉表现力”。OLED是自发光,每个像素独立开关,显示黑色时完全不发光,因此对比度极高,显示深色背景上的白色图形(比如PUBG的Logo)效果非常锐利、有冲击力。我选择的这款分辨率是128x64,对于显示图标和简单动画绰绰有余。选择I2C接口版本(而非SPI接口)是另一个关键决策。I2C只需要两根数据线(SDA, SCL)和电源线,极大简化了布线。虽然刷新速度不如SPI,但对于我们这个静态图片切换的应用来说完全够用,优先追求电路的简洁可靠。

发声模块:5V 无源蜂鸣器这里有个重要区分:有源蜂鸣器无源蜂鸣器。有源蜂鸣器内部自带振荡电路,给电就响,只能发出固定频率的声音。而无源蜂鸣器内部没有振荡源,就像一个简单的喇叭,需要外部输入不同频率的方波信号才能发出不同音调的声音。我们这个项目要播放旋律,必须使用无源蜂鸣器。购买时一定要确认,或者用万用表测一下,通常无源蜂鸣器用直流电阻档测量会有几欧姆到十几欧姆的电阻,且正面往往有贴片电容。

2.2 电路连接详解与防错指南

连接很简单,但细节决定成败。下图是连接的示意表格:

Arduino Nano 引脚连接至说明与注意事项
5VOLED VCC, 蜂鸣器正极(可选)为模块供电。确保电源容量足够。
GNDOLED GND, 蜂鸣器负极共地!所有GND必须连接在一起,这是电路正常工作的基础。
A4 (SDA)OLED SDAI2C数据线。
A5 (SCL)OLED SCLI2C时钟线。
D6蜂鸣器正极我选择D6,但理论上任何数字PWM引脚(如3, 5, 6, 9, 10, 11)都可以。

注意1:蜂鸣器驱动方式蜂鸣器工作电流不大(通常<30mA),可以直接由Arduino的IO口驱动。连接时,蜂鸣器正极接D6,负极接GND。不建议将蜂鸣器正极接在5V引脚上,然后用三极管或MOS管去控制,对于这个小项目属于过度设计,直接IO驱动最简单可靠。

注意2:I2C上拉电阻标准的I2C通信协议要求SCL和SDA线上各有一个上拉电阻(通常4.7kΩ或10kΩ)到VCC。好消息是,绝大多数市面上售卖的I2C接口OLED模块,都已经在模块板上集成了这两个上拉电阻。所以我们在连接时,不需要自己额外在面包板上添加电阻。如果你用的模块没有集成,或者你是在万能板上自己焊接的,那么就需要在SDA和SCL线上分别添加4.7kΩ的上拉电阻到5V。

注意3:电源稳定性当所有设备都接好后,特别是蜂鸣器鸣响的瞬间,可能会有一个小的电流冲击。如果使用电脑USB口供电,通常没问题。但如果发现Arduino有时会意外复位,可以考虑在Arduino的5V和GND之间并联一个100μF的电解电容,用于平滑电源波动。

连接完成后的实物,应该是一个非常简洁的“一线牵”结构:Arduino Nano在中间,左边伸出4根线到OLED,右边伸出2根线到蜂鸣器,电源正负并行供给两个模块。

3. 软件开发环境与核心库解析

3.1 必备库的安装与选择

Arduino生态的强大在于其丰富的库。我们这个项目主要依赖一个核心图形库。

Adafruit SSD1306 与 Adafruit GFX 库这是驱动SSD1306系列OLED最主流、功能最全的库。它依赖于另一个基础图形库Adafruit GFX Library,后者提供了画点、线、圆、矩形、显示文字等基本功能。 安装方法(Arduino IDE):

  1. 打开工具 -> 管理库...
  2. 在搜索框中输入“Adafruit SSD1306”。
  3. 找到并点击安装。通常IDE会提示你“此库依赖于Adafruit GFX Library,是否一并安装?”,选择“安装所有”。
  4. 安装完成后,你可以在文件 -> 示例中找到Adafruit SSD1306的示例程序,用于测试屏幕。

为什么不用U8g2库?U8g2是另一个非常强大的单色屏驱动库,支持更多控制器和字体。但对于新手,Adafruit的库API更直观,与GFX库的集成度更高,在绘制图形和混合显示时更方便。我们项目以显示预渲染的位图为主,Adafruit库完全胜任且更易上手。

3.2 项目代码结构规划

在开始写代码前,规划好文件结构能让逻辑更清晰,也便于管理。我建议在Arduino项目文件夹内建立以下三个文件:

  1. pubg_theme.ino:主程序文件。包含setup()loop()函数,以及程序的主要逻辑流。
  2. pitches.h:头文件。存放音符与频率的对应关系宏定义。
  3. pictures.h:头文件。存放所有要显示的图像的位图数组数据。

使用头文件(.h)的好处是模块化。将音符数据和图片数据分离出去,主程序文件会非常清爽,只关注“什么时候播放什么音”和“什么时候显示什么图”的逻辑。修改旋律或更换图片时,只需编辑对应的头文件,无需动主程序。

4. 旋律编程:从乐谱到Arduino代码

4.1 理解tone()函数与音符频率

让Arduino唱歌的核心是tone(pin, frequency, duration)函数。

  • pin:连接蜂鸣器的引脚号。
  • frequency:声音的频率,单位赫兹(Hz)。这个频率决定了音高。
  • duration(可选):发声的���续时间,单位毫秒(ms)。如果不指定,声音会持续直到调用noTone()或下一个tone()

因此,我们需要两样东西:一首歌的音符序列和每个音符的时值(持续时间)。

4.2 创建pitches.h文件

网络上可以找到标准的音符频率对照表。我们将常用的音符定义成宏,方便在旋律数组中直接使用符号化的名称,而不是难记的频率数字。

// pitches.h - 定义音符频率 #define NOTE_B0 31 #define NOTE_C1 33 #define NOTE_CS1 35 #define NOTE_D1 37 // ... 中间省略很多音符 ... #define NOTE_B7 3951 #define NOTE_C8 4186 #define NOTE_CS8 4435 #define NOTE_D8 4699 #define NOTE_DS8 4978 // 定义休止符 #define NOTE_REST 0

你可以从Arduino IDE自带的示例File -> Examples -> 02.Digital -> toneMelody中直接复制完整的pitches.h内容,这是最准确快捷的方式。

4.3 解析与编写PUBG主题旋律

这是项目中最需要耐心和“乐感”的部分。你需要找到PUBG主题曲的简谱或通过反复听来记录。

步骤拆解:

  1. 确定调性和音符:找到旋律的主音符。例如,主题曲开头几个音可能是C5, D5, E5, F5, G5等。你需要将它们映射到pitches.h中定义的宏上。
  2. 确定节奏和时值:音乐有全音符、二分音符、四分音符、八分音符等。我们需要为每个音符分配一个相对的“时长单位”。通常,我们定义一个四分音符的时值(如300ms),那么二分音符就是600ms,八分音符就是150ms。
  3. 创建数组:在主程序中,我们会创建两个并行数组。
    • int melody[]:存储音符序列(使用NOTE_C5这样的宏)。
    • int noteDurations[]:存储对应音符的时值(单位毫秒)。

我的实操记录与技巧:

  • 工具辅助:我用了电脑上的音频编辑软件(如Audacity)打开PUBG主题曲,放大波形图,可以比较直观地看到每个音符的起止,帮助估算相对时值。
  • 简化起步:不必一开始就追求100%还原。可以先抓主旋律的骨干音,确保节奏大体正确,做出一个可听的版本。
  • 调试技巧:在for循环中播放旋律时,我习惯在每次播放一个音符前后用Serial.print输出当前播放的音符名和时值,这样当某个音听起来不对时,能快速定位到数组中的位置进行修改。
  • 关于休止符:音乐中的停顿很重要。在melody数组中用NOTE_REST(频率为0)表示休止,在noteDurations中给它一个时值,播放时就会产生安静的间隔。

下面是我编写的旋律数组片段示例:

// 在 pubg_theme.ino 中 #include "pitches.h" // 旋律序列 int melody[] = { NOTE_E5, NOTE_D5, NOTE_C5, NOTE_A4, NOTE_C5, NOTE_E5, NOTE_D5, NOTE_REST, // 一个休止 NOTE_E5, NOTE_D5, NOTE_C5, NOTE_A4, // ... 后续音符 }; // 对应音符的时值(单位:毫秒) int noteDurations[] = { 250, 250, 250, 500, // 对应前四个音符 375, 375, 750, // 对应接下来三个音符 250, // 休止符的时长 250, 250, 250, 500, // ... 后续时值 };

5. 图像显示:从图片到OLED位图

5.1 图像预处理原理

OLED屏幕是单色(1位色深)的,每个像素只有亮(1)或灭(0)两种状态。因此,任何彩色或灰度图片在显示前都必须经过二值化处理,转换成黑白两色的位图。

这个过程的关键参数是亮度阈值。对于每个像素,计算其亮度(或灰度值),如果高于阈值,则视为白色(1),否则视为黑色(0)。image2cpp这类在线工具就是自动完成这个过程的。

图片选择经验:

  • 优先选择线条简洁、色块分明、对比度高的图标或Logo。比如PUBG的经典鸡头Logo、平底锅、三级头等。
  • 避免使用渐变、阴影、复杂细节过多的照片。这些内容经过二值化后会丢失大量信息,变成一团模糊的黑白点,显示效果很差。
  • 背景处理:如果原图背景不是纯黑或纯白,在转换时可能需要调整阈值,或者事先用图片处理软件(如Photoshop、GIMP)将背景抠成纯色。

5.2 使用 image2cpp 工具生成位图数组

工具网址:https://diyusthad.com/image2cpp(或搜索其他同类工具)

详细操作步骤:

  1. 上传图片:点击“选择图片”上传你的PNG或JPG。
  2. 调整画布尺寸:这是最关键的一步。在“Canvas size”中,将宽度(Width)设置为128,高度(Height)设置为64,与我们的OLED分辨率严格一致。确保“Scale”选项是选中状态,这样工具会帮你缩放图片以适应画布。
  3. 设置扫描模式与格式
    • 扫描模式(Scan method):选择“Vertical - 1 bit per pixel”。这是Arduino SSD1306库最常用的格式,它按列(垂直方向)逐字节组织像素数据。
    • 输出格式(Code output format):选择“Arduino code”
    • 亮度阈值(Brightness threshold):拖动滑块,实时观察右侧预览图。目标是让主体轮廓清晰,不必要的噪点最少。对于白底黑字的Logo,可能需要调高阈值让背景更干净。
  4. 调整图片位置:利用“Left”和“Top”偏移值,或者直接拖动预览图中的图片,将其调整到画布中央。
  5. 生成代码:点击“Generate code”按钮。工具会生成一个巨大的const unsigned char数组,这就是你的位图数据。
  6. 保存数组:将生成的整个数组(包括变量声明)复制下来。

5.3 创建与管理pictures.h文件

我们将所有图片的位图数组都集中放在这个头文件里。

// pictures.h #ifndef PICTURES_H #define PICTURES_H #include <Arduino.h> // 图片1: PUBG Logo const unsigned char pubg_logo [] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ... 很长很长的数据 ... }; // 图片2: 平底锅 const unsigned char pan_bitmap [] PROGMEM = { // ... 数据 ... }; // ... 可以继续添加更多图片数组 ... #endif

关键技巧:使用PROGMEM注意数组声明后的PROGMEM关键字。位图数据通常很大,会占用大量的SRAM(运行内存)。Arduino Nano的SRAM只有2KB,很容易耗尽导致程序行为异常。PROGMEM告诉编译器将这部分常量数据存储在Flash程序存储器中(空间大得多),只在需要显示时才读取到SRAM中,这是处理大容量静态数据的标准做法。

6. 主程序逻辑与协同控制实现

6.1 初始化与库引入

主程序pubg_theme.ino的开头部分,需要引入必要的库和头文件,并定义对象与引脚。

#include <Wire.h> // I2C通信库 #include <Adafruit_GFX.h> // 核心图形库 #include <Adafruit_SSD1306.h> // OLED驱动库 #include "pitches.h" // 自定义音符头文件 #include "pictures.h" // 自定义图片头文件 // 定义OLED屏幕对象,参数:宽度(128), 高度(64), I2C地址(0x3C),Wire对象 Adafruit_SSD1306 display(128, 64, &Wire, -1); // 定义蜂鸣器引脚 const int buzzerPin = 6; // 定义图片切换时间间隔(毫秒) const unsigned long imageInterval = 3500; // 3.5秒 unsigned long previousImageTime = 0; // 记录上次切换图片的时间 int currentImageIndex = 0; // 当前显示的图片索引 // 声明图片数组指针(方便循环调用) const unsigned char* imageArray[] = {pubg_logo, pan_bitmap, helmet_bitmap, /* ... 其他图片名 */}; const int imageCount = 7; // 图片总数

6.2setup()函数:一次性启动的奥秘

整个演示(播放一次音乐、按顺序显示一遍图片)只在启动时运行一次,所以核心代码都放在setup()中。loop()函数保持为空或仅用于维持系统运行。

void setup() { Serial.begin(9600); // 初始化串口,用于调试输出 // 1. 初始化OLED显示屏 if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // I2C地址通常是0x3C或0x3D Serial.println(F("SSD1306 allocation failed")); for(;;); // 初始化失败,程序死循环 } display.clearDisplay(); // 清屏 display.display(); // 将清屏命令生效 delay(100); // 短暂延时让屏幕稳定 // 2. 设置蜂鸣器引脚为输出 pinMode(buzzerPin, OUTPUT); // 3. 播放主题曲 playThemeSong(); // 4. 记录程序开始时间,用于后续图片切换计时 previousImageTime = millis(); }

playThemeSong()函数就是一个遍历melodynoteDurations数组的for循环,调用tone()函数播放每个音符。

6.3loop()函数:非阻塞的图片轮播

为了让图片切换与音乐播放结束后无缝衔接,并且不阻塞其他操作(虽然本项目没其他操作),我们使用基于millis()的非阻塞定时方法。

void loop() { // 获取当前时间 unsigned long currentTime = millis(); // 检查是否到达图片切换的时间点 if (currentTime - previousImageTime >= imageInterval) { // 更新上次切换时间 previousImageTime = currentTime; // 显示下一张图片 displayImage(currentImageIndex); // 更新图片索引,循环播放 currentImageIndex++; if (currentImageIndex >= imageCount) { currentImageIndex = 0; // 回到第一张,形成循环 // 如果只想显示一轮,可以在这里停止,或者清屏 // display.clearDisplay(); // display.display(); } } // 这里可以添加其他需要持续运行的任务 }

displayImage(int index)函数负责具体的显示工作:

void displayImage(int index) { display.clearDisplay(); // 清除上一帧 // drawBitmap(x坐标, y坐标, 位图数据指针, 位图宽度, 位图高度, 颜色) // 颜色:SSD1306_WHITE 或 SSD1306_BLACK display.drawBitmap(0, 0, imageArray[index], 128, 64, SSD1306_WHITE); display.display(); // 将缓存内容刷到屏幕上 }

时间间隔的计算:原文提到3.5秒是总旋律时长除以图片数量得出的。更严谨的做法是:先用for循环累加noteDurations数组得到总播放时间totalSongDuration,然后imageInterval = totalSongDuration / imageCount。这样能确保音乐结束,图片也刚好播完最后一帧。

7. 常见问题、调试技巧与进阶优化

7.1 硬件连接问题排查表

现象可能原因排查步骤
OLED屏幕不亮1. 电源接反或未接通。
2. I2C地址错误。
3. 屏幕本身损坏。
1. 用万用表检查VCC和GND间是否有5V电压。
2. 扫描I2C地址(上传I2C扫描器示例代码)。
3. 尝试更换模块。
OLED亮但无显示1. 初始化失败。
2. SDA/SCL接错引脚。
3. 代码中未调用display.display()
1. 检查串口监视器是否有初始化失败提示。
2. 确认SDA接A4,SCL接A5(对于Nano/Uno)。
3. 确保绘图后调用了display.display()
蜂鸣器不响1. 使用了有源蜂鸣器。
2. 正负极接反。
3. 引脚定义错误或损坏。
1. 确认是无源蜂鸣器。
2. 交换正负极试试。
3. 用tone(buzzerPin, 1000)测试引脚,或用LED替代测试引脚是否有输出。
蜂鸣器一直响一个音1. 使用了有源蜂鸣器。
2. 代码中tone()后没有delay()noTone()
1. 确认蜂鸣器类型。
2. 检查旋律播放代码,确保每个音符有持续时间,或音符间有delay()noTone()
程序上传后无任何反应1. 开发板型号选错。
2. 端口选错。
3. 电源不足。
1. 在IDE中确认板子型号(如Arduino Nano)。
2. 在工具->端口中选择正确的COM口。
3. 尝试用手机充电器给板子独立供电。

7.2 软件与代码调试技巧

  1. 分模块测试:不要一次性写完所有代码。先写一个简单的测试程序,只让蜂鸣器响一声,或者只让OLED显示一句“Hello World”。确保每个基础部件单独工作正常。
  2. 善用串口监视器:在代码关键位置添加Serial.print()语句,输出变量值、状态标志、执行到哪一步等。这是调试嵌入式程序最强大的工具。
  3. 检查库版本:有时库更新会导致API变化。如果你从别处复制的代码编译不通过,检查一下库函数名或参数是否与已安装的库版本匹配。
  4. 内存不足(Out of Memory):如果编译时提示“Low memory available”或程序运行不稳定,首先检查是否将大的位图数组放入了PROGMEM。其次,可以尝试减少图片数量或降低图片分辨率(如用128x32的显示模式)。

7.3 项目进阶优化思路

当基础功能实现后,你可以尝试以下优化,让项目更出彩:

  1. 音画同步强化:目前的方案是音乐播完后再按固定间隔切图。可以升级为更精确的同步——为每一张图片指定一个“开始播放的时间点”(相对于音乐开始的时间)。在loop()中,根据millis()计算当前音乐播放进度,然后决定显示哪张图。这需要更精细地规划时间轴。
  2. 加入动画效果:不要只是简单地切换静态位图。利用Adafruit GFX库的函数,可以实现图片淡入淡出、滑动进入、缩放等简单动画。例如,在切换图片时,让新图片从右侧滑入。
  3. 增加交互功能:添加一个按钮。默认状态只显示一张静态图片,当按下按钮时,才开始播放音乐和动画。这引入了中断或状态机的概念。
  4. 使用更丰富的音频:无源蜂鸣器只能播放单音旋律。如果想播放更复杂的和弦或真实音频片段,可以考虑接入DFPlayer Mini等MP3模块,或者使用具备DAC功能的开发板(如ESP32)。
  5. 设计外壳:用3D打印或激光切割为你的作品制作一个精致的外壳,让它从一个实验原型变成一个可以摆在桌面的趣味摆件。

这个项目的魅力在于,它用一个具体的、有趣的目标,牵引你走完了嵌入式开发中硬件连接、驱动库使用、数据处理、时序控制等多个关键环节。当你听到蜂鸣器奏出熟悉的旋律,看到OLED上闪过游戏的经典元素时,那种亲手创造快乐的成就感,正是驱动我们不断探索下去的动力。希望你在复现和改造这个项目的过程中,不仅能收获一个酷炫的作品,更能深入理解这些代码和电路背后的逻辑。

http://www.gsyq.cn/news/1462375.html

相关文章:

  • 2026重庆高性价比导游TOP10|家庭游路线与预算解析 - 随峰国旅
  • 2026 南京婚恋服务机构实测排行:基于核心需求的中立对比分析 - 互联网科技品牌测评
  • 电子失效分析工程师金字塔技能简介
  • 2026蓝铜胜肽冻干粉品牌推荐-听肌专注于科学护肤 - GrowthUME
  • MATLAB操控STK卫星的隐藏关卡:深入理解‘控制句柄’与场景对象树
  • 2026指南:苏州废旧物资回收公司,专业废铁/废铝/电路板/化工厂设备/旧设备/光伏发电设备回收品牌机构 - 品牌企业推荐师(官方)
  • 上海牛肉汉堡品牌加盟哪家靠谱?盈利模型清晰可见 - 17329971652
  • Spring Cloud Nacos 服务注册 IP 选择机制与配置详解
  • 从拖拽到声明式:重新定义图表创作的思维范式
  • 浙江杨梅采摘园技术指南:长兴基地全维度实测解读 - 奔跑123
  • 黑马点评-Redisson-01_why_redisson
  • CodeGraph 代码图谱实战:AI Agent 为什么不该再从 grep 开始?
  • 如何快速掌握LevelUI:LevelDB可视化管理的完整使用指南
  • 腾讯云代理行业深度拆解:避坑指南与合作选择
  • 3步掌握德州扑克最优策略:用TexasSolver免费从入门到精通的完整指南
  • Passage: The Apotheosis of the Twin Pincer
  • 小米MiMo邀请码最新(2026.06)
  • 浙江杨梅采摘体验指南:渚山杨梅园的硬核优势解析 - 奔跑123
  • GitHub中文翻译插件:3分钟实现GitHub界面全面本地化
  • 2026年 余杭区写字楼/未来科技城在租写字楼推荐榜单:优质办公空间与产业集聚价值深度解析 - 品牌企业推荐师(官方)
  • 售后完善:透明收费饮水机服务商在哪租 - 13425704091
  • DIY智能RGB壁灯:从电路连接到旧化涂装的完整制作指南
  • APK-Installer:Windows平台最便捷的安卓应用安装解决方案
  • STM32F103C6T6 UART转CAN通信工程:支持2Mbps高速透传,含CubeIDE工程与测试工具
  • 房价预测实战:用Sklearn的LinearRegression跑多元线性回归,结果不准?可能是最小二乘法的‘锅’
  • 2026年银川劳动纠纷律师避坑指南:5家靠谱专业推荐 - 本地品牌推荐
  • 10个必学的Linux命令及用法
  • DIY便携式电源:从18650电池组到300W逆变器的完整构建指南
  • 如何通过技术情报分析提升产业招商的针对性和成功率?
  • 基于树莓派与Arduino的激光钢琴:嵌入式系统与物联网实践