把“用 UNO 板和蜂鸣器演奏《Aoharu》”当成一个入门小项目来做时,很多人第一反应是去要一份现成代码。但真正接过线、烧过程序之后你会发现,问题几乎都不在 Arduino 本身,而在两个更基础的地方:你手上的蜂鸣器到底是不是无源蜂鸣器,以及你有没有把乐谱转换成“频率 + 时值”。一旦这两件事没搞清楚,复制多少代码都救不了你。
这篇文章不会给你一份完整的《Aoharu》代码包,因为不同来源的曲谱、不同蜂鸣器型号、不同 UNO 引脚,都会让“复制粘贴”失效。更值得做的是把整条从乐谱到发声的链路拆开讲清楚。理解了这套方法,你不仅能演奏《Aoharu》,还能把任何一首看得懂简谱的歌变成蜂鸣器音乐。
1. 先别急着接蜂鸣器:分清“发声”和“演奏”是两件事
很多人会把蜂鸣器当成一个小喇叭,认为“接上电就能播放歌曲”。这句话只说对了一半。蜂鸣器和喇叭不一样,有源蜂鸣器和无源蜂鸣器更是两回事。有源蜂鸣器内部自带振荡源,通电后直接以固定频率发声,适合做报警提示,但无法演奏旋律。无源蜂鸣器内部没有振荡源,你给它什么样频率的电信号,它就发出什么样音高的声音,这才是演奏的基础。
不少新手在选元件时只看“蜂鸣器”三个字,买回来才发现要么一直响、要么不响。问题不在 Arduino,而在选型。
1.1 有源蜂鸣器和无源蜂鸣器,为什么网上教程都让你选无源
有源和无源的区别,可以从一张表快速看懂:
| 对比项 | 有源蜂鸣器 | 无源蜂鸣器 |
|---|---|---|
| 内部振荡器 | 有 | 无 |
| 通电表现 | 直接发出固定频率的声音 | 可能不响,或只“咔哒”一声 |
| 声音频率 | 固定 | 由输入信号频率决定 |
| 能否演奏旋律 | 不能,通常只能当提示音 | 可以,能输出不同音高 |
| 驱动方式 | 高电平或低电平即可 | 需要 PWM 或 tone() 产生方波 |
| 典型应用 | 报警器、按键提示 | 音乐播放、发声交互、蜂鸣器音乐盒 |
Arduino 的 tone() 函数可以输出指定频率的方波,无源蜂鸣器就会按照这个频率振动。换句话说,代码里写tone(9, 440),蜂鸣器就会发出 440Hz 的声音,也就是标准音 La。如果换成 523Hz,就是高音 Do。
有源蜂鸣器则不是这样。它内部有一个固定的振荡源,不管你给它 100Hz 还是 1000Hz 的信号,它只会按自己的内部频率发声。所以网上所有“用蜂鸣器演奏歌曲”的教程,默认都用无源蜂鸣器。如果你手里的器件丝印上写着 HNB、HNC 之类带“有源”标识的型号,很可能就不适合做旋律播放。
判断方法也很简单:给蜂鸣器的两个引脚接上 5V 直流电,如果一通电就一直响,那基本是有源蜂鸣器;如果只有上电瞬间的“咔哒”声,或者完全没声,大概率是无源蜂鸣器。当然,特殊器件除外,但这个方法对绝大多数常见型号都适用。
1.2 三脚压电陶瓷蜂鸣器要不要碰?
如果你手里是一个三脚压电陶瓷蜂鸣器,建议先别急着按两脚无源蜂鸣器的接法去接。三脚封装并不等于“适合 Arduino 演奏”。
常见三脚蜂鸣器大概有两种情况:一种是内部已经包含驱动电路的自激式蜂鸣器,电源一通就响,频率固定,不适合用 tone() 播放旋律;另一种是压电陶瓷片引出三个脚,其中可能包含反馈极,需要按数据手册配外围电路才能正常工作。不管是哪一种,都不能默认它能直接接在 UNO 引脚上输出音乐。
所以我的建议是:如果想快速跑通“演奏歌曲”这个项目,优先买那种标着“无源蜂鸣器”“Arduino 专用”的两脚元件。如果手上只有三脚器件,先找型号手册确认引脚功能,再确认它是否支持外部频率输入。很多看起来像蜂鸣器的东西,其实不适合做音乐播放。
1.3 直连 UNO 还是加 ULN2003 驱动?
很多教程里,直接把无源蜂鸣器正极接在 UNO 数字引脚上,负极接 GND,就能用 tone() 驱动。小功率蜂鸣器工作电流通常在 20mA 左右,Arduino 引脚可以短时间输出这个量级的电流,所以入门场景直连完全够用。
但如果你要长时间播放、使用功率比较大的蜂鸣器,或者想让声音更稳更响,建议加一个驱动芯片。ULN2003 是一个常用的达林顿晶体管阵列,很多人用它驱动继电器、步进电机,实际上也能用来驱动蜂鸣器。
用 ULN2003 的思路是这样的:UNO 的数字引脚输出控制信号到 ULN2003 的输入脚,输出脚接蜂鸣器负极,蜂鸣器正极接 5V。因为 ULN2003 的输出是开漏结构,当输入信号为高电平时,输出管导通,蜂鸣器就有了电流通路。tone() 输出的是方波,经过 ULN2003 反相后仍然是方波,所以不会影响音高,只是驱动能力更强了。
如果你用的是蜂鸣器模块,而不是裸蜂鸣器,通常模块上已经带了三极管驱动,直接接 VCC、GND、I/O 三个引脚就行。但同样的道理,先确认模块上是无源蜂鸣器,再看引脚定义,不要想当然。
2. 把《Aoharu》变成 Arduino 能读的旋律:音调、频率和时值
Arduino 看不懂简谱上的 1 2 3,它只认频率和时间。tone(pin, frequency)会让引脚输出指定频率的方波,duration参数控制这段方波持续多久。所以演奏一首歌,本质就是把乐谱翻译成一个数组,数组里每个元素都包含“频率”和“时值”两个信息。
这是整个项目的核心。接线只会花你几分钟,真正花时间的地方是把乐谱逐音翻译成频率和时值。
2.1 从简谱到频率:先建一张中音 DO 到高音 DO 的表
先用一张表记住常见音名和频率的关系。这张表假设简谱是 C 调,也就是“1”对应中央 C:
| 简谱数字 | 音名 | 频率(Hz) |
|---|---|---|
| 低音 1 | C3 | 131 |
| 低音 2 | D3 | 147 |
| 低音 3 | E3 | 165 |
| 低音 4 | F3 | 175 |
| 低音 5 | G3 | 196 |
| 低音 6 | A3 | 220 |
| 低音 7 | B3 | 247 |
| 中音 1 | C4 | 262 |
| 中音 2 | D4 | 294 |
| 中音 3 | E4 | 330 |
| 中音 4 | F4 | 349 |
| 中音 5 | G4 | 392 |
| 中音 6 | A4 | 440 |
| 中音 7 | B4 | 494 |
| 高音 1 | C5 | 523 |
| 高音 2 | D5 | 587 |
| 高音 3 | E5 | 659 |
| 高音 4 | F5 | 698 |
| 高音 5 | G5 | 784 |
| 高音 6 | A5 | 880 |
| 高音 7 | B5 | 988 |
这张表不是绝对的。如果乐谱上标了“1=D”,意味着整首曲子的“1”不是 C4 的 262Hz,而应该对应 D4 的 294Hz。调号不同,整张频率表都要平移。网上有很多简谱转频率工具,也可以自己在 Excel 里做一张表,把低音、中音、高音分别映射到不同频率段。
对于《Aoharu》这类曲子,不同简谱版本可能标着不同的调号。我的建议是:先找到一份带调号和速度标注的简谱,再基于这张表把数字换成频率。不要凭感觉猜音高,否则后半段会越听越不对。
2.2 tone() 到底做了什么:频率、持续时间、节拍间隔
tone(pin, freq)会立刻让指定引脚输出一个频率的方波,然后函数返回。它不会阻塞程序继续往下跑。如果你希望这个音持续一段时间,随后停止,常见写法是:
tone(BUZZER_PIN, frequency); delay(noteDuration); noTone(BUZZER_PIN);tone(pin, freq, duration)是另一种写法,它会在持续 duration 毫秒后自动停止方波,但仍然是非阻塞的,调用后会立刻返回。所以如果你在这个调用后面再写一个delay(duration * 1.3),相当于前 duration 毫秒在响,后面的 0.3 倍 duration 毫秒是休息,然后再进入下一个音。
这个“音符间隔”很重要。两个相同音高的音如果连续播放,中间没有很短停顿,听起来会连成一个长音,失去颗粒感。演奏快节奏的曲子时,通常会在每个音符后面加一点空隙,让旋律有“弹跳感”。
还有一种更稳定的做法是只使用tone(pin, freq)+delay(noteDuration)+noTone(pin),然后在音符结束后单独加一个delay(gapDuration)。这样逻辑更直观,也方便单独调整音符间隔。
2.3 用结构体数组存储旋律:《Aoharu》的通用填法
为了不让代码变成一个超长的数组,可以用结构体把“频率”和“时值”打包成一个完整音符。下面是一个通用示例:
const int BUZZER_PIN = 9; struct Note { int freq; // 频率,0 表示休止符 int duration; // 持续时间,单位毫秒 }; Note song[] = { {262, 400}, // 中音 1 {294, 400}, // 中音 2 {330, 200}, // 中音 3,八分音符 {392, 800}, // 中音 5,二分音符 {0, 100}, // 休止符 // 继续填入《Aoharu》简谱中的其它音符 }; const int songLength = sizeof(song) / sizeof(song[0]); void playNote(int pin, int freq, int duration) { if (freq <= 0) { delay(duration); return; } tone(pin, freq); delay(duration); noTone(pin); delay(duration * 0.3); // 音符之间的空隙 } void setup() { pinMode(BUZZER_PIN, OUTPUT); } void loop() { for (int i = 0; i < songLength; i++) { playNote(BUZZER_PIN, song[i].freq, song[i].duration); } delay(3000); // 整首播完后休息 3 秒 }请注意,这不是《Aoharu》完整旋律,只是一个结构示例。你要做的事很简单:拿到一份《Aoharu》简谱,把每个数字对应的频率填进去,再把每个音的时长算出来填进去。
duration 的计算和歌曲速度有关。比如 120 BPM 的速度下,一个四分音符是 60000 / 120 = 500ms,一个八分音符是 250ms,一个二分音符是 1000ms。如果你的曲子速度是 100 BPM,那四分音符就是 600ms。别把“简谱上的几分音符”直接当成毫秒数,那一定会导致节奏不对。
2.4 休止符、连音和重复段怎么处理
休止符最简单:把 freq 设为 0,播放函数里只 delay,不调用 tone。上面的代码已经处理了这种情况。
连音或者延音线可以把两个相同音高的音符时长加起来。比如前一个音是四分音符 500ms,后一个音是八分音符 250ms,并且它们之间是连音线,那就应该写成一个时长 750ms 的音符,而不是两个连续的音。
重复段有两种处理方式:一种是直接把相同旋律复制到数组里,简单但会让代码很长;另一种是把旋律拆成小段,用函数去拼接播放,比如playPhrase(verseA, verseALen)然后playPhrase(chorusA, chorusALen)。对于《Aoharu》这种有前奏、主歌、副歌重复结构的曲子,第二种方式明显更好维护。
3. 最小可运行流程:从硬件接线到整曲播放
现在开始动手。你需要准备的东西不多:Arduino UNO 板、无源蜂鸣器、面包板、跳线若干,如果打算用驱动芯片就再准备一个 ULN2003。电脑上装好 Arduino IDE。
不建议一上来就把整首《Aoharu》填进去。先跑通一个音,再跑一小句,最后再整首。这样排查成本会低很多。
3.1 接线方式一:普通无源蜂鸣器直连 UNO
最简单的接法:无源蜂鸣器正极接 UNO 数字引脚 9,负极接 GND。如果你用的是蜂鸣器模块,通常是 VCC 接 5V、GND 接 GND、I/O 接数字引脚 9。模块上的 I/O 引脚是信号输入,不要接到电源上。
有些无源蜂鸣器是电磁式结构,引脚有正负极之分,板上会有“+”和“-”丝印,或者长脚是正极。压电式无源蜂鸣器一般不分正负,但建议还是按丝印接。如果接反了一直不响,可以把两个引脚互换试试。
如果你担心 UNO 引脚过流,可以串一个 100Ω 的电阻。但对常用小蜂鸣器来说,直连通常也能正常工作。串电阻会降低音量,看你的需求决定。
3.2 接线方式二:用 ULN2003 驱动蜂鸣器
如果使用 ULN2003 驱动,常见接法如下:
| UNO 引脚 | ULN2003 引脚 | 蜂鸣器 |
|---|---|---|
| D9 | IN1(引脚 1) | |
| 5V | 正极 | |
| GND | E(引脚 8) | |
| OUT1(引脚 16) | 负极 |
信号从 D9 进入 ULN2003 的 IN1,当 D9 输出高电平时,OUT1 内部的达林顿管导通,蜂鸣器的负极被接到地,电流从 5V 经过蜂鸣器到 OUT1,蜂鸣器发声。由于 tone() 输出的是方波,经过 ULN2003 反相后依然是方波,频率和音高关系不变。
如果你使用的是电磁式蜂鸣器,也就是内部有线圈的蜂鸣器,建议把 ULN2003 的 COM 引脚(引脚 9)接到 5V。这样可以利用 ULN2003 内部集成的续流二极管,在关断瞬间给线圈一个泄放路径,保护驱动芯片。如果是压电式蜂鸣器,COM 可以悬空不接。
需要注意的是,这里描述的是裸蜂鸣器加驱动芯片的接法。如果你用的是蜂鸣器模块,则不需要 ULN2003,因为模块上通常已经自带驱动三极管。两种方式不要混用,否则可能重复驱动造成逻辑混乱。
3.3 代码骨架:一段可以替换旋律的播放程序
把上一章的代码骨架再完善一下,加入一个固定测试模式。先让它播放一个标准音,确认接线和蜂鸣器都没问题:
const int BUZZER_PIN = 9; void setup() { pinMode(BUZZER_PIN, OUTPUT); } void loop() { tone(BUZZER_PIN, 440); // 标准音 A4 delay(1000); noTone(BUZZER_PIN); delay(1000); }如果这个程序运行后能听到 440Hz 的“La”,说明硬件链路基本正常。接下来把测试代码替换成上面那个带Note结构体的播放程序,先用几个音阶测试,比如中音 1 到 7,一个音一个音播放。确认音阶顺序正确后,再把你手里的《Aoharu》简谱填进去。
有一个容易被忽略的细节:tone()与一些库会有冲突。如果你后续接了 OLED、舵机或者使用 Timer 的库,可能会发生 tone() 不工作或者声音频率错乱的情况。最好在最小系统上先验证,再逐步加外部设备。
3.4 验证顺序:单音、短句、整曲
我建议按下面的顺序验证,而不是一次性做完:
- 播放单音:
tone(9, 440),看蜂鸣器是否响。 - 播放音阶:中音 1 到 7,听是不是从低到高,确认频率表没问题。
- 播放《Aoharu》的第一句:只填开头几个音符,听节奏和音高是否接近。
- 逐步扩展:每加一句就烧录一次,直到整首完成。
理由很简单:如果第一步就不响,没必要继续填乐谱。如果前两句听着不对,大概率是频率表或时值问题,这时候改几行代码比改一整首数组更轻松。
4. 把点歌做成一个小系统:串口、按键与播放队列
从“能播一首歌”到“能点歌”,这个问题很多人在评论区问过。其实把底层逻辑想清楚后,它并不是一个很难的事。核心只有一点:把“旋律数据”和“播放逻辑”分开。
很多人喜欢把整首歌写在loop()里,然后让单片机永远循环播放。这样换一首歌就得重新烧录,根本谈不上点歌。要做点歌,你需要建立歌单,然后用输入方式选择播放哪一首。
4.1 为什么要用播放函数而不是直接写在 loop 里
如果所有旋律都写在setup()或者loop()里,代码会变得很长,而且很难复用。用结构体数组把旋律数据抽象出来之后,换曲子时只需要换数组,播放逻辑完全不用动。
可以再进一步定义一个播放函数:
void playSong(const Note* song, int length) { for (int i = 0; i < length; i++) { playNote(BUZZER_PIN, song[i].freq, song[i].duration); } }这样无论你有几首歌,播放函数都只有一份。这个抽象是整个点歌系统的地基。按键选择、串口命令、OLED 菜单,本质上都只是在决定“调用哪一个数组”。
4.2 用串口接收歌名来切歌
最简单的点歌方式是通过串口发送命令。比如在串口监视器里输入aoharu然后回车,就播放《Aoharu》;输入star就播放《小星星》。代码可以这样写:
String command; void loop() { if (Serial.available() > 0) { command = Serial.readStringUntil('\n'); command.trim(); // 去掉首尾空格和回车 if (command == "aoharu") { playSong(aoharuMelody, aoharuLength); } else if (command == "star") { playSong(starMelody, starLength); } else if (command == "stop") { noTone(BUZZER_PIN); } } }这里涉及到一个小坑:不同串口助手的发送设置。如果发送时带了换行符,readStringUntil('\n')一般能截断;如果带了回车,可能会出现字符串末尾多一个\r。加上trim()可以消除大部分问题。如果发现command == "aoharu"始终不成立,可以把命令内容用Serial.println(command)打印出来,看看实际收到的字符是什么。
不过这种写法是阻塞播放。播放期间程序在delay()里等待,串口命令要等到当前整首播完才能被处理。对于演示项目够用,但不是真正意义上的“切歌”。
4.3 按键点歌和 OLED 显示歌名的最小思路
如果你想做成一个更像样的点歌机,可以加按键和 OLED。
按键接线使用内部上拉模式:按键一端接数字引脚,另一端接 GND。程序里检测按下边沿,更新当前歌曲索引,然后播放对应歌名。
OLED 可以通过 I2C 接口显示当前歌名和播放状态。这里需要注意的是,OLED 库和 tone() 可能占用同一个定时器资源,导致冲突。常见的处理方式是换一个不冲突的引脚,或者用非阻塞播放模式,把音符切换放在millis()定时逻辑里,而不是用在delay()里阻塞。
阻塞式播放写起来很简单,但点歌体验很差,因为你无法在一个音符还没播完时立刻切歌。真正要做得顺手,需要把播放循环改成状态机:每次loop()检查当前时间是否到了下一个音符的开始时间,是则播放;否则继续执行按键扫描和 OLED 刷新。这种改法会复杂不少,但换来的是整个系统可以同时响应外部输入。
4.4 上限和边界:蜂鸣器点歌适合什么,不适合什么
用蜂鸣器做点歌,适合的是:学习音调频率、理解单片机定时输出、做一个简单的音乐盒,或者作为更复杂交互项目的声音反馈模块。
它不适合用来做高音质音乐播放。Arduino 的tone()同一时刻只能输出一个频率,蜂鸣器本身也只有一个发音单元,所以只能演奏单旋律,不能同时播放伴奏和和声。如果你想播放 mp3 或者带和弦的完整歌曲,请用 DFPlayer Mini 播放音频模块,或者使用更强大的音频扩展板。蜂鸣器点歌更像一个“音乐盒”,而不是“音箱”。
5. 为什么蜂鸣器不响、乱响、声音难听:一条排查链路
很多问题不是玄学。按下面的顺序排查,大多数情况下几分钟就能找到原因。
5.1 完全不响:电源、引脚、代码顺序排查
先确认代码已经成功下载到 UNO,并且开发板电源灯亮。然后用最简代码测试:
tone(9, 440); delay(1000); noTone(9);这一步能把问题范围缩小。接着检查:
- 蜂鸣器正负极是否接反,尤其是电磁式无源蜂鸣器。
- 引脚号是否和代码里的数字一致。
- 蜂鸣器是不是有源蜂鸣器,如果是,需要换无源。
- 如果接了 ULN2003,检查输入引脚是否正确、GND 是否共地、输出脚是否接在蜂鸣器负极。
如果换了引脚还是没声音,建议用万用表测引脚输出,或换一个蜂鸣器。很多时候问题不是代码,而是手上这个蜂鸣器已经损坏,或者型号选错。