用Arduino蜂鸣器演奏《Aoharu》:选型、频率与点歌系统
2026/9/3 20:15:48 网站建设 项目流程

把“用 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)
低音 1C3131
低音 2D3147
低音 3E3165
低音 4F3175
低音 5G3196
低音 6A3220
低音 7B3247
中音 1C4262
中音 2D4294
中音 3E4330
中音 4F4349
中音 5G4392
中音 6A4440
中音 7B4494
高音 1C5523
高音 2D5587
高音 3E5659
高音 4F5698
高音 5G5784
高音 6A5880
高音 7B5988

这张表不是绝对的。如果乐谱上标了“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 引脚蜂鸣器
D9IN1(引脚 1)
5V正极
GNDE(引脚 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 验证顺序:单音、短句、整曲

我建议按下面的顺序验证,而不是一次性做完:

  1. 播放单音:tone(9, 440),看蜂鸣器是否响。
  2. 播放音阶:中音 1 到 7,听是不是从低到高,确认频率表没问题。
  3. 播放《Aoharu》的第一句:只填开头几个音符,听节奏和音高是否接近。
  4. 逐步扩展:每加一句就烧录一次,直到整首完成。

理由很简单:如果第一步就不响,没必要继续填乐谱。如果前两句听着不对,大概率是频率表或时值问题,这时候改几行代码比改一整首数组更轻松。

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 是否共地、输出脚是否接在蜂鸣器负极。

如果换了引脚还是没声音,建议用万用表测引脚输出,或换一个蜂鸣器。很多时候问题不是代码,而是手上这个蜂鸣器已经损坏,或者型号选错。

5.2 一直

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询