许多第一次接触 Arduino 的玩家,做完流水灯之后,第二件事往往就是“让它唱歌”。但很快就会发现两个极端:有人拿有源蜂鸣器接上电源,它只会发出一种刺耳的“嘀——”,怎么折腾都换不成旋律;有人用无源蜂鸣器写代码,结果要么没有声音,要么像老旧游戏机一样乱响。这篇文章要解决的,就是“用 UNO 板和蜂鸣器演奏《Aoharu》”这类音乐项目背后的核心问题:怎么把一段旋律变成 Arduino 能播放的声音,并且让它听起来尽量接近原曲。
先说结论:Arduino UNO 配合无源蜂鸣器,用内置的tone()函数,是实现蜂鸣器音乐最简单、成本最低、也最适合新手理解“声音与频率关系”的方案。真正容易踩坑的不是代码,而是硬件选型——你只要用了有源蜂鸣器,这个项目就基本失败了。本文会从硬件选型、频率原理、乐谱转换讲到完整代码,最后还会给出一个“评论区点歌”的扩展思路,让你做的东西真正可以玩起来。
1. 这篇文章真正要解决的问题
很多教程都在教你“抄代码”,但很少有人告诉你为什么蜂鸣器能发出不同音调,也没人解释为什么同样的代码在别人的板子上响,在你的板子上却沉默。做蜂鸣器音乐项目,你首先要想清楚三个问题:
第一,你要用的是哪种蜂鸣器。有源蜂鸣器内部自带振荡源,通电就会响,频率固定,只能当“三极管指示灯”用;无源蜂鸣器没有振荡源,需要外部提供一定频率的方波才能发声,正因如此,它可以通过改变方波频率来播放不同音高的音符。如果你的目标是演奏《Aoharu》这种有旋律的曲子,必须选择无源蜂鸣器。
第二,你的代码是否真的生成了对应频率的方波。Arduino 的tone(pin, frequency, duration)函数会在指定引脚上输出一个指定频率的方波,持续指定毫秒。这个函数是阻塞式的,但它内部已经帮你处理了定时器中断,对新手的理解负担很小。
第三,你的驱动电路是否足够。小功率蜂鸣器可以直接由 UNO 引脚驱动,但某些无源蜂鸣器需要的电流略大,直接接引脚也能响,只是声音偏小或有杂音。如果发现音量异常,可以考虑用三极管或 ULN2003 驱动,不过这在入门阶段并不是必须的。
解决完这三个问题,后面的乐谱转换和代码实现就顺理成章了。这篇文章最值得收藏的地方,是第四节的“简谱转频率”方法,它会给你一张通用频率表,以后无论想演奏什么歌,都可以照套路做出来。
2. 蜂鸣器音乐的核心原理:tone() 与频率表
2.1 声音的本质是振动
人能听到声音,是因为物体振动推动空气,形成疏密波传入耳朵。音高由振动频率决定,频率越高,音调越高。比如中央 C(do)的频率大约是 261.63Hz,意思是每秒振动 261.63 次。我们听流行歌曲,本质上就是在听一段连续的、不同频率和时长的声音序列。
蜂鸣器内部有一个压电陶瓷片或电磁线圈,当你在它两个引脚之间加上一个快速通断的电压(方波)时,它就会随之振动。方波频率越高,蜂鸣器振动越快,听到的音调就越高。Arduino 的tone()函数干的就是这件事:它按照你指定的频率,在引脚上输出对应的方波信号。
2.2 tone() 函数怎么用
tone()函数原型如下:
tone(pin, frequency, duration);pin:接蜂鸣器的引脚,比如 8。frequency:频率值,单位 Hz,范围通常是 31Hz 到 65535Hz。duration:持续时间,单位毫秒。如果不填,会持续输出,直到调用noTone(pin)。
注意,tone()在同一时刻只能在一个引脚上输出频率,如果你有两个蜂鸣器想同时响不同音符,需要额外处理。此外,tone()会占用定时器,会影响analogWrite()功能的某些引脚,但只驱动一个蜂鸣器时不必担心。
2.3 C 调音阶频率表
要把歌曲写进 Arduino,最常用的方式是先把简谱数字与频率对应起来。这里给出 C 调四到六组音阶的频率表,可直接复制使用:
| 音符 | 频率(Hz) | 音符 | 频率(Hz) |
|---|---|---|---|
| C4 | 262 | C5 | 523 |
| D4 | 294 | D5 | 587 |
| E4 | 330 | E5 | 659 |
| F4 | 349 | F5 | 698 |
| G4 | 392 | G5 | 784 |
| A4 | 440 | A5 | 880 |
| B4 | 494 | B5 | 988 |
如果需要升半音,比如 C#4,频率乘以 2 的 1/12 次方,也就是约 1.0595。Arduino 的浮点运算够用,但为了省空间,可以直接用整数近似值。
2.4 有源蜂鸣器和无源蜂鸣器的区别
这可能是整个项目里最关键的选型问题。有源蜂鸣器内部已经集成了一个振荡器,只要给直流电压就能响,你没法通过程序控制音调,只能控制响不响。无源蜂鸣器内部没有振荡器,必须外部输入方波,所以才能输出不同频率的声音。
从外观上看,有源蜂鸣器往往带有胶封或贴纸,引脚长度可能不一样;无源蜂鸣器通常是开放的,能看到内部线圈或压电片。如果实在分不清,可以用一节 3V 电池或 Arduino 的 5V 引脚短接测试:有源会响,无源不会响。这个测试请务必在老师或老手指导下进行,避免短路。
本文所有示例都基于无源蜂鸣器。购买时直接搜“无源蜂鸣器 Arduino 模块”即可,一般会注明“无源”,价格很便宜。
3. 环境准备与硬件接线
3.1 器材清单
做这个项目需要的东西非常少:
- Arduino UNO 开发板一块
- 无源蜂鸣器一个
- 面包板一个
- 杜邦线若干
如果需要更稳定的驱动,还可以准备一个 NPN 三极管(如 2N2222)和 1kΩ 电阻,或者 ULN2003 驱动模块。但对入门阶段来说,直接接引脚已经足够听出旋律。
3.2 接线方式
Arduino UNO 有 14 个数字 IO 引脚,我们选择 8 号引脚作为信号输出。轻触式蜂鸣器模块通常有三个引脚:VCC、GND、I/O,接线如下:
- VCC 接 5V
- GND 接 GND
- I/O 接 8
如果是两脚无源蜂鸣器(长脚为正极),接线方式:
- 长脚接数字引脚 8
- 短脚接 GND
注意,两脚蜂鸣器没有反接保护,如果反接,虽然不会立刻烧毁,但可能不响或声音很小,要仔细看引脚标识。部分三脚压电陶瓷蜂鸣器有三个脚,其中两个是电源,一个是反馈/驱动脚,具体需要参考你手中元件的 datasheet。一般 Arduino 初学者购买的是三线模块,使用更简单。
接线图如下:
Arduino UNO 无源蜂鸣器模块 5V ----------> VCC GND ----------> GND D8 ----------> I/O连线完成后,先不要急着写歌曲,可以先用一个简单的测试程序确认蜂鸣器能发声。
3.3 软件环境
开发环境使用 Arduino IDE,建议安装 2.x 版本。打开 IDE 后,在“工具 -> 开发板”里选择“Arduino Uno”,并选择正确的串口端口。之后所有代码都写在setup()和loop()两个函数中。
4. 核心流程拆解:从简谱到代码的转换方法
乐谱通常用五线谱或简谱记录,简谱用阿拉伯数字代表音高,1 2 3 4 5 6 7 对应 do re mi fa sol la si。以 C 调为例,1=C4,2=D4,3=E4,4=F4,5=G4,6=A4,7=B4。高音通常在数字上方加一个点,表示高一个八度;低音在下方加点。节奏则用横线、附点等表示。
在 Arduino 项目里,我们可以把一首歌抽象成两个数组:一个存储音符频率,一个存储每个音符的时长。
举个例子,经典儿歌“小星星”的前两句简谱是:
1 1 5 5 6 6 5 - 4 4 3 3 2 2 1 -对应频率和时长的数组如下:
int melody[] = { 262, 262, 392, 392, 440, 440, 392, 0, 349, 349, 330, 330, 294, 294, 262, 0 }; int noteDurations[] = { 4, 4, 4, 4, 4, 4, 2, 2, 4, 4, 4, 4, 4, 4, 2, 2 };这里的 4 表示四分音符,2 表示二分音符。播放时用 1000 除以节拍值即可得到毫秒数。比如四分音符的时长是 1000 / 4 = 250ms。
完整播放函数是这样:
void playMelody(int melody[], int noteDurations[], int length) { for (int thisNote = 0; thisNote < length; thisNote++) { int noteDuration = 1000 / noteDurations[thisNote]; tone(8, melody[thisNote], noteDuration); int pauseBetweenNotes = noteDuration * 1.30; delay(pauseBetweenNotes); noTone(8); } }这里乘以 1.3 是为了在音符之间留一点间隔,避免音与音完全粘连。实际项目里可以根据曲风调整这个系数。
5. 完整示例:用 UNO 和蜂鸣器演奏《Aoharu》
下面进入正题。以《Aoharu》的主题旋律片段为例,演示完整实现。
需要说明的是,这里使用的是歌曲中一段常见的旋律走向,并非完整原谱。因为不同编曲版本音高和节奏会有差异,核心目的是教你方法,而不是让你死记一段谱子。
5.1 乐谱片段与频率对照
假设《Aoharu》主歌某段简谱是:
5 6 1' 2' 3' 2' 1' 6 5 6 1' 2'其中1'表示高音 do,对应 C5,频率 523Hz。
为了让旋律更接近原曲,我把它拆分成两小节,避免误读。下面按 C 调排列:
| 简谱 | 音名 | 频率(Hz) |
|---|---|---|
| 5 | G4 | 392 |
| 6 | A4 | 440 |
| 1' | C5 | 523 |
| 2' | D5 | 587 |
| 3' | E5 | 659 |
| 2' | D5 | 587 |
| 1' | C5 | 523 |
| 6 | A4 | 440 |
| 5 | G4 | 392 |
| 6 | A4 | 440 |
| 1' | C5 | 523 |
| 2' | D5 | 587 |
5.2 定义音符宏
如果不想在 melody 数组里写一大串数字,可以在代码开头定义音符名称,方便阅读和修改。
#define NOTE_G4 392 #define NOTE_A4 440 #define NOTE_C5 523 #define NOTE_D5 587 #define NOTE_E5 659 #define NOTE_REST 0注意,这里用的是简化的整数频率,没有精确到小数点。实际听感上,30Hz 以内的偏差不会造成明显跑调,但如果你耳朵很敏感,可以使用官方 Arduino 音调库中的浮点定义,比如NOTE_C5是 523.25Hz。为了练习,整数足够。
5.3 完整代码
创建一个新 Arduino 工程,输入以下代码:
// 文件名:Aoharu_Buzzer.ino // 功能:用 UNO 和无源蜂鸣器播放《Aoharu》旋律片段 // 接线:蜂鸣器 I/O 接 D8,VCC 接 5V,GND 接 GND #define NOTE_G4 392 #define NOTE_A4 440 #define NOTE_C5 523 #define NOTE_D5 587 #define NOTE_E5 659 #define NOTE_REST 0 const int buzzerPin = 8; // 旋律数组:音符频率 int melody[] = { NOTE_G4, NOTE_A4, NOTE_C5, NOTE_D5, NOTE_E5, NOTE_D5, NOTE_C5, NOTE_A4, NOTE_G4, NOTE_A4, NOTE_C5, NOTE_D5 }; // 节拍数组:数字越小,时值越长 // 4 表示四分音符,8 表示八分音符,2 表示二分音符 int noteDurations[] = { 8, 8, 8, 8, 4, 8, 8, 8, 8, 8, 8, 4 }; void setup() { pinMode(buzzerPin, OUTPUT); Serial.begin(9600); Serial.println("Aoharu Buzzer Demo Start"); } void loop() { int length = sizeof(melody) / sizeof(melody[0]); for (int thisNote = 0; thisNote < length; thisNote++) { int noteDuration = 1000 / noteDurations[thisNote]; // 频率为 0 表示休止符,不发声,只做延时 if (melody[thisNote] != NOTE_REST) { tone(buzzerPin, melody[thisNote], noteDuration); } // 音符之间留出间隔,让旋律更清晰 int pauseBetweenNotes = noteDuration * 1.30; delay(pauseBetweenNotes); // 停止发声,避免连续音符叠加 noTone(buzzerPin); } // 播完一遍后停 2 秒,再重播 delay(2000); }5.4 代码逻辑讲解
这段代码的核心是两个数组:melody存音符频率,noteDurations存时值。loop()里用sizeof计算数组长度,因此你不需要手动改循环次数,以后增加音符时只要往两个数组里添加数据就行。
tone()函数会在noteDuration时长内产生方波,但如果你紧接着调用delay(),整个程序会卡住,蜂鸣器会持续发声直到noTone()被调用。这里用pauseBetweenNotes来控制节奏,实际演奏时等于每个音符的时长是noteDuration * 1.3,也就是说每个音符之间有一个约为音符时长 30% 的间隙。这个 1.3 系数可以调整,越小越连贯,越大越顿挫。
这里真正容易踩坑的地方是:如果tone()被调用后,没有调用noTone(),下一个tone()会覆盖前一个频率,但如果你用了delay(),程序会卡住,导致无法及时切换到下一个音符。因此,在循环里不要省略noTone()。
5.5 如何换成你自己想播放的旋律
很多人在学会这段代码后,最常问的问题是:“怎么把我的歌放进去?”其实非常简单:
- 把简谱的每个音对应成频率。
- 把每个音的相对时值换算成节拍数组。
- 按顺序填进两个数组里。
为了减少重复定义,你可以给每个常用音都取一个宏名称,形成自己的“音调库”,之后写任何歌都很快。
6. 运行结果与效果验证
6.1 编译与上传
在 Arduino IDE 中点击“验证”按钮,确认无编译错误。编译通过后,连接 Arduino UNO 到电脑,选择正确的端口,点击“上传”。
6.2 预期现象
正常情况下,上传完程序后,蜂鸣器会开始循环播放一段旋律,听起来是“sol la do re mi re do la, sol la do re”。虽然它和真正唱出来的声音有很大差距,但能清晰分辨出音高和节奏。
6.3 如何判断成功
如果出现以下现象,说明项目基本成功:
- 能听到明显的高低音变化,而不是单一频率的“嘀”。
- 音符之间有短暂停顿,不是一直响。
- 播放完一遍后,间隔 2 秒重新播放。
6.4 如果失败了从哪里排查
第一步,确认使用的是否是无源蜂鸣器。把蜂鸣器直接接 5V 和 GND,如果它响了,说明是有源蜂鸣器,tone() 无法改变它的音高。这是最容易踩的坑。
第二步,检查接线。确认蜂鸣器的 I/O 或正极引脚确实接到了 D8,而不是其他数字引脚。
第三步,打开串口监视器,看是否能收到“Aoharu Buzzer Demo Start”。如果串口没输出,说明板子和电脑通信有问题,或者代码没正确烧录。
如果都正常仍没有声音,尝试把蜂鸣器换到其他引脚,比如 D9,并修改代码里的buzzerPin。极少数情况下,某个引脚可能被其他功能占用或损坏。
7. 常见问题与排查思路
这里把蜂鸣器项目中遇到频率最高的问题整理成一个表格,便于收藏。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 蜂鸣器只会发出单一频率的声音 | 使用了有源蜂鸣器 | 直接接 5V 测试是否发声 | 更换为无源蜂鸣器 |
| 插上代码后完全没有声音 | 接线错误或蜂鸣器损坏 | 检查 VCC/GND/I/O,用万用表测试 | 重新接线或更换蜂鸣器 |
| 有声音但音调不对 | 频率表用错,或升半音没算 | 用串口打印实际播放频率 | 对照标准频率表修正 |
| 音符之间没有间隔,糊成一片 | 没有调用 noTone(),或 pauseBetweenNotes 太小 | 检查循环代码 | 在每次播放后加 noTone(),适当增大延时 |
| 声音特别小 | 蜂鸣器额定电压高于 5V,或驱动电流不足 | 测量引脚输出电压 | 改用三极管放大驱动,或换用有源蜂鸣器(但会失去音调) |
| 播放到某一段后卡住 | 数组长度不对,或 delay 太长 | 打印播放索引 | 用 sizeof 动态计算长度 |
| 上传程序时提示端口被占用 | 串口被串口监视器或其它软件占用 | 关闭串口监视器,重新插拔 USB | 重启 IDE,选择正确端口 |
其中“有源蜂鸣器”问题占比最高。很多套件默认附带的是有源蜂鸣器,因为控制简单,适合报警器,但不适合音乐。所以建议大家在购买时明确备注“无源蜂鸣器模块”。
8. 最佳实践与工程建议
8.1 不要阻塞在主循环里
上面的示例为了简单,使用了delay(),但这会让整块板子在播放音乐时什么都干不了。如果你想在播放音乐的同时响应按键、串口指令或者控制 LED,就不能用阻塞式延时。
推荐方案是使用millis()做非阻塞延时。以下是一个简化版思路:
unsigned long previousMillis = 0; void playNoteNonBlocking(int pin, int frequency, int duration) { tone(pin, frequency); previousMillis = millis(); while (millis() - previousMillis < duration) { // 这里可以处理其他任务 } noTone(pin); }实际项目中,更好的方式是把音调和节拍解析成时间轴事件,在loop()里不断检查时间,触发下一个音符。这样做出来的点歌机才能边播放边响应新指令。
8.2 建立自己的“歌曲库”
如果评论区点歌,你不可能每次都在代码里加一段旋律。更好的做法是预先存储几首歌的旋律数组,并通过串口输入命令来切换播放。
例如,定义两个歌曲数组:
int songAoharu[] = { ... }; int songOther[] = { ... };然后在串口中断里接收字符,如果收到'a'就播放 A 歌,收到'b'就播放 B 歌。这样别人让你“点歌”时,你只需在电脑上发送一个字符,蜂鸣器就会切换曲目,不算太复杂,但效果很酷。
8.3 音准校准
Arduino 的tone()基于晶振计时,出厂精度通常在数十 ppm 到几百 ppm,对蜂鸣器音乐来说足够准。但如果你觉得整体音高偏高或偏低,可以尝试调整频率值。不要盲目追求浮点数,因为蜂鸣器本身对细微频率变化不敏感,整数频率已经能分辨出一个“半音”的间隔。
8.4 保护引脚
虽然直接驱动无源蜂鸣器一般不会烧引脚,但长期使用或选用大功率蜂鸣器时,建议加上一个电阻限流或者用三极管驱动。ULN2003 本身是达林顿管阵列,常用于驱动继电器、步进电机等大电流设备,用来驱动蜂鸣器是绰绰有余的。如果你手上有 ULN2003 模块,可以把蜂鸣器接到它的输出端,而 Arduino 引脚只接输入控制端,这样会更安全。
接线参考:
Arduino D8 -> ULN2003 IN1 ULN2003 OUT1 -> 蜂鸣器正极 蜂鸣器负极 -> GND ULN2003 GND -> Arduino GND注意,ULN2003 是反向驱动,输出低电平有效,所以代码里tone()依然可以正常使用,不必改变逻辑。
8.5 代码工程的注释习惯
写蜂鸣器音乐代码时,一定要把乐谱来源和频率表注释在文件顶部。否则过一个月你自己都会忘记某个数组对应的是什么歌。推荐注释格式:
// 歌曲:《Aoharu》旋律片段 // 简谱:5 6 1' 2' 3' 2' 1' 6 | 5 6 1' 2' // 频率参考:C4=262, G4=392, A4=440, C5=523...这样一来,后续修改乐谱时,直接对照注释即可,不需要去数第几个数字。
9. 总结与后续学习方向
这篇博客的核心内容是:从硬件选型开始,明确“无源蜂鸣器才能播放旋律”,然后用tone()把简谱变成频率,再用数组存放音高和时值,最终让 UNO 播放出《Aoharu》的旋律片段。整个过程没有用到复杂的外设,也没有高深的理论,但它是你理解“数位信号如何产生模拟音调”的起点。
如果你已经成功跑通了这段代码,下一步可以做三件事:
一是扩展更多歌曲。把身边喜欢的歌的简谱按同样方法填进数组,建立自己的曲库。
二是加入交互功能。在面包板上加一个按键,每次按下切换下一首歌;或者用串口监视器输入指令,实现“评论点歌”的效果。
三是升级播放质量。换用带音频放大模块的喇叭,或者直接使用 SD 卡播放 WAV 音频模块,音质会好很多,但原理从“方波”变成了“PWM 采样”,是另一个值得深入的方向。
别让蜂鸣器音乐停在“能响”的阶段。从“能响”到“像歌”的距离并不远,只需要多花一点时间理解频率和时值,这个项目就会成为你 Arduino 入门的第一个真正“有灵魂”的作品。